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

準備

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  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.innerWidthwindow.innerHeightで画面サイズを取得し、SVGのサイズに設定しました。
軸を設定する場合を想定して適当にマージンを設定し、スケールのレンジに設定します。

<g>要素を使わないと

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

1
2
3
4
5
6
7
8
9
  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>要素で値を表示したい場合はどのような方法があるでしょうか。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  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

新しいタブで開く

<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()は従来通り書けば大丈夫です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<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>の属性を操作する必要がありません。rowstransform属性だけを再設定してあげればよいのです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  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

新しいタブで開く

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

1
2
3
4
5
6
7
8
  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で表示したものです。

広告