简 述: 讲解 dot 绘图语言的简要使用(graphviz 工具包),转载于 dot 绘图语言 一文(已沟通,获得转载许可)。

[TOC]


dot 绘图语言

graphviz 是一个很古老的开源软件包了,用来绘制结构化的图形。

通过它,你只需要编写简单的脚本(dot 语言),描述图的逻辑和关系, 它就能够自动帮你搞定布局之类的琐碎事情,生成你需要的图。

在编程当中,其实很多时候并不太在乎图的美观和布局,往往我们需要的是一种直观的图形方式来表达我们内在的复杂逻辑。 这时,graphviz 的威力就发挥出来了。只需要使用 dot 脚本简单描述下逻辑,就能很好的得到需要的直观图形。

这篇博文总结了下 dot 的常用语法和几个示例以让我更好的使用 dot。 我本人喜欢在 org-mode 中使用 org-babel 插入所需要的图,关于这方面的配置留到下篇博文再讲。😀

首先上一个很厉害也很实用的示例,有人用 dot 轻松的绘制出了离散数学的思维导图: https://zhuanlan.zhihu.com/p/20450190


1. 有向图和无向图

digraph G{
a -> b;
}

使用 digraph 定义有向图。上面的代码定义了有向图 G。 有向图中,使用 -> 符号表示边。上面的代码中,从节点 a 指向节点 b 有一条边。

graph G{
a -- b;
}

使用 graph 定义无向图。上面的代码定义了无向图 G。 无向图中,使用 – 符号表示边。上面的代码中,节点 a 和节点 b 之间有一条边。


2. 边和节点

如下定义了节点 a 和 b:

digraph G{
a;
b;
}

如下定义了节点 a 到 b 的边:

digraph G{
a -> b;
}

可以看到,在定义边的时候需要说明节点。如果这些节点之前没有定义过,在这里也会被定义。

如果一个节点到多个节点都有边,还有一种简写法:

digraph G{
a -> {b c};
}

甚至,多个节点到多个节点都有边:

digraph G{
{a b c} -> {d e f};
}

连着写也是可以的:

digraph G{
a -> b -> {c d};
}

实际上,末尾的分号是可写可不写的。而且无论你写不写分号,换不换行都不影响语义。 这个看个人习惯,dot 的语法并没有强制规定这个。


3. 设置属性

3.1. 对单个节点或边设置属性

在边或节点的后面加上一对方括号,里面使用 key=value 的形式设置它的属性。

边和节点都有个 label 属性,表示它实际显示的文字。默认情况下,边是不显示文字的,节点的文字就是节点的名称。 下面给节点和边手动设置 label 属性:

digraph G{
  a [label="节点 A"];
  b [label="节点 B"];
  a -> b [label="从 A 到 B"];
}

节点的 shape 属性表示节点的形状,边的 arrowhead 表示箭头的形状:

digraph G{
  a [label="节点 A", shape=circle];
  b [label="节点 B", shape=doublecircle];
  a -> b [label="从 A 到 B", arrowhead=vee];
}

可以看到,要设置这个边或这个节点的多个属性,之间用逗号隔开。

更多关于边和节点的形状,参考这里: 节点的形状:http://graphviz.org/content/node-shapes 边的形状:http://graphviz.org/content/arrow-shapes


3.2. 对所有的节点或边设置属性

如果你想一次设置对多个节点生效,这样:

digraph G{
node[shape=circle];
edge[arrowhead=vee];
  a [label="节点 A"];
  b [label="节点 B"];
  a -> b [label="从 A 到 B"];
}

在 node 后接中括号,里面写上属性设置,接下来定义的所有节点会默认使用这里的属性设置。

对多个边设置属性也类似节点。在 edge 后接中括号设置属性。


3.3. 设置图的属性

图本身也能设置一些属性,比如布局方式,图本身的说明标签:

digraph G{
rankdir = LR;
label = "示例图";
a -> b;
}

rankdir 设置了图的布局为 LR,即从左向右;默认图的布局是 TB,即从上向下。 label 设置了对图本身的说明标签。


3.4. 常用属性

这篇文章里对常用的图,边和节点的属性做了一个较好的总结: http://www.jianshu.com/p/5b02445eca1d


4. 注释

dot 支持 C 和 C++风格的注释,//和/* */两种。


5. 复杂的节点结构

通过对 label 进行一定格式的编写,就能够在 label 中呈现出如下这样的结构:

digraph structs {
    node [shape=record];
    struct1 [shape=record,label="<f0> left|<f1> mid\ dle|<f2> right"];
    struct2 [shape=record,label="<f0> one|<f1> two"];
    struct3 [shape=record,label="hello\nworld |{ b |{c|<here> d|e}| f}| g | h"];
    struct1 -> struct2;
    struct1 -> struct3;
}

