D3.js clusterの使い方

D3.jsで階層構造を可視化するclusterについて説明する。

clusterレイアウトは、決定木のようなクラスタ分析結果の表示に活用できる。データの準備やデータ構造の詳細はhierarchyの概要を参照すればよい。デモでは説明のため、もっとも単純なコードを採用している。

サンプルプログラム

コードを確認

サンプルコード

<!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);

この変数にルートが設定される。

次に、cluster用データへ変換する関数を呼び出す。

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の順である。また、子ノードから先頭に向かってリンクを描画する設定であり、先頭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の位置を設定するスタイルである。

まとめ

データを2回変換する必要がある点と、y座標がclusterの深さ方向である点がわかりにくい。注意が必要である。末端ノードを同じ位置に並べるのがclusterレイアウトだが、似た構造として、同じ親の子を同じ位置へ配置するtreeレイアウトがある。ほぼ同じプログラムで使い分けられる。