UML 绘制 -dot 语言

DOT 语言基础

基本的 DOT 文件

demo01.dot

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

从 DOT 文件生成图像

dot demo01.dot –Tpng –o demo01.png

渲染结果

PlantUML

PlantUML 是基于 Graphviz 的一个开源项目,并支持快速绘制:

  • 时序图
  • 用例图
  • 类图
  • 活动图 (here is the new syntax),
  • 组件图
  • 状态图
  • Deployment diagram,
  • 对象图
  • wireframe graphical interface

可以生成 png,svg,Latex 格式的图片,可以作为插件使用:

  • Intellij idea
  • Eclipse
  • NetBeans
  • Ckeditor
  • TinyMCE Editor
  • Sublime Text Editor
  • Vim
  • Emacs
  • Atom
  • ….

下面说一下 Sublime Text 安装 PlantUML 的过程:

  • 下载 PlantUML for Sublime 插件,并解压
  • 通过 Preferences -> Browse Packages … 打开 sublime 的 Packages 目录,解压后的插件放在 Packages 目录下
  • 重启 Sublime
  • 为了简化使用,可以在 Sublime 里配置个快捷键。打开 Preferences -> Key Binding - User,添加一个快捷键:
    { “keys”: [“alt+d”], “command”: “display_diagrams”}

上面的代码配置成按住 Alt + d 来生成 PlantUML 图片,你可以修改成你自己喜欢的按键。

参考自使用 Sublime + PlantUML 高效地画图

画状态图

我这里以状态图为例,如果你需要画其他图,到 PlantUML 查看

简单状态图

1
2
3
4
5
6
7
8
9
10
11
@startuml

[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string

State1 -> State2
State2 --> [*]

@enduml

快捷键 alt+d

简单状态图

合成状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@startuml
scale 350 width
[*] --> NotShooting

state NotShooting {
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}

state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved

state NewValuePreview {
State1 -> State2
}

}
@enduml
  • scale 350 width,指定图的宽度为 350,等比例缩放

合成状态图

长名字

如果状态的名称过长,使用 state 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@startuml
scale 600 width

[*] -> State1
State1 --> State2 : Succeeded
State1 --> [*] : Aborted
State2 --> State3 : Succeeded
State2 --> [*] : Aborted
state State3 {
state "Accumulate Enough Data\nLong State Name" as long1
long1 : Just a test
[*] --> long1
long1 --> long1 : New Data
long1 --> ProcessData : Enough Data
}
State3 --> State3 : Failed
State3 --> [*] : Succeeded / Save Result
State3 --> [*] : Aborted

@enduml

长名字

并发状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@startuml
[*] --> Active

state Active {
[*] -> NumLockOff
NumLockOff --> NumLockOn : EvNumLockPressed
NumLockOn --> NumLockOff : EvNumLockPressed
--
[*] -> CapsLockOff
CapsLockOff --> CapsLockOn : EvCapsLockPressed
CapsLockOn --> CapsLockOff : EvCapsLockPressed
--
[*] -> ScrollLockOff
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}

@enduml
  • 用 – 作为分隔符来合成并发状态。

并发状态

箭头方向

  • 使用 ->定义水平箭头,也可以使用下列格式强制设置箭头方向:
    1)-down-> (default arrow)
    2)-right-> or ->
    3)-left->
    4)-up->
1
2
3
4
5
6
7
@startuml
[*] -up-> First
First -> Second
Second --> Third
Third --> Fourth
Fourth -left-> Last
@enduml

箭头方向

注释

关键字:

  • note left of
  • note right of
  • note top of
  • note bottom of
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@startuml

[*] --> Active
Active --> Inactive

note left of Active : this is a short\nnote

note right of Inactive
A note can also
be defined on
several lines
end note

@enduml

  • 浮动注释
1
2
3
4
5
6
@startuml

state foo
note "This is a floating note" as N1

@enduml

  • 合成状态的注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@startuml

[*] --> NotShooting

state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}

note right of NotShooting : This is a note on a composite state

@enduml

主题修改

  • 使用 skinparam 命令改变字体和颜色。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@startuml
