d3로 동적인 네트워크 차트 그리기 (3) - D3.js - Simulation

유니·2022년 12월 23일
2

JavaScript

목록 보기
8/9

앞선 포스팅에서 간단한 네트워크 차트를 그려봤습니다.
앞에서는 데이터에 각 노드별 x, y좌표를 직접 설정 해주고, nodes-links를 직접 매핑해 줬습니다.
이번 포스팅에서는 D3의 forceSimulation과 simulation객체의 메서드들을 이용하여 이 과정을 자동화하고, 꿈틀거리며 그래프가 부드럽게 생성되는 애니매이션도 함께 구현해보겠습니다.

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" },
]

data-binding, attribute 정의

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마다 실행할 함수를 정의합니다.

코드 실행화면

그럼 이렇게 노드 좌표도 자동으로 계산해주면서, 꿈틀거리는(‼️) 네트워크 차트가 완성됩니다.

profile
추진력을 얻는 중

0개의 댓글