9
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

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

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

準備

  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>要素を使わないで横棒グラフを書く例です。

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

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

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

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

<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属性だけを再設定してあげればよいのです。

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

  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で表示したものです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
9
Help us understand the problem. What are the problem?