データベースに蓄えたデータでグラフを描こう!!

Node-REDのDashboardとPlotlyを使って、Queryの出力をグラフに加工します。

1.概要

R-MSMで取得し、GateWayやエッジサーバーからクラウドに上げ、データベースに蓄えたデータの活用方法の一つとして、グラフするという方法があります。グラフにすることで、データが可視化され、通常とは異なる動きを容易に捉えることができます。そして、そこから現象に対する因果関係を導くことも可能になるかもしれません。

本投稿では、R-MSMの環境センサ(BME680 or BME688)の気温データの24時間前から現時点までの分を、GCP上のMySQL(データベース)から取出し、Node-REDのダッシュボードのテンプレート・ノードと描画ライブラリであるPlotly.jsを使ってグラフ化してみます。(おまけで、Appendixに、データベースがmongoDBの場合を追記しました。)

手順としては、大きく2つに分かれます。

  1. データ取得部:24時間前から現時点までの気温データをQueryの式を使ってデータベースから取り出します
  2. グラフ化部:データベースから取り出した気温データを使って、Node-REDのダッシュボードでグラフ化します

どちらにもNode-REDを使いますが、a) Queryの式を使う部分とb) Plotly.jsを使ってグラフ化する部分は、javascriptのプログラム記述が必要になりますので、javascriptのプログラミングの知識が少し必要になります。まず、こちら(templateノードと外部ライブラリ(plotly.js)を使ったグラフの描画)をご覧になってPlotly.jsを触れてみてください。

2.完成形の確認

初めに、完成形を確認します。

  1. Node-REDのダッシュボード上の「温度取得」のボタンを押します。
  2. すると、24時間前から現時点までの気温データのグラフが表示されます。

ダッシュボードの画面を次に示します。

3. Node-REDのフローの作成

Node-REDのフローエディタの画面を次に示します。

「1. 概要」で書いた2つの手順(①データ取得部と②グラフ化部)に沿って作成しています。

3.1. データ取得部の作成:気温データをデータベースから取得

データ取得部は、24時間前の時点から現在までの気温データをデータベースから抽出しなさいというQuery文をMySQL(データベース)用に生成します。

3.1.1. 生成するQuery文(SELECT文)

MySQLでは、データの抽出は、SELECT文で行います。SELECT文の詳細に関しては、リファレンスマニュアルをご覧ください。ここでは、以下ようなのSELECT文を作成します。”ような”と記述したのは、時間に関しては都度変わるからです。この時間をどう都度変えて設定するかがデータ取得部の肝です。

SELECT文のあとの2行が抽出するデータの指示です。ここでは、datetimeとdata0です。date_format(date, format)関数は、format 文字列に従って、date 値を書式設定します。

FROMの次の行は、データを抽出するデータベース名とテーブル名です。ここでは、データベースrmsm_db内のテーブルdataから抽出せよという指示です。

最後のWHEREは、抽出する条件を指定します。ここでは、labelが’BME680’であるデータでかつdatetimeの値が、‘2022-05-09 15:51:26’ から‘2022-05-10 15:51:26’ の間にあるものという抽出条件の指示です。

上記のSELECT文は、「データベースrmsm_db内のテーブルdataから、labelの値が’BME680’でかつdatetimeの値が‘2022-05-09 15:51:26’ から‘2022-05-10 15:51:26’ の間にあるデータを抽出し、そこに含まれるdatetimeとdata0の情報をフォーマット指定がある場合にはそのフォーマットで出力しなさいという意味です。

3.1.2. 日時の設定方法

日時の設定方法の考え方を以下に示します。

  1. ダッシュボードの「温度取得」ボタンを押すと現在の日時をmsg.payloadに出力する
  2. msg.endTimeに、msg.payloadの現在の日時をフォーマットを指定して出力する
  3. msg.startTimeに、msg.payloadの現在の日時から24時間を引いた日時をフォーマットを指定して出力する
  4. ④ templateノードを使って、SELECT文のWHEREの日時の個所に、msg.endTimeとmsg.startTimeを埋め込む

①、②、③のNode-RED記述を以下に示します。

3.1.3. Select文の生成

次に、④のSELECT文を生成するNode-REDの記述です。Templateノードですので、命令を記述します。ここでは、SELECT文を、msg.topicに出力します。msg.topicに出力するのは、後段のmysqlノードの仕様だからです。

