☰
ECharts瀑布图实战:从占位柱原理到生产级配置
2026/11/3 8:58:42 网站建设 项目流程

瀑布图这个图表类型,我在数据可视化项目里用得不算最多,但每次用到都是关键场景——财务损益拆解、用户增长归因、库存变化追踪,这类需要把"一个总数怎么被一步步加减成另一个总数"讲清楚的场合,普通柱状图根本扛不住。ECharts 官方示例库里没有现成的瀑布图配置,很多人第一次做都会卡在"透明占位柱"这个思路上,我当初也是翻了不少文档才把逻辑理顺。这篇内容就把我用 ECharts 绘制瀑布图的完整思路、参数配置、踩坑记录摊开讲,从最基础的占位原理到多系列堆叠、颜色分段、标签格式化,再到数据量大了之后的性能处理,都会给到可直接复用的配置片段。适合已经会用 ECharts 画基础图表、想进一步搞定这种"有逻辑关系"的复合图表的前端和数据可视化从业者,也适合做经营分析、财务看板的产品和运营同学参考,看完你至少能独立把一张能上生产环境的瀑布图配出来。

1. 瀑布图到底在表达什么,为什么 ECharts 要绕个弯

1.1 从业务场景反推图表选型

先把这个图表的本质说透。瀑布图(Waterfall Chart)的核心不是"画柱子",而是表达累积过程:从一个初始值出发,经过若干次正向或负向的变化,最终落到一个终值。每一根柱子都站在前一根柱子的肩膀上,正负变化用不同颜色区分,首尾两根通常是汇总柱,从零起画。

我接触过的典型场景有这么几类。财务侧最常见的是利润表拆解:营业收入作为起点,减成本、减费用、加其他收益,最后得到净利润,老板一眼就能看出哪一项把利润吃掉了。运营侧是用户规模变化:月初存量、新增、流失、回流,月末存量,用来解释"为什么这个月用户没涨"。供应链侧是库存变动:期初库存、入库、出库、损耗、期末库存。这三类场景的共同点是——数据之间存在加减关系,读者关心的不只是单项大小,更是各项对总体的贡献方向。

普通柱状图做不到这一点,因为它每根柱子都从零起画,读者得自己在脑子里做加减法。折线图也不行,它表达的是趋势不是构成。所以瀑布图的价值就在于把"过程"可视化,让加减关系变成视觉上的台阶。

1.2 ECharts 没有原生瀑布图类型,这是好事还是坏事

ECharts 的 series 类型里有 bar、line、pie、scatter 等等,就是没有 waterfall。第一次遇到的人可能会觉得奇怪,但理解了瀑布图的绘制原理你就明白,它本质上就是堆叠柱状图的一个变种,用两根堆叠的柱子模拟出"悬浮"效果。

具体做法是:每根"可见柱"下面垫一根"透明柱",透明柱的高度等于该可见柱的起始基准值,可见柱本身的高度等于变化量的绝对值。两根柱子堆叠起来,视觉上可见柱就从基准位置开始画了。这个思路叫占位柱法,是瀑布图最经典的实现方式,Excel、Tableau 底层也是类似逻辑。

提示:理解占位柱是理解整个瀑布图配置的钥匙。后面所有的参数调整,本质上都是在控制"透明柱多高"和"可见柱多高"这两件事。

为什么说这是好事?因为用堆叠柱模拟,意味着你可以完全复用 ECharts 柱状图的所有能力——标签格式化、颜色渐变、tooltip 自定义、点击事件、动画,一样不少。如果官方给了一个封闭的 waterfall 类型,反而可能限制你的定制空间。绕的这个弯,换来的是灵活性。

1.3 一张合格瀑布图的四个视觉要素

在动手写配置之前,先把验收标准立起来,不然配出来自己都不知道对不对。我总结下来有四个要素:

  • 基准正确:每根柱子的起点必须精确等于前面所有变化的累积和,差一点整个图就错了。
  • 正负可辨:增加项和减少项用不同颜色,通常增加用绿或蓝,减少用红或橙,这是行业惯例,别反着来。
  • 首尾汇总:第一根是初始总值,最后一根是最终总值,这两根从零起画,颜色通常用中性色(深灰或深蓝)区分于中间的变化项。
  • 连接可读:柱子之间最好有虚线连接,帮助视线沿着台阶走,这个用 markLine 或者自定义图形都能做。

