HogeFugaHogera

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

Thinking with Joinsの邦訳 [D3.js]

Joinを考える (Thinking with Joins)

Mike BostockThinking with Joins (February 5, 2012)の邦訳。 ところどころ怪しいです。


貴方がD3を使って基本的な散布図を作っているとすれば、データを可視化するためにSVG circle要素の作製は欠かせない。D3が複数のDOM要素を作製するための基本機能を持たない事に気づき、貴方は驚かれるだろう。待て、WAT?^1

もちろん、appendメソッドは1つの要素を作れる。

svg.append("circle")
    .attr("cx", d.x)
    .attr("cy", d.y)
    .attr("r", 2.5);

ここでのsvgは、前もって作られた<svg>要素(又は、現在のページ上で選択した、とも)から成る単一要素のselectionを参照している。

しかし、それで出来上がるのは単なる1つの円であり、欲しいのは各々のデータポイントに対応する複数の円である。forループと総当たりから抜け出す前に、1つのD3の例からこの当惑する一連の出来頃について考えよう。

svg.selectAll("circle")
    .data(data)
  .enter().append("circle")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", 2.5);

ここでのデータは、xとy要素を持つJSONオブジェクトの配列である。例: [{"x": 1.0, "y": 1.1}, {"x": 2.0, "y": 2.5}, …]

このコードは、必要とする事をキッチリこなしており、各々のデータポイントに対するcircle要素を作り、配置するためにxy要素を使用している。しかし、selectAll("circle")の意味は何なのか? なぜ新しい要素を作る為に、存在しない事が分かっている要素を選択させるのか? WAT。

良く聞いてほしい。何かを行う為の方法をD3に伝えるのではなくて、貴方が望む事をD3に伝えるのである。貴方はデータに対応するcircle要素が欲しい。D3に円の作製を指示するのではなく、その場合には、選択したcircleがデータと対応するようにD3へ伝える。このコンセプトをdata joinと呼ぶ。

See the Pen WbWbyx by Rinozuka (@Rinozuka) on CodePen.

データポイントは、update selection (上図の円が重なる部分) を生成する要素に結びつく。残った、要素と結びついていないデータは、足りない要素を表すenter selection (上図の左部分)を生成する。同様に、データと結びついていない要素は、削除される要素を表すexit selection (上図の右部分)を生成する。

ここで、data joinを通して困惑するenter-append工程を読み解くことができる。

  1. svg.selectAll("circle")は、SVGコンテナが空なので、新しい空のselectionを返す。このselectionの親ノードは、SVGコンテナである。
  2. 次に、このselectionはデータ配列と結びつき、3つの取り得る状況、enter, update, exitを表す3つの新規selectionとなる。1.のselectionは空なので、updateとexit selectionは空であるが、一方のenter selectionは新しいデータそれぞれに対応するプレースホルダ (仮の領域) を持つ。
  3. update selectionはselection.dataによって返され、一方enterとexit selectionはupdate selectionに垂れ下がる。従って、selection.enterがenter selectionを返す。
  4. データに対して足りない要素は、enter selection上でselection.appendを呼ぶ事により、SVGコンテナに追加される。これは、各データポイントに対応する新しい円を、SVGコンテナに追加する。

Joinを考えるとは、selection (例えばcircle)とデータの間に関連を宣言する事であり、3つのenter, update, exitを通してこの関係を実装することである。

しかし、それがなぜ全ての問題点となるのか? なぜ複数要素を作る為の基本機能だけではないのか?^2 Data joinは美しさは、それが汎用的であることである。enter selectionのみしかハンドルしない上記コードの場合、それは静的な可視化には十分で、updateexitの多少の変更のみで動的可視化をサポートするように拡張できる。また、それはリアルタイムデータ対話的な調査複数のデータセット間のスムーズな変更の可視化を可能とする!

ここでは、3つ全ての状態をハンドリングする例を示す:

var circle = svg.selectAll("circle")
    .data(data);

circle.exit().remove();

circle.enter().append("circle")
    .attr("r", 2.5);

circle
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; });

このコードは動くたびに、data joinを再実行し、要素とデータ間の望んだ関連を維持する。新データセットが古いものより小さければ、余剰要素はexit selectionとなり、削除される。新データセットが大きければ、余剰データはenter selectionとなり、新しいノード(節)が追加される。新データセットが全く同じ大きさならば、全ての要素は単に新しい位置を持って更新され、追加や削除される要素はない。

要素に割当てるデータを制御する為には、key functionを与えることができる。

Joinを考えるとは、コードをより叙述的にすることである。あなたは、これら3つの状態を、分岐 (if) や繰り返し (for) なしで扱う。代わりに、要素をデータと関連づけるべき方法を記述する。与えれたenterupdateexit selectionが空になる事が起こった場合、対応するコードは何もしない。

必要なら、結合は、特定の状態に対する操作も対象とする。例えば、updateではなくてenterに一定の属性 (例えば、r属性として定義される円の半径) を設定できる。 再選択する要素と最小のDOMの更新により、描画性能が非常に向上する! 同様に、特定のデータに治脚てアニメーション効果を指定することできる。例えば、拡大して追加される円:

circle.enter().append("circle")
    .attr("r", 0)
  .transition()
    .attr("r", 2.5);

同様に、縮小は:

circle.exit().transition()
    .attr("r", 0)
    .remove();

いま、あたなたはjoinを考えている!

コメントや質問? {HNで議論しよう](http://news.ycombinator.com/item?id=3581614)。

追記

この投稿の続編として、http://bl.ocks.org/3808218内に一連のサンプルを書いた。