Raspberry Piで取得したセンサーデータをリアルタイムに可視化する(グラフ編)

Posted: / Tags: Raspberry_Pi Node.js D3.js



前々回前回に引き続き、Raspberry Piでセンサーデータを取得して、Milkcocoaを使ってリアルタイムにブラウザ上にグラフ化する方法を紹介していきます。

今回は以下の流れで、保存されたセンサーデータのリアルタイムなグラフの描画について説明していきます。

  • D3.jsで折れ線グラフを表示する際に必要な知識
  • Milkcocoaのデータストアからデータを取り出して、入出力の範囲を指定する
  • グラフを描画する
  • リアルタイムアニメーション

D3.jsで折れ線グラフを表示する際に必要な知識

D3.jsの基礎知識については以下で学習すると良いかと思います。

面倒な人は、最低でも以下を頭に入れておいて頂ければ大丈夫です。

select()でDOM取得、append()で要素の追加

例えば、idsvgchartdivsvg要素を追加するのは以下のコードです。

var svg = d3.select('#svgchart').append('svg');

スケールでマッピング

スケールとは、入力であるデータの値(量、時間など)から、出力であるグラフの値(棒グラフの長さ、散布図の円の直径)にマッピングをする機構です。入力の範囲を「ドメイン」、出力の範囲を「レンジ」といいます。

例えば、100から500の値を取るデータを、10から350の値でグラフ化したい場合のスケールは以下のようなコードになります。

var scale = d3.scale.linear()
            .domain([100, 500])
            .range([10, 350]);

axis()で軸の作成

axis()で軸を作成します。axis()はスケールとセットで使います。

例えば、xScaleというスケールでグラフの下に軸を作成する場合は以下です。

var xAxis = d3.svg.axis()
            .scale(xScale)
            .orient('bottom');

描画するときは、call()で呼びます。

svg.append('g')
    .call(xAxis);

datum()でデータを取得

datum()でグラフ化したいデータを読み込みます。その後にチェインされたメソッドの中で、第一引数(慣習的にd)を入力値として受け取れる無名関数が使えるようになります(第二引数(慣習的にi)でその順番も受け取れます)。

コードの例は、次のline()と一緒に紹介します。

line()で折れ線グラフの作成

折れ線グラフを作成します。x()y()でx軸、y軸がどういう値をとるのかを指定します。

以下のコードは、x軸にデータの順番、y軸にデータの値をとった折れ線グラフの例です(グラフの画像はイメージです)。

var dataset = [5,3,4,6,1];

var line = d3.svg.line()
            .x(function(d, i) { return i; })
            .y(function(d, i) { return d; });

svg.append('path')
    .datum(dataset)
    .attr('d', line);

attr()dの値に指定していることからわかるように、line()が返す値はpath要素で使うベジェ曲線のパスコマンドのリストです(path要素についての詳細はこちらを参照ください)。

Milkcocoaのデータストアからデータを取り出して、入出力の範囲を指定する

基礎が理解できたので、実際にMilkcocoaのデータストアのデータをグラフ化していきます。

まずは、データストアからデータを取り出して、ドメインとレンジを指定します。

以下の図に従って説明します(説明が必要だと判断した部分を抜粋して説明しています。実際のコードとは少し違う部分があることを了承下さい。)。

データストアからデータを取得する

stream()メソッドを使って、最新のデータを100個取ってきます。

取ったデータはそれぞれ、タイムスタンプ(timestamp)とそのときのセンサーデータの値(value)をdatastoreという配列に入れていきます。

milkcocoa.dataStore('light').stream().size(100).next(function(err, datas) {
  datastore = datas.map(function(d) {
    return {
      timestamp : d.timestamp,
      value : d.value.v
    }
  });

  // 読み込み時の描画をここで行う。先ほどのソースではinitialDraw()。

});

データストアから取得したデータを描画用のデータに変換する

タイムスタンプのままグラフ化しても結局いつなのかわからないので、日付に変換してあげます。

function get_graph_data(data){
  return data.map(function(d) {
    return {
      date : new Date(d.timestamp),
      value : d.value
    };
  });
}

var dataset = get_graph_data(datastore);

データの最小値・最大値からドメインとレンジを決める(スケールをつくる)

グラフの描画範囲(幅と高さ)からレンジを指定します。

// marginはグラフの端が切れないためのものです。割とテンプレです。
var margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = 1040 - margin.left - margin.right,
    height = 420 - margin.top - margin.bottom;

// 日付・時刻のスケールはほぼ無条件でこの書き方と決まっています
var xScale = d3.time.scale()
    .range([0, width]);

var yScale = d3.scale.linear()
    .range([height, 0]);

