D3.js forceSimulation 기본 구성

D3.js의 forceSimulation의 가장 간단한 구성 프로그램을 소개한다.

예제 프로그램

노드(동그라미)를 드래그할 수 있다.

코드 확인

D3.js의 forceSimulation은 노드의 위치를 스프링의 힘 등을 고려하여 계산하는 것이다. v3 이전에는 force layout으로 정의되었으나, 동일한 함수를 사용할 수 없게 되어서 이후 버전에는 다시 작성해야 한다. 설정할 수 있는 파라미터가 몇 가지 있는데, 여기서는 바로 사용할 수 있는 것을 목표로 최소 구성의 데모와 코드를 설명한다.

예제 코드

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v5 force simulation</title>
</head>

<body>
  <svg width="400" height="300"></svg>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script>
    // 1. 그리려는 데이터 준비
    var nodesData = [
      {},
      {},
      {},
      {},
      {},
      {}
    ]

    var linksData = [
      { "source": 0, "target": 1 },
      { "source": 1, "target": 4 },
      { "source": 2, "target": 3 },
      { "source": 2, "target": 5 },
      { "source": 5, "target": 1 }
    ]

    // 2. svg 요소 추가
    var link = d3.select("svg")
      .selectAll("line")
      .data(linksData)
      .enter()
      .append("line")
      .attr("stroke-width", 1)
      .attr("stroke", "black");

    var node = d3.select("svg")
      .selectAll("circle")
      .data(nodesData)
      .enter()
      .append("circle")
      .attr("r", 7)
      .attr("fill", "LightSalmon")
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

    // 3. forceSimulation 설정
    var simulation = d3.forceSimulation()
      .force("link", d3.forceLink())
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(200, 150));

    simulation
      .nodes(nodesData)
      .on("tick", ticked);

    simulation.force("link")
      .links(linksData);

    // 4. forceSimulation 그림 업데이트 함수
    function ticked() {
      link
        .attr("x1", function (d) { return d.source.x; })
        .attr("y1", function (d) { return d.source.y; })
        .attr("x2", function (d) { return d.target.x; })
        .attr("y2", function (d) { return d.target.y; });
      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 = [
  {},
  {},
  {},
  {},
  {},
  {}
]

var linksData = [
  { "source": 0, "target": 1 },
  { "source": 1, "target": 4 },
  { "source": 2, "target": 3 },
  { "source": 2, "target": 5 },
  { "source": 5, "target": 1 }
]

nodesData는 노드의 데이터이다. 단순히 그리기만 하려면 빈 배열이어되 된다. linksData는 링크 데이터로 연결하는 두 노드의 ID(source, target)가 필요하다.

2. svg 요소 추가

var link = d3.select("svg")
  .selectAll("line")
  .data(linksData)
  .enter()
  .append("line")
  .attr("stroke-width", 1)
  .attr("stroke", "black");

var node = d3.select("svg")
  .selectAll("circle")
  .data(nodesData)
  .enter()
  .append("circle")
  .attr("r", 7)
  .attr("fill", "LightSalmon")
  .call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));

링크와 노드의 svg 요소를 넣는다. call()로 드래그시의 이벤트 함수를 등록하고 있다. 이렇게 하면 노드를 드래그할 수 있다.

3. forceSimulation 설정

드디어 여기에서가 forceSimulation의 설정이다.

링크에 의한 스프링의 힘을 동작시킨다.

d3.forceSimulation().force("link", d3.forceLink())

노드 사이의 크롱력(coulomb)을 작동시킨다. 기본적으로 반발력이 설정된다. 이 행을 생략해도 forceSimulation은 동작한다.

.force("charge", d3.forceManyBody())

모든 노드의 중심 위치를 설정한다. 없어도 동작하지만, 없으면 무중력 상태가 되어 화면 밖에 요소가 튀어나와도 돌아오지 않게된다.

.force("center", d3.forceCenter(200, 150));

시뮬레이션에 노드에 대한 데이터 배열을 등록하고, .on('tick', ...)에서는 계산 갱신마다 호출하는 함수를 등록한다. 계산 결과는 노드의 데이터 배열에 쓰여지는 스펙이 되고, svg 요소를 움직이기 위해서는 계산 결과를 svg 요소의 위치에 반영해야 한다.

  simulation
    .nodes(nodesData)
    .on("tick", ticked);

시뮬레이션에 링크용 데이터 배열을 등록한다.

  simulation.force("link")
    .links(linksData);

4. forceSimulation 드로잉 업데이트 기능

아래에서 계산 갱신마다 호출하는 함수이다. svg 요소를 이동하기 위해 계산 결과를 svg 요소의 위치에 반영한다. 링크의 데이터 sourcetarget은 최초로 설정한 숫자가 아니고, 노드용의 데이터 배열에의 참조가 된다.

function ticked() {
  link
    .attr("x1", function(d) { return d.source.x; })
    .attr("y1", function(d) { return d.source.y; })
    .attr("x2", function(d) { return d.target.x; })
    .attr("y2", function(d) { return d.target.y; });
  node
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });
}

5. 드래그시 이벤트 함수

드래그시의 이벤트 함수이다. d에 저장된 것은 등록된 노드에 대한 데이터이다. 노드의 데이터에 fx, fy가 정의되어 있으면 해당 노드의 좌표가 고정된다. 드래그 중에 마우스와 동작을 연동시키기 위해, 드래그 시작되었을 때에 드래그 요소의 위치를 ​​고정하고, 드래그 중에는 마우스 좌표(event.x, event.y)를 반영한다. 드래그 종료시에 고정을 해제(null를 대입)한다.

또한, simulation은 시간이 지나면 정지하는 되므로 드래그 시작시 simulation이 active가 아닌 경우는 재시작시킨다. 이때 설정하고 있는 alphaTarget은 시뮬레이션을 매끄럽게 연결하기 위한 계수로 v4에서 도입된 것이다. v3에서는 다시 시작할 때 노드가 점프하도록 움직이는 것 같다. 0~1의 값을 설정할 수 있어 낮은 값이 부드럽게 되지만, 0으로 하면 재시작시에 노드가 전혀 움직이지 않게 된다.

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



최종 수정 : 2024-01-18