graphviz


graphviz是贝尔实验室开发的一个开源的工具包,它使用一个特定的DSL(领域特定语言): dot作为脚本语言,然后使用布局引擎来解析此脚本,并完成自动布局。graphviz提供丰富的导出格式,如常用的图片格式,SVG,PDF格式等。

用graphviz来绘图的时候,你的主要工作就是编写dot脚本,你只要关注图中各个点之间的关系就好了,你不需要考虑如何安排各个节点的位置,怎样布局能够使你所绘制的图看起来更美观一些。

graphviz中包含了众多的布局器:

  • dot 默认布局方式,主要用于有向图
  • neato 基于spring-model(又称force-based)算法
  • twopi 径向布局
  • circo 圆环布局
  • fdp 用于无向图

首先,在dot脚本中定义图的顶点和边,顶点和边都具有各自的属性,比如形状,颜色,填充模式,字体,样式等。然后使用合适的布局算法进行布局。布局算法除了绘制各个顶点和边之外,需要尽可能的将顶点均匀的分布在画布上,并且尽可能的减少边的交叉(如果交叉过多,就很难看清楚顶点之间的关系了)。所以使用graphviz的一般流程为:

  • 定义一个图,并向图中添加需要的顶点和边
  • 为顶点和边添加样式
  • 使用布局引擎进行绘制

安装运行

以Windows为例,从官网上下载安装包 graphviz-2.38.msi,https://graphviz.gitlab.io/_pages/Download/Download_windows.html

安装完后,在开始菜单里面,开始->所有程序->Grapgviz 2.38->gvedit.exe。打开后如图:

gvedit.png

点击新建文件,输入如下代码:

1
2
3
4
5
6
digraph test{
a;
b;

a -> b;
}

运行程序,点击下图中用红色标注的按钮,可以看到如下运行效果:

test_graphviz.png

如果想在命令运行或其他的编辑器中运行dot脚本,需要把graphviz安装后的bin目录添加到环境变量中,添加完后,打开命令行,输入以下命令,验证是否安装成功:

dot_cmd.png

有向图和无向图

使用graph定义无向图,使用digraph定义有向图,如上面的使用的test使用的是有向图,无向图和有向图最大的区别就是,有向图使用的连接线是->, 而无向图使用的是--。把上面的test改成无向图:

1
2
3
4
5
6
graph test{
a;
b;

a -- b;
}

运行如下图:

test_graph.png

图的属性

图的属性设置方式有两种,其一是直接在{}内列出,如:size = ”4, 4”;另一种方式使用:graph [属性列表],如下代码,为上面的test添加图的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
digraph test {
bgcolor = "#666666"; // 背景
fontname="Inconsolata, Consolas"; // 字体
fontcolor=white; // 字体颜色
fontsize = 10; // 字体大小

labelloc = b // 标签垂直顶=底部
labeljust = c // 标签水平居中

label="Attributes of a graph"

a;
b;

a -> b;
}

在graph [属性列表] 添加属性,效果和上面是一样的:

1
2
3
4
5
6
7
8
9
10
digraph test {
graph [bgcolor = "#666666", fontname="Inconsolata, Consolas",
fontcolor=white, fontsize = 10, labelloc = b, labeljust = c,
label="Attributes of a graph" ]

a;
b;

a -> b;
}

运行效果,如下图:

graph_attributes.png

更多的属性设置,可以参考官方的文档https://graphviz.gitlab.io/_pages/doc/info/attrs.html

节点和连接线

节点和连接线的属性

节点和连接线的属性和图的属性设置是差不多的,用node的列表可以设置节点的属性,edge设置连接线的属性。如下代码,把节点设置为矩形,连接线为虚线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

digraph test {
graph [bgcolor = "#666666", fontname="Inconsolata, Consolas",
fontcolor=white, fontsize = 10]

node [shape="record"];
edge [style="dashed"];

a;
b;
c;
d;

a -> b;

b -> d;
c -> d;
}

