[原创] gnuplot调教记

gnuplot是命令行绘图的极佳选择之一。用它画出一副简单的图极其容易,但是对画出的图进行微调却很令人抓狂,这主要“得益”于gnuplot超级强大的功能,以及无穷无尽纷繁复杂的说明文档,通常让人没时间去细细研究。
如果你经常有gnuplot的使用需求,那么可以仔细研读一下它的文档,否则,还是像我一样,现学现用吧。
我折腾了一天多时间,把我想要的一副图给画出来了,里面涉及到了很多图像的微调过程,有些解决方案很难搜到,但我运气稍好,终究还是弄出来了,在此,作为一个使用案例来分析一下。因此,本文并不是gnuplot的使用教程,而是用gnuplot绘图时,对某些奇怪问题的解决办法的分享。

本文基于gnuplot版本:4.4,系统:RHEL 5.3

『1』基本需求
先说一下我要画图的基本需求。听上去很简单:就是二维平面上的X,Y坐标点线图。我有一个数据文件data.txt,第一列是日期,第二列是该日期下的数值,例如:

20141020	1525
20141019	1429
20141018	1321
20141017	1780
20141016	1655

下面来看看怎么把它画成图。

『2』编写最简单的shell脚本
在shell脚本中调用gnuplot,绘制出一个最简单的图:

#!/bin/bash

gnuplot << EOF
  set terminal png
  set xdata time
  set timefmt "%Y%m%d"
  set output "output.png"
  plot "data.txt" using 1:2
    quit
EOF

画出的图是这样的:

文章来源:http://www.codelast.com/
显然这个图非常难看,并且图上的某些内容让人匪夷所思,我们先不管它,先看看它是怎么运作的。
在shell脚本中两个 EOF 中间的是gnuplot的绘图参数设置命令,每一条命令的含义分别是:
set terminal png:将输出图片设置为png格式。
set xdata time:设置X轴为时间。
set timefmt "%Y%m%d":设置输入数据的时间格式。
set output "output.png":设置输出图片的文件名,这里你可以使用绝对路径,此处为了简单,只使用相对路径,将输出到当前目录下。
plot "data.txt" using 1:2:将 data.txt 作为输入数据文件,使用其第一列作为X轴,第2列作为Y轴。
文章来源:http://www.codelast.com/
可见要画出一幅图是相当简单的,但是正如前面所说,要调整这幅图到你满意的程度,并不是一件简单的事情——如果不调整,上面的乱糟糟图根本就没法看嘛!

『3』漫长的参数调整过程
(1)去掉右上角的文字说明
右上角有一个 "data.txt" using 1:2 的说明,要去掉它,需要添加一句:

set nokey

画出的图变成了:

文章来源:http://www.codelast.com/
现在干净一点了。

(2)更改X轴的日期显示格式
将X轴的日期改为“月-日”(例如“10-16”),加一句代码即可:

set format x "%m-%d"

画出的图变成了:

文章来源:http://www.codelast.com/
X轴日期显示格式已改,没有悬念。有人说,但是它怎么有重复的日期啊?这个问题后面再说。

(3)清除Y轴显示的数值
添加代码:

set format y ""

画出的图变成了:

文章来源:http://www.codelast.com/
现在Y轴没数字了。

(4)添加网格
图中的任意两个点,它们的Y坐标值是否相同?如果这两个点离得稍微远一点,就看不清楚了,因此,为图像添加上网格,可以很好地辅助我们识别它们是否在同一水平线上:

set grid

画出的图变成了:

文章来源:http://www.codelast.com/
(5)去除X轴上重复的日期
添加如下代码:

set xtics 60*60*24

画出的图变成了:

文章来源:http://www.codelast.com/
这句代码为什么能“神奇”地修复X轴坐标重复的问题?如果你有耐心,可以看看这篇文章的解释。它的大概原理是:当X轴显示的值是时间的时候,xtics 的增量单位是,如果你要它的增量单位是,就需要将 xtics 设置为一天的秒数,即60*60*24

(6)设置输入的数据文件的分隔符
前面我们输入的数据文件data.txt以\t为两列的数据分隔符,但如果我有自定义的分隔符,是否要先把它转成\t,然后才能给gnuplot使用呢?不需要,只要用命令指定分隔符就可以了:

set datafile separator "\t"

(7)点与点之间连线
上面的图,画出的点实在太小了,如果相邻两点之间能用直线相连,看起来就清楚得多了:

