[시각화] D3를 이용하여 간단한 노드관계 만들어보기
프론트엔드를 함에 따라 기본적인 개발과정을 따를 수 있으면 한 번 씩은 바라보는 주제가 있습니다.
바로 시각화 입니다. 어떤 주제로 개발을 할 때, 개발에 사용했던 데이터를 이용하여 시각화를 하는 것은 꽤나 값진 경험이 될 것입니다.
그렇기에 오늘은 D3를 이용하여 간단한 노드를 연결하는 작업을 해 볼 것입니다.
우선, D3가 무엇인 지 알아보아야겠죠?
D3를 이전에 정리했던 글에서 살펴보실 수 있습니다.
간단하게 D3만 써보기 위해 CRA(Create React App)을 이용하여 JS 리액트 프로젝트만 생성한 후
노드를 연결해보는 작업을 하겠습니다
리액트 프로젝트를 생성하고 기본적으로 형성되어있는 보일러플레이트 코드를 제거하는 과정은 생략하겠습니다.
1단계 : 필요한 라이브러리 설치
React 초기 세팅을 모두 마쳤으면 D3를 설치합니다.
npm install d3
2단계 : Mock Data 준비
노드, 그리고 노드 사이의 링크를 나타내기 위해 Mock data를 이용하여 구성 해보겠습니다.
뭔가 그럴싸해 보이게 하기 위해 10개 정도씩 준비해 보았습니다.
const nodes = [
{ id: "node1", name: "Node 1" },
{ id: "node2", name: "Node 2" },
{ id: "node3", name: "Node 3" },
{ id: "node4", name: "Node 4" },
{ id: "node5", name: "Node 5" },
{ id: "node6", name: "Node 6" },
{ id: "node7", name: "Node 7" },
{ id: "node8", name: "Node 8" },
{ id: "node9", name: "Node 9" },
{ id: "node10", name: "Node 10" },
];
const links = [
{ source: "node1", target: "node2", name: "Link 1-2" },
{ source: "node1", target: "node3", name: "Link 1-3" },
{ source: "node1", target: "node4", name: "Link 1-4" },
{ source: "node2", target: "node3", name: "Link 2-3" },
{ source: "node2", target: "node4", name: "Link 2-4" },
{ source: "node2", target: "node5", name: "Link 2-5" },
{ source: "node3", target: "node4", name: "Link 3-4" },
{ source: "node3", target: "node5", name: "Link 3-5" },
{ source: "node3", target: "node6", name: "Link 3-6" },
{ source: "node4", target: "node5", name: "Link 4-5" },
{ source: "node4", target: "node6", name: "Link 4-6" },
{ source: "node4", target: "node7", name: "Link 4-7" },
{ source: "node5", target: "node6", name: "Link 5-6" },
];
3단계 : 그래프 구성요소 만들기
새로운 구성요소를 만들 것입니다. 구성요소가 마운트 될 때 useEffect 후크를 사용하여 그래프를 초기화합니다.
D3.js는 force-directed 그래프를 생성하는 데 사용할 수 있는 'forceSimulation'기능을 제공합니다.
여기에서는 useEffect Hooks에서 그래프를 생성하는 데 사용할 것입니다.
forceSimulation 기능은 force에 따라 노드의 위치를 자동으로 업데이트하는 시뮬레이션을 실행합니다.
이 시뮬레이션의 tick 이벤트를 수신하여 SVG에서 링크, 그리고 노드의 위치를 업데이트 할 수 있습니다.
여기서 노드의 x, y 속성과 링크의 x1, y1, x2, y2 속성을 노드의 현재 위치를 기준으로 설정합니다.
import React, { useRef, useEffect } from "react";
import * as d3 from "d3";
function Graph({ data }) {
//...
const svgRef = useRef();
const width = 1600;
const height = 1000;
useEffect(() => {
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d) => d.id)
.distance(100)
)
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
const svg = d3.select(svgRef.current);
const link = svg
.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", (d) => Math.sqrt(d.value));
const node = svg
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 5)
.attr("fill", "#69b3a2");
const labels = svg
.append("g")
.selectAll("text")
.data(nodes)
.join("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.text((d) => d.name);
function getConnectedNodes(node, depth = 0, nodeSet = new Set()) {
if (depth > 2) return nodeSet;
nodeSet.add(node.id);
links.forEach((link) => {
if (link.source === node.id && !nodeSet.has(link.target)) {
getConnectedNodes(
nodes.find((n) => n.id === link.target),
depth + 1,
nodeSet
);
}
if (link.target === node.id && !nodeSet.has(link.source)) {
getConnectedNodes(
nodes.find((n) => n.id === link.source),
depth + 1,
nodeSet
);
}
});
return nodeSet;
}
node.on("click", (event, d) => {
const connectedNodes = Array.from(getConnectedNodes(d));
node.attr("fill", (n) =>
connectedNodes.includes(n.id) ? "#69b3a2" : "#ddd"
);
link.style("stroke", (l) =>
connectedNodes.includes(l.source.id) &&
connectedNodes.includes(l.target.id)
? "#999"
: "#ddd"
);
});
simulation.on("tick", () => {
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
labels.attr("x", (d) => d.x).attr("y", (d) => d.y);
});
}, []);
return (
<svg ref={svgRef} width={width} height={height} />
<g className="nodes" />
<g className="links" />
</svg>
);
}
export default Graph;
SVG에서 링크와 노드를 그리면 아래와 같은 결과가 출력됩니다
이제, D3를 이용하여 여러분들의 상상의 나래를 펼쳐보아요!