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 とかやってるわけじゃないので、もうちょっといい方法もあるかもしれません。