LoginSignup
9
11

More than 5 years have passed since last update.

HighchartsでI2Cセンサーが取得した温度をグラフにする

Last updated at Posted at 2017-03-19

I2Cセンサ-で拾った温度をHighchartsでグラフ化してみます。

1.システム構成

今回のシステム構成例を下図に示します。
i2c_センサ構成図.png

1.1 Raspberry Pi

I2Cセンサーと接続し1秒毎に温度を測定します。その後、データベースに測定した値を送ります。C++でプログラムしました。

1.2 Linuxサーバー

センサーの値を記録するデータベースサーバーです。PHPスクリプトによりWebブラウザとのインターフェースを提供します。SQLの問い合わせ文とPHPでプログラムしました。

1.3 AT互換機

いわゆるパーソナルコンピュータです。HighchartsというJavaScriptのグラフ作成APIを使ってセンサーの値をグラフ化します。JavaScriptを使ってデータベースアクセス部をプログラムしました。

2. 作成したコードの紹介

書いたプログラムを各ちほーに分けて紹介します。

2.1 C++ちほー

RaspberyPiで動作するプログラムです。前回記事を書いた時から変更した部分を主に紹介します。
ソースコードをgithubに置きました。https://github.com/ihal/i2c_sensor

2.1.1 1秒毎のスリープ

sleep(1)でも、かなり正確に1秒間スリープするのですが、やはり誤差の累積は避けられません。そこでSystem Clockと同期を取るようにしました。なおRaspberryPiはNTPでLinuxサーバー経由で上位NTPサーバーと時刻同期する様に設定しています。

sys_governor.cpp
#include <iostream>
#include <thread>
#include <chrono>

void governor() {
  std::chrono::milliseconds t1(0);
  std::chrono::milliseconds sleep_time(1000);

  t1=std::chrono::duration_cast<std::chrono::milliseconds>(
    std::chrono::system_clock::now().time_since_epoch()
  );
  sleep_time= std::chrono::milliseconds(500)+(std::chrono::milliseconds(1000) - (t1 % std::chrono::milliseconds(1000)));

  std::this_thread::sleep_for( std::chrono::milliseconds(sleep_time));
}

2.1.1.1 教えてください。

できれば0秒丁度でタイマー割り込みしたいのですが、方法が解りません。
どなたかご教授頂けないでしょうか。

2.1.2 データベースへ測定値を送る。

libpqxxライブラリを使ってセンサーの測定値をデータベースPostgreSQLへ送ります。

sys_postgres.cpp
#include "sys_postgres.h"
#include "sys_log.h"
using namespace std;
using namespace pqxx;
void DB::Connect() {
   try{
      C= new connection("dbname=db01 user=DBユーザ名 password=DBパスワード "
                   "hostaddr=LinuxサーバーのIPアドレス port=5432");
      if (C->is_open()) {
         cout << "Opened database successfully: " << C->dbname() << endl;
      } else {
         cerr << "Can't open database" << endl;
      }
   }
   catch (const std::exception &e){
      cerr << e.what() << std::endl;
   }
}

void DB::Reconnect() {
  try {
    if(!C->is_open()) {
        C->activate();
    }
  }
  catch(const pqxx::broken_connection &e) {
    return;
  }
  cout << "Reconncted database successfully: "<< C->dbname() << endl;
}
void DB::Insert(class LOG l) {
  if(!C->is_open()) return;//開いてなければ何もしない。
  /* Create SQL statement */
  string sql;

  sql ="INSERT INTO log1 VALUES ( "+l.Compose()+");";
//  cout <<sql <<endl;
  try{
    /* Create a transactional object. */
    work W(*C);
    /* Execute SQL query */
    W.exec( sql );
    W.commit();
  }
  catch (const std::exception &e){
    cout << e.what() << std::endl;
  }
}
void DB::Disconnect() {
  C->disconnect();
  delete C;
  cout << "Closed database." << endl;
}

2.1.3 センサーの値を保持する。

センサーの値を保持するクラスです。現在時刻の取得や書式も管理しています。

sys_log.cpp
#include "sys_log.h"
using namespace std;
string LOG::Compose() {
    ostringstream ost;

    ost << "'"
        << setfill('0')
        << setw(4)
        << year
        << "-"
        << setw(2)
        << month
        << "-"
        << setw(2)
        << day
        << " "
        << setw(2)
        << hour
        << ":"
        << setw(2)
        << minute
        << ":"
        << setw(2)
        << second
        << "+9" << "', "
        << fixed<<setprecision(0)
        << temp1*100 << ","
        << humi1*100 << ","
        << (pres2-1000)*100 << ","
        << temp2*100 << ","
        << (pres3-1000)*100 << ","
        << temp3*100
        ;
    return ost.str();
}
void LOG::Write( 
                 double _temp1,double _humi1,
                 double _pres2,double _temp2,
                 double _pres3,double _temp3
               ) {
    temp1 = _temp1;
    humi1 = _humi1;
    pres2 = _pres2;
    temp2 = _temp2;
    pres3 = _pres3;
    temp3 = _temp3;
}

