Back to prev

Let Draw More Easier

Jan 31, 2021
Linkang Chan
@Jesse Chan

日常工作中我们会多或少有画图的需求,这些图不限于:流程图、示意图、折线图、柱状图等等。在能满足绘图需求的同时,也需要根据对应的场景选择对应的风格和格式。所以为了绘制出合适的图我们需要用到不同的工具。 这篇文章主要是程序员的角度来说一说如何在具体的场景下绘制出对应的图。

我个人比较倾向于能够通过书写代码的形式生成对应的图,但是这样往往会带来一些环境搭建以及编译转换中的负担,同时还要再学习一套新的语法,这也或多或少是对普通需求人的一种劝退,但是本文不尽然都是通过代码绘图的介绍,也会有一些通过鼠标拖拽形式的绘图工具。

好了,废话不多说,Let’s Draw begin.

上古神器

所谓的上古神器,指的是计算机比较初期时,Unix 上使用的一些工具。这些工具主要是用在文档排版上的,比如 《c程序设计语言》 K&R 这个版本中介绍指针,二叉树的配图,又或是 APUE 中大量的示意图等等。这些配图在上世纪八九十年代时可以轻松的被绘制出来,而且看起来都很精美。 实际上这些书都是通过 troff 这个排版工具完成的。不同于 Tex, troff 在排版上相当完备且无需过多的折腾(关于 troff 我会写单独一片文章来介绍)。完成 troff 文档的编写是需要通过一组工具的配合才能完成的。上面提到的那些图实际上是通过 pic 这个标记语言完成的。

除了pic外,可以用在 troff 中的还有grap,dformat等。 目前大部分的 类Unix 发行版上都默认安装了这些工具,在 GNU 分支下,发型版中都是 groff, groff 中进行了一定扩展,但是基本的语法和使用方式基本没多大变化。

我们先看看 pic 是如何使用的,我们从一个简单例子开始:

.PS
arrow; box "input"; arrow; box "process"; arrow; box "output";arrow
.PE

使用保存内容到a.ms中,运行:

$ pic a.ms | groff -ms -Tps | ps2pdf - > a.pdf

这会在 pdf 中生成如下的图:

pic_ex

这个例子在写文档时能够直接在文档中插入,省去了打开绘图软件,绘制后保存并插入到指定的文档中的过程。这个例子是简单的介绍了 pic 的使用。 而当通过一定的脚本进行组合之后,可以对一些固定格式的图片进一步进行简化,比如dfromat,这个用 awk 所完成的白十来行的脚本工具,可以用于绘制数据格式的图形。比如下面这个:

dformat_ex

这是一个数据结构的比特位的示意图。你可能会觉得绘制这样的图会比较复杂,需要写很多的代码。然而只需要下面的这几行简单的代码就能完成:

.begin dformat
style bitwid 0.125
style linethrutext 0
CSR
   31-8 Reserved
   7    Lock
   6    Word
   5    Single
   4    Wake
   3-1  Max Transactions
   0    Stop
.end

而编译命令就只要:

$ awk -f dformat.awk bits.ms | pic | groff -Tps -ms  | ps2pdf - bits.pdf

这边的 dformat.awk 可以从 [arnoldrobbins/dformat] repo 中获取到。具体的使用可以查看 repo 中的使用说明。

上面说的工具是在 groff(troff) 文档中绘制一些表示图,但是如果我们要绘制图表时,在 troff 中可以使用 grap。 语法也是比较简单的。比如我们要绘制一个点图,只需要写如下的代码:

.G1
54.2
49.4
49.2
50.0
48.2
44.60
.G2

把内容保存在 olymp.g 中,并执行:

$ grap olymp.g | pic | groff -Tps -ms | ps2pdf - olymp.pdf

这会生成下面的图:

grap_ex

在快速绘图的情况下,这种方式算是非常简单而快捷得了。

说到绘图gnuplot也必须提一嘴,Gnuplot 是一款命令行绘制函数、数据等图表的工具。这在命令行中使用也是非常的简洁明了的。

比如我们定义了一个函数,想绘制出不同的参数下的参数情况。可以写下如下的代码:

set terminal png
set output "./output/rand.png"
g(x, s) = exp(-0.5*(x/s)**2)/s
plot g(x,1), g(x,2), g(x,3)
exit

将上述代码保存到rand中,此时只需要执行gnuplot rand即可。 执行无误后会在 output 中生成下面的图片:

rand.png

这似乎和 python 的一些库比较类似。但实际上看语法 gnuplot 的形式比较接近数学的形式。但是学习的成本略微有一些高,一些细节上的语法和troff 比较类似,和现代的一些画图工具的差别有点大。

上面这些主要是命令行绘图工具,大部分可以在不安装任何依赖的情况下,开箱即用。当然了一些在 vim 中或者其他工具中的完成的绘图就另当别论了。其他的一些图片生成和处理工具就不在此细说了,比如:imageMagick/ffmpeg,这些工具涉及到更高一些的图片处理工作,我会在笔记或者另外的文章中整理这些。本文暂不讨论了。

现代之作

随着时代的发展,很多的工具都在不断的简化,旨在降低使用门槛,或者是降低获取成本。这其中比较有名的是 [graphviz] 和 platuml 等工具语言。 先说说 graphviz 吧.

graphviz

grahpviz 是一个开源的图像可视化软件。比较适合用来绘制结构化的图表和网络,依托 dot 语言,可以通过其他的语言批量生成一些图片绘制脚本。比如在一些性能监测程序中会通过 graphviz 来生成函数调用栈关系,内存申请层级关系等等。

我们首先从 dot 语言开始,简单的绘制几个图,先从最基本的一个图开始:

digraph G {
  size="4,4"
  rankdir=LR
  node [shape=box, color=blue]
  node1 [style=filled]
  node2 [style=filled, fillcolor=red]

  node0->node1->node2
}

定义了一个有向图 G, 布局方向是从左到右(默认是从上到下的布局,这边通过 rankdir=LR 修改方向)。 三个节点,每个节点都是 box 形状的,边框是蓝色。 其中 node1 是用蓝色填充的, node2 是用红色填充的。 将上述代码保存在node.dot,并运行下面的命令:

$ dot -Tpng node.dot -o node.png

就会生成下面的图形:

node.png

实际上大部分的示意图都可以通过 dot 绘制,比如绘制一个二叉树的表达式:

digraph ast {
    fontname = "PingFang";
    fontszie = 10;

    node [shape = "plaintext", fontname = "PingFang", fontsize = 10];
    edge [fontname = "PingFang", fontsize = 10];

    mul [label = "mul(*)"];
    add [label = "add(+)"];

    add -> 3;
    add -> 4;
    mul -> add;
    mul -> 5;
}

在这个例子中可以看到指定了字体和其大小,也修改节点的默认样式。编译后会生成下面的图像:

ast.png

这种通过代码修改节点样式,不需要微调额外的关心节点之间间距的方式,对笔者而言是非常舒服的操作方式。不过痛点也是有的,要想实现一些比较骚的操作时,需要发散一下思维,以及要去翻阅文档,这一点对于使用鼠标画图工具而言灵活性稍微差点。不过当定义好节点样式之后,绘制图片也是非常方便的。

再举一例,通过 graphviz 绘制类似于 git 的 graph 示意图。

digraph G {
	rankdir="LR";
	node[width=0.15, height=0.15, shape=point, color=black];
	edge[weight=2, arrowhead=none, color=black];
	node[group=master];
	1 -> 2 -> 3 -> 4 -> 5;
	node[group=branch];
	2 -> 6 -> 7 -> 4;
}

这会生成如下的示意图:

git.png

虽然在此时绘制这样的图用代码会显得有点冗余,但是在大量绘制的时候,在定义好节点样式后,绘制起来还是非常舒服的。

PlantUML

说到画图,则不得不说到 PlatnUML 这个开源的工具,在绘制时序图,用例图等等,甚至是一些非UML的图也是可以胜任的(不过这个和上述的 graphviz 也是有着一定的联系的)

一个最简单的例子:

@startuml
 Alice -> Bob: test
@enduml

当你下载了 PlantUML 的 jar 包后,将上述内容保存到first.txt中,在命令执行:

$ java -jar plantuml.jar first.txt
$ ls 
first.png first.txt plantuml.jar 

生成的图像如下:

first_plantuml.png

时序图在介绍流程时是非常好用的,之前的方式都是使用 visio 通过手动绘制,这个过程比较痛苦的是不停的在线段是否笔直和间距中调整。通过代码这个工作可以很快的完成。

对于这样的时序,如果在命令中加入-txt, 则会生成 ASCII 形式的 txt 文件,比如:

$ java -jar plantuml.jar -txt first.txt
$ cat first.atxt
     ,-----.          ,---.
     |Alice|          |Bob|
     `--+--'          `-+-'
        |     test      |
        |-------------->|
     ,--+--.          ,-+-.
     |Alice|          |Bob|
     `-----'          `---'

这对在代码中插入图形注释又提供了很大的便利,有关 ASCII 绘图的,后面会提到一些工具来实现。接下来让我们看看使用 plantUML 绘制 JSON 的结构图,使用如下的代码:

@startjson
{
  "firstName": "John",
  "lastName": "Smith",
  "isAlive": true,
  "age": 27,
  "address": {
    "streetAddress": "21 2nd Street",
    "city": "New York",
    "state": "NY",
    "postalCode": "10021-3100"
  },
  "phoneNumbers": [
    {
      "type": "home",
      "number": "212 555-1234"
    },
    {
      "type": "office",
      "number": "646 555-4567"
    }
  ],
  "children": [],
  "spouse": null
}
@endjson

在经过编译之后,会生成如下的图像:

puml_json.png

或许你会觉得这样的配色不是很舒服,plantUML 提供了修改箭头的颜色和样式等功能。让一些个性化定制成为可能。plantUML 也提供了很详细的文档说明和示例。方便我们学习和扩展,比如可以参考:

chan.