msg.topic にはデータベースに対する クエリ を指定する必要があります。また、結果は msg.payload に格納されて返ります。

テンプレートでは、前段でmsg.EndTimeとmsg.StartTimeに設定した時間を利用します。そのために、以下に示すようにテンプレートの記述の中に'{{StartTime}}’と'{{EndTime}}’として記述することで、前段からのmsgの内容を展開できます。

例えば、ボタンを押した時間が、msg.EndTime = 2022-05-10 15:51:26だとすると、24時間前の時間がmsg.StartTime=2022-05-09 15:51:26になります。そして、Templateノードで、msg.StartTimeとmsg.EndTimeが展開されて、下記のSELECT文がmsg.topicとして、mysqlノードに送られます。

3.1.4. データベースからの出力

上記のSELECT文をmsg.topicに乗せてmysqlノードに送ります。すると、抽出結果がmsg.payloadに乗って帰ってきます。

上記のSELECT文で抽出した結果を例として以下に示します。

3.1.5. ダッシュボードのタブ/グループの設定

(1) ダッシュボードの構成

buttonノードのボタンは、表示するグラフと同じくダッシュボードに表示されます。ダッシュボードの画面は、タブとグループとで領域が分けられています。下の図に、タブとグループとそれに含まれるノードを示します。
タブは、画面と考えてください。タブを切り替えると画面が切り替わります。タブは①タブ名で識別されます。ダッシュボード画面の左上にある三本線が③タブ切替ボタンです。
グループは、タブに所属します。タブには複数のグループを入れることができます。グループは②グループ名で識別されます。
ノードは、どのタブのどのグループに所属するかをして指定しないといけません。
下の例では、タブ名「簡易気温ログ」というタブにグループ名「気温ログ」というグループが含まれます。そして「気温ログ」の中に、「温度取得」という名前のbuttonノードと「温度ログ」という名前のtemplateノードが含まれています。

(2) タブとグループの設定

タブとグループの設定は、各ノードのプロパティ設定画面で行います。ここでは、新規にタブとグループを作成します。2つのノードともこのグループの所属させます。タブとグループの追加方法は、こちら(ダッシュボードのタブとグループの追加)をご覧ください。

タブ名 ・・・名前:簡易気温ログ
グループ名・・・名前:気温ログ,        幅:24
buttonノード・・・名前:温度取得,    サイズ:4×1
templateノード・・・名前:温度ログ,  サイズ:24×10

以上でデータ取得部の作成の説明は終わりです。

3.2. グラフ化部の作成:Node-REDのダッシュボードでグラフ化

次にデータ取得部で取得したデータをグラフ化する方法について説明します。グラフ化部では、Node-REDのdashboardのTemplateノードを使います。そして、グラフを描くために、Plotly.jsというライブラリを使います。PlotlyはMITライセンスです。

3.2.1. Node-REDへのPlotlyライブラリのロード記述追加

Plotlyでグラフを描くために、Node-REDのフローエディタのタブのどれかで、Plotly.jsを読み込む必要があります。

簡単な方法を以下に示します。

Node-REDのフローエディタのタブのどれかに以下を行い、plotly.jsをロードします。

  1. 1) dashboardのTemplateノードを配置します。
  2. 2) コード種別で、「<head>ヘッドセクションへ追加」を選択します。
  3. 2) HTMLコードの欄に下記記述を追加します。
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>

3.2.2. Node-REDへのグラフ化ノードの追加

Node-REDのフローエディタに、dashboardのTemplateノードを配置します。そのTemplateノードをクリックして、設定を行います。

設定内容を以下に示します。

3.2.3. グラフ化の流れ

HTMLコードの欄に記載するソースコードを示して説明する前に、グラフ化の流れを説明します。

  1. ① mysqlの週出結果であるmsg.payloadに変化があるかを検知します。
  2. ② msg.payloadに変化があったら、msg.payloadの中身を確認します。
     空のデータであったり、グラフ化に適さないmsg.payloadであったらグラフ化しません。
  3. ③ msg.paylaodがグラフ化可能ならば、function plot()を呼び、グラフ化する。

以下に該当するHTMLコードの前半部を示します。

3.2.4. HTMLコード前半部の説明

まず初めに、上記のHTMLコード前半部を説明します。

3.2.4.1. <div></div>記述

dashboard上での領域の設定を行います。

