📈

【D3.js】 hierarchy で階層ツリーの作成・ノードハイライトの有効

2021/08/27に公開

DXなどの文脈で、データ活用への意識が高まり、データビジュアライゼーションが求められる機会が増えてくるのではないかと勝手に思っております。
特に新しい技術ではありませんが、d3.jsを本格的に学び始めましたので、勉強の復習がてらに書いていきたいと思います。

概要

この記事ではまず、d3のhierarchyAPIを使って、階層図を作成します。
階層図といえばということで、徳川略系図の作成をしました。笑

それから、作成した階層図のノードをクリックすることで、クリックされたノードの親にハイライトを有効にしていきます。

今回のデモ

コードの解説

では、コードの内容を紹介したいと思います。

ツリーレイアウトの作成

ライブラリを利用するとはいえ、結構ソース量が必要だなというのが正直なところでした。
まずは、今回作る階層図のデータを用意します。

データはJSON形式で用意しました。前述のデモリンク内のdata.jsonファイルをご確認ください。
子要素は、親と同配列に子要素の配列を用意してその中に格納していきます。

{
  "name": "家康",
  "children": [
    {
      "name": "信康"
    },
    {
      "name": "秀康"
    },
    {
      "name": "秀忠",
      "children": [
  ...
}

以下のtree関数は、描画するツリーレイアウトを作成します。
まず、d.hierarchyにJSONデータを渡してノード設定を構築します。
次に、d3.treeAPIで構築したノードをツリーレイアウトを作成します。

const tree = (data) => {
  const root = d3.hierarchy(data);
  root.dx = 20;
  root.dy = width / (root.height + 1);
  return d3.tree().nodeSize([root.dx, root.dy])(root);
};

const root = tree(DATA);

nodeSize()は、ノードのサイズを指定します。値を大きくするとノードの親子間、兄弟間が広くなります。
root.heightは子孫ノードの最大数を持ちます。(今回のデータはルートノードから最下位層の子要素まで8つのノードが存在します。

svgタグの生成

d3の描画はsvgで行います。描画するツリーのsvgタグを生成します。

const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

svgタグのサイズも同時に設定します。

パスの生成

svgタグに、ノードを繋ぐパスを追加する形でタグを生成します。
パスのカラーなどのスタイルも合わせて設定を行います。

const link = g
  .append("g")
  .attr("fill", "none")
  .attr("stroke", "#555")
  .attr("stroke-opacity", 0.4)
  .attr("stroke-width", 1.5)
  .selectAll("path")
  .data(root.links())
  .join("path")
  .attr(
    "d",
    d3
      .linkHorizontal()
      .x((d) => d.y)
      .y((d) => d.x)
  );

root.links()は子孫のリンク配列を返します。ソースとターゲットのプロパティが定義され、親子を繋ぐターゲットを取得します。
パスのスタイルは、d3.linkHorizontal()他にも種類があります。

ノードの生成

さらに、ノード描画用のタグを追加します。
見た目を整えるために、フォントの設定や描画位置の調整若干行っています。

const g = svg
  .append("g")
  .attr("font-family", "sans-serif")
  .attr("font-size", 12)
  .attr("transform", `translate(${root.dx * 2}, ${root.dy})`);

ノードのドットと、文字列を描画します。
ドットの形式やサイズを設定しています。root.descendants()関数は子孫ノードの配列を返します。

const node = g
  .append("g")
  .attr("stroke-linejoin", "round")
  .attr("stroke-width", 3)
  .selectAll("g")
  .data(root.descendants())
  .join("g")
  .attr("transform", (d) => `translate(${d.y}, ${d.x})`)

node
  .append("text")
  .attr("x", -5)
  .attr("dy", "0.31em")
  .attr("text-anchor", "end")
  .text((d) => d.data.name)
  .clone(true)
  .lower()
  .attr("stroke", "white");

node.append("circle").attr("fill", "#999").attr("r", 3);

svgを画面へ描画

ここまでで、描画に必要な要素を全て生成しました。
最後に、ブラウザへ追加して画面にツリーを表示します。

document.getElementById("app").append(svg.node());

ここでは、HTMLにappidが既に存在するものとして、そのエレメント内にsvgを追加します。
画面に描画されました!
Image from Gyazo

ノードのハイライトを有効

階層ツリーの作成は完了しました。
次に、ノードをクリックしたときに、クリックされたノードから、最上位の祖先までのパスをハイライトさせていきたいと思います。

ハイライト用のメソッド作成

まず、ノードがクリックされたときに発火させるメソッドを作ります。
メソッドは、クリックされたノードを返し、そのノードから祖先の情報を取得してパスのスタイルを変更させていきます。

node.ancestors()という祖先の配列を返すAPIがあるので、それを活用します。
引数にノードを受け取り、祖先のパスに一致する場合にスタイルを変更させることで実現します。

const clicked = (d) => {
  var paths = d3.selectAll("path");
  var ancestors = d.ancestors();
  paths.style("stroke", "#555").style("stroke-opacity", 0.4);

  paths
    .filter((d) => ancestors.includes(d.target))
    .style("stroke", "red")
    .style("stroke-opacity", 1);
};

※連続してノードがクリックされても良いように、常に初期化するようにしています。

このメソッドをノードに対して追加します。

const node = g
  .append("g")
  .attr("stroke-linejoin", "round")
  .attr("stroke-width", 3)
  .selectAll("g")
  .data(root.descendants())
  .join("g")
  .attr("transform", (d) => `translate(${d.y}, ${d.x})`)
  .on("click", (e, d) => clicked(d)); // ←追加!

完成です!
Image from Gyazo

動作と、全てのソースコードはこちらでご確認ください!

参考

Discussion