D3.jsで<g>要素を活用する

D3.jsでSVGを描写するときは`<g>`要素を使って要素をグループ化すると便利です。

- [グループ化してない棒グラフのデモページ][block1]
- [グループ化した棒グラフのデモページ][block2]

## 準備

```JavaScript
  const nameFormat = d3.format('02d');
  const data = d3.shuffle(d3.range(0, 20)).map((d, i) => {
    return {
      name: `name-${nameFormat(d)}`,
      value: Math.round(Math.random() * 100),
    };
  });
  const size = {
    width: window.innerWidth, 
    height: window.innerHeight
  };
  const margin = {top: 10, right: 10, bottom: 10, left: 40};
  const svg = d3.select('#chart')
                .attr('width', size.width)
                .attr('height', size.height);
  
  const xScale = d3.scaleLinear()
                    .domain([0, 100])
                    .range([margin.left, size.width - margin.right]);
  const yScale = d3.scaleBand()
                    .domain(data.map(d => d.name))
                    .range([margin.top, size.height - margin.bottom])
                    .padding(0.2);
  const yAxis = d3.axisLeft(yScale);
```

上はごくごく普通のD3.jsの初期設定です。  
20個のオブジェクトが入った適当な配列を作成しました。`d3.range()`で20個のデータが入った配列を作成し、`d3.shuffle()`でシャッフルしたものを`array.map()`でオブジェクトに変換しています。

`window.innerWidth`と`window.innerHeight`で画面サイズを取得し、SVGのサイズに設定しました。  
軸を設定する場合を想定して適当にマージンを設定し、スケールのレンジに設定します。

## `<g>`要素を使わないと

ここに、上で準備したデータの横棒グラフを作成します。
以下のコードは`<g>`要素を使わないで横棒グラフを書く例です。

```JavaScript
  const bars = svg.selectAll('.bars')
                  .data(data)
                  .enter()
                  .append('rect')
                  .attr('class', 'bars')
                  .attr('x', xScale(0))
                  .attr('y', d => yScale(d.name))
                  .attr('width', d => xScale(d.value) - xScale(0))
                  .attr('height', yScale.bandwidth());
```

特に情報を加えたり、イベントなどを与えない場合は、これでいいと思います。
では、この棒グラフの中に補助的に`<text>`要素で値を表示したい場合はどのような方法があるでしょうか。

一例として以下のコードが考えられます。

```JavaScript
  const texts = svg.selectAll('.texts')
                    .data(data)
                    .enter()
                    .append('text')
                    .attr('class', 'texts')
                    .attr('x', xScale(0))
                    .attr('y', d => yScale(d.name) + yScale.bandwidth() / 2)
                    .attr('dx', '.5em')
                    .attr('dy', 1)
                    .attr('text-anchor', 'start')
                    .attr('dominant-baseline', 'middle')
                    .text(d => d.value);
```

#### [Example 1][block1]

`<text>`要素に、一度使用したデータ`data`を新たにバインドしました。これでもいいのですが、`<rect>`と`<text>`の両方に`.data(data).enter()`や`.attr('y', d => yScale(d.name))`が出てくるのは少し無駄に感じられます。


## `<g>`要素でグループ化しよう

SVGの要素をグループ化する`<g>`要素を使います。

`<rect>`要素で棒グラフを作るのと同じ要領で、まずデータをバインドした`<g>`要素を生成し、それらを`.attr('transform', 'translate(X, Y)')`で動かします。今回は`rows`ということで`yScale`を使って縦位置だけ動かしましたが、ここで横位置を左マージン`margin.left`または`xScale(0)`だけ動かしてもいいかもしれません。（個人的にはここで横位置は動かしたくない派です。）

次に`selection.append(type)`で`<g>`の子要素として`<rect>`や`<text>`を生成します。  
親要素が`transform`属性で縦位置に動かしてあるので、これら子要素に与える位置`x`, `y`は親要素からの相対位置だけで書くことができます。

また、`selection.append(type)`で生成した子要素は親要素のデータを継承します。従って、`<rect>`の`width`や、`<text>`の`.text()`は従来通り書けば大丈夫です。