这四点里,基准正确是硬性要求,其余三点是体验优化。下面所有章节的配置,都是围绕这四点展开的。

2. 核心数据结构设计:先把账算对,再谈画图

2.1 原始数据长什么样,转换后长什么样

画瀑布图最容易出错的地方不在 ECharts 配置,而在数据预处理。原始业务数据通常是一串"变化量",比如[+120, -45, -30, +18],但 ECharts 需要的是两组数:透明占位柱的高度、可见柱的高度。这个转换必须自己算,算错了图就废了。

我习惯用一个转换函数把原始数据加工成三个数组:placeholder(占位柱)、visible(可见柱)、base(每根柱的起始基准,用于 tooltip 和标签)。转换逻辑是这样的:维护一个累积变量cumulative,初始为 0;对每一项,如果它是正数,占位柱高度等于当前累积值,可见柱高度等于该值本身,然后累积值加上它;如果是负数,占位柱高度等于累积值加上该负数(也就是变化后的位置),可见柱高度等于该负数的绝对值,累积值同样加上它。

用一段代码把这个逻辑固化下来,这是整个瀑布图的地基:

function buildWaterfallData(rawData) { // rawData 形如 [{ name: '期初', value: 1000, type: 'total' }, ...] const placeholder = []; const visible = []; const base = []; let cumulative = 0; rawData.forEach((item) => { if (item.type === 'total') { // 汇总柱:从零起画,占位为 0 placeholder.push(0); visible.push(item.value); base.push(0); cumulative = item.value; } else { const value = item.value; if (value >= 0) { placeholder.push(cumulative); visible.push(value); base.push(cumulative); } else { placeholder.push(cumulative + value); visible.push(Math.abs(value)); base.push(cumulative + value); } cumulative += value; } }); return { placeholder, visible, base, finalTotal: cumulative }; }

这段代码里有个细节值得说:汇总柱的处理。首根汇总柱的cumulative从 0 开始,直接赋值即可;但末根汇总柱的cumulative应该是前面所有变化的累积结果,所以我在type === 'total'分支里直接把item.value赋给cumulative,前提是末根汇总柱的 value 就是最终值。如果你的数据里末根汇总柱的 value 是单独算的,要确保它和累积结果一致,不一致说明原始数据有问题,得先查账。

2.2 为什么不用负值直接画,非要搞透明占位

有人会问:ECharts 柱状图支持负值啊,减少项直接传负数不就行了?这个想法很自然,但行不通。原因在于,柱状图的负值是从零轴往下画的,而瀑布图里减少项的起点不是零,是前面累积到的高度。比如累积到 800 时减 200,这根柱子应该从 800 画到 600,而不是从 0 画到 -200。

透明占位柱解决的正是"起点不为零"这个问题。它把柱子"顶"到正确的高度,可见柱再从这个高度往上(或往下)画。这是堆叠柱的经典技巧,理解了这一层,后面调参数就不会迷糊。

注意:占位柱的itemStyle必须设成完全透明,color: 'transparent'或者opacity: 0都行,但别用color: '#fff',因为背景色一变就露馅了。我见过有人用白色占位,结果换成深色主题后整张图多出一堆白块。

2.3 数据校验:三个必须做的自检

数据转换完,别急着往 ECharts 里塞,先做三个自检,能省掉大量调试时间。

第一,累积终值校验。把所有变化量加起来,加上初始值,应该等于末根汇总柱的值。不等就是原始数据错了,或者汇总柱的 value 填错了。

第二,占位柱非负校验。占位柱高度理论上不应该出现负数,如果出现,说明累积值在中途变成了负数,这时候要么数据本身有问题(比如成本超过收入),要么你的业务逻辑允许负基准,需要特殊处理。大多数财务场景不允许,得回头查数据。

第三,数组长度一致校验。placeholder、visible、base三个数组长度必须和原始数据一致,堆叠柱要求两个系列的数据点一一对应,长度不一致 ECharts 会静默出错,图会画得莫名其妙。

我一般会写个简单的断言函数,开发环境跑一遍,生产环境去掉:

function validateWaterfall(rawData, result) { const sum = rawData .filter(d => d.type !== 'total') .reduce((acc, d) => acc + d.value, 0); const initial = rawData[0].value; console.assert( Math.abs(initial + sum - result.finalTotal) < 1e-6, '累积终值校验失败,请检查数据' ); console.assert( result.placeholder.every(v => v >= 0), '占位柱出现负值,请检查累积过程' ); }

这三个自检看着简单,但我在实际项目里靠它们抓到过好几次数据源的问题,比在图上看半天强多了。

3. 完整配置实现:从零搭出一张能上生产的瀑布图

3.1 基础配置骨架与两个系列的堆叠关系

数据准备好了,开始写 option。核心就是两个 bar 系列,第一个是占位柱,第二个是可见柱,两者stack属性设成同一个值,ECharts 就会把它们堆叠起来。

const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: rawData.map(d => d.name), axisTick: { alignWithLabel: true } }, yAxis: { type: 'value' }, series: [ { name: 'placeholder', type: 'bar', stack: 'waterfall', itemStyle: { color: 'transparent', borderColor: 'transparent' }, emphasis: { itemStyle: { color: 'transparent' } }, data: result.placeholder, silent: true, tooltip: { show: false } }, { name: '变化量', type: 'bar', stack: 'waterfall', data: result.visible, label: { show: true, position: 'top', formatter: (params) => { const raw = rawData[params.dataIndex]; if (raw.type === 'total') return raw.value; return (raw.value >= 0 ? '+' : '') + raw.value; } } } ] };