plot "data.txt" using 1:2 with linespoints

画出的图变成了:

文章来源:http://www.codelast.com/
这是怎么做到的呢?plot 命令中的 with linespoints 指定了画出的图是点线图。

(8)修改一下线的颜色,线的粗细,点的大小
代码:

plot "data.txt" using 1:2 with linespoints linecolor 3 linewidth 1 pointtype 5 pointsize 1 

画出的图变成了:

文章来源:http://www.codelast.com/
这下就好看多了,在上面的代码中,linecolor指定了线的颜色,linewidth指定了线的宽度,pointtype指定了点的类型,pointsize指定了点的大小,可以参考gnuplot的官方文档获知诸如pointtype之类的代号分别代表什么样的点/线。

(9)在点的旁边显示其纵坐标值
前面我们已经把Y轴刻度去掉了,因此有必要在点的旁边显示其纵坐标值:

plot "data.txt" using 1:2 with linespoints linecolor 3 linewidth 1 pointtype 5 pointsize 1, "" using 1:2:2 with labels

画出的图变成了:

文章来源:http://www.codelast.com/
这一步的代码,比之前的代码多了 "" using 1:2:2 with labels 这部分。其中,with labels 表示在点旁边画上标签(label),但是这个label的值是什么呢?这里使用 using 1:2:2 表示第一列为X坐标值,第二列为Y坐标值,同时第二列也作为label的值。如果数据文件data.txt中有第三列,你可以用 using 1:2:3 来使用第三列作为label的值。有人说,上面还有一个空的双引号,是作什么用的呢?Sorry,这个真不记得了...

(10)不要让label和数据点交错在一起
上面的数据点被label的数值盖住了,看起来很不爽,我们把数值偏移一定的位置,让它们错开:

plot "data.txt" using 1:2 with linespoints linecolor 3 linewidth 1 pointtype 5 pointsize 1, "" using 1:2:2 with labels center offset 0,0.5

文章来源:http://www.codelast.com/
这下漂亮多了!起这个作用的代码是 center offset 0,0.5。其中,center 加不加我没发现有什么区别;offset 0,0.5 表示把label在X轴方向平称0,在Y轴方向平移0.5。注意,平移的单位既不是像素,也不是和Y轴一样的单位,而是“相对距离”,不信我们可以把 offset 0,0.5 改成 offset -1,1,会看到label向左、向上都平移了一个单位,也就是下面这个样子:

文章来源:http://www.codelast.com/
可见label平移的距离远不止一个像素点,并且向上平移的距离也不是Y轴的“1”。
这个把label和数据点错开的方法,代码极其简单,但是为了找到这个solution,我搜遍了网页,试了无数种方法,其他的都有各种各样的显示问题,就是这样写才最好,找得我要吐血。

(11)为X轴添加上说明文字
添加代码:

set xlabel "Date" textcolor lt 1

画出的图变成了:

文章来源:http://www.codelast.com/
其中,textcolor lt 1 指定了文字的颜色。

(12)把X轴的说明文字改成中文
简单地把上面的“Date”改为中文的“日期”,看看画出的图会变成什么:

文章来源:http://www.codelast.com/
不出所料,中文乱码了。
解决这个问题没有那么容易,虽然解决方案有千千万,不过我认为最实用的方法就是:在系统中安装支持中文的字体,并且在使用gnuplot绘图时指定使用这个字体:

set term png font "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc,12"

这句话的前半部分含义与 set terminal png 相同,后半部分指定了使用的字体的路径以及文字的大小。wqy-microhei.ttc 即为文泉驿微米黑字体文件,可以从很多地方下载到,当然你也可以尝试使用其他的字体。字体文件不一定要放在 /usr/share/fonts/truetype/wqy/ 路径下,但是最好遵循Linux系统中其他字体的规则。
这样,画出的图就变成了:

文章来源:http://www.codelast.com/
(13)为图片添加标题(title)
在上面已经搞定中文输出的基础上,我们再添加中文的title就很容易了:

set title "每天售出的商品数量" textcolor lt 1

画出的图变成了:

文章来源:http://www.codelast.com/
(14)图片两边太挤了,空出一定的空隙
添加代码:

set offset graph 0.10, 0.10

画出的图变成了:

文章来源:http://www.codelast.com/
上面的代码使得我们最左、最右的两个点不会碰到坐标轴,看上去舒服多了。至于它的具体含义,我不记得了,总之可以通过调整offset的数值来观察效果。

