有个软件,名叫 dot,来自 Graphviz。它只懂 DOT 语言,可以根据这种语言的描述来画图。node
它会画什么图呢?git
有向图。编程
马尔克斯的《百年孤独》这本书的主角是一个家族。这个家族的谱系,就能够构成有向图。若未读过《百年孤独》,不妨去读几遍。虽然也算世界名著,但应该很快就能看完。有很多人一年能读几百本书,《百年孤独》老是会出如今他们所列书单以内。下面就尝试用 DOT 语言描述一下这个家族的谱系。segmentfault
这个很孤独的家族,一共七代人:ide
@ 七代人 # {第一代 何塞·阿尔卡蒂奥·布恩迪亚 乌尔苏拉} {第二代 何塞·阿尔卡蒂奥(水手) 庇拉尔·特尔内拉(占卜者) 奥雷里亚诺·布恩迪亚(上校) 阿玛兰妲(灼手者) 十七位不知名女性} {第三代 阿尔卡蒂奥(小独裁者) 桑塔索菲亚·德拉·彼达 奥雷里亚诺·何塞(姑妈控) 奥雷里亚诺·特里斯特、奥雷里亚·诺斯特诺以及其余十五人} {第四代 蕾梅黛丝(美人儿) 何塞·阿尔卡蒂奥第二(斗鸡者) 奥雷里亚诺第二(养牛者) 费尔南达·德尔·卡皮奥(女王样)} {第五代 何塞·阿尔卡蒂奥(掘金者)雷纳塔·蕾梅黛丝(梅梅) 马乌里肖·巴比伦 阿玛兰妲·乌尔苏拉(牝猫)} {第六代 奥雷里亚诺·巴比伦(姨妈控)} {第七代 猪尾巴} @
为了简单起见,这个家族的风流韵事以及未能留下子嗣的女性,被我刻意忽略了。我以为这些女性与这个家族的关系能够作成一张无向图,这个之后再说吧。gitlab
将上述 DOT 代码嵌入到有向图的结构中,可得ui
@ 第一次尝试 # digraph test_1st { # 七代人 @ } @
digraph
是一个关键字,或将其视为一个指令,用于声明有向图结构。spa
注:假若看过《 文化编程》一文,即可使用 orez 从这份文档中抽取 DOT 代码。
假设上述 DOT 代码皆位于 foo.dot 文件(文本文件),绘图命令以下:code
$ orez -t -e "第一次尝试" foo.md -o first.dot $ dot -Tpng first.dot -o first.png
这样,dot 即可以画出图,但结果非我所期,年代与人物都位于同一行,并且图片扁且长,肉眼难辨:blog
这是由于,个人描述中所用的「第 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