尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ECharts实现3D饼图:echarts-gl与原生伪3D方案实战指南

发布时间:2026/9/29 5:16:00

资讯中心
01
ARTICLE

ECharts实现3D饼图:echarts-gl与原生伪3D方案实战指南

ECharts实现3D饼图:echarts-gl与原生伪3D方案实战指南
一个“3D饼图”的需求能把一张图表从“20分钟搞定”拖成“研究两小时”。ECharts本身的饼图能力相当能打扇形、环形、南丁格尔玫瑰图随手就来可一旦有人提出“要带厚度、能转、有光影”的3D效果原生图表就明显不够用了。这篇文章记录的是我在数据可视化大屏里做3D饼图的完整过程从方案选型、几何数据生成、参数调试到tooltip、rem适配等常见坑的解决思路希望能给同样被这个需求卡住的同学一条能直接抄的路径。1. 先把3D饼图的三个坑说清楚1.1 ECharts原生能力边界先说结论ECharts原生并不提供“3D饼图”这个图表类型。官方饼图 series-pie 支持扇形、圆环、圆角、南丁格尔玫瑰、label引导线等一堆能力但这一切都发生在一个2D平面上。就算你给扇区加阴影、加渐变、加扇区间的空隙本质上还是没有几何纵深的平面图。真正负责“3D”的是 echarts-gl 这个扩展库它给ECharts带来了 globe、map3D、scatter3D、bar3D 这些新的 series 类型但偏偏没有 pie3D。很多人第一次搜“ECharts 3D饼图”搜出来的结果大多是“用 bar3D 魔改的饼图”“用平面饼图做出伪3D效果”这类方案原因就在这里。先把这一点搞清楚后面所有实现才不会跑偏。1.2 两条路线的取舍既然没有现成组件实际项目里就分化出了两条主流路线。第一条是用 echarts-gl 的 bar3D 去“拼”饼图。原理是把饼图的每个扇区细分成很多小份每份用一根小柱子来表示。柱子可以设置高度、颜色、倒角还能打光拼起来之后就是一个有厚度的立体饼图。这条路的优点是真正的3D可以旋转视角视觉冲击力很强缺点也很明显图例联动基本失效数据更新和动画处理要比普通饼图麻烦不少。第二条是不引入 echarts-gl只用原生 pie 组件做“视觉伪3D”。通常做法是在底部铺一层略大、略偏移的阴影饼图模拟厚度再在上面叠一层带渐变的正常饼图模拟受光面。这条路的优点是开发成本极低图例、tooltip、下钻、数据更新全部沿用原来的逻辑稳定可靠缺点是从物理上就不存在纵深无论怎么转视角它都是一个“看起来立体”的平面图。我的经验是to B 业务大屏、数据实时刷新、图表组件很多的场景优先选第二条做展示稿、单图大屏、产品宣传、需要拖动旋转演示的选第一条。不要一上来就追求“真3D”先想清楚交付场景。2. 用echarts-gl把饼图“捏”成3Dbar3D极坐标法2.1 思路扇区切成柱子用高度表达厚度bar3D 在 cartesian3D 坐标系里渲染的是一根根长方体柱子柱子的位置由 x、y 坐标决定柱子有多高由 value 的第三维决定。把饼图数据映射到这个空间里需要三步第一步计算每个扇区的角度范围。比如“搜索引擎”占比30%那么在360度里它就应该占108度。第二步把每个扇区再次细分。一个扇区从起始角度到结束角度切成 N 份每份是一个极小的扇形我们用一根柱子去近似它。每一根柱子的中心点就放在这个小扇形角度中间、以及某个半径位置上。理论上分得越细拼出来的扇区边缘越平滑。第三步给柱子赋高度。不同扇区可以按 value 大小映射不同的高度这样饼图每个部分的“厚度”就不一样视觉上会更灵动。我之前做的时候习惯把高度映射成8 value比例 * 50小扇区也有一个最低厚度不至于薄成纸片。这里有一点值得说明因为柱子的俯视投影是正方形而饼图扇区是圆弧形的所以无论怎么切边缘都必然有锯齿感。解决办法是增加切分数并让柱子足够密集。另外如果想做“实心饼”而不是“甜甜圈”可以在半径方向上铺多圈柱子。比如设置 loopCount 2内圈放一排柱子、外圈放一排柱子饼面就会更饱满。柱子的总数量等于扇区数乘以切分数乘以圈数性能要提前评估好。2.2 完整可运行代码下面这段代码是基于 echarts echarts-gl 的完整实现。安装依赖时直接执行npm install echarts echarts-gl即可注意 echarts 用 5.x、echarts-gl 用 2.0.x 是比较稳的组合。import * as echarts from echarts; import echarts-gl; const chartDom document.getElementById(chart); const myChart echarts.init(chartDom); const pieData [ { name: 搜索引擎, value: 1240 }, { name: 直接访问, value: 780 }, { name: 邮件营销, value: 560 }, { name: 联盟广告, value: 320 }, { name: 视频广告, value: 210 } ]; const palette [#5470c6, #91cc75, #fac858, #ee6666, #73c0de]; function buildPie3D(pieData, radius 80) { const points []; const meta []; const total pieData.reduce((sum, item) sum item.value, 0); const sliceCount 90; // 每个扇区切分成多少根柱子 const loopCount 2; // 半径方向布置几圈柱子 let angleStart -90; // 起始角从12点方向开始 pieData.forEach((item, idx) { const angleSpan (item.value / total) * 360; const height 8 (item.value / total) * 50; // 高度映射 for (let i 0; i sliceCount; i) { // 当前小份柱子的起始角和结束角 const a1 ((angleStart (angleSpan / sliceCount) * i) * Math.PI) / 180; const a2 ((angleStart (angleSpan / sliceCount) * (i 1)) * Math.PI) / 180; const midA (a1 a2) / 2; // 小扇形中心角度 for (let j 0; j loopCount; j) { // 内圈取 0.3R外圈取 0.75R柱子中心落在半径中点附近 const r radius * (0.3 0.45 * j); const x Math.cos(midA) * r; const y Math.sin(midA) * r; points.push({ value: [x, y, height], itemStyle: { color: palette[idx % palette.length] } }); meta.push({ name: item.name, value: item.value }); } } angleStart angleSpan; }); return { points, meta }; } const { points, meta } buildPie3D(pieData, 80); const option { tooltip: { trigger: item, formatter: (params) { const info meta[params.dataIndex]; return info ? ${info.name}br/${info.value} : params.value.join(, ); } }, xAxis3D: { type: value, min: -90, max: 90 }, yAxis3D: { type: value, min: -90, max: 90 }, zAxis3D: { type: value, max: 60 }, grid3D: { show: false, boxWidth: 130, boxHeight: 130, boxDepth: 130, viewControl: { alpha: 35, // 俯仰角 beta: 0, // 左右旋转角 distance: 260, // 视距 autoRotate: true, // 自动旋转 autoRotateSpeed: 4 }, light: { main: { intensity: 1.1, shadow: false }, ambient: { intensity: 0.5 } } }, series: [ { type: bar3D, data: points, barSize: 2.8, // 柱子宽度影响拼接密度 bevelSize: 0.4, // 倒角尺寸 bevelSmoothness: 4, // 倒角平滑度 shading: lambert, // 光照模型 itemStyle: { opacity: 0.96 } } ] }; myChart.setOption(option);把这段代码跑起来看到的是一块悬浮在空间里的“厚饼”不同扇区有不同高度鼠标拖拽可以旋转视角。官网的在线编辑器可以直接跑 echarts-gl新手调试这类3D效果比本地起工程快很多。2.3 参数调优和视觉打磨这套方案能不能出效果几乎全看参数。我第一次做的时候特征就是“远看像一堆积木”后来挨个调参才明白这里面的门道。扇区切分数 sliceCount 直接决定拼接细腻度。建议最小值不要低于60我常用90到120。切分数太高会显著增加柱子数量导致渲染变卡。barSize 和切分数要配合着调barSize 太小柱子之间会有明显间隙饼面会“漏风”太大柱子又会相互重叠边缘糊成一片。正常情况下半径80、切分数90时barSize 设在2到4之间比较稳妥。loopCount 控制半径方向上的圈数。想要“实心饼”2到3圈就够只做一圈也可以视觉上更像“厚底甜甜圈”看甲方喜好。但圈数每加1柱子总数就翻一倍性能压力会成倍增加。shading 要选 lambert不要选 realistic。realistic 的光照计算开销大容易掉帧在大屏场景下根本不划算。光照强度上主光源 intensity 到1.0到1.2最舒服太高扇区会白成一片太低又会显得脏。bevelSize 我习惯给0.3到0.5柱子边缘有一点点圆角立体感立刻上去一个档次。视角方面alpha 给30到40、beta 给0就是从正前方偏上一点点俯视这是新闻里3D饼图最常见的角度信息面积最大、变形最小。如果甲方要求自动旋转autoRotateSpeed 给3到5就行转太快看久会晕。3. 不想引入GL2D饼图也能伪出立体感3.1 双层偏移渐变造厚度bar3D方案虽然效果硬核但有一个绕不开的痛点图例联动失效。因为series里是一个个柱子不是一个扇区ECharts的legend认不出“这组柱子属于搜索引擎”。业务方要是想快速查看某一项、点击图例参与联动这个方案就得打折扣。所以我更常推荐第二套方案原生pie组件通过“阴影层 渐变层”叠加做出立体效果。思路其实很简单。先铺一层底色饼图radius 设得比主图大几个百分点center 往下偏移一点点颜色用很淡的灰或深色低透明度这层用来模拟侧边厚度。再铺主饼图radius 略小、center 居中每个扇区用垂直方向的线性渐变填充上方亮、下方暗模拟受光面和背光面。两层叠加以后视觉上就是一块“有厚度、有光照”的立体饼图。const palette [#5470c6, #91cc75, #fac858, #ee6666, #73c0de]; function gradientFill(baseColor) { // 垂直渐变上方保持亮色下方加深模拟厚度阴影 return new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: baseColor }, { offset: 1, color: rgba(0,0,0,0.55) } ]); } option { tooltip: { trigger: item }, series: [ { name: 厚度阴影, type: pie, radius: 62%, center: [50%, 52%], data: pieData.map(d ({ name: d.name, value: d.value })), silent: true, label: { show: false }, itemStyle: { color: rgba(0,0,0,0.25) } }, { name: 主饼图, type: pie, radius: 58%, center: [50%, 50%], data: pieData.map((item, idx) ({ name: item.name, value: item.value, itemStyle: { color: gradientFill(palette[idx]) } })), label: { show: true, formatter: {b}\n({d}%) }, labelLine: { length: 18, length2: 12 }, emphasis: { scale: true, scaleSize: 6 } } ] };看起来是不是像双层汉堡这就是2D伪3D的基本盘。实际项目里我还会在底层再叠一层更大的浅色饼图做“地面投影”透明度压到0.1左右立体感会更柔和。3.2 适用场景和边界这套方案的优点是开发成本极低交互完整。图例点击、数据高亮、下钻、tooltip、动画全部走ECharts原生逻辑你不需要给业务方解释“为什么点击图例没反应”。数据更新也比较省心直接用 setOption 换 data 就能跑。大屏项目里如果只是要一个“视觉上差不多的3D饼图”我强烈建议从这套方案开始。边界也很清楚它不是真的3D。无论怎么配置它只能从一个固定角度观看不能拖动旋转。有人尝试给外层容器加 CSS 的 perspective 和 rotateX 来做倾斜视角我也试过效果并不理想因为canvas内部的文字、标签会跟着形变看起来是糊的不建议这么干。4. 常见问题与调优实录这一节把我在实际过程中踩过的坑和查到的解决方案集中整理出来很多都是社区里反复出现的高频问题。4.1 tooltip不换行、显示不了业务值先说不换行。原生pie的tooltip默认是直接把 name、value、percent 拼在一行里名称一长就会撑爆提示框。解决方法是 formatter 里手动插入换行符并且用 extraCssText 控制提示框的宽度和换行规则。tooltip: { trigger: item, formatter: (params) ${params.name}br/数值${params.value}br/占比${params.percent}%, extraCssText: max-width:200px;white-space:normal;word-break:break-all; }再说 bar3D 方案的tooltip。因为series里的每一个数据项是一根柱子而不是一个扇区params.value 默认是[x, y, height]业务上的原始数值拿不到。我的做法是在生成柱子的同时维护一个meta数组通过 params.dataIndex 去meta里反查原始数据。具体实现见上面2.2的完整代码。4.2 vue3 rem适配图表失效这个热搜词对应的场景我太熟了。vue3项目里一般会用 postcss-pxtorem 把 css 里的 px 自动转成 rem结果发现ECharts图表里的字体、图形大小完全不跟随屏幕缩放。原因很简单postcss-pxtorem 只处理CSS样式而ECharts在 init 时读取的是容器元素的像素宽高后续绘制的所有图形、文字都发生在canvas内部根本不会经过postcss。所以“rem对图表没效果”不是bug是ECharts的绘制机制使然。解决方案分两层。第一图表的容器不要用固定px用百分比、vw或者flex布局保证它在不同分辨率下能拿到合理的宽高。第二监听窗口变化后调用 chart.resize()必要时重新计算基于容器宽度的字体大小。大屏场景下我会把根字号按设计稿比例动态调整然后 chart.resize() 前先重新 setOption 刷新一遍 textStyle 相关的配置这样能保证文字大小跟着整体缩放走。4.3 labelLine小圆点偏移这个现象在普通饼图和伪3D方案里都会碰到扇区标签的引导线末端那个小圆点离文字或者扇区边缘忽远忽近位置很怪。大多数情况下是 avoidLabelOverlap 在起作用。ECharts 开启这个选项后会自动调整标签位置避免重叠调整过程中引导线的锚点就会跟着偏移。 解决思路是如果扇区不多、文字不密集直接关闭 avoidLabelOverlap如果必须开启就把 labelLine 的 length 和 length2 调大一些给标签避让留出更多空间。还可以给 label 设置 edgeDistance控制标签与饼图边缘的距离减少引导线拐弯造成的视觉错位。bar3D方案里没有labelLine这个概念因为每个柱子就是一个数据项。如果要在3D场景中显示文字我是建议直接用 tooltip 和 legend 做信息展示不要硬上 label3D旋转时文字跟随的成本很高效果也不稳定。4.4 大屏性能与交互设置bar3D 方案的性能是大屏场景里必须提前考虑的问题。柱子的总数等于扇区数乘以 sliceCount 再乘以 loopCount按5个扇区、90份切割、2圈计算就是900根柱子这在多数设备上已经够用。但如果扇区数超过10个我建议 sliceCount 降到50、loopCount 设为1先保住帧率再说。grid3D.viewControl 里的旋转能力在展示场景也要“收着点”。自动旋转开起来很炫但业务方在看数据时往往会嫌干扰我一般会做成“默认静止、鼠标悬浮可旋转”的模式也就是把 autoRotate 设为 false然后通过 rotateSensitivity 控制用户旋转的灵敏度。alpha 和 beta 建议锁在第一视角避免用户不小心拖到奇怪的角度后回不去。另外light.main 里的 shadow 默认关掉就好。3D柱状图开启阴影后需要实时计算投影帧率会明显下降而它对“这是不是个饼图”这个核心判断的帮助又很小得不偿失。问题常见原因解决方向tooltip不换行、宽度固定formatter未处理、默认样式限制formatter加换行符extraCssText设置宽度与换行bar3D的tooltip看不到业务值数据项是柱子坐标而非扇区数据维护meta索引通过dataIndex反查rem对图表无效canvas内部不经过postcss容器用百分比监听resize后chart.resize()labelLine末端小圆点偏移avoidLabelOverlap自动避让关闭避让或调整length、edgeDistance3D饼图卡顿柱子数量过多降低sliceCount、loopCount关闭shadow点击图例无响应bar3D数据项无法按扇区聚合改2D伪3D方案或自行实现图例联动最后说一句我从这个需求里得到的体会如果3D饼图要放进一个数据更新频繁、图表很多的指挥大屏我会优先选2D伪3D方案维护成本低、图例交互完整如果是单图展示、重点是视觉冲击bar3D方案确实更带感但要想清楚图例联动和数据刷新的成本。没有哪一种方案是银弹先问清楚业务方要的是“好看”还是“能交互”再选型往往比闷头写代码更管用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。