D3.js v4/v5 treeを開閉する方法 – サンプル
D3.jsの階層構造を可視化するtreeの折りたたみ動作についてのプログラムデモです。データの準備とデータ構造については、こちらを、treeの基本的な使い方についてはこちらをご覧ください。
プログラムデモ (A, C, Hのノードをクリックしてください)
サンプルコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>D3 collapsible tree in v4/v5</title> <!-- 1. スタイルの設定 --> <style> .node { cursor: pointer; } .node circle { fill: #fff; stroke: steelblue; stroke-width: 1.5px; } .link { fill: none; stroke: #555; stroke-opacity: 0.6; stroke-width: 1.5px; } </style> </head> <body> <svg width="800" height="600"></svg> <script src="https://d3js.org/d3.v5.min.js"></script> <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); root.x0 = height / 2; root.y0 = 0; var tree = d3.tree().size([height, width - 160]); // 4. svgデータの描画用データの変換 g = d3.select("svg").append("g").attr("transform", "translate(80,0)"); update(root); // 5. クリック時の呼び出し関数 function toggle(d) { if(d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } } // 6.svg要素の更新関数 var i = 0; function update(source) { // tree レイアウト位置を計算 tree(root); // 子、孫方向の位置設定 root.each(function(d) { d.y = d.depth * 320; }); // ノードデータをsvg要素に設定 var node = g.selectAll('.node') .data(root.descendants(), function(d) { return d.id || (d.id = ++i); }); // ノード enter領域の設定 var nodeEnter = node .enter() .append("g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .on("click", function(d) { toggle(d); update(d); }); nodeEnter.append("circle") .attr("r", 5) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); nodeEnter.append("text") .attr("x", function(d) { return d.children || d._children ? -13 : 13; }) .attr("dy", "3") .attr("font-size", "150%") .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) .text(function(d) { return d.data.name; }) .style("fill-opacity", 1e-6); // ノード enter+update領域の設定 var nodeUpdate = nodeEnter.merge(node); var duration = 500; nodeUpdate.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); nodeUpdate.select("circle") .attr("r", 8) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); nodeUpdate.select("text") .style("fill-opacity", 1); // ノード exit領域の設定 var nodeExit = node .exit() .transition() .duration(duration) .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) .remove(); nodeExit.select("circle") .attr("r", 1e-6); nodeExit.select("text") .style("fill-opacity", 1e-6); // リンクデータをsvg要素に設定 var link = g.selectAll(".link") .data(root.links(), function(d) { return d.target.id; }); // リンク enter領域のsvg要素定義 var linkEnter = link.enter().insert('path', "g") .attr("class", "link") .attr("d", d3.linkHorizontal() .x(function(d) { return source.y0; }) .y(function(d) { return source.x0; })); // リンク enter+update領域の設定 var linkUpdate = linkEnter.merge(link); linkUpdate .transition() .duration(duration) .attr("d", d3.linkHorizontal() .x(function(d) { return d.y; }) .y(function(d) { return d.x; })); // リンク exit領域の設定 link .exit() .transition() .duration(duration) .attr("d", d3.linkHorizontal() .x(function(d) { return source.y; }) .y(function(d) { return source.x; }) ) .remove(); // 次の動作のために現在位置を記憶 node.each(function(d) { d.x0 = d.x; d.y0 = d.y; }); } </script> </body> </html> |
解説
1. スタイルの準備
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<style> .node { cursor: pointer; } .node circle { fill: #fff; stroke: steelblue; stroke-width: 1.5px; } .link { fill: none; stroke: #555; stroke-opacity: 0.6; stroke-width: 1.5px; } </style> |
始めにノードとリンクのスタイルを設定しておきます。
2. 描画用のデータ準備
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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. 描画用のデータ変換
1 2 3 4 5 |
root = d3.hierarchy(data); root.x0 = height / 2; root.y0 = 0; var tree = d3.tree().size([height, width - 160]); |
準備したデータを、描画用のデータ構造に変更します。hierarchy用のデータに変更した後、先頭のノードを表すrootに初期位置(x0, y0)を設定しておきます。また、tree用のデータ構造に変更する関数(d3.tree)を呼び出し、後のupdate関数内で用います。
4. svg要素の配置
1 2 |
g = d3.select("svg").append("g").attr("transform", "translate(80,0)"); update(root); |
treeを構成する要素を配置する”g”要素だけを設定し、配置はupdate関数内で行います。update関数の詳細は後述します。
5. クリック時の呼び出し関数
1 2 3 4 5 6 7 8 9 |
function toggle(d) { if(d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } } |
ノードをクリックした際に呼び出す関数を定義します。tree関数は、childrenが定義されているかどうかで末端ノードを判断するため、変数childrenにnullを代入するとそれ以下の構造を切り離すことができます。ノードをもう一度クリックした際に再表示するため、折り畳み時に_children変数を定義してchildrenを退避し、再クリック時に_childrenをchildrenに戻すことで開閉動作を可能にします。
6.svg要素の更新関数
1 2 3 |
var i = 0; function update(source) { } |
update関数を説明します。関数の手前にあるiは、update関数内でノードデータをsvg要素に設定する際に用います。始めはsourceに先頭の要素(root)が代入されて呼び出されます。(61行目)
1 2 3 4 5 |
// tree レイアウト位置を計算 tree(root); // 子、孫方向の位置設定 root.each(function(d) { d.y = d.depth * 320; }); |
まず、tree関数で各ノードの位置を計算します。折りたたみ後も折り畳み前とノードを同じ位置にするため深さ(子、孫)方向の位置を修正します。
1 2 |
var node = g.selectAll('.node') .data(root.descendants(), function(d) { return d.id || (d.id = ++i); }); |
ノードデータをsvg要素に設定します。root.descendants()は入れ子になったtreeのノードを配列に変換する関数です。ここで、data()の第二引数として関数
1 |
function(d) { return d.id || (d.id = ++i); } |
を設定していますが、これはとても重要で、データを割り当てる要素のindexを返り値で設定できるものです。これがないとindexの0から順番にデータが割り当てられるため、折り畳み時に常に配列の最後の要素が折りたたまれる動作となります。初めてupdate関数が呼び出された時はidが設定されていないのでd.id = ++iとして、順番にidを割り振ります。
ノードのenter領域のsvg要素を設定していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// ノード enter領域の設定 var nodeEnter = node .enter() .append("g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .on("click", function(d) { toggle(d); update(d); }); nodeEnter.append("circle") .attr("r", 5) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); nodeEnter.append("text") .attr("x", function(d) { return d.children || d._children ? -13 : 13; }) .attr("dy", "3") .attr("font-size", "150%") .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) .text(function(d) { return d.data.name; }) .style("fill-opacity", 1e-6); |
始めに”g”要素を設定し、その中に”circle”と”text”要素を設定します。enter領域では、一旦クリックした要素(source)位置にノード位置を設定した後、update領域を含めて所定の位置にノードを移動させます。こうすることで、treeを開くようなアニメーション動作が可能になります。はじめてupdate関数を呼び出した際はすべてのノードが先頭ノード位置に一旦設定されます。また、”g”要素にはclickイベントを設定し、クリックした際にもう一度update関数が呼び出してtreeを更新します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// ノード enter+update領域の設定 var nodeUpdate = nodeEnter.merge(node); nodeUpdate.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); nodeUpdate.select("circle") .attr("r", 8) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); nodeUpdate.select("text") .style("fill-opacity", 1); |
一旦クリック位置に設定したノードを、更新するtree位置に変更します。enter領域とupdate領域の双方を更新したいので、
1 |
var nodeUpdate = nodeEnter.merge(node); |
として双方をマージします。
1 2 3 |
.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); |
はアニメーションを指定する関数で、durationで設定した値の1/1000秒かけて、次のattrで指定した位置に移動します。
次に折り畳み時にノードを消す動作(exit領域)を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 |
var nodeExit = node .exit() .transition() .duration(duration) .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) .remove(); nodeExit.select("circle") .attr("r", 1e-6); nodeExit.select("text") .style("fill-opacity", 1e-6); |
今度はクリックしたノード(source)に徐々に移動するように設定します。”circle”と”text”も徐々に小さく、薄くなるように設定し、消えていくようなアニメーションにします。
1 2 3 |
// リンクデータをsvg要素に設定 var link = g.selectAll(".link") .data(root.links(), function(d) { return d.target.id; }); |
リンクも同様に設定していきます。root.links()はtreeのリンクを{source:node, target:node}の形で配列に変換する関数です。リンクのデータ割り当て時も、tree開閉時に整合を取ることを考え、子(target)のidをindexとして指定します。
リンクのsvg要素を設定していきます。
1 2 3 4 5 6 |
// リンク enter領域のsvg要素定義 var linkEnter = link.enter().insert('path', "g") .attr("class", "link") .attr("d", d3.linkHorizontal() .x(function(d) { return source.y0; }) .y(function(d) { return source.x0; })); |
ここで、ポイントはsvg全体の描画領域のgの直後に’path’を挿入し、ノードのsvg要素よりも前に’path’を設定することです。svg要素は後ろに配置されたものが画面の上に描画されるため、’path’よりも’circle’が上に表示され、treeがきれいに描画できます。
また、’path’の”d”に設定している
1 2 3 |
d3.linkHorizontal() .x(function(d) { return source.y0; }) .y(function(d) { return source.x0; }) |
はtreeで用いる”path”用の水平方向の三次スプライン曲線を算出してくれる関数です。
通常は、155-157行目のように
1 2 3 |
d3.linkHorizontal() .x(function(d) { return d.y; }) .y(function(d) { return d.x; }) |
の形で用いますが、ノードと同様に、一旦クリックしたノード(source)上に座標を配置した後、所定の位置に移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// リンク enter+update領域の設定 var linkUpdate = linkEnter.merge(link); linkUpdate .transition() .duration(duration) .attr("d", d3.linkHorizontal() .x(function(d) { return d.y; }) .y(function(d) { return d.x; })); // リンク exit領域の設定 link .exit() .transition() .duration(duration) .attr("d", d3.linkHorizontal() .x(function(d) { return source.y; }) .y(function(d) { return source.x; }) ) .remove(); |
ノードと同様にリンクのupdate、exit領域を設定します。
1 2 3 4 |
node.each(function(d) { d.x0 = d.x; d.y0 = d.y; }); |
最後に、更新した位置を記憶しておきます。これにより、tree開閉時に徐々に移動する動作が可能になります。
まとめ
treeの開閉は少し難しいですが利用シーンの多いモジュールかと思います。少しでも活用していただければ幸いです。