void LOG::GetTime() {
  //現在時刻の取得
  time(&timer);
  date   = localtime(&timer);

  year   = date->tm_year+1900;
  month  = date->tm_mon+1;
  day    = date->tm_mday;
  hour   = date->tm_hour;
  minute = date->tm_min;
  second = date->tm_sec;

}

2.1.4 メインループ

メインループは1秒間に1回、I2Cセンサーの値を読むを繰り返します。CTRL-C(SIGINT)で停止します。

i2c_logger.cpp
#include <iostream>
#include <thread>
#include <signal.h>

#include "dev_hdc1000.h"
#include "dev_mpl115a2.h"
#include "dev_ms5607.h"

#include "sys_governor.h"
#include "sys_log.h"
#include "sys_display.h"
#include "sys_postgres.h"

// using namespace std;
static int sig =0;
void sig_handler(int signo)
{
  sig=1;
}
int main()
{
  HDC1000 hdc1000_1;
  MPL115A2 mpl115a2_1;
  MS5607 ms5607_1;

  DISPLAY d;
  LOG     l;
  DB      db;

  hdc1000_1.Init();
  mpl115a2_1.Init();
  ms5607_1.Init();

  std::cout << "Hello\nWorld!\n";
  db.Connect();

  if( signal(SIGINT, sig_handler) == SIG_ERR) {
    std::cerr << "sig_handler fail."<<std::endl;
  }

  while(1) {
    std::thread th0(governor);
    //
    std::thread th1([&]{hdc1000_1.Get();});
    std::thread th2([&]{mpl115a2_1.Get();});
    std::thread th3([&]{ms5607_1.Get();});
    //
    l.GetTime();//現在時刻の取得
    //
    th1.join();
    th2.join();
    th3.join();
    //
    l.Write(
      hdc1000_1.Read(0),//Temp
      hdc1000_1.Read(1),//Humi
      mpl115a2_1.Read(1),
      mpl115a2_1.Read(0),
      ms5607_1.Read(1),//
      ms5607_1.Read(0) //Temp
    );
    // 表示
    d.Disp(l);
    // DBインサート
    db.Insert(l);
    //  Wait
    th0.join();
    //  Terminate
    if(sig) {
      std::cout <<"Recieve SIGINT\n";
      db.Disconnect();
      std::cout <<std::flush;
      break;
    }
  }
}

2.1.5 Makefile

Makefileです。

Makefile

CXX = g++
TARGET = i2c_logger
CXXFLAGS = -Wall -O2 -pthread -std=c++0x -pipe
SRCS = i2c_logger.cpp dev_hdc1000.cpp dev_ms5607.cpp dev_mpl115a2.cpp sys_governor.cpp sys_display.cpp sys_log.cpp sys_postgres.cpp
OBJS := $(SRCS:.cpp=.o)
LIBS = -lwiringPi -lpqxx -lpq
$(TARGET): $(OBJS)
    $(CXX) -o $@ $(OBJS) $(LIBS)
clean:
    rm -f $(TARGET) $(OBJS)

2.2 SQLちほー

Linuxサーバーで動作するPostgreSQLのデータ形式とPHPのプログラムを紹介します。
PostgreSQLでRaspberyPiから送られてくるセンサーの値をデータベースに保管します。PHPはユーザーが要求する期間のセンサーの値をデータベースから検索し、JSON形式で返します。

2.2.1 PostgreSQLの設定

PostgreSQLが使用できる状態と仮定します。インストールの仕方については省略します。ソースからmakeする時は、
bash
./configure --enable-nls --with-libxml --with-libxslt

とすると日本語化されます。使用したVersionは(9.6.2)です。

bashから
psql -U postgres
postgres=# SELECT version();
                                                    version
-----------------------------------------------------------------------------------------------------------------
 PostgreSQL 9.6.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.4.5 20101001 (Vine Linux 4.4.5-6.1vl6), 64-bit

psqlコマンドは\qで終了します。


最初に、データを入れるDBを作成します。rootからsuでpostgresユーザーになり、createdbでDBを作成します。DB名はdb01とします。

bash
su - postgres
createdb db01

次に作成したDBにpsqlコマンドでユーザー名postgresでアクセスします。

bash
psql -U postgres db01

次にPostgreSQL上でのユーザー(みたいなもの?)を作成します。例としてユーザ名をDBユーザー名、パスワードをDBパスワードとします。

psql
CREATE ROLE DBユーザー名 WITH LOGIN PASSWORD 'DBパスワード';

CREATE ROLEと帰ってくれば成功です。

psql
db01-#

というプロンプトが表示されている場合は 末尾のセミコロンを忘れずに入力しているか確認してください。


次にI2Cセンサーの値を入れるテーブルを作成します。テーブル名はlog1とします。

psql
CREATE TABLE log1 (
    date timestamp with time zone,
    temp1 smallint,
    humi1 smallint,
    pres2 smallint,
    temp2 smallint,
    pres3 smallint,
    temp3 smallint,
    ts_now timestamp with time zone DEFAULT now(),
    id serial NOT NULL
);

