Contents
templateノードと外部ライブラリ(plotly.js)を使ったグラフの描画
1.目的
R-CPSで取得したデータをNode-REDでグラフにする際、ダシュボードのchartノードを使えばほぼ用が足ります。chartノードには、折れ線グラフ、棒グラフ、円グラフ、鶏頭図、レーダーチャートが準備されており、複数のデータを凡例を使ってグラフ化することもできます。しかし、もう少し複雑なグラフを描きたい場合もあります。
そのような場合には、ダッシュボードのtemplateノードを使って、javascriptのグラフ描画用のライブラリでグラフ化することができます。ここでは、plotly.jsというライブラリを使ってグラフ化する方法を説明します。
2.ダッシュボードのtemplateノード
ダッシュボードにあるtemplateノードのヘルプには、以下の様な説明があります。
「Template WidgetにはHTMLコードおよびAngular/Angular-Materialディレクティブを指定できます。
このノードで動的なユーザインターフェイス要素を作成し、入力によって見た目を変更したり、メッセージをNode-REDに送り返したりできます。」
そして、すぐ下に例が記載されています。
「このコードはmsg.payloadで受け取った数値が偶数か奇数かを表示します。同時に、偶数であれば緑に、奇数であれば赤にテキストの色を変更します。」
この例を使って、以下に動作確認用のフローを載せます。
このフローを読み込みデプロイして、ダッシュボードを開くと、functionノードで生成した乱数(0~1000)が、奇数の場合には赤い文字で「奇数」、偶数の場合には、緑の文字で「偶数」と表示されます(オリジナルに加えて数字も表示されるように修正しています)。
templateノードのプロパティの画面の「HTMLコード」の欄に以下の様なHTML言語を記載しています。
<div layout="row" layout-align="space-between">
<p>数値は</p>
<font color="{{((msg.payload || 0) % 2 === 0) ? 'green' : 'red'}}">
{{msg.payload}} : {{(msg.payload || 0) % 2 === 0 ? '偶数' : '奇数'}}
</font>
</div>
templateノードに特有なのは、前段から送られてくるmsg.payloadを読み込んで使用しているところです。msg.payloadを2で割った余りが0か1を調べています。余りが1であれば、font colorをgreenにし、偶数と表示します。余りが0であれば、font colorをredにし、奇数と表示します。
このようにtemplateノードは、前段から送られてくるmsg.payloadを読み込むことができ、HTML記述に従ってダッシュボードに表示することができます。
上記のフローのコードを次に載せます。動作を確認してみてください。
[{"id":"1062254d2014df24","type":"ui_template","z":"3ddc84187f02f680","group":"10dab6168db8d88c","name":"test","order":2,"width":0,"height":0,"format":"<div layout=\"row\" layout-align=\"space-between\">\n <p>数値は</p>\n <font color=\"{{((msg.payload || 0) % 2 === 0) ? 'green' : 'red'}}\">\n {{msg.payload}} : {{(msg.payload || 0) % 2 === 0 ? '偶数' : '奇数'}}\n </font>\n</div>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":510,"y":60,"wires":[[]]},{"id":"9d7ddb503345b13b","type":"change","z":"3ddc84187f02f680","name":"floor","rules":[{"t":"set","p":"payload","pt":"msg","to":"$floor(payload.data)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":60,"wires":[["1062254d2014df24"]]},{"id":"271031cdc011f611","type":"function","z":"3ddc84187f02f680","name":"Random","func":"msg.payload = {\"data\": Math.random() * 1000};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":60,"wires":[["9d7ddb503345b13b"]]},{"id":"78bda264ae5f0872","type":"inject","z":"3ddc84187f02f680","name":"1秒毎","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":60,"wires":[["271031cdc011f611"]]},{"id":"10dab6168db8d88c","type":"ui_group","name":"Odd-Even","tab":"37553850c0b6f4d8","order":1,"disp":true,"width":"4","collapse":false,"className":""},{"id":"37553850c0b6f4d8","type":"ui_tab","name":"Template Test","icon":"dashboard","disabled":false,"hidden":false}]
3.plotly.js
次に、plotly.jsに付いて説明します。公式のHPはこちらです。
HPには、「d3.jsとstack.glの上に構築されたplotly.jsは、叙述式の高レベルなチャートライブラリです。plotly.jsは、3Dチャート、統計グラフ、SVGマップを含む40以上のチャートタイプを同梱しています。plotly.jsはフリーでオープンソースです。GitHubでソースを閲覧したり、問題を報告したり、貢献することができます。」とあります。そして、非常に多くのサンプルが載せられています。
以下に、2つのグラフの例をtemplateノードを使って描いてみます.
3.1.ライブラリの読込み
plotly.jsをtemplateノードで使用する際に、plotly.jsを読み込む必要があります。通常のHTMLであれば、<head></head>の部分に記載します。
<head>
<!-- Plotly.js -->
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
Node-REDでは、templateノードを使います。フローのいずれのタブでも構わないので、どこかに1つライブラリ呼び出しのtemplateノードを配置します。
templateノードを1つ配置したら、プロパティ画面を開きます。
① コード種別を“<head>ヘッドセクションへ追加”を選びます。
② HTMLコードの欄に以下のライブラリの所在を記載します。
③ 「完了」を押します。
フローのコードを以下に載せます。
[{"id":"73684e2ca67deabc","type":"comment","z":"3ddc84187f02f680","name":"フローの何処かに配置必要","info":"","x":150,"y":40,"wires":[]},{"id":"fb33c05b89ee8fd4","type":"ui_template","z":"3ddc84187f02f680","group":"10dab6168db8d88c","name":"Load Plotly CDN","order":2,"width":0,"height":0,"format":"<script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"global","className":"","x":130,"y":80,"wires":[[]]},{"id":"10dab6168db8d88c","type":"ui_group","name":"Line-Scatter","tab":"37553850c0b6f4d8","order":1,"disp":true,"width":"14","collapse":false,"className":""},{"id":"37553850c0b6f4d8","type":"ui_tab","name":"Template Test","icon":"dashboard","disabled":false,"hidden":false}]
3.2. サンプルグラフの描画
1)折れ線と散布図(Line and Scatter Plot)
一つ目の例として「折れ線と散布図」を描かせます。plotly.jsのHPのこちらに載っている例です。
新たに1つtemplateノードを配置します。
プロパティ画面を開きます。
① グループとタブを設定します。
② サイズを設定します。初めは、大きめに設定して下さい。
③ HTMLコードの欄に、コードを記載します。
<div></div>を記載し、plotするスペースを設けます。
<script></spcript>の間に、サンプルプログラムを記載します。
④ 最低必要な以上の設定を終えたら「完了」を押します。
<div id="myDiv">
<!-- Plotly chart will be drawn inside this DIV -->
</div>
<script>
var trace1 = {
x: [1, 2, 3, 4],
y: [10, 15, 13, 17],
mode: 'markers',
type: 'scatter'
};
var trace2 = {
x: [2, 3, 4, 5],
y: [16, 5, 11, 9],
mode: 'lines',
type: 'scatter'
};
var trace3 = {
x: [1, 2, 3, 4],
y: [12, 9, 15, 12],
mode: 'lines+markers',
type: 'scatter'
};
var data = [trace1, trace2, trace3];
Plotly.newPlot('myDiv', data);
</script>
ここで、注目すべきは、plotly.newPlotに渡す引数です。
一つ目が、div idの値です。これで、どこの領域にグラフを描くのかを明らかにしています。
二つ目が、dataです。このdataを描画します。ここでは、データはJSONデータの形式で3つ渡されています。いずれも同じ形式です。
1) x: x軸のデータを配列として渡しています。
2) y: y軸のデータを配列として渡しています。
3) mode: ‘lines’, ‘markers’, ‘lines+markers’の3つが設定されています。
4) type: ‘scatter’が設定されています。
設定の詳細は、JavaScript Figure Reference: Single-Pageを参照ください。
デプロイし、ダッシュボード画面に移動すると以下の様なグラフが描かれています。
2) 凡例、軸タイトル付き散布図(Grouped Scatter Plot with Custom Scatter Gap)
次に、凡例と軸タイトルが付いた散布図の例として、HPのこちらにあるGrouped Scatter Plot with Custom Scatter Gapを描かせます。
<script></script>の間を書き換えればOKです。
<div id="myDiv">
<!-- Plotly chart will be drawn inside this DIV -->
</div>
<script>
var trace1 = {
x: ['South Korea', 'China', 'Canada'],
y: [24, 10, 9],
name: 'Gold',
type: 'scatter',
mode: 'markers'
};
var trace2 = {
x: ['South Korea', 'China', 'Canada'],
y: [13, 15, 12],
name: 'Silver',
type: 'scatter',
mode: 'markers'
};
var trace3 = {
x: ['South Korea', 'China', 'Canada'],
y: [11, 8, 12],
name: 'Bronze',
type: 'scatter',
mode: 'markers'
};
var data = [trace1, trace2, trace3];
var layout = {
scattermode: 'group',
title: 'Grouped by Country',
xaxis: {title: 'Country'},
yaxis: {title: 'Medals'},
scattergap: 0.7
};
Plotly.newPlot('myDiv', data, layout);
</script>
上で見た「折れ線と散布図」と比べると、Plotly.newPlot()の引数に、新たに”layout”が増えています。
layoutでは、scattermode, title, xaxis, yaxis scattegapの5つの情報が設定されています。
1) scattermode:同じ位置座標にある散布点をグラフ上にどのように表示するかを決定します。group “では、散布点は共有位置を中心に隣り合ってプロットされます。overlay “では、散布点は互いに重なってプロットされます。
2) title: グラフのタイトルを指定します。
3) xaixs: x軸の設定です。ここではtitleを設定しています。
4) yaixs: y軸の設定です。ここではtitleを設定しています。
5) scattergap: 隣接する位置座標の散布点間のギャップ(プロット分数)を設定します。
(scattergapは、ダッシュボードではうまく動作しないようです)
設定の詳細は、JavaScript Figure Reference: Single-Pageを参照ください。
デプロイ後、ダッシュボードに以下の様なグラフが描かれます。
4.msg.payloadのデータを渡してグラフ化する
ここまでは、plotly.jsの説明ということもあり、<script></script>の中にあらかじめ書かれたデータをグラフ化しました。次に、templateノードにmsg.payloadとして送られてきたデータをグラフ化します。
4.1. グラフ化の例
msg.payloadに来たデータをどのように、
Plotly.newPlot(‘myDiv’, data, layout);
のdataに渡すかがポイントになります。従って、x軸のデータとy軸のデータをそれぞれ配列形式で、msg.payloadにobjectとして格納して渡すことになります。
ここでは、y軸に0~1の間の乱数を1000倍したデータを使い、発生した時間をx軸データとして渡すことにします。
そのデータをtemplateノードでモニタして、前回から変化があれば、xとyのデータに設定してグラフを描くという動作をさせます。
基本的なフローは以下の様になります。(フローのサンプルは、最後に載せています)
① injectノードで1秒毎に次段のfunctionノードにmsg.payloadを送ります。
② functionノード”Random”で0~1の間の乱数を生成し、1000倍します。それをmsg.payload.dataに載せます。乱数の生成を参照ください。
③ Date/Time Formatterノードで、現在の日時をmsg.payload.datetimeに載せます。打刻とハードウエアIDの挿入を参照ください。
④ functionノード“配列”で10個の配列data[10]にmsg.payload.dataとmsg.payload.datetimeを格納します。
新しいデータが入ってきたら古いデータを破棄します。配列データの更新は配列データの平均・分散・標準偏差を参照ください。
⑤ templateノードの中身を説明します。
<div id="myDiv"></div>
<script>
(function(scope) {
// msg.payloadのデータを監視する
scope.$watch('msg.payload', (current, previous) => {
// グラフを描画する
var time = current.data.map(function(value){return new Date(value.datetime)});
var rdat = current.data.map(function(value){return(value.data)});
var data = {
x:time,
y:rdat,
name:'random_value',
mode: 'lines',
type: 'scatter'
}
const layout = {
title: "ランダム値",
xaxis: {title: '時間'},
yaxis: {title: 'ランダム値'}
};
Plotly.newPlot('myDiv', [data], layout);
});
})(scope);
</script>
1行目は、<div></div>でプロットする領域を指定しています。
2行目から24行目が<script></script>の記述領域です。
3行目から23行目は、即時関数(IIFE: Immediately Invoked Function Expression)と呼ばれるパターンで、scopeが関数に渡されます。
5行目でscopeの中身をチェックします。scope.$watch関数は、msg.payloadに変化がないかどうかを調べ、変化があった場合にはコールバック関数を実行します。msg.paylaodが以前と今回で異なる場合には、続く“{}”内が実行されます。その差にmsg.payloadは、valueとして渡されます。
7行目と8行目は、value(=msg.payload)から、datetimeを取出し“time”に代入します。同様にdataを取出し、”rdat”に代入します。
10行目と11行目でx:time, y:rdatに代入され、21行目で描画されます。今回は系列が1つしかないので、[data]で直接plotly.newPlot()に代入しています。
4.2. 縦軸を対数に変える
このグラフは、乱数を使って、0~1000までの数値を表示させています。そこで、縦軸を対数にしてみます。
ダッシュボードのChartノードでは、うまく縦軸を対数にできませんが、plotly.jsであれば簡単にできます。
yaxisの設定に、type: ‘log’とrange: [1,3]を追加するだけです。
rangeはautoscaleで良ければ不要です。
対数軸の場合のレンジ指定は少し異なります。10=10の1乗、1000=10の3乗ですので、range: [1,3]は、10~1000を指定していることになります。リニア軸と同じように[10, 1000]とすると、10の10乗~10の1000乗と認識されてしまうので注意が必要です。
yaxis: {
title: ‘ランダム値’,
type: ‘log’,
range: [1,3]
}
yaxisの設定を対数にして描画させると下のようなグラフになります。
対数グラフ版のフローを最後に載せます。動作を確認してみてください。
[{"id":"c0f1c5f862605a84","type":"comment","z":"3ddc84187f02f680","name":"Plotlyの描画","info":"","x":110,"y":120,"wires":[]},{"id":"78bda264ae5f0872","type":"inject","z":"3ddc84187f02f680","name":"1秒毎","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":160,"wires":[["271031cdc011f611"]]},{"id":"271031cdc011f611","type":"function","z":"3ddc84187f02f680","name":"Random","func":"msg.payload = {\"data\": Math.random() * 1000};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":160,"wires":[["dafe4620d0964305"]]},{"id":"dafe4620d0964305","type":"moment","z":"3ddc84187f02f680","name":"日付","topic":"","input":"","inputType":"date","inTz":"Asia/Tokyo","adjAmount":0,"adjType":"days","adjDir":"add","format":"YYYY-MM-DD hh:mm:ss","locale":"ja-JP","output":"payload.datetime","outputType":"msg","outTz":"Asia/Tokyo","x":390,"y":160,"wires":[["edc68ec8cd67b149"]]},{"id":"edc68ec8cd67b149","type":"function","z":"3ddc84187f02f680","name":"配列","func":"/* 配列操作ノード */\nif (!context.data) {\n context.data = new Array(30);\n for (var i=0; i<30; i++){\n var time = \"2023-12-06 12:00:\" + String(i);\n context.data[i] = { \"data\": 10, \"datetime\": time};\n }\n} \ncontext.data.shift();\nvar newData = {\"data\":msg.payload.data, \"datetime\":msg.payload.datetime};\ncontext.data.push(newData);\nmsg.payload = {\"data\":context.data};\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":160,"wires":[["7a48455ec15df1ee","a5889c2303cb5cb0"]]},{"id":"7a48455ec15df1ee","type":"ui_template","z":"3ddc84187f02f680","group":"3a6241538b5153aa","name":"","order":1,"width":"14","height":"9","format":"<div id=\"myDiv\"></div>\n<script>\n (function(scope) {\n // msg.payloadのデータを監視する\n scope.$watch('msg.payload', (current, previous) => {\n // グラフを描画する\n var time = current.data.map(function(value){return new Date(value.datetime)});\n var rdat = current.data.map(function(value){return(value.data)});\n var data = {\n x:time,\n y:rdat,\n name:'random_value',\n mode: 'lines',\n type: 'scatter'\n }\n const layout = {\n title: \"ランダム値\",\n xaxis: {title: '時間'},\n yaxis: {\n title: 'ランダム値',\n type: 'log',\n range: [1,3]\n }\n };\n Plotly.newPlot('myDiv', [data], layout);\n });\n })(scope);\n</script>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":640,"y":160,"wires":[[]]},{"id":"a5889c2303cb5cb0","type":"debug","z":"3ddc84187f02f680","name":"debug 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":640,"y":120,"wires":[]},{"id":"fb33c05b89ee8fd4","type":"ui_template","z":"3ddc84187f02f680","group":"","name":"Load Plotly CDN","order":2,"width":0,"height":0,"format":"<script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"global","className":"","x":130,"y":80,"wires":[[]]},{"id":"73684e2ca67deabc","type":"comment","z":"3ddc84187f02f680","name":"フローの何処かに配置必要","info":"","x":150,"y":40,"wires":[]},{"id":"3a6241538b5153aa","type":"ui_group","name":"linear","tab":"0bfddcc62b7cbad1","order":1,"disp":true,"width":"14","collapse":false,"className":""},{"id":"0bfddcc62b7cbad1","type":"ui_tab","name":"Test Chart","icon":"dashboard","disabled":false,"hidden":false}]
5. データベースに蓄えたデータでグラフを描こう!!
第4章の例では、乱数で生成したデータをグラフ化することを行いました。しかし、実際にはR-MSMからPDHを経由し、エッジサーバやクラウドのデータベースに蓄えたデータを出力してグラフを描くという用途が一番多いように思います。
第4章で説明したplotly.jsの基本をベースにデータベースMySQLのQueryを使ってデータを抽出し、グラフを描く内容を以下に載せています。ご活用ください。