D3.js forceSimulation 노드(node) 상호작용 설명
D3.js forceSimulation의 링크(link, edge) 없는 노드 간의 상호 작용을 설명한다.
예제 프로그램
노드(동그라미)를 드래그할 수 있다.
예제 코드
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 v7 force simulation node detail</title>
</head>
<body>
<svg width="800" height="550"></svg>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
// 1. 그리려는 데이터 준비
var nodesData = [];
for (var i = 0; i < 50; i++) {
nodesData.push({
"x": 800 * Math.random(),
"y": 600 * Math.random(),
"r": 30 * Math.random() + 5
});
}
// 2. svg 요소 추가
var node = d3.select("svg")
.selectAll("circle")
.data(nodesData)
.enter()
.append("circle")
.attr("r", function (d) { return d.r })
.attr("fill", "LightSalmon")
.attr("stroke", "black")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// 3. forceSimulation 설정
var simulation = d3.forceSimulation()
// .force("link", d3.forceLink()) // 여기서는 불필요
.force("collide",
d3.forceCollide()
.radius(function (d) { return d.r })
.strength(1.0)
.iterations(16))
.force("charge", d3.forceManyBody().strength(5))
.force("x", d3.forceX().strength(0.1).x(400))
.force("y", d3.forceY().strength(0.1).y(300));
// .force("center", d3.forceCenter(300, 200)); // 여기서는 불필요
simulation
.nodes(nodesData) // simulation에 노드용 데이터 등록
.on("tick", ticked); // 계산 업데이트마다 호출할 함수 등록
// 4. forceSimulation 그림 업데이트 함수
function ticked() {
node
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
}
// 5. 드래그 이벤트 함수
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
</script>
</body>
</html>
설명
1. 그리려는 데이터 준비
var nodesData = [];
for(var i = 0; i < 50; i++) {
nodesData.push({
"x": 800 * Math.random(),
"y": 600 * Math.random(),
"r": 30 * Math.random() + 5
});
}
먼저 노드에 대한 데이터(nodesData
)를 준비한다. forceSimulation
은 nodesData
의 x
, y
좌표를 업데이트하지만 먼저 x
, y
를 정의하면 초기 위치로 설정할 수 있다. 반경을 노드별로 변경하기 위해 r
을 변수로 정의한다.
2. svg 요소 추가
var node = d3.select("svg")
.selectAll("circle")
.data(nodesData)
.enter()
.append("circle")
.attr("r", function(d) { return d.r })
.attr("fill", "LightSalmon")
.attr("stroke", "black")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
노드의 svg
요소를 추가한다. call(...)
로 드래그 되었을 때의 이벤트 함수를 등록하고 있다.
그리고 아래와 같이 노드 마다 다른 반경(random 값)을 설정하고 있다.
.attr("r", function(d) { return d.r })
3. forceSimulation 설정
var simulation = d3.forceSimulation()
// .force("link", d3.forceLink()) // 여기서는 불필요
.force("collide",
d3.forceCollide()
.radius(function(d) { return d.r })
.strength(1.0)
.iterations(16))
.force("charge", d3.forceManyBody().strength(5))
.force("x", d3.forceX(400).strength(0.1))
.force("y", d3.forceY(300).strength(0.1));
// .force("center", d3.forceCenter(400, 300)); // 여기서는 불필요
simulation
.nodes(nodesData) // simulation에 노드용 데이터 등록
.on("tick", ticked); // 계산 업데이트마다 호출할 함수 등록
forceSimulation
에서는 다음과 같은 상호작용을 설정할 수 있다.
설정 | 설명 |
---|---|
"link" |
노드간을 연결하는 링크의 작용력 (여기서는 생략) |
"collide" |
노드 간의 접촉 반발력 |
"charge" |
노드 사이의 크롱력(비접촉 작용력) |
"x" , "y" |
위치 기반 필드의 힘 |
"center" |
모든 노드의 질량 중심 |
"r" |
레이디얼 포스 |
링크 이외의 부분을 자세히 살펴 보겠다.
“collide” : 노드 간의 접촉 반발력
.force("collide",
d3.forceCollide()
.radius(function(d) { return d.r })
.strength(1.0)
.iterations(16))
함수 | 설명 |
---|---|
radius |
simulation할 노드의 반경을 설정한다. 기본값은 1이다. 이번에는 변수에 function(d) { return dr; } 을 설정하고 nodesData로 정의한 반경 r을 할당한다.반지름 변수만을 함수로 하였지만, 다른 파라미터도 모두 함수를 설정할 수 있다. |
strength |
겹치는 노드 사이의 반발력이다. 0.0 ~ 1.0의 소수점으로 설정한다. 기본값은 0.7이다. |
iterations |
simulation의 반복 횟수. 반복 횟수를 늘리면 계산이 크게 안정되고 노드 겹침을 피하기 쉬워지지만 계산 시간이 늘어난다. 기본값은 1이다. |
“charge” : 노드 사이의 쿨롱력(Coulomb, 비접촉 작용력)
.force("charge", d3.forceManyBody().strength(5))
함수 | 설명 |
---|---|
strength |
양의 값을 지정하면 중력과 마찬가지로 노드가 서로 끌어당겨지고, 음의 값을 지정하면 정전기처럼 노드가 서로 반발한다. 값의 크기로 힘의 크기를 설정한다. 기본값은 -30이다. |
theta |
계산의 근시의 정밀도를 결정하는 정수. 모든 입자간의 쿨롱력을 계산하면 시간이 걸리기 때문에, 멀리 있는 노드 덩어리로서 계산하는(Barnes-Hut 근사) 것으로 고속화하고 있다. 기본값은 0.9이다. (이번에는 설정되지 않음) |
distanceMin |
쿨롱 힘을 계산하는 최소 거리. 두 노드가 겹치면 거리가 0이 되고 힘이 무한대가 되는 것을 피한다. 기본값은 1이다. (이번에는 설정되지 않음) |
distanceMax |
노드 사이의 최대 거리를 설정한다. 지정되어 있지 않은 경우는, 현재의 최대 거리를 돌려준다. 디폴트는 무한대이다. 최대 거리를 지정하면 성능이 향상된다. (이번에는 설정되지 않음) |
“x”, “y” : 위치 기반 필드의 힘
.force("x", d3.forceX().strength(0.1).x(400))
.force("y", d3.forceY().strength(0.1).y(300))
함수 | 설명 |
---|---|
strength |
강도의 크기를 나타내는 지표로, 계산 1 스텝으로 지정한 위치로 어느 정도 돌아오는지를 결정하는 계수이다. 0.1이면 지정한 위치를 향해 계산 1단계로 10% 이동한다. 0.0 ~ 1.0이 추천 값이고, 디폴트는 0.1이다. |
x |
필드의 힘의 중심 x 좌표이다. 기본값은 0이다. |
y |
필드의 힘의 중심 y 좌표이다. 기본값은 0이다. |
“center” : 모든 노드의 질량 중심
// .force("center", d3.forceCenter(300, 200)); // 여기서는 불필요
함수 | 설명 |
---|---|
d3.forceCenter(x, y) |
모든 노드의 질량 중심 좌표이다. 뷰포트의 중앙에 그리기를 유지하는 데 도움이 된다. 다른 작용력과 달리 노드 간의 상대 위치를 변경하지 않는다. |
4. forceSimulation 드로잉 업데이트 기능
function ticked() {
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
시뮬레이션 단계별로 호출되는 함수이다. svg
요소를 이동하기 위해 계산 결과를 svg
요소의 위치에 반영한다.
5. forceSimulation 드로잉 업데이트 기능
function dragstarted(event, d) {
if(!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if(!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
드래그시의 이벤트 함수이다. 노드의 데이터에 fx
, fy
가 정의되어 있으면 해당 노드의 좌표가 고정된다. 드래그 중에 마우스와 동작을 연동시키기 위해, 드래그 시작되어 있을 때에 드래그 요소의 위치를 고정하고, 드래그 중에는 마우스 좌표(event.x
, event.y
)를 반영하여, 드래그 종료시에 고정을 해제(null
를 대입)한다.
또한 simulation은 시간이 지나면 정지하는 사양이므로 드래그 시작이 되었을 때, simulation
이 active
가 아닌 경우는 재시작시킨다. 이때 설정하고 있는 alphaTarget
은 시뮬레이션을 매끄럽게 연결하기 위한 계수로 0 ~ 1의 값을 설정할 수 있어 낮은 값이 더 부드러워진다.
최종 수정 : 2024-01-18