环形图这东西,说简单也简单,series里把type设成pie,再配个radius数组就出来了;说难也难,难在真正落到业务页面里,数据标签怎么摆、图例怎么对齐、中间那块空白放什么、多环怎么套、响应式怎么处理,一堆细节能把人磨到怀疑人生。我这些年做后台可视化,环形图几乎是出现频率最高的图表类型之一,占比分布、进度展示、分类统计,到处都有它的影子。很多刚接触 ECharts 的朋友,照着官方示例改两下能跑起来,可一旦数据变成动态的、容器尺寸不固定、还要叠加中心文字和自定义配色,问题就全冒出来了。
这篇内容我打算把 ECharts 环形图从“能画出来”到“画得能交付”之间那段路完整走一遍。核心关键词就是echarts 环形图,围绕它的配置结构、半径参数、标签布局、中心内容、多环嵌套、动态数据更新、响应式适配这些实际痛点展开。适合两类人看:一类是刚上手 ECharts、想把环形图彻底搞明白的前端新手;另一类是做过一些图表、但总在细节上翻车的开发者。我不会只贴配置,而是把每个参数为什么这么设、换了会怎样、什么场景该用哪种方案讲清楚,你照着改就能用在自己的项目里。
1. 环形图到底是怎么“挖空”的:核心原理与方案选型
1.1 从饼图到环形图,只差一个半径数组
很多人以为环形图是独立图表类型,其实在 ECharts 里它和饼图是同一套渲染逻辑,区别只在于radius这个属性。饼图的radius是一个值或者百分比,从圆心铺到边缘;环形图则给它一个数组,第一个值是内半径,第二个值是外半径,中间那块就被“挖”掉了。你可以把它想象成一张圆形纸片,环形图就是拿圆规在中间又画了个小圆剪掉,剩下的圆环才是可见区域。
series: [{ type: 'pie', radius: ['45%', '70%'], data: [ { value: 1048, name: '分类A' }, { value: 735, name: '分类B' }, { value: 580, name: '分类C' } ] }]这段配置里'45%'是内半径,'70%'是外半径,百分比是相对于容器宽高中较小那个维度的一半来算的。这里有个容易被忽略的点:百分比基准是“短边的一半”,不是整个宽度。所以当容器是长方形时,环形图不会撑满宽度,而是以短边为准居中。我见过有人把容器设成很扁的横条,结果图小得可怜,还以为是配置写错了,其实就是这个基准在起作用。
内半径和外半径的差值决定了环的“厚度”。差值太小,环太细,数据块之间的视觉区分度下降;差值太大,环太粗,又失去了环形图那种轻盈感。我的经验是,内外半径差保持在 20% 到 30% 之间比较舒服,比如['50%', '75%']或者['40%', '65%']。当然这不是铁律,具体还得看容器比例和整体设计风格。
1.2 为什么用环形图而不是饼图:留白带来的信息增量
既然饼图也能表达占比,为什么还要用环形图?核心在于中间那块空白。饼图的圆心是“死”的,除了放个总数没别的用处;环形图的中心是一个天然的视觉焦点,可以承载标题、总量、单位、甚至一个小图标。这在仪表盘场景里价值极大——用户一眼看到环,第二眼看到中心数字,信息层级非常清晰。
从视觉心理学角度讲,人眼对环形图的面积判断其实不如饼图直观,因为环的弧长和面积关系没那么线性。但环形图的优势在于对比更聚焦:去掉中心区域后,视线被迫沿着环走,各段之间的边界更清晰。所以当分类数量在 3 到 6 个之间、且需要突出“整体中的部分”时,环形图是很好的选择;分类太多(超过 8 个)时,无论饼图还是环形图都会挤成一团,这时候就该考虑条形图了。
还有一个实际考量:环形图在深色背景的仪表盘上表现更好。饼图整块填充,深色背景下容易显得笨重;环形图中间透出背景色,整体更透气。这也是为什么很多数据大屏偏爱环形图。
1.3 方案选型:单环、多环还是嵌套环
实际项目里环形图有三种常见形态,选错了后面全是坑。单环最简单,一个series搞定,适合单一维度的占比展示。多环是多个series叠加,每个环代表一个维度,比如内环是“大类占比”、外环是“子类占比”,这种叫嵌套环或者双环图。还有一种是同心多环但数据独立,几个环各自独立表达不同指标,视觉上叠在一起。
多环的实现要点在于每个series的radius要错开,且series数组顺序决定绘制层级。内环的radius外半径要小于外环的内半径,否则会重叠。比如内环['30%', '45%'],外环['55%', '75%'],中间留 10% 的间隙,视觉上才分得开。这里有个坑:多个 series 的center必须一致,否则环会偏心。默认都是['50%', '50%'],一般不用改,但如果手动设过就要检查。
我个人的建议是,除非业务确实需要表达层级关系,否则别轻易上多环。多环的标签布局极其难调,内外环的标签很容易打架,而且用户理解成本高。单环加中心文字,往往比双环更清晰。
2. 半径、角度与标签:那些决定成败的关键参数
2.1 radius 与 center 的配合逻辑
前面说了radius是半径数组,center是圆心位置,默认['50%', '50%']。这两个参数配合起来决定了环在容器里的位置和大小。很多人调环形图位置时直接改center,比如设成['50%', '40%']把图往上挪,给下方图例腾空间。这招确实管用,但要注意center的百分比同样是相对于容器宽高,横向和纵向基准不同,所以['50%', '40%']在宽容器里横向居中、纵向偏上,效果符合预期。
真正容易翻车的是容器尺寸变化时。如果radius用固定像素值(比如[80, 140]),容器变大图不变,显得很小;容器变小图溢出。所以除非有特殊需求,一律用百分比。百分比的自适应能力是 ECharts 内置的,配合resize()调用,基本能应对绝大多数响应式场景。
还有一个细节:当radius的内半径设为0时,环形图就退化成了饼图。反过来,如果内半径设得很大、接近外半径,环会细成一条线。我调试时习惯先把内半径设成'0%'确认数据分布没问题,再逐步加大内半径找视觉平衡点,这样比一上来就调环宽更高效。
2.2 标签布局:inside、outside 与 labelLine 的取舍
环形图的标签是最折磨人的部分。label.position有三个常用值:'inside'标签在扇区内部,'outside'在外部带引导线,'center'在圆心(一般配合单值使用)。数据项多、名称长的时候,'outside'是默认选择,但引导线一多就乱成一团麻。
我的处理策略是这样的:分类数 ≤ 4 且名称短,用'inside',干净利落;分类数 5 到 8,用'outside'但必须调labelLine;分类数再多,考虑把标签隐藏,靠图例和 tooltip 补充信息。labelLine有两个关键参数:length(第一段线长)和length2(第二段线长),默认值在密集场景下会让线互相穿插。把length调短到 8 到 12、length2调到 10 左右,能明显缓解拥挤。
label: { position: 'outside', formatter: '{b}\n{d}%' }, labelLine: { length: 10, length2: 12, smooth: false }formatter里{b}是名称、{d}是百分比、{c}是数值。用\n换行让名称和百分比分两行显示,比挤在一行清爽得多。这里提醒一句:百分比是 ECharts 自动算的,基于当前所有数据项的value总和,不需要自己算。但如果你传的数据里有null或者非数字,百分比会算错,务必在传参前做数据清洗。
2.3 最小扇区与标签重叠的处理
数据里经常有小占比项,比如某个分类只占 1%,扇区细得几乎看不见,标签还会和相邻的挤在一起。ECharts 提供了minAngle参数,可以给每个扇区设一个最小角度(单位是度),保证小数据也能被看到。比如minAngle: 5,任何扇区至少占 5 度。但要注意,设了minAngle后百分比显示会和实际值不符,因为角度被强行放大了。所以如果标签要显示百分比,慎用minAngle,或者改用minShowLabelAngle控制标签显示阈值。
minShowLabelAngle更实用:小于这个角度的扇区不显示标签,避免重叠。比如设成10,占比太小的项标签自动隐藏,用户 hover 时通过 tooltip 看详情。这个参数配合labelLayout(ECharts 5 引入)效果更好,labelLayout: { hideOverlap: true }能自动隐藏重叠的标签,省去大量手动计算。
提示:
labelLayout是 ECharts 5 才有的能力,如果项目还锁在 4.x,只能靠minShowLabelAngle和手动调labelLine来缓解重叠,升级前先确认依赖版本。
3. 中心内容与图例:让环形图真正“有用”的加分项
3.1 中心文字:graphic 还是 title 还是 label
环形图中间放文字,常见三种做法。第一种用title组件,把left和top设成'center',text放主数字、subtext放说明。这种做法最简单,但title的定位是相对整个容器的,如果环形图不在容器正中(比如center调过),文字就会偏。第二种用graphic组件,可以精确控制位置,还能组合多个文本元素,灵活度最高。第三种是在series里用label的'center'位置,但这种方式只适合单数据项,多数据项时每个扇区都想往中心挤,会乱。
我一般推荐graphic,尤其是需要“大数字 + 小单位 + 说明文字”这种组合时。graphic的left/top支持百分比,和center对齐很方便。下面是一个典型配置:
graphic: { type: 'group', left: 'center', top: 'center', children: [ { type: 'text', style: { text: '1,286', fontSize: 28, fontWeight: 'bold', fill: '#333', textAlign: 'center' } }, { type: 'text', top: 36, style: { text: '总访问量', fontSize: 13, fill: '#999', textAlign: 'center' } } ] }这里type: 'group'把两个文本打包,整体居中,内部再用top做相对偏移。注意textAlign: 'center'要加在每个文本上,否则文字会左对齐,看起来偏。graphic的坐标系默认是容器像素,left: 'center'是相对容器的,所以只要环形图center是默认的['50%', '50%'],文字就能严丝合缝落在环心。
3.2 图例位置与环形图的配合
图例和环形图的关系很微妙。图例放右边,环形图就得往左挪;图例放下边,环形图就得往上挪。默认图例在顶部居中,环形图在正中间,两者不冲突但也不紧凑。我的习惯是图例放右侧垂直排列,环形图center设成['40%', '50%']往左偏一点,这样整体布局最平衡。
legend: { orient: 'vertical', right: '5%', top: 'center', itemWidth: 10, itemHeight: 10, itemGap: 14, textStyle: { fontSize: 12 } }itemWidth和itemHeight默认是 25 和 14,对环形图来说偏大,缩到 10 左右更精致。itemGap控制图例项间距,数据项多时调小一点能塞下更多。还有个小技巧:图例的icon可以设成'circle',和环形图的圆形扇区呼应,视觉更统一。默认是圆角矩形,其实也不难看,看设计规范。
图例和扇区的联动是 ECharts 内置的,点击图例项会切换对应扇区的显示,这个不用额外写代码。但要注意,图例的data要和series.data的name严格对应,名字对不上图例就点不动。动态数据场景下,图例data最好从series.data里 map 出来,避免手写遗漏。
3.3 配色:别让默认色板毁了你的图
ECharts 默认色板是那套经典的蓝绿黄橙红,能用但缺乏辨识度。环形图的配色有两个原则:相邻扇区色差要够大,否则边界模糊;整体色调要协调,别一个荧光绿配一个深紫。我一般用同色系的深浅渐变,或者从设计规范里取 5 到 6 个主色。
color: ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E8684A', '#6DC8EC']这套色板偏柔和,适合后台系统。如果是数据大屏,可以换成高饱和度的。这里有个实操心得:颜色数量要 ≥ 数据项数量,不够的话 ECharts 会循环取色,可能出现相邻两项同色,那就尴尬了。数据项动态变化时,要么准备足够长的色板,要么用colorBy: 'data'配合自定义函数按索引取色。
还有一点,环形图中间是空的,如果背景是深色,扇区颜色要适当提亮,否则整体发闷。我调试深色主题时会把色板整体亮度提高 10% 到 15%,对比度立刻上来。
4. 动态数据、响应式与性能:落地时的硬骨头
4.1 数据更新:setOption 的合并与替换
环形图的数据经常是异步来的,第一次渲染空数据、拿到接口后再更新。这里的关键是setOption的第二个参数。默认setOption(option)是合并模式,新配置和旧配置合并,series数组会按索引合并。如果数据项数量变了,合并模式可能残留旧数据,导致扇区数量对不上。正确做法是setOption(option, true)用替换模式,或者用notMerge: true,每次全量替换。
myChart.setOption({ series: [{ type: 'pie', radius: ['45%', '70%'], data: newData }] }, true);但全量替换有个副作用:动画会重放,每次更新都从头转一遍,体验不好。如果只是数据值变化、项数不变,用合并模式更平滑。我的判断标准是:项数变了用替换,项数没变用合并。项数变化时还可以配合animationDurationUpdate控制更新动画时长,设成 300 到 500 毫秒比较自然。
4.2 响应式:resize 不是万能药
容器尺寸变化时调chart.resize()是常识,但很多人只监听了window.resize,忽略了容器自身尺寸变化(比如侧边栏折叠、弹窗打开)。window.resize只在窗口变化时触发,容器内部布局变化它感知不到。稳妥的做法是用ResizeObserver监听容器元素:
const observer = new ResizeObserver(() => { myChart.resize(); }); observer.observe(document.getElementById('chart-container'));ResizeObserver兼容性现在没问题,主流浏览器都支持。记得在组件销毁时observer.disconnect(),否则内存泄漏。另外resize()调用有性能开销,容器频繁变化时(比如拖拽调整面板宽度)要加防抖,100 到 200 毫秒比较合适。
还有一个隐藏坑:容器初始尺寸为 0 时初始化图表,比如在display: none的 tab 里初始化,ECharts 拿不到尺寸,图会渲染成 0x0。解决办法是等容器可见后再init,或者初始化后手动调一次resize()。我踩过这个坑,排查了半天才发现是 tab 切换的时机问题。
4.3 大数据量下的性能取舍
环形图的数据项一般不会太多,超过 20 项就该考虑换图表类型了。但如果确实有几十项,性能问题主要出在标签和引导线上。可以关掉labelLine、把label设成'inside'或者干脆show: false,靠 tooltip 补充信息。animation也可以关掉,animation: false能省不少渲染开销。
另外emphasis(hover 高亮)在数据项多时也会拖慢响应,可以简化emphasis的配置,只保留scale: false和简单的itemStyle变化。实测下来,20 项以内的环形图性能完全不用担心,真正需要优化的是那些把环形图当进度条、每秒刷新一次的场景——那种场景下关动画、用合并模式更新,能明显降低 CPU 占用。
5. 常见问题排查与避坑清单
5.1 环形图不显示或显示异常
最常见的原因是容器没有高度。ECharts 依赖容器有明确的宽高,如果容器只有宽度没有高度(比如height: auto且内部无内容),图渲染不出来。给容器设固定高度或者height: 100%并确保父级有高度,问题就解决了。
第二个原因是数据为空或格式错误。series.data是空数组时,环形图什么都不画,这是正常行为。如果数据有但图不显示,检查value是不是数字类型,字符串'100'在某些版本下会导致计算异常。还有name重复也会出问题,ECharts 按name做图例联动,重名会导致联动错乱。
第三个原因是**radius配置错误**。比如写成radius: '70%'(字符串而非数组),那就成了饼图;写成radius: [70, 140](数字而非百分比),容器变化时不自适应。检查radius是不是数组、元素是不是百分比字符串,能排除大部分显示问题。
5.2 标签重叠与引导线混乱
标签重叠的排查顺序是这样的:先看label.position是不是'outside',是的话调labelLine.length和length2缩短引导线;再看数据项数量,超过 8 项考虑隐藏部分标签;最后上labelLayout: { hideOverlap: true }兜底。如果还乱,那就是容器太窄,标签没地方放,只能改布局或者换图表类型。
引导线混乱还有一个原因是labelLine.smooth设成了true,曲线在密集场景下会互相缠绕,改成false用直线清爽很多。另外label.alignTo: 'edge'能让标签对齐到容器边缘,视觉更整齐,但会拉长引导线,看情况用。
5.3 中心文字错位
中心文字错位基本是两个原因:一是环形图center不是默认值,但graphic还是按容器居中,两者对不上;二是graphic的left/top用了像素值,容器变化时没跟着变。解决办法是让graphic的定位和series.center保持一致,都用百分比,或者干脆把series.center保持默认,只调radius来改变环的大小。
还有一种情况是graphic里的文本用了textAlign: 'center'但没设textVerticalAlign,垂直方向没对齐,看起来偏上或偏下。补上textVerticalAlign: 'middle'就正了。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 环形图完全不显示 | 容器无高度 / 数据为空 / radius 配置错误 | 检查容器 CSS 高度、data 数组、radius 是否为百分比数组 |
| 环太细或太粗 | 内外半径差值不当 | 调整 radius 数组,差值保持 20% 到 30% |
| 标签重叠严重 | 数据项过多 / 引导线过长 | 缩短 labelLine、设 minShowLabelAngle、启用 hideOverlap |
| 中心文字偏移 | graphic 与 center 不一致 | 统一用百分比定位,检查 textAlign 和 textVerticalAlign |
| 图例点击无反应 | 图例 data 与 series name 不匹配 | 核对 name 拼写,动态数据从 series 派生图例 |
| 更新数据后扇区错乱 | setOption 用了合并模式 | 项数变化时改用替换模式 setOption(option, true) |
| 容器变化图不跟随 | 只监听了 window.resize | 改用 ResizeObserver 监听容器 |
| 深色背景下发闷 | 色板亮度不足 | 整体提高色板亮度 10% 到 15% |
这张表基本覆盖了我这些年遇到的高频问题,实际排查时按“显示 → 布局 → 交互 → 更新”的顺序走,大部分问题能在五分钟内定位。
5.5 几个容易被忽略的实操心得
第一个心得:调试时先把label和labelLine全关掉,只看环的形状和数据比例,确认没问题再逐步打开标签。这样能把“数据问题”和“标签问题”分开排查,效率高很多。
第二个心得:多环图的内外环数据顺序要一致。比如内环是“大类 A、B、C”,外环是“A 的子类、B 的子类、C 的子类”,如果顺序乱了,视觉上内外环对不齐,用户会看懵。数据组装时按同一维度排序,能省掉很多解释成本。
第三个心得:环形图的startAngle默认是 90 度(从 12 点方向开始),顺时针排列。如果业务要求从某个特定分类开始,调startAngle就行,不用重排数据。比如想让占比最大的项从 12 点开始,把数据按值降序排,startAngle保持默认即可。
第四个心得:导出图片时记得设backgroundColor。默认导出是透明背景,放到白色文档里没问题,但放到深色 PPT 里扇区边缘会糊。导出前临时设backgroundColor: '#fff',或者用getDataURL时传背景色参数。
环形图这个组件,配置项就那么多,但组合起来的变化空间很大。我现在的习惯是维护一套自己的基础配置模板,把常用的半径、标签、图例、配色都预设好,新项目直接复制改数据,比每次从零调快得多。模板里我会把labelLayout、ResizeObserver这些容易忘的细节都带上,避免重复踩坑。真正花时间的从来不是“怎么画一个环”,而是“怎么让这个环在各种数据、各种容器、各种交互下都稳稳当当”,这部分只能靠一个个项目磨出来。