要让 Mac 上的 Graphviz 层级图像缩进大纲一样易读,请为每个节点指定行和缩进坐标,然后使用 neato -n2 渲染。这样既能保留父子边,也能让你控制阅读顺序。

本指南将创建一个包含六个节点的示例,并导出为 PDF。目标很明确:每行一个节点,后代紧接在其父节点下方,且同一深度的节点缩进相同。Graphviz 在其 -n 命令行选项中介绍了固定位置渲染。

为什么普通的 Graphviz 树看起来不像大纲?

层级描述的是关系;缩进大纲还规定了阅读顺序。在本例中,根节点包含两个分支,第一个分支的后代必须排在第二个分支之前。期望的顺序如下:

Node R
    Node A
        Node C
        Node D
    Node B
        Node E

图中的关系是根节点到 A、根节点到 B、A 到 C、A 到 D,以及 B 到 E。D 和 B 之间没有关系,只因为 B 出现在下一行。DOT 使用 -> 运算符表示有向边;节点声明与节点的显示属性是语言中彼此独立的部分。请参阅官方的 DOT 语言参考。

Graphviz 的 dot 引擎会生成分层或分层式布局。它会尝试减少交叉和边长,同时让边朝着共同方向延伸。这一目标适合绘制分支图,但并未规定上方示例所示的每个节点占一行的大纲布局。请参阅 dot 布局文档。

一些常见属性只能解决部分问题:

设置 文档所述用途 未解决的问题
rankdir=LR 将布局方向改为从左到右 未指定纵向大纲顺序
rank=same 将子图中的节点放在同一层级 未为每个节点指定独立的大纲行
ordering=out 约束出边按照输入顺序从左到右排列 未定义基于深度的缩进和行坐标

这些边界与 rankdir、rank 和 ordering 的官方参考文档一致。对于本文要求的精确行布局,显式坐标比组合这些属性更直接。

如何在 Mac 上准备 Graphviz?

打开 Terminal。如果你已经在使用 Homebrew,请运行以下命令安装 Graphviz。官方 Graphviz 下载页面列出了这种 Mac 安装方式:

brew install graphviz

如果你使用的是 MacPorts,同一下载页面列出了 sudo port install graphviz。选择你已经在用的包管理器。如果两者都尚未安装,请先通过该页面中的相应链接完成安装,再继续本指南。

检查渲染器是否可用:

neato -V

-V 选项会输出 Graphviz 版本信息。比较不同电脑上的结果时,请记录输出的版本;本文不要求显示某个特定版本号。Graphviz 在其命令行参考中介绍了此选项。

如何创建缩进层级图?

以下完整的 shell 代码块会创建一个新的工作文件夹,并在其中写入 DOT 文件。在 Terminal 中运行;完成后会打印文件夹路径。带引号的 heredoc 分隔符会按原样保留 DOT 文本。

outline_dir=$(mktemp -d "$HOME/graphviz-outline.XXXXXX")
if [ -n "$outline_dir" ] && cd "$outline_dir"; then
  cat > hierarchy.dot <<'DOT'
digraph Hierarchy {
  graph [splines=ortho, bgcolor=white];
  node [
    shape=box,
    width=1.2,
    height=0.35,
    fixedsize=true,
    fontname="Courier",
    fontsize=12
  ];

  r [label="Node R", pos="100,300"];
  a [label="Node A", pos="136,252"];
  c [label="Node C", pos="172,204"];
  d [label="Node D", pos="172,156"];
  b [label="Node B", pos="136,108"];
  e [label="Node E", pos="172,60"];

  r -> a;
  r -> b;
  a -> c;
  a -> d;
  b -> e;
}
DOT
  pwd
fi

节点位置定义了大纲布局。每往下一行,纵向移动 48 个点;每增加一层深度,横向移动 36 个点。这些数字只是示例间距,并非 Graphviz 默认值。pos 文档说明节点位置指的是节点中心;与 -n 一起使用时,neato 会将位置解释为点。

节点 深度 行,从零开始 中心位置
Node R 0 0 100,300
Node A 1 1 136,252
Node C 2 2 172,204
Node D 2 3 172,156
Node B 1 4 136,108
Node E 2 5 172,60

