5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

D3.jsの enter-updata-exit パタンでLive Data表示

Last updated at Posted at 2017-11-14

 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は以下のような集合演算で定義されます。

  1. update : データセットの集合とView要素の集合の共通部分
  2. enter : データセットの集合 - View要素の集合
  3. exit : View要素の集合 - データセットの集合

 もっと分かりやすく言うと以下のように定義できます

  1. update : 現在のデータセットで既に描かれているもの
  2. enter : 新しくデータセットに追加され、まだ描かれていないもの
  3. exit : 既に描かれているが、データセットから削除されているもの。

 さて、基本的な問題なのですが、集合の各要素はどのように特定できるのでしょうか。例えばupdateを計算する時にデータセットとView要素の両方にその要素が含まれていることを判断する必要があり、そのためには要素にユニークなidを指定する必要があります。updateやenter、exitを計算する時に、どの要素が新たに追加されたとか、どの要素が削除されているとかを判断するために、要素を特定する方法が必要です。

 プログラムの全体の流れは、「最初にdata配列に20個のデータを入れてrenderし、次に以下の動作を繰り返すものです。data配列の先頭要素を削除し、その代わりにお尻に新要素を追加し、renderし直す。」というものです。ソースコードの以下の(行1)と(行2)を変えるだけで、内部データや表示が大きく異なります。

 まず以下の行を見てください。以下のdata()関数のcallbackで集合の要素をidで特定するように指定しています。

(行1)
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が計算されて保持されています。つまり以下のように考えられます。

  1. selection.op1 : updateに関数op1を適用します
  2. selection.enter().op2: enterに関数op2を適用します
  3. selection.exit().op3: exitに関数op3を適用します。

※op1やop2,op3はd3のslection関数で、一般にはチェーン化されています。

 (行1)の場合、棒グラフは③のtransition()によって、右から左にアニメーション効果を発揮しながら遷移していきます。これはidで特定したそれぞれの棒グラフ要素が、時間の経過とともに右から左へ流れていく様子を表しています。それでは以下の(行2)の場合はどうでしょう?

(行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>
5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?