1. 为什么要在同一个图表里塞柱状图和折线图
做过数据看板的人大概率都遇到过这种需求:老板或者产品经理指着屏幕说,“这个销售额用柱子表示,增长率用线画出来,放一张图里,我要一眼看明白”。这不是炫技,这是真实业务场景里最高频的图表诉求之一。柱状图负责表达绝对量,比如每月营收、每日订单数、各渠道用户量;折线图负责表达趋势和比率,比如同比增长率、转化率、环比变化。两者叠加,读者既能看到“盘子有多大”,又能看到“跑得快不快”。
ECharts 作为国内数据可视化领域使用最广的图表库之一,天然支持这种组合图表。它的核心机制在于series数组里可以混合声明不同类型的系列,再配合双 Y 轴(yAxis数组)来解决量纲不一致的问题。听起来简单,但实际操作中坑不少:柱子和折线对不齐、双轴刻度错位、图例点击后布局塌陷、数据为空时折线断掉……这些问题官方文档不会逐一告诉你,只有真正在项目里踩过才知道。
这篇文章适合三类人看:第一类是对 ECharts 有基本了解、能画出单一柱状图或折线图,但没做过组合图的开发者;第二类是做过组合图但总觉得“哪里不对劲”,想搞清楚双轴对齐、数据缩放这些细节的人;第三类是需要在真实业务看板里快速落地、不想反复试错的人。我会从设计思路讲到参数配置,再到完整可运行的代码和排查经验,尽量让你看完就能直接抄到项目里用。
需要提前说明的是,下面涉及的具体参数值、配色方案、刻度策略,一部分来自官方文档的明确说明,另一部分是我在多个数据看板项目中总结出来的常见实践。不同业务场景下最优解可能不同,但底层逻辑是通用的。
2. 组合图表的核心设计思路拆解
2.1 单轴还是双轴:先想清楚量纲问题
很多人第一次做组合图,最容易犯的错误就是把柱状图和折线图塞进同一个 Y 轴。结果就是:如果柱子数值是几万,折线数值是百分之几,折线会被压成贴着 X 轴的一条直线,完全看不出趋势。反过来,如果折线数值大、柱子数值小,柱子就矮得看不见。
所以第一件事是判断两个系列的量纲是否一致。判断方法很简单:把两组数据的最大值和最小值分别列出来,如果数量级差距超过一个数量级(比如一组是千级、一组是百分数),那就必须用双 Y 轴。如果两组数据量纲接近,比如都是金额、都是人数,那单轴反而更直观,因为读者不需要在左右两个刻度之间来回换算。
双轴的本质是给两组数据各自一套独立的刻度尺。ECharts 里通过yAxis配置成数组来实现,第一个元素是左轴,第二个元素是右轴。每个 series 通过yAxisIndex指定自己绑定哪根轴。这个设计很灵活,但代价是:两根轴的刻度线如果不对齐,视觉上会非常别扭。后面我会专门讲怎么处理这个问题。
2.2 series 混合声明的底层逻辑
ECharts 的series是一个数组,每个元素代表一个数据系列。数组里元素的顺序决定了绘制层级:排在后面的系列会覆盖在前面的系列之上。这一点在组合图里特别重要——通常我们希望折线压在柱子上面,所以折线系列的type: 'line'应该放在柱状图type: 'bar'之后。
每个系列除了type之外,还有几个关键属性需要关注。name用于图例显示和 tooltip 标识;data是数据数组;yAxisIndex决定绑定哪根 Y 轴;itemStyle控制颜色和样式;smooth控制折线是否平滑。这些属性单独看都很简单,但组合在一起时,优先级和相互影响需要理清楚。比如yAxisIndex如果不写,默认是 0,也就是左轴,很多人忘了给折线指定yAxisIndex: 1,结果折线还是被压在左轴上,白折腾半天。
2.3 视觉层级与信息密度的平衡
一张图里同时有柱子和折线,信息密度天然就比单一图表高。如果不做视觉分层,读者会不知道先看什么。我的经验是:柱子用低饱和度的填充色,折线用高饱和度的线条色,并且折线加粗一点。这样视觉上柱子是“背景板”,折线是“主角”,符合“先看趋势、再看量”的阅读习惯。
另外,柱子的宽度和折线的点大小也要协调。柱子太宽会显得笨重,太窄又像牙签。折线的数据点标记(symbol)如果每个点都显示,在数据量大时会很乱,通常只在数据点较少(比如少于 12 个)时才显示标记,否则只保留线条。这些细节没有标准答案,但有一个原则:让读者在 3 秒内抓住你想表达的核心信息。
3. 核心配置项逐一拆解与实操要点
3.1 yAxis 双轴配置的关键参数
双 Y 轴是组合图的骨架。下面是一个典型的双轴配置结构:
yAxis: [ { type: 'value', name: '销售额(万元)', position: 'left', axisLabel: { formatter: '{value}' }, splitLine: { show: true, lineStyle: { type: 'dashed', color: '#e0e0e0' } } }, { type: 'value', name: '增长率', position: 'right', axisLabel: { formatter: '{value}%' }, splitLine: { show: false } } ]这里有几个关键决策点。第一,position必须明确指定 left 和 right,否则两根轴可能重叠。第二,splitLine只在一根轴上显示,通常是左轴,因为两根轴的网格线如果都显示,会形成密集的横线,视觉干扰很大。第三,axisLabel.formatter要根据数据含义加上单位,百分比加%,金额加万或元,这样读者不用猜。
还有一个容易被忽略的点:两根轴的min和max如果不手动设置,ECharts 会自动计算,但自动计算的结果可能导致两根轴的零刻度不在同一水平线上。比如左轴从 0 开始,右轴从 -10 开始,那柱子的底部和折线的起点就会错位。解决办法是给两根轴都设置min: 0(如果数据都是正数),或者手动计算一个合理的范围。
3.2 series 中柱状图与折线图的属性差异
柱状图和折线图虽然都是系列,但它们的属性侧重点完全不同。柱状图关注的是barWidth、barGap、itemStyle.color;折线图关注的是smooth、symbol、lineStyle.width。
柱子的barWidth我一般设置为'40%'到'60%'之间。太宽会显得笨重,太窄在数据点多的时候会看不清。如果只有一组柱子,barWidth可以大一点;如果有多组柱子并列,就要小一点,给彼此留出间隙。barGap控制同类别下不同柱子之间的间距,默认是'30%',通常不需要改。
折线的smooth属性我建议设为true,除非数据本身波动非常剧烈、需要强调每一个拐点。平滑曲线在视觉上更柔和,适合表达趋势。但要注意,平滑曲线在数据点很少时可能产生“过冲”现象,也就是曲线在两点之间鼓出去,看起来像是数据超出了实际范围。如果数据点少于 5 个,建议关掉smooth。
symbol控制数据点的标记形状,默认是'circle'。如果数据点超过 15 个,建议设为'none',只保留线条,否则标记会挤成一团。lineStyle.width我一般设为 2 到 3,比默认的 2 稍粗一点,让折线在柱子背景上更突出。
3.3 tooltip 与 legend 的联动配置
组合图的 tooltip 需要同时显示柱子和折线的数值,并且要带上单位。ECharts 的tooltip.trigger设为'axis'时,会同时显示该轴上所有系列的数据。但默认的格式可能不够清晰,建议用formatter自定义:
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: function(params) { let result = params[0].axisValue + '<br/>'; params.forEach(function(item) { let unit = item.seriesName === '增长率' ? '%' : '万元'; result += item.marker + item.seriesName + ':' + item.value + unit + '<br/>'; }); return result; } }axisPointer.type设为'shadow'时,鼠标悬停会显示一个阴影带,同时覆盖柱子和折线,视觉上更统一。如果设为'line',则只显示一条竖线,适合折线为主的场景。
legend 的配置相对简单,但有一个坑:当图例被点击隐藏某个系列时,双轴的刻度不会自动调整。比如隐藏了柱子系列,左轴的最大值还是按柱子的数据算的,导致折线被压扁。解决办法是监听legendselectchanged事件,动态调整轴的max值。这个后面在排查技巧里会详细讲。
4. 完整实操流程与可复现代码
4.1 数据准备与结构设计
假设我们要做一个“月度销售与增长率”的看板,数据如下:
| 月份 | 销售额(万元) | 增长率 |
|---|---|---|
| 1月 | 320 | 12.5% |
| 2月 | 280 | -8.3% |
| 3月 | 410 | 18.7% |
| 4月 | 390 | 6.2% |
| 5月 | 520 | 15.4% |
| 6月 | 480 | -3.1% |
数据结构上,X 轴用月份数组,两个系列各自用数值数组。注意增长率有负数,所以右轴的min不能设为 0,需要让 ECharts 自动计算或者手动设一个负值下限。
4.2 完整配置代码与逐段注释
下面是一份可以直接复制到 ECharts 在线编辑器或项目里运行的完整配置:
option = { // 标题配置 title: { text: '月度销售与增长率', subtext: '数据来源:模拟数据', left: 'center' }, // 提示框配置 tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: function(params) { let result = params[0].axisValue + '<br/>'; params.forEach(function(item) { let unit = item.seriesName === '增长率' ? '%' : '万元'; result += item.marker + item.seriesName + ':' + item.value + unit + '<br/>'; }); return result; } }, // 图例配置 legend: { data: ['销售额', '增长率'], top: 30 }, // 网格配置,留出空间给双轴名称 grid: { left: '10%', right: '10%', bottom: '10%', containLabel: true }, // X轴配置 xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月'], axisTick: { alignWithLabel: true } }, // 双Y轴配置 yAxis: [ { type: 'value', name: '销售额(万元)', position: 'left', min: 0, axisLabel: { formatter: '{value}' }, splitLine: { show: true, lineStyle: { type: 'dashed', color: '#e0e0e0' } } }, { type: 'value', name: '增长率', position: 'right', axisLabel: { formatter: '{value}%' }, splitLine: { show: false } } ], // 系列配置 series: [ { name: '销售额', type: 'bar', yAxisIndex: 0, data: [320, 280, 410, 390, 520, 480], barWidth: '45%', itemStyle: { color: '#7cb5ec', borderRadius: [4, 4, 0, 0] } }, { name: '增长率', type: 'line', yAxisIndex: 1, data: [12.5, -8.3, 18.7, 6.2, 15.4, -3.1], smooth: true, symbol: 'circle', symbolSize: 8, lineStyle: { width: 3, color: '#f7a35c' }, itemStyle: { color: '#f7a35c' } } ] };这段代码里,我特意给柱子加了borderRadius: [4, 4, 0, 0],让柱子顶部有圆角,视觉上更现代。折线的symbolSize设为 8,比默认的 4 大一点,在柱子背景上更显眼。这些细节看似微小,但实际效果差别很大。
4.3 参数计算:如何让双轴刻度对齐
双轴刻度对齐是组合图里最考验经验的地方。ECharts 默认会为每根轴独立计算刻度,导致两根轴的网格线不在同一水平线上。虽然splitLine只显示一根轴的,但读者会下意识地用左轴的刻度去估算右轴的值,如果不对齐就会产生误读。
一个实用的技巧是:手动设置两根轴的interval,让它们的刻度数量一致。比如左轴范围是 0 到 600,分成 6 段,每段 100;右轴范围是 -10 到 20,分成 6 段,每段 5。这样两根轴的刻度线就能大致对齐。
计算方法是:先确定左轴的最大值maxLeft和分段数n,则intervalLeft = maxLeft / n。右轴同理。分段数一般取 4 到 6 之间,太少显得粗糙,太多显得密集。如果数据范围不规整,可以手动把max向上取整到一个方便的数,比如 520 取 600,-8.3 取 -10。
yAxis: [ { type: 'value', name: '销售额(万元)', min: 0, max: 600, interval: 100, // ...其他配置 }, { type: 'value', name: '增长率', min: -10, max: 20, interval: 5, // ...其他配置 } ]这样配置后,两根轴各有 6 个刻度区间,网格线在视觉上是对齐的。实测下来,这种手动对齐的方式比完全依赖自动计算要稳定得多,尤其是在数据动态变化的看板里。
5. 常见问题与排查技巧实录
5.1 折线被压扁或柱子看不见
这是最常见的问题,根本原因通常是yAxisIndex没指定,或者双轴的范围设置不合理。排查步骤:
- 检查折线系列的
yAxisIndex是否为 1,柱状图是否为 0。 - 检查右轴的
min和max是否覆盖了折线数据的范围。如果折线数据有负数,右轴min不能是 0。 - 如果柱子看不见,检查左轴的
max是否远大于柱子数据,导致柱子相对太矮。
下面这张速查表可以帮你快速定位:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 折线贴底 | 折线绑定了左轴 | 设置yAxisIndex: 1 |
| 柱子太矮 | 左轴 max 过大 | 调整左轴 max 或改用双轴 |
| 折线超出顶部 | 右轴 max 过小 | 增大右轴 max 或设为自动 |
| 双轴刻度错位 | 未手动设置 interval | 手动设置两根轴的 interval |
| 图例隐藏后布局塌陷 | 轴范围未动态调整 | 监听 legendselectchanged 事件 |
5.2 数据为空或全为 null 时的显示异常
真实项目里,数据接口返回空数组或者全是 null 的情况很常见。ECharts 默认会把 null 当作断点,折线会断开,柱子会消失。如果希望 null 显示为 0,需要在数据预处理阶段把 null 替换成 0。如果希望折线断开而不是连起来,可以设置connectNulls: false(默认就是 false)。
但有一个坑:如果整个系列的数据都是 null,ECharts 可能会报错或者不渲染。解决办法是在渲染前判断,如果数据全为空,就给一个空数组或者显示“暂无数据”的提示层。这个逻辑最好在业务代码里处理,不要指望 ECharts 自动兜底。
5.3 图例点击后的动态轴调整
前面提到,隐藏某个系列后轴范围不会自动调整。解决方法是监听legendselectchanged事件:
myChart.on('legendselectchanged', function(params) { let selected = params.selected; let newMax = 0; if (selected['销售额']) { newMax = Math.max(...salesData); } myChart.setOption({ yAxis: [ { max: newMax > 0 ? Math.ceil(newMax * 1.2) : 100 }, {} ] }); });这段代码的逻辑是:如果销售额系列被隐藏,就把左轴的最大值设为一个默认值;如果显示,就按数据最大值的 1.2 倍来设置,留出一点顶部空间。实测下来,这个交互细节能显著提升看板的专业感。
5.4 移动端适配的注意事项
在移动端,双轴图表的空间非常紧张。左右两根轴的名称和刻度会占用大量横向空间,导致绘图区域被压缩。我的经验是:移动端优先考虑单轴方案,如果必须双轴,就把轴名称去掉,只保留刻度,并且把刻度字体调小。另外,grid的left和right要适当减小,给绘图区留出更多空间。
还有一个细节:移动端的 tooltip 触发方式建议用trigger: 'axis'配合axisPointer.type: 'line',因为手指点击不如鼠标精确,阴影带在移动端容易误触。
6. 进阶玩法与性能优化建议
6.1 多组柱状图叠加折线的布局策略
有时候业务需要展示多组柱子,比如“线上销售额”和“线下销售额”两组柱子,再加一条“总增长率”折线。这时候柱子的barWidth要调小,barGap也要调整,否则两组柱子会挤在一起。折线的yAxisIndex依然指向右轴,但要注意折线的数据点位置默认是在类目中间,如果两组柱子并列,折线点可能会偏左或偏右。可以通过xAxis的boundaryGap和系列的barGap配合微调。
6.2 大数据量下的渲染性能
当数据点超过 500 个时,柱状图和折线图的渲染会明显变慢。优化手段包括:关闭动画(animation: false)、减少symbol显示、使用large: true开启大数据模式(柱状图支持)、对数据进行采样或聚合。如果数据量真的很大,建议改用dataZoom让用户自己选择查看范围,而不是一次性渲染全部数据。
6.3 主题与配色的统一管理
组合图的配色最忌讳柱子和折线颜色太接近,或者对比度过高显得刺眼。我的做法是:柱子用冷色调的低饱和度色,折线用暖色调的高饱和度色,形成冷暖对比。如果项目有统一的设计规范,就把颜色提取成变量,方便全局替换。ECharts 支持通过theme注册全局主题,适合多图表统一风格的场景。
7. 我在实际项目里踩过的坑
第一个坑是双轴的零刻度不对齐。早期做的一个看板,左轴从 0 开始,右轴自动计算后从 -5 开始,结果柱子的底部和折线的起点差了半格,视觉上非常别扭。后来养成了习惯:只要用双轴,就手动设置min,确保两根轴的零刻度在同一水平线上。
第二个坑是图例顺序和绘制顺序不一致。ECharts 的图例顺序默认跟series数组顺序一致,但绘制层级是后面的覆盖前面的。如果想让折线在图例里排在柱子前面,但绘制时又要在柱子上面,就需要调整legend.data的顺序,而不是series的顺序。这个细节官方文档没有明说,是我调试了半天才发现的。
第三个坑是数据更新时的动画抖动。看板数据定时刷新时,如果直接setOption全量更新,柱子和折线会重新播放动画,看起来像在闪。解决办法是用setOption的第二个参数notMerge: false,并且只更新series.data,不更新整个配置。这样 ECharts 会做增量更新,动画更平滑。
最后分享一个小技巧:如果折线的数据点正好落在柱子的顶部,可以把折线的z值设高一点(比如z: 10),确保折线绘制在柱子之上。虽然默认情况下折线就在柱子上面,但在某些复杂配置下可能会被覆盖,显式设置z值更保险。