这段配置里几个关键点必须强调。stack: 'waterfall'两个系列要完全一致,名字随便起但必须相同。占位柱系列要设silent: true和tooltip: { show: false },否则鼠标移上去会触发占位柱的 tooltip,显示一堆没意义的数字。emphasis里也要把颜色设成透明,不然 hover 时占位柱会突然显形,视觉上很突兀。

标签的formatter用了一个技巧:通过params.dataIndex反查原始数据,这样能区分汇总柱和变化柱,汇总柱显示原值,变化柱显示带正负号的值。这个反查依赖rawData在闭包里可访问,实际项目里注意作用域。

3.2 颜色分段:让正负和汇总一眼可辨

基础配置出来的图所有柱子一个颜色,信息量不够。瀑布图的颜色逻辑通常是三段式:汇总柱一个色、增加项一个色、减少项一个色。ECharts 的itemStyle.color支持回调函数,可以按数据项动态返回颜色。

itemStyle: { color: (params) => { const raw = rawData[params.dataIndex]; if (raw.type === 'total') return '#2c3e50'; return raw.value >= 0 ? '#27ae60' : '#e74c3c'; } }

这里我用了深蓝灰做汇总、绿色做增加、红色做减少,是财务看板的常见配色。如果你的产品主色调不同,换成品牌色系即可,但正负的冷暖对比要保留,这是认知习惯,别为了好看牺牲可读性。

有个细节:颜色回调函数在每次渲染时都会执行,如果rawData很大,注意别在回调里做复杂计算,直接查数组就行。我见过有人在回调里重新遍历原始数据找对应项,数据量上千时明显卡顿。

3.3 连接线:用 markLine 把台阶串起来

柱子之间的虚线连接不是必须的,但加上之后可读性提升明显,尤其是柱子多的时候,视线容易沿着线走。ECharts 没有直接的"瀑布连接线"配置,得用markLine手动构造,或者用自定义系列画。我一般用markLine的data数组,每两个相邻柱子的顶部基准点连一条线。

不过markLine的坐标是基于数据坐标系的,要精确连到每根柱子的顶部,得算出每根柱子的顶部 y 值。对于变化柱,顶部 y 值等于base + visible(正数情况)或base(负数情况,因为负数柱是从 base 往下画的,顶部其实是 base)。这个计算有点绕,我封装了一个函数:

function buildConnectLines(rawData, result) { const lines = []; for (let i = 0; i < rawData.length - 1; i++) { const currentTop = result.base[i] + result.visible[i]; const nextBase = result.base[i + 1]; lines.push([ { coord: [i, currentTop], symbol: 'none' }, { coord: [i + 1, nextBase], symbol: 'none' } ]); } return lines; }

然后把lines塞进可见柱系列的markLine.data,样式设成虚线、浅灰。这里要注意,markLine的coord第一个值是 x 轴索引,第二个是 y 轴数值,索引从 0 开始对应类目。如果 x 轴是类目轴,这样写没问题;如果 x 轴是时间轴或数值轴,得换成实际坐标值。