skinparam backgroundColor LightYellow
skinparam state {
StartColor MediumBlue
EndColor Red
BackgroundColor #e0f
BackgroundColor<<Warning>> #aaa
FontColor<<Warning>> Orange
BorderColor Orange
FontName Monaco
}

[*] --> NotShooting

state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle <<Warning>>
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}

NotShooting --> [*]
@enduml

有一个网站,可以实时预览 PlantText UML Editor

使用 graphviz 绘图

Graphviz 简介

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

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

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

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

一旦熟悉这种开发模式,就可以快速的将你的想法绘制出来。配合一个良好的编辑器 (vim/emacs) 等,可以极大的提高开发效率,与常见的 GUI 应用的所见即所得模式对应,此模式称为所思即所得。比如在我的机器上,使用 Sublime Text 编辑 dot 脚本,然后将 F7/Cmd-B 映射为调用 dot引擎 去绘制当前脚本,并打开一个新的窗口来显示运行结果:

对于开发人员而言,经常会用到的图形绘制可能包括:函数调用关系,一个复杂的数据结构,系统的模块组成,抽象语法树等。

基础知识

graphviz 包含 3 中元素,顶点。每个元素都可以具有各自的属性,用来定义字体,样式,颜色,形状等。下面是一些简单的示例,可以帮助我们快速的了解 graphviz 的基本用法。

第一个 Graphviz 图

比如,要绘制一个有向图,包含 4 个节点 a,b,c,d。其中 a 指向 bbc 指向 d。可以定义下列脚本:

1
2
3
4
5
6
7
8
9
10
digraph abc{
a;
b;
c;
d;

a -> b;
b -> d;
c -> d;
}

使用 dot 布局方式,绘制出来的效果如下:

默认的顶点中的文字为定义顶点变量的名称,形状为椭圆。边的默认样式为黑色实线箭头,我们可以在脚本中做一下修改,将顶点改为 方形,边改为 虚线

定义顶点和边的样式

digraph 的花括号内,添加顶点和边的新定义:

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

则绘制的效果如下:

进一步修改顶点和边样式

进一步,我们将顶点 a 的颜色改为 淡绿色,并将 cd 的边改为 红色,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
digraph abc{
node [shape="record"];
edge [style="dashed"];

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

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

绘制的结果如下:

应当注意到,顶点和边都接受属性的定义,形式为在顶点和边的定义之后加上一个由方括号括起来的 key-value 列表,每个 key-value 对由逗号隔开。如果图中顶点和边采用统一的风格,则可以在图定义的首部定义 node, edge 的属性。比如上图中,定义所有的顶点为方框,所有的边为虚线,在具体的顶点和边之后定义的属性将覆盖此全局属性。如特定与 a 的绿色,cd 的边的红色。

以图片为节点

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

1
2
3
4
5
6
7
8
9
10
11
12
13
digraph abc{
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"];
}

子图的绘制

graphviz 支持子图,即图中的部分节点和边相对对立 (软件的模块划分经常如此)。比如,我们可以将顶点 c 和 d 归为一个子图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
digraph abc{
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"];
}

cd 划分到 cluster_cd 这个子图中,标签为 c and d, 并添加背景色,以方便与主图区分开,绘制结果如下:

应该注意的是,子图的名称必须以 cluster 开头,否则 graphviz 无法设别。

数据结构的可视化

实际开发中,经常要用到的是对复杂数据结构的描述,graphviz 提供完善的机制来绘制此类图形。

一个 Hash 表的数据结构

比如一个 hash 表的内容,可能具有下列结构:

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; /* slot count */
int num_entries; /* total number of entries */
struct st_table_entry **bins; /* slot */
};

绘制 Hash 表的数据结构

从代码上看,由于结构体存在引用关系,不够清晰,如果层次较多,则很难以记住各个结构之间的关系,我们可以通过下图来更清楚的展示:

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
digraph st2{
fontname = "Verdana";
fontsize = 10;
rankdir=TB;

node [fontname = "Verdana", fontsize = 10, color="skyblue", shape="record"];

edge [fontname = "Verdana", fontsize = 10, color="crimson", style="solid"];

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

st_table:bins -> st_table_entry:head;
st_table:type -> st_hash_type:head;
st_table_entry:next -> st_table_entry:head [style="dashed", color="forestgreen"];
}

