血圧グラフ(nbld.go)

前回の血圧グラフが好評で、欲しいとか言われたので、少し整理した。任意の年月日からのグラフをかけると嬉しい。ファイル名が味気ない。ログファイルの一種なので、迷わず年月日.pdfにしよう。

するってーと、gnuplot-script内で、年月日を捻り出さないといけない。色々調べたけど、そんな事まで気が回らない風。データの値を変数に取りこむのはできないみたいだ。

すると残る手は、golang側から与えるしなない。受け渡しは、普通に考えれば、gnuplot-scriptのコマンド引数だ。gnuplot 5系だと、-c で可能らしいんだけど、4系にはそんなのない。

4,5で共通に使える機能として、-e がある。

exec.Command("gnuplot", "-e", "when=190401", "topdf.plt").Run()

の様にすると、gnusplot-script内に変数whenが生えてくるそうな。複数の変数が欲しい場合、セミコロンでセパレートすれば良いそうだ。

次は、何処に出力ファイルを置くか? そりゃ、Desktopでしょ。でも、貰われて行った先のPATHなんて、千差万別。スクリプトの中に埋めこんではおけない。どうする? 常套手段があるじゃん。環境変数。Windowsの場合は、下記のようにするそうな。

echo %userprofile%   ;; same as $HOME
cd %temp%            ;; cd /tmp

golangからは、

        fmt.Printf("%s \n",  os.Getenv("USERPROFILE"))

で、良いそうな。

でも、各ユーザーのHOME直下にアプリを置くというDRYな原則を徹底させれば、そこには、画面への表示窓口のDesktopが必ずあるから、面倒な事は回避できるな。

次に考えるべきは、アプリとgnuplot-scriptの二本になってる事。これ、なんとかしたい。 gnuplot-scriptって、ただの文字列なんで、アプリの中へ収納しちゃえ。使う時、ファイルに書き出し、用が無くなったら削除。

golangにHere documentの機能ってあるのかな?

golang でダブルコーテーションや改行を string 型の変数に入れる

よかった。バッククォートで囲むとそのままの形で収納される。ただし、文字列はutf8になってる必要がある。それから、文字列内では、バックスラッシュが使えない。

install

初めての人もいるので、おさらいと言うか説明。アプリ(nbld)とグラフを描くソフトのgnuplotが必要。このうちのgnuplotは、

gnuplot download

にある、gp526-win64-mingw_2.exe あたりをインストールすれば良いだろう。PATHを通しておく事を忘れないように。

アプリは、巻末にあるnbld.goをコンパイルしてください。じゃ不親切なので、下記に用意した。

nbld.exe (for Windows10 64Bit version)とかサンプルPDF等

アプリを置く場所は、Windows10の自分用dir。どこにあるかは、次のようにして探す。

Win+Rで、ファイル名を指定して実行の窓をだし、そこに、cmd.exeを指定して実行。そうすると、黒い窓がでてくる 。そこのプロンプト表示されてるのが、自分のdirだ。

そこに、アプリを入れる。ついでに、gnuplotが動いているか確認。

Microsoft Windows [Version 10.0.17763.379]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Users\sakae>gnuplot

        G N U P L O T
        Version 4.6 patchlevel 6    last modified September 2014
        Build System: MS-Windows 32 bit
	:

こんな風に出てくればOK。エラーになるなら、gnuplotのインストールかPATHの設定に失敗してるんで、詳しい人に聞いてくれ。

あと、もうひとつ。アプリで使うCSVファイルの種を用意する。Notepadとかで、下記のようなファイルを作る。ファイル名は、current.csv アプリと同じ場所に用意する。

19033104,129,68,54
19033121,120,63,60

年月日時 最高血圧 最低血圧 脈拍数を カンマでつなげたもの。実測値を数例入力してほしい 。 このファイルがアプリから更新されていく。なお、EXCELからも読めるけど、絶対に更新はしないでください。(単純なテキストデータしか扱えません)

使い方 (入力編)

アプリからは年月単位で、データを入力します。起動する時、年月を指定します。年は令和ではなくて、西暦年の下2桁。月も2桁です。