提示:连接线在柱子很多时会显得杂乱,超过 10 根柱子建议关掉,或者只连关键节点。视觉元素不是越多越好,能表达清楚就行。

3.4 标签与 tooltip 的精细化处理

标签默认显示在柱子顶部,但瀑布图里减少项的柱子是往下画的,标签放顶部会跑到柱子上面去,和连接线打架。解决办法是按正负动态设置position:增加项top,减少项bottom,汇总柱top。

label: { show: true, position: (params) => { const raw = rawData[params.dataIndex]; if (raw.type === 'total') return 'top'; return raw.value >= 0 ? 'top' : 'bottom'; }, formatter: (params) => { const raw = rawData[params.dataIndex]; if (raw.type === 'total') return raw.value; return (raw.value >= 0 ? '+' : '') + raw.value; }, fontSize: 12, color: '#333' }

tooltip 的定制更值得花心思,因为这是用户获取详细信息的主要入口。默认 tooltip 会把占位柱和可见柱都列出来,占位柱那行纯属干扰。我一般用formatter完全自定义,只展示业务关心的信息:项目名、变化量、变化后的累积值。

tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: (params) => { const idx = params[0].dataIndex; const raw = rawData[idx]; const after = result.base[idx] + (raw.type === 'total' ? raw.value : raw.value); const sign = raw.type === 'total' ? '' : (raw.value >= 0 ? '+' : ''); return `${raw.name}<br/>变化量:${sign}${raw.value}<br/>累计值:${after}`; } }

这里after的计算对汇总柱和变化柱略有不同,汇总柱的累计值就是它本身,变化柱的累计值是base + value。实际写的时候建议把这段逻辑抽成独立函数,tooltip 和标签共用,避免两处计算不一致。

4. 进阶场景:多系列堆叠、动态数据与性能处理

4.1 分组瀑布图:一个维度下拆多个系列

业务里经常遇到"总变化量还要按渠道拆开"的需求,比如用户流失 500,其中自然流失 300、竞品挖走 200,想在瀑布图上体现这个细分。这时候单靠两个系列不够了,得用多组堆叠。

思路是把每个细分渠道做成一对"占位+可见"系列,所有占位系列 stack 到同一个组,所有可见系列 stack 到另一个组,但这样堆叠关系会乱。更稳妥的做法是:每个渠道独立一对系列,但 stack 名字按渠道区分,同时通过barGap和barCategoryGap控制间距,让同一类目的多根柱子并排显示。

不过说实话,分组瀑布图在 ECharts 里做起来比较别扭,柱子一多就挤成一团。我的经验是,如果细分维度超过 3 个,不如换成"瀑布图 + 明细表"的组合,图上只画主变化,明细放表格里,信息密度更高也更清晰。硬要在图上堆,反而两边都不讨好。

4.2 数据动态更新时的重算逻辑

看板类项目里,瀑布图的数据经常要随筛选条件变化而更新。这时候不能只调setOption换数据,因为占位柱的高度依赖累积计算,数据一变,所有占位柱都得重算。

正确做法是:数据变化时,先跑一遍buildWaterfallData重新生成三个数组,再setOption更新两个系列的data。如果只更新可见柱不更新占位柱,图会错位,这是新手常犯的错。

function updateWaterfall(chartInstance, newRawData) { const result = buildWaterfallData(newRawData); chartInstance.setOption({ xAxis: { data: newRawData.map(d => d.name) }, series: [ { data: result.placeholder }, { data: result.visible } ] }); }

注意setOption默认是合并模式,series 数组按索引合并,所以两个系列的顺序必须和初始化时一致,占位柱在前、可见柱在后。如果顺序反了,数据会串到错误的系列上。我一般会在初始化时给系列加id,更新时用id匹配,比靠索引稳。

4.3 大数据量下的渲染优化

瀑布图柱子数量一般不会太多,财务拆解十几项、运营归因七八项,都在 ECharts 舒适区内。但如果遇到上百项的场景(比如按 SKU 拆解库存变化),就得考虑优化了。

几个实用手段:一是关掉动画,animation: false,上百根柱子的入场动画很吃性能;二是标签默认不显示,改成 hover 时通过 tooltip 看,或者只给关键节点显示标签;三是连接线关掉,markLine 在柱子多时渲染开销不小;四是考虑large: true和largeThreshold,ECharts 对大数据量柱状图有专门的优化模式,开启后渲染方式会变,性能提升明显,但部分交互特性会受限,得权衡。