『4』总结
经过上面的一系列调整,我们终于得到了一幅看上去比较顺眼的图片。最后把完整的代码贴上来:

#!/bin/bash

gnuplot << EOF
  set term png font "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc,12"
  set nokey
  set xdata time
  set timefmt "%Y%m%d"
  set format x "%m-%d"
  set format y ""
  set offset graph 0.10, 0.10
  set xtics 60*60*24
  set grid
  set title "每天售出的商品数量" textcolor lt 1
  set xlabel "日期" textcolor lt 1
  set output "output.png"
  set datafile separator "\t"
  plot "data.txt" using 1:2 with linespoints linecolor 3 linewidth 1 pointtype 5 pointsize 1, "" using 1:2:2 with labels center offset 0,0.5
    quit
EOF

另附几个常见错误的解决办法:
(1)Could not find/open font when opening font "arial", using internal non-scalable font
没有设置字体的时候可能会出现这个错误,这时,你可以指定一个系统中已有的字体,或者安装一个新字体并指定使用它,就可以避免这个问题,例如,安装文泉驿正黑字体:

yum install wqy-zenhei-fonts.noarch

然后在绘图代码中:

set term png font "/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc" 10

表示使用 /usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc 这个刚安装上的字体文件作为图像中的字体,并且字号为10。
文章来源:http://www.codelast.com/
(2)shell调用gnuplot时,shell的内置变量($1,$2,…)与gnuplot的内置变量冲突的问题
在shell脚本中,$1,$2,… 表示传给shell脚本的参数,这与在gnuplot的交互模式下,gnuplot定义的含义是不同的。
在gnuplot的交互模式下,$1,$2,… 之类的变量是指输入数据文件的第1列,第2列,…
所以现在问题来了,当用shell脚本调用gnuplot时(就像本文上面给出的代码一样),如何能让gnuplot代码的$1,$2仍然取到的是数据文件的第1列,第2列?
这篇文章给出了解答,这里把几种方法列举如下:

'$1'
"\$1"
\$1

(3)如何在画出的曲线图中,每N个坐标点才标一个Y坐标值,而不是每个坐标点都标?
某些数据的点非常密集,如果我们把每个点旁边都标上它的Y坐标值,会导致图上的文字密密麻麻地挤在一起,从而使得我们根本看不清,因此,在这种情况下,我们只需要每N个点才标一个Y坐标值就可以了。
这个解决办法需要有点技巧。
假设某个数据文件 data.txt 的第1列是点的顺序编号(1,2,3,……),第2列是该点的纵坐标值,假设最后绘制图像的那一句是:

plot "data.txt" using 1:2 with linespoints linecolor 3 linewidth 1 pointtype 5 pointsize 1, "" using 1:2:2 with labels center offset 0,0.5

现在要对这一句进行特殊处理,使得它做到“每5个点绘制一个Y坐标值”,那么我们只需要把 using 1:2:2 这里改成:

using 1:2:(int(\$1) % 5 == 0 ? sprintf("%.3f", \$2) : "")

解释:
本来1:2:2的最后一个 2 表示将数据文件data.txt的第2列作为label的值(在这里也就是Y坐标值),但是我们把它改成了一个条件判断语句,gnuplot是支持的,很强大。
这个条件判断语句的含义是:当data.txt数据文件的第1列的值是5的整数倍的时候,我们打印出第2列的实际数值(以小数点后3位小数的格式),否则我们就打印出一个空字符串(这实际上导致了什么也不会打印出来)。
大家注意了,这里的为了取数据文件的第1列、第2列,用了 \$1 以及 \$2 的形式,我在前面第(2)条里说过了,这是由于shell脚本内置的参数变量$1,$2与gnuplot有冲突,所以要转义。
另外,为什么会在 \$1 外面包一个 int() 呢?如果没有的话,gnuplot会打印出类似于“can only mod ints”的一个错误提示。
这种解决方案比较微妙,但是它确实达到了我想要的效果。
那有人会说,如果data.txt的第一列不是点的顺序编号呢?在这种情况下,可以用程序自动地为data.txt加上一列(第1列),使其成为顺序编号,然后用第2、第3列来绘图就可以了。

文章来源:https://www.codelast.com/
➤➤ 版权声明 ➤➤ 
转载需注明出处:codelast.com 
感谢关注我的微信公众号(微信扫一扫):

wechat qrcode of codelast

发表评论