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.innerWidth
とwindow.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>
の属性を操作する必要がありません。rows
のtransform
属性だけを再設定してあげればよいのです。
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で表示したものです。