D3.js cluster 사용법

결정 트리와 같은 클러스터 분석 결과 표시에 활용할 수 있다. 데이터 준비 및 데이터 구조에 대해 자세히 알아 보세요. 데모에서는 해설을 위해 가장 간단한 코드를 채용하고 있다.

예제 프로그램

코드 확인

샘플 코드

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 hierarchy cluster</title>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <!-- 1. 스타일 준비 -->
  <style>
    .link {
      fill: none;
      stroke: #555;
      stroke-opacity: 0.4;
      stroke-width: 1.5px;
    }
  </style>
</head>

<body>
  <svg width="800" height="600"></svg>
  <script>
    // 2. 그리려는 데이터 준비
    var width = document.querySelector("svg").clientWidth;
    var height = document.querySelector("svg").clientHeight;
    var data = {
      "name": "A",
      "children": [
        { "name": "B" },
        {
          "name": "C",
          "children": [{ "name": "D" }, { "name": "E" }, { "name": "F" }]
        },
        { "name": "G" },
        {
          "name": "H",
          "children": [{ "name": "I" }, { "name": "J" }]
        },
        { "name": "K" }
      ]
    };

    // 3. 그리려는 데이터 준비
    root = d3.hierarchy(data);

    var cluster = d3.cluster()
      .size([height, width - 160])
    //  .nodeSize([50,300]) ;
    //  .separation(function(a, b) { return(a.parent == b.parent ? 1 : 2); });

    cluster(root);

    // 4. SVG 요소 설정
    g = d3.select("svg").append("g").attr("transform", "translate(80,0)");
    var link = g.selectAll(".link")
      .data(root.descendants().slice(1))
      .enter()
      .append("path")
      .attr("class", "link")
      .attr("d", function (d) {
        return "M" + d.y + "," + d.x +
          "C" + (d.parent.y + 100) + "," + d.x +
          " " + (d.parent.y + 100) + "," + d.parent.x +
          " " + d.parent.y + "," + d.parent.x;
      });

    var node = g.selectAll(".node")
      .data(root.descendants())
      .enter()
      .append("g")
      .attr("transform", function (d) { return "translate(" + d.y + "," + d.x + ")"; })

    node.append("circle")
      .attr("r", 8)
      .attr("fill", "#999");

    node.append("text")
      .attr("dy", 3)
      .attr("x", function (d) { return d.children ? -12 : 12; })
      .style("text-anchor", function (d) { return d.children ? "end" : "start"; })
      .attr("font-size", "200%")
      .text(function (d) { return d.data.name; });
  </script>
</body>

</html>

예제 코드 설명

1. 스타일 준비

링크 스타일을 설정한다.

<style>
.link {
  fill: none;
  stroke: #555;
  stroke-opacity: 0.4;
  stroke-width: 1.5px;
}
</style>

이번에는 노드 스타일이 svg 요소로 직접 설정된다.

2. 그리려는 데이터 준비

그리려는 데이터를 준비한다.

var data = {
  "name": "A",
  "children": [
    { "name": "B" },
    {
      "name": "C",
      "children": [{ "name": "D" }, { "name": "E" }, { "name": "F" }]
    },
    { "name": "G" },
    {
      "name": "H",
      "children": [{ "name": "I" }, { "name": "J" }]
    },
    { "name": "K" }
  ]
};

데이터 구조에 대한 자세한 내용은 여기를 참조하라.

3. 그리려는 데이터 변환

준비된 데이터를 그리려는 데이터 구조로 변경한다.

root = d3.hierarchy(data);

var cluster = d3.cluster()
  .size([height, width - 160])
  //  .nodeSize([50,300]) ;
  //  .separation(function(a, b) { return(a.parent == b.parent ? 1 : 2); });

cluster(root);

“준비한 데이터 → hierarchy용 데이터 → 그리기 종류별(이번은 cluster)의 데이터"로 2단계 변환이 필요하다.

먼저, 명령에서 datahierarchy의 데이터 구조 루트로 변경한다.

root = d3.hierarchy(data);

루트에는 이 변수가 설정된다.

그런 다음, 클러스터에 대한 데이터로 변경하는 함수를 호출한다.

var cluster = d3.cluster()
  .size([height, width - 160])
  //  .nodeSize([50,300]) ;
  //  .separation(function(a, b) { return(a.parent == b.parent ? 1 : 2); });

cluster(root);

d3.cluster()로 호출한 함수에 root를 인수로 설정하면 다음 데이터가 root에 부여된다.

좌표 설명
x 노드의 배열 (형제) 방향의 좌표
y 노드의 깊이 (부모 - 자식) 방향의 좌표 (선두가 0)

