動くサンプルが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
元データ。{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);