在此布局中,坐标规则是 x = 100 + 36 × depth 和 y = 300 − 48 × row。注意从 Node D 回到 Node B:行号增加了,但深度降低了。正是这一次变化,让图看起来像大纲,而不是一条链。

所有方框尺寸相同,因此中心坐标相同的节点,其左边缘也会对齐。长度相同的示例标签便于比较文本位置。使用 fixedsize=true 时,Graphviz 会采用指定的节点尺寸,而不会为了容纳内容而扩大方框;标签过大会产生警告。请参阅 fixedsize 参考文档。

如何渲染并检查结果?

在同一个 Terminal 会话和工作文件夹中运行:

neato -n2 -Tpdf hierarchy.dot -o hierarchy.pdf

这里,-n2 会保留提供的节点布局,不进行避免重叠的调整,并为未指定自身位置的边计算路径。因此,每个节点都需要有一个 pos 值。-Tpdf 和 -o 选项分别用于选择 PDF 输出格式和输出位置。这些行为见于命令行参考。

在 Finder 中打开工作文件夹,再打开 hierarchy.pdf。根据 Graphviz 的 PDF 输出页面,PDF 输出要求 Graphviz 构建版本包含 Cairo 渲染器。本例已根据文档进行核对,但由于研究环境中没有 Graphviz,因此未在此处实际渲染测试。

请分别检查以下三点:

  1. 从上往下阅读:R、A、C、D、B、E 应依次占据各行。
  2. 比较缩进:A 和 B 应对齐;C、D 和 E 应再向右缩进一级并彼此对齐。
  3. 沿边检查关系:R 连接到 A 和 B;A 连接到 C 和 D;B 连接到 E。

前两项检查坐标布局,第三项检查图中的实际关系。将这些检查分开,有助于发现看似整齐、实际却包含错误父子边的图。

连接线会和文件浏览器中的树完全一样吗?

节点行和缩进是明确指定的;连接线的路径仍由系统计算。本例指定了 splines=ortho,会让边沿轴对齐线段布线。Graphviz 文档还指出,这种布线方式不支持端口,因此添加罗盘端口并不是强制特定折线样式的有据可依方法。请参阅 splines 参考文档。

因此,这种构造能生成带自动直角连接线的缩进层级图,但不会规定一条共用的垂直主干,并精确放置各分支的连接点。如果连接线几何形状至关重要,就需要额外构造并显式指定边路径;仅靠 splines=ortho 无法表达这一要求。

如果想要不显示连接线的整洁大纲,请将图设置改为 splines=none,然后重新渲染。DOT 文件仍然包含父子边,但渲染器会省略这些边的线条。这是同一 splines 参考文档中介绍的渲染选项。

如何在不改变含义的情况下调整示例?

先独立于边列表写下预期的阅读顺序。先访问父节点,再访问其第一个子节点及其后代,然后才转到下一个子节点。为序列中的每个节点分配一行,再根据深度分配缩进。

例如,要在 Node D 后面为 Node A 添加另一个子节点,请在阅读顺序中将它插入 Node B 之前。为它指定深度 2,将后续各行下移,并添加一条从 A 指向新节点的边。现有父子关系不会仅仅因为行号改变而发生变化。

编辑后的版本看起来不对时,请按以下顺序排查:

  • **图形扩散成了另一种布局:**确认命令仍然使用 neato -n2。本例依赖提供的位置,而非普通的自动布局。
  • **渲染提示缺少位置:**检查每个节点 ID,包括只出现在边语句中的 ID。在这种工作流程中,每个节点都需要有带位置的声明。
  • **标签超出方框:**增大所有方框的尺寸,或缩短标签。固定尺寸的方框不会自动扩展。
  • **增大方框后各行发生重叠:**增大行间距。所选渲染模式会保留位置,而不会修正重叠。
  • **分支连接到了错误的节点:**修正边语句。移动方框无法修复错误的关系。

缺少位置和发生重叠的情况符合 Graphviz 文档中关于 -n2 行为的说明;标签问题符合其 fixedsize 行为。对于这个手动定位的示例,增大间距是相应的设计调整。

将层级的边列表作为关系的依据,将坐标表作为显示布局的依据。对于 Mac 桌面上的小型示意图,这种分工便于维护:层级关系改变时调整边;阅读顺序改变时调整行;标签需要更多空间时调整间距。

来源