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

Sample Code

サンプルコードはこれ

こういう図ができる。


データの準備

leaf 間のリンクデータを追加します。

    var link_data = [
        {"source" : "Azura", "target" : "Enoch"},
        {"source" : "Enoch", "target" : "Abel"},
        {"source" : "Enoch", "target" : "Noam"},
        {"source" : "Azura", "target" : "Noam"},
        {"source" : "Abel", "target" : "Enos"},
        {"source" : "Abel", "target" : "Noam"},
        {"source" : "Noam", "target" : "Enos"},
        {"source" : "Enos", "target" : "Cain"}
    ];

変わらないところ

cluster layout を円周上に描画するところは 前回 と同じです。leaf 間のリンクだけ上書きしてます。path の書き分けのために class 指定して CSS で描き分けてるところくらいかな。

変わるところ

まず、leaf 間リンク情報を Node オブジェクトに変換します。d3.map() については d3/d3-collection: Handy data structures for elements keyed by string. を参照。leaf 一覧 (nodes.leaves()) を name attribute で引っ張れるようにしています。

    var nodes_name_map = d3.map(nodes.leaves(), function(d) { return d.data.name; });

leaf 間リンクのデータは name だけ持っているので、それをキーに Node オブジェクトを取ってきます。

    var interleaf_links = link_data.map(function(d) {
        return {
            "source" : nodes_name_map.get(d.source),
            "target" : nodes_name_map.get(d.target)
        };
    });
    console.log("interleaf_links");
    console.log(interleaf_links);


leaf 間 path の描画

d3.radialLine() はそのまま使います。ここで重要なのは node.path(target) です。

# node.path(target) <>

Returns the shortest path through the hierarchy from this node to the specified target node. The path starts at this node, ascends to the least common ancestor of this node and the target node, and then descends to the target node. This is particularly useful for hierarchical edge bundling.

GitHub - d3/d3-hierarchy: 2D layout algorithms for visualizing hierarchical data.

path は node と target の間に共通の parent がある場合に、そこを経由した曲線を作成してくれます。……というのもあって上の図はあえて cluster layout tree の上に上書きしてみました。この機能、d3.js v3 では d3.layout.bundle() が提供していたようです。

    // path
    svg.selectAll("path.interleaf")
        .data(interleaf_links)
        .enter()
        .append("path")
        .attr("class", "interleaf")
        .attr("d", function(d) {
            return line(d.source.path(d.target));
        });

Hierarchical Edge Bundling

leaf のみにして parent node を描画しないようにすればできあがりです。

コードはこれ。

--- d3-hierarchical-edge-bundling.js    2017-06-03 23:56:00.309974700 +0900
+++ d3-hierarchical-edge-bundling2.js   2017-06-04 00:26:54.340704000 +0900
@@ -51,9 +51,6 @@
     var node_size = 20;
     var cluster = d3.cluster().size([360, radius]);
     var nodes = cluster(root_node);
-    var parent2child_links = nodes.links();
-    console.log("parent2child_links");
-    console.log(parent2child_links);

     var nodes_name_map = d3.map(nodes.leaves(), function(d) { return d.data.name; });
     var interleaf_links = link_data.map(function(d) {
@@ -73,19 +70,6 @@
         .curve(d3.curveBundle.beta(0.85))
         .radius(function(d) { return d.y; })
         .angle(function(d) { return path_angle(d.x); });
-    svg.selectAll("path.parent2child")
-        .data(parent2child_links)
-        .enter()
-        .append("path")
-        .attr("class", "parent2child")
-        .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
-                ]);
-            });

     // path
     svg.selectAll("path.interleaf")
@@ -99,7 +83,7 @@

     // circle (overwrite path)
     svg.selectAll("circle")
-        .data(nodes.descendants())
+        .data(nodes.leaves())
         .enter()
         .append("circle")
         .attrs({
@@ -112,7 +96,7 @@

     // text
     svg.selectAll("text")
-        .data(nodes.descendants())
+        .data(nodes.leaves())
         .enter()
         .append("text")
         .attrs({

おわりに

3回に分けて、順を追いつつ Hierarchical Edge Bundling の話を見てきました。D3.js Gallery にあるのは凄くきれいで洗練されてるんだけど、何をやっているのか理解したいという目的で見るとちょっと難しいね。まああれは、D3.js でどんなことができるのか、"上" のイメージを見せるものだよね。本当はこの上で、CSSを組み合わせて動的にハイライトさせるとかそういう処理があったりするんですが、そのへんは発展課題で。最小限でやるとこんな感じじゃないかなあと思っていますが、それほど javascript とかやってるわけじゃないので、もうちょっといい方法もあるかもしれません。

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

はじめに

前回 は cluster layout で階層データを描いていました。ここから Hierarchical Edge Bundling にいく……前にこれをちょっと変えます。上下方向(水平・垂直方向)に広げて描いていた階層を円周上にレイアウトしてみたいと思います。

参考

Sample Code

サンプルコードはこれ

こういう図ができる。

変わらないところ

データの取り扱いや cluster layout で各ノードの座標を決めるところまでは同様。というか、ノードや path の描画もほぼ同じ。

変わるところ

座標系を極座標に変換している、というところがちがいます。

    var radius = Math.min(width/2, height/2);
 
    var cluster = d3.cluster().size([360, radius]);

これは、各ノードの x 座標を角度(degree)、y 座標 (root nodeからの垂直方向の距離)を半径(原点からの距離)として設定しています。

path の描画

変更しているのは d3.line() ではなく d3.radialLine() にして極座標指定で線を描いているところ。

    var line = d3.radialLine()
        .curve(d3.curveBundle.beta(0.85))
        .radius(function(d) { return d.y; })
        .angle(function(d) { return path_angle(d.x); });
    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
                ]);
            });

function path_angle(x) {
    return rad(x);
}

とりあえず素直に前回作ったものを変換してみた。すると上の図のようになる…。root node ("Eve") の x 座標自体が角度として設定されるので、そのまま変換するとちょっと不格好な path になる。まあ、parent node 〜 child node の path にはもともと方向性があるので、そのまま変換すると偏りが出てしまうのだな。ここはもうちょっと工夫が必要。

ノードの描画

これも極座標を直交座標に変換している。90度 (Math.PI/2) ずらしているけど、これは d3.radialLine().angle() が -y (12時) のところを起点とするため。(参照 : d3/d3-shape: Graphical primitives for visualization, such as lines and areas.)

    // circle (overwrite path)
    svg.selectAll("circle")
        .data(nodes.descendants())
        .enter()
        .append("circle")
        .attrs({
            "cx" : node_x,
            "cy" : node_y,
            "r" : node_size/2
        })
        .append("title")
        .text(function(d) { return d.data.name; });

function node_angle(x) {
    return rad(x) - Math.PI/2;
}
function node_x(d) {
    return d.y * Math.cos(node_angle(d.x));
}
function node_y(d) {
    return d.y * Math.sin(node_angle(d.x));
}

描画上のトリック

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

円形レイアウトにしたので、座標系の原点を SVG 領域の真ん中にシフトさせています(センタリング)。縮小するのは前回と同じではみだし防止。

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

覚えておくこと

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