我实测过一个 200 项的瀑布图,关掉动画和连接线、标签只显示汇总柱之后,首屏渲染从 1.2 秒降到 300 毫秒左右,交互也流畅了。数据量再大,老实说就不该用瀑布图了,换成热力图或者直接上表格更合适。

5. 常见问题排查与实操避坑清单

5.1 图错位、柱子悬空、标签乱跑的原因对照

瀑布图的问题大多集中在"位置不对"这一类,我把踩过的坑整理成一张速查表,遇到问题先对号入座。

现象最可能原因排查方向
柱子悬空,底部不接前一根占位柱高度算错,累积值没更新检查cumulative是否在每项后正确累加
减少项柱子往上画了可见柱传了负值可见柱必须传绝对值,负号只用于颜色和标签
汇总柱位置不对汇总柱的占位没设 0汇总柱placeholder必须为 0
柱子重叠成一团两个系列 stack 名不一致检查stack字符串是否完全相同
hover 出现透明柱 tooltip占位柱没设 silent加silent: true和tooltip: { show: false }
标签和柱子对不上formatter 里索引取错用params.dataIndex反查原始数据
更新数据后图错乱只更新了可见柱占位柱和可见柱必须同时重算更新

这张表里,前三条是数据层问题,中间三条是配置层问题,最后一条是更新逻辑问题。按这个顺序排查,基本能覆盖九成以上的故障。

5.2 三个我踩过的真实坑

第一个坑是浮点数精度。财务数据带小数时,累积计算会出现0.1 + 0.2 = 0.30000000000000004这种情况,导致占位柱高度有微小偏差,柱子之间出现肉眼可见的缝隙。解决办法是在累积时做精度处理,比如统一乘以 100 转成整数运算,或者用toFixed保留两位再转回数字。我现在的习惯是数据进来先做一次精度归一,后面全程用整数或定点数。

第二个坑是空数据和零值。某一项变化量为 0 时,可见柱高度为 0,ECharts 不画柱子,但标签还在,会出现一个孤零零的标签悬在空中。处理办法是在 formatter 里判断,值为 0 时不显示标签,或者给零值柱一个最小高度(比如 1px)保证可见。另外数据为空数组时,整个图会报错,得在渲染前做空值判断,给个占位提示。

第三个坑是响应式下的标签重叠。窄屏时柱子挤在一起,标签互相压。我的处理是监听容器 resize,宽度小于某个阈值时把标签旋转 45 度或者干脆隐藏,只保留 tooltip。ECharts 的label.rotate支持动态设置,配合media查询能做出不错的响应式效果。

注意:精度问题在财务场景是硬伤,差一分钱都可能被业务方质疑。别嫌麻烦,数据入口处就做归一,比事后在图上看出来再回头查强得多。

5.3 验收前必做的五项检查

图配完,别急着交付,按这五项过一遍:累积终值是否等于末根汇总柱、正负颜色是否符合业务惯例、汇总柱是否从零起画、tooltip 是否只显示有效信息、窄屏下标签是否重叠。这五项过了,基本就是一张合格的瀑布图。我还会额外看一眼连接线有没有穿过柱子,markLine 的坐标算错时线会乱穿,视觉上很别扭。

6. 一些配置之外的思考

瀑布图这个图表,技术实现其实不难,难的是把业务逻辑翻译成正确的数据结构和视觉语言。我见过不少项目,图配得很漂亮,但基准算错了,整张图传达的信息是错的,这比不画还糟糕。所以每次做这类"有逻辑关系"的图表,我都会先把数据校验跑通,再谈样式。

另外一点体会是,ECharts 的灵活性是把双刃剑。它没给你现成的瀑布图,逼着你去理解堆叠柱的原理,理解透了之后,类似的"悬浮柱"需求——比如甘特图、区间柱状图——都能用同一套思路搞定。这种底层理解带来的迁移能力,比记住某个配置项值钱得多。

最后分享一个小技巧:调试瀑布图时,先把占位柱的color临时设成半透明灰色,肉眼确认每根占位柱的高度对不对,确认无误再改回透明。这个笨办法帮我省了大量排查时间,尤其是数据项多、累积逻辑复杂的时候,比盯着数字算快多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询