python curses でグラフ…

glances で windows-curses を入れたついでというか。まあ相当残念なネタであることは覚悟しといたらいいよ。

「コンソールにまつわるもの」としてもう一つ ANSI シーケンスそのもの、と colorama のネタも持っているんだけど、今回はcurses のほうだけにしとく。

一連の COVID19 ネタは「良い見える化とそれの共有」の話で、「ほらね今やこういうのって簡単なのさぁ」みたいなことで pygal にも触れたわけなんだけれど、その、「かつてはそんなに簡単なことではなかった」の一例だと思うのよね、「テキストオンリーベースのターミナルだけを使ってグラフを表現する」というタスクは。

その「かつてはそんなに簡単なことではなかった」の中身は、もちろん「数学的なサポート」などの欠如なんかも多分にあるけれど、それ以前の問題として「可視化表現に使えるインフラの欠如による自由度のなさ」なわけね。決して「そのプアな表現をするためのプログラミングも困難」だったわけじゃない。たとえばこういうことな:

ほかのとこで説明した通り、Windows では windows-curses が必要。
 1 # -*- coding: utf-8 -*-
 2 # pygal を紹介する際に「pygal は簡単なんだけど numpy 部分がこ難しくみえちゃうのが歯痒い」
 3 # かったので、あえて numpy を使わずに書いてみた。
 4 import io
 5 import os
 6 import sys
 7 import datetime
 8 import csv
 9 import curses
10 
11 
12 def _main(stdscr, tkoprefdat):
13     _COLSNM = (
14         "testedPositive",
15         "peopleTested",
16         "hospitalized",
17         "serious",
18         "discharged",
19         "deaths",
20         "effectiveReproductionNumber",
21     )
22     reader = csv.reader(io.open(tkoprefdat, encoding="utf-8-sig"))
23     next(reader)
24     def _f(d):
25         if not d or d == "-":
26             return 0
27         return float(d)
28     trgprefecdat = {"testedPositive": [], "deaths": [], "serious": []}
29     tpn = "大阪府"
30     for line in reader:
31         pn = line[3]
32         data = list(map(_f, line[5:]))
33         if pn == tpn:
34             for k in trgprefecdat.keys():
35                 trgprefecdat[k].append(data[_COLSNM.index(k)])
36     #
37     stdscr.clear()
38     curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
39     curses.init_pair(3, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
40     curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK)
41     height, width = stdscr.getmaxyx()
42     height -= 4
43     width -= 2
44     for k in trgprefecdat.keys():
45         trgprefecdat[k] = trgprefecdat[k][-width:]
46         maxv = max(trgprefecdat[k])
47         trgprefecdat[k] = [(height - int((v / maxv) * height)) for v in trgprefecdat[k]]
48     for i in range(len(trgprefecdat["testedPositive"])):
49         # addstr の x, y が矩形 [width, height] 範囲外を指すと即(なんの説明もない
50         # 不親切な)エラーが返るので注意。
51         stdscr.addstr(trgprefecdat["testedPositive"][i], i, "#", curses.color_pair(2))
52         stdscr.addstr(trgprefecdat["serious"][i], i, "#", curses.color_pair(3))
53         stdscr.addstr(trgprefecdat["deaths"][i], i, "#", curses.color_pair(4))
54     tit = "(" + tpn + "の感染流行波の形 ) "
55     for i, c in enumerate(tit):
56         stdscr.addstr(height + 1, i * 2, c)
57     tit = "「東洋経済オンライン「新型コロナウイルス 国内感染の状況」」を加工 "
58     for i, c in enumerate(tit):
59         stdscr.addstr(height + 2, i * 2, c)
60     stdscr.getch()
61 
62 
63 if __name__ == '__main__':
64     import urllib.request
65     import shutil
66     if os.path.exists("prefectures.csv"):
67         ctm = datetime.datetime.utcfromtimestamp(os.stat("prefectures.csv").st_mtime)
68         now = datetime.datetime.utcnow()
69         if (now - ctm).total_seconds() > 60 * 60:
70             os.remove("prefectures.csv")
71     if not os.path.exists("prefectures.csv"):
72         cont, msg = urllib.request.urlretrieve(
73             "https://toyokeizai.net/sp/visual/tko/covid19/csv/prefectures.csv")
74         shutil.move(cont, "prefectures.csv")
75     curses.wrapper(_main, "prefectures.csv")

curses であることのメリットは、色付けのこともあるけどカーソル位置を自由にコントロール出来て好きな場所に文字印字出来ること、ね。実際一切 curses 使わずに全く同じグラフは書ける、でしょ、わかるよね?

結果はワタシのコンソールだとこんな感じ:

直近の「トレンド」の雰囲気をつかむだけなら、こんなんでも結構十分なんではないかという気はするよね。例えばこれを毎日監視してたりすれば、「上がりバナ」にはすぐに気付くであろうよ。

さて。