运行效果,如下图:

node_line.png

用node和edge设置的属性是全局的,会影响所有的节点,如果想给只给某几个节点或某条连接线设置属性,可以只设置某个节点和连接线的属性列表,如下代码,设置a节点的颜色为淡绿色,并将c到d的边改为红色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
digraph test {
graph [bgcolor = "#666666", fontname="Inconsolata, Consolas",
fontcolor=white, fontsize = 10]

node [shape="record"];
edge [style="dashed"];

a [style="filled", color=black, fillcolor="chartreuse"];
b;
c;
d;

a -> b;

b -> d;
c -> d [color=red];
}

运行效果,如下图:

node_line_style.png

当线和线label 比较多时,可以给线的属性decorat=true,使得每条线的label标识其所属的连接线。

还可以给每条线加上headlabel和taillabel,给每条线的起始点和终点加上label,他们的颜色由labelfontcolor 来决定,而label的颜色由fontcolor来决定。

如下代码:

1
2
3
4
5
6
7
8
9
10
11
graph test {
edge [decorate=true];
A -- B [label = "s1"];
A -- C [label = "s2"];
A -- D [label = "s3"];
B -- C [label = "s4"];
B -- D [label = "s5"];

edge [decorate = false, labelfontcolor = blue, fontcolor = red];
E -- F [headlabel = "e ", taillabel = "f ", label = " e - f"];
}

运行效果,如下图:

line_decorate.png

以图片为节点

除了颜色,节点还可以使用图片。不过需要注意的是,在使用图片作为节点的时候,需要将本来的形状设置为none,并且将label置为空字符串,避免出现文字对图片的干扰:

digraph test {
    node [shape="record"];
    edge [style="dashed"];

    a [style="filled", color="black", fillcolor="chartreuse"];
    b;
    c [shape="none", image="logos/browser-icon-chrome-resized.png", label=""];
    d;

    a -> b;
    b -> d;
    c -> d [color="red"];
}

运行效果,如下图:

include_image.png

箭头的方向

用dir属性可以设置每条边箭头的方向,有forward(默认)、back、both、none 四种。如下代码:

1
2
3
4
5
6
digraph test {
A -> B [dir=both];
B -> C [dir=none];
C -> D [dir=back];
D -> A [dir=forward];
}

运行效果,如下图:

graphviz_dir.png

线的位置

默认时图中的线都是从上到下的,我们可以将其改为从左到右,在文件的最上层打入rankdir=LR 就是从左到右,默认是TB(top -> bottom),也可以是RL,BT。

如下代码,则把线的设置为从左至右:

1
2
3
4
5
6
7
8
digraph test {
rankdir=LR

a;
b;

a -> b;
}

运行效果,如下图:

lr.png

如果需要点能排在一行(列),这时要用到rank,用花括号把rank=same,然后把需要并排的点一次输入。

如下代码,a和e排成一列、c和d排成一列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    digraph test {
rankdir=LR

a;
b;
c;
d;
e

a -> b -> c -> d -> e;

{rank=same; c; d}
{rank=same; a; e}
}

运行效果,如下图:

rank.png

设立一条边时,可以制定这条边从起点的那个位置射出和从哪个位置结束。遵守:上北下南,左西右东的原则,分别使用方位n(North), s(South), w(West), e(East)来定义,一共有8个连接点:{n, ne, e, se, s, sw, w, nw},如下代码:

1
2
3
4
5
6
7
8
9
10
11
digraph test {
node[shape = box];
c:n -> d[label = n];
c1:ne -> d1[label = ne];
c2:e -> d2[label = e];
b:se -> a[label = se];
c3:s -> d3[label = s];
c4:sw -> d4[label = sw];
c5:w -> d5[label = w];
c6:nw -> d6[label = nw];
}

运行效果,如下图:

nwse.png

如下代码,绘制一颗二叉树:

