关于 svg-diagram
svg-diagram 是一套生成 SVG 图表的约定,面向需要在文档中制作清晰、可维护示意图的用户。它规定布局、间距、连接线、字体和检查方式。
主要功能
- 01规定 SVG 文件存放与引用方式
- 02提供布局和间距标准
- 03定义连接线与箭头规则
- 04说明文字宽度与标签放置
- 05要求声明支持 CJK 的字体
适用场景
- 01为 Markdown 文档制作流程图
- 02绘制系统架构图
- 03检查 SVG 图表布局和可读性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ bybit-exchange/ svg-diagram --skill svg-diagram安装后在 Claude Code 中输入/svg-diagram确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignoreLICENSEpackage.jsonREADME.mdtools/svg-lint/README.mdtools/svg-lint/bin/svg-lint.mjstools/svg-lint/lib/color.mjsformat-text.mjsregistry.mjspalette.mjslint.mjsformat-json.mjspanels.mjsgeometry.mjsparse-svg.mjsreport.mjstext-metrics.mjsdocument.mjstools/svg-lint/lib/checks/baseline-offset.mjsbox-height.mjsblock-spacing.mjsarrow-marker.mjsconnector-geometry.mjsxml-escaping.mjsviewbox-clipping.mjsfont-stack.mjstext-overflow.mjslight-bg-fallback.mjspalette-conformance.mjsoverlap.mjs详细介绍
SVG 绘图约定
输出位置与引用
图表应使用 SVG,绝不使用 ASCII 艺术图。这条规则决定了是否绘图;本规范介绍如何绘图,首先说明文件的存放位置。
- 将文件放在与文档同级的
assets/目录中(例如docs/foo.md→docs/assets/foo-arch.svg) - 在 Markdown 中使用相对路径引用:
。不要内嵌 SVG XML。 - 文档中的数据发生变化时,也必须更新对应的 SVG。与旁边正文相矛盾的图表,还不如没有图表
核心原则
首次绘制就做到位
首次生成的 SVG 就必须满足本文末尾验证清单中的每一项。不要依赖用户反馈来修复间距、内边距、对齐、viewBox 裁切等基本布局问题。生成后,请自行逐项检查清单,并在展示结果前修复发现的问题。
元素不得重叠
- 文字不得覆盖方框或线条
- 线条不得穿过方框(以箭头终止于该方框的情况除外)
- 文字与线条之间保持 10–20px 的间距
- 同级/相邻方框之间的间距请参阅下文的“区块间距”部分(统一标准:≥25px,建议为 25–30px)
- 颜色图例应放在图表主体之外(底部或侧边),不得与内容区域重叠
<path d="M35,200 C 35,120 ..." .../>
<text x="55" y="200">Label text</text>
<text x="35" y="200">Label text</text>
视觉平衡
左右两部分应具有相近的视觉分量;可添加或移除注释框,使两侧保持均衡。
布局规则
居中
content width = right edge of rightmost element - left edge of leftmost element
left margin = (viewBox width - content width) / 2
标题应相对于内容中心居中,而非 viewBox 中心(只要内容不对称,两者就会不同):
<text x="[content center x]" y="[top_padding + font_ascent]" text-anchor="middle">Title</text>
只有内容左右对称时,content center x = viewBox width / 2 才成立。内容不对称时,使用 <g transform="translate(...)"> 将整个分组居中,并相应地将标题的 x 更新为新的内容中心。
事后整体平移
如果 SVG 完成后才发现左右(或上下)内边距不对称,无需修改每个坐标。将内容包在一个 <g transform="translate(dx, dy)"> 中即可:
<svg viewBox="0 0 800 460" ...>
<defs>...</defs>
<g transform="translate(22, 0)">
</g>
</svg>
计算偏移量:dx = (right padding - left padding) / 2,其中内边距是从 viewBox 边缘到最近内容元素的距离。
dx > 0:向右平移内容(适用于内容过于靠左的情况)dx < 0:向左平移内容(适用于内容过于靠右的情况)
适用情况:整体内容偏离中心,且你不想逐个修改每个 x 坐标。缺点:文件中的坐标不再等于渲染后的坐标,因此后续修改时需要在脑中减去偏移量——如果接下来要大幅调整布局,请将偏移量合并到各个坐标中,并移除 <g>。
viewBox 边距
标题属于内容的一部分,因此上下边距应从标题开始测量:
title y = margin + font ascent (≈ font-size × 0.75)
viewBox height = bottom y of content + margin
建议边距:20–25px
<svg viewBox="0 0 680 295" ...>
<text x="340" y="32" font-size="16">Title</text>
...
</svg>
方框布局
- 同一行中的方框尺寸差异 ≤60px;文字居中(
text-anchor="middle") - 同一列中的方框共用中心线
- 外部方框应完全包住内部方框:
outer size = content size + padding × 2
虚线分组框(容器)
用于将相关框分组在一起(例如“服务端”“客户端”“员工实例”)。
样式一致性:在同一张图中,每个虚线分组框都必须具有以下一致属性:
stroke-dasharray(推荐6,4)rx(圆角半径,推荐 8–12)fill(推荐#f8fafc或none)- 内边距(推荐 15–20px)
- 标题字号(推荐 11px,以区别于 12px 的框内正文)
标题位置:将分组标题放在框的左上角内侧(x = box x + 10, y = box y + 14),并使用次要文本颜色 #64748b。
垂直居中的陷阱:应根据标题 + 内部内容的总高度计算分组框的垂直位置,而不是只根据框本身计算。添加标题后,框顶部大约有 20–25px 的空间被占用,内部框应从其下方开始:
<rect x="50" y="100" width="200" height="120" rx="10"
stroke="#94a3b8" stroke-dasharray="6,4" fill="#f8fafc"/>
<text x="60" y="114" font-size="11" fill="#64748b">Server</text>
<rect x="65" y="125" width="170" height="36" rx="6" .../>
<rect x="65" y="170" width="170" height="36" rx="6" .../>
指向分组的连接线:当连接线在逻辑上指向一组元素而非其中某一个元素时,应让连接线终止于分组框边界,而不是某个单独成员。也可以将这些元素放入分组框,并让箭头指向该框。
框尺寸
根据字号确定框高,以确保有足够的内边距:
| 内容 | 高度公式 | 示例(12px 字号) |
|---|---|---|
| 单行 | 字号 × 3 | 36px |
| 两行 | 字号 × 3 + 行高 | 36 + 18 = 54px |
| 多行 | 字号 × 3 + (行数 - 1) × 行高 | 每增加一行 +18px |
公式来源:font-size × 3 = 上内边距(≈字号)+ 字形高度(≈字号)+ 下内边距(≈字号)。非标准字号也按相同比例缩放。
建议行高:字号 × 1.5(例如,12px 字号对应 18px)
在框内垂直居中文本(基线定位)
在 SVG 中,<text y=...> 指的是基线,而非字形顶部。要使文本在框内视觉居中:
text y = box center y + font-size × 0.35
= box y + box height/2 + font-size × 0.35
0.35 系数是“字形视觉中心到基线”的经验值(约为字号的三分之一)。
区分这两个系数:
font-size × 0.75(上行高度):用于从顶部定位的元素,例如标题(y = top_padding + ascent)font-size × 0.35(基线偏移):用于在框内垂直居中
<rect x="30" y="50" width="110" height="36" rx="6" .../>
<text x="85" y="72" ...>Single line</text>
<rect x="30" y="50" width="110" height="54" rx="6" .../>
<text x="85" y="70" ...>First line</text>
<text x="85" y="88" ...>Second line</text>
显示尺寸
<svg viewBox="0 0 750 400" width="750" xmlns="http://www.w3.org/2000/svg">
| 图表类型 | 宽度 |
|---|---|
| 简单列表 | 400px |
| 流程图 | 600–700px |
| 架构图 | 700–800px |
连接线规则
选择连接线
同轴连接使用直线 L;路径转弯或绕行时使用平滑的 C/Q 曲线。切勿使用直角折线。
<path d="M190,77 L 320,77" .../>
<path d="M130,145 L 130,213" .../>
<path d="M100,100 L100,200 L200,200" .../>
<path d="M100,100 C 100,150 150,200 200,200" .../>
| 情况 | 推荐 | 语法 |
|---|---|---|
| 同轴(水平/垂直) | L | L endx,endy |
| 简单转弯 | Q | Q ctrlx,ctrly endx,endy |
| S 形曲线 / 复杂路径 | C | C ctrl1x,ctrl1y ctrl2x,ctrl2y endx,endy |
箭头规则
- 方向由路径的最后一段决定:向右表示 x 增大,向下表示 y 增大
- 对称间距:箭头与源框之间为 5px,与目标框之间也保持相同的视觉间距
- 标签居中:
x = (source right edge + target left edge) / 2,并使用text-anchor="middle"
修正曲线路径上的箭头方向:orient="auto" 会使箭头与路径末端的切线对齐。C/Q 曲线的末端切线可能会倾斜,导致箭头看起来歪斜。解决方法:放置第二个控制点(cp2),使 cp2 → 终点的方向恰好与箭头所需的朝向一致。
<path d="M100,100 C 100,200 250,200 300,250" marker-end="url(#arrow)"/>
<path d="M100,100 C 100,200 300,220 300,250" marker-end="url(#arrow)"/>
<path d="M100,100 C 100,200 270,250 300,250" marker-end="url(#arrow)"/>
控制箭头方向的规则:
| 所需方向 | cp2 约束 | 示例 |
|---|---|---|
| ↓ 向下 | cp2.x = end.x, cp2.y < end.y | C ...,... ex,ey-20 ex,ey |
| → 向右 | cp2.y = end.y, cp2.x < end.x | C ...,... ex-20,ey ex,ey |
| ← 向左 | cp2.y = end.y, cp2.x > end.x | C ...,... ex+20,ey ex,ey |
| ↑ 向上 | cp2.x = end.x, cp2.y > end.y | C ...,... ex,ey+20 ex,ey |
计算路径端点(间距为 5px,箭头超出线条末端 6px):
核心规则:起点距离源框 5px,终点距离目标框 11px(5px 间距 + 6px 箭头延伸长度)。
由于我们使用 refX="2"(箭头尾部的凹口),箭头尖端会超出线条末端 6px(8-2=6),且线条与箭头在凹口中心处连接,因此连接处看起来连续。
四个方向的公式:
| 方向 | 起点 | 终点 |
|---|---|---|
| → 向右 | 源框右边缘 + 5 | 目标框左边缘 - 11 |
| ← 向左 | 源框左边缘 - 5 | 目标框右边缘 + 11 |
| ↓ 向下 | 源框底边缘 + 5 | 目标框顶边缘 - 11 |
| ↑ 向上 | 源框顶边缘 - 5 | 目标框底边缘 + 11 |
<path d="M185,70 L 209,70" marker-end="url(#arrow)"/>
<path d="M215,70 L 191,70" marker-end="url(#arrow)"/>
<path d="M130,145 L 130,159" marker-end="url(#arrow)"/>
<path d="M130,165 L 130,151" marker-end="url(#arrow)"/>
双向示例(两个框之间有向上和向下的箭头):
<path d="M320,380 L 320,409" marker-end="url(#arrow)"/>
<path d="M480,415 L 480,386" marker-end="url(#arrow)"/>
绕开障碍物布线
绕行路径与障碍物边界之间至少保持 20px 的距离:
<path d="M600,313 Q 650,313 650,400 Q 650,640 635,685"
fill="none" stroke="#a855f7" stroke-dasharray="6,4" marker-end="url(#arrow)"/>
块间距(全局统一标准)
相邻框/块之间的间距应为 ≥25px,建议 25–30px。
细分:起点间距 5px + 终点间距和箭头延伸长度 11px + 可见线条长度 ≥6px + 安全余量 ≈ 25px。可见线条至少为 6px,否则箭头会退化成一个点。
保持紧凑:不要超过 30px。间距过大(>40px)会使整个图示显得松散,连接线也会过长。两端分别保持 5px / 11px,中间的可见线条长度保持在 6–12px。
整体密度:完成后退一步检查整体密度——如果内容区域相对于 viewBox 明显偏小(留有大片空白区域),请缩小间距或 viewBox。常见原因:框间距 >30px、viewBox 边距 >25px、分组框内边距过大。
这是本文档唯一的块间距标准;“不得出现元素重叠”和验证清单均参照此标准。
SVG 绘制顺序(z-order)
SVG 没有 z-index——后绘制的元素会显示在上层。按以下顺序排列:
<rect ... stroke-dasharray="6,4" fill="#f8fafc"/>
<path d="..." marker-end="url(#arrow)"/>
<rect ... fill="#dbeafe"/>
<text ...>Label</text>
常见错误:先绘制连接线,再绘制虚线背景框 → 连接线会被遮住。
例外:跨层回环线:必须跨越多个框的连接线——例如迭代循环、跨区域虚线——必须在框之后绘制(第 4 层),否则会被框遮住:
<path d="..." stroke-dasharray="6,4" marker-end="url(#arrow-purple)"/>
估算文本宽度(防止溢出)
放置文本前,估算其右边缘,并确认不会侵入相邻元素:
| 字体大小 | 拉丁字符宽度(约) | CJK 字符宽度(约) |
|---|---|---|
| 8px | 4.5px | 8px |
| 9px | 5.0px | 9px |
| 10px | 5.5px | 10px |
| 11px | 6.0px | 11px |
| 12px | 7.0px | 12px |
估算公式:
text-anchor="start":右边缘 ≈ x + 字符数 × 字符宽度text-anchor="middle":左/右边缘 ≈ x ± (字符数 × 字符宽度) / 2text-anchor="end":左边缘 ≈ x - 字符数 × 字符宽度
放置前检查:文本右边缘 + 10px < 其右侧相邻元素的左边缘。
<text x="274" y="115" font-size="9">4K tokens (= mini-batch)</text>
<text x="154" y="138" font-size="9" text-anchor="middle">4K tokens (= mini-batch)</text>
在曲线上放置标签
描述弧线或曲线的标签不能放在线条本身上(线条会穿过文本)——应放在曲线的凸侧或凹侧:
<text x="350" y="218">Skip Connection</text>
<path d="M82,165 C 82,225 620,225 646,225" .../>
<text x="350" y="222">Skip Connection</text>
<path d="M82,149 C 82,260 600,255 640,255" .../>
规则:标签与曲线上最近点之间至少保持 15px。对于向下弯曲的曲线,将标签放在曲线上方;对于向上弯曲的曲线,将标签放在下方。
坐标管理
在注释中记录边界
<rect x="280" y="55" width="340" height="435" .../>
移动元素时,以下内容必须同步更新
- viewBox 尺寸
- 所有指向该元素或从该元素出发的箭头(起点、终点、控制点)
- 相关标签的位置
- 包围该元素的虚线背景/分组框(泳道、容器)——这是最容易遗漏的一项
- 同一行/列中与其对齐的其他框
- 其他所有受影响的元素
样式规则
字体(必需)
每个 SVG 都必须在 <style> 中声明支持 CJK 的字体栈——这是硬性要求:
<style>
text { font-family: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', system-ui, sans-serif; }
</style>
回退顺序:macOS(PingFang SC)→ Windows(Microsoft YaHei)→ Linux(Noto Sans CJK SC)→ 系统默认字体。必须保留 Noto Sans CJK SC,否则 Linux 上的服务器端渲染会回退到不支持 CJK 的字体。
| 元素 | 大小 |
|---|---|
| 标题 | 16px |
| 框内正文 | 12px |
| 次要文本/注释 | 10px |
竖排文本:<text writing-mode="tb">vertical text</text>
颜色
基础颜色:主要文本 #1e293b,次要文本 #64748b,弱化文本/箭头 #94a3b8
语义颜色:
| 含义 | 填充 | 描边 | 文本 |
|---|---|---|---|
| 输入/主要 | #dbeafe | #3b82f6 | #1e40af |
| 处理中/进行中 | #fef3c7 | #f59e0b | #b45309 |
| 数据/输出 | #d1fae5 | #22c55e | #166534 |
| AI/分析 | #f3e8ff | #a855f7 | #6b21a8 |
| 敏感/警告 | #fce7f3 | #ec4899 | #9d174d |
箭头定义
使用带凹口的箭头,而不是普通三角形——视觉效果更好:
<defs>
<marker id="arrow" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#64748b"/>
</marker>
<marker id="arrow-blue" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#3b82f6"/>
</marker>
<marker id="arrow-orange" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#f59e0b"/>
</marker>
<marker id="arrow-green" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#22c55e"/>
</marker>
<marker id="arrow-purple" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#a855f7"/>
</marker>
<marker id="arrow-red" markerWidth="8" markerHeight="8" refX="2" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L8,4 L0,8 L2,4 z" fill="#ef4444"/>
</marker>
</defs>
箭头颜色参考:
| ID | 颜色 | 用途 |
|---|---|---|
arrow | #64748b | 默认 / 中性连接 |
arrow-blue | #3b82f6 | 输入 / 主要流程 |
arrow-orange | #f59e0b | 处理 / 进行中 |
arrow-green | #22c55e | 数据 / 输出 / 成功 |
arrow-purple | #a855f7 | AI / 分析 / 特殊 |
arrow-red | #ef4444 | 警告 / 危险操作 |
关键参数:
orient="auto"会旋转箭头,使其跟随路径方向- 必须显式声明
markerUnits="userSpaceOnUse",否则默认的strokeWidth会根据线条粗细缩放箭头(当 stroke-width=2 时,箭头会放大一倍,超出预期的间距) refX="2"会将线条末端与箭头尾部凹口的中心对齐,使连接处看起来更自然refY="4"会使箭头垂直居中(箭头高度为 8,中点为 4)- 箭头路径
M0,0 L8,4 L0,8 L2,4 z会形成带凹口的形状,尖端位于 x=8 - 尖端会超出线条末端 6px(8-2=6),因此终点按
target edge - 11计算
粗线上的箭头:当 stroke-width > 1.5(例如粗虚线)时,8×8 的箭头看起来太小。请定义按比例放大的标记:
<marker id="arrow-red-lg" markerWidth="12" markerHeight="12" refX="3" refY="6"
orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L12,6 L0,12 L3,6 z" fill="#ef4444"/>
</marker>
| 线条 stroke-width | 标记大小 | refX | 尖端延伸距离 | 终点公式 |
|---|---|---|---|---|
| ≤ 1.5 | 8×8 | 2 | 6px | target edge - 11 |
| 1.5 ~ 2.5 | 12×12 | 3 | 9px | target edge - 14 |
| > 2.5 | 16×16 | 4 | 12px | target edge - 17 |
XML 特殊字符(高风险,务必检查)
SVG 是 XML,因此文本中的特殊字符必须转义,否则整个 SVG 都无法渲染:
| 字符 | 转义形式 | 示例 |
|---|---|---|
& | & | Validate & Sanitize |
< | < | x < 10 |
> | > | x > 0 |
" | " | 属性值中 |
' | ' | 属性值中 |
<text>Load & Save</text>
<text>Load & Save</text>
以编程方式生成 SVG 时进行转义
使用 Python/JS 生成 SVG 时,来自外部来源(CSV、数据库、API)的文本必须先转义,然后才能拼接到 SVG 中:
def svg_escape(text):
return str(text).replace('&','&').replace('<','<').replace('>','>').replace('"','"')
# ❌ Splicing external data directly ("Research&Development" → XML parse failure, image won't render)
svg += f'<text>{team_name}</text>'
# ✅ Escape first
svg += f'<text>{svg_escape(team_name)}</text>'
高风险数据来源:团队名称(包含 &)、需求描述(包含 <>)、用户输入、文件名
故障排查
| 问题 | 检查内容 |
|---|---|
| 元素重叠 | 文本与线条的间距是否 ≥10px;绕行路径是否穿过边界? |
| 箭头不可见 | 块间距是否小于 25px——请增大间距 |
| 标签未居中 | 确认 text-anchor="middle",且 x = 间隙中心 |
| 箭头方向错误 | 检查最后一段路径的方向(x/y 是递增还是递减) |
| XML 解析错误 | 检查文本中的 & < > 是否已转义 |
| 底部内容被裁切 | viewBox 高度是否过小;其高度必须等于最低元素的底边位置 + 25px |
| 添加标题后虚线分组框未居中 | 垂直居中必须按“标题 + 内容”的总高度计算——参见“虚线分组框” |
| 整体留白过多 | 检查框间距是否 >30px,或 viewBox 边距是否 >25px |
验证清单
- 没有元素重叠(文本、框、线条)
- 文本右边缘没有侵入相邻元素(根据字符宽度估算)
- 曲线标签与曲线上最近点之间的距离 ≥15px
- 两端的箭头留白对称(5px),可见线段 ≥6px
- 标记声明
markerUnits="userSpaceOnUse" - 粗线(stroke-width > 1.5)使用放大的标记
- 连接线使用 C/Q 曲线,不使用直角
- 标签居中(
text-anchor="middle") - 绕行路径避开障碍物
- 区块间距 ≥25px(建议 25–30px,参见“区块间距”)
- 标题居中,各框排列成行且大小相近
- 存在
width属性,viewBox 与内容相符 - 框的高度足够(一行文字 ≥ 字号 × 3)
- viewBox 的顶部和底部边距相当(20–25px,从标题顶部测量)
- viewBox 左右内边距对称(如果不对称,使用
<g transform="translate(dx,0)">平移) - 特殊字符已转义(
&→&,<→<) - 每个元素都位于可见的 viewBox 内(底部 = 最低元素的底边 + 25px)
- 每条连接线都有明确含义(带有标签,或可根据上下文推断其起点和终点)
- 虚线分组框的样式保持一致(dasharray、圆角半径、填充、内边距、标题字号)
- 没有大块空白区域(间距控制在 30px 以内,viewBox 紧贴内容)
编辑工作流
1. 收集:动手修改前先收集全部需求
收到修改 SVG 的请求时,不要立即开始编辑。先判断:
- 用户只提出了一项修改 → 回复“还有其他需要调整的地方吗?”,以便一次性收集完整需求
- 用户提出了多项修改 → 进入执行阶段,一次性全部应用
目标:减少往返次数,避免每次小调整都重新输出整个 SVG。
2. 执行:一次性应用所有修改,并附上修改清单
完成所有修改后,先打印修改清单,再展示最终 SVG:
Change list:
1. Title spacing 20px → 30px
2. Box A fill #dbeafe → #f3e8ff
3. Connector L1 path adjusted (routes around the new box)
不要在每项修改后展示图表——用户只需要最终结果。
3. 调整布局:理解意图并同步调整相关元素
当用户说“将框 A 向右移动 20px”时,不要机械地只修改框 A 的 x。还要处理相关元素:
- 所有连接到框 A 或从框 A 出发的箭头路径
- 框 A 所在行与其他框的对齐
- 框 A 的文本标签位置
- 包含框 A 的外部/背景框大小
间距/对齐请求(“缩紧一点”“左对齐”“居中”)→ 理解整体意图,并一次性调整所有相关元素,避免用户逐像素反复迭代。