D3.jsを使ってセンサーで取得した1日ごとの玄関の光量の総和をグラフ化する

Posted: / Tags: D3.js Raspberry_Pi



Raspberry Piで取得したセンサーデータをリアルタイムに可視化する」のシリーズに引き続き、可視化の話です。

上記のシリーズでは、Milkcocoaを使ってリアルタイムな情報を表示することにフォーカスをして話しました。

ただ、照度データ(その場所がどれくらい明るいか)はリアルタイムに通知することには価値があるとしても、リアルタイムにグラフで見ることにはあまり価値があるとは言えません。

なので、今回は、溜まったデータを1日ごとに比べて、どの日が特に使われているかをわかるようにしてみます。

ソースの全体は以下からご覧下さい(2回目以降の読み込みを高速化するためにローカルストレージを使って色々やっています)。

今回は、説明が必要と判断した部分だけ断片的に説明します。

データストアにたまった膨大なセンサーデータを取得してみる

Milkcocoaでデータストアにたまったデータを取得するにはstream()メソッドを使用します。

しかし、1回のstream()で取ってこれるデータ数の上限は999個になっているため少し工夫をします(size()メソッドの上限が999)。

stream().next()のコールバックの中でもう一度next()を使います。

そうすると、1回目のnext()で取得したデータの、次の(続きの)データを取り出すことが出来ます。

// your-app-idの部分は、Milkcocoaに(無料)登録してアプリを作成した際に生成されるアプリ固有の文字列です。
var milkcocoa = new MilkCocoa("your-app-id.mlkcca.com");
var ds = milkcocoa.dataStore('test');

ds.push({v:1});
ds.push({v:2});

var stream = ds.stream().size(1).sort('asc');

stream.next(function(err, d){
  console.log('first',d.value.v);
  stream.next(function(err, d){
    console.log('second',d.value.v);
  });
});

上記を実行すると、コンソールに以下のような結果が表示されると思います。

first 1
second 2

再帰を使ってすべてのデータを取得する

next()をネストすることで、続きのデータを取得できることがわかったので、すべてのデータを取得する方法を考えます。

すべてのデータをとってくるためには、取ってくるデータがなくなるまで何度もnext()をネストさせれば良いですね。

空の配列を用意して、データを取ってくるごとにその配列に結合して、最後に出来上がった配列をコールバックに渡してあげると、すべてのデータに対して処理が出来ます。

それが実現するためには、以下のような再帰的なコードを書くとできます。

// mlikcocoa, dataStoreなど割愛
var stream = ds.stream().size(999).sort('asc');

function loop(stocks, callback) {
  stream.next(function(err, elems) {
    stocks = stocks.concat(elems); // 結合
    if(elems.length > 0) loop(stocks, callback); // elemsが空になるまでloop()を実行
    else callback(stocks); // コールバックにstocksを渡す
  });
}

loop([], function(data) {
  // dataにすべてのデータが
  data.forEach(function(d,i){
    console.log(d.value.v);
  });
});

これで、データストアのすべてのデータを取得できるようになりました。

データを1日単位でまとめる

すべてのデータが取得できるようになったので、1日ごとのデータを表示するために、取得したデータを日ごとにわけます。

(pushしたデータの形式は、Raspberry Piで取得したセンサーデータをリアルタイムに可視化する(センサー編)と同じ、以下のようなものです。)

[{
  id: 'hogehoge'(自動生成),
  timestamp: 123.....(自動生成),
  value: {
    v: 1,
  }
},
{
...
}]

1日ごとにデータの総量を表示したいので、日ごとに値を格納する、グラフ表示用の新しい配列を作ります。

タイムスタンプを日付に変換して、同じ日付のものを配列の同じ要素に足していきます。

var daily = [{
  date: '',
  value: 0
}];

loop([], function(data) {
  var j = 0;

  data.forEach(function(d,i){
    // 'Tue Jan 01 2013 09:00:00 GMT+0900'みたいな文字列
    var date = new Date(d.timestamp);
    // ['Tue','Jan','01','2013','09:00:00','GMT+0900']みたいな配列に
    var dateArray = date.toString().split(' ');
    // 日付だけ取り出す。'Jan-01-2013'みたいな文字列に
    var day = dateArray[1] + '-' + dateArray[2] + '-' + dateArray[3];

    // 日付が一緒だったらvalueに足していく。日付が変わったら次の日付に移動(j++)。
    if(daily[j].date != day){
      j++;
      daily.push({
        date: day,
        value: d.value.v
      });

    } else {
      daily[j] = {
        date: day,
        value: daily[j].value + d.value.v
      }
    }
  });

  // ここに描画処理
});

これで、以下のような1日ごとの明るさの総量の配列を用意できました。

[{
  date: 'Jan-01-2013',
  value: 12512
},
{
  date: 'Jan-02-2013',
  value: 5324
},
{
...
}]

グラフで表示してみる

グラフの描画方法はRaspberry Piで取得したセンサーデータをリアルタイムに可視化する(グラフ編)とほとんど同じなので、そちらを参考にして頂きたいですが、いくつか違う点があります。

parse()で日付オブジェクトに変換

先ほどのコードで、日付を'Jan-01-2013'のようなかたちで保存しましたが、これは単なる文字列です。 x軸にはtimeスケールをつかっているので、日付(Date)に変換してあげる必要があります。

こちらは以下のようなコードでできます(フォーマットの種類についてはこちらを参照下さい)。

var parseDate = d3.time.format('%b-%d-%Y').parse; // 月(最初の3文字)-日(2桁)-年(4桁)の場合のパース関数
parseDate('Jan-01-2013'); // Dateが返る

data()とenter領域について

折れ線グラフでは、すべてのデータをひと繋ぎのパスで表示しましたが、棒グラフでは、それぞれのデータごとに棒を表示します。

そのため、データの読み込みの関数は、折れ線グラフで使用したdatum()ではなく、data()を使用します。

datumdata()の違いをざっくり言うと、描画処理が1回で済む場合はdatum()、データごとに描画処理を行いたい場合はdata()という感じです。

data()には、select()で選択したDOMの数とデータの個数(配列の要素数)によって処理領域が変化する、という仕様があります。詳しくは以下の動画を参照下さい。

例えば、3つのp要素に配列の値を表示するコードを書いたとき、配列の要素数の過不足によってenter領域、exit領域が作られます。

<body>
  <p></p>
  <p></p>
  <p></p>
</body>
var enterArray = [0,1,2,3,4];
var update = d3.select('body').selectAll('p').data(enterArray); // [0,1,2]
var enter = update.enter(); // [3,4]
var exitArray = [0,1];
var update = d3.select('body').selectAll('p').data(exitArray); // [0,1]
var exit = update.exit(); // 最後のp要素

今回は、select()で選択した要素数は0なので、すべての処理をenter領域に行うということになります(グラフ描画のときは基本すべてenterになると思います)。

グラフの描画範囲について

棒グラフそれぞれのデータを表示するときには、目盛りの真ん中に棒が来て欲しいので、以下のような工夫をします。

  • 棒グラフの幅は「描画範囲÷データの数」
  • 棒を、目盛りから「描画範囲÷データの数の半分」だけ左に移動
  • 棒がグラフの描画範囲から見きれないように、レンジの両端を「描画範囲÷データの数の半分」だけ削る
var xScale = d3.time.scale()
              .range([width/dataset.length/2, width-width/dataset.length/2]);
              // レンジの両端を「描画範囲÷データの数の半分」だけ削る
var bars = svg.selectAll(".bar")
                    .data(dataset);

bars.enter().append("rect")
    .attr("width", width / dataset.length) // 幅は「描画範囲÷データの数」
    .attr("x", function(d) { return xScale(d.date) - (width/dataset.length)/2; })
    // 目盛りから「描画範囲÷データの数の半分」だけ左に移動
    ....

色々割愛しましたが、この流れでコードを書いていけば出来るかと思います。詳しくはソースを見て下さい。

まとめ

今回は、何日間にもかけてたまったデータを可視化する方法を紹介してきました。

一定間隔ではなく値が変化したらデータを保存するようにしている上、センサーデータの値を単純に足して表示しているだけなので、数字自体に意味はありません。

意味のあるデータにするには、時間で重み付けするか等間隔でデータを取得するかして、センサーデータも照度の単位であるルクスに変換すると良いのではないでしょうか。

Raspberry Piで取得したセンサーデータをリアルタイムに可視化する」のシリーズとこの記事で、リアルタイムな情報とストックされた情報を表示することができました。

Milkcocoaとデータビジュアライズライブラリを組み合わせることで幅広い表現ができるので、是非色々試して頂ければ幸いです。


※Raspberry PiやTesselでMilkcocoaを使うハンズオンが7月6日に開催されます。マイコンボードを持っていない方も会場で購入できるので、興味を持たれた方は是非参加してみて下さい!