众所周知,我用Emacs
的ledger-mode
来记账(参见以前的文章《程序员的记账工具——ledger与ledger-mode》)。作为一个出色的命令行报表工具,ledger
的命令balance
和register
足以涵盖大部分的使用场景:
balance
可以生成所有帐号的余额的报表,用于每天与各个账户中的真实余额进行比较;register
可以生成给定帐号的交易明细,用于在余额不一致时与真实账户的流水一条条核对;
美中不足的是,ledger
的报表不够直观,因为它们是冷冰冰的文字信息,而不是振奋人心的统计图形。好在,正如ledger
不存储数据,而只是一份份.ledger
文件中的交易记录的搬运工一样,gnuplot
也是这样的工具——它不存储数据,它只负责将存储在文本文件的数据以图形的形态呈现出来。
如何运用gnuplot
gnuplot
是很容易使用的。以最简单的情况为例,首先将如下内容保存到文件/tmp/data.csv
中
-1 -1
0 0
1 1
然后在命令行中启动gnuplot
,进入它的 REPL 中,并执行如下命令
plot "/tmp/data.csv"
即可得到这三组数据的展示
三组数据分别是坐标为(-1, -1)
、(0, 0)
,以及(1, 1)
的点。
因此要让gnuplot
绘制开销的图形,首先就是从账本中提取出要绘制的数据,再决定如何用gnuplot
绘制即可。
用ledger
提取开销记录
尽管ledger
的子命令register
可以打印出给定帐号的交易明细,但此处更适合使用csv
子命令。例如,下列的命令可以将最早的10条、吃的方面的支出记录,都以 CSV 格式打印出来
➜ Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food'
"2019/09/10","","32034acc","efe2a5b9:c720f278:58a3cd91:0dc07b7b","A","20","",""
"2019/09/11","","a61b6164","5d45e249:fe84ca06:778d1855:daf61ede","A","5","",""
"2019/09/11","","674ec19f","5d018df1:ebf020db:29d43aba:d0c84127","A","15","",""
"2019/09/11","","e55ff018","370ca545:7d3aa2d0:86f5f330:1379261b","A","20","",""
"2019/09/12","","f6aa675c","08315491:4c8f1ee7:5eeaddf3:f879914e","A","10.5","",""
"2019/09/12","","139b790f","a137e4ee:9bc8ee49:7d7ccd8b:472d6007","A","23.9","",""
"2019/09/12","","b24b716d","de348971:5364622c:b2144d94:01e74ff3","A","148","",""
"2019/09/13","","e7c066fa","b418a3b2:a3e21e87:a32ee8ac:8716a847","A","3","",""
"2019/09/13","","9eb044fe","702a13e9:3de7f1bd:9b20a278:1d20668d","A","24","",""
"2019/09/13","","ba301270","d2b7eeb3:381f9473:54f86a33:391a8662","A","36","",""
--anon
选项可以将交易明细中的敏感信息(如收款方、帐号)等匿名处理。
尽管ledger
打印出的内容有很多列,但只有第一列的日期,以及第六列的金额是我所需要的。同时,由于一天中可能会有多次吃的方面的开销,因此同一天的交易也会有多笔,在绘图之前,需要将同一天之中的开销累加起来,只留下一个数字。这两个需求,都可以用csvsql
来满足。
用csvsql
聚合数据
以前文中的10条记录为例,用如下的命令可以将它们按天聚合在一起
ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense'
其中:
- 选项
-H
让csvsql
知道从管道中输入的数据没有标题行。后续处理时,csvsql
会默认使用a
、b
、c
等作为列名; - 选项
--query
用于提交要执行的 SQL 语句; - 选项
--tables
用于指定表的名字,这样在--query
中才能用 SQL 对其进行处理;
结果如下
➜ Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense'
a,SUM(`f`)
2019-09-10,20
2019-09-11,40
2019-09-12,182.4
2019-09-13,63
用gnuplot
读取数据并绘图
用重定向将csvsql
的输出结果保存到文件/tmp/data.csv
中,然后就可以用gnuplot
将它们画出来
➜ Accounting ledger --anon --head 10 -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT `a`, SUM(`f`) FROM `expense` GROUP BY `a` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/data.csv
➜ Accounting cat /tmp/plot_expense.gplot
set format x '%y-%m-%d'
set style data boxes
set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'
set title '吃的开销'
set output '/tmp/xyz.png'
set timefmt '%Y-%m-%d'
set xdata time
set xlabel '日期'
set xrange ['2019-09-10':'2019-09-13']
set ylabel '金额(¥)'
set yrange [0:200]
set datafile separator comma
plot '/tmp/data.csv' using 1:2
➜ Accounting gnuplot /tmp/plot_expense.gplot
生成的图片文件/tmp/xyz.png
如下
在脚本文件/tmp/plot_expense.gplot
中用到的命令都可以通过gnuplot
的在线手册查阅到:
set format
命令用于设置坐标轴的刻度的格式。set format x "%y-%m-%d"
意味着设置 X 轴的刻度为形如19-09-10
的格式;set style data
命令设置数据的绘制风格。set style data box
表示采用空心柱状图;set terminal
命令用于告诉gnuplot
该生成什么样的输出。set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'
表示输出结果为 PNG 格式的图片,并且采用给定的字体;set title
命令控制输出结果顶部中间位置的标题文案;set output
命令用于将原本输出到屏幕上的内容重定向到文件中;set timefmt
命令用于指定输入的日期时间数据的格式。set timefmt '%Y-%m-%d'
意味着输入的日期时间数据的为形如2019-09-10
的格式;set xdata
命令控制gnuplot
如何理解属于 X 轴的数据。set xdata time
表示 X 轴上的均为时间型数据;set xlabel
命令控制 X 轴的含义的文案。set ylabel
与其类似,只是作用在 Y 轴上;set xrange
命令控制gnuplot
所绘制的图形中 X 轴上的展示范围;set datafile separator
命令控制gnuplot
读取数据文件时各列间的分隔符,comma
表示分隔符为逗号。
想要按周统计怎么办
假设我要查看的是2021年每一周在吃的方面的总开支,那么需要在csvsql
中将数据按所处的是第几周进行聚合
➜ Accounting ledger -b '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_dow.csv
➜ Accounting head /tmp/expense_dow.csv
00,633.6
01,437.3
02,337.5
03,428.4
04,191.5
05,330.4
06,154.6
07,621.4
08,485.6
09,375.73
同时也需要调整gnuplot
的脚本
set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'
set title '吃的开销'
set output '/tmp/xyz2.png'
set xlabel '第几周'
set xrange [0:54]
set ylabel '金额(¥)'
set yrange [0:1000]
set datafile separator comma
plot '/tmp/expense_dow.csv' using 1:2 with lines
结果如下
想要同时查看两年的图形怎么办
gnuplot
支持同时绘制多条曲线,只要使用数据文件中不同的列作为纵坐标即可。假设我要对比的是2020年和2021年,那么先分别统计两年的开支到不同的文件中
➜ Accounting ledger -b '2020-01-01' -e '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_2020.csv
➜ Accounting ledger -b '2021-01-01' -f 2021.ledger csv 'Expense:Food' | csvsql -H --query 'SELECT strftime("%W", `a`) AS `week`, SUM(`f`) FROM `expense` GROUP BY `week` ORDER BY `a` ASC' --tables 'expense' | tail -n '+2' > /tmp/expense_2021.csv
再将处于同一周的数据合并在一起
➜ Accounting csvjoin -H -c a /tmp/expense_2020.csv /tmp/expense_2021.csv | tail -n '+2' > /tmp/expense_2years.csv
最后,再让gnuplot
一次性绘制两条折线
set terminal png font '/System/Library/Fonts/Hiragino Sans GB.ttc'
set title '吃的开销'
set output '/tmp/xyz2years.png'
set xlabel '第几周'
set xrange [0:54]
set ylabel '金额(¥)'
set yrange [0:1000]
set datafile separator comma
plot '/tmp/expense_2years.csv' using 1:2 with lines title "2020", '/tmp/expense_2years.csv' using 1:3 with lines title "2021"
结果如下
后记
其实仍然是非常不直观的,因为最终生成的是一张静态的图片,并不能做到将鼠标挪到曲线上时就给出所在位置的纵坐标的效果。