このテーブルへの検索(select)とデータの挿入(insert)の権限をDBユーザー名に与えます。

psql
GRANT SELECT,INSERT on log1 to DBユーザー名;

以上で、RaspberryPi側のc++プログラムを起動すればI2Cセンサーの値が1秒毎にテーブルに入ってきます。確認するには下記のSQL問い合わせをします。

psql
SELECT * FROM log1 WHERE date>= now() - interval '10 second';
         date          | temp1 | humi1 | pres2 | temp2 | pres3 | temp3 |            ts_now             |    id
------------------------+-------+-------+-------+-------+-------+-------+-------------------------------+----------
 2017-03-19 13:11:28+09 |  4444 |  1449 |  1700 |  4145 | -2165 |  2143 | 2017-03-19 13:11:29.001925+09 | 11500908
 2017-03-19 13:11:29+09 |  4444 |  1449 |  1832 |  4145 | -2164 |  2143 | 2017-03-19 13:11:30.001982+09 | 11500909
 2017-03-19 13:11:30+09 |  4442 |  1449 |  1528 |  4126 | -2163 |  2143 | 2017-03-19 13:11:31.001955+09 | 11500910
 2017-03-19 13:11:31+09 |  4445 |  1449 |  1569 |  4145 | -2163 |  2143 | 2017-03-19 13:11:32.002155+09 | 11500911
 2017-03-19 13:11:32+09 |  4442 |  1449 |  1659 |  4126 | -2163 |  2143 | 2017-03-19 13:11:33.002147+09 | 11500912
 2017-03-19 13:11:33+09 |  4444 |  1449 |  1659 |  4126 | -2164 |  2143 | 2017-03-19 13:11:34.002217+09 | 11500913
 2017-03-19 13:11:34+09 |  4445 |  1449 |  1569 |  4145 | -2168 |  2143 | 2017-03-19 13:11:35.002218+09 | 11500914
 2017-03-19 13:11:35+09 |  4444 |  1449 |  1528 |  4126 | -2166 |  2143 | 2017-03-19 13:11:36.001324+09 | 11500915
 2017-03-19 13:11:36+09 |  4444 |  1449 |  1659 |  4126 | -2162 |  2143 | 2017-03-19 13:11:37.00138+09  | 11500916
(9 )

2.2.2 集計テーブルの作成

次に分、時、日毎の平均値による集計テーブルを作成します。VIEWで作成すると表示までに時間が掛かりますのでPostgreSQL9.3からの新機能マテリアライズドビューを使ってみます。

まず分毎の集計をテーブル名log1_minuteとして作成します。分毎のテーブル作成は処理に時間が掛かります。

psql
CREATE MATERIALIZED VIEW log1_minute AS (
SELECT 
 CAST(date_nsec||':00' AS TIMESTAMP WITH TIME ZONE) AS date,
 CAST(temp1 AS INT2) AS temp1,
 CAST(humi1 AS INT2) AS humi1,
 CAST(pres2 AS INT2) AS pres2,
 CAST(temp2 AS INT2) AS temp2,
 CAST(pres3 AS INT2) AS pres3,
 CAST(temp3 AS INT2) AS temp3,
 id                  AS id
FROM (
SELECT
/* 60秒(1分)単位 */
 SUBSTRING(CAST(date AS VARCHAR) ,1 ,16) AS date_nsec,
 AVG(temp1) AS temp1,
 AVG(humi1) AS humi1,
 AVG(pres2) AS pres2,
 AVG(temp2) AS temp2,
 AVG(pres3) AS pres3,
 AVG(temp3) AS temp3,
 MIN(id)    AS id
 FROM log1
 GROUP BY date_nsec
 ORDER BY date_nsec
) AS LOG_TEMP
);

次にインデックスを作成します。

psql
CREATE UNIQUE INDEX index_minute on log1_minute(id);

忘れずに作成したDBユーザー名にGRANTしておきましょう。

psql
GRANT SELECT ON log1_minute TO DBユーザー名;

以下、同様に時、日毎の集計テーブルを作成します。

psql
CREATE MATERIALIZED VIEW log1_hour AS (
SELECT 
 CAST(date_nsec||':00:00' AS TIMESTAMP WITH TIME ZONE) AS date,
 CAST(temp1 AS INT2) AS temp1,
 CAST(humi1 AS INT2) AS humi1,
 CAST(pres2 AS INT2) AS pres2,
 CAST(temp2 AS INT2) AS temp2,
 CAST(pres3 AS INT2) AS pres3,
 CAST(temp3 AS INT2) AS temp3,
 id                  AS id
FROM (
SELECT
/* 1時間単位 */
 SUBSTRING(CAST(date AS VARCHAR) ,1 ,13) AS date_nsec,
 AVG(temp1) AS temp1,
 AVG(humi1) AS humi1,
 AVG(pres2) AS pres2,
 AVG(temp2) AS temp2,
 AVG(pres3) AS pres3,
 AVG(temp3) AS temp3,
 MIN(id)    AS id
 FROM log1_minute
 GROUP BY date_nsec
 ORDER BY date_nsec
) AS LOG_TEMP
)
;
CREATE UNIQUE INDEX index_hour on log1_hour(id);
GRANT SELECT ON log1_hour TO DBユーザー名;

