앞선 포스팅에서 간단한 네트워크 차트를 그려봤습니다.
앞에서는 데이터에 각 노드별 x, y좌표를 직접 설정 해주고, nodes-links를 직접 매핑해 줬습니다.
이번 포스팅에서는 D3의 forceSimulation과 simulation객체의 메서드들을 이용하여 이 과정을 자동화하고, 꿈틀거리며 그래프가 부드럽게 생성되는 애니매이션도 함께 구현해보겠습니다.
d3.forceSimulation(nodes)
nodes 배열을 사용하는 새로운 시뮬레이션을 생성합니다.
시뮬레이션은 자동으로 시작되며, simulation.on("tick", callback)을 통해 각 tick마다(default-300회) 실행될 콜백을 지정할 수 있습니다.
만일 시뮬레이션의 실행시점을 직접 조작하고싶다면, simulation.stop()
을 호출하고, 원하는 시점에 simulation.tick()
을 실행하면 됩니다.
simulation.force(name[, force])
시뮬레이션에 지정된 name과 함께 force를 할당하고, 그 시뮬레이션을 리턴합니다.
이 때 name은 사용자가 원하는대로 이름 붙이는 값이므로 마음대로 써도 됩니다. (이 name은 메모리에서 내부적으로 forces Map의 key값이 될 뿐입니다)
(simulation.force("중력", d3.forceManyBody())
이렇게 해도 됨)
하지만 공식문서에서 charge, center, link 와 같은 이름을 사용하기때문에 어느정도 저런 명칭을 사용하는 암묵적인 약속(?)은 있는 듯 합니다.
simulation.on(typenames, [listener])
typenames("tick"
혹은 "end"
)에 해당하는 이벤트에 대한 리스너를 등록합니다.
tick
: 시뮬레이션 내부 타이머의 틱마다 발생합니다.
end
: 시뮬레이션 내부 타이머의 틱이 종료될 때 발생합니다.
simulation.on("tick", callback)
을 이용하여, 부드러운 애니매이션을 표현하는게 가능해집니다. 노드의 좌표가 tick마다 조금씩 변경되면서 x,y 속성이 계산되고, 이 때마다 node, links를 그리면 부드러운 애니매이션이 구현되는 것입니다.
이제 node에 직접 x, y를 설정하거나 links를 전처리하지 않아도 됩니다.
let nodes = [
{ name:"ye" },
{ name:"jun" },
{ name:"mi" },
{ name:"sung" },
]
let links = [
{ source:"ye", target:"jun" },
{ source:"jun", target:"mi" },
{ source:"mi", target:"sung" },
{ source:"sung", target:"jun" },
]
node, link selection에 데이터를 바인딩하고, 속성을 정의하는 로직은 기존 방식과 동일합니다.
const node =
d3.select("#node")
.selectAll("g")
.data(nodes)
.join("g")
.each(function(d) {
d3.select(this)
.append("circle")
.attr("r", 5)
.style("fill", "red");
d3.select(this)
.append("text")
.text(d => d.name);
})
const link =
d3.select("#link")
.selectAll("line")
.data(links)
.join("line")
.attr("stroke", "black");
function drawNodes(){
node.attr("transform", d =>"translate("+[d.x, d.y]+")" );
}
function drawLines() {
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)
}
d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-200)) // 1️⃣
.force('center', d3.forceCenter(250, 250)) // 2️⃣
.force('link', d3.forceLink(links).id(d => d.name)) // 3️⃣
.on("tick", () => {
drawNodes();
drawLines();
}); // 4️⃣
1️⃣ 노드간의 중력을 정의합니다. default값은 -30이며, strength() 메서드를 이용하여 힘의 크기를 정의할 수 있습니다. 음수이면 척력, 양수이면 인력을 의미합니다.
2️⃣ viewport에서 그래프의 중앙 좌표를 설정합니다.
3️⃣ nodes와 매핑될 links 배열을 넘겨, 매핑된 결과 생성합니다. 이 links는 source
, target
을 속성으로 가지는 객체의 배열입니다. id() 메서드를 통해 node의 어느 속성을 식별자로 가질지 설정할 수 있습니다. (설정하지않으면 node의 index로 매핑됩니다)
4️⃣ simulation tick마다 실행할 함수를 정의합니다.
그럼 이렇게 노드 좌표도 자동으로 계산해주면서, 꿈틀거리는(‼️) 네트워크 차트가 완성됩니다.