ECharts双Y轴柱状图实战:左右共用X轴的配置与踩坑指南
做数据可视化这些年“左右柱状图公用一个X轴”这个需求我见过的次数多到数不清。业务方的表述五花八门有人直接说“做双纵轴”有人说“左边放人数右边放比例”也有人丢过来一张竞品截图让我照着做。但归根结底他们想要的东西高度一致在同一个坐标系里左右两侧各自保留一套Y轴刻度而X轴只有一根让两组量纲差异很大的柱子在同一个图里完成对比。这个需求在ECharts里实现起来并不复杂但踩坑的地方非常多。很多人做完之后发现两组柱子错位、两边刻度对不上、或者柱子叠在一起根本分不开原因往往是对几个关键配置项理解得不够透彻。这篇文章我把常见的三种形态、核心配置逻辑、完整可运行的代码以及我实际调试中遇到的各种问题全部整理出来希望能帮你少走弯路。1. 先搞清楚你要的到底是哪种“左右共用X轴”左右共用X轴这个说法本身比较模糊实际场景里至少有三种不同的形态。如果一开始不把需求分清楚后面配置起来很容易南辕北辙。1.1 双Y轴柱状图最经典的“左右开弓”这是最常见的一种图表的底部是一条完整的分类X轴左侧Y轴服务于一组数据右侧Y轴服务于另一组数据。两组柱子并排出现在同一个坐标系里。这种形态适合什么场景典型的就是“量率”的组合。比如运营报表里左侧Y轴是某个活动的报名人数单位是“人”数值可能从几百到几千右侧Y轴是转化率单位是“%”数值只有个位数到两位数。这两个指标量纲完全不同如果共用一条Y轴转化率那条曲线或者柱子会被压成一条贴着X轴的线什么信息都看不出来。把两个Y轴分开之后各自都能获得完整的表达空间。我在实际项目里见过很多人把这种图做成“左柱右线”也就是左边柱状图、右边折线图。但往往业务方就是要求两侧都是柱状图这种场景在ECharts里同样支持只是柱子的间距和宽度需要额外处理否则两组柱子会叠成一团这就是后面要重点说的部分。1.2 对称蝴蝶图中间轴线左右对比很多人容易忽略第二种形态最终呈现的效果是中间一条纵向的分类轴左侧向左边延伸一组柱子右侧向右边延伸一组柱子共用同一个分类维度。严格来说这种图用的不是“一个X轴”而是左右各一条数值X轴中间共用一条分类Y轴。但从视觉结果上来讲它表达的正是“左右两套数据共用一组分类刻度”业务方往往也会把它描述成“左右柱状图公用X轴”。典型的场景就是网络热搜里提到的“使用柱状图展示每门课程男女选修人数”——每门课是一个分类左边画男生人数右边画女生人数一左一右形成强烈的视觉对比。这种图在人口结构分析、竞品对比、用户画像之类的场景里非常常用。它的优势是直观单个分类内部左右对比一目了然多个分类之间也能快速扫描出整体差异。1.3 正负分列柱状图X轴在中间的变体第三种形态在金融和财务场景里比较常见X轴的值域被设置成对称的负值和正值分类轴居中负值柱子向左延伸正值柱子向右延伸。视觉上同样是“左右两侧共用一条X轴”。比如展示某产品线一周的收支情况正数代表盈利负数代表亏损零线居中左右一眼就能看出哪几天赚、哪几天亏。这种图在ECharts里通常只需要一个X轴把它的min、max设置成关于0对称的值再加一个axisLine的偏移就行。它跟前两种的配置思路不太一样但同样属于“左右共用X轴”的需求范畴所以放在一起梳理。2. 核心配置拆解ECharts里到底动了哪些参数不管是做双Y轴还是做蝴蝶图ECharts的配置核心都围绕几个关键项展开。理解这些配置项的设计思路比死记硬背代码有用得多。2.1 yAxis数组与position属性一左一右怎么设ECharts的yAxis配置项本身支持传入一个数组。数组里的第一项默认对应左侧Y轴第二项默认对应右侧Y轴。但实际开发中我强烈建议不要依赖默认行为而是显式地把position写清楚yAxis: [ { type: value, name: 报名人数人, position: left }, { type: value, name: 完课率%, position: right } ]这样写的好处是代码的可读性更强后期别人接手维护时一眼就能看出哪条轴对应哪边。另外一个容易被忽略的点是左右两侧Y轴的name最好带上单位否则看图的人很难判断数值代表的到底是什么量级。如果你想让右侧Y轴的刻度线不显示可以设置splitLine: { show: false }这样网格线只保留左侧的一套页面看起来更清爽。但这里有个取舍如果两侧都显示网格线用户读图时会分不清某个网格线到底对应左轴还是右轴反而不利于阅读。2.2 xAxis与grid公用X轴的对齐细节公用X轴的本质其实是让你的xAxis只有一个并且两组series都引用同一个xAxis。如果写了两组独立的xAxis但没有正确关联柱子的位置就会错乱。grid的配置在双轴场景里特别容易被忽视。grid控制的是绘图区域在整个图表容器中的位置左右两侧如果有Y轴弹性grid.left和grid.right就需要留出足够的空间来容纳Y轴刻度标签。我的一种习惯做法是grid: { left: 12%, right: 12%, bottom: 12%, top: 10%, containLabel: true }这里有个细节值得展开讲讲containLabel为true时left和right的值会把坐标轴刻度的标签面积也算进去能有效避免“Y轴标签太长被截断”的问题。但如果你的X轴分类名称特别长containLabel反而会导致绘图区域被挤压得过小这时候需要手动调整left和right的百分比而不是一味迷信containLabel。2.3 series里的yAxisIndex柱子到底归谁管双轴配置的核心秘密就在yAxisIndex这个属性上。每个series相当于一个数据系列你需要告诉它“你的数据挂在第几个Y轴上”。series: [ { name: 报名人数, type: bar, yAxisIndex: 0, data: [3200, 2100, 2800, 1800, 2400, 3650, 3100] }, { name: 完课率, type: bar, yAxisIndex: 1, data: [35, 62, 48, 71, 56, 29, 41] } ]yAxisIndex: 0代表挂在第一个Y轴左侧yAxisIndex: 1代表挂在第二个Y轴右侧。实际开发中我见过不少“轴出来了但柱子不对”的情况最后排查下来都是因为忘了写yAxisIndex导致两组柱子全部默认画在左轴上右轴刻度却空空如也数值完全对不上号。xAxisIndex也是一样的原理在蝴蝶图这种双X轴场景里你需要给每侧柱子指定xAxisIndex: 0或xAxisIndex: 1否则柱子会全部跑向同一个X轴。3. 完整实操从零写一个可复用的双轴柱状图说再多原理不如直接跑一遍完整代码。我拿一个真实的业务场景来演示。3.1 数据准备与场景设定假设你要给一个在线课程平台做周报。你需要在一张图里同时展示“每天的课程报名人数”和“每天的完课率”。报名人数是绝对值单位是“人”数据在2000到4000之间波动完课率是比率单位是“%”数据在30%到80%之间波动。如果共用一个Y轴完课率那组柱子会因为数值过小变成一条紧贴X轴的细线完全没法看。所以必须拆成左右两套Y轴。我从ECharts官网示例和多年项目实践中总结出来的最佳配置如下option { grid: { left: 12%, right: 14%, bottom: 12%, top: 10%, containLabel: true }, xAxis: { type: category, data: [周一, 周二, 周三, 周四, 周五, 周六, 周日], axisLabel: { interval: 0, rotate: 0 } }, yAxis: [ { type: value, name: 报名人数人, position: left, min: 0, max: 4000, splitLine: { show: true } }, { type: value, name: 完课率%, position: right, min: 0, max: 100, splitLine: { show: false } } ], series: [ { name: 报名人数, type: bar, yAxisIndex: 0, barWidth: 24, itemStyle: { color: #3b82f6 }, data: [3200, 2100, 2800, 1800, 2400, 3650, 3100] }, { name: 完课率, type: bar, yAxisIndex: 1, barWidth: 24, itemStyle: { color: #f59e0b }, data: [35, 62, 48, 71, 56, 29, 41] } ] };这段配置直接丢进ECharts就能渲染出一个可用的双轴柱状图。关键的几个参数我解释一下第一yAxisIndex必须显式标注这是两组柱子能正确挂到左右两轴的前提。第二barWidth把两组柱子的宽度固定为一样的24像素这样周一和周二的两个柱子并排时不会出现“左柱宽、右柱窄”的视觉错位。如果想让柱子之间留更多空隙可以调整barCategoryGap也就是同分类下柱组的间距。第三右侧Y轴我关闭了splitLine只保留左侧的网格线这样背景不会太乱。3.2 五步完成核心配置如果你跟我一样习惯从零开始搭建图表可以按下面这五个步骤来执行每一步都有明确的目的先搭grid确定绘图区域在容器中的位置留出左右两侧放Y轴的空间。再配xAxis设置type: category以及分类数据这一步决定了所有柱子共用的水平刻度。然后配yAxis数组第一项position: left第二项position: right分别设置好name、min、max。接着写series两组数据都设置为type: bar分别通过yAxisIndex指定挂载的Y轴。最后调整细节barWidth、barGap、itemStyle.color、tooltip等美化项。这个顺序是我自己调试时的固定套路。很多新手喜欢先把所有配置写完再统一调样式结果出了问题不知道是轴没配对还是数据没写好。按步骤来每一步都可以单独验证。3.3 对称蝴蝶图的变体实现说完双Y轴柱状图再来看蝴蝶图。热搜词里“使用柱状图展示每门课程男女选修人数”这个场景就是蝴蝶图的典型应用。我直接给出可运行的核心代码。假设有4门课每门课有男生和女生的选修人数option { tooltip: { trigger: axis, axisPointer: { type: shadow } }, legend: { data: [男生, 女生], top: 5% }, grid: { left: 18%, right: 18%, top: 15%, bottom: 10%, containLabel: true }, xAxis: [ { type: value, inverse: true, name: 男生人, axisLabel: { formatter: function (value) { return Math.abs(value); } } }, { type: value, name: 女生人 } ], yAxis: { type: category, data: [高等数学, 大学英语, 程序设计, 线性代数], axisTick: { show: false } }, series: [ { name: 男生, type: bar, xAxisIndex: 0, data: [320, 210, 480, 260], itemStyle: { color: #3b82f6 } }, { name: 女生, type: bar, xAxisIndex: 1, data: [280, 390, 220, 410], itemStyle: { color: #f59e0b } } ] };这里最核心的一点是左侧X轴的inverse: true它会将数值轴的方向反转让柱子向左侧延伸。同时我在axisLabel里用formatter取了绝对值因为如果数据本身就是正数加了inverse之后刻度标签会变成负数视觉上容易引起误解。如果你不需要“男女”这种带零轴的对比也可以把左轴改成从0到最大值、右轴改成从0到最大值这样两边柱子都会向外延伸。但那条中间的分类轴必须保留这就是蝴蝶图的骨架。3.4 进阶美化渐变、图片、悬浮提示基础图表完成了但直接拿去演示往往不够好看。这里分享几个我常用的美化技巧都是ECharts里很成熟的方案。第一个是渐变色。柱状图想要带点质感可以用LinearGradientitemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #2f6fed } ]) }这里四个参数依次是x1、y1、x2、y2分别代表渐变起止点。(0, 0, 0, 1)表示从上到下的垂直渐变(0, 0, 1, 0)则是从左到右的水平渐变。如果想让渐变跟随柱子的高度变化甚至可以根据数据值动态生成渐变方向这在做排行榜类图表时效果好到不讲道理。第二个是自定义图片。热搜词里有人问“ECharts柱状图柱子可以用自定义图片显示不”答案是肯定的。ECharts的pictorialBar系列类型就是专门干这个的series: [ { name: 报名人数, type: pictorialBar, symbol: image://https://example.com/icon.png, symbolSize: [24, 40], data: [3200, 2100, 2800, 1800, 2400] } ]symbol可以传图片路径格式是image://加上URL。也可以用矢量路径path://好处是不会失真、加载更快。做成类似“3D柱状图”的视觉感时很多人会把柱子底部加一层半透明的底座再用pictorialBar叠加一个高光图标画面立刻高级不少。第三个是悬浮提示。双轴图表里tooltip的配置非常重要。默认的trigger: item是鼠标悬停到某个柱子上才显示该柱子的数据但双轴场景下我建议用trigger: axis配合axisPointer这样鼠标在某个X轴分类附近移动时会把该分类下左右两组柱子都标记出来tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: function (params) { let res params[0].name br/; params.forEach(function (item) { res item.marker item.seriesName item.value; if (item.seriesName 完课率) { res %; } res br/; }); return res; } }这里formatter是自定义函数可以在里面加上单位、千分位、颜色标记等。很多人觉得tooltip无非就是个悬浮框但实际业务里用户看图时第一接触点就是tooltip它的可读性直接决定了图表好不好用。4. 常见问题与排查技巧实录写再漂亮的配置也免不了遇到“渲染出来不对劲”的时候。我把这些年排查双轴柱状图问题的心得整理成一张速查表再挑几个高频问题细讲。4.1 经典问题排查速查表很多问题都是重复出现的有一个速查表可以节省大量调试时间。以下是我最常遇到的几类问题和对应解法现象可能原因处理方法两组柱子完全叠在一起barGap未设置且两组数据共用同一分类轴在series中设置barGap: 50%或调整barWidth柱子不在对应轴上yAxisIndex缺失或写错检查每个series的yAxisIndex确保左右数据分别指向0和1右侧Y轴刻度存在但无柱数据挂在左轴右侧无对应series在第二组series中显式指定yAxisIndex: 1左侧柱子显示为负数蝴蝶图X轴inverse: true导致标签变负用axisLabel.formatter取绝对值或改造数据柱子被容器截断grid.left或grid.right过小增加左右百分比或开启containLabel: trueX轴刻度文字重叠X轴分类名称过长设置axisLabel.interval: 0配合rotate: 30或rotate: 45柱子太宽/太窄barWidth未设置浏览器自适应导致固定barWidth或在barCategoryGap里设置百分比这张表是我在实际项目中不断整理出来的。每次遇到新问题我都会顺手填进去现在已经成了团队内部的小手册。4.2 柱子错位、重叠、对不齐的原因与解决柱子重叠的原因通常有两个。一是两组柱子都画在了同一个Y轴索引上这种情况最常见于从单轴图表复制过来后忘记改yAxisIndex。另一个原因是barGap的设置。ECharts里同一分类下多组柱子的间距由barGap控制。当barGap为-100%时柱子会完全重叠这在做“正负对比图”时是有意为之——正负柱子背靠背重合。但如果只是想并排展示默认正值的barGap就能分开。我一般习惯设置barWidth: 24并保留默认的barGap这样视觉上最稳定。还有一个隐蔽的对不齐问题当你为barWidth设置了固定像素值但图表容器宽度变化时柱子并不会等比缩放于是出现“容器拉宽了柱子还是那么宽”的情况。如果希望柱子跟随容器自适应建议不要设置固定barWidth而是用barCategoryGap: 30%这种方式按比例控制。4.3 刻度不齐、比例失真的处理左右两个Y轴的min和max如果不合理图表会传达错误的视觉信息。举一个真实的例子左轴报名人数范围是0到4000右轴完课率范围是0到100那么在4000这个位置对应的完课率是100%柱子的高度比例恰好能反映“完课率满格为100%”。但如果把右轴范围写成0到50完课率40%的柱子就会冲到80%的位置虽然数据没变但视觉上会让人以为完课率很高这就叫比例失真。对于双轴柱状图我处理比例的原则是除非有明确的业务约定否则左右两轴都从0开始。如果想让两边的柱子“高度趋势可对比”可以把两轴的max设置为一个大致等比例的值。但说句实在话双轴图的本质就是“各画各的”跨轴直接对比柱子高度本来就是伪命题别过度纠结。如果需要控制X轴显示范围也就是热搜词里提到的“x轴范围修改”可以在xAxis里设置min和max。但因为X轴通常是category类型直接写min: 2、max: 5可能会跟你预期不符更稳妥的方案是用dataZoom来控制可视窗口。尤其是数据量很大的时候dataZoom还能提供拖拽缩放交互一图多用。4.4 鼠标悬浮“点哪儿显示哪儿”的交互细节“鼠标点那儿在哪儿显示柱状图”这个需求最常规的理解是tooltip随鼠标移动而显示。实际上ECharts的默认交互已经支持鼠标悬浮。但很多人反馈“鼠标移过去没有提示”多半是tooltip没配置或者没有任何可用于提示的数据点。如果你的意图是实现“点击某个位置动态生成柱子”那就需要用到chart.on(click)事件和setOption动态更新数据。比如在营销看板里点击左侧某根柱子右侧就联动展示该分类的明细数据这种交互在数据大屏里非常讨喜。我自己处理这类交互时的一个习惯是在事件回调里通过params.componentIndex和params.dataIndex定位到用户点击的是哪个系列、哪个分类然后更新图表数据myChart.on(click, function (params) { if (params.seriesName 报名人数) { // 根据params.dataIndex更新右侧为您想要的柱子数据 myChart.setOption({ series: [ {}, { data: newData[params.dataIndex] } ] }); } });注意setOption时第二个系列如果需要保留原有配置可以传一个空对象这样ECharts会合并而不是覆盖整个系列。5. 不止EChartsOrigin和其他工具中的类似处理很多非前端岗位的同学拿到这个需求时用的不是ECharts而是Origin、Excel之类的桌面工具。热搜词里恰好有一个“origin柱状图叠在一起怎么分开”这里一起说一下。5.1 Origin中柱状图重叠分离的三种方式Origin里出现“柱子叠在一起”的现象通常是绘图类型误选了“堆叠柱状图”。选中数据后默认绘图类型如果选择的是“Column”每一列数据会作为一个独立的柱子并列显示但如果选择的是“Stack”类型的Column所有列就会堆叠在同一根柱子上。要把堆叠改成并列可以在图层内容Layer Contents里查看当前图形类型然后把堆叠模式切换回“Independent”或者重新用“Plot: Column”来绘制。如果不是堆叠而是两根柱子距离太近导致看起来像叠在一起可以在图层面板里双击柱子区域打开Plot Details对话框在“Spacing”选项卡中调整“Gap Between Bars”把间隙比例调大。同时检查“Overlap”这个选项是否为0或正数如果是负数柱子之间会产生反向叠加这也会造成重叠的视觉效果。如果你想把多列数据做成分组柱状图也不是难事。Origin的“Group”选项卡里勾选“Independent”或“Incremental”让每组柱子使用独立的颜色和间距就能得到标准的并列柱状图。核心思路跟ECharts里的barGap是相通的都在控制柱子之间的间距逻辑。5.2 从双轴柱状图到组合型大屏图表的思路延展使用ECharts做数据可视化很少有人只用一个图表完事。热搜词里提到的“图表包含折线图柱状图雷达图”就是典型的复合图表需求。在双轴柱状图的基础上你完全可以把右侧的柱子换成折线形成“柱线组合图”也可以在同一个页面里放多个图表中间用事件联动串起来。这里分享一个我提了很多次的建议在做任何图表前先明确“这张图要回答什么问题”再去想“用哪种图表类型”。双轴柱状图解决的是“量纲不同但又要同框对比”的问题折线图解决的是“趋势变化”的问题雷达图解决的是“多维度能力评估”的问题。如果数据和问题不匹配就算代码写得再花哨图做得再漂亮也没法传达有效信息。结尾我在做这类图表时感受最深的一点是ECharts本身不复杂难的是把“业务需求”翻译成“配置参数”的这一跳。左右共用一个X轴听起来只是个简单的坐标轴问题但真正上手做的时候你会遇到yAxisIndex、barGap、containLabel、蝴蝶图反转、tooltip联动等一系列细节任何一个环节出问题最终呈现的效果都会大打折扣。如果你也是第一次做双轴柱状图我给的建议是先不要急着把代码堆出来花两分钟在白纸上画一下你要的最终效果左边轴的单位是什么、右边轴的单位是什么、柱子的颜色怎么区分、鼠标悬浮时想显示什么内容。想清楚这一层再回到ECharts里配置你会发现很多东西是水到渠成的。最后再送一个小技巧调试双轴图表时记得先把两组数据改成差异明显的值比如左边都是几千、右边都是几十这样一眼就能看出轴和数据是否匹配上了。