이번 예제에서는 부여되는 좌표는 x가 화면 세로 방향, y가 화면 가로가 되므로 주의가 필요하다.

또, d3.cluster()에는 이하의 설정이 가능하다.

설정 설명
cluster.size() 그리려는 구조의 크기를 [정렬 방향, 깊이 방향]의 2가지 요소 배열로 설정한다.
인수가 설정되어 있지 않은 경우는 현재의 사이즈를 반환한다.
기본값은 [1, 1]이다.
cluster.nodeSize() 노드 1개의 크기를 [정렬 방향, 깊이 방향]의 2가지 요소 배열로 설정한다.
인수가 설정되어 있지 않은 경우는 현재의 사이즈를 반환한다.
기본값은 null이다.
nodeSize()null이면 위의 size()를 사용한다.
nodeSize()가 지정되어 있는 경우 선두의 요소의 위치가 (0,0)으로 설정된다.
cluster.separation() 인접한 요소 사이의 간격을 결정하는 함수를 설정합니다. 기본값은 다음과 같다.
function separation(a, b) {
return a.parent == b.parent ? 1 : 2;
}
본 프로그램 예제는 디폴트 설정으로 인접하는 노드가 다른 부모의 경우, 같은 부모의 경우에 비해 2배 틈이 비어 있다.

여기서, 이번은 size()를 사용하여 cluster의 폭을 설정하고 있지만, 선두의 좌표가 0, 깊이 방향 말단의 좌표가 설정한 폭이 되도록 계산되기 때문에 아래와 같이 svg의 드로잉 영역의 폭보다 작게 cluster의 폭을 설정하고 있다.

.size([height, width - 160])

4. svg 요소 설정

g = d3.select("svg").append("g").attr("transform", "translate(80,0)");

첫 번째 요소의 좌표가 0으로 설정되므로 그룹을 나타내는 "g"요소를 설정하여 전체를 x 방향으로 이동시킨다. 이 그룹 요소 안에 노드와 링크를 설정한다.

먼저 링크용 svg 요소를 설정한다.

var link = g.selectAll(".link")
  .data(root.descendants().slice(1))
  .enter()
  .append("path")
  .attr("class", "link")
  .attr("d", function(d) {
    return "M" + d.y + "," + d.x +
      "C" + (d.parent.y + 100) + "," + d.x +
      " " + (d.parent.y + 100) + "," + d.parent.x +
      " " + d.parent.y + "," + d.parent.x;
  });

데이터 할당 부분으로 아래의 함수를 사용하고 있는데, 이는 중첩된 노드를 배열로서 늘어놓아 주는 함수이다.

root.descendants()

순서는 지정한 노드에 대한 깊이 방향, 배열 방향의 순서로 표시된다. 이 예제에서는 A→B→C→G→H→K→D→E→F→I→J의 순서이다. 그리고, 자식 노드부터 선두를 향해 링크를 draw하는 설정으로 선두 A로부터의 링크는 없기 때문에 slice로 선두의 노드를 제외하고 있다.

그런 다음 노드의 svg 요소를 설정한다.

var node = g.selectAll(".node")
  .data(root.descendants())
  .enter()
  .append("g")
  .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })

node.append("circle")
  .attr("r", 8)
  .attr("fill", "#999");

node.append("text")
  .attr("dy", 3)
  .attr("x", function(d) { return d.children ? -12 : 12; })
  .style("text-anchor", function(d) { return d.children ? "end" : "start"; })
  .attr("font-size", "200%")
  .text(function(d) { return d.data.name; });

노드에는 'circle''text'의 2개를 설정하기 위해서, 우선 'g' 요소를 설정하여, 그 안에 'circle''text'를 설정해 간다. 'g' 요소로 위치를 설정하고 있는데 cluster의 깊이 방향이 y이고 정렬 방향이 x임을 유의한다.

자식 노드가 많은 노드의 우측은 링크가 밀집하기 때문에, 자식 노드가 있는 경우는 우측, 없는 경우는 좌측에 text를 설정하고 있다. "text-anchor"text의 위치를 ​​설정하는 스타일이다.

마무리

데이터를 두 번 변환해야 하는 점과 y 좌표가 cluster의 깊이 방향인 점을 알기 어려운 점이다. 주의가 필요하다. 마지막 노드를 같은 위치에 늘어놓는 것이 cluster 레이아웃이지만, 비슷한 구조로 같은 부모의 자식을 같은 위치에 배치하는 tree 레이아웃이 있다. 거의 같은 프로그램으로 구분하여 사용할 수 있다.




최종 수정 : 2024-01-18