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)를 준비한다. forceSimulationnodesDatax, 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은 시간이 지나면 정지하는 사양이므로 드래그 시작이 되었을 때, simulationactive가 아닌 경우는 재시작시킨다. 이때 설정하고 있는 alphaTarget은 시뮬레이션을 매끄럽게 연결하기 위한 계수로 0 ~ 1의 값을 설정할 수 있어 낮은 값이 더 부드러워진다.




최종 수정 : 2024-01-18