HogeFugaHogera

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

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

棒グラフを作ろう その2

November 6, 2013Mike BostocのLet’s Make a Bar Chart, IIを邦訳したもの。part 1はコチラ


前章では、HTMLを使った基礎的な棒グラフの作り方をカバーした。ここでは、Scalable Vector Graphics(SVG)を使った例を展開し、タブ区切り形式 (tab-separated values: TSV) の外部データを読み込む事により、更に現実的なSVGのグラフを作製する。

SVGとは

HTMLは通常四角形の形状をとるが、SVGベジェ曲線、グラデーション、クリッピングやマスクなどの強力な基礎的描画機能を使用できる。低レベルな棒グラフに対してSVGの多くの機能は全然必要ないだろうけれど、SVGを覚えれば、可視化を考える必要がある場合、その表現手法の引き出しとして役立つだろう。

何事もそうだが、この利点には当然対価がある。長大なSVG仕様にはやる気をなくすだろうけど、始めから全ての機能を理解する必要はない。サンプルを見ることは、新しいテクニックを得る為の楽しい方法である。

デスクトップアプリケーション (例えば、Adobe Illustrator) からSVGを直接書き出す事ができるため、テータと手描きの絵を組み合わせたハイブリッドな手法をとることもできる。

また、SVGとHTMLは明確に違うものであるが、多くの似通った共通点がある。SVGマークアップを書いて、Webページに直接組み込める (<!DOCTYPE html>が前もって必要)。ブラウザの開発者ツールを使ってSVG要素を調べる事ができる。また、SVG要素はCSSで装飾される。ただし、HTMLは異なり、SVG要素はコンテナの左上から相対的に位置が指定される。それに、フローレイアウトやテキストの折り返しもサポートされていない。

グラフの作製 (手動)

JavaScriptを使って図を作る前に、SVG上での静的仕様を再考しよう。

<!DOCTYPE html>
<style>

.chart rect {
  fill: steelblue;
}

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

</style>
<svg class="chart" width="420" height="120">
  <g transform="translate(0,0)">
    <rect width="40" height="19"></rect>
    <text x="37" y="9.5" dy=".35em">4</text>
  </g>
  <g transform="translate(0,20)">
    <rect width="80" height="19"></rect>
    <text x="77" y="9.5" dy=".35em">8</text>
  </g>
  <g transform="translate(0,40)">
    <rect width="150" height="19"></rect>
    <text x="147" y="9.5" dy=".35em">15</text>
  </g>
  <g transform="translate(0,60)">
    <rect width="160" height="19"></rect>
    <text x="157" y="9.5" dy=".35em">16</text>
  </g>
  <g transform="translate(0,80)">
    <rect width="230" height="19"></rect>
    <text x="227" y="9.5" dy=".35em">23</text>
  </g>
  <g transform="translate(0,100)">
    <rect width="420" height="19"></rect>
    <text x="417" y="9.5" dy=".35em">42</text>
  </g>
</svg>

これまで通り、スタイルシートは色とその他装飾用属性をSVGに適用している。しかし、フローレイアウトを使用して暗黙的にポジショニングをするdiv要素とは異なり、SVG要素は原点を基準に解釈したハードコーディングで絶対指定しなければならない。

SVGで混乱する共通点は、属性として記すべきプロパティと、スタイルとして設定できるプロパティとの間に目立つ。スタイリングプロパティの全一覧は仕様として文書化されているが、簡単かつ大まかには、幾何 (例: rect要素のwidth) は属性として記されるべきで、一方の装飾 (例: fill color)はスタイルとして記述できる。属性は何に対しても使用できるが、著者としては装飾に対してはスタイルを使用する事をお勧めする。これだと、組み込みスタイルがCSSと協調して問題なく振る舞う。 

SVGでは、text要素上で明示的に位置を指定をする文字列が必要である。text要素はパディングやマージンをサポートしないため、(この棒グラフでSVGを使用する場合^1) 文字列は棒の先端から3ピクセルのオフセットが必要となり、またdyオフセットが文字列を縦方向にセンタリングする為に使われる。

前章と比べ実装が大きく変化したにも関わらず、出力されるグラフは以前と同じである。

グラフの作製 (自動)

次はD3を使用してグラフを作ろう。もう、このコードの一部はお馴染だろう。

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

.chart rect {
  fill: steelblue;
}

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

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

var data = [4, 8, 15, 16, 23, 42];

var width = 420,
    barHeight = 20;

var x = d3.scale.linear()
    .domain([0, d3.max(data)])
    .range([0, width]);

var chart = d3.select(".chart")
    .attr("width", width)
    .attr("height", barHeight * data.length);

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

bar.append("rect")
    .attr("width", x)
    .attr("height", barHeight - 1);

bar.append("text")
    .attr("x", function(d) { return x(d) - 3; })
    .attr("y", barHeight / 2)
    .attr("dy", ".35em")
    .text(function(d) { return d; });
    
</script>

データセットの大きさ (data.length)を基に高さを算出するために、JavaScript上でsvg要素のサイズ (var width, barHeight) を設定する。この方法では、グラフの大きさを全体の高さではなくそれぞの棒の高さを基準にし、各棒にラベルを書く為の充分なスペースが確保できる。

それぞれの棒は、recttextからなるg要素によって成り立つ。各データポイントに対応するg要素を作る為にdata join (enter selection)を使用する。次に、g要素を縦方向に動かし、棒と棒に対応するラベルの位置に対するローカルな原点を作製する。