id=”plot”は、Plotlyでグラフ表示する際に使用します。ここの領域にグラフを表示しなさいということです。

styleで領域のサイズを指定しています。ここでは、heightで高さを指定しています。

<div id="plot" style="height: 574px"></div>
3.2.4.2. <script></script>

次に、javascriptでメッセージ処理を行う記述を行います。

<script>
(function(scope) {
    ... JavaScriptコード ...
})(scope);
</script>
3.2.4.3. scope.$watch関数とその中身
  1. ① scope.$watch関数は、msg.payloadに変化がないかどうかを調べ、変化があった場合にはコールバック関数を実行します。
  2. ② msg.payloadに変化があった場合には、msg.payloadをcurrent関数の引数としてコールバック関数を呼び出します。
  3. ③ current == null であったり、’object’でなかったりした場合は、何もせずにreturnします。
  4. ④ currentの中身があって、長さが0でなければ、msg.tag = 0を出力します。そして、currentを引数にして、plot関数を呼び出します。
  5. ⑤ currentの中身がなかったり、長さが0であれば、msg.tag=1を出力します。そして、plotly.purge(‘plot’)を実行し、グラフを消去します。
scope.$watch('msg.payload', (current) => {   
    if (current == null) {                   
        return;
    }
    if (typeof current !== 'object') {       
        return;
    }

    if (current && current.length !== 0) {   
        scope.send({tag: 0});
        plot(current);
    } else {                                 
        scope.send({tag: 1});
        Plotly.purge('plot');
    }
});

うまくデータベースからデータが抽出されていれば、plot関数が呼び出され、グラフが描画されます。

3.2.5. HTMLコード後半部(plot関数)の説明

後半部は、plot関数の記述です。

まず流れを説明します。

  1. ① 引数current(msg.payload)をデータごとに配列を分離します。そのために、配列のmapメソッドを使います。mapメソッドは、各要素1つずつに対して「コールバック関数」を実行し、その結果を新しい配列として返すとても便利なメソッドです。これを使って、datetimeとdata0をそれぞれtimeという配列とtempという配列に分けます。
  2. ② Plotlyに渡すためのデータのjsonを作ります。x軸は、上で作ったtime配列で時間軸とします。y軸は、同じくうえで作ったtemp配列で温度軸とします。グラフは散布図とします(scattergl)。
  3. ③ 同じくPlotlyに渡すデータとして、layoutの情報をjsonフォーマットで作ります。タイトルやx軸のラベルやy軸のラベルを記述します。
  4. ④ PlotlyのnewPlotメソッドを使って、グラフを描かせます。

 

3.2.5.1. mapメソッドを使ったmsg.payloadの分離

データベースから抽出したデータは、msg.payloadの中でObjectとして複数のデータが混在しています。今回の場合は、日時情報のtimedateと温度情報のdata0です。グラフでは、x軸(横軸)を日時にし、y軸(縦軸)を温度とします。そのために、msg.payloadの中に混在しているtimedateとdata0をそれぞれの配列に分離します。

そのために、javascriptの配列のmapメソッドを使います。上にも書きましたが、mapメソッドは、各要素1つずつに対して「コールバック関数」を実行し、その結果を新しい配列として返すとても便利なメソッドです。

以下の例では、current(msg.payload)のデータ、datetimeとdata0をそれぞれ、timeとtempの配列に分離しています。datetimeで説明します。

  1. 1) 配列currentのmapメソッドを呼び出します。
  2. 2) currentのObjectデータをvalueという引数でコールバック関数に渡します。配列のデータの数だけコールバック関数の呼び出しを繰り返します。
  3. 3) コールバック関数は、valueの下にあるdatetimeというデータに対して、日付オブジェクトを新たに作成して返します。
    日付オブジェクトは1970年 1月1日 UTC からの経過したミリ秒(秒の1/1000)に等しい時間をもつオブジェクトです。
  4. 4) 配列timeにコールバック関数の結果を入力します。
var time = current.map(function(value){return new Date(value.datetime)});
var temp = current.map(function(value){return(value.data0)});
3.2.5.2. Plotlyによる描画