```JavaScript
const rows = svg.selectAll('.rows')
                .data(data)
                .enter()
                .append('g')
                .attr('class', 'rows')
                .attr('transform', d => `translate(0, ${yScale(d.name)})`);
// `translate(${xScale(0)}, ${yScale(d.name)})`としてもいいかもしれない
// xScale(0)はmargin.leftと同じ値

rows.append('rect')
    .attr('class', 'bars')
    .attr('x', xScale(0)) // 親要素からの相対位置
    .attr('y', 0) // 親要素からの相対位置
    .attr('width', d => xScale(d.value) - xScale(0)) // 従来通り
    .attr('height', yScale.bandwidth()); // 従来通り

rows.append('text')
    .attr('class', 'texts')
    .attr('x', xScale(0)) // 親要素からの相対位置
    .attr('y', yScale.bandwidth() / 2) // 親要素からの相対位置
    .attr('dx', '.5em') // 細かな位置はdx, dyで調整
    .attr('dy', 1)
    .attr('text-anchor', 'start')
    .attr('dominant-baseline', 'middle')
    .text(d => d.value); // 従来通り
```

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

```html
<g class="rows" transform="translate(x, y)">
  <rect class="bars" x="x" y="0" width="width" height="bandwidth"></rect>
  <text class="texts" x="x" y="bandwidth/2" dx=".5em" dy="1" text-anchor="start" dominant-baseline="middle">value</text>
</g>
<g class="rows" transform="translate(x, y)">
  <rect class="bars" x="x" y="0" width="width" height="bandwidth"></rect>
  <text class="texts" x="x" y="bandwidth/2" dx=".5em" dy="1" text-anchor="start" dominant-baseline="middle">value</text>
</g>
<g class="rows" transform="translate(x, y)">
  <rect class="bars" x="x" y="0" width="width" height="bandwidth"></rect>
  <text class="texts" x="x" y="bandwidth/2" dx=".5em" dy="1" text-anchor="start" dominant-baseline="middle">value</text>
</g>
```

また`<g>`要素を使う利点として、イベントが扱いやすくなります。
例えば、棒グラフを並び替えたいときに、いちいち`<rect>`や`<text>`の属性を操作する必要がありません。`rows`の`transform`属性だけを再設定してあげればよいのです。

```JavaScript
  rows.on('click', (d, i, nodes) => {
    
    console.log('click event!');
    
  }).on('mouseover touchstart', (d, i, nodes) => {
    
    console.log('mouseover!');
    
  }).on('mouseout touchend', (d, i, nodes) => {
    
    console.log('mouseout!');
    
  });
```

`<g>`要素に`selection.on(type, listener)`でイベントを設定することによって、子要素`<rect>`と`<text>`の両方がイベントを発火する要素になります。
先ほどの`<g>`要素を使わない例では、`<text>`にマウスが乗るとイベントが解除されてしまいます。
`<g>`要素でグループ化することで、そういったムズムズする挙動を回避することができます。

今回の例では利点を感じることは少ないかもしれませんが、例えばマウスオーバーでポップオーバーを表示する場合などは利点がよくわかると思います。

#### [Example 2][block2]

余談ですが`<g>`要素はレイヤーとして使うこともできます。レイヤーとして扱うことで各要素の重ね順がわかりやすくなります。大変便利です。

```JavaScript
  const axisLayer = svg.append('g')
                        .attr('class', 'axis-layer');
  
  const baseLayer = svg.append('g')
                        .attr('class', 'base-layer');
  
  const overlay = svg.append('g')
                        .attr('class', 'overlay-layer');
```

## Demos on Bl.ocks

この記事で作成したデモページを[Bl.ocks](https://bl.ocks.org/)で表示したものです。

- [Example1][block1]
- [Example2][block2]

<!-- References -->
[block1]: https://bl.ocks.org/cieloazul310/0899634796f25696571feba55add9c0d
[block2]: https://bl.ocks.org/cieloazul310/0b08e8374cae0657649ce0f3ca66116b