/*****************************************************/
CREATE MATERIALIZED VIEW log1_day AS (
SELECT 
 CAST(date_nsec||' 00:00:00+09' AS TIMESTAMP WITH TIME ZONE) AS date,
 CAST(temp1 AS INT2) AS temp1,
 CAST(humi1 AS INT2) AS humi1,
 CAST(pres2 AS INT2) AS pres2,
 CAST(temp2 AS INT2) AS temp2,
 CAST(pres3 AS INT2) AS pres3,
 CAST(temp3 AS INT2) AS temp3,
 id                  AS id
FROM (
SELECT
/* 1日単位  日本時間+09のTZでselectされる事を前提とする。*/
 SUBSTRING(CAST(date AS VARCHAR) ,1 ,10) AS date_nsec,
 AVG(temp1) AS temp1,
 AVG(humi1) AS humi1,
 AVG(pres2) AS pres2,
 AVG(temp2) AS temp2,
 AVG(pres3) AS pres3,
 AVG(temp3) AS temp3,
 MIN(id)    AS id
 FROM log1_hour
 GROUP BY date_nsec
 ORDER BY date_nsec
) AS LOG_TEMP
)
;
CREATE UNIQUE INDEX index_day on log1_day(id);
GRANT SELECT ON log1_day TO DBユーザー名;


マテリアライズドビューは下記SQLで適時更新する必要があります。
尚、REFRESH MATERIALIZED VIEW CONCURRENTLYはPostgreSQL9.4からの機能です。

psql
REFRESH MATERIALIZED VIEW CONCURRENTLY log1_minute;
REFRESH MATERIALIZED VIEW CONCURRENTLY log1_hour;
REFRESH MATERIALIZED VIEW CONCURRENTLY log1_day;

2.2.3 WEBアプリケーションから検索する為のPHPスクリプト

WEBアプリケーションからPostgreSQLの検索を行う為のPHPスクリプトです。
Linuxサーバーのpublic_html内へ後で作成するHighchartのJavaScriptと同じディレクトリに置きます。HighStockの例題lazy-loadingのphpプログラム from-sql.phpを参考に作成しました。
PHPからPostgreSQLへアクセスするのでphp5-pgsqlを予めインストールしておきます。

apt-get install php5-pgsql

下記が今回のphpコードです。

json4.php
<?php
/**
 * 
 * @param callback {String} The name of the JSONP callback to pad the JSON within
 * @param start {Integer} The starting point in JS time
 * @param end {Integer} The ending point in JS time
 */

ini_set("display_errors", On);
error_reporting(E_ALL);

$tz=date_default_timezone_get();
//date_default_timezone_set('Asia/Tokyo');
//setlocale( LC_TIME, "ja_JP.utf8" ); // ロケールを日本に設定

if (PHP_INT_SIZE != 8) {
  die('This script need 64bit INT!');//2038年問題回避の為
}

// get the parameters
$callback = $_GET['callback'];
if (!preg_match('/^[a-zA-Z0-9_]+$/', $callback)) {
    die('Invalid callback name');
}
$start = @$_GET['start'];
if ($start && !preg_match('/^[0-9]+$/', $start)) {
    die("Invalid start parameter: $start");
}
$end = @$_GET['end'];
if ($end && !preg_match('/^[0-9]+$/', $end)) {
    die("Invalid end parameter: $end");
}
if (!$end) $end = time() * 1000;
if (!$start) {
//  $startTime = "2016-05-01 00:00:00(JST)";
  $start=1462028400*1000;
}

$start1=$start;
$end1=$end;


// PHPに渡される引数は協定世界時(UTC)

// set some utility variables
$range = $end - $start;

// find the right table
// 1 hour range loads second data
if ($range < 1 * 60 * 60 * 1000) {
    $table = 'log1';
    $interval=1*1000;//1秒
// two days range loads minute data
} elseif ($range < 2 * 24 * 3600 * 1000) {
    $table = 'log1_minute';
    $interval=60*1000;//1分
// one month range loads hourly data
} elseif ($range < 31 * 24 * 3600 * 1000) {
    $table = 'log1_hour';
    $interval=60*60*1000;//1時間
// one year range loads daily data
} elseif ($range < 15 * 31 * 24 * 3600 * 1000) {
    $table = 'log1_day';
    $interval=24*60*60*1000;//1日
// greater range loads monthly data
} else {
//  $table = 'stockquotes_month';
    $table = 'log1_day';
    $interval=24*60*60*1000;//1日
} 


// normalize
date_default_timezone_set('UTC');
// DBへグリニッジ標準時GMT(協定世界時UTC)で時間指定
if ($interval == 1 * 1000) { // second
  $timeFormat = 'Y-m-d H:i:s';
} elseif ($interval == 60*1000) { // minute
  $timeFormat = 'Y-m-d H:i:00';
} elseif ($interval == 60*60*1000) { // hour
  $timeFormat = 'Y-m-d H:00:00';
} elseif ($interval == 24*60*60*1000) { // day
  $timeFormat = 'Y-m-d 15:00:00'; //日本時間 午前0時
} else {
  $timeFormat = 'Y-m-d 15:00:00'; //日本時間 午前0時
}
$startDateTime = new DateTime();
$startDateTime->setTimestamp(intVal($start/1000));