应该注意到,在顶点的形状为 record 的时候,label 属性的语法比较奇怪,但是使用起来非常灵活。比如,用竖线 “|” 隔开的串会在绘制出来的节点中展现为一条分隔符。用 <> 括起来的串称为锚点,当一个节点具有多个锚点的时候,这个特性会非常有用,比如节点 st_tabletype 属性指向 st_hash_type,第 4 个属性指向 st_table_entry 等,都是通过锚点来实现的。
我们发现,使用默认的 dot 布局后,绿色的这条边覆盖了数据结构 st_table_entry,并不美观,因此可以使用别的布局方式来重新布局,如使用 circo 算法:

则可以得到更加合理的布局结果。

Hash 表的实例

另外,这个 hash 表的一个实例如下:

脚本如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
digraph st{
fontname = "Verdana";
fontsize = 10;
rankdir = LR;
rotate = 90;

node [ shape="record", width=.1, height=.1];
node [fontname = "Verdana", fontsize = 10, color="skyblue", shape="record"];

edge [fontname = "Verdana", fontsize = 10, color="crimson", style="solid"];
node [shape="plaintext"];

st_table [label=<
<table border="0" cellborder="1" cellspacing="0" align="left">
<tr>
<td>st_table</td>
</tr>
<tr>
<td>num_bins=5</td>
</tr>
<tr>
<td>num_entries=3</td>
</tr>
<tr>
<td port="bins">bins</td>
</tr>
</table>
>];

node [shape="record"];
num_bins [label=" <b1> | <b2> | <b3> | <b4> | <b5> ", height=2];
node[ width=2 ];

entry_1 [label="{<e>st_table_entry|<next>next}"];
entry_2 [label="{<e>st_table_entry|<next>null}"];
entry_3 [label="{<e>st_table_entry|<next>null}"];

st_table:bins -> num_bins:b1;
num_bins:b1 -> entry_1:e;
entry_1:next -> entry_2:e;
num_bins:b3 -> entry_3:e;
}

上例中可以看到,节点的 label 属性支持类似于 HTML 语言中的 TABLE 形式的定义,通过行列的数目来定义节点的形状,从而使得节点的组成更加灵活。

软件模块组成图

Apache httpd 模块关系

在实际的开发中,随着系统功能的完善,软件整体的结构会越来越复杂,通常开发人员会将软件划分为可理解的多个子模块,各个子模块通过协作,完成各种各样的需求。
下面有个例子,是某软件设计时的一个草稿:

IDP 支持层为一个相对独立的子系统,其中包括如数据库管理器,配置信息管理器等模块,另外为了提供更大的灵活性,将很多其他的模块抽取出来作为外部模块,而支持层提供一个模块管理器,来负责加载/卸载这些外部的模块集合。
这些模块间的关系较为复杂,并且有部分模块关系密切,应归类为一个子系统中,上图对应的 dot 脚本为:

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
29
30
31
32
33
34
35
36
digraph idp_modules{

rankdir = TB;
fontname = "Microsoft YaHei";
fontsize = 12;

node [ fontname = "Microsoft YaHei", fontsize = 12, shape = "record" ];
edge [ fontname = "Microsoft YaHei", fontsize = 12 ];

subgraph cluster_sl{
label="IDP支持层";
bgcolor="mintcream";
node [shape="Mrecord", color="skyblue", style="filled"];
network_mgr [label="网络管理器"];
log_mgr [label="日志管理器"];
module_mgr [label="模块管理器"];
conf_mgr [label="配置管理器"];
db_mgr [label="数据库管理器"];
};

subgraph cluster_md{
label="可插拔模块集";
bgcolor="lightcyan";
node [color="chartreuse2", style="filled"];
mod_dev [label="开发支持模块"];
mod_dm [label="数据建模模块"];
mod_dp [label="部署发布模块"];
};

mod_dp -> mod_dev [label="依赖..."];
mod_dp -> mod_dm [label="依赖..."];
mod_dp -> module_mgr [label="安装...", color="yellowgreen", arrowhead="none"];
mod_dev -> mod_dm [label="依赖..."];
mod_dev -> module_mgr [label="安装...", color="yellowgreen", arrowhead="none"];
mod_dm -> module_mgr [label="安装...", color="yellowgreen", arrowhead="none"];
}

