2021/09/10
その2ではRaspberry Pi Zero Wにセンサーを取り付け、読み取った値をサーバにアップロードする設定を行いました。本編では、データベースに保存された値を読み取り、JavaScriptでWebブラウザ上にグラフを描画する設定を行います。
- 温度・湿度・気圧をBME280/Raspberry Pi/Azureで可視化 その1(AzureにWEBサーバとDBの準備編)
- 温度・湿度・気圧をBME280/Raspberry Pi/Azureで可視化 その2(Raspberry Pi Zero WにBME280取り付け・データ送信編)
- 温度・湿度・気圧をBME280/Raspberry Pi/Azureで可視化 その3(チャート描画編) ←本稿
グラフ描画に使うライブラリについて
WEBブラウザ上グラフが描けるJavaScriptのライブラリは、いくつかあります。今回はGoogle ChartsとCanvasJSの2つを使ってみました。
作成の参考にさせていただいたサイトです。
サーバサイドでグラフを作り描画するのではなく、サーバからは描画に必要なデータのみを取得し、ブラウザ上のJavaScriptでデータを組み立て描画するところがポイントです。今回の構成では、JavaScriptからPHPを呼び出し、MySQLから必要なデータを取り出しレスポンス、ブラウザ上でデータをもとにグラフを描画するという流れです。
WebAppsのPHPタイムゾーン設定
AzureWebAppsのPHP設定は、デフォルトタイムゾーンがUTCになっており、日本時間と9時間ずれていました。これを正しく直しておかないと、PHPで意図した時間を返してくれないので直します。phpファイル毎に全てタイムゾーンを入れるのも、結構な手間ですからね。
レンタルサーバやオンプレサーバであれば、php.iniで設定を変更できるのですが、WebAppsの場合どうやって変えるの分からず、地味にはまりました。
.user.ini ファイルをルート ディレクトリに追加します。
Azure App Service での PHP の構成方法
php.ini
ファイルで使用するものと同じ構文を使用して、構成設定を.user.ini
ファイルに追加します。
なるほど…。ルートディレクトリに[.user.ini]ファイルをつくって、同じ構文でタイムゾーンをいれればいいのかな?まあ、書いてる通りやってみよう。
レンタルサーバのphp.iniを参考に、[date.timezone = Asia/Tokyo]と入れて保存>アプリを再起動してみます。
タイムゾーンがUTCからAsia/Tokyoに変更されました。ダブルコーテーション「”」で括ると解説しているサイトもありましたが、入れなくても反映しました。phpinfo()で確認しました。
Google Charts版プログラム
上記参考サイトのコードを参考にコードをカスタマイズしました。
- 温度データを、1日/1週間/1ヶ月月/3ヵ月とセレクト。
- 1週間、1ヶ月、3ヵ月は1日の最高・平均・最低の3軸で表示。
chart1.html
<html>
<head>
<meta charset="UTF-8">
<title>センサーデータ</title>
<!--Load the AJAX API-->
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
// Load the Visualization API and the piechart package.
google.charts.load('current', {'packages':['corechart']});
// Set a callback to run when the Google Visualization API is loaded.
google.charts.setOnLoadCallback(drawChart);
function drawChart(showspan = 'Day') {
var urlWithGetData = "CreateJsonFromMySql1.php?range=" + showspan + "&target=temp";
var jsonData = $.ajax({
url: urlWithGetData,
dataType: "json",
async: false
}).responseText;
// Create our data table out of JSON data loaded from server.
var data = new google.visualization.DataTable(jsonData);
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
var options = {
title: 'Temperature report from BME280',
curveType: 'function',
height: 500,
hAxis: {title: 'Time', showTextEvery: 1, slantedText: true, slantedTextAngle: 90},
vAxis: {title: 'Temperature[℃]'},
legend: { position: 'bottom' }
};
chart.draw(data, options);
}
</script>
</head>
<body>
<!--Div that will hold the Line chart-->
<form>
<select name="showspan" onchange="drawChart(this.value)">
<option value="Day" selected="">Day</option>
<option value="Week">Week</option>
<option value="Month">Month</option>
<option value="3Month">3Month</option>
</select>
</form>
<div id="chart_div"></div>
</body>
</html>
CreateJsonFromMySql1.php
<?php
require_once 'DbManager.php';
require_once 'Encode.php';
$range = (isset($_GET['range'])) ? $_GET['range'] : 'Day';
$target = (isset($_GET['target'])) ? $_GET['target'] : 'temp';
$arryCol = [];
$arryRow = [];
switch ($range){
case '3Month':
case 'Month':
case 'Week':
// Date range for query. Week/Month rangeがWeekの場合7 Month30 3Month90を代入
$dayRange = (($range == 'Week') ? 7 : (($range == 'Month') ? 30 : 90));
$qryDate = strtotime("-{$dayRange} day");
// Variables to shorten recoring date 西暦と時刻を切り取り日付だけにする
$substrStart = 5;
$substrLen = 5;
// Column definition for GoogleCharts Table 配列のカラムに日付、最小値、平均値、最大値を代入
array_push($arryCol, MakeColElement('Date', 'string'));
array_push($arryCol, MakeColElement('min', 'number'));
array_push($arryCol, MakeColElement('ave', 'number'));
array_push($arryCol, MakeColElement('max', 'number'));
break;
case 'Day':
// rangeがDayの場合 クエリの一日前の日を2020-01-05 10:10:10形式で代入
$days = date("Y-m-d H:i:s", strtotime("-1 day"));
// Variables to shorten recoring date 西暦と日付部分を切り取り時刻だけにする
$substrStart = 11;
$substrLen = 5;
// Column definition for GoogleCharts Table
array_push($arryCol, MakeColElement('Time', 'string'));
array_push($arryCol, MakeColElement('Value', 'number'));
break;
default:
break;
}
try {
$db = GetDb();
switch ($range){
//MonthとWeekの場合
case '3Month':
case 'Month':
case 'Week':
for ($day=0; $day <$dayRange ; $day++) {
// prepare query string. min/ave/max will be targeted.
$days = date("Y-m-d", $qryDate);
$qry = "select datetime, min($target), avg($target), max($target) from sensordata where datetime like '$days%' ORDER BY id ASC";
$stt = $db->prepare($qry);
// call
$stt->execute();
$row = $stt->fetch(PDO::FETCH_ASSOC);
$rsltToJson = [substr($row['datetime'],$substrStart,$substrLen), $row['min('.$target.')'], $row['avg('.$target.')'], $row['max('.$target.')']];
array_push($arryRow, MakeRowElements($rsltToJson));
// + 1 day for next day.
$qryDate = strtotime("+1 day", $qryDate);
}
break;
case 'Day':
$qry = "select datetime, temp from sensordata where datetime >= '$days' ORDER BY id ASC";
$stt = $db->prepare($qry);
$stt->execute();
// loop till fetch all aquired values and store.
while($row = $stt->fetch(PDO::FETCH_ASSOC)) {
$rsltToJson = [substr($row['datetime'],$substrStart,$substrLen), $row[$target]];
array_push($arryRow, MakeRowElements($rsltToJson));
}
break;
}
// delete
$db = NULL;
}
catch (PDOEXception $e) {
die("connection error:{$e->getMessage()}");
}
// return created Json format.
echo ReturnGoogleChartsTable($arryCol, $arryRow);
function MakeColElement ($label, $type)
{
return '{"id":"","label":"'.$label.'"'.',"pattern":"","type":"'.$type.'"'.'}';
}
function MakeRowElements ($arry)
{
$ret = '{"c":[';
for ($i=0; $i < count($arry); $i++)
{
$element = '{"v":"'.$arry[$i].'","f":null}';
$ret .= ($i != (count($arry)-1)) ? $element.',' : $element ;
}
$ret = $ret.']}';
return $ret;
}
function ReturnGoogleChartsTable ($arryCol, $arryRow)
{
$ret = '{
"cols": ['
.ConnectArry($arryCol)
.'],'
.'"rows": ['
.ConnectArry($arryRow)
.']
}';
return $ret;
}
function ConnectArry($arry)
{
$ret = '';
for ($i=0; $i < count($arry); $i++)
{
$ret .= ($i != (count($arry)-1)) ? $arry[$i].',' : $arry[$i] ;
}
return $ret;
}
?>
保存したPHPプログラムにブラウザでアクセスすると、デフォルトパラメータのtempとdayが選択され、JSON形式のデータが出力されます。このデータをJavaScriptでグラフとして描画しています。
CanvasJS版プログラム
上記参考サイトのコードを参考に、カスタマイズしました。
- 温度・湿度・気圧を1つのグラフに表示できるように3軸グラフとした。
- 1日/1週間/1か月/1か月平均の4つセレクト。
実際のデータはこのように見えます(デモ)。
chart2.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>センサーデータC</title>
<script type="text/javascript" src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript">
function drawChart(selectspan) {
var temp_data;
//データをjson形式で取得し、temp_dataという変数に代入する
$.ajax({
url: "CreateJsonFromMySql2.php", //温度データ取得のURL
type: "GET", //GETメソッドで取得
dataType: 'json', //処理結果はjson形式で受信
async: false, //非同期通信
data: {
target: 'temp',
range: selectspan
},
success: function(json1) { temp_data = json1;}
});
//データをjson形式で取得し、hum_dataという変数に代入する
var hum_data;
$.ajax({
url: "CreateJsonFromMySql2.php",
dataType: 'json',
async: false,
data: {
target: 'hum',
range: selectspan
},
success: function(json1) { hum_data = json1;}
});
//データをjson形式で取得し、press_dataという変数に代入する
var press_data;
$.ajax({
url: "CreateJsonFromMySql2.php",
dataType: 'json',
async: false,
data: {
target: 'press',
range: selectspan
},
success: function(json1) { press_data = json1;}
});
var chart = new CanvasJS.Chart(chartContainer01, {
title: {text: "BEM280 温度・湿度・気圧データ"},
axisX: { labelAngle: -90, labelFontSize: 14, labelFontColor: '#222' },
axisY:[{
title: "Tempreture",
lineColor: "#369EAD",
tickColor: "#369EAD",
labelFontColor: "#369EAD",
titleFontColor: "#369EAD",
//includeZero: false,
suffix: "℃"
},
{
title: "Humidity",
lineColor: "#C24642",
tickColor: "#C24642",
labelFontColor: "#C24642",
titleFontColor: "#C24642",
includeZero: false,
suffix: "%"
}],
axisY2: {
title: "Pressure",
lineColor: "#7F6084",
tickColor: "#7F6084",
labelFontColor: "#7F6084",
titleFontColor: "#7F6084",
includeZero: false,
suffix: "hPa"
},
toolTip: {shared: true},
legend: {cursor: "pointer",itemclick: toggleDataSeries},
//axisY: { title: "temperature", suffix: '℃',axisYIndex: 0, labelFontSize: 14, labelFontColor: '#222' },
theme: "light1", //デフォルトテーマに設定
data: [{
type: 'line', //温度グラフの種類
name: "temperature",
color: "#369EAD",
showInLegend: true,
axisYIndex: 0,
dataPoints: temp_data //表示するデータ
},
{
type: 'line', //湿度グラフの種類
name: "humidity",
color: "#C24642",
axisYIndex: 1,
showInLegend: true,
dataPoints: hum_data //表示するデータ
},
{
type: 'line', //気圧グラフの種類
name: "pressure",
color: "#7F6084",
axisYType: "secondary",
showInLegend: true,
dataPoints: press_data //表示するデータ
}]
});
chart.render();
//マウスカーソル一つに3つの値を表示する設定
function toggleDataSeries(e) {
if (typeof (e.dataSeries.visible) === "undefined" || e.dataSeries.visible) {
e.dataSeries.visible = false;
} else {
e.dataSeries.visible = true;
}
e.chart.render();
}
}
</script>
</head>
<body onload= drawChart('Day')>
<form>
<select name="selectspan" onchange="drawChart(this.value)">
<option value="Day" selected="">1日</option>
<option value="Week">1週間</option>
<option value="Month">1ヵ月</option>
<option value="Average">1ヵ月平均</option>
</select>
</form>
<div id="chartContainer01" style="height: 450px; width: 100%;"></div><br>
</body>
</html>
CreateJsonFromMySql2.php
<?php
require_once 'DbManager.php';
require_once 'Encode.php';
//Day,Week,Month,Averageのいずれかで表示期間変動版
//getでtemp,hum,pressのどれかを取得
$target = isset($_GET['target']) ? $_GET['target'] : 'temp';
$range = isset($_GET['range']) ? $_GET['range'] : 'Day';
//Day,Week,MonthのいずれかでdayRangeに代入
switch ($range){
case 'Average':
case 'Month':
$dayRange = 30;
break;
case 'Week':
$dayRange = 7;
break;
case 'Day':
$dayRange = 1;
break;
default:
break;
}
$day = date("Y-m-d H:i:s",strtotime("-$dayRange day"));
$qryDate = strtotime("-{$dayRange} day");
$db = GetDb();
//Averageの場合とそれ以外(Day,Week,Month)で処理を分ける
switch ($range){
case 'Average':
//ここに平均値の処理を書く
for ($day=0; $day <$dayRange ; $day++) {
$days = date("Y-m-d", $qryDate);
$sth = $db->prepare("SELECT datetime,avg($target) FROM sensordata where datetime like '$days%' ORDER BY id ASC");
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
$jsonData[] = [
'label'=>substr($row['datetime'],5,5),
'y'=>(float)round($row['avg('.$target.')'],2)
];
// + 1 day for next day.
$qryDate = strtotime("+1 day", $qryDate);
}
break;
case 'Month':
case 'Week':
case 'Day':
$sth = $db->prepare("SELECT datetime,$target FROM sensordata where datetime >= '$day' ORDER BY id ASC");
$sth->execute();
while($row = $sth->fetch(PDO::FETCH_ASSOC)){
$jsonData[]=[
'label'=>substr($row['datetime'],5,11),
'y'=>(float)$row[$target]
];
}
break;
}
//jsonとして出力
header('Content-type: application/json');
echo json_encode($jsonData);
?>
JavaScript側からPHPにクエリを渡す際、例えば「気圧」を「1週間分」の時、呼び出すURLはCreateJsonFromMySql2.php?target=press&range=Weekのようになります。クエリストリングの「?」に続けて、対象「target=press」と期間「range=Week」を付与しリクエストします。PHP側ではGETメソッド$_GET[‘target’]、$_GET[‘range’]でクエリの内容を把握します。このグラフは3軸なので、温度、湿度、気圧と3回クエリを送付するようにしました。以下の画像はブラウザの開発者ツール(F12を選択)で表示できます。
保存したPHPプログラムにブラウザでアクセスすると、デフォルトパラメータのtempとdayが選択され、JSON形式のデータが出力されます。このデータをJavaScriptでグラフとして描画しています。
3軸グラフを作るにあたって、公式マニュアルにあったソースコードを参考にしました。またグラフの色、配置、表示方法等マニュアルに細かに解説してあります。英語ですが、根気よく探すと自分の望む表示方法がきっと見つかると思います。
あとがき
コロナの外出自粛を受けて、家でできるテーマを決めて、じっくり取り組んでみようと思いやってみました。プログラミングの知識はワードプレスのカスタマイズでPHPをちょっといじった事がある、素人に毛が生えた程度でしたが、調べながらなんとかここまで完成させることが出来ました。
Python、PHP、JavaScript、どれも参考にさせていただいたサイトに出会わなかったら、全くできなかったと思います。やり方はいろいろあり、これがすべてではないと思いますので、少しでも私の記事が参考になれば幸いです。