1
2
3
4
5
6
7
8
9
10
11
12
13
digraph test {
label = "Binary search tree";
node[shape = record];
A[label = "<f0> | <f1> A |<f2> "];
B[label = "<f0> | <f1> B |<f2> "];
C[label = "<f0> | <f1> C |<f2> "];
D[label = "<f0> | <f1> D |<f2> "];
E[label = "<f0> | <f1> E |<f2> "];
A:f0:sw -> B:f1;
A:f2:se -> C:f1;
B:f0:sw -> D:f1;
B:f2:se -> E:f1;
}

运行效果,如下图:

bst.png

表格
  1. gaphviz的表格也是一种节点类型,使用label标签来指定内容和格式

  2. 内置布局方式通过shape为record(矩形框) & Mrecord(圆角矩形框)来指定,如果和当前方向一致使用|来隔开,使用{|}表示和当前方向相反,比如说默认是纵向布局方式,那|隔开的也是从上到下,{|}则是从左到右,只是要注意,如果是在一个{|}内部再{|},又变成了从上到下(如果只是文字换行,可以在文字末尾加\n(居中对齐)\l(左对齐)\r(右对齐)进行切分)

  3. 内置布局最大的问题是不能分别调整每个格子里的颜色和字体,所以如果需要更丰富的控制可以通过使用HTML表格标签,这种方式不要求shape为record或Mrecord样式,另外需要注意的是使用<>而不是双引号来引用内容

如下代码:

1
2
3
4
5
6
7
8
9
digraph test {
node [shape=record];
s1 [label="<l>left|middle|<r>right"];
s2 [label="<h>one|two"];
s3 [label="a|{b|{c|d|e}|f}|g|h"];

s1:l -> s2:h;
s1:r ->s3;
}

table.png

绘制hash表的数据结构

如下面的代码是一个C语言的struct结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    struct st_hash_type {
int (*compare) ();
int (*hash) ();
};

struct st_table_entry {
unsigned int hash;
char *key;
char *record;
st_table_entry *next;
};

struct st_table {
struct st_hash_type *type;
int num_bins;
int num_entries;
struct st_table_entry **bins;
};

用dot脚本描述,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
digraph test {
fontname="Inconsolata, Consolas";
fontsize = 10;

node [shape="record", color="skyblue"];
edge [color="crimson", style="solid"];

st_hash_type [label="{<h>st_hash_type|(*compare)|(*hash)}"]
st_table_entry [label="{<h>st_table_entry|hash|key|record|<n>next}"]
st_table [label="{st_table|<t>type|num_bins|num_entries|<b>bins}"];

st_table:b -> st_table_entry:h;
st_table:t -> st_hash_type:h;
st_table_entry:n -> st_table_entry:h [style="dashed", color="forestgreen"];
}

运行效果,如下图:

hash.png

子图

graphviz支持子图,即图中的部分节点和边相对对立,子图使用subgraph定义,必须以cluster开头命名,比如,下面的代码将顶点c和d归为一个子图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
digraph test {

node [shape="record"];
edge [style="dashed"];

a [style="filled", color="black", fillcolor="chartreuse"];
b;

subgraph cluster_cd{
label="c and d";
bgcolor="mintcream";
c;
d;
}

a -> b;
b -> d;
c -> d [color="red"];
}

运行效果,如下图:

subgraph.png

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
digraph test {
subgraph cluster0 {
style=filled;
color=lightgrey;
node [style=filled, color=white];

a0 -> a1 -> a2 -> a3;
label="process #1"
}

subgraph cluster1 {
color=blue;
node [style=filled];

b0 -> b1 -> b2 -> b3;
label="process #2"
}

start -> a0;
start -> b0;
a1 -> b3;
b2 -> a3;
a3 -> end;
b3 -> end;

start[shape=Mdiamond];
end[shape=Msquare];
}

shape=Mdiamond设置的图形的形状,关于更多的图形可以参考http://www.graphviz.org/doc/info/shapes.html,运行效果,如下图:

subgraph_01.png

其他

更多的图形的绘制可以参考官方的文档。