使用 Graphviz 绘图

Table of Contents

1. 简介

Graphviz 是一款开源的绘图软件,主要用来绘制关系、流程图。与多数“所见即所得”的绘图软件不同,Graphviz 使用 DOT 语言来绘图(可理解为“所思即所得”),这样我们可专注于图片要表达的内容本身,而不去考虑如何画一根线、如何处理元素之间的布局等等,这些工作都由 Graphviz 去做;由于通过编程方式绘图,源文件也是纯文本形式的,便于我们用任意编辑器编辑,同时便于使用版本控制软件管理。

DOT 语言用于描述节点、线和图(Graph),其中图又分为有向图和无向图。

2. 无向图

无向图即节点之间的连线没有箭头指向的方向。定义语法为:

graph 图名称 {
        节点1 -- 节点2;
}

节点之间的连线用“–”。另外,DOT 语言的注释如下:

// 行注释
/* 块注释 */

例,定义一棵二叉树:

// example.dot
graph example {
        a -- b;
        a -- c;
        b -- d;
        b -- e;
        c -- f;
        c -- g;
}

然后用 dot 命令生成图片(命令使用方法请见“附录”)。

结果如下图:

1.png

3. 有向图

有向图即节点之间通过箭头表示方向。定义语法如下:

digraph 图名称 {
        节点1 -> 节点2; // 节点之间关系用”->“表达
}

例:

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

输出如下:

2.png

a -> b;
a -> c;

还可用等价的语法表示:

a -> {b, c};

4. 属性设置

在 DOT 语言中我们还可以对图、节点和连线进行样式的属性设置。

以下几节列举一些常用的,完整的属性请见官方文档:http://www.graphviz.org/content/attrs

4.1. 通用属性

4.1.1. label

label 可为图、线和节点设置文字标签。

例,为节点设置标签:

digraph example {
        0 [label="顶点0"];
        0 -> 1 [label="从0到1"];
}

效果如下:

6.png

例,为图设置标题:

digraph example {
        graph [label="图的标题"];
        a -> b;
}

效果如下:

7.png

4.1.2. 设置字体样式

4.1.2.1. 字体大小设置
digraph example {
        a [fontsize=18, label="18号字体"]
        b [fontsize=12, label="12号字体"];
        a -> b;
}

效果如下:

8.png

4.1.2.2. 字形设置

要设置字形,需要使用的 HTML 标签。注意 label 里使用“<…>”包围字符串。

digraph example {
        b [label=<<b>粗图</b>、<i>斜体</i>、<u>下划线</u>>];
        a -> b;
}

9.png

4.2. 线属性

4.2.1. 箭头样式设置

digraph example {
        a -> b [label="默认是实线"];
        b -> c [style=dotted, label="虚线"];
        c -> d [style=bold, label="粗线"];
        d -> e [dir=none, label="无箭头"];
}

效果如图:

10.png

4.2.2. 反向箭头

digraph example{
        a -> b;
        b -> c [dir=back]
}

效果如图:

3.png

4.2.3. 双向箭头

digraph example {
        a -> b [dir=both];
}

效果如图:

4.png

4.2.4. 横向箭头

digraph example {
        rankdir = LR;  // LR 表示箭头方向是从左到右,要想从右到左,改成RL即可
        a -> b -> c -> d;
}

效果如图:

5.png

4.3. 节点属性

4.3.1. 改变节点的形状

下面代码列举了一些常用形状:

digraph example {
        a [shape=box, label="矩形"];
        b [shape=circle, label="圆"];
        c [shape=polygon, label="平行四边形"];
        d [shape=ellipse, label="椭圆"];
        a -> b -> c -> d;
}

效果如下图:

11.png

4.3.2. 节点边框设置

4.3.2.1. 无边框

代码如下,取消节点“b”的边框样式:

digraph example {
        b [shape=none];
        a -> b;
}

效果如下图:

12.png

还可以把节点变成文本样式:

digraph example {
        rankdir = LR;  // 节点的方向是从左到右
        node [shape=plaintext];
        a -> b -> c -> d;
}

效果如下图:

13.png

