forceSimulation이라는 메소드를 제공하며, 이 메소드는 각 노드의 물리력을 시뮬레이션해서 노드의 좌표를 계산함.d3.forcesimulation()을 이용해 물리력으로 자연스럽게 움직이는 네트워크 그래프를 그릴 수 있음.const simulation = d3
.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-200).distanceMax(200))
.force('center', d3.forceCenter(centerX, centerY))
.force(
'link',
d3.forceLink(links).id((d) => d.key),
)
.on('tick', () => {
drawNodes(); // node 그리기
drawLinks(); // link 그리기
});
d3.forceSimulation은 simulation 객체를 반환함.
simulation.force(name, force) : 노드에 중력 등의 물리력을 적용해 노드의 속도나 좌표를 수정함.simulation.on('tick', listener) : 시뮬레이션이 실행될 때, tick 이벤트가 실행될 때마다 listener로 넘긴 콜백함수를 실행함.d3.forceSimulation으로 네트워크 그래프를 그리기 위해서는 nodes, links 데이터가 필요함.
const nodes = [
{"id": "Alice"},
{"id": "Bob"},
{"id": "Carol"}
];
const links = [
{"source": 0, "target": 1}, // Alice → Bob
{"source": 1, "target": 2} // Bob → Carol
];
nodes는 그래프에서 각 노드가 표현할 데이터를 가진 배열임. 배열 안에 그래프에 그려질 모든 노드 정보가 들어있어야 하며, 각각의 노드는 객체여야 함.
node 객체는 simulation이 실행될 때 다음 속성을 가지게 됨.
index : nodes 배열 안에서 node의 인덱스x : 노드의 현재 x 좌표y : 노드의 현재 y 좌표vx : 노드의 현재 x축 속도vy : 노드의 현재 y축 속도links는 그래프에서 간선으로 연결될 두 노드를 각각 source, target으로 가진 link 객체를 가진 배열임.
links의 source, target 노드는 기본적으로 노드의 index를 이용해 초기화됨. index는 nodes 배열에서 해당 노드의 인덱스를 의미함.
link.id(id) 메소드를 이용하면 links 배열에서 node의 id로 index 대신 node의 다른 값을 사용할 수 있음.
// links의 id로 index 대신 노드의 id값 사용
d3.forceLink(links).id((d) => d.id)
const nodes = [
{"id": "Alice"},
{"id": "Bob"},
{"id": "Carol"}
];
const links = [
{"source": "Alice", "target": "Bob"},
{"source": "Bob", "target": "Carol"}
];
d3.forceSimulation으로 nodes의 속도와 좌표를 계산했다면, 이를 실제로 그리는 작업이 필요함.
// link 그리기
const drawLink = (links: Link[]) => {
d3.select(linkSelector)
.selectAll('line')
.data(links)
.join('line')
.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 그리기
const drawNode = (nodes: Node[]) => {
d3.select(nodeSelector)
.selectAll('path')
.data(nodes)
.join('path')
.attr('d', (d) => normalSymbol)
.attr('transform', (d) => `translate(${[d.x, d.y]})`)
}
d3.select(selector) : selector에 해당하는 첫 번째 요소를 선택함. selector로는 요소 이름에 해당하는 string을 넘길 수도 있고(ex. ‘a’), 요소의 reference를 직접 넘길 수도 있음. 일반적으로 selector로 svg의 g 요소를 넘겨서 사용함.
selection 객체를 반환함.
selection.selectAll(selector) : d3.select와 유사하나, selector에 해당하는 모든 요소를 선택함.
selection.data(data) : data를 selection으로 선택된 요소에 연결시킴. 네트워크 그래프에서는 data로 nodes, links 데이터를 넘김.
selection.join(selector) : selection에 연결된 data를 참고해서 그래프의 노드와 링크를 추가, 삭제, 업데이트함.
function Graph({ nodes, links }) {
const linkRef = useRef<SVGGElement | null>(null);
const nodeRef = useRef<SVGGElement | null>(null);
const drawLinks = useCallback(() => {
/* link 그리기 */
}, [linkRef])
const drawNodes = useCallback(() => {
/* node 그리기 */
}, [nodeRef])
useEffect(() => {
const simulation = d3
.forceSimulation(nodes)
.force(
'link',
d3.forceLink(links)
)
.on('tick', () => {
drawNodes();
drawLinks();
});
return () => simulation.stop(); // unmount시 simulation 멈춤
}, [links, nodes, drawLinks, drawNodes])
return (
<div>
<svg>
<g ref={linkRef}>
<line stroke="white 1px" />
</g>
<g ref={nodeRef}>
<path fill="yellow" />
</g>
</svg>
</div>
);
}