こういうやたらにレガシーなスキルを身につけることが何か役に立つのか、と言えば、「わざわざ身につけるつもりなら見合わない」とは思う。そうではなくて、「いざとなったらこういうやり方もある」という事実を「頭の片隅に入れておく」ことが、柔軟性の源となる、かもしれない、てハナシね。以前 curses の話をした際には「いまでも curses (や termcap/terminfo)の需要は結構ある」と書いたが、これはおそらく主として「サーバメンテナンスをリモートで行う」などの用事がある際にのみ頻出となりうるわけね、それ以外のシチュエーションはさすがにこれだけに固執する理由はほとんどなくて、ほかのもっとリッチなものを選ぶべき、となるだろうと思う。

ただ、「手っ取り早くコストをかけずに」の一つのやり方として、ということであれば、このアプローチは実は技術的には「vt100 的コンソール」にしかほとんど依存していない点が割と優れていて、特に Unix 系ならほぼ 100% 通用するという点は、強いといえば強い。ので、やはり「こういう考え方/やり方もある」と知っとくのは悪くないと思う。


2021-04-03追記:
こちらもどぞ:


2022-01-26追記:
どういう動機でワタシのこのページに辿り着くのかを想像してみるに、きっと「python で」はあまり関係なく、なおかつ「cueses」にもさほどこだわりなく、シンプルに「vt100 的コンソールでグラフを描きたい」という人もいるんではないかと思うのね。そういう人以外にはちょっと雑念になっちゃうかもしれない追記。

今想像した動機よりはきっと Sparklines の方が近いんではないかという、だけれども今のワタシの動機のほうにむしろ相応しい asciichart というプロジェクトがある。GitHub ページの構造の都合「Python プロジェクト」にみえるけれど、ドキュメントの方を読めばわかる通り、これは node.js、つまり javascript のプロジェクトで、Python は「ports のひとつ」という扱いである。Sparklines のほうが近いのでは、といった意味はわかるよね?

今回の追記では、この Python ports で遊んでみてもまぁいいと思ったんだけれど、ここ数日 Go で遊んでたもんで、なので、ちょっと Go のほうの ports である asciigraph の方でやってみようかと。

1 [me@host: ~]$ # Goモジュールとして使いたいなら:
2 [me@host: ~]$ go get github.com/guptarohit/asciigraph
3 [me@host: ~]$ # 完成品としての CLI ツールとしても欲しければ
4 [me@host: ~]$ go install github.com/guptarohit/asciigraph/cmd/asciigraph

後者の CLI だけで既に結構オモロイと思うよ:

「~/../../go/bin/」はワタシの環境でパスを通さずに使う場合。適宜読み替えてちょ。
 1 [me@host: ~]$ echo {1..10} | ~/../../go/bin/asciigraph.exe -h 10 -c "-h 10 of {1..10}"
 2  10.00 ┤        ╭
 3   9.10 ┤       ╭╯
 4   8.20 ┤      ╭╯
 5   7.30 ┤     ╭╯
 6   6.40 ┤    ╭╯
 7   5.50 ┤   ╭╯
 8   4.60 ┤   │
 9   3.70 ┤  ╭╯
10   2.80 ┤ ╭╯
11   1.90 ┤╭╯
12   1.00 ┼╯
13         -h 10 of {1..10}
14 [me@host: ~]$ echo {1..10} | ~/../../go/bin/asciigraph.exe -h 1 -c "-h 1 of {1..10}"
15  10.00 ┼   ╭─────
16   1.00 ┼───╯
17         -h 1 of {1..10}
18 [me@host: ~]$ echo {1..10} | ~/../../go/bin/asciigraph.exe -h 20 -c "-h 20 of {1..10}"
19  10.00 ┤        ╭
20   9.55 ┤        │
21   9.10 ┤       ╭╯
22   8.65 ┤       │
23   8.20 ┤      ╭╯
24   7.75 ┤      │
25   7.30 ┤     ╭╯
26   6.85 ┤     │
27   6.40 ┤     │
28   5.95 ┤    ╭╯
29   5.50 ┤    │
30   5.05 ┤   ╭╯
31   4.60 ┤   │
32   4.15 ┤  ╭╯
33   3.70 ┤  │
34   3.25 ┤ ╭╯
35   2.80 ┤ │
36   2.35 ┤ │
37   1.90 ┤╭╯
38   1.45 ┤│
39   1.00 ┼╯
40         -h 20 of {1..10}

tail -f みたいに継続入力に追従して描画することも出来るようなので、リアルタイム監視用にも使えるみたいね。公式では ping -i を追いかける例を載っけてる。

自分で Go プログラムを作るならば、の例を自作しようかとも思ったが、まぁ公式の例で十分ろ。とても簡単。

このプロジェクトのアプローチは curses のようなもの、つまり「端末の機能」を使うという発想ではなくて、「某ちゃんねらー」が好む、まさにアスキーアートね。このプロジェクトだけでなく、他にも近いのは見たことがある。こういうのも場合によってはアリだと思うよ、てことで。(ただし「文字通りの文字」を使ったお絵描きなので、ロケールとかの繊細な問題の影響をモロに受けることに注意。)