如果你已经能把 ECharts 折线图、柱状图和饼图摆得明明白白,下一步最容易卡住的地方往往不是 API,而是不知道该在什么场景用哪种图。桑基图就是数据可视化里很典型的一种“一眼看懂流向”的图表:从哪儿来、经过哪一步、在哪儿流失、最终落到哪儿,宽度直接代表量级。这篇内容围绕 ECharts 桑基图入门展开,适合已经会一点 ECharts 配置、但还没系统用过桑基图的前端、数据分析或运营同学。我会从数据结构、核心配置、完整可运行示例一路写到企业级大屏和 Vue3 项目里的踩坑点,尽量让你看完就能把桑基图接进自己的项目。
很多人第一次接触桑基图,是因为看到别人做的用户路径、归因分析或者资金流向大屏,觉得效果很高级。真正动手时才发现,难的不是 series.type 写成 sankey,而是数据怎么组织、节点怎么保持守恒、线条怎么不糊成一团。下面按我实际项目里的使用顺序来拆,先把适用边界讲清楚,再进配置和代码。
1. 先搞清楚桑基图适合表达什么,不适合表达什么
1.1 从折线、柱状、饼图到流向图:什么时候该换桑基图
折线图擅长看趋势,柱状图擅长做对比,饼图擅长看静态占比,这三种图在 ECharts 里几乎是最常用的基础图形。桑基图的定位不太一样,它更像一张“带权重的流程图”,核心表达的是流转关系。比如用户从不同渠道进入商品详情,再分流到加购、直接购买、流失,最后到支付成功和复购,这一整条链路用柱状图很难表达清楚,因为柱状图只能告诉你每个环节有多少人,却看不出人是从哪里流过来的。
桑基图的价值在于把“量级”和“路径”绑在一起。每一条连线的粗细代表数值大小,节点的位置代表所处层级,流向代表转移方向。做企业级数据可视化时,桑基图经常出现在归因分析、订单转化、供应链流转、资金流向、能量损耗这些场景里。它的阅读门槛比普通图表高一点,但一旦数据设计对了,业务方往往能比看表格更快发现问题,比如某个渠道进量很大,但中途流失特别严重。
桑基图也不是万金油。如果你的目标只是比较“今年和去年销售额谁高”,柱状图更直接;如果只是看“各品类占比”,饼图或环形图更轻。桑基图适合多级流转,不适合单点对比。节点一多,交叉线会迅速变成一团毛线,所以实际项目里我一般控制在 30 个节点以内,超过就先聚合,把次要路径合并成“其他”,否则图好看但没人看得懂。
1.2 节点、边、权重和守恒:桑基图的四根支柱
桑基图的数据由节点和边组成。节点就是流转中的状态,比如“搜索广告”“商品详情”“支付成功”;边就是状态之间的转移,比如“搜索广告”到“商品详情”有多少人。ECharts 里节点写在 series.data 里,边写在 series.links 里,每条边必须有 source、target、value 三个字段。source 和 target 用的是节点名称,不是索引,这一点和关系图有点区别,写错一个字就会导致连线不显示。
权重是桑基图的灵魂。它决定了线条宽度,也决定了视觉上的注意力分配。比如搜索广告到商品详情是 1800 人,社交媒体到商品详情是 900 人,那么前者的线宽就是后者两倍左右。读者不需要看数字,也能感知到主次。这也是桑基图在汇报时特别好用的原因:业务方先看到哪条线粗,注意力自然就跟过去了。
守恒是桑基图最容易被忽略的规则。所谓守恒,就是中间节点的流入总量应该等于流出总量。比如“商品详情”流入了 5000 人,那么它流出的“加入购物车”“直接购买”“未支付流失”加起来也应该是 5000。叶子节点可以只进不出,也可以只出不进。ECharts 不会强制校验守恒,所以你不守恒也能画出来,但视觉上会出现节点宽度和连线对不上、流向像凭空产生的问题。真实项目里,我建议先在 Excel 或 SQL 里把守恒表拉平,再喂给 ECharts。
1.3 入门 ECharts 桑基图之前要准备什么
版本上,建议直接用 ECharts 5.x。旧版本虽然也有桑基图,但配置项、按需引入和文档示例差异不小,新手很容易搜到旧代码后跑不起来。项目里可以用 npm 安装 echarts,也可以先用 CDN 单页测试。如果走按需引入,别忘记注册 SankeyChart、TooltipComponent 和 CanvasRenderer,否则会遇到“图表空白但控制台没明显报错”的情况。
容器准备是第二个关键。ECharts 初始化时如果容器宽高为 0,会直接提示 can't get DOM width or height。很多人写 Vue 或 React 时,把图表容器放在 v-if 或 tab 里,初始化时容器还没渲染出来,自然拿不到尺寸。我的习惯是等 DOM 挂载完成、容器有明确高度后再 init,并且用 ResizeObserver 监听容器变化,而不是只监听 window.resize。
数据准备上,先拿 10 到 20 个节点做最小闭环。不要一上来就把生产库里的几十万行明细直接塞进去,桑基图布局计算比折线图重得多。先用聚合后的数据验证节点关系、守恒和视觉可读性,再去接真实数据源。官方文档和社区示例是最好的起点,但要注意示例数据往往简化过,复制到项目里要改字段名和层级。
2. ECharts 桑基图的数据结构和核心配置项
2.1 nodes 和 links 的字段约定与常见错误
先看一份最小数据结构:
const nodes = [ { name: '搜索广告' }, { name: '社交媒体' }, { name: '商品详情' } ]; const links = [ { source: '搜索广告', target: '商品详情', value: 1800 }, { source: '社交媒体', target: '商品详情', value: 900 } ];节点数组里最重要的是 name,必须唯一。如果你写了两个“商品详情”,ECharts 无法判断边到底连到哪一个,布局会异常。节点上还可以挂 itemStyle、label、depth 等字段,但入门阶段先把 name 写对。边里的 source 和 target 必须能在节点 name 里找到,value 必须是正数,字符串数字在某些版本里可能被容忍,但不要依赖这个行为,最好在数据层就转成 Number。
下面这张表是我在排查桑基图数据时最常遇到的几类问题:
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 图完全空白 | 容器无高度,或没注册 SankeyChart | 给容器明确宽高,检查按需引入 |
| 节点出现但没连线 | source/target 和 name 不匹配 | 逐字比对,必要时用 map 校验 |
| 连线方向奇怪 | 数据存在环 | 桑基图不适合循环,先打断环或换图 |
| 节点宽度异常 | value 为负数、NaN 或守恒被破坏 | 清洗数据,检查中间节点流入流出 |
| 控制台提示 cyclic | 链路形成闭环 | 找出 A→B→C→A 这类路径并拆分 |
还有一个常见坑是“节点没声明但边里用了”。比如 links 里写了 source: '首页推荐',但 nodes 里没有这个 name,ECharts 可能不会报很明确的错误,只是图上看不到。我的做法是写一个校验函数,遍历 links,把 source 和 target 都收集起来,再和 nodes 的 name 做差集,差集不为空就直接在开发环境抛错。
2.2 series 里最该先调的 8 个参数
series 是桑基图配置的核心,下面这些参数我几乎每次都会调:
| 参数 | 作用 | 入门建议 |
|---|---|---|
| type | 指定图表类型 | 固定写 'sankey' |
| data | 节点数组 | 每个节点至少 { name } |
| links | 边数组 | 必须有 source、target、value |
| nodeAlign | 节点对齐方式 | 常用 'justify',让布局更紧凑 |
| orient | 流向方向 | 横向用 'horizontal',窄屏可试 'vertical' |
| layoutIterations | 布局迭代次数 | 默认 32,节点多时降到 16 或 8 |
| draggable | 是否可拖拽节点 | 演示可开,生产看需求 |
| emphasis.focus | 高亮相邻关系 | 设为 'adjacency' 很实用 |
nodeAlign 这个参数值得单独说。它控制节点在水平方向上的对齐策略。'justify' 会让节点尽量均匀分布,整体更饱满;'left' 和 'right' 会让节点靠某一侧,适合强调起点或终点。实际项目里如果发现图右侧空了一大块,通常是节点层级和 nodeAlign 不匹配,换成 'justify' 往往就能改善。
layoutIterations 影响布局质量和性能。迭代次数越高,节点和连线的交叉可能越少,但计算时间也越长。节点少于 20 个时,我一般保持默认;节点到 50 个左右,会降到 16,并关闭动画。draggable 在演示时很好用,业务方可以自己拖节点看流向,但生产大屏上如果没人操作,最好关掉,减少误触和性能开销。emphasis.focus 设为 'adjacency' 后,鼠标移到节点上,只高亮相关连线,能大幅降低“毛线团”的阅读压力。
2.3 tooltip、label、edgeLabel 的信息设计
桑基图的 tooltip 需要区分节点和边。节点上显示名称和总量,边上显示来源、去向和数值。ECharts 的 tooltip formatter 接收的 params 里,dataType 可以判断是 'node' 还是 'edge'。下面是一个可直接用的写法:
tooltip: { trigger: 'item', triggerOn: 'mousemove', confine: true, extraCssText: 'white-space: normal; max-width: 280px; word-break: break-all;', formatter: function (params) { if (params.dataType === 'edge') { return params.data.source + ' → ' + params.data.target + '<br/>人数:' + params.data.value; } return params.name + '<br/>节点总量:' + (params.value || 0); } }热词里经常有人问 ECharts tooltip 自动换行。默认 tooltip 往往不换行,长文案会撑得很宽。解决方式就是 extraCssText 里加 white-space: normal 和 max-width,再配合 word-break。如果文案里有中文和英文混排,word-break: break-all 更稳。confine: true 能让 tooltip 限制在图表容器内,大屏边缘的节点就不会把提示框顶出去。
label 控制节点文字。节点多的时候,不要把所有信息都堆上去。我一般只显示名称,必要时用 formatter 拼一个简短数值。文字太长可以用 overflow: 'truncate',再配合 width 限制。edgeLabel 是边上的文字,默认关闭,因为边一多就会互相遮挡。只有节点很少、汇报需要直接看数值时才打开。打开后也要调 fontSize 和 color,否则线条上的文字会非常脏。
2.4 配色和线条:让流向可读而不是一团毛线
桑基图好不好看,配色占一半。最稳的做法是按层级上色,同一层的节点用同一种颜色,连线用渐变或 source 颜色。ECharts 的 lineStyle.color 支持 'source'、'target'、'gradient',也支持具体颜色值。'gradient' 会让线条从源节点颜色过渡到目标节点颜色,视觉上更顺,但节点多时颜色可能过花。我一般先用 'source',再调低 opacity,让线条退到背景层。
lineStyle: { color: 'source', curveness: 0.5, opacity: 0.45 }, nodeWidth: 14, nodeGap: 10curveness 控制线条弯曲程度,0 是直线,0.5 左右比较自然。opacity 很关键,1 会糊成实心色块,0.3 到 0.5 通常更舒服。深色大屏背景下,opacity 可以稍高一点,但不要超过 0.6。nodeWidth 和 nodeGap 分别控制节点宽度和节点间距,节点多的时候要适当减小 nodeGap,否则画布不够用。
levels 可以按深度统一配置样式:
levels: [ { depth: 0, itemStyle: { color: '#5B8FF9' }, lineStyle: { color: 'source', opacity: 0.45 } }, { depth: 1, itemStyle: { color: '#61DDAA' }, lineStyle: { color: 'source', opacity: 0.45 } }, { depth: 2, itemStyle: { color: '#F6BD16' }, lineStyle: { color: 'source', opacity: 0.45 } } ]depth 从 0 开始,对应第一层节点。用 levels 的好处是配色统一,不会因为节点顺序变化而乱掉。如果背景是浅色,颜色饱和度要低一些;如果是深色大屏,节点可以亮一点,但连线要暗下去。别小看这一点,很多桑基图看起来乱,不是布局问题,而是线条和节点抢了同样的视觉权重。
3. 手把手做一个可运行的桑基图
3.1 页面骨架与 ECharts 引入方式
先给一份完整 HTML,直接保存成 .html 文件就能跑。这里用 CDN 引入 ECharts 5,适合快速验证。项目里再换成 npm 按需引入。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>ECharts 桑基图入门示例</title> <style> html, body { margin: 0; padding: 0; height: 100%; background: #0b1020; } #chart { width: 100vw; height: 100vh; } </style> </head> <body> <div id="chart"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> // 图表代码放在这里 </script> </body> </html>容器必须有宽高,这是硬条件。上面用 100vw 和 100vh,简单直接。真实项目里可能是父级 div 控制高度,这时要确保父级高度不是 auto,也不是 0。很多人把图表放在 flex 布局里,父级高度由内容撑开,结果 ECharts 初始化时量到 0。解决办法是给父级明确高度,或者等布局稳定后再 init。
3.2 准备一份守恒的电商转化数据
这份数据我设计成从渠道到商品详情,再到加购、购买、支付和复购的链路。每个中间节点的流入和流出都保持相等。你可以先用这份数据跑通,再替换成自己的业务数据。
| 来源 | 去向 | 人数 |
|---|---|---|
| 首页推荐 | 商品详情 | 1200 |
| 搜索广告 | 商品详情 | 1800 |
| 社交媒体 | 商品详情 | 900 |
| 活动页 | 商品详情 | 1100 |
| 商品详情 | 加入购物车 | 2600 |
| 商品详情 | 直接购买 | 1400 |
| 商品详情 | 未支付流失 | 1000 |
| 加入购物车 | 支付成功 | 1800 |
| 加入购物车 | 未支付流失 | 800 |
| 直接购买 | 支付成功 | 1400 |
| 支付成功 | 复购 | 800 |
| 支付成功 | 完结 | 2400 |
检查一下守恒:四个来源进入商品详情总计 5000,商品详情流出 2600 + 1400 + 1000 = 5000。加入购物车流入 2600,流出 1800 + 800 = 2600。直接购买流入 1400,流出 1400。支付成功流入 1800 + 1400 = 3200,流出 800 + 2400 = 3200。未支付流失是叶子节点,流入 1800。复购和完结也是叶子节点。这样的数据画出来,节点宽度和线条宽度会非常自然。
真实业务数据往往不守恒,比如订单表里同一个用户被多端记录,或者退款、取消状态没纳入。这时候要么补全状态,要么在数据层做归因拆解。不要直接把不守恒的数据丢给 ECharts,然后靠调样式掩盖,那样业务方一看细节就会问出问题。
3.3 完整配置代码与逐段说明
下面是完整初始化代码,放在 script 标签里即可。
const chartDom = document.getElementById('chart'); const chart = echarts.init(chartDom); const nodes = [ { name: '首页推荐' }, { name: '搜索广告' }, { name: '社交媒体' }, { name: '活动页' }, { name: '商品详情' }, { name: '加入购物车' }, { name: '直接购买' }, { name: '未支付流失' }, { name: '支付成功' }, { name: '复购' }, { name: '完结' } ]; const links = [ { source: '首页推荐', target: '商品详情', value: 1200 }, { source: '搜索广告', target: '商品详情', value: 1800 }, { source: '社交媒体', target: '商品详情', value: 900 }, { source: '活动页', target: '商品详情', value: 1100 }, { source: '商品详情', target: '加入购物车', value: 2600 }, { source: '商品详情', target: '直接购买', value: 1400 }, { source: '商品详情', target: '未支付流失', value: 1000 }, { source: '加入购物车', target: '支付成功', value: 1800 }, { source: '加入购物车', target: '未支付流失', value: 800 }, { source: '直接购买', target: '支付成功', value: 1400 }, { source: '支付成功', target: '复购', value: 800 }, { source: '支付成功', target: '完结', value: 2400 } ]; const option = { backgroundColor: '#0b1020', tooltip: { trigger: 'item', triggerOn: 'mousemove', confine: true, extraCssText: 'white-space: normal; max-width: 280px; word-break: break-all;', formatter: function (params) { if (params.dataType === 'edge') { return params.data.source + ' → ' + params.data.target + '<br/>人数:' + params.data.value; } return params.name + '<br/>节点总量:' + (params.value || 0); } }, series: [ { type: 'sankey', data: nodes, links: links, nodeAlign: 'justify', orient: 'horizontal', layoutIterations: 32, draggable: true, emphasis: { focus: 'adjacency' }, nodeWidth: 14, nodeGap: 10, lineStyle: { color: 'source', curveness: 0.5, opacity: 0.45 }, label: { show: true, color: '#e8f0ff', fontSize: 12, formatter: '{b}' }, edgeLabel: { show: false }, levels: [ { depth: 0, itemStyle: { color: '#5B8FF9' }, lineStyle: { color: 'source', opacity: 0.45 } }, { depth: 1, itemStyle: { color: '#61DDAA' }, lineStyle: { color: 'source', opacity: 0.45 } }, { depth: 2, itemStyle: { color: '#F6BD16' }, lineStyle: { color: 'source', opacity: 0.45 } }, { depth: 3, itemStyle: { color: '#FF9845' }, lineStyle: { color: 'source', opacity: 0.45 } } ] } ] }; chart.setOption(option); window.addEventListener('resize', function () { chart.resize(); });逐段看几个点。backgroundColor 设成深色是为了大屏效果,浅色背景改成对应颜色即可。tooltip 的 formatter 里判断了 dataType,这样节点和边不会显示同样的信息。series 里 data 和 links 直接对应前面两个数组。nodeAlign 用 justify,横向布局更饱满。emphasis.focus 设成 adjacency,鼠标悬停时只高亮相邻节点和边,避免整张图都在抢注意力。
levels 按 depth 配置颜色,第一层渠道一种色,第二层商品详情一种色,第三层加购和购买一种色,第四层支付成功一种色。注意 depth 是根据数据计算出来的,不是手动指定。如果发现颜色没按预期生效,先检查节点是否真的处于对应层级。最后一个 window.resize 监听只是保底,真实项目更推荐 ResizeObserver。
3.4 自适应、resize 和 pxtorem 不生效的处理
热词里有人问 pxtorem 对 ECharts 没起到效果,Vue3 项目里尤其常见。原因不复杂:PostCSS 的 pxtorem 只处理 CSS 文件里的 px 值,ECharts 的图形是画在 Canvas 里的,选项里的 fontSize、nodeWidth、nodeGap 这些数值都是 JavaScript 对象,pxtorem 根本看不到。容器宽高可以用 rem,但 Canvas 内部绘制仍然按 px 算。
处理思路有三步。第一步,容器宽高用 CSS 百分比、vw、vh 或 rem 控制,ECharts 只负责填满容器。第二步,ECharts 内部的字体和间距用 JS 根据根字号动态计算。比如:
const rootFontSize = parseFloat( getComputedStyle(document.documentElement).fontSize ); const option = { series: [{ label: { fontSize: rootFontSize * 0.75 }, nodeWidth: rootFontSize * 0.9, nodeGap: rootFontSize * 0.6 }] };这样根字号变化时,你重新计算 option 并 setOption,再调用 chart.resize(),就能让 Canvas 内部也跟着缩放。第三步,如果项目用 transform: scale 做大屏适配,要特别小心鼠标事件。ECharts 内部的坐标计算和外层缩放叠加后,tooltip 位置可能偏移。能不用 scale 就不用,优先用响应式容器加动态字号。
另外,resize 不要监听得太频繁。window.resize 触发很密集,桑基图布局又比较重,最好加 100 到 200 毫秒防抖。Vue3 里可以在 onMounted 创建 ResizeObserver,在 onBeforeUnmount 里 disconnect,避免组件销毁后还在触发重绘。
4. 放进真实项目:Vue3、大屏、性能和交互
4.1 Vue3 组件封装:按需引入与响应式更新
真实项目里不建议每个页面都全量引入 echarts。按需引入的写法如下:
import * as echarts from 'echarts/core'; import { SankeyChart } from 'echarts/charts'; import { TooltipComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([SankeyChart, TooltipComponent, CanvasRenderer]);组件里用 ref 拿到 DOM,onMounted 初始化,onBeforeUnmount 销毁。数据变化时用 watch 监听,重新 setOption。注意桑基图更新数据时,旧节点状态可能残留,可以传第二个参数 true 做 notMerge 合并,或者先 clear 再 setOption。下面是一个简化版 Vue3 组件结构:
<template> <div ref="chartRef" class="sankey-chart"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'; import * as echarts from 'echarts/core'; import { SankeyChart } from 'echarts/charts'; import { TooltipComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([SankeyChart, TooltipComponent, CanvasRenderer]); const props = defineProps({ chartData: { type: Object, required: true } }); const chartRef = ref(null); let chart = null; function buildOption() { return { tooltip: { trigger: 'item', confine: true, extraCssText: 'white-space: normal; max-width: 280px; word-break: break-all;' }, series: [{ type: 'sankey', data: props.chartData.nodes, links: props.chartData.links, nodeAlign: 'justify', emphasis: { focus: 'adjacency' }, lineStyle: { color: 'source', curveness: 0.5, opacity: 0.45 }, label: { show: true, fontSize: 12 } }] }; } onMounted(async () => { await nextTick(); chart = echarts.init(chartRef.value); chart.setOption(buildOption()); }); watch( () => props.chartData, () => { if (!chart) return; chart.setOption(buildOption(), true); }, { deep: true } ); onBeforeUnmount(() => { if (chart) { chart.dispose(); chart = null; } }); </script> <style scoped> .sankey-chart { width: 100%; height: 100%; min-height: 420px; } </style>这里有两个经验点。第一,init 之前加 nextTick,确保 DOM 有尺寸。第二,onBeforeUnmount 里 dispose,否则页面切换多次后内存会持续上涨,大屏项目尤其明显。如果容器尺寸会变,再加 ResizeObserver,不要只依赖 window.resize。
4.2 企业级大屏适配:rem、px、DPR 和 ECharts 的边界
大屏适配里,rem 方案很常见:根据设计稿宽度设置根字号,页面元素用 rem。ECharts 容器可以用 rem,但图表内部是 Canvas,需要单独处理。前面讲过 pxtorem 不生效,这里再补一个 DPR 的点。ECharts 初始化时可以指定 devicePixelRatio:
chart = echarts.init(chartRef.value, null, { renderer: 'canvas', devicePixelRatio: window.devicePixelRatio || 1 });高分辨率屏幕上,合适的 devicePixelRatio 能让文字和线条更清晰。但如果页面被浏览器缩放,或者大屏用 transform: scale 整体缩放,DPR 和鼠标坐标可能对不上。我的建议是,大屏尽量按真实分辨率布局,用 vw、vh、百分比和动态字号适配,少用整体 scale。如果非要用 scale,至少把 ECharts 放在 scale 容器外层,或者测试 tooltip 和点击事件是否偏移。
字号适配可以用一个比例函数。比如设计稿宽 1920,当前窗口宽 2560,比例是 1.333,那么 fontSize 也从 12 变成 16。注意不要只改容器大小,不改 option,否则容器变大了,文字还是很小。每次尺寸变化时,先算新字号,再 setOption 合并,最后 resize。这个过程要防抖,不然大屏窗口拖动时会卡。
4.3 节点一多就卡:桑基图性能治理清单
桑基图布局是计算密集型的,节点和边一多,浏览器主线程压力很明显。下面这张表是我遇到卡顿时的排查顺序:
| 现象 | 优先检查 | 处理建议 |
|---|---|---|
| 首次渲染慢 | 节点数、边数 | 聚合次要节点,合并为“其他” |
| 鼠标移动卡 | tooltip、emphasis | 简化 formatter,关闭复杂 HTML tooltip |
| 拖拽不流畅 | draggable、动画 | 生产关闭 draggable,animation 设 false |
| 数据更新白屏 | notMerge、重复 init | 复用实例,setOption 更新 |
| 页面切换内存涨 | 未 dispose | onBeforeUnmount 调用 dispose |
节点数超过 50、边数超过 200 后,我会默认关闭动画,把 layoutIterations 降到 16 甚至 8。布局质量会略有下降,但交互流畅度提升很明显。如果业务方不需要拖拽,draggable 直接设为 false。tooltip 如果用了很复杂的 HTML 结构,也会拖慢悬停响应,尽量用轻量字符串。
还有一个容易忽略的点是数据更新频率。大屏如果每 3 秒刷新一次,不要每次都重新 init,也不要用全量深拷贝。复用 chart 实例,只更新变化的 nodes 和 links。如果数据结构稳定,甚至可以只更新 links 的 value,但桑基图布局依赖节点关系,关系变了还是要重新 setOption。
4.4 交互增强:下钻、联动、导出和状态保持
桑基图点击节点做下钻很常见。比如点击“商品详情”,右侧展示该节点的明细来源。代码上监听 click:
chart.on('click', function (params) { if (params.dataType === 'node') { // 根据 params.name 请求明细数据 // 然后 chart.setOption(newOption, true) } });联动其他图表时,可以用 dispatchAction 高亮对应节点。导出图片用 chart.getDataURL(),设置 pixelRatio 和 backgroundColor。状态保持稍微麻烦一点,因为桑基图更新数据后布局会重算,用户拖拽过的节点位置默认不会保留。如果业务上需要保留,可以在拖拽结束事件里记录节点位置,但 ECharts 对桑基图节点位置的直接控制能力有限,实际项目里我更建议用固定数据顺序和 nodeAlign 来稳定布局,而不是依赖用户拖拽。
5. 常见问题排查与实战避坑
5.1 空白、报错、警告的排查路径
桑基图空白时,按这个顺序查:容器有没有宽高、ECharts 有没有初始化成功、SankeyChart 有没有注册、nodes 和 links 是否为空、source/target 是否匹配、数据是否成环。控制台如果提示 cyclic,说明链路闭环了,桑基图不适合直接画循环流,需要先拆环或换图。如果提示 can't get DOM width or height,九成是容器高度为 0,先给父级和自身明确高度。
按需引入漏注册也很常见。全量引入 echarts.min.js 时不会遇到,但 npm 按需引入只写echarts.use([TooltipComponent]),没写 SankeyChart,就会空白。检查方法是看控制台有没有 “Component series.sankey not exists” 之类的提示。版本不匹配也会导致配置项无效,比如从旧文档复制过来的 edgeLabel 写法在新版本里行为不同,最好以当前使用的版本文档为准。
5.2 布局乱、连线交叉、文字溢出怎么调
布局乱的第一反应不要是调颜色,而是调数据和 nodeAlign。节点顺序会明显影响连线交叉,把同一来源的节点放一起,把主要流向放在中间,往往比调 layoutIterations 更有效。nodeAlign 用 justify 可以让层级分布更均匀。orient 改成 vertical 适合窄屏,但纵向布局节点多时会更拥挤。
文字溢出用 label.overflow: 'truncate' 配合 width。如果名称太长,可以在数据层做短名映射,tooltip 里再显示全称。边上的文字默认别开,开了就要接受遮挡风险。连线太密时,降低 opacity、用 source 颜色、减小 curveness,都能让图干净一些。但如果节点数量本身超标,任何样式都是治标不治本,聚合数据才是根本。
| 问题 | 调整项 | 经验值 |
|---|---|---|
| 连线交叉多 | 节点顺序、layoutIterations | 先调顺序,再降迭代 |
| 节点挤在一起 | nodeGap、nodeWidth | 节点多时减小 nodeGap |
| 文字看不清 | label.fontSize、overflow | 名称短化,宽度限制 |
| 线条太糊 | lineStyle.opacity、curveness | opacity 0.35 到 0.5 |
| 整体太散 | nodeAlign | 优先试 justify |
5.3 与热词相关的零散问题:饼图、柱状图、地图、社区资源
热词里还有很多 ECharts 相关问题,这里顺带说清楚。饼图 labelLine 末尾小圆点偏移,通常和 labelLine.length、labelLine.length2、label.offset 有关,调整这几个值可以让引导线和文字对齐。柱状图柱子用自定义图片,普通柱状图可以通过 itemStyle.color 传图片对象实现重复填充,想要更复杂的形状可以用 pictorialBar。地图类图表要注意底图来源和授权合规,不是拿到一份边界数据就能直接商用,这部分单独评估。
tooltip 自动换行前面已经给了 extraCssText 方案。pxtorem 对 ECharts 不生效,是因为 Canvas 绘制不走 CSS 转换,解决思路是动态计算字号并重新 setOption。社区资源很好用,但复制示例时一定先看 ECharts 版本,再改数据字段。很多“跑不起来”的问题,不是代码错了,而是版本和引入方式不一致。
5.4 我的避坑清单
- 先做守恒表,再写 option。数据不守恒,后面调什么都是白费。
- 节点 name 必须唯一,source 和 target 必须与 name 完全一致。
- 中间节点流入等于流出,叶子节点可以只进不出或只出不进。
- 桑基图不画环,出现环先拆环或换关系图。
- 容器必须有明确宽高,Vue3 里 init 前加 nextTick。
- 按需引入别漏 SankeyChart,否则图会空白。
- 节点超过 30 个先考虑聚合,超过 50 个基本要牺牲交互换性能。
- 生产环境关闭 draggable 和 animation,大屏会更稳。
- pxtorem 管不到 Canvas,字号和间距要 JS 动态算。
- 组件销毁时 dispose,避免内存泄漏和重复实例。
我个人在项目里最先做的一件事,往往不是调样式,而是先拿一份守恒的数据把流向跑通。只要节点和边的关系是对的,后面配色、大屏适配、下钻都只是工程问题。桑基图真正的门槛不在 ECharts API,而在你是否愿意先把业务链路拆清楚。