DOT

头像
garfileo
    阅读 5 分钟
    5

    有个软件,名叫 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


    garfileo
    6k 声望1.9k 粉丝

    这里可能不会再更新了。


    引用和评论

    0 条评论