Posted at

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

More than 1 year has passed since last update.

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