状态图

有限自动机示意图

上图是一个简易有限自动机,接受 aa 结尾的任意长度的串。其脚本定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
digraph automata_0 {
size = "8.5, 11";
fontname = "Microsoft YaHei";
fontsize = 10;

node [shape = circle, fontname = "Microsoft YaHei", fontsize = 10];
edge [fontname = "Microsoft YaHei", fontsize = 10];

0 [ style = filled, color=lightgrey ];
2 [ shape = doublecircle ];

0 -> 2 [ label = "a " ];
0 -> 1 [ label = "other " ];
1 -> 2 [ label = "a " ];
1 -> 1 [ label = "other " ];
2 -> 2 [ label = "a " ];
2 -> 1 [ label = "other " ];

"Machine: a" [ shape = plaintext ];
}

形状值为 plaintext 的表示不用绘制边框,仅展示纯文本内容,这个在绘图中,绘制指示性的文本时很有用,如上图中的 Machine: a

OSGi 中模块的生命周期图

OSGi 中,模块具有生命周期,从安装到卸载,可能的状态具有已安装,已就绪,正在启动,已启动,正在停止,已卸载等。如下图所示:

对应的脚本如下:

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
digraph module_lc{
rankdir=TB;
fontname = "Microsoft YaHei";
fontsize = 12;

node [fontname = "Microsoft YaHei", fontsize = 12, shape = "Mrecord", color="skyblue", style="filled"];
edge [fontname = "Microsoft YaHei", fontsize = 12, color="darkgreen" ];

installed [label="已安装状态"];
resolved [label="已就绪状态"];
uninstalled [label="已卸载状态"];
starting [label="正在启动"];
active [label="已激活(运行)状态"];
stopping [label="正在停止"];
start [label="", shape="circle", width=0.5, fixedsize=true, style="filled", color="black"];

start -> installed [label="安装"];
installed -> uninstalled [label="卸载"];
installed -> resolved [label="准备"];
installed -> installed [label="更新"];
resolved -> installed [label="更新"];
resolved -> uninstalled [label="卸载"];
resolved -> starting [label="启动"];
starting -> active [label=""];
active -> stopping [label="停止"];
stopping -> resolved [label=""];
}

其他实例

一棵简单的抽象语法树 (AST)
表达式 (3+4)*5 在编译时期,会形成一棵语法树,一边在计算时,先计算 3+4 的值,最后与 5 相乘。

对应的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
digraph ast{
fontname = "Microsoft YaHei";
fontsize = 10;

node [shape = circle, fontname = "Microsoft YaHei", fontsize = 10];
edge [fontname = "Microsoft YaHei", fontsize = 10];
node [shape="plaintext"];

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

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

简单的 UML 类图

下面是一简单的 UML 类图,DogCat 都是 Animal 的子类,DogCat 同属一个包,且有可能有联系 (0..n)

脚本:

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

fontname = "Courier New"
fontsize = 10

node [ fontname = "Courier New", fontsize = 10, shape = "record" ];
edge [ fontname = "Courier New", fontsize = 10 ];

Animal [ label = "{Animal |+ name : String\l+ age : int\l|+ die() : void\l}" ];

subgraph clusterAnimalImpl{
bgcolor="yellow"
Dog [ label = "{Dog||+ bark() : void\l}" ];
Cat [ label = "{Cat||+ meow() : void\l}" ];
};

edge [ arrowhead = "empty" ];

Dog->Animal;
Cat->Animal;
Dog->Cat [arrowhead="none", label="0..*"];
}

状态图


