D3.js v4 の Hierarchical Edge Bundling を理解する(1)

はじめに

最近、某プロジェクト用の補助ツールとかを作るために D3.js - Data-Driven Documents を触ってるんですね。で、Hierarchical Edge Bundling - bl.ocks.org みたいなのを描きたいなあとおもったんですが、壁がありまして。

  • そもそもこのサンプルスクリプト自体が(自分にとっては)複雑で何をやっているのかわからない…
    • もともと javascript 自体ほとんど描いたことがない状態から始めているので、ますますわからない。この辺はちょっとは勉強したつもりですが、言語仕様とかについての理解はまだ曖昧なところがあります。なので、この記事の中で使われている用語やそもそもの理解について、不適切なものがあったり誤りがあったりする可能性があります。
  • D3.js v4 になったら (2016/6月) いろいろ変わったところがあって、過去のサンプルスクリプトや参考書の類いがそのままは使えない

D3.js の本は2冊ほど買ってたんですけどね。サンプルコードの類いは v3 ベースなので、v4 でやろうとすると結構違うんですよね。

エンジニアのための データ可視化[実践]入門 ~D3.jsによるWebの可視化 (Software Design plus)

エンジニアのための データ可視化[実践]入門 ~D3.jsによるWebの可視化 (Software Design plus)

とかね。処理そのものは参考になるものの D3.js v4 ではどう書けばいいのかがわからない…。

ともあれ地道に試すしかない。処理の流れとしては cluster layout があって、それをさらに bundle layout で変換するというカタチらしい。まずは階層化データの取り扱いと cluster layout を理解しよう。各レイアウトの基本的なサンプルコードは svg要素の基本的な使い方まとめ を参考にしています。

Sample Code

サンプルコードはこれ

こういう図ができる。

データの準備

d3/d3-hierarchy にあるサンプルデータそのまま使ってまずは試してみる。

    var data = {
        "name": "Eve",
        "children": [
            { "name": "Cain" },
            {
                "name": "Seth",
                "children": [
                    { "name": "Enos" },
                    { "name": "Noam" }
                ]
            },
            { "name": "Abel" },
            {
                "name": "Awan",
                "children": [
                    { "name": "Enoch" }
                ]
            },
            { "name": "Azura" }
        ]
    };

こうした階層化されたデータを作る方法については、d3.nest() を使うとか d3.stratify() を使うとかあるみたいなんだけど、その辺はまた追々…。

階層化されたノードオブジェクトの生成

var root_node = d3.hierarchy(data);
console.log("root_node");
console.log(root_node);

大本の階層化データを変換して、階層化されたノードオブジェクトをつくる。基本的にデータ構造は同じだけど、これによって、各ノードに階層構造のメタデータ等が付随する Node オブジェクトに変換される。

  • depth : root node からの深さ (root だと 0)
  • height : leaf node からの高さ (leaf だと 0)
  • parent : 親ノードへの参照
  • children : 子ノードのリスト

このとき、元のデータの children という accessor function で子ノードの情報を参照する、というのがデフォルトになっているので注意。もし元データに children 以外の名前で子ノードの情報を持たせているのであれば、d3.hierarchy(data, function(d) { return d.foobar; }) みたいに 子ノードアクセスのための accessor function を指定してやれば良いはず。

レイアウトの設定

階層化された Node オブジェクトのデータを描画するために、特定のレイアウトでオブジェクトの位置決めをする。今回使うのは cluster layout になる。(参照 : d3/d3-hierarchy: 2D layout algorithms for visualizing hierarchical data.)

    var node_size = 20;
    var cluster = d3.cluster()
        .size([width, height]);
    var nodes = cluster(root_node);
    var links = nodes.links();

    console.log("clustered nodes");
    console.log(nodes);
    console.log("clustered nodes (leaves)");
    console.log(nodes.leaves());
    console.log("clustered nodes (ancestors)"); // from root
    console.log(nodes.ancestors());
    console.log("clustered nodes (descendants)"); // from root
    console.log(nodes.descendants());
    console.log("clustered links");
    console.log(links);

ここで描画したい大きさに基づく実際のレイアウトが決まって座標情報が付与される。

  • x,y : ノードの座標(x,y)


レイアウトの描画

ノードの描画

ノードを扱うためのメソッドがいくつかあるので、それを使いつつノードやノード間の線(path)を描画していく。

  • leaves : leaf になっているノードのリスト (この例だと, [Cain, Enos, Noam, Abel, Enoch, Azura])
  • ancestors : (特定の Node の)親ノードのリスト (「祖先」一覧)
  • descendants : (特定の Node の)子ノードのリスト (「子孫」一覧)

サンプルでは root 以下全部のノードをまとめて全部描いてる。

    // circle (overwrite path)
    svg.selectAll("circle")
        .data(nodes.descendants())
        .enter()
        .append("circle")
        .attrs({
            "cx" : function(d) { return d.x; },
            "cy" : function(d) { return d.y; },
            "r" : node_size/2
        })
        .append("title")
        .text(function(d) { return d.data.name; });
ノード間の線(path)の描画

nodes.links() で、各ノード間の path (始点・終点) 情報が取れる。

ので、これを元に線を描く。

    var line = d3.line()
        .curve(d3.curveBundle.beta(0.85))
        .x(function(d) { return d.x; })
        .y(function(d) { return d.y; });
    svg.selectAll("path")
        .data(links)
        .enter()
        .append("path")
        .attr("d", function(d) {
                return line([
                    d.source,
                    {"x" : d.source.x, "y" : (d.source.y + d.target.y)/2 },
                    {"x" : d.target.x, "y" : (d.source.y + d.target.y)/2 },
                    d.target
                ]);
            });

端点(d.source, d.target)だけだと直線になってしまうので、中間点を適当に作って曲線を描画してある。

描画上のトリック

素直に SVG の領域全体に描くと…

こうなる(はみ出す)ので、ちょっと縮小した上で "ずらす" 処理を入れてあります。

    var svg = d3.select("body")
        .append("svg")
        .attrs({"width" : width,
                "height" : height})
        .append("g")
        .attr("transform", "scale(0.8, 0.8)translate(20, 20)");

覚えておくこと

  • 元データを変換していく
    • データ構造
    • 見た目のデータ