Reactにはd3.jsを使い易くするためのライブラリRechartsがあるようですが、ここでは生のd3.jsを使うことにします。d3.jsは単なるグラフを描くライブラリではなく、もっと奥深い機能が豊富に提供されているので、React越しに間接的に見るより、生のd3.jsに直接触る方が興味深いからです。
http://recharts.org/
以下のサイトのchapter6のコードをベースに進めていきます。(All source code in this repository are licensed under MIT license)
https://github.com/NickQiZhu/d3-cookbook-v2
d3.jsの特徴としてenter-updata-exit パタンがあります。これはデータセット(JavaScriptの配列)とView要素(DOM)を結びつけるものです。今回示すコードは20個の棒グラフを描き、定期的に、左端のグラフを消して、右端に新しいグラフを追加するものです。これは時間の経過とともに、新しい要素が追加され、古くなった要素が削除されている現象をシュミレートしています。グラフが時々刻々右から左へ遷移していくことを期待しています。
d3.jsライブラリは、提供されたデータのグラフを書き、その後、時々刻々と変化するデータを反映して、グラフも更新していきます。それを効率よく行うために考えられたのがenter-updata-exit パタンです。
さて、各要素が{id:xxx,value:yyy}(xxxとyyyは整数値)の形をしている、2つの集合を考えます。データセットの集合とView要素の集合です。データセットとは例えばdataなどの配列名でJavaScriptコード内で保持しているデータの集合です(現在の新鮮なデータです)。View要素の集合とは、既にDom上に描かれたデータを表していて、Dom上のPropertiesに__data__として保存されています(古いデータです)。update, enter, exitは以下のような集合演算で定義されます。
- update : データセットの集合とView要素の集合の共通部分
- enter : データセットの集合 - View要素の集合
- exit : View要素の集合 - データセットの集合
もっと分かりやすく言うと以下のように定義できます
- update : 現在のデータセットで既に描かれているもの
- enter : 新しくデータセットに追加され、まだ描かれていないもの
- exit : 既に描かれているが、データセットから削除されているもの。
さて、基本的な問題なのですが、集合の各要素はどのように特定できるのでしょうか。例えばupdateを計算する時にデータセットとView要素の両方にその要素が含まれていることを判断する必要があり、そのためには要素にユニークなidを指定する必要があります。updateやenter、exitを計算する時に、どの要素が新たに追加されたとか、どの要素が削除されているとかを判断するために、要素を特定する方法が必要です。
プログラムの全体の流れは、「最初にdata配列に20個のデータを入れてrenderし、次に以下の動作を繰り返すものです。data配列の先頭要素を削除し、その代わりにお尻に新要素を追加し、renderし直す。」というものです。ソースコードの以下の(行1)と(行2)を変えるだけで、内部データや表示が大きく異なります。
まず以下の行を見てください。以下のdata()関数のcallbackで集合の要素をidで特定するように指定しています。
var selection = d3.select("body").selectAll("div.v-bar")
.data(data, function(d){return d.id;});// ★idで特定
もちろんView要素はDomに存在します。Chromeブラウザで見ると、Elementsのdiv.v-bar要素のPropertiesに__data__があって、例えば{id:15,value:99}という値がバインドされています。このidで集合の要素を特定しています。データセットとView要素を通してidで要素を特定できるわけです。ある要素のidがデータセットにはあるが、View要素にはなければ、それはenterに属します。データセットに無くて、View要素にあれば、exitに属します。
上の(行1)を実行すると、基本的にはselectionはupdateを表現します。同時にその構造体内部にはenterやexitが計算されて保持されています。つまり以下のように考えられます。
- selection.op1 : updateに関数op1を適用します
- selection.enter().op2: enterに関数op2を適用します
- selection.exit().op3: exitに関数op3を適用します。
※op1やop2,op3はd3のslection関数で、一般にはチェーン化されています。
(行1)の場合、棒グラフは③のtransition()によって、右から左にアニメーション効果を発揮しながら遷移していきます。これはidで特定したそれぞれの棒グラフ要素が、時間の経過とともに右から左へ流れていく様子を表しています。それでは以下の(行2)の場合はどうでしょう?
var selection = d3.select("body").selectAll("div.v-bar")
.data(data);
データセットをバインドするときにdata(data)で、特にデータセットの要素の特定方法を指定していません。この場合は実験の結果、単にdata配列のindexが要素の特定に使われるようです。dataは先頭の1個が削除され、お尻に1個追加されているのですが、その動きを反映していません。内部のデータセットに関して言えば、配列のサイズが同じで、indexも0〜19に固定されているので、enterもexitも空です。例えばindex=5で特定された6番目の要素についていえば、時間とともに上下に伸び縮みするアニメーションになります。
逆に(行1)の場合のように右から左に流れるように遷移していくアニメーションはイメージに合います。言葉を換えれば正しいVisual Metaphorが描かれていると言えます。Visual Metaphor(視覚的メタファー)とは非言語的な視覚イメージによって表現するコミュニケーション手段のことです。D3.jsは単にグラフを描くライブラリではなく効果的なVisual Metaphorを作るツールでもあります。
★行1の場合
idでdata要素を特定しているので、配列のindexに依存せず要素を特定できる。
index=19の時はenter要素となり、index=18〜0まではupdate要素であり、
index=-1になるとexit要素となる。
特定の要素に対してtransition()でアニメ効果を追加できる。
各々の要素を特定できているので、各要素が右から左へ遷移していく様子を描画できる。
★行2の場合
最初は20個の要素が全てenter要素であり、
2度目以降はそのすべての要素がupdate要素になる。
各々の要素を特定する術はなく、indexで特定しているので、右から左への遷移は描画でき無い。
以下がソースコードになります。コメントを追加したのと、行2のコードを追加した以外はオリジナルなままです。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3.jsの enter-updata-exit パタンについて</title>
<link rel="stylesheet" type="text/css" href="../../css/styles.css"/>
<script type="text/javascript" src="../../lib/d3.js"></script>
</head>
<body>
<script type="text/javascript">
var id= 0,
data = [],
duration = 500,
chartHeight = 100,
chartWidth = 680;
for(var i = 0; i < 20; i++) push(data);
function render(data) {
var selection = d3.select("body").selectAll("div.v-bar")
.data(data, function(d){return d.id;}); // ★(1)
// .data(data); // ★(2)
// enter
selection.enter()
.append("div")
.attr("class", "v-bar")
.style("z-index", "0")
.style("position", "fixed")
.style("top", chartHeight + "px")
.style("left", function(d, i){
return barLeft(i+1) + "px";
})
.style("height", "0px") // 最初はゼロ、次にupdateで正しい値にtransitionする
.append("span");
// update
selection
.transition().duration(duration)
.style("top", function (d) {
return chartHeight - barHeight(d) + "px";
})
.style("left", function(d, i){
return barLeft(i) + "px";
})
.style("height", function (d) {
return barHeight(d) + "px";
})
.select("span")
.text(function (d) {return d.value;});
// exit
selection.exit()
.transition().duration(duration) //★(3)
.style("left", function(d, i){
return barLeft(-1) + "px";
})
.remove();
}
function push(data) {
data.push({
id: ++id,
value: Math.round(Math.random() * chartHeight)
});
}
function barLeft(i) {
return i * (30 + 2);
}
function barHeight(d) {
return d.value;
}
setInterval(function () {
data.shift(); //★削除して追加
push(data); //★(1)の場合は右から左へ遷移するアニメーション効果、
//★(2)の場合は単なる上下の動作
render(data);
}, 2000);
render(data);
d3.select("body")
.append("div")
.attr("class", "baseline")
.style("position", "fixed")
.style("z-index", "1")
.style("top", chartHeight + "px")
.style("left", "0px")
.style("width", chartWidth + "px");
</script>
</body>
</html>