Introduction
桑基图只回答一类问题:一个量在流转,我想知道它从哪来、到哪去、路上漏了多少。
判断标准不是"数据有没有层级"——那是树图和 treemap 的活。判断标准是同一批东西在不同阶段之间移动。钱从收入走到各项开支、病人从筛选走到最终分析、能量从输入端走到各处损耗,这三件事是同一种结构。
具体到三条:
- 数值可加。 两条流并作一条就等于把两个数加起来,这件事得说得通。相关系数、pH 值、五星评分加起来没有意义,也就画不了桑基图。
- 至少两个阶段。 只有一层拆分(总量 → 若干类)是杀鸡用牛刀,堆叠条形图更清楚也更省版面。
- 关心的是分配,不是排名。 想知道"谁最大"就画条形图;想知道"这一块是怎么散出去的"才画桑基。
不该用的场合同样明确。想精确比较两个数量:带子的宽度没有共同基线,两条带子分处画面上下两端时,差个百分之几根本看不出来,条形图在这件事上永远更好。数据里有环:A → B → A 这种结构常规桑基图排不出层级,SankeyMATIC 也不支持——真需要表达循环,得找支持 circular Sankey 的实现,或者干脆换网络图。
读的时候按这个顺序:
- 先看清起点、终点和总量。 从哪儿进来、能到哪几个地方去、一共多少。这一步定住了后面所有比例的分母。
- 再找最粗的那条带。 它是主路径,通常解释了大部分总量,其余都是它的旁支。
- 在分叉点用宽度看大致比例,具体数看标签。 宽度告诉你"大概一半对一半"还是"九比一",这是桑基图真正擅长的;两条带子宽度接近时眼睛分不出 48% 和 52%,那就必须去看数字。
- 找中途停下的节点。 不再往右延伸的那些就是"漏掉的"——流失、损耗、排除。它们往往是这张图最该被看见的部分:在表格里,它们不过是几行不起眼的小数字。
- 别拿垂直位置当描述。 节点的上下顺序是布局算法排的,换个工具、甚至同一个工具改个参数,位置就全变了。
Example Data
桑基图要的输入是:一张边表——每行三列:从哪、到哪、多少。不需要节点表,节点是从边表里的名字推出来的;也不需要预先算层级,深度由连接关系决定。
SankeyMATIC 把边表、节点色和样式装在同一份纯文本里,三段依次是:
| 段 | 长什么样 | 管什么 |
|---|---|---|
| 流 | 源 [数值] 目标 |
数据。一行一条流,这是唯一经常改的部分 |
| 节点 | :节点名 #RRGGBB |
给单个节点上色。不写就走 node theme 的自动配色 |
| 样式 | // === Settings === 之后的几十行 |
画布、节点宽度间距、连线透明度、字体、数值格式 |
三段里只有第一段是每张图独有的。样式段可以整块在图之间复制:下面两张图的样式段只差三个数——画布高度、节点间距、字号。想换风格就在网页上把滑块调到满意,点 Save my work,存下来的文件里是一份完整的样式段。这是把手调出来的样式变成一段可提交文本的唯一办法,也是这套流程能复现的根本:滑块的位置留不住,文本留得住。
// 开头的整行都是注释,所以下面两块源码第一行的 // fig: <名字> 只是这一页拿来给图命名用的,SankeyMATIC 会直接忽略——整块复制走就行,不用先删掉那一行。标签太长可以用 \n 断行,配合样式段里的 labels linespacing 控制行距。
两份边表都收录为 Tessera toy:apple_revenue_flow 是从 Apple 2025 财年第三季度 10-Q 整理的 adapted 数据(15 条流);trial_participant_flow 是模拟随机对照试验队列的 synthetic 数据(1200 人筛选,21 条流)。SankeyMATIC 仍直接使用下面的纯文本,toy 页面负责记录结构、来源和分类。
Palettes
用 walter_white2。五个色低饱和、彼此分得开,这正是桑基图需要的——色块面积极大,鲜艳色铺满半张图会盖过数据本身。
关键原则是:颜色该说明"这条路径是谁",不是"这是第几个节点"。 默认的分类配色给每个节点分一个不同的颜色,看着热闹,可从颜色里读不出任何信息;那种情况下颜色只是噪音,不如全图统一成一个色。两张图各有一套说法:
收支图——冷色是还留在公司里的钱,暖色是花出去的,绿色是最后剩下的。于是从左看到右,颜色本身就在讲一件事:一条 #5AB5BF 的青色主干(收入、毛利、营业利润、税前利润)在每个环节被 #BF8888(成本、税)和 #F29E6D(研发、销管)削掉一块,走到头剩下 #808C56 的净利润。
队列图——#D1BE9E 是尚未分组的主干,#5AB5BF 干预臂、#808C56 对照臂,#BF8888 是任何形式的流失(未接受、失访、中止、排除分析)。两条臂的颜色一路保持到各自的分析终点,于是"哪些人掉了"是一个统一的视觉信号,而"这些人属于哪个组"从头到尾没断过。
带子的颜色不单独写,它从两端里挑一头继承,而挑错了图会自己跟自己打架。 SankeyMATIC 的默认值是 flow inheritfrom outside-in:靠左的带子取来源的颜色,靠右的取去向的颜色——而"靠左""靠右"是按水平位置算的。于是收支图上就会出现这一幕:净销售额 → 销售成本 位置偏左,带子取了来源的青色,可它的终点 销售成本 是粉的;而 毛利 → 研发费用 位置偏右,带子取了去向的橙色。同样是花出去的钱,两条流一条青一条橙,青的那条看上去甚至还像是留下来的。
两张图都改成 flow inheritfrom target:每条带子一律取去向的颜色。因为这两张图问的其实是同一个问题——这笔钱去了哪、这些人最后到了哪。一条规则管全图:主干的下一站还是主干,所以主干的颜色不断;分出去的成本、费用、税、流失,自动变成各自终点的颜色。
节点色一律写成 :名字 #色值。流多到几十条、想拿脚本从 Excel 生成源码时,配色千万别按行号对齐:一列颜色对着一列流,中间插进一条新流,整套颜色就往下错一格;而当颜色条数和流的条数恰好相等时,脚本连警告都不会给。按节点名关联就没有这个问题。
Recipe
| No. | Method | Input Data | Palettes |
|---|---|---|---|
| 1 | SankeyMATIC |
边表 · Apple 10-Q | walter_white2 |
| 2 | SankeyMATIC |
边表 · 模拟 RCT | walter_white2 |
1 · 收支拆解
Apple 2025 财年第三季度。这是桑基图最经典的形态:总量守恒,先汇聚再逐级分叉。左边四条产品线加上服务,汇成净销售额,然后在四个环节被一层层削掉,剩下净利润。
注意它不是单纯的"从左往右分叉"——左半是收敛(产品线 → 产品 → 净销售额),右半才是发散。桑基图能同时表达这两种方向,这是它比堆叠条形图强的地方。
// fig: revenue
// Apple 2025 财年第三季度(截至 2025-06-28),单位:百万美元
// 数据来源:Apple Inc. Form 10-Q
// ── ① 产品线汇聚成净销售额 ────────────────────────────
iPhone [44582] Products
Mac [8046] Products
iPad [6581] Products
Wearables & accessories [7404] Products
Products [66613] Net sales
Services [27423] Net sales
// ── ② 成本与毛利 ──────────────────────────────────
Net sales [50318] Cost of sales
Net sales [43718] Gross margin
// ── ③ 运营费用 ────────────────────────────────────
Gross margin [8866] R&D
Gross margin [6650] SG&A
Gross margin [28202] Operating income
// ── ④ 税前 → 净利 ─────────────────────────────────
Operating income [171] Other expense
Operating income [28031] Pretax income
Pretax income [4597] Income taxes
Pretax income [23434] Net income
// ── 节点配色 · walter_white2 ──────────────────────
// 冷色 = 钱还在体内,暖色 = 流出去了,绿色 = 最终留下的
:iPhone #5AB5BF
:Mac #5AB5BF
:iPad #5AB5BF
:Wearables & accessories #5AB5BF
:Services #5AB5BF
:Products #5AB5BF
:Net sales #5AB5BF
:Gross margin #5AB5BF
:Operating income #5AB5BF
:Pretax income #5AB5BF
:Net income #808C56
:Cost of sales #BF8888
:Income taxes #BF8888
:Other expense #BF8888
:R&D #F29E6D
:SG&A #F29E6D
// === Settings ===
size w 1200
h 700
margin l 14
r 80
t 18
b 20
bg color #FFFFFF
transparent N
node w 14
h 50
spacing 70
border 0
theme a
color #888888
opacity 1
flow curvature 0.5
inheritfrom target
color #999999
opacity 0.55
layout order automatic
justifyorigins Y
justifyends N
reversegraph N
attachincompletesto nearest
labels color #000000
hide N
highlight 0.5
fontface sans-serif
linespacing 0.2
relativesize 110
magnify 100
labelname appears Y
size 16
weight 400
labelvalue appears Y
fullprecision Y
position below
weight 400
labelposition autoalign 0
scheme auto
first before
breakpoint 4
value format ',.'
prefix '$'
suffix ''
themeoffset a 6
b 0
c 0
d 0
meta mentionsankeymatic Y
listimbalances Y
