有个软件,名叫 dot,来自 Graphviz。它只懂 DOT 语言,能够根据这种语言的描述来画图。
它会画什么图呢?
有向图。
马尔克斯的《百年孤独》这本书的主角是一个家族。这个家族的谱系,就可以构成有向图。若未读过《百年孤独》,不妨去读几遍。虽然也算世界名著,但应该很快就能看完。有不少人一年能读几百本书,《百年孤独》总是会出现在他们所列书单之内。下面就尝试用 DOT 语言描述一下这个家族的谱系。
这个很孤独的家族,一共七代人:
@ 七代人 #
{第一代 何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉}
{第二代 何塞·阿尔卡蒂奥(水手) 庇拉尔·特尔内拉(占卜者) 奥雷里亚诺·布恩迪亚(上校) 阿玛兰妲(灼手者) 十七位不知名女性}
{第三代 阿尔卡蒂奥(小独裁者) 桑塔索菲亚·德拉·彼达 奥雷里亚诺·何塞(姑妈控) 奥雷里亚诺·特里斯特、奥雷里亚·诺斯特诺以及其他十五人}
{第四代 蕾梅黛丝(美人儿) 何塞·阿尔卡蒂奥第二(斗鸡者) 奥雷里亚诺第二(养牛者) 费尔南达·德尔·卡皮奥(女王样)}
{第五代 何塞·阿尔卡蒂奥(掘金者)雷纳塔·蕾梅黛丝(梅梅) 马乌里肖·巴比伦 阿玛兰妲·乌尔苏拉(牝猫)}
{第六代 奥雷里亚诺·巴比伦(姨妈控)}
{第七代 猪尾巴}
@
为了简单起见,这个家族的风流韵事以及未能留下子嗣的女性,被我刻意忽略了。我觉得这些女性与这个家族的关系可以做成一张无向图,这个以后再说吧。
将上述 DOT 代码嵌入到有向图的结构中,可得
@ 第一次尝试 #
digraph test_1st {
# 七代人 @
}
@
digraph
是一个关键字,或将其视为一个指令,用于声明有向图结构。
注:倘若看过《文化编程》一文,便可使用 orez 从这份文档中抽取 DOT 代码。
假设上述 DOT 代码皆位于 foo.dot 文件(文本文件),绘图命令如下:
$ orez -t -e "第一次尝试" foo.md -o first.dot
$ dot -Tpng first.dot -o first.png
这样,dot 便可以画出图,但结果非我所期,年代与人物都位于同一行,而且图片扁且长,肉眼难辨:
这是因为,我的描述中所用的「第 x 代」,这里面的时间只对我有意义, dot 并没有这种时间概念。我只好明确地告诉它:
@ 时间 #
第一代 -> 第二代 -> 第三代 -> 第四代 -> 第五代 -> 第六代 -> 第七代
@
@ dot,我告诉你时间!#
digraph have_time {
# 时间 @
# 七代人 @
}
@
结果依然非我所期,dot 的确能把年代按照时间的顺序画出,但是人物依然还都在同一行。见下图
这是因为,尽管在描述七代人之时,我已刻意将同一代人放入大括号内,但 dot 依然不清楚各个人物属于哪代。例如
{第一代 何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉}
这种形式,在 DOT 语言里,称为子图。它的完整形式可以写为:
subgraph 子图_1 {第一代 何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉}
subgraph
用于声明一个子图,而 子图_1
是子图的名字——可根据需要,自行取名。在 dot 看来,大括号内的 第一代
、何塞·阿尔卡蒂奥·布恩迪亚
以及 乌尔苏拉
,它们并没有区别,都是图的结点,类似于渔网上的结点。
要让 dot 理解各个人物属于哪代,需要指定子图内的结点的阶级(rank)属性。dot 能够理解四种阶级属性,same,min,source,max 或 sink。same 表示子图内的结点属于同一阶级。min 表示子图内的结点与子图外的最底层结点属于同一阶级。max 表示表示子图内的结点与子图外的最高层结点属于同一阶级。source 与 sink 比 min 与 max 更严酷,分别让子图内的结点位于最底层或最高层。
现在只需要在各代人物所构成的子图内使用 same 属性,即:
{rank=same 第一代 何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉}
{rank=same 第二代 何塞·阿尔卡蒂奥(水手) 庇拉尔·特尔内拉(占卜者) 奥雷里亚诺·布恩迪亚(上校) 蕾梅黛丝·摩斯科特 阿玛兰妲(灼手者) 十七位不知名女性}
{rank=same 第三代 阿尔卡蒂奥(小独裁者) 桑塔索菲亚·德拉·彼达 奥雷里亚诺·何塞(姑妈控) 奥雷里亚诺·特里斯特、奥雷里亚·诺斯特诺以及其他十五人}
…… …… ……
{rank=same 第七代 猪尾巴}
由于每个子图都是 rank=same
,那么就可以将这个属性提到所有子图的外部,于是有
@ 第二次尝试 #
digraph test_2nd {
# 时间 @
rank=same
# 七代人 @
}
@
共同属性提出来,放在各个子图的上部,那么它描述的就是这些子图的母图属性。母图的属性会被属性设定语句之后的子图继承。
dot 现在真正明白了我的意图,便画出下面的图:
接下来,要理清这七代人之间的关系:
@ 七代人的关系 #
{何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉} -> {何塞·阿尔卡蒂奥(水手) 奥雷里亚诺·布恩迪亚(上校) 阿玛兰妲(灼手者)}
{何塞·阿尔卡蒂奥(水手) 庇拉尔·特尔内拉(占卜者)} -> 阿尔卡蒂奥(小独裁者)
{奥雷里亚诺·布恩迪亚(上校) 庇拉尔·特尔内拉(占卜者)} -> 奥雷里亚诺·何塞(姑妈控)
{奥雷里亚诺·布恩迪亚(上校) 十七位不知名女性} -> 奥雷里亚诺·特里斯特、奥雷里亚·诺斯特诺以及其他十五人
{阿尔卡蒂奥(小独裁者) 桑塔索菲亚·德拉·彼达} -> {蕾梅黛丝(美人儿) 何塞·阿尔卡蒂奥第二(斗鸡者) 佩特拉·科斯特 奥雷里亚诺第二(养牛者)}
{奥雷里亚诺第二(养牛者) 费尔南达·德尔·卡皮奥(女王样)} -> {何塞·阿尔卡蒂奥(掘金者) 雷纳塔·蕾梅黛丝(梅梅) 阿玛兰妲·乌尔苏拉(牝猫)}
{雷纳塔·蕾梅黛丝(梅梅) 马乌里肖·巴比伦} -> 奥雷里亚诺·巴比伦(姨妈控)
@
家族的谱系,记录的不过是谁和谁生了谁。将上述 DOT 代码中的 ->
理解为「生」即可。例如:
{何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉} -> {何塞·阿尔卡蒂奥(水手) 奥雷里亚诺·布恩迪亚(上校) 阿玛兰妲(灼手者)}
表示何塞·阿尔卡蒂奥·布恩迪亚与乌尔苏拉,生育了三个孩子,何塞·阿尔卡蒂奥、奥雷里亚诺·布恩迪亚、阿玛兰妲。属于同一代人的合法夫妇可以用两个子图来构成结点之间的走向,本质上这是一种乘法运算。
这个家族的第五代与第六代,有一对男女发生了乱伦,这时就没法用子图的形式来表示这对男女生了猪尾巴,否则会导致 dot 误以为第五代与第六代是同一代人……程序就是这样傻,所以不要随便乱伦。
奥雷里亚诺·巴比伦与阿玛兰妲·乌尔苏拉生了猪尾巴,这件事,只能像下面这样记录:
@ 乱伦 #
奥雷里亚诺·巴比伦(姨妈控) -> 猪尾巴
阿玛兰妲·乌尔苏拉(牝猫) -> 猪尾巴
@
于是有
@ 第三次尝试 #
digraph test_3rd {
# 时间 @
rank=same
# 七代人 @
# 七代人的关系 @
# 乱伦 @
}
@
dot 的输出结果为:
像这样的图,就叫有向图。
dot 画出来的这种有向图,其特点具有层次性,所以特别适合绘制像家谱这种形式的图。
现在,这个家谱图是画了出来,但是它基本上还是写意的,并不美观。要美化一下,也是可以的,但这需要劳心而伤神。对于这个家谱图,从「美学」的角度,我愿意做的美化是去掉「第 x 代」的椭圆框,并且将家族中的女性的名字标为蓝色。女人是水做的,而且还是海水。
首先去掉「第 x 代」的外框:
@ 第四次尝试 #
digraph test_4th {
rank=same
node[shape=plaintext]
# 时间 @
node[shape=ellipse]
# 七代人 @
# 七代人的关系 @
# 乱伦 @
}
@
这里所作的变动是,在时间段落之前增加 node[shape=plaintext]
语句。shape=plaintext
的意思是,结点的形状是无外框的普通文本。
由于 node[shape=plaintext]
设置的是图中所有结点的形状,而我想保留每个人的名字的椭圆外框,因此不得不在 # 七代人 @
代码段落之前将结点外形重新设为 ellipse
。
现在,dot 可以画出:
接下来,修改所有女性结点的名字颜色,没有什么好方法(或许是有,但我不知道),只能逐一设定:
@ 女性名字为蓝色的七代人 #
{第一代 何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉[color=blue, fontcolor=blue]}
{第二代 何塞·阿尔卡蒂奥(水手) 庇拉尔·特尔内拉(占卜者)[color=blue, fontcolor=blue] 奥雷里亚诺·布恩迪亚(上校) 阿玛兰妲(灼手者)[color=blue, fontcolor=blue] 十七位不知名女性[color=blue, fontcolor=blue]}
{第三代 阿尔卡蒂奥(小独裁者) 桑塔索菲亚·德拉·彼达[color=blue,fontcolor=blue] 奥雷里亚诺·何塞(姑妈控) 奥雷里亚诺·特里斯特、奥雷里亚·诺斯特诺以及其他十五人}
{第四代 蕾梅黛丝(美人儿)[color=blue,fontcolor=blue] 何塞·阿尔卡蒂奥第二(斗鸡者) 奥雷里亚诺第二(养牛者) 费尔南达·德尔·卡皮奥(女王样)[color=blue,fontcolor=blue]}
{第五代 何塞·阿尔卡蒂奥(掘金者) 雷纳塔·蕾梅黛丝(梅梅)[color=blue,fontcolor=blue] 马乌里肖·巴比伦 阿玛兰妲·乌尔苏拉(牝猫)[color=blue,fontcolor=blue]}
{第六代 奥雷里亚诺·巴比伦(姨妈控)}
{第七代 猪尾巴}
@
将重写的七代人名单再加入 DOT 的图结构中:
@ 第五次尝试 #
digraph test_5th {
rank=same
node[shape=plaintext]
# 时间 @
node[shape=ellipse]
# 女性名字为蓝色的七代人 @
# 七代人的关系 @
# 乱伦 @
}
@
现在,dot 便可以画出:
图中的各条边,也可以增加一些文字标注。譬如,第五代与第六代的乱伦生子,可以强调一下:
@ 带标注的乱伦 #
奥雷里亚诺·巴比伦(姨妈控) -> 猪尾巴 [color=red,label="乱伦"]
阿玛兰妲·乌尔苏拉(牝猫) -> 猪尾巴 [color=red,label="乱伦"]
@
于是 dot 可根据以下代码
@ 第六次尝试 #
digraph test_6th {
rank=same
node[shape=plaintext]
# 时间 @
node[shape=ellipse]
# 女性名字为蓝色的七代人 @
# 七代人的关系 @
# 带标注的乱伦 @
}
@
画出
关于 DOT 的更多知识,自然是要去看它的文档:https://graphviz.gitlab.io/_pages/pdf/dotguide.pdf
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。