D3.js clusterで円周上にノードを配置する方法

D3の階層構造を表すclusterで、円周上にノードを配置する方法を紹介する。

階層構造、つまりhierarchyのデータ構造とデータ準備については、hierarchyのデータ構造と使い方を参照する。

サンプルプログラム

コードを確認

サンプルコード

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v5 hierarchy cluster radial v4/v5</title>
</head>

<body>
  <svg width="800" height="600"></svg>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script>
    // 1. 描画するデータを用意
    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" },
        {
          "name": "L",
          "children": [{ "name": "M" }, { "name": "N" }]
        },
        { "name": "O" },
        { "name": "P" }
      ]
    };

    var rx = width / 2;
    var ry = height / 2

    // 2. 描画するデータへ変換
    root = d3.hierarchy(data);
    var cluster = d3.cluster().size([360, ry - 80])
    cluster(root);

    // 3. SVG要素を設定
    g = d3.select("svg").append("g").attr("transform", "translate(" + rx + "," + ry + ")");
    var link = g.selectAll(".link")
      .data(root.links())
      .enter()
      .append("path")
      .attr("class", "link")
      .attr("fill", "none")
      .attr("stroke", "#555")
      .attr("stroke-width", "1.5px")
      .attr("opacity", "0.6")
      .attr("d", d3.linkRadial()
        .angle(function (d) { return (d.x + 90) * Math.PI / 180; })
        .radius(function (d) { return d.y; }));

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

    node.append("circle")
      .attr("r", 8)
      .attr("stroke", "steelblue")
      .attr("stroke-width", "1.5px")
      .attr("fill", "white");

    node.append("text")
      .attr("dy", 3)
      .attr("dx", function (d) { return d.x < 90 || d.x > 270 ? 8 : -8; })
      .style("text-anchor", function (d) { return d.x < 90 || d.x > 270 ? "start" : "end"; })
      .attr("font-size", "200%")
      .attr("transform", function (d) { return d.x < 90 || d.x > 270 ? null : "rotate(180)"; })
      .text(function (d) { return d.data.name; });

  </script>
</body>

</html>

サンプルコードの説明

1. 描画するデータを用意

描画するデータを用意する。clusterの基本的な使い方はclusterの使い方、データ構造の詳細はhierarchyのデータ構造と使い方を参照する。

2. 描画するデータへ変換

root = d3.hierarchy(data);
var cluster = d3.cluster().size([360, ry - 80])
cluster(root);

用意したデータを描画用のデータ構造へ変換する。「用意したデータ -> hierarchy用データ -> 描画種類別データ(今回はcluster)」という2段階の変換を行う。ここではx座標に回転角、y座標に半径を設定し、極座標系の位置を計算している。

3. SVG要素を配置

g = d3.select("svg").append("g").attr("transform", "translate(" + rx + "," + ry + ")");

まずSVG領域にグループを表す"g"要素を設定し、全体の中心座標を設定する。このグループ要素の中にノードとリンクを設定する。

  var link = g.selectAll(".link")
    .data(root.links())
    .enter()
    .append("path")
    .attr("class", "link")
    .attr("fill", "none")
    .attr("stroke", "#555")
    .attr("stroke-width", "1.5px")
    .attr("opacity", "0.6")
    .attr("d", d3.linkRadial()
      .angle(function(d) { return (d.x + 90) / 180 * Math.PI; })
      .radius(function(d) { return d.y; }));

リンクを設定する。

root.links()

これは階層構造のルートからリンクを配列として抽出する関数である。[{"source": nodedata, "target": nodedata}, ...]のような形式の配列が定義される。

d3.linkRadial()
  .angle(function(d) { return (d.x + 90) / 180 * Math.PI; })
  .radius(function(d) { return d.y; })

これは極座標系に設定されたノード間を接続するSVG要素path"d"属性値を返す関数である。角度はラジアンで指定する点に注意する。

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

  node.append("circle")
    .attr("r", 8)
    .attr("stroke", "steelblue")
    .attr("stroke-width", "1.5px")
    .attr("fill", "white");

  node.append("text")
    .attr("dy", 3)
    .attr("dx", function(d) { return d.x < 90 || d.x > 270 ? 8 : -8; })
    .style("text-anchor", function(d) { return d.x < 90 || d.x > 270 ? "start" : "end"; })
    .attr("font-size", "200%")
    .attr("transform", function(d) { return d.x < 90 || d.x > 270 ? null : "rotate(180)"; })
    .text(function(d) { return d.data.name; });

ノードを設定する。最初に"g"要素を設定し、その中に"circle""text"を設定する。

root.descendants()

これは階層構造のノードを配列として取得する関数である。また、transform属性で回転角を設定しているが、リンクの設定時とは90度ずれるため注意が必要である。"text"は配置される位置に応じて読みやすい角度へ変更している。

まとめ

ノード数が多くなると、こちらの例のようなインパクトのある図を描ける。横長のトーナメント表よりコンパクトに表示でき、デザイン性も高い。