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

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

## 例

以下のような配列から、

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

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

<img width="828" alt="playground03.png" src="https://qiita-image-store.s3.amazonaws.com/0/154454/47541615-d101-8d7a-464b-8142406dbaa4.png">

まず初めに配列`data`から要素のグループ化を行なう`<g>`要素を生成します。  
ここで生成した`<g>`要素にバインドされるデータは`data[i]`、すなわち`{name: 'name-00', values: [20, 10, ...]}`という内容のオブジェクトです。

```JavaScript
  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>`を生成します。

```JavaScript  
  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は以下のようになります。

```html
  <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>`要素にバインドされたデータ`v`は`data[i].values[idx]`、すなわち`20`や`10`のような数値データのみなので、名前のプロパティを持ちません。

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

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

## 解決法

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

```JavaScript
  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>`に`xScale`と`yScale`で座標を与えることで同じようなチャートが作成できます。  
階層構造のDOMを作成する方法は慣れるまでは理解しづらいかもしれませんが、慣れるとDOMの扱いがわかりやすくなります。

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

- [Example][blocks]

[gist]: //gist.github.com/cieloazul310/d21f955557b0262c3d7372bd3647e033
[blocks]: //bl.ocks.org/cieloazul310/d21f955557b0262c3d7372bd3647e033