Interactive Arc Diagram
Code
ChartUtil.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"use client";
import { ChartProps } from "@/types";
import React, { useRef, useCallback, useEffect } from "react";
import { useResizeObserver } from "@/hooks/useResizeObserver";
import * as d3 from "d3";
const ChartUtil = ({ data: dataRaw }: ChartProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const renderFunc = useCallback(() => {
const container = containerRef.current as HTMLElement;
if (!container) return;
const margin = { top: 50, right: 50, bottom: 75, left: 50 },
width = 1600 - margin.left - margin.right,
height = 1000 - margin.top - margin.bottom;
const svg = d3
.select(container)
.html("")
.append("svg")
.attr("viewBox", [0, 0, 1600, 1000])
.attr("width", "100%")
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const data = Object.create(dataRaw);
// List of node names
const allNodes = data.nodes.map((d: any) => d.name);
// List of groups
let allGroups: number[] = data.nodes.map((d: any) => d.grp);
allGroups = [...new Set(allGroups)];
// A color scale for groups:
const color: any = d3
.scaleOrdinal()
.domain(allGroups.map((d: any) => `${d}`))
.range(d3.schemeSet3);
// A linear scale for node size
const size = d3.scaleLinear().domain([1, 10]).range([0.5, 8]);
// A linear scale to position the nodes on the X axis
const x = d3.scalePoint().range([0, width]).domain(allNodes);
const idToNode: any = {};
data.nodes.forEach(function (n: any) {
idToNode[n.id] = n;
});
// Add the links
const links = svg
.selectAll("mylinks")
.data(data.links)
.join("path")
.attr("d", (d: any) => {
const start = x(idToNode[d.source].name) ?? 0; // X position of start node on the X axis
const end = x(idToNode[d.target].name) ?? 0; // X position of end node
return [
"M",
start,
height - margin.bottom, // the arc starts at the coordinate x=start, y=height-30 (where the starting node is)
"A", // This means we're gonna build an elliptical arc
(start - end) / 2,
",", // Next 2 lines are the coordinates of the inflexion point. Height of this point is proportional with start - end distance
(start - end) / 2,
0,
0,
",",
start < end ? 1 : 0,
end,
",",
height - margin.bottom,
] // We always want the arc on top. So if end is before start, putting 0 here turn the arc upside down.
.join(" ");
})
.style("fill", "none")
.attr("stroke", "grey")
.style("stroke-width", 1);
// Add the circle for the nodes
const nodes = svg
.selectAll("mynodes")
.data(data.nodes)
.join("circle")
.attr("cx", (d: any) => x(d.name) ?? 0)
.attr("cy", height - margin.bottom)
.attr("r", (d: any) => size(d.n))
.attr("fill", (d: any) => color(d.grp))
.attr("stroke", "white");
// And give them a label
const labels = svg
.selectAll("mylabels")
.data(data.nodes)
.join("text")
.attr("x", 0)
.attr("y", 0)
.text((d: any) => d.name)
.style("text-anchor", "end")
.attr(
"transform",
(d: any) =>
`translate(${x(d.name)},${height - margin.bottom + 10}) rotate(-90)`
)
.attr("fill", "darkgrey")
.style("font-size", 6);
// Add the highlighting functionality
nodes
.on("mouseover", function (event, d: any) {
// Highlight the nodes: every node is green except of him
nodes.style("opacity", 0.2);
d3.select(this).style("opacity", 1);
// Highlight the connections
links
.style("stroke", (a: any) =>
a.source === d.id || a.target === d.id ? color(d.grp) : "#b8b8b8"
)
.style("stroke-opacity", (a: any) =>
a.source === d.id || a.target === d.id ? 1 : 0.2
)
.style("stroke-width", (a: any) =>
a.source === d.id || a.target === d.id ? 4 : 1
);
labels
.style("font-size", (b: any) => (b.name === d.name ? 18.9 : 2))
.attr("y", (b: any) => (b.name === d.name ? 10 : 0));
})
.on("mouseout", (d) => {
nodes.style("opacity", 1);
links
.style("stroke", "grey")
.style("stroke-opacity", 0.8)
.style("stroke-width", "1");
labels.attr("y", 0).style("font-size", 6);
});
}, [dataRaw]);
useEffect(() => {
containerRef.current !== null && renderFunc();
}, [renderFunc]);
const resizeHandler = useCallback(() => {
containerRef.current !== null && renderFunc();
}, [renderFunc]);
useResizeObserver(containerRef, resizeHandler);
return <div ref={containerRef}></div>;
};
export default ChartUtil;