d3.js

D3.jsで子要素のDOMから親要素のデータを取得

D3.jsで階層構造のDOMの子要素から親要素のデータを取得する方法です。

以下のような配列から、

  const data = [
    {name: 'name-00', values: [20, 10, ...]},
    {name: 'name-01', values: [12, 45, ...]},
    ...,
    {name: 'name-17', values: [48, 90, ...]}
  ];

下図のようなチャートを作成します。

playground03.png

まず初めに配列dataから要素のグループ化を行なう<g>要素を生成します。

ここで生成した<g>要素にバインドされるデータはdata[i]、すなわち{name: 'name-00', values: [20, 10, ...]}という内容のオブジェクトです。

  const rows = svg.selectAll('.rows')
                  .data(data)
                  .enter()
                  .append('g')
                  .attr('class', 'rows')
                  .attr('transform', d => `translate(0, ${yScale(d.name)})`)
                  .on('click', (d) => {
                    console.log(d);
                    // => {name: 'name-00', values: [20, 10, ...]}
                  });

<g>要素にバインドしたデータdata[i]のプロパティdata[i].valuesの配列を使用して、<g>要素の子要素にdata[i].values.lengthの数だけ<circle>を生成します。

  rows.selectAll(".circle")
      .data(d => d.values)
      .enter()
      .append('circle')
      .attr('class', 'circle')
      .attr('cx', v => xScale(v))
      .attr('cy', 0)
      .attr('r', 4);

子要素のselection.attr(name, value)で使用する関数の引数をvとしています。
vの部分を従来のdと置いても問題なく動作しますが、親要素の関数の引数dとの混同を避けるためにvとしました。

上記の2つのコードで生成されるDOMは以下のようになります。

  <g class="rows" transform="translate(0, 15)">
    <circle class="circle" cx="24" cy="0" r="4"></circle>
    <circle class="circle" cx="12" cy="0" r="4"></circle>
    ...
    <circle class="circle" cx="168" cy="0" r="4"></circle>
  </g>
  <g class="rows" transform="translate(0, 35)">
    <circle class="circle" cx="35" cy="0" r="4"></circle>
    ...
  </g>
  ...

次に、各<circle>要素にイベントを設定します。
「マウスオーバー、クリックで画面右上に名前nameと数値valueを表示する」という簡潔なイベントを考えます。

selection.on(type, function(d, i, nodes) {...})を使いますが、ここで一つ問題があります。
<circle>要素にバインドされたデータvdata[i].values[idx]、すなわち2010のような数値データのみなので、名前のプロパティを持ちません。

  rows.selectAll('.circle')
      .on('click', (v, i, nodes) => {
        console.log(v); // => 14
      });

子要素から親要素のデータにあるプロパティdata[i].nameにアクセスするには工夫が必要です。

解決法

d3.select(nodes[i].parentNode).datum()で親要素のデータを取得できます。

  rows.selectAll('.circle')
      .on('click', (v, i, nodes) => {
        // <circle>要素を選択している
        d3.select(nodes[i]);
        // nodes[i] = <circle class="circle">

        // <circle>要素の親要素である<g>要素を選択している
        d3.select(nodes[i].parentNode);
        // nodes[i].parentNode = <g class="rows">

        // <circle>要素にバインドされたデータを表示
        console.log(v);
        // => 14
        console.log(d3.select(nodes[i]).datum());
        // => 14

        // <circle>要素の親要素である<g>要素にバインドされたデータを表示
        console.log(d3.select(nodes[i].parentNode).datum());
        // => Object {name: 'name-00', values: [14, 11, ...]}

        const text = `${d3.select(nodes[i].parentNode).datum().name}: ${v}min.`;

        svg.append('text')
            .attr('class', 'guide')
            .text(text);
      });

<g><circle>の階層構造のDOM生成がわかりづらかったら、dataを単一の配列に整形し、それぞれの<circle>xScaleyScaleで座標を与えることで同じようなチャートが作成できます。

階層構造のDOMを作成する方法は慣れるまでは理解しづらいかもしれませんが、慣れるとDOMの扱いがわかりやすくなります。

Demos on Bl.ocks

この記事のtipsで作成した図をBl.ocksで表示したものです。