HogeFugaHogera

IT系の備忘録とか、他徒然なるままに

Let’s Make a Bar Chart part 3の邦訳[D3.js]

棒グラフを作ろう その3

November 13, 2013Mike BostocのLet’s Make a Bar Chart, IIIを邦訳したもの。part 2はコチラ。 今回の翻訳は意訳が多いので、ご注意ください。

このチュートリアルの前のパートで、HTMLでの基礎的なグラフを作り、次にSVGを使って作った。今回は、グラフを回転して列にし、軸を追加する事により表示を改良する。現実的なデータセットにも切り替え、英語における各文字の相対的出現頻度を表示する。

Bar Chart
データ元: Cryptological Mathematics, Robert Lewand.

回転して列にする

棒グラフを回転させて列のグラフにする事は、ほぼxyを入れ替える事である。しかしながら、それに付随して幾つか小さな変更が必要となる。これは、ggplot2のような高レベルな可視化文法を使用しないで、SVGを直接操る事による負債である。しかし一方では、SVGはカスタマイズ性を提供し、それはSVGWeb標準であるので、要素を調査するみたいにブラウザの開発者ツールを使用できるし、可視化以上のことにSVGを使用できる。

x scaleからy scaleに名前を変更する際に、rangeは[0, width]よりも[height, 0]とする。これは、SVG座標系の原点は左上の角だからである。値0はグラフの上部よりも下部を示す方が望ましい。同様に、これは"y"と"height"属性の設定によって、棒の"rect(長方形)"の位置を決める必要があるという意味であり、一方以前は"width"を設定するだけで十分であった ("x"属性のデフォルト値は0であり、前の棒は左寄せであった)。

以前は、固定した高さの棒を作る為に、var barHeightにデータポイント(0, 1, 2, ...)の数を掛けた。得られたグラフの高さはデータセットの大きさに従う事になった。しかし、ここではったく逆の振る舞いを求められる。グラフの横幅を固定し、棒の横幅を可変とする。故にbarHeightではなく、ここでは、データセットの大きさをdata.lengthで取得し、得られたグラフの横幅を割ることにより、baeWidthを算出する。

最後に、棒のラベルは棒それ自体にではなく列の別の箇所、列の頂点の真下に作られるべきである^1。新しい"dy"属性の".75em"という値は、文字のベースラインではなくcap heightでラベルの位置を固定する。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.chart rect {
  fill: steelblue;
}

.chart text {
  fill: white;
  font: 10px sans-serif;
  text-anchor: middle;
}

</style>
<svg class="chart"></svg>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>

var width = 960,
    height = 500;

var y = d3.scale.linear()
    .range([height, 0]);

var chart = d3.select(".chart")
    .attr("width", width)
    .attr("height", height);

d3.tsv("data.tsv", type, function(error, data) {
  y.domain([0, d3.max(data, function(d) { return d.value; })]);

  var barWidth = width / data.length;

  var bar = chart.selectAll("g")
      .data(data)
    .enter().append("g")
      .attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });

  bar.append("rect")
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); })
      .attr("width", barWidth - 1);

  bar.append("text")
      .attr("x", barWidth / 2)
      .attr("y", function(d) { return y(d.value) + 3; })
      .attr("dy", ".75em")
      .text(function(d) { return d.value; });
});

function type(d) {
  d.value = +d.value; // coerce to number
  return d;
}

</script>

順序データの変換

数として比較 (減算や除算) できる数量的な値と異なり、順序データは階級によって比較される。文字は順序を持ち、アルファベットにおいては、AはBの前に存在し、BはCの前に存在する。D3のlinear (線形)、pow (指数)、log (対数) は数量データを変換する為に働くが、Ordinal-Scalesは順序データを変換する。すなわち、文字をよって簡単に棒の位置を決める為にordinal scaleを使用できる。

大抵の明示的形式では、ordinal scaleは、データ値の離散集合 (例えば名前) を、それに対応する表示値の離散集合 (例えばピクセル位置) に関連付ける。数量scaleと同様、これらセットはそれぞれdomainrangeと呼ばれる。

var x = d3.scale.ordinal()
    .domain(["A", "B", "C", "D", "E", "F"])
    .range([0, 1, 2, 3, 4, 5]);