$endDateTime = new DateTime();
$endDateTime->setTimestamp(intVal($end/1000));

$strp = $startDateTime->format($timeFormat);
$startDateTime = new DateTime($strp);
$strp = $endDateTime->format($timeFormat);
$endDateTime = new DateTime($strp);


/*
//Start時間の端数は切り上げ処理
$normal = intVal($startDateTime->format('U'))*1000;
if( $start>$normal) {
  $startDateTime->setTimestamp(($normal+$interval)/1000);
} else {
  $startDateTime->setTimestamp($normal/1000);
}
*/

$start = intVal($startDateTime->format('U'))*1000;
$end   = intVal($endDateTime->format('U'))*1000;
$startTime = $startDateTime->format('Y-m-d H:i:s+00');
$endTime   = $endDateTime->format('Y-m-d H:i:s+00');


date_default_timezone_set($tz);


// SQL
$sql = "SELECT
          EXTRACT(EPOCH FROM date) AS unixtime,
          temp1
         FROM $table
         WHERE date between
           '$startTime' AND
           '$endTime'
         ORDER BY unixtime
         LIMIT 5000
";

// DB ACCESS
$dsn = 'pgsql:dbname=db01 host=localhost port=5432';
$user = 'DBユーザー名';
$password = 'DBパスワード';


// print it
header('Content-Type: text/javascript');
echo "/* ".$sql."*/\n";

try{
    $dbh = new PDO($dsn, $user, $password);
    $stt = $dbh->query($sql);
    $rows = array();
    while($row = $stt->fetch(PDO::FETCH_ASSOC)){
      $r1=$row['unixtime'] * 1000;
      $r2=$row['temp1'] / 100;
      $rows[intVal($r1)]= $r2 ;// Key => Value
    }
//正規化前 引数通り
date_default_timezone_set('UTC');
$debugDateTime = new DateTime();
$debugDateTime->setTimestamp(intVal($start1/1000));
$startTime1=$debugDateTime->format('Y-m-d H:i:s');
$debugDateTime->setTimestamp(intVal($end1/1000));
$endTime1=$debugDateTime->format('Y-m-d H:i:s');
date_default_timezone_set($tz);
echo "/* console.log(' start = $start1, end = $end1, startTime = $startTime1, endTime = $endTime1 '); */"."\n";

//echo "/*\n";
//print_r($rows);
//echo "*/\n";

//正規化後 引数修正
    echo "/* console.log(' start = $start, end = $end, startTime = $startTime, endTime = $endTime table = $table'); */";

//    echo $callback ."(\n".json_encode($tempData)."\n);";
// . join(",\n",$rows) . "\n]);";
    echo $callback . "([\n";
    $i=intVal($start);
    $debugDateTime = new DateTime();
    while(1) {
      if( array_key_exists( intVal($i), $rows)) {
        echo "[".$i.",".$rows[$i]."]";
      } else {
        echo "[".$i.","."null"."]";//欠損データはnullとする。
      }
      $debugDateTime->setTimestamp(intVal($i/1000));
      $debugStr=$debugDateTime->format('Y-m-d H:i:s');
      $i=$i+$interval;
      if( $i <= intVal($end)) {
        echo ",";
      } else {
        break;
      }
echo "  /*".$debugStr."(JST)*/";
      echo "\n";
    }
echo "  /*".$debugStr."(JST)*/";
    echo "\n";
    echo "]);";
}catch (PDOException $e){
    print('Error:'.$e->getMessage());
    die();
}
$dbh = null;
?>

2.3 JavaScriptちほー

最後にグラフ化です。Highcartsを使った簡単な1秒毎のリアルタイム更新グラフと、Highstockを使った月単位のトレンドグラフを作成します。尚、データベース上は、湿度、大気圧も取得していますが、今回グラフ化するのは温度だけです。

2.3.1 リアルタイム更新グラフ

Highchartsの例題Spline updating each secondを改造して1秒毎の温度をグラフ化してみます。

test7.html
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Highcharts Example</title>

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
        <style type="text/css">
${demo.css}
        </style>
        <script type="text/javascript">