4.3.2.2. 改变边框颜色
digraph example {
        b [color="red"];
        a -> b;
}

效果如下图:

14.png

4.3.3. 填充颜色

4.3.3.1. 渐变色填充
digraph example {
        b [style=filled, fillcolor="red:black"];  // 红色到黑色的渐变
        a -> b;
}

效果如下图:

15.png

4.3.3.2. 纯色填充
digraph example {
        b [style=filled, fillcolor="red"];
        a -> b;
}

效果如下图:

16.png

4.3.4. 节点局部属性设置

如果想更改图中某些局部节点的样式,可以把这些节点加在一对花括号中,并设置样式。示例如下:

digraph example {
        a -> b -> c -> d;

        // 设置“点1”和“点2”的样式
        {
                node [shape=plaintext, style="filled", fillcolor="red"];
                点1 -> 点2;
        }
}

效果如下图:

17.png

4.4. label 属性的高级用法——结构体

下面展示下如何用 label 将一个节点“分割”成多个部分,类似 C 语言的结构体。

如下代码,将一个节点表达成一个链表结构,然后用连线来连接:

digraph example {
        node [shape=record];
        // label属性中,用“|”分割出元素,“<..>”之间的定义成一个变量
        struct1 [label="<head> head | item1 | item2 | ... | <tail>tail"];
        // 用“节点名:元素名”的方式引用结构体的变量
        struct1:head -> struct1:tail;
}

效果如下图:

18.png

还可以对结构体元素中再进行分割,只需在元素中用“{ … }”划分。如下,表达 ELF 结构体:

digraph example {
        node [shape=record];
        struct1 [label="ELF Header | Header Table | {Segment1 | Segment2 | ...} | Section Header Table"];
}

效果如下图:

19.png

5. 子图

子图展示出来的效果就像下面这样,把一组节点单独划分到一个“图”中:

20.png

只需用 subgraph 关键字定义子图即可,注意子图的名字必须以“cluster”开头。上图对应的代码如下:

digraph example {
        a -> b;

        subgraph cluster_sub1 {
                // 子图可以设置自己的样式
                label ="子图";
                labelloc = "t";
                bgcolor = "gray";
                a -> c -> d;
        }
}

5.1. 将某个节点指向一个子图

DOT 语言中不能将一个节点指向一个子图,例如表达成:

a -> cluster_sub1

不能这样表达,DOT 也不支持节点指向子图。要达到这种效果,只有靠设置属性来改变生成后的外观效果。

例如下面的代码:

digraph example {
        compound = true;  // 必须设置该属性
        subgraph cluster_sub1 {
                c -> d;
                c -> e;
        }

        A -> B;
        A -> C;
        A -> d [lhead=cluster_sub1];
}

其中 c、d 和 e 节点单独在一个子图中,我们想让 A 节点直接指向子图 cluster_sub1,这时只用将 A 节点指向子图的某个节点,然后为这条线设置 lhead 属性即可。上面代码生成后的效果如下:

21.png

6. 用其他工具修改图片

Graphviz 最大的问题就是输出图片时是依赖自动布局算法,某些时候可能需输出的图片并非我们想要的样子,如果想调整图片,可以先输出成 SVG 格式:

$ dot -Tsvg example.dot -o example.svg

然后再用 Inkscape 等工具修正即可。

7. 附录

7.1. 安装 Graphviz

1、GNU/Linux 下可通过自带的包管理器安装,如 Fedora:

$ sudo dnf install graphviz

2、官方下载二进制包:http://www.graphviz.org/

7.2. 命令使用方法

在 shell 下执行如下命令生成图片:

$ dot -Tpng [dot脚本文件] -o [输出的文件名.png]

-T 参数指定输出的格式。可通过 -Tv 参数可列出支持导出的格式:

$  dot -Tv
Format: "v" not recognized. Use one of: bmp canon cmap cmapx cmapx_np dot eps fig gtk gv ico imap imap_np ismap jpe jpeg jpg pdf pic plain plain-ext png pov ps ps2 svg svgz tif tiff tk vdx vml vmlz x11 xdot xdot1.2 xdot1.4 xlib