血圧グラフ(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は、
にある、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容量が必要です。
こちらから必要なパッケージを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