$(function () {
    $(document).ready(function () {
        Highcharts.setOptions({
            global: {
                useUTC: false
            }
        });

        $('#container').highcharts({
            chart: {
                type: 'spline',
                animation: Highcharts.svg, // don't animate in old IE
                marginRight: 10,
                events: {
                    load: function () {
                        // set up the updating of the chart each second
                        var series = this.series[0];
                        setInterval(function () {
//                            var x = (new Date()).getTime(); // current time
                            var x = (new Date()).getTime()-1*1000; // 1秒前
                            var y = Math.random();
/*
                            series.addPoint([x, y], true, true);
*/
                            $.getJSON('json4.php?start=' + Math.ceil(x) +
                               '&end=' + Math.ceil(x) + '&callback=?', function (data,status) {
                                series.addPoint(data[0], true, true);
//                                console.log('date='+data+'length='+data.length+' x='+x+' y='+y);
//                                console.log('date='+data[0][0]+'length='+data[0].length+' x='+x+' y='+y);
                            });
                        }, 1000);
                    }
                }
            },
            title: {
                text: 'Live random data'
            },
            xAxis: {
                type: 'datetime',
                tickPixelInterval: 150
            },
            yAxis: {
                title: {
                    text: 'Value'
                },
                plotLines: [{
                    value: 0,
                    width: 1,
                    color: '#808080'
                }]
            },
            tooltip: {
                formatter: function () {
                    return '<b>' + this.series.name + '</b><br/>' +
                        Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' +
                        Highcharts.numberFormat(this.y, 2);
                }
            },
            legend: {
                enabled: false
            },
            exporting: {
                enabled: false
            },
            series: [{
                name: 'Random data',
                data: (function () {
                    // generate an array of random data
                    var data = [],
                        time = (new Date()).getTime(),
                        i;

                    for (i = -19; i <= 0; i += 1) {
                        data.push({
                            x: time + i * 1000,
                            y: Math.random()
                        });
                    }
                    return data;
                }())
            }]
        });
    });
});
        </script>
    </head>
    <body>

<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>

<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>

    </body>
</html>

例題の35行目から40行目付近の1秒間隔の処理プログラム内のseries.addPointを下記、json4.phpからデータを持ってくるプログラムに置き換えています。