当然ながら、一つのg要素に付き一組のrecttext要素があり、data joinを再度行わずにg要素に直接それら要素を追加する。Data joinは、データを基にした出生数分の変数を作る時のみ使用される。ここでは、1つの親に対して1つの子を追加している。追加したrecttextは親要素であるgからデータを継承するので、棒の幅とラベル位置を算出するためにデータを使用することができる。

データの読み込み

区切りファイルからデータセットを読み込む、より現実的なグラフを作ってみよう。外部データファイルは、データからグラフの実装を分けて考えるので、複数のデータセットや時間的に変化するライブデータでも再利用しやすい形で作る。

タブ区切り値 (Tab-separated values: TSV)は、使いやすい表状のデータ形式である。この形式は、エクセルや他の表計算ソフトから書き出したり、テキストエディターを使って手作業で作る事もできる。各行は表における行を表し、それはタブで区切られた複数の列により構成される。最初の行はヘッダー列であり、列名を明記する。我々のデータセットは単純な数字の配列であったけれど、ここでデータを示す名前を加える。今、データはこの様に見える。

name value
Locke   4
Reyes   8
Ford    15
Jarrah  16
Shephard    23
Kwon    42

このデータを手作業で書ているなら、区切り文字 (TSVならタブ、CSVならカンマ)、改行に注意し、二重引用符を二重引用符でエスケープするのを忘れない事!

Webブラウザ上でこのデータを使用するためには、webサーバからファイルをダウンロードしてパース (parse)し、ファイルの文字列をJavaScriptで使用できるオブジェクトに変換する必要がある。幸いにも、これら2つの行程はd3.tsvという1つの関数で対応できる。

データ読み込みは新たな複雑さを招く。それは、ダウンロードは非同期という事である。d3.tsvを呼ぶと、バックグラウンドでファイルをダウンロードする一方、即座に返り値を返す。ダウンロードが完了すると、新しいデータと共にコールバック関数が呼ばれるが、ダウンロードに失敗した場合はエラーが呼ばれる。すなわち、コードは順不同に評価されるのである。

// 1. ダウンロードが始まる前、コードは1番目にここから走る。 .

d3.tsv("data.tsv", function(error, data) {
  // 3. ダウンロードが完了すると、最後にここが走る。
});

// 2. ファイルのダウンロード中、二番目にここが走る

よって、グラフの実装を2つの工程に分ける必要がある。第1工程では、ページが読み込みデータ取得前に、出来る限り初期化を行う。データダウンロード後にページの再構成が発生しないように、ページ読み込時に適切なグラフのサイズを設定する。第2工程では、コールバック関数内にあるグラフの残りを完了する。

再構成コード:

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

.chart rect {
  fill: steelblue;
}

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

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

var width = 420,
    barHeight = 20;

var x = d3.scale.linear()
    .range([0, width]);

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

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

  chart.attr("height", barHeight * data.length);

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

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

  bar.append("text")
      .attr("x", function(d) { return x(d.value) - 3; })
      .attr("y", barHeight / 2)
      .attr("dy", ".35em")
      .text(function(d) { return d.value; });
});

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

</script>

それで、一体何が変わったのか? これまでのように同じ箇所でx-scaleを定義しているが、domainはデータの最大値に依存してるから、データが読み込まれるまでdomainを定義できない。よって、domainはコールバック関数内で設定される。同様に、グラフの横幅は静的に設定されるが、グラフの高さは棒の個数に依存するため、コールバック関数内で設定される必要がある。

現在、データセットは名前と値で構成されており、値はdではなくd.valueで参照しなければならない。各データポイントは数値ではなくオブジェクトである。JavaScriptでの等価表現はこの様になる:

var data = [
  {name: "Locke",    value:  4},
  {name: "Reyes",    value:  8},
  {name: "Ford",     value: 15},
  {name: "Jarrah",   value: 16},
  {name: "Shephard", value: 23},
  {name: "Kwon",     value: 42}
];

過去のグラフの実装でdを参照する箇所は、現在d.valueで参照する必要がある。特に、以前は棒の横幅を算出するためにxと定義したscaleを通していたが、現在はscaleするためにデータ値を渡す関数を書く必要がある (function(d) { return x(d.value); })。同様に、データセットから最大値を得る時に、各データポイントを判断する方法をd3.maxに教える為のアクセサ関数を渡す必要がある。

ここで、外部データについて更に理解することがある! name列は文字列で構成され、一方のvalue列は数値で構成されている。不幸にも、d3.tsvは自動的に型を判定して変換する高性能な関数ではない。代わりに、d3.tsvの第2引数に渡すtype関数を記述する。この型変換関数は各列を表すデータオブジェクトを変更でき、より適した表現に変更、変換を行う。

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

型変換関数はnullも返すこともあり、それは列が無視されるケースである。これはクライアント上のデータセットをフィルタリングする際に便利である。

型変換は絶対に必要ではないが、とても良いアイデアである。通常、TSVやCSVの全ての列群は文字列である。もし文字列から数値への変換を忘れた場合、JavaScriptは期待する動作をせずに、"1" + "2"を数値の3ではなく文字列の"12"として返す。同様に、数値でなく文字列でソートする場合、d3.maxの辞書順の振る舞いに驚くだろう!

Next: Part3

チュートリアルpart2のコードは、bl.ocks.org/7341714(http://bl.ocks.org/mbostock/7341714)上に置いてある。

今回作製した棒グラフは前章で作った骨子版グラフより印象的でないかもれないが、このチュートリアルSVGと外部データの導入であり、それは実世界の可視化において必要不可欠な2つの要素である。そして、現在このグラフを完成させるのにとてもよい位置にいる。このシリーズの次のチュートリアルでは、軸と図のスタイリングをカバーする。