注意到节点被分成了几个小方框,而且每一个方框都可以指定一个名称。

在连接节点时,通过这个名称就能更细的指定具体连接到哪一个小方框上:

digraph structs {
node [shape=record];
struct1 [shape=record,label="<f0> left|<f1> middle|<f2> right"];
struct2 [shape=record,label="<f0> one|<f1> two"];
struct3 [shape=record,label="hello\nworld |{ b |{c|<here> d|e}| f}| g | h"];
struct1:f1 -> struct2:f0;
struct1:f2 -> struct3:here;
}

使用这个特点,能够使用 dot 很方便的画出数据结构的图。


6. 子图

通过子图,可以对边和节点进行分组。

如果子图名以 cluster 开头,这个子图会用一个方框框起来。 而且 cluster 会被当成一个新的布局来处理。 你还可以对 cluster 设置某些属性,比如背景色,标签之类的。

digraph graphname
{
  a -> {b c};
  c -> b;
  subgraph cluster_bc
  {
    bgcolor=red;
    label="cluster";
    b;
    c;
  }
}

7. 几个示例

通过下面几个示例可以看到,用 dot 绘制标准化的图形,很简单直观。

悄悄的说一句,这些图,是我从官方的《dotguide》里面剽窃来的,嘘。。。


7.1. 函数调用图

digraph G {
size ="4,4";
main [shape=box]; /* this is a comment */
main -> parse [weight=8];
parse -> execute;
main -> init [style=dotted];
main -> cleanup;
execute -> { make_string; printf}
init -> make_string;
edge [color=red]; // so is this
main -> printf [style=bold,label="100 times"];
make_string [label="make a\nstring"];
node [shape=box,style=filled,color=".7 .3 1.0"];
execute -> compare;
}

7.2. 数据结构二叉树

digraph g {
node [shape = record,height=.1];
node0[label ="<f0> |<f1> G|<f2>"];
node1[label ="<f0> |<f1> E|<f2>"];
node2[label ="<f0> |<f1> B|<f2>"];
node3[label ="<f0> |<f1> F|<f2>"];
node4[label ="<f0> |<f1> R|<f2>"];
node5[label ="<f0> |<f1> H|<f2>"];
node6[label ="<f0> |<f1> Y|<f2>"];
node7[label ="<f0> |<f1> A|<f2>"];
node8[label ="<f0> |<f1> C|<f2>"];
"node0":f2 ->"node4":f1;
"node0":f0 ->"node1":f1;
"node1":f0 ->"node2":f1;
"node1":f2 ->"node3":f1;
"node2":f2 ->"node8":f1;
"node2":f0 ->"node7":f1;
"node4":f2 ->"node6":f1;
"node4":f0 ->"node5":f1;
}

7.3. 数据结构 hash 表

digraph G {
  nodesep=.05;
  rankdir=LR;
  node [shape=record,width=.1,height=.1];

  node0 [label = "<f0> |<f1> |<f2> |<f3> |<f4> |<f5> |<f6> | ",height=2.5];
  node [width = 1.5];
  node1 [label = "{<n> n14 | 719 |<p> }"];
  node2 [label = "{<n> a1  | 805 |<p> }"];
   node3 [label = "{<n> i9  | 718 |<p> }"];
   node4 [label = "{<n> e5  | 989 |<p> }"];
   node5 [label = "{<n> t20 | 959 |<p> }"] ;
   node6 [label = "{<n> o15 | 794 |<p> }"] ;
   node7 [label = "{<n> s19 | 659 |<p> }"] ;

   node0:f0 -> node1:n;
   node0:f1 -> node2:n;
   node0:f2 -> node3:n;
   node0:f5 -> node4:n;
   node0:f6 -> node5:n;
   node2:p -> node6:n;
   node4:p -> node7:n;
}

8. 最后及其它资料

可以看到,dot 这种语言语法简单,稍微学习就能编辑简单的脚本来描述复杂的图形。 对于这种方式生成图形,初始用时可能有些不习惯,毕竟使用这种“写代码”的方式来生成图形是很反直觉的。

但是熟悉之后就会发现,使用 dot 在画某些图形,特别是思维导图类时, dot 能帮助我们从烦琐的布局中解脱出来,让我们的思绪和注意力集中在真正的逻辑上。 专心思考一件事情时,任何一个分心点,都有可能让思维大打折扣。

在学习使用 dot 的过程中参考了以下资料,特此感谢: