关于 d2-diagram
d2-diagram 是用于编写可编辑 D2 图表并通过 TALA 布局渲染的技能,适合需要在文档或代码中使用图表的作者。它还可将渲染出的形状与连接关系同预期事实表比对。
主要功能
- 01以 D2 文本编写图表
- 02生成终端、ASCII、SVG 和 PNG 输出
- 03使用 TALA 布局
- 04检查渲染后的形状与连接
- 05依据事实表比对图表
- 06检测重复形状和截断渲染
适用场景
- 01在文档中加入可编辑的架构图
- 02生成适用于终端或 README 的图表
- 03根据说明验证图表连接关系
- 04无需重新运行 D2 检查 SVG 或文本渲染
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mizchi/ explainer --skill d2-diagram安装后在 Claude Code 中输入/d2-diagram确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/d2-facts.mjsvlmkit-workspace.d2vlmkit-workspace.facts.jsonvlmkit-workspace.svgvlmkit-workspace.txt详细介绍
d2-diagram
以下路径中:<skill> 是此技能所在目录,<skills> 是安装这些技能的目录(mizchi/explainer 中为 skills/,在已安装这些技能的项目中为 .claude/skills/)。
D2 文件就是图表;图片是它的渲染结果。编写者负责文本——ID、标签、容器和连接;TALA 负责安排方框的位置以及线条的弯折方式。终端渲染的作用是让编写者每次都能在不离开 shell 的情况下查看图片,也让图表可以放在 README 代码块、PR 评论或 --help 文本中。
direction: right
web: Web frontend
api: API server
db: PostgreSQL {shape: cylinder}
web -> api: HTTPS
api -> db: SQL
.-‾‾‾‾‾‾-.
│╲-______-╱│
HTTPS SQL │ │
┌─────────────┐──────────────▶┌───────────┐─────────────▶│ │
│Web frontend │ │API server │ │PostgreSQL│
│ │ │ │ │ │
└─────────────┘ └───────────┘ ╲-______-╱
(原样运行 d2 --layout=tala x.d2 x.txt,D2 版本为 0.8.1-HEAD。)
此技能适用于交付物为 D2 文本 的图表:之后会有人编辑它,由 TALA 排版,并且可以在 D2 支持的任何地方渲染。分步过程(算法、请求)不做动画:只绘制一次结构,并在说明文字中标出步骤编号。为了让 D2 图表以代码为依据,先从代码中列出模块及其依赖项(导入、package.json),将它们写入事实清单,然后严格按清单绘制。
调用
d2 layout # must list `tala (bundled)`. That, not the version
# string, is the gate: v0.8.1-HEAD already bundles it.
d2 --layout=tala arch.d2 arch.txt && cat arch.txt # the terminal render (Unicode box drawing)
d2 --layout=tala --ascii-mode standard arch.d2 arch.txt # plain ASCII (+ - | / < > v) for a README
d2 --layout=tala arch.d2 --stdout-format ascii - # to stdout, for a pipe or a PR comment
d2 --layout=tala arch.d2 arch.svg # the figure for docs
d2 --layout=tala arch.d2 arch.png # a raster to LOOK at (see the loop, step 5)
d2 validate arch.d2 # syntax only; exit 1 with file:line:col
d2 fmt --check arch.d2 # formatted? exit 1 and names the file, changes nothing
d2 fmt arch.d2 # ...and this rewrites it in place
node <skill>/assets/d2-facts.mjs arch.d2 # what the picture DRAWS
node <skill>/assets/d2-facts.mjs arch.d2 --expect f.json # ...held to a sheet
LC_ALL=C.UTF-8 wc -L arch.txt # the width in columns — the locale matters, see below
如果缺少 d2,请安装它——切勿通过在线 Playground 来驱动图表:
curl -fsSL https://d2lang.com/install.sh | sh -s -- # the installer; it rejects the obsolete --tala flag
brew install d2 # macOS
go install github.com/d2lang/d2@latest # Go 1.27+; what to do when the install script gets a 403 through a proxy
环境变量中的 D2_LAYOUT=tala 与此标志作用相同。对于 .svg / .png,文件中的 vars: { d2-config: { layout-engine: tala } } 设置会生效,但对 .txt 无效。在本次构建测试(0.8.1-HEAD)中,文本渲染回退到了 dagre,结果有 163 列,而不是 120 列。因此无论如何都要传入此标志;同时保留文件中的配置,以便后续使用者的 SVG 也采用相同的引擎。sequence_diagram 是证明规则的例外——它会自行排版,因此无论是否使用此标志,文本渲染结果都逐字节相同。
D2 中没有任何内容会告诉你图有误
validate 会读取语法。渲染器会按文件内容绘制。没有
--expect,没有布局报告,而且**引用不在作用域内的 ID 不会报错——它会创建一个新形状。**因此,下面是一张悄然出错的图:
edge: { cdn; gateway }
cluster: { orders; inventory }
gateway -> orders # ← two NEW empty boxes at the root; the real ones keep no arrow
在本技能自己的验证轮次中,一位编写者恰好提交了这样的结果:d2 validate
返回 0,d2 fmt 检查干净,宽度为 94 列,日志还写着“可以追踪所有连接”。渲染结果却有四个重复的框、两个孤立框,而且系统的入口调用被画成浮动的一对,位于所有区域之外
(报告)。
**跨容器连接应在根级使用完整路径编写。**这是始终有效的唯一写法,也是本轮中所有正确完成任务的编写者都采用的写法:
edge.gateway -> cluster.orders # at the root: full path on both ends
cluster: {
billing -> _.outside.stripe # from INSIDE: `_` is this container's parent
}
_ 表示上一级,且仅在容器内部有效。在 cluster 内部,_ 指向根,因此,兄弟容器中的形状应写作 _.outside.stripe,而不是 _.stripe;后者会凭空创建一个根级的 stripe(这是本轮第四个幽灵形状)。在根级,_ 没有可指代的父级:d2 会因 invalid underscore 编译失败,后来的另一位编写者遇到这个问题后,误以为转义功能坏了。
assets/d2-facts.mjs 补上了 D2 所没有的检查。它会渲染文件,再从 SVG 中读取图形——d2 会将每个形状和每条连接的完全限定 ID 写入其中——因此,它报告的是读者实际看到的内容,而不是源文件看起来表达的意思。它没有依赖项;可以将它复制到另一个仓库,放在图表旁边使用。
node assets/d2-facts.mjs arch.d2 # boxes, containers and their members, labels, edges, columns
node assets/d2-facts.mjs arch.d2 --expect arch.facts.json
node assets/d2-facts.mjs arch.d2 --seeds 4,5,6 # the seed the final render will use, so the width it reports is real
node assets/d2-facts.mjs --from-svg arch.svg --from-txt arch.txt # read renders that already exist, no d2 run
最后一种形式展示了检查器本身如何受到测试:tests/d2/d2-facts.test.mjs 会使用提交在 tests/d2/fixtures/d2-scenario/ 下的渲染结果对其进行测试,无须安装 d2;因此,幽灵框、截断、反向边和宽度问题各自都有测试,一旦检查不再触发,测试就会失败。它也可用于检查其他人生成的渲染结果,或审查 PR 中的 .svg 文件。
即使没有清单,它也会自动检测三种必然属于缺陷的问题:同一个名称被画出两次(见上文的作用域陷阱)、与兄弟形状重叠或超出容器的框,以及被截断的终端渲染结果——top / left 定位点可能会把形状推到 ASCII 画布之外,而 d2 对一个只包含片段的文本文件仍会返回 0。一位编写者有五张表的架构图通过了“71 列”的检查,但刚测量的渲染结果中有三张表缺失;这就是为什么要逐框对比。它还会对没有任何连接的框发出警告。提供清单后,它还会检查:
{
"boxes": ["gateway", "orders", "broker"],
"deps": ["gateway->orders", "orders->broker"],
"forbidden": ["orders->billing"],
"containers": { "cluster": ["orders", "inventory"] },
"order": ["browser->gateway", "gateway->orders"],
"exhaustive": true,
"maxColumns": 100,
"allowOrphans": ["legend"]
}
deps会按方向检查:反向箭头会显示✗ edge reversed: the sheet says broker->billing, the picture draws billing->broker。exhaustive会将清单中未列出的边视为错误,而不是警告。order会根据 y 坐标读回序列图中的消息顺序——这是 D2 唯一能明确显示顺序的地方。- 名称可以匹配 ID 的最后一段或其标签,因此,对于写作
gw: API gateway的文件,清单可以写gateway。 - 它无法查看
sql_table内部:列级连接(orders.customer_id -> customers.id)会被报告为orders->customers。约束徽标也不会检查;可以用grep -o '>FK</text>' arch.svg | wc -l统计它们(grep -c统计的是行数,而 SVG 只有一行,所以无论有多少徽标,它都会输出 1),再查看 PNG,确认每个箭头实际从哪一行引出。
应在绘制图表之前根据简报编写清单,就像先编写测试一样。它是 TALA 重新排版后唯一能保留下来的产物。
工作表中的名称如何找到对应图形
工作表中的名称不会只按 ID 匹配;搜索范围会逐步扩大,直到恰好找到一个匹配的图形:
- ID 的最后一段——
orders可以找到cluster.orders - 精确的标签——
gateway可以找到gw: API gateway - ID 或标签的子字符串,前提是恰好只有一个图形匹配
第 3 步有意采用宽松匹配,这可能会让你感到意外。一个名为 pg、标签为 Postgres orders (authoritative until cutover) 的方框就是工作表中的 orders,因此,指向它的边会触发 forbidden: ["checkout->orders"]。这并非工作表或图示出错,而是它们对名称的理解不一致——因此,现在每一行启用宽松匹配时都会说明使用了哪种匹配方式:
✗ forbidden edge drawn: checkout->orders (orders matched no id — it is a
substring of box pg's label "Postgres orders (authoritative until cutover)")
编辑任何内容前,请先读完这条说明:有了它,你可以选择限定工作表的范围,或修正图示。没有它,编写者可能会为了通过检查器而给一个本来正确的方框重新命名
(tests/d2/fixtures/d2-scenario/v2/label-substring/)。
为什么选择 TALA,以及何时不该使用
TALA(Terrastruct 自动布局算法)是 D2 自有的软件架构图引擎,以 MPL-2.0 开源,并随 D2 一起打包(公告中提到的版本是 0.9.0;这里测量的 0.8.1-HEAD 构建也包含它——请查看 d2 layout,而不是版本号)。dagre 和 ELK 会将有向图分层布局,而 TALA 会像在白板上绘图一样排列方框——采用正交布局,尽可能保持对称,并按容器分组;它也是唯一支持所有 D2 功能的引擎:每个容器的 direction、容器的 width / height、near: <other shape>,以及用于固定图形位置、让引擎放置其余图形的 top / left。
| 图示…… | 引擎 |
|---|---|
| 容器中的方框,带有多个方向的连接(架构图、部署图、数据模型) | tala |
| 方向明确、包含多个跳转的单一流程(管道、DAG) | 使用 direction: right 的 tala;如果布局仍然难以阅读,则使用 elk——公告明确指出,TALA 对流程图的处理效果较差 |
| 30 个以上的图形 | 按容器拆分成两个文件。TALA 的运行时间会随规模非线性增长:这里,30 个节点耗时 0.94s,而 ELK 耗时 0.05s;无论使用哪种引擎,这种规模的布局也会难以阅读 |
让随机性可复现。 TALA 会尝试三个种子(--tala-seeds,默认值为
1,2,3),并保留最佳的完整结果,因此同一文件两次渲染出的图像相同——实测结果完全一致。标签更改仍可能导致布局发生连锁变化,因此不要在文字描述中说明几何位置(“左边的方框”);请使用 ID。在尝试使用种子之前,请先了解以下两条实测注意事项:
- 更换种子集通常不会改变结果。 一位编写者为包含五张表的架构尝试了十二组种子,得到的布局每次都完全相同,均为 109 列;另一位使用
--tala-seeds 4,5,6将列数从 113 降到了 100。可以试一次,保留实测效果最好的数字,但不要指望它一定有效。 - 种子无法保存在文件中。
d2 layout tala提示“tala-seeds 下的图表数据优先于命令行标志”,但vars.d2-config.tala-seeds会在编译时被拒绝。如果已提交的图表布局依赖某个种子,只有每次渲染都传入该标志才能得到对应布局——因此,请将该标志写入脚本或 Makefile 目标中,否则就不要依赖它。
按任务选择
| 任务类型 | 编写方式 |
|---|---|
| “绘制架构/展示服务之间的关系” | 每个区域使用一个容器(cluster: our cluster { orders; inventory }),在根级别连接子项(edge.gateway -> cluster.orders),或在容器内部使用 _. 连接。在根级别设置 direction: right;在最完整的容器上设置容器级 direction,以控制宽度,见下文 |
| “绘制数据模型/表” | 使用 shape: sql_table,包含 id: int {constraint: primary_key} 和 customer_id: int {constraint: foreign_key} 行;连接各列(orders.customer_id -> customers.id) |
| “绘制组件之间的请求/调用流程” | 在根级别使用 shape: sequence_diagram;按首次使用顺序列出参与者;每行写一条带标签的消息。D2 没有返回或异步箭头,而且终端渲染会去掉所有样式,因此区别只能体现在标签中——在开头或结尾放置不超过四个字符、不带标点的裸标记(ret 201 Created、order.placed async),并在图上方的说明文字中解释。reserved (ret) 不会保留:渲染会用线条字符覆盖括号,而且标签填满泳道时,箭头会完全没有箭头头部。图内也无法添加图例——shape: text 会渲染为空框 |
| “绘制类” | 使用 shape: class,并添加 +method(): type/-field: type 行 |
| “绘制部署/网络” | 为主机和区域使用容器;为存储使用 shape: cylinder,为外部服务使用 shape: cloud,为代理使用 shape: queue |
“我们已经有一个 .d2 文件” | 编辑它;运行 d2 fmt --check;前后都进行渲染;未经说明,不要更换其引擎或主题 |
| “将其放入 README/代码块/PR 评论” | 将 standard ASCII 渲染放入围栏代码块;按下文测量宽度;将 .d2 文件放在旁边 |
| “需要对照代码进行检查” | 先将代码中的模块和依赖项(导入、package.json)列入清单,再精确绘制这些内容,然后运行 d2-facts --expect |
循环
1. write arch.facts.json the boxes, edges and width the brief asks for
2. write arch.d2 ids ASCII; the label after the colon is what the reader sees
3. d2 fmt --check arch.d2 && d2 validate arch.d2 syntax; exit 1 names file:line:col
4. node assets/d2-facts.mjs arch.d2 --expect arch.facts.json
duplicates, missing / reversed / invented edges,
container members, overlaps, order, width
5. d2 --layout=tala arch.d2 arch.png then READ arch.png with your file-reading tool — you can
look at an image, and this is the only step that shows
labels, shapes, key badges and which row an arrow leaves.
Both writers who skipped it reported the same thing:
"a PNG read would have answered this"
6. d2 --layout=tala arch.d2 arch.txt && cat arch.txt the deliverable, if it is going in a terminal
7. fix the text; go to 3
最多进行五轮。每一步回答不同的问题:第 3 步是“能否编译”,第 4 步是“是否表达了我的意思”,第 5 步是“是否易读”,第 6 步是“能否适配目标位置”。编译错误只占一行——
arch.d2:2:9: invalid style keyword: "colour"、2:1: connection missing destination——修复错误指出的那一行。
完成条件
d2 validate和d2 fmt --check均以 0 退出。d2 layout列出了tala (bundled),且渲染使用了它。d2-facts --expect以 0 退出:没有重复名称,没有被截断的渲染结果;清单中的每个框和边都已按其方向绘制;没有禁止的边;宽度未超出预算——测量时使用与最终渲染相同的--seeds。- 你至少查看过一次 PNG,而且它是交付物之一。
- 说明文字中的每个形状和连接都通过其 id 命名,说明文字中的每个 id 也都存在于文件中。
.d2文件与 SVG 一起提交;SVG 是构建产物,或与.d2一起提交——绝不能只粘贴出来。
宽度:按终端的显示方式来测量
LC_ALL=C.UTF-8 wc -L arch.txt # 122 — correct, and CJK-aware
wc -L arch.txt # 120 in the C locale: an undercount, and 87 vs 111 on a Japanese figure
awk '{if(length($0)>m)m=length($0)}END{print m}' arch.txt # 338 — BYTES. Never this.
d2-facts.mjs 会打印与 columns 相同的数字,应引用这个数字。
两位作者独立遇到了低估问题;其中一位花了一轮时间缩小渲染图,但它原本就在预算范围内。
这些调整不会带来单调变化。每次调整后都要测量。 同一轮中在两个不同的图表上测得:
| 调整项 | 图表 A | 图表 B |
|---|---|---|
根节点 direction: right → down | 144 → 145(更糟) | 148 → 109(有效) |
| 缩短标签 | 144 → 146(更糟) | 109 → 135(更糟) |
为最满的容器单独设置 direction | 146 → 113(有效) | — |
--tala-seeds 4,5,6 | 113 → 100 | 尝试 12 组种子,均无变化 |
在一个形状上设置 top / left | 113 → 74 | 102 → 75 |
因此:先设置根节点的 direction,再设置包含最多方框的容器自身的 direction——这是首先要尝试的办法,而不是最后才试——并在每次调整后重新测量。缩短标签反而很可能让图变宽,因为 TALA 会重新排布整张图。--scale 完全不会改变文本渲染。以上都不适用于 sequence_diagram,因为它会自行排版:在那里,唯一可调整的是标签长度,宽度只会朝任一方向变化一两列。
关于这些权宜办法,有几点需要注意:
- 容器级的
direction往往会被忽略。 在两个容器上设置它,生成的渲染结果逐字节完全相同;在最满的容器上设置,则让宽度减少了 33 列。TALA 似乎只会在容器内部的流向不明确时采用该设置。 top/left的单位是 SVG 像素,不是列数,而且必须同时设置(二者仅适用于 TALA)。对于 100 列宽的图,大约每列对应 11.5px,但每位使用者都是尝试了三到五个值才找到合适的数字。只固定一两个位置重要的形状,注明原因,并预期下次更改标签后需要重新调整固定值。- 固定位置可能会截断终端渲染结果,而这看起来像是宽度达标了。 一位作者固定了一个表格,看到宽度从 113 降到 71,但这个数字只是残缺结果:固定位置把形状推到了 ASCII 画布之外,文本中五个表格有三个消失了,而 SVG 却变大了。
d2会以 0 退出。现在d2-facts会将此情况报告为错误;如果设置了固定位置,请重新检查渲染结果。 - 不设置固定位置,可能无法满足预算。 对于五个
sql_table,没有任何有文档说明的调整能将宽度降到 100 列以下:direction: down将宽度降至 113,缩短标签降至 127,使用换行容器降至 121,只有不安全的固定位置能进一步缩小。遇到这种情况时,可按容器将图拆分到两个文件中,或者保留较宽的图,并在说明文字中注明终端版本宽 113 列——不要用未经重新检查的固定位置来换取达标数字。
阅读终端渲染结果
在 0.8.1-HEAD 上测得。此处查看结构;查看 PNG 中的标签和形状。
- 边标签会写在线条上,其中的空格会变成线条字符(
vlmkit check integrity─page─html、201─Created)。终端显示的边标签控制在一两个词以内。 - 所有连接样式都会被移除。
stroke-dash、stroke、bold都会渲染成普通线条:一位作者将带样式的渲染结果与无样式副本进行差异比较,发现两者完全相同。虚线表示异步的区别无法保留,因此要在标签中体现。 - 长标签上的箭头不可靠。 标签长到填满通道时,箭头可能完全没有箭头;标签中的
<<也会被直接吞掉。被吞掉的字符并不固定:同一个文件一次渲染时标签的括号消失,下一次渲染时句点又消失了(order.placed→order─placed)。检查最终拼写的渲染效果,并优先使用一个不带标点的短词。 shape: text和|md …|块会渲染为空框。 标题、图例或段落应放在渲染图周围的正文中,或者放在仅渲染为 SVG 的文件变体中。sql_table行会渲染,但约束标记不会。 PK / FK 标记仅在 SVG / PNG 中可见;列级边在文本视图中经常会连到错误的行,因此无法据此阅读。- 边标签可能落在方框或容器边框上(
┌────sync call────┐│),看起来像两个方框合并在一起。标签落在哪里取决于位置,而非长度:一位作者的连接原本就在根层,他缩短并加长标签后,标签仍位于同一边框行、相同宽度处。能够改变位置的是会重新排列图形的因素——容器的direction、不同的 seed——因此应将其视为需要在 PNG 上检查的布局结果,而不是需要缩短的标签。 - CJK 标签会在每个字形之间补空格(
ウ ェ ブ),导致容纳它们的方框错位。ID 始终使用 ASCII;如果图要在终端中阅读,标签也使用 ASCII,并将日文放在周围的正文中。SVG 和 PNG 能正确渲染日文。 standard模式只使用+ - | / < > v,布局保持不变——适用于在 GitHub 上阅读 README,因为代码块中的框线字符取决于查看者的字体。- 图标和图片会被移除;时序图、
sql_table和class形状及其行会被渲染。
作者遵循的规则
- ID 使用 ASCII 且保持稳定(用
api,不要用API server);标签写在冒号后面。ID 可以缩写——d2-facts也会匹配标签——但下一位编辑者会先看 ID。 - 跨容器连接写在根层,或使用
_.,而_表示上一级:在cluster内部,根层是_,因此指向同级容器中的形状时写作_.outside.stripe。绝不要保留超出作用域的名称:它会变成一个新方框(见上文)。 - 根层只设置一个
direction;需要以相反方向排列的容器应设置自己的方向(仅限 TALA,而且不一定总会生效)。 - 将连接写成句子:
a -> b: verb。--表示无向连接,<->表示双向连接。连接上的样式({style.stroke-dash: 3})在终端中不可见——如果此处需要体现区别,就将其写入标签。 - 每个文件顶部都设置
vars: { d2-config: { layout-engine: tala } },这样即使渲染时未带标志,SVG 仍会使用 TALA。 - 除非必须固定形状的位置,否则不要设置坐标;如果必须设置,就只对该形状同时设置
top和left,并附上说明原因的注释。
交付
- 在聊天中:在围栏代码块中提供终端渲染图(它本身就是终端中的图片)、正文中提到的 ID,以及作为文件提供的
.d2和 PNG / SVG(如果可用,请使用SendUserFile)。 - 在 PR 或 issue 中:在围栏代码块中提供
standard渲染图,或提交并链接 SVG。差异中的.d2是审查目标。 - 在文档中:将
.d2提交到页面旁,并由构建生成 SVG(在脚本中使用d2 --layout=tala docs/arch.d2 docs/arch.svg);如果网站没有构建步骤,则提交 SVG。
故障模式
d2: command not found→ 安装(见上文);不要改用其他工具绘制。d2 layout未列出tala→ 版本太旧;请升级。安装程序的--tala标记已不存在,使用时会被拒绝。- 一个框出现两次,或者你写的箭头不见了 → 范围外的引用创建了一个幽灵对象。
d2-facts会指出它;修复方法是使用完整路径或_.,而不是再加一条箭头。 - 文本渲染结果呈 dagre 风格且很宽,但文件写着
layout-engine: tala→ 某些构建版本中的.txt路径会忽略文件内配置;请传入--layout=tala。 "tala-seeds" needs a value/is not a valid config→ seeds 只是一个标记。connection missing destination、maps must be terminated with }→ 会标出行和列;通常是末尾多了->,或有未闭合的花括号。invalid style keyword→ D2 使用color、stroke、stroke-dash、fill、font-size、bold、opacity、shadow、3d、multiple。- 每次调整后框的位置都变了,尽管你只改了一个标签 → TALA 重新排布了布局。固定少数位置重要的形状,或者接受这种变化并停止描述位置。
- 两个框看起来像是碰在一起,无法判断 →
d2-facts会报告以 px² 为单位的同级元素重叠;D2 本身没有碰撞报告。
assets/vlmkit-workspace.d2 是 vlmkit 工作区(mizchi/vlmkit)的 D2 文件,而
assets/vlmkit-workspace.txt 是它的 TALA 终端渲染结果——一次干净的运行应呈现的样子。生成上述数字的验证运行位于
tests/d2/fixtures/d2-scenario/ 和
docs/reports/2026-09-14-d2-diagram-v1.md。