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>
);
}