脚本:

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
digraph finite_state_machine {
rankdir = LR;
size = "8,5"

node [shape = doublecircle];

LR_0 LR_3 LR_4 LR_8;

node [shape = circle];

LR_0 -> LR_2 [ label = "SS(B)" ];
LR_0 -> LR_1 [ label = "SS(S)" ];
LR_1 -> LR_3 [ label = "S($end)" ];
LR_2 -> LR_6 [ label = "SS(b)" ];
LR_2 -> LR_5 [ label = "SS(a)" ];
LR_2 -> LR_4 [ label = "S(A)" ];
LR_5 -> LR_7 [ label = "S(b)" ];
LR_5 -> LR_5 [ label = "S(a)" ];
LR_6 -> LR_6 [ label = "S(b)" ];
LR_6 -> LR_5 [ label = "S(a)" ];
LR_7 -> LR_8 [ label = "S(b)" ];
LR_7 -> LR_5 [ label = "S(a)" ];
LR_8 -> LR_6 [ label = "S(b)" ];
LR_8 -> LR_5 [ label = "S(a)" ];
}

时序图

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
29
digraph G { 
rankdir="LR";
node[shape="point", width=0, height=0];
edge[arrowhead="none", style="dashed"]
{
rank="same";
edge[style="solided"];
LC[shape="plaintext"];
LC -> step00 -> step01 -> step02 -> step03 -> step04 -> step05;
}
{
rank="same";
edge[style="solided"];
Agency[shape="plaintext"];
Agency -> step10 -> step11 -> step12 -> step13 -> step14 -> step15;
}
{
rank="same";
edge[style="solided"];
Agent[shape="plaintext"];
Agent -> step20 -> step21 -> step22 -> step23 -> step24 -> step25;
}
step00 -> step10 [label="sends email new custumer", arrowhead="normal"];
step11 -> step01 [label="declines", arrowhead="normal"];
step12 -> step02 [label="accepts", arrowhead="normal"];
step13 -> step23 [label="forward to", arrowhead="normal"];
step24 -> step14;
step14 -> step04 [arrowhead="normal"];
}

rankdir="LR" 表示,布局从左 L 到右 R。可以看到,在代码中有 {} 括起来的部分。

1
2
3
4
5
6
{ 
rank="same";
edge[style="solided"];
Agency[shape="plaintext"];
Agency -> step10 -> step11 -> step12 -> step13 -> step14 -> step15;
}

每一个 rank="same" 的 block 中的所有节点都会在同一条线上。我们设置了所有的线为虚线,但是在该 block 中,将线改为 solided

附录

Graphviz中文教程指南.pdf (book118.com)

事实上,从 dot 的语法及上述的示例中,很容易看出,dot 脚本很容易被其他语言生成。比如,使用一些简单的数据库查询就可以生成数据库中的 ER 图的 dot 脚本。
如果你追求高效的开发速度,并希望快速的将自己的想法画出来,那么 graphviz 是一个很不错的选择。
当然,graphviz 也有一定的局限,比如绘制时序图 (序列图) 就很难实现。graphviz 的节点出现在画布上的位置事实上是不确定的,依赖于所使用的布局算法,而不是在脚本中出现的位置,这可能使刚开始接触 graphviz 的开发人员有点不适应。graphviz 的强项在于自动布局,当图中的顶点和边的数目变得很多的时候,才能很好的体会这一特性的好处:

比如上图,或者较上图更复杂的图,如果采用手工绘制显然是不可能的,只能通过 graphviz 提供的自动布局引擎来完成。如果仅用于展示模块间的关系,子模块与子模块间通信的方式,模块的逻辑位置等,graphviz 完全可以胜任,但是如果图中对象的物理位置必须是准确的,如节点 A 必须位于左上角,节点 B 必须与 A 相邻等特性,使用 graphviz 则很难做到。毕竟,它的强项是自动布局,事实上,所有的节点对与布局引擎而言,权重在初始时都是相同的,只是在渲染之后,节点的大小,形状等特性才会影响权重。
本文只是初步介绍了 graphviz 的简单应用,如图的定义,顶点/边的属性定义,如果运行等,事实上还有很多的属性,如画布的大小,字体的选择,颜色列表等,大家可以通过 graphviz 的官网来找到更详细的资料。
文中的代码都已经在 Github 上。