做数据可视化的老哥应该都有这种体会:拖着一堆业务指标往图里塞的时候,最大的麻烦往往不是数据算不对,而是图表画出来之后“没法看”。尤其多系列折线图、柱状图,一旦每个点都开了数据标签,那画面基本就是一场文字车祸现场——标签叠标签、柱顶标签压着折线标签、左右两边的数值糊成一团,客户瞄一眼就皱眉。
Echarts 里这个“标签遮挡”问题,几乎是每个做 dashboard 的人都绕不过去的坎。它不涉及多复杂的算法,但处理不好特别掉价。这篇东西我就结合自己的实际项目经验,把多列折线图、柱状图场景下标签互相遮挡的成因、应急处理手法、能“治本”的布局方案,以及周边容易顺带踩的坑,一次讲清楚。
1. 先弄清标签为什么“叠成一锅粥”——遮挡产生的底层逻辑
想解决问题,不能一上来就试运气。我最早处理这种问题的时候也是瞎调 position、乱改 offset,改了半天该挡还是挡。后来花时间把 Echarts 的渲染机制捋了一遍,才明白遮挡是“必然”的,不是配置写错了。
1.1 Echarts 标签渲染的基本机制
折线图和柱状图的标签,本质上是一个跟随数据点定位的文本图形。Echarts 内部会先把数据值(value)经过坐标系转换,映射成 canvas 上的像素坐标,然后在数据点周围按你配置的 position 方向偏移,把文本画出来。这个流程本身不复杂,问题出在它默认“不管理”标签之间的位置关系。
你可以把 Echarts 想象成一个贴便利贴的人:他负责把每张便利贴准确贴到对应位置,但没人负责检查便利贴之间是否互相覆盖。也就是说,Echarts 的标签默认不做跨系列的碰撞检测,更不会自动避让。只有当配置项里显式声明了标签布局策略(比如labelLayout),它才会进入避让逻辑。这个逻辑我后面会详细讲,这里先记住一个结论:多系列同时开启标签,重叠是预期行为,不重叠才需要人为干预。
还有一个容易忽略的点,就是标签不是只由数据点本身的位置决定,它还受文本长度、字体大小、对齐方向、偏移量影响。同样是position: 'top',值“10000”和值“1.2万”渲染出的文本宽度完全不同,遮挡范围自然也不一样。这就是为什么很多人用固定偏移解决不了问题——文本宽度是动态的,固定写法只能适配特定数据。
1.2 多列图的“遮挡频发区”
根据我实际做项目的经验,标签遮挡集中发生在四个场景里,每个场景的成因略有不同。
第一个是 x 轴刻度密集的时候。比如按天、按小时展示数据,x 轴点非常多,两个相邻点的像素距离只有三五十像素,可一个标签文本宽度就有六七十像素,这种情况下折线每个点都开标签,必叠无疑。这种情况本质上是“横向空间不够”,靠标签自身避让无法根治。
第二个是多系列数值相近或走势平滑的区域。比如三根折线都在 80 到 90 之间波动,同一个 x 坐标下三个数据点的 y 像素位置几乎相同,三张标签就在同一水平线上扎堆。这种遮挡是最让人头疼的,因为它规律性很强,一眼就能看到三行数字粘在一起。
第三个是柱状图和折线图混搭的场景。柱状图标签默认显示在柱子顶端,如果折线图的数据点恰好也在那个高度附近(比如折线表示转化率、柱状表示成交量,数值在视觉上接近),柱顶标签和折线点标签就会在“半空”撞车。这种混合图表的情况最复杂,因为两者的数据量纲不同但视觉位置会碰到一起。
第四个是标签文案过长。百分比、大数字带单位、保留了多位小数,这些都会让标签变宽。单个标签一旦变宽,除了会压到旁边点的标签,还可能把同一点上其他系列的位置也挤掉。
搞清楚这些问题分布区域,后面选方案才有依据。如果只是数值相近导致的局部重叠,用自动避让就够了;如果是 x 轴密集导致的系统性重叠,那就得从抽样或者合并展示入手。
2. 常规应急手段:先让图“能看”的三种快速处理
有些图表是给内部日报用的,要求是“信息完整、能看清趋势”,不需要每个数值都常驻图上。这种时候最省事的做法不是去搞复杂布局,而是直接从显示策略上做减法。应急手段有三种,我常用的是第一种和第三种组合。
2.1 裁剪显示:让 formatter 帮你“做减法”
Echarts 的 label 配置里有个formatter回调,它可以决定标签最终显示成什么内容,返回空字符串就是什么都不显示。利用这个特性,可以有条件地只展示部分标签。
比如我想让折线上的标签只显示最大值、最小值和最后一个点,其他点一律不显示:
label: { show: true, formatter: function(params) { var data = params.seriesData; // 当前系列的数据数组 var value = params.value[1]; var max = Math.max.apply(null, data.map(function(item) { return item[1]; })); var min = Math.min.apply(null, data.map(function(item) { return item[1]; })); if (value === max || value === min || params.dataIndex === data.length - 1) { return value; } return ''; } }这样做的好处是图上信息量不爆炸,只突出几个“有故事”的点,整体会干净很多。
还有一种更实用的裁剪方式是按索引间隔显示,比如每三个点展示一个标签,配合 x 轴密集的情况:
formatter: function(params) { return params.dataIndex % 3 === 0 ? params.value[1] : ''; }注意一个前提:这种写法牺牲了部分数据的可见性,适合“看趋势”的场景,不适合“查精确值”的场景。精确值查询交给 tooltip 或者下钻去解决会更合理。
2.2 透明度与层级:不靠隐藏靠“降噪”
有时候领导指定“每个点都要有数字”,这种情况下强行隐藏标签是要挨骂的。退而求其次的办法是降低非重点标签的视觉权重,把次要系列标签颜色调淡、字号调小,让重点系列标签保持高对比度。
操作上只需要给不同系列配置不同的 label 样式,比如次要系列用rgba(0,0,0,0.35)这种低透明度颜色,主系列用深色加粗字体。视觉上人眼会优先注意到对比度高的标签,低透明度的标签即使重叠了,也不会造成“一团浆糊”的第一印象。
这里要澄清一个常见误区:很多初学者以为调高 z 值就能让某个标签“压过”其他标签、解决遮挡。z 值只能决定渲染层级,也就是谁在上谁在下,它改变不了“盖住就无法阅读”的事实。标签是文本,不是纯色块,被盖住哪怕一点点都会影响辨识度。所以层级调整只能作为辅助,不能当方案用。
2.3 抽样策略:通过 dataZoom 联动控制标签密度
前面说按索引间隔显示是静态的,但有些场景需要动态变化。比如图表绑定了 dataZoom,用户把缩放条拉大之后,可视区间内 x 轴点数变少,标签本来不重叠,因为缩放比例变化又开始叠了。
这种动态场景可以在dataZoom的on回调里重新设置 label 的 formatter,根据当前startValue和endValue动态控制哪些点显示标签。还有个取巧的办法:把标签的显示逻辑和dataZoom的粗细绑定,缩放范围小时显示稀疏标签,缩放范围大时显示密集标签。
这块我在实际项目里通常用防抖函数包一层,避免拖动缩放条时频繁 setOption 导致卡顿:
chart.on('datazoom', debounce(function() { var opt = chart.getOption(); var batch = []; // 根据 start/end 计算需要显示的标签点,然后重新更新 series.label chart.setOption(batch); }, 120));抽样策略的本质是用“减少标签数量”换取“可读性”,它能快速让图不糊,但也丢信息。所以每次用的时候我都会问一句:这个图上丢掉的数字,用户想不想看?如果答案是不确定,我一般还会保留 tooltip 作为兜底,保证悬浮时能看到全量数据。
3. 进阶处理:一次真正能“治本”的标签布局方案
应急手段应付临时需求还行,但碰到正经的可视化大屏项目,客户要求所有关键标签都要清晰显示,这时候就得用更系统的方法。Echarts 从 4.x 开始引入了labelLayout配置,5.x 又加强了不少,这其实是目前处理标签遮挡最正统的路径。
3.1 官方内置:labelLayout.hideOverlap 一行代码快速去重
Echarts 的labelLayout支持一个非常实用的属性:hideOverlap: true。它的作用是开启标签自动隐藏——当某个标签和已经渲染过的标签发生矩形区域重叠时,后渲染的标签会被自动隐藏。
用法很简单,在每个系列的 labelLayout 里配置即可:
series: [ { type: 'line', label: { show: true }, labelLayout: { hideOverlap: true } }, { type: 'line', label: { show: true }, labelLayout: { hideOverlap: true } } ]这个配置我实测下来效果很明显,尤其适合多个折线系列之间“数值相近、同高度并列”的场景。它内部的原理是:Echarts 会等标签渲染出来之后拿到每个标签的实际边界矩形,然后逐一比对,检测到相交就把后出现的那个隐藏掉。看起来不复杂,但它省掉了你自己手动计算文本宽度的麻烦,因为它是基于真实渲染尺寸做检测的,比我用 canvas.measureText 估算要准确。
但它有几个限制:
第一,它是