時刻とセンサーデータの値の最小値、最大値からドメインを指定します。

// extentはデータ(dataset)の最小値・最大値を配列で返すものです。
xScale.domain(d3.extent(dataset, function(d) { return d.date; }));
yScale.domain(d3.extent(dataset, function(d) { return d.value; }));

グラフを描画する

データの範囲(スケール)を指定したので、実際に描画していきます。

まずは、x軸、y軸、折れ線のそれぞれでスケールの設定を行います。

x軸、y軸の設定

基礎知識のところで書いた通りに指定します。

var xAxis = d3.svg.axis()
            .scale(xScale)
            .orient('bottom');

var yAxis = d3.svg.axis()
            .scale(yScale)
            .orient('left');

折れ線の設定

line()メソッドを使って、折れ線の設定をします。

var line = d3.svg.line()
            .x(function(d) {
              return xScale(d.date);
            })
            .y(function(d) {
              return yScale(d.value);
            });

x軸、y軸それぞれ、データをスケールでマッピングした値をとるようにしています。

スケールメソッドは、引数の値をマッピングした値を返すようになっています。(マップ後 = scale(マップ前)

描画

設定が終わったので、実際に描画します。

// width, height, marginなどはテンプレです。
var svg = d3.select('#svgchart').append('svg')
          .attr('class', 'chart')
          .attr('width', width + margin.left + margin.right)
          .attr('height', height + margin.top + margin.bottom)
        .append('g')
          .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

function initialDraw() {
  svg.append('g')
      .attr('class', 'x axis')
      .attr('transform', 'translate(0,' + height + ')')
      .call(xAxis);

  svg.append('g')
      .attr('class', 'y axis')
      .call(yAxis)

  svg.append('path')
      .datum(dataset)
      .attr('class', 'line')
      .attr('d', line);
}

これで、データを読み込んで表示するまでは完成しました。

リアルタイムアニメーション

データがpushされたら、リアルタイムにアニメーションしながら更新するようにしてみます。

pushされたら更新する

Milkcocoaのon()メソッドを使います。データを追加したら先頭を削除して、配列の数は常に100個にします。

milkcocoa.dataStore('light').on('push', function(pushed) {

  // このdatastoreは配列のほうです
  datastore.push({
    timestamp : pushed.timestamp,
    value : pushed.value.v
  });
  // 先頭を削除
  datastore.shift();

  // 更新時の描画をここで行う。先ほどのソースではupdateDraw()。

});

更新時にアニメーションさせる

D3.jsのアニメーションのメソッドであるtransition()は、とても使いやすいです。

「アニメーションをします」と宣言したあとに、「要素がこう変わります」と教えるだけで、自動的にアニメーションが生成されます。アニメーションの仕方を指定することなく、始まりと終わりを教えてあげるだけで勝手に補間してくれるわけです。

今回アニメーションさせたいのは「軸と折れ線」なので以下のようなコードになります。

function updateDraw() {

  // 新しいデータ
  var dataset = get_graph_data(datastore);

  // ドメイン(入力値の範囲)の更新
  xScale.domain(d3.extent(dataset, function(d) { return d.date; }));
  yScale.domain(d3.extent(dataset, function(d) { return d.value; }));

  // アニメーションしますよ、という宣言
  svg = d3.select("#svgchart").transition();

  this.svg.select(".line")   // 折れ線を
      .duration(750) // 750msで
      .attr("d", line(dataset)); // (新しい)datasetに変化させる描画をアニメーション

  this.svg.select(".x.axis") // x軸を
      .duration(750) // 750msで
      .call(xAxis); // (domainの変更によって変化した)xAxisに変化させる描画をアニメーション

  this.svg.select(".y.axis") // y軸を
      .duration(750) // 750msで
      .call(yAxis); // (domainの変更によって変化した)yAxisに変化させる描画をアニメーション
}

これでデータストアにpushされると、アニメーションしながら更新されるかと思います。

IoT x Milkcocoa

3回にわたって、Raspberry Piで取得したセンサーデータをリアルタイムにグラフ化する過程を説明してきました。

マイコンボードとMilkcocoaで、実世界の情報が簡単にリアルタイムで見れるようになりました。

もちろん、何でもかんでもリアルタイムにすればいいというものではありません。

ただ、Apple Watchなどのデバイスが登場してきて、以前よりも「瞬間」の情報にフォーカスされることは予想できます。

そこで、瞬間の情報を得る体験だけでなく、開発の経験もあわせて持っておけば、IoT開発においてより具体的なアイデアが発想できて良いのではないかと思っています。

試すこと自体にとても価値があると思っているので、是非試して頂ければと思っています。


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