放射線量可視化(2):グラフ用ライブラリの検討/データを集計する

注: この作業によって作成した可視化グラフはhttp://FromTo.Cc/rad/で、結果の定期ツイートは@xckbradで公開されています。

さて、線量計DoseRae2のシリアルポートからデータを取得できるようにした前回からの続きです。

グラフ用のライブラリを選ぼう

さて、線量を可視化するにあたって、グラフをWebに描画するライブラリを選択する必要があります。古典的にはgnuplotやImageMagick、GD::Graphなどがあるのでしょうが、 リアルタイムで更新できるグラフを記述できて、 なおかつ見栄えの良いグラフを生成できるのは、 調べた感じでXML/SWF ChartsHighcharts JSあたりかと。

双方のDemo Galleryを見ると、どちらもカッコいいグラフがいろいろ並んでいるかと思います。

で、今回はHighcharts JSを用いました。 これは以前某所でXML/SWF Chartsを使ったので、 今回はHighcharts JSを使ってみようかな…、 というのが最大の理由ですが、 結果的に両方使った経験から、 今回に関してはHighcharts JSを利用してよかったと思います。 独断と偏見でこの2つのライブラリを比較するとこんな感じです。

XML/SWF ChartsHighcharts JS
公式サイトXML/SWF ChartsHighcharts JS
描画方法FlashSVGまたはVML(IE6〜8)
見栄えとても綺麗綺麗
データ入力XMLJavaScript
リアルタイムデータHTTP GETでのXMLデータAjaxのJSON, XML等のデータ
使うのに必要な技術XMLの生成JavaScriptプログラミング
リアルタイム更新に必要な技術XMLの生成以外特になしAjax(jQuery他のフレームワーク利用可)
非商用ライセンス無料無料
非商用制限グラフをクリックすると公式サイトほぼ制限なし
商用ライセンス$49/ドメイン, $599/無制限$80/サイト, $360/人, 等
時間軸の扱い特別扱いなし時間軸のサポートが充実
iOSデバイス非対応対応
日本語関係の問題日本語フォントの回転ができない気づいた点は特になし

最大の特徴はHighcharts JSがFlash不要でiOSデバイスにも対応している点 (ただし、Android Browser 2.xは未対応?)。 一方、XMLを生成するだけでリアルタイム更新まで対応できるXML/SWF Chartsは利用が簡単です(次回更新までのタイムアウト値をXMLに記述できる)。

意外にポイントなのは、時系列軸の扱い。 Highcharts JSは横軸が時系列の場合、 期間などに応じて自動的に値をフォーマットしてくれる便利な自動処理が付いていて、 時系列グラフを描くことがとても容易です。

ということで、Highcharts JSはなかなかいいグラフ用ライブラリです。

データの5分単位の集計

さて、前回の内容で、 /tmp/raedata.txtというファイルに現在の線量が5秒おきに更新されるようになったわけですが、これを5分単位で集計するプログラムを作ってみました。 データの格納場所は単に慣れているというだけでPostgreSQLを使いましたが、 明らかにオーバースペックです(笑)。テーブルはこんな感じ。 tstampに時刻を、usvにその時点の測定値(μSv/h)を記録します。

CREATE TABLE radiation (
    id SERIAL,
    tstamp TIMESTAMP WITH TIME ZONE,
    usv DOUBLE PRECISION
);

で、5秒に1回データを読み取り、5分に1回書き出します。 具体的には、AnyEventを使って、

#!/usr/bin/perl
use strict;
use AnyEvent;
use Time::HiRes;
use DBI qw(:sql_types);
use DBD::Pg qw(:pg_types);

sub timer_event1();
sub timer_event2();

my $data_file = '/tmp/raedata.txt';
my %values = ();

my $db = DBI->connect
    ("dbi:Pg:dbname=doserae2", 'getrae', '****パスワード****',
     { RaiseError => 1,
       PrintError => 0,
       AutoCommit => 1, });
if (not $db) {
    die "connecting database failed";
}
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
    localtime(time);
my $modmin = 5.5 - (($min % 5) + $sec / 60.0);
my $cv = AnyEvent->condvar;
my $w1 = AnyEvent->timer
    (
     after => 0,
     interval => 5,
     cb => \&timer_event1,
    );
my $w2 = AnyEvent->timer
    (
     after => ($modmin * 60),
     interval => (5 * 60),
     cb => \&timer_event2,
    );
$cv->recv();

のようにして、timer_event1()に5秒ごとに起動されるデータ読み取りルーチンを、timer_event2()に5分毎に起動されるデータ書き出しルーチンを記述します。イベントドリブンの無限ループなので、基本的にこのプログラムは終了しません。

5秒に1度起動される、読み取り部分はこんな感じ。 ファイルの最終更新から5秒以上経っていれば何もしない (何らかの事情で一時的にgetraeが死んでいる)。 データはハッシュ%valuesに格納される。 5分(300秒)以上経ったデータはハッシュから削除される。

sub timer_event1() {
    my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
        $atime, $mtime, $ctime, $blksize, $blocks)
        = stat($data_file);
    my $cur = time;
    my $age = $cur - $mtime;
    if ($age > 5) {
        # do nothing
        return;
    }
    open my $dataf, '<:utf8', $data_file or die "cannot open $data_file";
    my $data = <$dataf>;
    close $dataf;
    my $val = 0.0;
    if ($data =~ /^RAE([\d\.]+)$/) {
        $val = $1;
        $values{$cur} = $val;
    }
    foreach my $i (keys %values) {
        if ($i < $cur - 300) {
            delete $values{$i};
        }
    }
}

そして、5分に1回起動される記録部分はこんな感じです。 読み取り部分と同時実行されないように冒頭で2秒スリープ。 その後、現在の%valuesの値の平均を計算し、 データベースに記録します。

sub timer_event2() {
    sleep 2;
    my $cnt = 0;
    my $sum = 0;
    foreach my $i (keys %values) {
        $sum += $values{$i};
        $cnt++;
    }
    if ($cnt) {
        my $avg = sprintf("%.3f", $sum * 1.0 / $cnt);
        my $t = localtime;
        print STDERR "$t $avg\n";
        eval {
            my $sth = $db->prepare(<<EOT);
INSERT INTO radiation (tstamp, usv)
VALUES (CURRENT_TIMESTAMP, ?)
EOT
            $sth->bind_param(1, $avg + 0.0, {sql_type => SQL_DOUBLE});
            $sth->execute();
        };
        if ($@) {
            die "DB Error :$@";
        }
    }
}

あとは、このプログラムを/rae/sbin/raeminuteという名前で保存して実行ビットを立て、前回と同様の自動監視プログラムをcronで毎分起動し、落ちていればraeminuteを起動するようにしました。

これでめでたく5分おきのデータが記録できるようになりました。

%psql doserae2 pgsql
psql (9.0.5)
"help" でヘルプを表示します.

doserae2=# select * from radiation order by id;
  id  |            tstamp             |  usv  
------+-------------------------------+-------
    1 | 2011-11-27 17:29:53.412789+09 | 0.075
    2 | 2011-11-27 17:34:53.411397+09 | 0.079
    3 | 2011-11-27 17:39:53.411472+09 | 0.077
    4 | 2011-11-27 17:44:53.411608+09 | 0.076
...(略)...

そして日曜プログラミングは来週に続く