LoginSignup
2

More than 5 years have passed since last update.

D3.jsで作るフォーム1: チェックボックス付き折りたたみツリー

Last updated at Posted at 2018-08-21

動くサンプルがObservableにあります。D3.jsはversion 5です。
https://beta.observablehq.com/@mojaie/collapsible-tree-with-checkboxes-d3-js

(2018/09/05追記)
やや改良したバージョンのものをObservableに置きました。
https://beta.observablehq.com/@mojaie/d3-js-collapsible-tree-with-checkboxes-revised

スクリーンショット 2018-08-21 9.35.44.png

元データ。{id: 'root'}は仮想の最上階層です。

data = [
  {id: 'root'},
  {id: 'Hominidae', parent: 'root'},
  {id: 'Ponginae', parent: 'Hominidae'},
  {id: 'Pongini', parent: 'Ponginae'},
  {id: 'Pongo', parent: 'Pongini'},
  {id: 'P. pygmaeus', parent: 'Pongo'},
  {id: 'P. abelii', parent: 'Pongo'},
  {id: 'P. tapanuliensis', parent: 'Pongo'},
]

HTML部分。Checked nodesのところに選択中の項目が表示されます。

<script src="https://d3js.org/d3.v5.js"></script>
<div>Checked nodes: <span id="selected"></span></div>
<div id="view"></div>

ツリーの大枠を描画するtree関数

  function tree(selection) {
    selection
        .classed('viewport', true)
        .style('overflow-y', 'scroll')
        .style('height', '500px')
      .append('div')
        .classed('body', true)
        .style('transform', 'scale(1.5)')
        .style('transform-origin', 'top left');
  }
  • ツリーの中身はupdateTreeで描画します。元データに追加や削除などの変更があった場合は、updateTreeでツリーの項目を再描画します。
  • d3.stratifyはd3-hierarchyモジュールに含まれます。上記元データのようなid, parentで記述された親子関係のレコードをツリー状のデータ構造に変換します。
  function updateTree(selection, items) {
    const root = d3.stratify()
      .id(d => d.id)
      .parentId(d => d.parent)(items);
    selection.select('.body')
        .call(nextLevel, root);
    // Remove dummy root node
    selection.select('.body > span').remove();
    selection.select('.body > ul').style('padding-left', 0);
  }
  • nextLevel関数によって、ツリー構造のデータを再帰的にたどってツリーの各項目を描画します。
  • 元データが変更された場合、exit-enterにより差分のDOMのみが描画されます。
  • 子ノードがあるノードは、展開、折りたたみを切り替えるクリックイベントを設置します。
  function nextLevel(selection, node) {
    const label = selection.append('span');
    const arrow = label.append('span').classed('arrow', true);
    label.call(renderNode, node.data);
    if (!node.hasOwnProperty('children')) return;
    const items = selection.append('ul')
        .style('list-style-type', 'none')
      .selectAll('li')
        .data(node.children, d => d.id);
    items.exit().remove();
    items.enter()
      .append('li').merge(items)
        .each(function (d) {
          d3.select(this).call(nextLevel, d);
        });
    label.select('.arrow')
        .text('')
        .on('click', function () {  // Collapse on click
          const childList = selection.select('ul');
          if (!childList.size()) return;
          const expanded = childList.style('display') !== 'none';
          d3.select(this).text(expanded ? '' : '');
          childList.style('display', expanded ? 'none' : 'inherit');
        });
  }

renderNode関数で各ノードの描画を行います。チェックボックスが変更された際は、checkboxValues関数でチェックされている全ノードのIDを取得し、HTMLのChecked nodesの部分に反映します。

  function renderNode(selection, rcd) {
    selection.append('input')
        .attr('type', 'checkbox')
        .on('change', function () {
          d3.select('#selected')
              .text(checkboxValues(d3.select('#view')));
        });
    selection.append('span')
        .text(rcd.id);
  }

  function checkboxValues(selection) {
    return selection.select('.body')
       .selectAll('input:checked').data().map(d => d.id);
  }

ツリーを出力します。一度ツリーを出力した後、元データに変更があった場合はupdateTreeだけを実行します。

  d3.select('#view').append('div')
      .call(tree)
      .call(updateTree, data);

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2