我在公司接到的第一个数据可视化需求,就是画一张柱状图:把过去半年的订单量按月份展示出来。当时觉得简单,半小时后就交了第一版,结果业务方看了一眼就退了回来——柱子挤成一团,月份标签全叠在一起,悬浮框里还带着英文默认单位。也就是从那次返工开始,我才认真把ECharts柱状图从头到尾研究了一遍。
这篇文章就是基于那段时间的踩坑和后续十几个项目的经验整理的。ECharts 官方示例里柱状图看起来很简单,几行配置就能出图,但真正拿到业务里你会发现一堆细节:堆叠顺序怎么控制、双轴怎么对齐、柱子上能不能放图片、大屏上 tooltip 怎么换行、动态数据更新要注意什么。这篇文章会把柱状图从基础绘制到各种实际场景中的玩法都过一遍,适合刚接触 ECharts 的初学者,也适合已经会用基础配置但想在项目里做得更细的人参考。
1. 为什么柱状图值得单独写一篇教程
1.1 柱状图在业务场景中的地位
柱状图大概是所有可视化图表中使用频率最高的一种。电商后台看销量、运营后台看转化、监控大屏看流量,凡是“某个时间点对应的数值是多少”或者“几个分类直接比大小”的场景,第一反应基本都是柱状图。它比折线图更容易看出数值差距,比饼图更适合展示多分类和趋势变化,所以在实际项目中几乎不可能绕开。
也正因为太常见,很多人都默认它简单,真到项目里才发现不是那么回事。尤其是数据指标变多之后,柱状图很快就不只是“画几根柱子”那么简单了。你可能要同时展示两个量级完全不同的指标,可能要在柱子顶部显示汇总信息,可能还要让柱子和折线叠在同一个坐标系里。这些组合需求在官方示例里各自有一张图,但放同一个项目里时,配置会越叠越复杂。
1.2 官方示例和真实业务的差距
官方示例里的柱状图干干净净,几个柱子代表一组数据,看个大概。但真实业务里很少有这么简单的情况:要么是两个指标放一起但量级差了几十倍,要么是希望把上个月和这个月的数据做成堆叠对比,要么是需要显示百分比趋势同时还要看原始数值。这些需求在官方示例里各有一张图,但放同一个项目里时,配置会越叠越复杂,Y 轴怎么分配、图例怎么布局、悬浮框怎么展示都要重新设计。
就拿我之前做的订单数据屏来说:业务方既要看各区域销售额,又要看订单量,还要对比目标完成率。最开始我只放了一个柱状图,销售额 8000 的时候订单量才 20 多,订单量的柱子完全被压制在坐标轴底部,肉眼根本看不出区域差异。后来换成了双轴柱状图加目标进度条,才把信息完整呈现出来。这也是为什么我不建议“背配置”,而是建议先理解柱状图的数据映射逻辑。
1.3 从这篇文章能得到什么
这篇不是一份让你背配置的文档,我会照着实际项目的推进顺序来写:先跑通一个最基础的图,再一步步加需求维度,最后讲优化方案和踩坑记录。你可以按顺序读,也可以直接跳到对应的需求段落把配置抄走。文中代码基于 ECharts 5.x,早期版本的大多数配置也兼容,注意个别 API 的差异即可。
2. 从零跑通一个最简柱状图:option结构是理解一切的基础
2.1 先写一个能跑起来的最小例子
ECharts 的使用套路其实非常固定:一个 DOM 容器、一个初始化方法、一个 option 配置对象、一次 setOption。所有图表类型都逃不出这个流程,柱状图只是 option 中 series 的 type 设为 'bar'。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="main" style="width: 600px; height: 400px;"></div> <script> const chartDom = document.getElementById('main'); const myChart = echarts.init(chartDom); const option = { xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] }, yAxis: { type: 'value' }, series: [ { name: '订单量', type: 'bar', data: [120, 200, 150, 80, 170, 110, 230], itemStyle: { color: '#5470c6' } } ] }; myChart.setOption(option); </script> </body> </html>这段代码跑起来,页面里就会出现七根柱子。很多教程到这里就完了,但我想多说几句背后的结构,因为后面所有复杂配置都是在这个结构上叠加的。
2.2 直角坐标系中的柱状图是怎么回事
柱状图的本质是直角坐标系里把数值转换成高度。xAxis 配置的是类目轴,也就是柱子在水平方向上的位置;yAxis 配置的是数值轴,决定柱子有多高。series 里的 data 数组负责把数值“喂”给 y 轴,每个值和 xAxis 的 data 按索引一一对应。
这里有个容易忽略的点:x 轴和 series 的 data 是按索引对齐的,不是按值匹配。所以 xAxis.data 是 ['周一', '周二', '周三'],series.data 就必须是三项。如果顺序乱了,柱状图上显示的类别和数值就会错位,而且 ECharts 不会报错,这种错误在项目里很难发现。我建议在做动态数据时,前端就先把数组对齐好再传进去,不要依赖后端返回的字段顺序。
2.3 柱子宽度、间距和上下边界是怎么决定的
柱子的宽窄不是浏览器随机决定的,背后有一套分配规则。ECharts 会先计算类目轴的带宽(bandWidth),再按barCategoryGap决定不同类目之间的空隙比例,按barGap决定同一类目下不同系列之间的空隙比例。默认barCategoryGap是 20%,意味着类目间距占整个带宽的 20%。
实际项目里默认值经常不够用,我会直接指定:
series: [{ type: 'bar', barWidth: 20, // 直接指定柱子宽度,单位像素 barMaxWidth: 40, // 柱子宽度上限,防止数据少时柱子胖得离谱 data: [/* ... */] }]barWidth和barMaxWidth的选择取决于容器宽度和分类数量。我的经验是:分类少于 6 个时,柱子宽度设在 20-30 像素比较协调;分类超过 12 个时,不要固定 barWidth,改用 barMaxWidth 限制上限,让 ECharts 自己压缩,否则柱子之间会挤到几乎贴在一起。
2.4 轴上的文字太挤怎么办
一个最常见的返工理由就是 x 轴标签重叠。尤其是分类多、文字长的时候,默认配置几乎必挤。三个常用处理思路:
- 加间隔:
xAxis.axisLabel.interval = 0是强制全部显示,如果重叠就调大数值。 - 旋转:
xAxis.axisLabel.rotate = 30,让长标签斜着展示。 - 隐藏部分:
axisLabel.hideOverlap: true,自动隐藏重叠的标签。
这三个方案没有绝对优劣。我一般先试hideOverlap,再考虑rotate,因为用户读图时第一眼想看的是整体柱子趋势,而不是每个分类都标出来。只有极少数场景需要每个分类都显示,那时候才用interval: 0配合旋转。
3. 柱状图的三种高频变形:堆叠、双轴和横向布局
3.1 堆叠柱状图:多个系列叠成一根柱子
需求方说“我想看看两种商品的销量合计,还想看各自占了多少”,这时候堆叠柱状图是最直接的方案。做法是在多个系列的配置里给同一个 stack 值,这些系列就会在同一个类目下像叠积木一样叠起来。
series: [ { name: '手机', type: 'bar', stack: 'total', data: [320, 302, 301, 334, 390, 330, 320] }, { name: '电脑', type: 'bar', stack: 'total', data: [120, 132, 101, 134, 90, 230, 210] } ]这里有个隐藏逻辑:堆叠后,后续系列的柱子不是从零开始画,而是从上一个系列的顶部开始画。所以 series 数组里的顺序就决定了堆叠的上下顺序。我建议把数值较大的系列放下面,视觉上更稳当;数值极小的系列如果放在底层,它的颜色区域可能会被上面系列遮住看不清,这一点在配置时需要特别留意。
堆叠的另一个用途是做“目标达成拆解”。比如把销售额拆成新客和老客两部分,堆叠起来既能看到总数,又能看到结构变化。如果只想看占比,可以给 yAxis 配置max: 100,然后数据改成百分比,这样柱子高度恒定但分区比例变化明显。
3.2 双轴柱状图:量级差异大时的常用处理
一个业务场景:既要看销售额(几千块),又要看订单量(几十个)。如果共用一个 y 轴,销售额会把订单量压成一条贴地的线。此时可以给右侧再加一条 y 轴。
yAxis: [ { type: 'value', name: '销售额', position: 'left' }, { type: 'value', name: '订单量', position: 'right' } ], series: [ { name: '销售额', type: 'bar', yAxisIndex: 0, data: [3200, 4300, 5200, 4100, 6300] }, { name: '订单量', type: 'bar', yAxisIndex: 1, data: [15, 22, 18, 30, 27] } ]双轴图的核心是yAxisIndex。series 里的yAxisIndex为 0 表示用第一条 y 轴(左侧),为 1 表示用第二条 y 轴(右侧)。不写就默认用 0。看起来简单,但在多系列时特别容易漏配,导致所有柱子都在左侧轴上对齐量级,图形失真。
双轴图有个常见争议:两个 y 轴的刻度范围是独立计算的,所以两组数据之间的视觉关系可能是“假”的。比如左侧销售额 0-10000,右侧订单量 0-50,一眼看过去两者的柱子高度对比并不代表真实比例。在给业务方讲解时要提前说明这一点,否则容易被误读。如果单纯为了对比两组数据的趋势,其实也可以用双折线图或普通单轴归一化处理。
3.3 横向柱状图:分类名特别长时的救星
当分类名称很长,比如“华东大区-杭州萧山仓”、“华南大区-深圳宝安仓”,x 轴无论如何旋转都放不下时,直接把图反过来,用横向柱状图是最稳妥的。做法就是把 xAxis 和 yAxis 的类型换一下:类目轴放到 yAxis,数值轴放到 xAxis。
xAxis: { type: 'value' }, yAxis: { type: 'category', data: ['华东大区-杭州萧山仓', '华南大区-深圳宝安仓', '华北大区-北京大兴仓'], inverse: true // 让第一个分类显示在最上面 }inverse: true是我个人比较推荐加上的,否则默认第一个分类出现在 y 轴底部,读起来别扭。这种横向布局在移动端也友好,屏幕宽度有限时,横向列表比竖向柱状图高效得多。横向柱状图还可以配合label: { position: 'right' },让数值显示在柱子右侧而不是柱子上方,长分类名时更不拥挤。
4. 柱子的“卖相”工程:渐变、圆角、图片柱和标签布局
4.1 渐变是提升图表质感的最直接手段
业务大屏上纯色柱子总显得单薄,渐变色是提升质感最廉价的方案。ECharts 里的渐变不是写 CSS 的 linear-gradient 字符串,而是用echarts.graphic.LinearGradient对象。
itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#83bff6' }, { offset: 1, color: '#2f6fed' } ]) }参数里的四个数字是渐变方向:(0, 0)表示起始点在左上角,(0, 1)表示结束点在右下角,这样是垂直渐变。如果想让柱子从上到下颜色由浅变深,就用(0, 0, 0, 1);想让柱子从左到右渐变,用(1, 0, 0, 0)。
渐变还有个额外好处:同一色系渐变比纯色更容易分辨不同柱子,尤其是柱子数量多的时候。但要注意,渐变对象的 offset 从 0 到 1 必须连续,中间留白会出现颜色断层,视觉上很难看。
4.2 圆角柱子的注意点
圆角柱子上手很快:
itemStyle: { borderRadius: [6, 6, 0, 0] }四个值分别对应左上、右上、右下、左下。顶部两个角圆润,底部保持直角,看起来像从地面长出来的柱子,这是目前业务项目里最常见的样式。
但堆叠柱状图里圆角要小心。如果每段都设置borderRadius: [6, 6, 0, 0],中间层的柱子上下都直角,顶部层的柱子有圆角,但堆叠的边界处会有割裂感。一般建议只在最底层加底部圆角、最顶层加顶部圆角,中间层保持直角。如果实在想统一加圆角,可以把圆角设成小值,比如[3, 3, 3, 3],视觉上不会太突兀。
4.3 柱子的颜色怎么规范?不要依赖默认色板
项目里我强烈建议给每个系列固定颜色,不要依赖默认色板。默认色板在系列超过一定数量后会重复或不够用,而且换主题时颜色会变,业务方会质疑“为什么上周还是红色这周变绿了”。
如果系列多,可以用一个数组循环取色:
const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666']; const data = [100, 200, 150, 120]; const seriesData = data.map((value, index) => ({ value, itemStyle: { color: colorList[index % colorList.length] } }));这里循环取色的好处是即使数据长度超过颜色数量也不会报错,顶多颜色循环,不会出现 undefined。如果项目中有设计规范,建议把品牌色做成常量文件,在多个图表里复用,保证整个页面的视觉统一性。
4.4 柱子上能不能放自定义图片?可以
网上经常有人问“ECharts 柱状图的柱子能不能用自定义图片显示”。答案是可以的,不用改什么特殊配置,还是走itemStyle.color,但传的是一个带 image 属性的对象。
const img = new Image(); img.src = '/images/cola.png'; series: [{ type: 'bar', data: [120, 200, 150], itemStyle: { color: { image: img, repeat: 'repeat' } } }]repeat: 'repeat'表示图片在柱子里平铺,柱子多高就铺多少层。这种效果适合做带纹理的柱子,比如饮料销量图让柱子填充饮料包装图。这里有个先决条件:图片要先加载完成再 setOption,否则可能出现空白柱子。我通常在img.onload回调里再调用 setOption,或者在页面初始化时就预加载好图片资源。
如果不想用纹理填充,还有一种思路是柱子本身用透明或弱色,然后在柱子的顶部加一个图片标记,类似 markPoint 的效果,这样柱子和图片是分开的,加载失败时还能退回到纯色柱子。
4.5 柱子上的数值标签
在柱子上显示具体数值,用label配置:
label: { show: true, position: 'top', color: '#333', fontSize: 12 }当柱子很高时,标签放在top可能会超出容器,这时可以设置overflow: 'break'配合width,或者在 label 的 formatter 里对数值做单位转换,比如超过 10000 显示为“1.2万”。我在电商项目里经常用这个技巧,否则数值标签会占掉大半张图,柱子反而成了背景。
标签的 formatter 可以做很多事情,不只是加单位,还可以加前后缀、换行、根据数值大小变颜色。比如:
label: { show: true, position: 'top', formatter: function (params) { return params.value > 1000 ? (params.value / 1000).toFixed(1) + 'k' : params.value; } }这样柱子上就不会显示一长串数字,既美观又容易读。
5. 柱状图叠加折线图:我最常用的一套组合图配置
5.1 这类图适合什么场景
柱状图叠加折线图的场景一般是:柱状图展示绝对值,折线图展示比率或另一个量级的指标。典型的就是“销售额柱状图 + 增长率折线图”,大屏上报表里出现频率非常高。这个需求用到的基础是双轴,再混合不同 series 类型。柱状图和折线图在同一个坐标系里,既有对比又有趋势,是信息密度比较高的一种组合。
5.2 完整配置可以直接抄
option = { tooltip: { trigger: 'axis', formatter: function (params) { let res = ''; params.forEach(function (item) { if (item.seriesName === '销售额') { res += item.marker + item.seriesName + ':' + item.value + ' 万元<br/>'; } else { res += item.marker + item.seriesName + ':' + item.value + ' %<br/>'; } }); return res; } }, legend: { data: ['销售额', '增长率'] }, xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月'] }, yAxis: [ { type: 'value', name: '销售额(万元)', splitLine: { show: true } }, { type: 'value', name: '增长率(%)', splitLine: { show: false }, axisLabel: { formatter: '{value}%' } } ], series: [ { name: '销售额', type: 'bar', yAxisIndex: 0, data: [120, 200, 150, 80, 170, 240], itemStyle: { borderRadius: [4, 4, 0, 0] } }, { name: '增长率', type: 'line', yAxisIndex: 1, smooth: true, data: [12, 18, -5, 24, 30, 35] } ] };5.3 组合图里必须注意的对齐问题
这类组合图看着简单,但有几个细节容易踩坑。
一是右侧 y 轴的splitLine: { show: false },这样图上的网格线只有左侧销售额的,右侧增长率不再重复画线,否则背景会显得特别乱。
二是 y 轴范围没对齐时,折线会被“压扁”或“拉飞”。ECharts 默认自动计算,但两个轴的数值范围往往差距很大。比如销售额在 0-240,增长率在 -5 到 35,如果不设置min/max,折线会显得特别平。我一般会给右侧 y 轴设置min: 0或者根据业务设个对称范围,让折线的波动幅度在视觉上更明显。
三是 tooltip 换行。搜索结果里经常有人搜 “echarts tooltip 自动换行”,本意是悬浮框里多个系列的内容不要挤在一行。用 formatter 手拼 HTML 是最直接的方案,每行用一个<br/>结尾,就像上面代码里那样。如果项目里 tooltip 复杂,建议单独抽一个格式化函数来维护,别全堆在 option 里。
5.4 折线图上的数值可以不显示,但趋势要有
组合图里折线图一般不需要显示每个点的数值,重点看趋势。如果确实要显示,用:
label: { show: true, position: 'top', formatter: '{c}%' }折线图的数据点多了以后,可以把symbolSize调小或者设成 0 只留线,视觉上更干净。不过 symbolSize 设成 0 之后,tooltip 触发时用户不容易点中数据点,建议至少保留 4-6 像素的 symbol,增强交互手感。
6. 大屏项目里柱状图的细节打磨和深坑记录
6.1 容器初始化为 0 导致图表不出现
做可视化大屏时,最经典的坑是图表所在的 div 在初始化时是隐藏的,或者宽度为 0,ECharts 初始化出来的图表就宽 0 高 0,等容器显示出来图表也回不来。排查时控制台还不报错,只显示一张空白图。这种问题经常出现在 Tab 页、折叠面板、弹窗这类动态展示的容器里。
我的常规做法是统一封装一个 initChart 方法,初始化后延时几百毫秒主动调一次resize():
function initChart(domId) { const dom = document.getElementById(domId); const chart = echarts.init(dom); setTimeout(() => chart.resize(), 200); return chart; }如果还是不行,检查父容器是不是有display: none的样式,或者有没有被 transform 缩放到 0。这类问题光靠 resize 也解决不了,必须保证初始化时容器可见。
6.2 窗口尺寸变化时图表要及时 resize
浏览器窗口变了,图表不会自动跟着变。需要监听 resize 事件:
window.addEventListener('resize', function () { myChart.resize(); });这个监听能解决大部分问题,但有两个例外:一是页面隐藏再显示时容器尺寸变了但没触发 resize 事件,需要在前端路由切换或 Tab 切换时手动调用 resize;二是大屏缩放方案中,整个页面用 transform 等比缩放时,图表内部需要重新计算,光靠 resize 不够,建议结合具体缩放方案统一处理。如果是 Vue 或 React 项目,还要注意在组件卸载时移除 resize 监听,避免内存泄漏和重复触发。
6.3 数据量大的时候柱子怎么优化
如果一年 365 天每天一组数据,柱子全画出来不仅图面拥挤,渲染性能也会下降。几个常用手段:
barMaxWidth: 10限制柱子最大宽度,避免太宽像墩子。dataZoom加缩放条,只展示部分数据。- 对数据进行前端聚合,比如按周或按月汇总。
- 关闭不必要的动画:
animation: false,大数据量时动画反而拖慢首屏渲染。
我遇到过一个场景是每分钟一个数据点,一天 1440 个柱子。这时候第一屏根本不需要全部展示,我用dataZoom默认显示最近 50 个点,用户可以通过缩放条查看历史数据,这样性能不会崩,交互也比一次性渲染 1440 根柱子好得多。
6.4 常见报错排查记录
我在带项目时发现新手报错的几种情况,整理成一张表:
| 报错/现象 | 常见原因 | 处理方式 |
|---|---|---|
Can't get DOM width or height | init 时容器不存在或大小为 0 | 确认 DOM 已渲染,设置容器宽高 |
| 图表空白但不报错 | data 为空数组或全 null | 打印 data 检查,注意 null 值展示 |
| 柱子和轴标签错位 | series.data 和 xAxis.data 长度不一致 | 统一数据源长度,前端处理好对齐 |
| 双轴图表柱子全贴一边 | series 的 yAxisIndex 未设置或设错 | 按系列明确指定 yAxisIndex |
| setOption 后图例不更新 | legend.data 没同步 | legend 的 data 要随 series 更新 |
6.5 动态数据更新:setOption 的正确姿势
实时报表经常要每几秒刷新一次数据。很多人会直接chart.clear()再重新 init,其实没必要。正确的做法是复用同一个实例,直接调 setOption,ECharts 内部会做 diff 并增量更新:
setInterval(() => { myChart.setOption({ series: [ { data: getNewData() } ] }); }, 5000);注意这里没写 type,只写了 data。ECharts 会根据已有的 series 配置做更新,这是官方推荐的“增量更新”写法。如果每次都把完整 option 塞进去,视觉上容易闪动,交互也会卡。
动态更新时还有个容易忽略的点:如果数据长度会变化,xAxis 的 data 也要一起更新。只更新 series.data 而 xAxis.data 不变,就会造成前后数据错位。我在实时监控项目里的做法是每次更新都同时 setOption 里带 xAxis.data 和 series[0].data,保持一致。
我自己在那次返工之后总结了一句话:柱状图的复杂度不在画几根柱子,而在真实业务里围绕这些柱子产生的需求细节。配置项记不住没关系,能查文档就行,但要知道一个问题大概从哪个配置入手,知道怎么排查数据错位、怎么处理容器尺寸、怎么控制双轴,这些经验才是项目里真正值钱的部分。希望这篇教程能帮你少走一点弯路。