例を短くするため、アルファベットの最初の6文字しか示していない。下の完全のコードでは、データからscaleのdomeinを計算し、そこには全てのアルファベットが含まれる。

x("A")の結果は0、x("B")は1、以下同様となる。domainとrangeを記述する際には、値の順番だけが重要であり、domain中の要素iはrange上の要素iに関連付けられる。

手で各棒の位置を入力するのはうんざりだろうから、代わりにrangeBandsrangePointsを使って連続的な値を離散集合の値に変換できる。rangeBands関数は、棒グラフの場合にグラフを等間隔、等幅に分割するためにrange値を算出する。同様にrangePoints関数は、散布図の場合に等間隔のrange値を算出する。例えば:

var x = d3.scale.ordinal()
    .domain(["A", "B", "C", "D", "E", "F"])
    .rangeBands([0, width]);

width960なら、ここではx("A")0x("B")160、以下同様となる。この位置は各棒の左端を位置を示し、一方x.rangeBand()は各棒の横幅を返す。だが、rangeBandsは任意のその他引数で棒の間にpaddingを追加することができる。また、rangeRoundBandsの差異は、SVGの輪郭をくっきりさせる (アンチエイリアスしない) ために、ピッタリと正確なピクセル境界位置を算出する点である。

全てのrangeはx.range()で取得できる。上の状態で実行すれば、[0,160,320,480,640,800]が返る。

var x = d3.scale.ordinal()
    .domain(["A", "B", "C", "D", "E", "F"])
    .rangeRoundBands([0, width], .1);

今、x("A")17で、各棒の幅は141ピクセルとなる。また、このようにdomainの手で書いた文字ではなく、array.maparray.sortを使用してデータからそれらを算出できる。まとめると:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.chart rect {
  fill: steelblue;
}

.chart text {
  fill: white;
  font: 10px sans-serif;
  text-anchor: middle;
}

</style>
<svg class="chart"></svg>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>

var width = 960,
    height = 500;

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);

var y = d3.scale.linear()
    .range([height, 0]);

var chart = d3.select(".chart")
    .attr("width", width)
    .attr("height", height);

d3.tsv("data.tsv", type, function(error, data) {
  x.domain(data.map(function(d) { return d.name; }));
  y.domain([0, d3.max(data, function(d) { return d.value; })]);

  var bar = chart.selectAll("g")
      .data(data)
    .enter().append("g")
      .attr("transform", function(d) { return "translate(" + x(d.name) + ",0)"; });

  bar.append("rect")
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); })
      .attr("width", x.rangeBand());

  bar.append("text")
      .attr("x", x.rangeBand() / 2)
      .attr("y", function(d) { return y(d.value) + 3; })
      .attr("dy", ".75em")
      .text(function(d) { return d.value; });
});

function type(d) {
  d.value = +d.value; // coerce to number
  return d;
}

</script>

余白を整える

Ordinal scaleはしばしば、D3の軸属性にに素早くラベル付き目盛りを表示するために使われ、グラフの見やすさを改善する。しかし、軸を追加する前に、余白のスペースを空ける必要がある。

余白の規定より、D3の余白は、top,reight,bottom,left要素で構成されるオブジェクトとして書かれる。次に、グラフ領域(余白を含む)の外側の大きさは、余白分を差し引くことにより、グラフの絵がある内側の大きさを計算するために使用される。 例えば、960✕500のグラフの適切な値は:

var margin = {top: 20, right: 30, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

HTMLと異なり、SVGの表示範囲は暗黙でoverflow: hidden (はみ出したSVGコンテンツは表示されない) が規定される。IE9overflow: visible (はみ出したSVGコンテンツも表示する) であり、他のブラウザはoverflow: hiddenのため、暗黙の指定に頼るべきではない。

従って、960と500が、それぞれ外側の幅と高さであり、一方、計算された内側の幅と高さは、090と450になる。これら内側の範囲はscaleの初期設定で使用する事が出来る。SVGコンテナに余白を適用するためには、SVG要素の幅と高さに外側の範囲を設定し、top-leftの余白要素でグラフエリアの原点を相殺するためにg要素を追加する。

var chart = d3.select(".chart")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

その後にグラフに追加される要素は、この余白を継承する。

軸の追加

軸とx-scaleを結びつけ、上下左右のどれか1つの位置を指定する事で、軸を定義する。x-軸を棒の下に表示するため、ここではbottomの位置を使用する。

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

返ってくるxAxisオブジェクトは、selection.callを使用して反復伝播することで、複数の軸を描画するために使用される。この事は、必要ならばどの軸にも押せるゴム印のようなものと考えれる。軸要素は局所的な原点に対して相対的に配置されるので、望んだ位置に変換する為には、g要素内でtransform属性をセットする。

chart.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

軸コンテナは、スタイルを適用する為のクラス名も持つ。axisという名前は、ここでは任意であるので、好きな様に呼んでよい。複数のクラス名 (例えばx axis) は、複数の軸にまたがる共通のスタイルと異なるスタイルを軸に設定するのに役立つ。

軸要素は、domainを表示するpaht要素と、各目盛マークに対応する複数.tickクラスのを持つg要素で構成される。目盛りはどれも、textラベルとlineマークで構成される。大抵のD3の例では、従って、以下の簡素なスタイルを使用する:

.axis text {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

これは、Rのような軸となる:

しかしながら、軸は更にカスタマイズできる。これは更に凝った軸で、ggplot2風のスタイルである。

スタイルの他に、軸が作られた後に、選択した軸の要素とその要素を操作する事によって、軸の見た目を更にカスタマイズできる。軸の要素はそのpublicなAPIの一部である。上のggplot2-style axisは、2つの軸(内部のグラフ領域とその周囲の領域)を上乗せして描画される。主、副軸は別々にスタイルされる。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.bar {
  fill: steelblue;
}

.axis text {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

</style>
<svg class="chart"></svg>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>

var margin = {top: 20, right: 30, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var chart = d3.select(".chart")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.tsv("data.tsv", type, function(error, data) {
  x.domain(data.map(function(d) { return d.name; }));
  y.domain([0, d3.max(data, function(d) { return d.value; })]);

  chart.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  chart.append("g")
      .attr("class", "y axis")
      .call(yAxis);

  chart.selectAll(".bar")
      .data(data)
    .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.name); })
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); })
      .attr("width", x.rangeBand());
});

function type(d) {
  d.value = +d.value; // coerce to number
  return d;
}

</script>

意図の伝達 (Communicating)

チュートリアルの本パート上で、筆者は読書に謝罪すべきだろう。私は読書に対して酷い仕打ちをした。グラフ構造の技術的な詳細な説明をするために努力した上で、効果的な可視化の重要な要素、すなわち効果的な意図の伝達(effective communication)についてもっともらしく説明した。グラフの意図が見る人に対して全く伝わらないなら、その問題は単なるグラフの外観の善し悪しではない! 読者に対し、それを解釈する為の充分な状態を提供するには、グラフにラベルを付けるべきである。

これは、あなたが考えるよりもっと普遍的な問題である。データを可視化する際、データセットやあたなの意図を示す追加の情報をグラフに追加したり、しない事は簡単である。この棒グラフが英語の文字の相対頻度を示す事を、貴方は知っている。しかし、それを明示的に示さない限り、グラフを見る読書には伝わらない。ラベル、キャプション、凡例やその他説明要素は、グラフを理解するためにとても重要である。タイトルはtext要素をy-axis追加することによりに、望んだ位置に配置される。

chart.append("g")
    .attr("class", "y axis")
    .call(yAxis)
  .append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .text("Frequency");

単位に対する適切な数値の書式設定は、データの表示をより見やすくする。このグラフの表示は相対頻度なので、パーセント (%) 表示が、0~1で表示するデフォルトの振る舞いより適している。axis.ticksの第2引数であるformat stringは目盛りの書式設定を変更し、適切な精度の軸間隔を自動的に選択する。

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(10, "%");

書式設定を完全に制御するには、代わりにaxis.tickFormat関数を使用する。

Next: Part 4

The code for part 3 of the tutorial is available at bl.ocks.org/3885304.

本シリーズの次のチュートリアルでは、表示間の相互作用と遷移をカバーする。次のセクションが公開された事を知らせる為に、筆者(原著者)のTwitterをフォローしてほしい。