データを持ってくるプログラム
                            $.getJSON('json4.php?start=' + Math.ceil(x) +
                               '&end=' + Math.ceil(x) + '&callback=?', function (data,status) {
                                series.addPoint(data[0], true, true);

Webブラウザからtest7.htmlにアクセスすると下記の様な1秒更新グラフが作成出来ます。

無題1.png

2.3.2 月単位のグラフ

次はHighstockの例題1.7 million points with async loadingを改造して1秒毎の温度をグラフ化してみます。

test6.html
<!--
-->
<!-- jsファイル読み込み -->
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js"></script>


<script type="text/javascript" src="chart4.js"></script>

<!-- チャート表示 -->
<div id="container"></div>

test6.htmlと同じディレクトリに下記JavaScriptのコードを置きます。

chart4.js
/*
TEST6用
*/

$(function() {
    /**
     * Load new data depending on the selected min and max
     */
    function afterSetExtremes(e) {
        var chart = $('#container').highcharts();
        chart.showLoading('Loading data from server...');
/*
        $.getJSON('json4.php?start=' + Math.round(e.min) +
                '&end=' + Math.round(e.max) + '&callback=?', function (data,status) {
*/
        $.getJSON('json4.php?start=' + Math.ceil(e.min) +
                '&end=' + Math.ceil(e.max) + '&callback=?', function (data,status) {


                chart.series[0].setData(data);
                chart.hideLoading();
            });
    }
    function utc2dateString(utc_msec) {
      d=new Date();
      d.setTime(utc_msec);
      year=d.getFullYear()+12-2000;
      return '平成'+year+''+(d.getMonth()+1)+''+d.getDate()+'';
    }

    $.getJSON('json4.php?callback=?', function(data,status) {
      // Add a null value for the end date
//      data = [].concat(data, [[Date.UTC(2011, 9, 14, 19, 59), null, null, null, null]]);
      // Create a timer
      var start = + new Date();
      // Highchart全体設定
      Highcharts.setOptions({
        global: {  // グローバルオプション
          useUTC: false   // GMTではなくJSTを使う
        },
        lang: {  // 言語設定
          rangeSelectorZoom:  '表示範囲',
          resetZoom:          '表示期間をリセット',
          resetZoomTitle:     '表示期間をリセット',
          rangeSelectorFrom:  '表示期間',
          rangeSelectorTo:    '',
          printButtonTitle:   'チャートを印刷',
          exportButtonTitle:  '画像としてダウンロード',
          downloadJPEG:       'JPEG画像でダウンロード',
          downloadPDF:        'PDF文書でダウンロード',
          downloadPNG:        'PNG画像でダウンロード',
          downloadSVG:        'SVG形式でダウンロード',
          months: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
          shortMonths: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
          weekdays: ['', '', '', '', '', '', ''],
          shortWeekdays: ['', '', '', '', '', '', ''],
          numericSymbols: null   // 1000を1kと表示しない
        }
      });
      // 株価チャートを生成
      window.chart = new Highcharts.StockChart({
        chart: {
          type: 'line',
          zoomType: 'x',
          renderTo : 'container',  // チャートを描画する要素のID
          events: {
            load:function(chart) {
              this.setTitle(null,{
                text:'Built chart at '+(new Data()-start)+'ms'
              });
            }
          }
        },
        navigator: {  // ナビゲータ
          adaptToUpdateDate: false,
          series:{
            data : data
          },
          xAxis: {
            labels: {
              formatter: function(){ return utc2dateString(this.value); }
            },
            dateTimeLabelFormats: {
              millisecond: '%H:%M:%S.%L',
              second: '%H:%M:%S',
              minute: '%H:%M',
              hour: '%H:%M',
              day: '%b月%e日',
              week: '%b月%e日',
              month: '\'%y年%b',
              year: '%Y'
            }
          },
          baseSeries: 0
        },
        scrollbar: {
          liveRedraw: false
        },
        title: {  // チャートタイトル
          text : '温度図'
        },
        subtitle: {
          text: 'Displaying 1.7 million data points in Highcharts Stock by async server loading'
        },
        rangeSelector: { // 表示幅選択ボタン
          selected : 6,// デフォルトを選択
          inputDateFormat: '%Y/%m/%d',
          inputEditDateFormat: '%Y/%m/%d',
          inputEnabled: true, // it supports only days
          buttons : [
//type  "millisecond", "second", "minute", "hour", "day", "week", "month", "ytd", "all"
//ytd year-to-date 1年前から今日まで。
          {
            type : 'hour',
            count : 1,     // 60 分のデータを表示
            text : '1時間' // ボタンの表示名
          }, {
            type : 'hour',
            count : 12,     // 24時間のデータを表示
            text : '半日'   // ボタンの表示名
          }, {
            type : 'day',
            count : 5,     // 24時間のデータを表示
            text : '5日'   // ボタンの表示名
          }, {
            type : 'month',
            count : 1,     // 30日分のデータを表示
            text : '1ヶ月' // ボタンの表示名
          }, {
            type : 'year',
            count : 1,     // 360日分のデータを表示
            text : '1年'
          }, {
            type : 'all',    // 全データ
            count : 1,
            text : 'All'
          }]
        },
        xAxis: {  // X軸
/*
          labels: {
            formatter: function(){ return utc2dateString(this.value); }
          }
*/
          events : {
            afterSetExtremes : afterSetExtremes
          },
          dateTimeLabelFormats: {
            millisecond: '%H:%M:%S.%L',
            second: '%H:%M:%S',
            minute: '%H:%M',
            hour: '%H:%M',
            day: '%b月%e日',
            week: '%b月%e日',
            month: '\'%y年%b月',
            year: '%Y年'
          },
          type: 'datetime',
//          minRange: 3600 * 1000 // one hour
          minRange: 60 * 1000 // one minute
        },
        yAxis: {
          title: {
            text: '温度'
          },
//        max: 45,
//        min: 20,
          floor: 0,
          allowDecimals: true
        },
        credits: {  // 右下のクレジット
          enabled: true
        },

        legend: {  // 凡例
          enabled: true,
          align: '',
          verticalAlign: 'top',
          floating: true,
          x: 0
        },

        series: [{  // 系
          name : '温度',
//          turboThreshold : 0,
          data : data,
//          type: 'spline',
          type: 'line',
          color: '#4572a7',    // 色の指定
          shadow : true,
          tooltip : {
            valueDecimals : 2,
            valueSuffix:''
          },
          dataGrouping: {
            enabled: true
          }
        }],
        plotOptions: {  // プロットオプション
          series: {
            dataGrouping: {
              dateTimeLabelFormats: {
                millisecond: ['%Y/%m/%d %H:%M:%S.%L', '%Y/%m/%d %H:%M:%S.%L', '-%H:%M:%S.%L'],                second: ['%Y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '-%H:%M:%S'],
                minute: ['%Y/%m/%d %H:%M', '%Y/%m/%d %H:%M', '-%H:%M'],
                hour: ['%Y/%m/%d %H:%M', '%Y/%m/%d %H:%M', '-%H:%M'],
                day: ['%Y/%m/%d', '%Y/%m/%d', '-%Y/%m/%d'],
                week: ['%Y/%m/%d', '%Y/%m/%d', '-%Y/%m/%d'],
                month: ['%B %Y', '%B', '-%B %Y'],
                year: ['%Y', '%Y', '-%Y']
              }
            }
          }
        },
        tooltip: {  // ツールチップ
/*
          formatter: function() {
            return '<b>'+this.series.name+'</b><br/>'+
              Highcharts.dateFormat('%e -%b -%Y',new Date(this.x))
              + ' date, '+this.y + ' C';
          },
*/
          headerFormat: '<b>{point.key}</b><br/>',
          pointFormat: '<span style="color:{series.color}">{series.name}</span>: {point.y}<br/>',
//          dateFormat: '%Y/%m/%d %H:%M:%S'
          dateTimeLabelFormats: {
            millisecond:"%b月%e日(%A) %H:%M:%S.%L",
            second:"%b月%e日(%A) %H:%M:%S",
            minute:"%b月%e日(%A) %H:%M",
            hour:  "%b月%e日(%A) %H時",
            day:"%Y年%b月%e日(%A)",
            week:"Week from %A, %b %e, %Y",
            month:"%B %Y",
            year:"%Y"
          }
        }
      });//chart create
    });//getjson

});

Webブラウザからtest6.htmlにアクセスすると下記の様なグラフが作成出来ます。
無題2.png

3.うるう秒

うるう秒ですが、データベース上は2017年1月1日 8時59分59秒が2回記録されるのですが、PHPで上での処理が時刻をキーにしている為、グラフ上ではうるう秒は見えなくなります。

SQL上では下記の様に見えます。

psql
select * from log1 where date between '2017-01-01 8:59:55' and '2017-01-01 9:00:05';
          date          | temp1 | humi1 | pres2 | temp2 | pres3 | temp3 |            ts_now             |   id
------------------------+-------+-------+-------+-------+-------+-------+-------------------------------+---------
 2017-01-01 08:59:55+09 |  3766 |  1619 |  2874 |  3266 |  -647 |  2393 | 2017-02-04 09:33:43.476261+09 | 8159030
 2017-01-01 08:59:56+09 |  3764 |  1619 |  2874 |  3266 |  -649 |  2393 | 2017-02-04 09:33:43.477388+09 | 8159031
 2017-01-01 08:59:57+09 |  3766 |  1619 |  2745 |  3266 |  -651 |  2393 | 2017-02-04 09:33:43.479477+09 | 8159032
 2017-01-01 08:59:58+09 |  3764 |  1619 |  2916 |  3285 |  -646 |  2393 | 2017-02-04 09:33:43.479837+09 | 8159033
 2017-01-01 08:59:59+09 |  3764 |  1619 |  2916 |  3285 |  -647 |  2392 | 2017-02-04 09:33:43.482198+09 | 8159034
 2017-01-01 08:59:59+09 |  3764 |  1619 |  2745 |  3266 |  -646 |  2393 | 2017-02-04 09:33:43.483674+09 | 8159035
 2017-01-01 09:00:00+09 |  3764 |  1619 |  2874 |  3266 |  -647 |  2393 | 2017-02-04 09:33:43.485929+09 | 8159036
 2017-01-01 09:00:01+09 |  3764 |  1619 |  2916 |  3285 |  -650 |  2393 | 2017-02-04 09:33:43.487128+09 | 8159037
 2017-01-01 09:00:02+09 |  3766 |  1619 |  2874 |  3266 |  -647 |  2392 | 2017-02-04 09:33:43.489287+09 | 8159038
 2017-01-01 09:00:03+09 |  3764 |  1619 |  2745 |  3266 |  -650 |  2393 | 2017-02-04 09:33:43.490448+09 | 8159039
 2017-01-01 09:00:04+09 |  3766 |  1619 |  2916 |  3285 |  -650 |  2393 | 2017-02-04 09:33:43.492612+09 | 8159040
 2017-01-01 09:00:05+09 |  3764 |  1619 |  2874 |  3266 |  -647 |  2393 | 2017-02-04 09:33:43.494003+09 | 8159041
(12 )

jsonでは下記の様にうるう秒は見えなくなります。

json
/* json4.php?start=1483228795000&end=1483228805000&callback=jQuery_test */
/* SELECT
          EXTRACT(EPOCH FROM date) AS unixtime,
          temp1
         FROM log1
         WHERE date between
           '2016-12-31 23:59:55+00' AND
           '2017-01-01 00:00:05+00'
         ORDER BY unixtime
         LIMIT 5000
*/
/* console.log(' start = 1483228795000, end = 1483228805000, startTime = 2016-12-31 23:59:55, endTime = 2017-01-01 00:00:05 '); */
/* console.log(' start = 1483228795000, end = 1483228805000, startTime = 2016-12-31 23:59:55+00, endTime = 2017-01-01 00:00:05+00 table = log1'); */jQuery_test([
[1483228795000,37.66],  /*2017-01-01 08:59:55(JST)*/
[1483228796000,37.64],  /*2017-01-01 08:59:56(JST)*/
[1483228797000,37.66],  /*2017-01-01 08:59:57(JST)*/
[1483228798000,37.64],  /*2017-01-01 08:59:58(JST)*/
[1483228799000,37.64],  /*2017-01-01 08:59:59(JST)*/
[1483228800000,37.64],  /*2017-01-01 09:00:00(JST)*/
[1483228801000,37.64],  /*2017-01-01 09:00:01(JST)*/
[1483228802000,37.66],  /*2017-01-01 09:00:02(JST)*/
[1483228803000,37.64],  /*2017-01-01 09:00:03(JST)*/
[1483228804000,37.66],  /*2017-01-01 09:00:04(JST)*/
[1483228805000,37.64]  /*2017-01-01 09:00:05(JST)*/
]);

4. 参考にしたWebページ

下記ページを参考にしました。ありがとうございました。

+C++11で、chronoライブラリを使って時間を計測する
+std::this thread::sleep for
+libpqxx: How to reconnect to a Postgresql database after connection process has died
+PHPでデータベースに接続するときのまとめ
+PHPのタイムゾーン(時差の計算)についての備忘録
+【9.3新機能チェック】マテリアライズドビューを試してみる
+Postgre sql9.3新機能紹介(slideshare)
+Highstockスゴイ!日本語化もしてみた。
+Highcharts の応用メモ
+JSのグラフライブラリを今選ぶならHighchartsで決まり

9
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11