Thinking with Joinsの邦訳 [D3.js]
Joinを考える (Thinking with Joins)
Mike BostockのThinking with Joins (February 5, 2012)の邦訳。 ところどころ怪しいです。
貴方がD3を使って基本的な散布図を作っているとすれば、データを可視化するためにSVG circle要素の作製は欠かせない。D3が複数のDOM要素を作製するための基本機能を持たない事に気づき、貴方は驚かれるだろう。待て、WAT?^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要素を作り、配置するためにx
とy
要素を使用している。しかし、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工程を読み解くことができる。
svg.selectAll("circle")
は、SVGコンテナが空なので、新しい空のselectionを返す。このselectionの親ノードは、SVGコンテナである。- 次に、このselectionはデータ配列と結びつき、3つの取り得る状況、
enter
,update
,exit
を表す3つの新規selectionとなる。1.のselectionは空なので、updateとexit selectionは空であるが、一方のenter selectionは新しいデータそれぞれに対応するプレースホルダ (仮の領域) を持つ。 - update selectionはselection.dataによって返され、一方enterとexit selectionはupdate selectionに垂れ下がる。従って、selection.enterがenter selectionを返す。
- データに対して足りない要素は、enter selection上でselection.appendを呼ぶ事により、SVGコンテナに追加される。これは、各データポイントに対応する新しい円を、SVGコンテナに追加する。
Joinを考えるとは、selection (例えばcircle
)とデータの間に関連を宣言する事であり、3つのenter
, update
, exit
を通してこの関係を実装することである。
しかし、それがなぜ全ての問題点となるのか? なぜ複数要素を作る為の基本機能だけではないのか?^2 Data joinは美しさは、それが汎用的であることである。enter
selectionのみしかハンドルしない上記コードの場合、それは静的な可視化には十分で、update
とexit
の多少の変更のみで動的な可視化をサポートするように拡張できる。また、それはリアルタイムデータ、対話的な調査、複数のデータセット間のスムーズな変更の可視化を可能とする!
ここでは、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) なしで扱う。代わりに、要素をデータと関連づけるべき方法を記述する。与えれたenter
、update
やexit
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内に一連のサンプルを書いた。