C:\Users\sakae>nbld.exe -ire 1904
1904> 104 132 69 55  ;; 1日の朝4時。日の桁は、先行する0を省略しても良い 
1904> 121 121 65 57  ;; 時は、00から23まで。必ず2桁入力
 :
1904> 1121 110 61 57
1904> fin

入力するのは、日時 最高血圧 最低血圧 脈拍数です。それぞれをスペースで区切ります。

入力の妥当性をチェックしてます。4項目あるか。日時は昇順になってるか。異常に低い値を入力してないか。おかしな点があると、再入力を求められます。

1904> 725 123 62 59   ;; 時の違反
Bad data
1904> 821 12362 59    ;; 項目間は要スペース。4項目必要
Bad data
1904> 821 23 60 59    ;; 最高血圧が23って、超異常と思うぞ
Bad data
1904> 821 123 60 59
1904> 821 123 60 59   ;; (同一データを含め)昇順違反してる
Bad seq.

参考までに、入力の妥当性チェック部分。上段は、日時のエラー検出、下段は、バイタル値のチェック部分になる。上限は出来あがりのグラフを確認してね。

           if rs[ymdh] < 100 || rs[ymdh] > 3123 || (rs[ymdh]%100) > 23 ||
                rs[hi] < 80 || rs[low] < 40 || rs[pls] < 40 {

データ入力の終了は、行頭で、fin としてください。これで、入力データでファイルが更新され、PDFファイルが、Desktopに出現します。

finと入力すると、current.csvファイルが更新されるのですが、途中で中止したい場合は、Ctl+C してください。(入力データは破棄されます)

まちがった値を入力して後で気がついた場合、notepad等で直接current.csvファイルを修正して下さい。

一度に1月分もまとめて入れるのは苦痛なので、10日分ぐらいに分けて入力してます。

使い方 (確認編)

アプリのオプションは次のようになってます。

C:\Users\sakae>nbld -h
Usage of nbld:
  -from int
        Make PDF at YYMM[DD] (default 1302)
  -ire int
        Input YYMM's data (default 1811)

-fromに続いて年月または年月日を入力すると、(データがもし有れば)そこから、10週分を拾い出してグラフを作ってくれます。

何も指定しないで起動すると、直近の10週分をグラフにしてくれます。午前のデータは起床時に分類され、午後のデータは就寝時として扱われます。

データ数が10週に満たない場合は、警告が出ます。

PDFのファイル名は、グラフの始点になった年月日です。

ファイル名

アプリは、データ入力、データ抽出、gnuplotとの交信の役目をするため、下記ファイルを勝手に作成、削除してます。既にあるファイルに遠慮するとかはしません。

同名なファイルが存在する時は、使用をお控えください。このアプリを使うのは、自己責任でおねがいします。

nbld.exe     アプリ
YYMMDD.pdf   年月日.pdfというグラフ出力ファイル。Desktop上に配置される
current.csv  データファイル
Backup.csv   データ入力前のcurrent.csvのコピー
am.dat       gnuplotとのデータ渡し用一時ファイル(自動作成、削除される)
pm.dat       同上
topdf.plt    同上

なお、csvファイルは、1年で約15KByteぐらいづつ、成長していきます。大した容量ではないので、ずっと残しておいて、経年変化を確認すると良いでしょう。

current.csvはとても大事なファイル。時々別メディアにバックアップしておきましょう。アプリが自動作成するBackup.csvは気休めでしかありません。

再コンパイルの方法

自分仕様に改造したいとか、公開されてるアプリが信用できないという方は、自分でソースから コンパイルする事をお勧めします。

コンパイルに必要なものは、googleが公開してるシステムだけです。インストールには、350MByte程も、Disk容量が必要です。

GoLang

こちらから必要なパッケージをDLして、システムをインストールして下さい。追加のパッケージは一切利用してません。

nbld.go を用意した上で

C:\Users\sakae>go build nbld.go

するだけで、nbld.exe が作成されます。

nbld.go

// blood PDF by gnuplot
// Using gnuplot script topdf.plt
package main

import (
	"encoding/csv"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"strconv"
	"bufio"
)

const dbfile = "current.csv"

type AAy [][4]int // main data type
var bld AAy       // blood data

const ( // rows index name 0 .. 3
	ymdh = iota
	hi
	low
	pls
)

func failOnError(err error) {
	if err != nil {
		panic(err)
	}
}

func ag(datafile string) string {     // for gnuplot option
        fd, err := os.Open(datafile)
        failOnError(err)
        defer fd.Close()
	r := bufio.NewReader(fd)
	line, err := r.ReadString('\n')
	failOnError(err)
	return fmt.Sprintf("when=%s", line[:6])
}

func mycsv(csvfile string) {
	var rows [4]int
	fd, err := os.Open(csvfile)
	failOnError(err)
	defer fd.Close()
	reader := csv.NewReader(fd)
	all, err := reader.ReadAll()
	failOnError(err)
	for _, el := range all {
		for i, v := range el {
			rows[i], err = strconv.Atoi(v)
			failOnError(err)
		}
		bld = append(bld, rows)
	}
}

func savecsv(csf string) {
	buf, _ := ioutil.ReadFile(csf)
	_ = ioutil.WriteFile("Backup.csv", buf, 0666)
	fd, err := os.OpenFile(csf, os.O_CREATE|os.O_WRONLY, 0666)
	failOnError(err)
	err = fd.Truncate(0)
	failOnError(err)
	defer func() {
		fd.Close()
	}()
	w := csv.NewWriter(fd)
	for _, el := range bld {
		rows := make([]string, 0)
		for _, v := range el {
			rows = append(rows, strconv.Itoa(v))
		}
		w.Write(rows)
	}
	w.Flush()
}

func ire(ym int) {
	var a, b, c, d string
	var rs [4]int
	for {
 		a, b, c, d = "", "", "", ""
		fmt.Printf("%d> ", ym)
		fmt.Scanln(&a, &b, &c, &d)
		if a == "fin" {
			break
		}
		rs[ymdh], _ = strconv.Atoi(a)
		rs[hi], _ = strconv.Atoi(b)
		rs[low], _ = strconv.Atoi(c)
		rs[pls], _ = strconv.Atoi(d)
                if rs[ymdh] < 100 || rs[ymdh] > 3123 || (rs[ymdh]%100) > 23 ||
                     rs[hi] < 80 || rs[low] < 40 || rs[pls] < 40 {
			fmt.Println("Bad data")
			continue
		}
		rs[ymdh] += (ym * 10000)
		if rs[ymdh] <= bld[len(bld)-1][ymdh] {
			fmt.Println("Bad seq.")
			continue
		}
		bld = append(bld, rs)
	}
	savecsv(dbfile)
}

func (ds AAy) pp(fn string) {
	xf,_ := os.Create(fn)
        defer xf.Close()
	for _, el := range ds {
		fmt.Fprintf(xf, "%d %d %d\n", el[ymdh], el[hi], el[low])
	}
}

func (ds AAy) hd(n int) AAy {
	if len(ds) < n {
		fmt.Println("Warnning: Req size is to big, apply possible size.")
		return ds
	}
	return ds[:n]
}

func (ds AAy) tl(n int) AAy {
	if len(ds) < n {
		fmt.Println("Warnning: Req size is to big, apply possible size.")
		return ds
	}
	return ds[len(ds)-n:]
}

func (ds AAy) am() AAy {
	var rs AAy
	for _, el := range ds {
		if el[ymdh]%100 < 12 {
			rs = append(rs, el)
		}
	}
	return rs
}

func (ds AAy) pm() AAy {
	var rs AAy
	for _, el := range ds {
		if el[ymdh]%100 >= 12 {
			rs = append(rs, el)
		}
	}
	return rs
}

func (ds AAy) frm(ym int) AAy {
	var rs AAy
	lmt := ym * 10000
	if ym > 9999 {            // Case yymmdd
		lmt = ym * 100
	}
	for _, el := range ds {
		if el[ymdh] > lmt {
			rs = append(rs, el)
		}
	}
	return rs
}

func (ds AAy) utl(ym int) AAy {
	var rs AAy
	lmt := ym*10000 + 3124
	for _, el := range ds {
		if el[ymdh] < lmt {
			rs = append(rs, el)
		}
	}
	return rs
}

func (ds AAy) eq(ym int) AAy {
	var rs AAy
	for _, el := range ds {
		if el[ymdh]/10000 == ym {
			rs = append(rs, el)
		}
	}
	return rs
}

func mksf(sf string, gsrc string){
        fd, err := os.OpenFile(sf, os.O_RDWR|os.O_CREATE, 0755)
        failOnError(err)
        defer fd.Close()
        fd.WriteString(gsrc)
}

func main() {
gsrc :=`# blood graph for gnuplot
set encoding utf8
set terminal pdfcairo mono font ",20" size 21cm, 29cm
#set output "./zzz.pdf"
set output "Desktop\\" . when . ".pdf"

stats "am.dat" using 2:3
amh = sprintf(" 最高血圧(平均=%.1f 偏差=%.1f)", STATS_mean_x, STATS_stddev_x)
aml = sprintf(" 最低血圧(平均=%.1f 偏差=%.1f)", STATS_mean_y, STATS_stddev_y)
stats "pm.dat" using 2:3
pmh = sprintf(" 最高血圧(平均=%.1f 偏差=%.1f)", STATS_mean_x, STATS_stddev_x)
pml = sprintf(" 最低血圧(平均=%.1f 偏差=%.1f)", STATS_mean_y, STATS_stddev_y)

set grid
set yrange [50:160]
set ytics 10
unset key                   # no label on right top
set xdata time
set timefmt "%y%m%d%H"
set format x "%m/%d"        # m/d/y -> m/d

set multiplot layout 2,1

set title  '起床時: ' . amh . aml . when
plot "am.dat" using 1:2 with lines lt -1, "am.dat" using 1:3 with lines lt -1

set title  '就寝時: ' . pmh . pml . when
plot "pm.dat" using 1:2 with lines lt -1, "pm.dat" using 1:3 with lines lt -1

unset multiplot
set terminal dumb
## end of gnuplot-script
`
	var vire *int = flag.Int("ire", 1811, "Input YYMM's data")
	var stym *int = flag.Int("from",1302, "Make PDF at YYMM[DD]")
	flag.Parse()

	mycsv(dbfile)
	if *vire != 1811 {
		ire(*vire)
	}

	sz := 70               // 10 week's
	if *stym <= 1302 {
		bld.am().tl(sz).pp("am.dat")
		bld.pm().tl(sz).pp("pm.dat")
	} else {
		bld.am().frm(*stym).hd(sz).pp("am.dat")
		bld.pm().frm(*stym).hd(sz).pp("pm.dat")
	}

	mksf("topdf.plt", gsrc)
	exec.Command("gnuplot", "-e", ag("am.dat"), "topdf.plt").Run()
        os.Remove("am.dat")
        os.Remove("pm.dat")
	os.Remove("topdf.plt")
}

index.html 作成用

今回、アプリ等を公開する為に、DATAってdirを用意したんだけど、その中のコンテンツにリンクするってか、indexを作るやつを、でっちあげた。

dir内を更新したら、必ずこのスクリプトを走らせておく事。

#!/bin/sh
# make index.html for in dir
dir="DATA"
of="index.html"

cat <<EOF  >$dir/$of
<html>
<head> <title>Contents</title> </head>
<body> <h1>Contents</h1> <pre> <table>
  <tr> <th>File</th> <th>Size</th>  </tr>
  <tr>  <td>------------------</td>  <td>----------</td>  </tr>
EOF

for f in $dir/*
do
    sz=`wc -c $f | awk '{print $1}'`
    bn=`basename $f`
    if [ "$bn" = "$of" ]; then
	continue
    fi
    echo '<tr>'
    echo '<td><a href=" ' $bn '">' $bn '</a></td>'
    echo '<td align="right">' $sz '</td>'
    echo '</tr>'
done   >> $dir/$of

cat <<EOFx >>$dir/$of
  </table> </pre> </body> </html>
EOFx