同样一张饼图,数据从五个品类变成十二个品类之后,右侧那几根引导线就开始互相打结,标签你压我我压你,老板看一眼就问“这写的啥”。这类问题在数据可视化里出现频率极高,核心关键词就是ECharts 饼图文字重叠——它不是什么高深算法题,而是布局参数、容器尺寸、数据分布三件事没对上。这篇内容面向所有用 ECharts 画饼图、环形图、玫瑰图的人,从刚接手的同学到已经调过两三轮的老手都能拿走东西:前半段讲清楚标签位置到底是怎么算出来的、重叠为什么必然发生,后半段给出四套可以直接复制粘贴的配置方案,覆盖三五项的小数据、十几项的中等数据、几十项的长尾数据,以及窄屏移动端。顺带把参数含义、版本坑、排查表格一并整理好,以后遇到同类问题不用再靠猜。
1. 先弄清楚:饼图标签为什么会重叠
动手调参数之前,得先知道重叠是怎么产生的。很多同学拿到问题第一反应是把labelLine.length从 15 改成 30,结果发现标签整体往外挪了一点,重叠依旧——因为重叠的根源不在长度,而在多个标签被挤到了同一条近乎水平的输出通道上。
1.1 重叠的四种典型形态
我把实际项目里遇到的重叠归了归类,基本上跑不出下面这四种。
| 形态 | 表现 | 典型触发条件 |
|---|---|---|
| 引导线交缠型 | 右侧多条引导线拧成一束,末端标签上下叠压 | 小扇区数量多,且集中在某一侧 |
| 标签压饼型 | 标签文字压在扇区或圆心附近,互相遮挡 | position: 'inside'且中文名较长 |
| 长文本互压型 | 单个标签含名称+数值+百分比,横向过长 | 名称超过 6 个汉字、未做换行 |
| 溢出裁切型 | 标签被容器边缘切掉一半 | 容器高度小、radius过大、alignTo未启用 |
交缠型是最常见的。扇形角度越小,它中线指向的方向越接近相邻扇区,两个标签的锚点距离就越近;当十几个小扇区扎堆在图表右下角那一小段弧上,标签只能往同一个方向挤,挤不下就只能叠。压饼型多见于想省掉引导线的设计稿,position: 'inside'对小扇区极不友好,一个占比 2% 的扇区可能只有几像素宽,文字必然溢出到隔壁。长文本互压本质是文字宽度没有约束,ECharts 默认不会自动换行,除非你显式打开溢出策略。溢出裁切型则完全是容器尺寸和半径没算账的结果。
1.2 从渲染流程看标签位置是怎么生成的
ECharts 饼图的标签布局大致走这么几步,理解了顺序,调参就不会乱。
第一步,把data里每一项的值归一到 360 度,得到每个扇区的起始角和结束角,再取中线角度。第二步,按label.position决定锚点:outside时锚点在扇区中线的外侧弧上,inside时在扇区内部,center时所有标签都堆在圆心。第三步,outside模式下从锚点向外画引导线,先沿中线方向延伸labelLine.length(默认 15 像素),再拐一个弯水平延伸labelLine.length2(默认 15 像素),形成那个经典的折线。第四步,如果avoidLabelOverlap为true(这是默认值),布局器会尝试把位置冲突的标签沿垂直方向推开,推的时候尽量小幅度移动。第五步,如果alignTo设成了'edge',标签会被重新拉向图表左右边缘,并按边缘距离排布。
看清楚这个链条就能明白两件事。其一,avoidLabelOverlap只在最后阶段做微调,能推开的幅度有限,当标签数量超过物理空间能容纳的上限时,它怎么推都会叠。其二,labelLine.length和length2改变的是引导线的形状,不是标签之间的相对间距——你把第一段拉长,所有标签一起往外挪,相对位置基本没变,这就是为什么改长度往往无效。
提示:判断该往哪个方向优化,先看扇区数量。低于 8 个基本靠
alignTo: 'edge'就能解决;8 到 15 个要动数据和文本;超过 15 个,建议直接换图型,别硬扛。
2. 方案选型:四种解决思路的成本对比
饼图标签重叠没有唯一解,取决于数据量、容器尺寸、是否允许改数据结构。我把常见做法摊开对比一下,你可以按自己的场景对号入座。
2.1 四种思路的横向对比
| 方案 | 核心手段 | 适配数据量 | 改动成本 | 视觉代价 |
|---|---|---|---|---|
| 引导线错位 | alignTo: 'edge'+avoidLabelOverlap | 3 ~ 8 项 | 低,只改配置 | 几乎无损 |
| 标签转图例 | label.show: false+ 自定义legend.formatter | 任意 | 低 | 需要视线在图形和图例间跳转 |
| 长尾合并 | 数据侧合并小占比项为“其他” | 10 项以上 | 中,需要处理数据 | 损失细节,但可下钻 |
| 换图型 | 玫瑰图、横向条形图、旭日图 | 15 项以上 | 中到高 | 语义变化,需沟通 |
引导线错位是首选,因为它是纯配置改动,不动数据不动业务逻辑。图例方案在移动端几乎是最优解,屏幕就几百像素宽,饼图本身占一半,剩下那点地方放标签注定拥挤,不如把名称和数值全部下沉到图例区。长尾合并在做经营分析类看板时特别常见,占比低于 2% 的二十个品类合并成一个“其他”,图上从二十五瓣变成七瓣,老板一眼能看懂,点开“其他”再看明细。换图型是最后的退路,但很多时候它才是对的——饼图这个东西,超过十个分类本来就已经不适合表达了,人眼对扇形角度的分辨能力远不如对长度的分辨能力。
2.2 我怎么根据数据量做决策
我的判断口径很朴素,按条目数划线。
五条以内:直接上label: { position: 'outside', alignTo: 'edge' },配avoidLabelOverlap: true,基本一把过。这个区间连文本都不用截断。
六到十条:加大bleedMargin,把标签往容器边缘推,同时开labelLayout.hideOverlap,让真正挤不下的极少数标签自动隐藏。这里要注意一个体验问题——隐藏之后用户会好奇“为什么少了一个”,所以隐藏的必须是占比很小、肉眼本来就分辨不出的那几项,占比大的绝不能让它消失。
十一到二十条:数据侧先做合并,把小于阈值的项归到“其他”,同时把minAngle设为 1 到 3 度,保证小扇区至少有点可见度。剩下的再用alignTo: 'edge'排。
二十条以上:我会去找产品同学聊,问这个数据的表达目的到底是“看构成”还是“看排名”。如果是排名,横向条形图完胜,标签天然不会重叠。如果是看构成,用玫瑰图或者按大类分组的旭日图,也比硬塞一个五十瓣的饼图强。
这个流程不是死规矩,但它能省掉大量反复调试的时间。很多人一开始就在参数上死磕,磕到深夜才发现问题是数据本身太多。
3. 核心参数拆解:label 与 labelLine 的正确用法
参数这块值得单独拎出来讲,因为 ECharts 饼图标签相关的配置项有十几个,名字还挺像,搞混一个就白调半天。
3.1 position / alignTo / edgeDistance 的联动关系
label.position决定锚点,alignTo决定最终对齐方式,edgeDistance决定贴边距离,三者是串行关系。
position: 'outside'是解决重叠的基础,它把标签放到饼图外面,引导线负责连回去。alignTo有三个取值:'none'(默认,标签跟着引导线末端走)、'labelLine'(标签和引导线末端对齐)、'edge'(标签贴向容器左右边缘)。真正解决重叠的是'edge',因为它的布局器会把所有标签按左右两侧分组,然后按 y 坐标排序、依次往下摆,遇到冲突就推开——这就是那个“垂直推挤”算法的用武之地。默认的'none'模式下,标签位置严格跟随扇区角度,几乎没有腾挪空间。
edgeDistance控制标签离容器边缘多远,默认 25%。调小可以让标签更贴近图形、留出更多水平空间给文字;调大则相反。容器宽度吃紧的时候,我一般会把它降到 10% 到 15% 左右,给长文本挤出一点呼吸空间。
bleedMargin是个容易被忽略的参数,默认 5,含义是标签与容器边缘的最小“出血”边距。它主要防止标签贴边贴到被裁掉。容器 padding 很小的时候,把这个值调大一点更安全。
distanceToLabelLine控制标签和引导线末端的间隙,默认 5。这个值太小会让文字看起来黏在线上,太大又显得断开了。我一般设 8 到 12 之间,视觉上比较舒服。
labelLine这一组里,length和length2控制两段折线的长度,smooth把折线改成平滑曲线,minTurnAngle(默认 90 度)决定拐角小于多少度时不拐弯。minTurnAngle挺有意思,把它设大一点(比如 100 到 120),一些角度接近水平的引导线就不会拐那个没必要的弯,视觉上更干净。
3.2 avoidLabelOverlap 到底做了什么
这个参数默认就是true,所以“重叠了”不代表它没生效,而是它推不动。它的工作机制是:遍历所有标签的位置矩形,发现相交就把其中一个沿垂直方向平移,平移量尽量小,反复迭代若干次。当所有标签加起来的总高度超过容器高度时,它必然失败——这是物理上限,不是 bug。
所以看到重叠先算一笔账:假设容器高度 400 像素,每个标签行高 20 像素,理论上最多容纳 20 行。看起来够用?但实际可用区域要扣掉饼图本身占的高度。饼图半径设成容器的 40%,中间那个圆就吃掉了一半以上的垂直空间,标签只在上下两端有位置。这时候 12 个标签挤在两侧各 6 个,每侧可用的垂直空间可能只有 100 像素,也就是 5 行,第 6 个必然叠。这个账算清楚,你就知道该缩半径、该减少条目、还是该换方案了。
顺带说,avoidLabelOverlap跟alignTo: 'edge'是绝配,跟alignTo: 'none'组合效果很差,因为位置被角度锁死了。
3.3 labelLayout:ECharts 5 才有的重排能力
ECharts 5 引入了labelLayout,这是解决重叠的第二个工具箱,作用是在所有标签位置算完之后,再做一遍统一处理。
hideOverlap: true(需要 5.2 及以上版本)会把重叠的标签直接隐藏掉,属于“眼不见为净”的暴力解法,但配合数据合并一起用效果不错。moveOverlap: 'shiftY'会把重叠的标签在垂直方向挪开,行为跟avoidLabelOverlap有点像,但发生在更晚的阶段,能补上前面没推开的缺口。
更狠的用法是给labelLayout传一个函数,自己接管坐标。官方那个经典示例的思路是:判断标签在左半边还是右半边,然后强行把引导线的最后一个点拉齐到标签矩形的边缘,让所有水平线段长度一致、末端对齐,视觉上整整齐齐两列。代码骨架大致是这样:
series: [{ type: 'pie', label: { position: 'outside', alignTo: 'none' }, labelLine: { length: 20, length2: 25 }, labelLayout: function (params) { const isLeft = params.labelRect.x < chartWidth / 2; const points = params.labelLinePoints; // 第三段(水平线末端)对齐到标签矩形的左/右边缘 points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width; return { labelLinePoints: points }; } }]这个方案的好处是彻底摆脱了“标签跟着角度乱跑”的问题,代价是引导线可能变得比较长、绕得比较远。用它的时候把labelLine.smooth打开,曲线会顺眼很多。
4. 实操:四套可以直接抄的配置
前面讲原理,这一段全是能跑的代码。四套方案按复杂度递增,你可以从第一套开始试,不够再换下一套。
4.1 方案A:中小数据量的引导线错位
适用场景:3 到 8 个分类,容器宽度 400 像素以上。这是覆盖 80% 场景的基础配置。
const option = { tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['0%', '55%'], // 半径收一收,给标签留空间 center: ['50%', '50%'], avoidLabelOverlap: true, minAngle: 2, // 小扇区保底 2 度,便于点中 label: { show: true, position: 'outside', alignTo: 'edge', // 关键:贴边排布 edgeDistance: '12%', // 离边近一点,腾出文字空间 bleedMargin: 10, distanceToLabelLine: 10, formatter: function (p) { // 名称与百分比拆两行,降低单行宽度 return '{name|' + p.name + '}\n{val|' + p.percent + '%}'; }, rich: { name: { fontSize: 12, color: '#333', lineHeight: 16 }, val: { fontSize: 11, color: '#888', lineHeight: 14 } } }, labelLine: { show: true, length: 12, length2: 18, smooth: 0.2, // 轻微平滑,视觉更柔和 minTurnAngle: 100, lineStyle: { color: '#bbb' } }, data: [] }] };这里有两个小设计。第一,formatter用富文本把名称和百分比拆成两行,单行宽度立刻降了一半,这是最省事的“降宽度”手段,比截断文字体验好得多。第二,radius上限定在 55% 而不是常见的 70%,腾出来的 15% 就是标签的生存空间。很多人标签重叠的直接原因就是饼图画太大,把一个 400x300 的容器塞得满满当当,标签只能往外溢出。
minAngle: 2这个参数值得说一下。它的作用是给每个扇区一个最小的角度保底,占比极小的项也能有个可见的扇形,否则可能出现“数据里有、图上完全看不到”的尴尬。但它有个副作用:所有保底角度加起来会挤占大扇区的角度,总角度还是 360,所以设太大(比如 5 度以上)会扭曲真实占比。2 到 3 度是个比较克制的取值。
4.2 方案B:移动端与窄容器,标签让位给图例
适用场景:容器宽度小于 360 像素,或者屏幕高度特别紧。这时候布局器再怎么推也推不出空间,正确做法是放弃图上标签。
const rawData = [ { value: 1048, name: '线上直销' }, { value: 735, name: '渠道分销' }, { value: 580, name: '门店零售' }, { value: 484, name: '团购集采' }, { value: 300, name: '内部试用' } ]; const total = rawData.reduce((s, d) => s + d.value, 0); const option = { legend: { bottom: 0, left: 'center', orient: 'horizontal', itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 12 }, // 图例里补上百分比,弥补图上不显示数值的损失 formatter: function (name) { const item = rawData.find(d => d.name === name); const pct = item ? ((item.value / total) * 100).toFixed(1) : '0'; return name + ' ' + pct + '%'; } }, series: [{ type: 'pie', radius: ['0%', '60%'], center: ['50%', '42%'], // 上移,给底部图例让位置 avoidLabelOverlap: true, label: { show: false }, // 图上不画标签 labelLine: { show: false }, emphasis: { scale: true, scaleSize: 6, label: { show: true, fontSize: 13, fontWeight: 'bold' } }, data: rawData }] };注意label.show: false之后,emphasis.label.show: true让鼠标悬停时依然能看到具体值,这个补偿动作很有必要,否则用户点上去什么都看不到会很慌。图例的formatter拿不到percent字段(它只传name),所以要自己从原始数据里查一遍算百分比,这个坑很多人踩过,代码里已经处理了。
容器特别矮的时候,还可以把图例改成orient: 'vertical'放右边,饼图center左移,形成“左图右例”的布局,垂直方向的空间利用率更高。
4.3 方案C:数据项多,先合并长尾再谈布局
适用场景:10 到 25 项。这时候参数调优已经到了极限,必须动数据。
function mergeTail(data, threshold, keepCount) { const sorted = [...data].sort((a, b) => b.value - a.value); const total = sorted.reduce((s, d) => s + d.value, 0); const main = []; const rest = []; sorted.forEach((item, idx) => { const pct = item.value / total; // 占比低于阈值 且 排名在保留数之外,才算长尾 if (pct < threshold && idx >= keepCount) { rest.push(item); } else { main.push(item); } }); if (rest.length > 0) { const restValue = rest.reduce((s, d) => s + d.value, 0); main.push({ name: '其他(' + rest.length + '项)', value: restValue, // 挂上明细,供 tooltip 或下钻使用 children: rest }); } return main; } // 阈值 2%,至少保留前 6 项 const chartData = mergeTail(rawData, 0.02, 6);合并之后一般能稳定在 6 到 8 项,回到方案A 的适用区间,参数直接复用。这里有两个设计细节值得强调。
一是阈值不要写死成固定条数。纯按条数截断(比如“只显示前 8 名”)的问题在于,可能第 8 名占比 15%,而第 9 名占比 14.8%,硬切太粗暴。按百分比阈值加排名双重条件更合理,也让阈值可以随业务调整。二是合并出来的“其他”项名称里带上条目数(比如“其他(9项)”),用户一眼就知道这里藏了东西,配合 tooltip 展示明细,透明度和可读性兼顾。
如果业务允许,“其他”还应该支持点击下钻,展开成第二层饼图。这样既解决了视觉重叠,又没丢信息。
4.4 方案D:还是挤,那就换个图形
适用场景:超过 15 项,或者分类名本身就长到没法塞进标签。
玫瑰图(南丁格尔图)是个过渡方案,用半径代替角度差异来强化小项的可见度:
series: [{ type: 'pie', roseType: 'radius', radius: ['18%', '68%'], avoidLabelOverlap: true, label: { position: 'outside', alignTo: 'edge', formatter: '{b}', fontSize: 11, overflow: 'truncate', width: 70, ellipsis: '...' }, labelLine: { length: 10, length2: 12, minTurnAngle: 100 }, data: chartData }]overflow: 'truncate'配合width会把超长名称截断加省略号,完整名称放到 tooltip 里。这一招在分类名普遍 8 个字以上的场景里特别管用,比自动换行稳定,因为换行会让标签高度翻倍,垂直空间更紧张。中文用overflow: 'breakAll'可以按字符硬换行,但对齐效果不如truncate整齐。
如果是看排名而非构成,横向条形图是真的香:
const option = { grid: { left: 90, right: 40, top: 20, bottom: 20 }, xAxis: { type: 'value', show: false }, yAxis: { type: 'category', inverse: true, // 值大的排上面 axisLine: { show: false }, axisTick: { show: false }, axisLabel: { fontSize: 12 } }, series: [{ type: 'bar', barWidth: 14, label: { show: true, position: 'right', formatter: '{c}' }, itemStyle: { borderRadius: 7 }, data: chartData .sort((a, b) => b.value - a.value) .map(d => ({ name: d.name, value: d.value })) }] };横向条形图的标签在右端,天然只有一行,永远不会重叠,名称在 y 轴左侧有完整的显示空间。跟产品沟通时,我的说法是:“饼图表达的是占比,条形图表达的是排名,如果大家关心的是谁排第一,那饼图其实一直在误导我们。”
4.5 尺寸自适应与 resize 的正确处理
标签重叠跟容器尺寸强相关,容器一变,原来的布局可能就崩了。所以resize的处理必须做对。
const chart = echarts.init(document.getElementById('pie-wrap')); // 用防抖,避免拖拽窗口时疯狂重绘 let timer = null; window.addEventListener('resize', function () { clearTimeout(timer); timer = setTimeout(function () { chart.resize(); // 尺寸变化后按新的宽度做一次断点切换 applyLayoutByWidth(chart.getWidth()); }, 150); }); function applyLayoutByWidth(width) { if (width < 360) { // 窄屏:关标签,开图例 chart.setOption({ legend: { show: true, bottom: 0 }, series: [{ label: { show: false }, labelLine: { show: false } }] }); } else { chart.setOption({ legend: { show: false }, series: [{ label: { show: true, position: 'outside', alignTo: 'edge' }, labelLine: { show: true } }] }); } }这里有几个坑必须提。第一,resize一定要防抖,否则拖拽窗口时每一像素都在重绘,卡到怀疑人生。第二,chart.getWidth()返回的是 canvas 的实际像素宽度,如果用devicePixelRatio做过缩放,取到的值可能跟你以为的 CSS 宽度不一样,判断断点时拿container.clientWidth更稳。第三,setOption默认是合并模式,改series时如果不给index,可能覆盖到别的系列;多系列图表建议写成series: [{ ... }, { ... }]明确位置,或者用chart.setOption(opt, { replaceMerge: ['series'] })。第四,容器初始尺寸如果是 0(比如藏在display: none的 tab 里),init出来的图表宽度就是 0,布局全乱,切回 tab 时必须手动调一次resize。
5. 常见问题与排查技巧实录
这一段是我自己踩过的坑,按出现频率排。
5.1 标签被容器裁掉,字看不见了
典型现象:标签只显示一半,或者整块消失。原因通常是容器尺寸不够,但标签还按原来的半径往外延伸。
排查顺序:先看容器高度。饼图半径如果用百分比,是相对容器的宽高中较小值的一半来算的,一个 800x200 的容器,半径只有 100 像素,但标签往外延伸的length + length2 + 文字宽度可能要 150 像素,超出后就被裁。解决办法三条:把radius降到 45% 以下、把center往上挪给下方标签腾地方、给容器加overflow: visible(这一条只对 canvas 外面的容器生效,别指望它救 canvas 内部的绘制)。
还有个隐蔽原因:容器的padding被算进了 canvas 尺寸,但edgeDistance的百分比是相对图形区域算的,两者不一致时视觉上就像被裁了。我一般把容器 padding 全部清零,只在外层包裹 div 上加边距。
5.2 长文本换行、截断、省略该怎么选
三个策略适用不同情况,别混用。
自动换行(overflow: 'breakAll'+width)适合名称长度在 6 到 12 个字、且不介意标签变高的场景。缺点是行数一多,垂直空间被吃掉,反而加剧重叠。
截断加省略号(overflow: 'truncate'+width+ellipsis: '...')适合名称长得没边、但有 tooltip 兜底的情况。我最常用这个,因为布局稳定,标签高度可控。
手动插入换行符(formatter里拼\n)适合你明确知道要断在哪里,比如“华东区-直营网点”这种带层级关系的名称,在-处断开比按宽度硬切自然得多。配合lineHeight可以精确控制两行之间的间隙。
注意:
overflow只在width有值的时候生效。很多人开了overflow: 'truncate'却没配width,结果毫无变化,然后以为是版本问题。
5.3 富文本标签为什么没生效
rich是 ECharts 处理复杂标签的利器,但有几个硬性前提:formatter必须返回带{styleName|文本}格式的字符串;rich要写在跟formatter同一级的label对象里,不能写在normal下面,也不能写在外层;rich里的每一项支持fontSize、color、lineHeight、padding、backgroundColor等属性,但不支持font-weight这种连字符写法,要用fontWeight。
另一个常见困惑是富文本标签里的rich样式不继承全局textStyle,每个段落要单独配颜色,否则可能全是默认黑色。
5.4 快速排查速查表
| 现象 | 第一怀疑对象 | 处理动作 |
|---|---|---|
| 标签全挤在一侧 | alignTo未设'edge' | 改为'edge',同时把edgeDistance降到 15% 以下 |
| 标签叠压但数量不多 | radius太大 | 上限从 70% 降到 55%,center微调 |
| 少数标签凭空消失 | labelLayout.hideOverlap生效 | 确认是否要保留,占比大的项手动在formatter返回空串之外处理 |
| 引导线交叉成一团 | length/length2过大且minTurnAngle太小 | minTurnAngle提到 100 以上,smooth开一点 |
| 悬停后标签位置跳变 | emphasis.label位置未指定 | 显式设置emphasis.label.position与基础一致 |
| 数据更新后布局错乱 | setOption合并模式残留旧配置 | 用notMerge: true或replaceMerge: ['series'] |
| 移动端怎么调都挤 | 空间物理上限 | 直接切到方案B,标签转图例 |
这张表我贴在项目笔记里,遇到问题先对一遍,能省掉大量盲调的时间。
6. 版本差异和动态数据,两个容易翻车的点
最后分享两个跟参数无关、但能让方案失效的问题,都是我实际被坑过的。
6.1 版本差异带来的隐性失效
ECharts 5.x 之间的小版本差异,对标签布局影响不小。labelLayout.hideOverlap是 5.2 才加进来的,在 5.0 和 5.1 上写了也白写,不报错、不生效,最难查。alignTo: 'edge'在早期的 5.0.x 上行为跟后面有细微差别,边缘距离的百分比基准调整过。如果你的项目依赖是锁死的老版本,调了半天没反应,第一步先去package.json里确认版本号,而不是怀疑配置写错。
另一个隐蔽点是 CDN 上引的版本跟本地开发版本不一致。本地跑得好好的,上线就出问题,八成是这个。我现在的习惯是在初始化后打一行console.log(echarts.version),彻底断掉这个念想。定制构建的包还要更小心,有些按需引入的构建会裁剪掉饼图相关的 layout 模块,标签布局能算但重排不生效,表现就是“参数都对,就是不整齐”。
6.2 动态数据和反复 setOption 造成的布局残留
实时刷新的看板里,数据每隔几秒变一次,标签的位置不会每次都重新算一遍初始化状态,某些情况下会保留上一轮的布局痕迹——尤其是用了自定义labelLayout函数、在里面手动改了labelLinePoints的场景。表现是标签慢慢往一个方向漂,越漂越偏。
处理办法有两个。一是尽量不用有副作用的labelLayout函数,如果非要用,确保每次返回的坐标都是基于当前params完全重算的,别去读上次的结果。二是数据量变化较大时(比如从 5 项变到 12 项),用chart.setOption(option, { notMerge: true })强制清空重绘,虽然有一点性能开销,但换来的是布局绝对干净。这个开销在秒级刷新频率下完全可以接受,实测一次重绘在几十毫秒量级,肉眼无感。
还有一个数据侧的细节:value为 0 或null的项,ECharts 默认还是会尝试给标签留位置,只是角度接近 0,视觉上就是一根线和一坨文字贴在一起。数据清洗阶段直接把这部分过滤掉,能消掉一批莫名其妙的重叠。
调这些东西说到底就一句话:先算清楚容器有多少空间,再看数据要不要精简,最后才是参数微调。顺序搞反了,参数调到天亮也没用。