データ配列の準備が出来たら、Plotlyを呼び出すための準備を行います。

  1. 1) 表示データの準備:データをjson形式でまとめます。x軸、y軸のデータやグラフの形式など
  2. 2) レイアウト情報の準備:レイアウト情報をjson形式でまとめます。グラフのタイトルやx軸のタイトル、y軸のタイトルやレンジなど
  3. 3) plotlyのnewPlotメソッドを呼び出して、グラフ表示します。
Plotly.newPlot('plot', [data], layout);

注意事項は、dataは配列です。ですので、1つの場合でも、[data]のようにカギ括弧でくくります。
複数の場合は、[dataA, dataB]のように並べます。あらかじめ var data = [dataA, dataB]と別名にした場合は、以下の様にカギ括弧が不要になります。

Plotly.newPlot('plot', data, layout);

3.2.6. HTMLコードのソース

dashboardのTemplateノードのHTMLコードの部分に記載するコード全体を以下に示します。

<div id="plot" style="height: 574px"></div>
<script>
  (function(scope) {
    // msg.payloadのデータを監視する
    scope.$watch('msg.payload', (current) => {
      if (current == null) {
        return;
      }
      if (typeof current !== 'object') {
        return;
      }

      if (current && current.length !== 0) {
        scope.send({tag: 0});
        plot(current);
      } else {
        scope.send({tag: 1});
        Plotly.purge('plot');
      }
    });

    // グラフを描画する
    function plot(current) {
      var time = current.map(function(value){return new Date(value.datetime)});
      var temp = current.map(function(value){return(value.data0)});

      var data = {
        x:time,
        y:temp,
        name:'環境センサ',
        mode: 'lines',
        type: 'scattergl',
        line: {
          width: 2
        }
      }

      const layout = {
        title: "温度ログ",
        xaxis: {
          title: '時間'
        },
        yaxis: {
          title: '温度[℃]'
          //range: [0, 40]
        }
      };

      Plotly.newPlot('plot', [data], layout);
    }
  })(scope);
</script>

Plotlyに関しては、いろいろなグラフを描くことができる奥の深いライブラリです。以下の参考ページなどを参考にしてください。

Plotly.jsの参考ページ

Appendix:データベースがmongoDBの場合

A1.概要

MySQLは、RDBS(Relational Data Base Ssytem)でSQLを使ったデータベースです。一方、近年のビッグデータの潮流の中で、noSQLデータベースが盛んに利用されるようになってきています。その代表格がmongoDBです。mongoDBは、JSONファイルなどのドキュメントをそのままデータベースの中に保存できます。抽出時には、JSONのkeyを条件や抽出するデータの項目とすることができます。

以下では、データベースがmongoDBとして上記のグラフ表示と同じ表示させてみる場合に変更が必要な内容を説明します。

A2. Node-REDのフローエディタへのmongoDBノード導入

まず、初めにmongoDBへアクセスするためのノードを、Node-REDのフローエディタでパレット管理を使って追加します。

追加が完了すると、ストレージの欄に、”mongodb in” と “monogodb out”の2つのノードが追加されています。

A3. Node-REDフローの変更

次に、グラフ表示のNode-REDのフローを変更します。

変更はデータ取得部のみです。

  1. ① mysqlのノードを削除
  2. ② mongodB in ノードを配置:サーバーとノードのプロパティ記述を変更する
     データベース名とコレクション名(mysqlのテーブルに相当)を記載
  3. ③ mongodB inノードの操作の項目でfindを選ぶ
  4. ④ EndTimeのノードのOutput TimezoneをUTCであるEurope/Londonと記載
  5. ⑤ 同様に、StartTimeのノードのOutput TimezoneをEurope/Londonと記載
  6. ⑥ functionノードの中のコードにQuery文を記述
     1) 抽出条件(mysqlのWHEREに相当)を、msg.payloadに記載
     2) 取得するデータはmsg.projectionに記載

A.4.functionノードのコード記述時の注意事項

・ mongodDBでは、抽出したデータに_idというID番号がついてきます。それを表示させないために、意図的にmsg.projectionで”_id”:0としています。

以下、functionノードのコード記述のソースです。

msg.projection = {"_id": 0, "data0":1, "datetime":1}
msg.payload = {
    label:"t2",
    datetime:{"$gte":new Date(msg.StartTime), "$lte":new Date(msg.EndTime)}
}
return msg;

A.5.グラフ表示の確認

Node-REDでdashboardの画面を開き、「温度取得」ボタンを押すと、mongoDBから抽出されたデータでグラフが表示される。