echarts-gl实战:Vue中打造可交互的3D饼图与环形图 做可视化大屏这几年我接到过最多的需求就是“这个饼图能不能做成3D的”。产品经理不会管你用的是ECharts还是Three.js他们只看到竞品大屏上那个能旋转、会发光的立体饼图然后转头让你照着做一个出来。如果你也是Vue技术栈我建议先别急着上Three.jsECharts官方生态里的echarts-gl扩展用pie3D系列就能在Vue项目里实现3D饼图和环形图开发成本低得多踩坑也有迹可循。这篇内容我会从原理讲到落地把3D饼图和环形图的实现拆开揉碎包括坐标转换的核心算法、Vue组件里的完整代码、高亮选中和自动旋转的交互细节以及我在实际项目中踩过的坑。适合已经会用ECharts画普通2D饼图、但第一次接触echarts-gl的开发者。1. 先想明白为什么原生ECharts画不了3D饼图1.1 zrender是2D渲染引擎pie系列天生是平面图形ECharts底层的渲染引擎叫zrender它是一个2D画布渲染库。你平时用的折线图、柱状图、饼图本质都是在二维坐标系里计算位置然后通过Canvas或者SVG绘制图形。所以原生ECharts的pie系列只有一个平面圆形视角你给它加多少shadowBlur、多少光影效果它都只是一个“看起来有点立体感”的扁平饼图并不是真正的三维几何体。这一点想通之后你就明白为什么网上搜“ECharts 3D饼图”会搜到各种五花八门的实现方式了。有人用两个2D饼图叠在一起“伪3D”有人用柱状图去模拟扇区这些都是绕过底层限制的偏方。用当然能用但一旦涉及旋转、视角切换就全部露馅了。1.2 echarts-gl的pie3D系列解决了什么echarts-gl是ECharts官方团队发布的三维可视化扩展它底层借助了WebGL渲染并且封装了几种常用的三维图表类型比如3D散点图、3D柱状图、3D曲面图还有我们今天的主角pie3D系列。pie3D的巧妙之处在于它并没有像Three.js那样让你从零开始建场景、建相机、建网格体而是沿用ECharts的option配置体系。你只需要给每个扇区写一个seriestype设为pie3D再配上parametricEquation参数方程ECharts就会自动在三维场景里生成对应的扇形曲面。换句话说你写的还是ECharts配置但渲染出来的却是真正的三维图形可以旋转、缩放、加光源。1.3 方案对比Three.js vs echarts-gl vs CSS 3D我把三种主流方案放在一起对比过各自的优劣势非常明显方案实现成本3D真实感与ECharts联动适合场景Three.js高需要理解场景、相机、几何体、材质最高可以任意定制需要手动整合数据麻烦复杂3D场景、工业仿真echarts-gl pie3D低沿用option配置中等偏上光影和材质可选天然兼容图例、tooltip都是现成的大屏可视化、业务报表CSS 3D 2D饼图低但效果简陋低仅做视觉欺骗一般只需要静态截图时我个人建议如果只是做一个展示用的3D饼图优先选echarts-gl。等你真遇到了pie3D解决不了的定制需求再考虑Three.js也不迟。后面我讲的所有实现都基于echarts-gl。2. 环境准备与版本选型这一步能劝退一半的人2.1 安装echarts-gl在Vue项目里安装依赖常规操作是同时装上echarts和echarts-glnpm install echarts echarts-gl --save # 或者 yarn add echarts echarts-gl装完之后关键一步是引入。echarts-gl不是一个独立的图表库它是ECharts的插件扩展所以必须在引入ECharts之后再引入echarts-glimport * as echarts from echarts; import echarts-gl;注意import echarts-gl这一行没有显式的导出变量它的作用是执行副作用在ECharts内部注册pie3D、bar3D等系列。如果你把它放在echarts之前引入或者漏掉了这一行后面写type: pie3D的时候就会报错提示你series类型不存在。2.2 版本兼容矩阵版本坑是我见过最多的。echarts-gl的版本和echarts必须匹配否则会出现各种莫名其妙的报错比如图形不渲染、点击无响应、控制台报Cannot read property xxx of undefined。echarts版本推荐echarts-gl版本说明echarts 4.xecharts-gl 1.x老项目常见API相对稳定echarts 5.xecharts-gl 2.x目前主流组合配合Vue 3无压力如果你用的是echarts 5.4以上的版本建议直接上echarts-gl的最新版2.0.9及以上。如果项目里已经有旧的echarts 4.x不要强行升级echarts到5.x直接把echarts-gl降到1.x更稳妥。我见过有人升级完echarts之后发现整个项目的图表样式全都变了因为5.x默认主题、坐标轴配置都有调整这种升级成本不划算。2.3 Vue2和Vue3中引入方式差异Vue2和Vue3在引入ECharts时有点不一样。Vue2一般在main.js里全局挂载// Vue2 import Vue from vue; import * as echarts from echarts; import echarts-gl; Vue.prototype.$echarts echarts;Vue3推荐用组合式API直接在需要的组件里按需引入// Vue3 import * as echarts from echarts; import echarts-gl; import { onMounted, onBeforeUnmount, ref } from vue; const chartRef ref(null); let chart null; onMounted(() { chart echarts.init(chartRef.value); chart.setOption(generateOption()); }); onBeforeUnmount(() { chart chart.dispose(); });不管哪种方式核心的区别只在初始化生命周期和实例管理上ECharts本身的option写法是一样的。这篇文章后面的代码示例我以Vue3的写法为主Vue2读者只需要把生命周期钩子对应改成mounted、beforeDestroy即可。2.4 引入后第一个报错的排查思路如果你安装完依赖、引入也写对了但页面一加载就报错最常见的报错就是这几种第一Component series.pie3D not exists. Load it first.这个就是echarts-gl没有成功注册检查一下import顺序确保echarts-gl在echarts之后引入。第二TypeError: Cannot read properties of undefined (reading getShader)大概率是echarts-gl和echarts版本不匹配按照前面的版本对应表调整。第三页面空白控制台没有报错但canvas元素渲染出来是空的。这种情况往往是容器高度为0或者option里的series数据结构不对。先用一个固定的height样式排除掉容器问题再查series数据。3. 核心算法扇区如何从二维角度变成三维曲面3.1 曲面参数方程getParametricEquationecharts-gl的pie3D本身不是一个真正意义上的“饼图系列”它更像是一个参数化曲面生成器。你需要告诉它这个扇区从哪个角度开始、到哪个角度结束、内部半径是多少、外部半径是多少、悬停时要不要放大。这些信息被打包成一个曲面参数方程ECharts再根据这个方程生成三维网格体。先看核心函数这是整个3D饼图的地基function getParametricEquation(startRatio, endRatio, isSelected, isHover, k, h, internalDiameterRatio) { // 计算起始角度和结束角度 const midRatio (startRatio endRatio) / 2; const startRadian startRatio * Math.PI * 2; const endRadian endRatio * Math.PI * 2; const midRadian midRatio * Math.PI * 2; // 如果只有一个扇区则不使用3D效果始终保持在同一平面 if (startRatio 0 endRatio 1) { isSelected false; } // 计算选中态下的扇区偏移量k是偏移系数 const offsetX isSelected ? Math.cos(midRadian) * k : 0; const offsetY isSelected ? Math.sin(midRadian) * k : 0; // 计算悬停放大效果hoverRate决定放大倍数 const hoverRate isHover ? 1.05 : 1; return { u: { min: startRadian, max: endRadian, step: (endRadian - startRadian) / 100 }, v: { min: internalDiameterRatio || 0, max: 1, step: 0.01 }, x: function (u, v) { return v * Math.sin(u) * hoverRate offsetX; }, y: function (u, v) { return v * Math.cos(u) * hoverRate offsetY; }, z: function (u, v) { return 0; } }; }3.2 参数背后的几何意义这个函数看起来复杂其实拆开就三个关键点。第一个关键是u和v。u是角度方向的自变量范围从startRadian到endRadian对应着扇区在圆环上扫过的角度范围。v是半径方向的自变量从internalDiameterRatio到1internalDiameterRatio是0就是实心饼图大于0就变成环形。这样理解之后3D饼图和环形图的区别就一句话改v的最小值。第二个关键是x和y的计算公式。x v * Math.sin(u)y v * Math.cos(u)这是标准的极坐标转直角坐标公式。v控制离圆心的距离u控制角度方向。如果你上过高中的三角函数课这一步很好理解。注意z固定为0因为扇形在三维空间里是平的厚度由ECharts内部自动生成。第三个关键是hoverRate和offsetX、offsetY。hoverRate是悬停放大系数这里设成1.05意思是悬停时扇区整体放大5%。offsetX和offsetY是选中状态下的位移等你想实现点击扇区“弹出来”的效果时这两个偏移量就派上用场了。3.3 数据加工函数getPie3D每个扇区独立成一个series有了曲面方程下一步就是把业务数据加工成ECharts认得的series数组。这里有一个非常重要的设计每个扇区都不是同一个饼图里的一个扇区而是独立的series。function getPie3D(pieData, internalDiameterRatio) { const series []; let sumValue 0; let startValue 0; let endValue 0; const k 0.3; // 扇区被选中时向外偏移的系数 const h 1; // 扇区高度比例 // 计算数据总和 for (let i 0; i pieData.length; i) { sumValue pieData[i].value; } // 为每个扇区生成一个独立的pie3D series for (let i 0; i pieData.length; i) { const startRatio startValue / sumValue; endValue startValue pieData[i].value; const endRatio endValue / sumValue; series.push({ name: pieData[i].name, type: pie3D, data: [ { name: pieData[i].name, value: pieData[i].value, itemStyle: { color: pieData[i].color } } ], label: { show: true, formatter: pieData[i].name \n pieData[i].value, position: outside, distance: 2, textStyle: { fontSize: 12, color: #333 } }, emphasis: { label: { show: true, fontSize: 16, fontWeight: bold } }, parametric: true, shading: lambert, startRatio: startRatio, endRatio: endRatio, internalDiameterRatio: internalDiameterRatio || 0, parametricEquation: getParametricEquation( startRatio, endRatio, false, false, k, h, internalDiameterRatio ) }); startValue endValue; } return series; }这段代码的逻辑是先把所有数据的value加起来算出总和然后遍历原始数据根据每一项value在总和中的占比算出它的起始角度比例和结束角度比例也就是startRatio和endRatio。这两个比例会同时传给series配置和parametricEquation保证图层的几何形状和数据角标完全对应。3.4 为什么官方示例要先对数据排序我在很多博客里看过这个函数但很少有人讲清楚为什么要对数据排序。官方demo里有一行pieData.sort((a, b) a.value - b.value);这行代码在数据总量小的时候你感受不到区别但一旦你的数据里有特别小的扇区比如只有1%的占比而它前面刚好是一个60%的大扇区小扇区在渲染时会被大扇区遮挡导致鼠标悬停、点击都难以命中。排序之后小扇区会排列在相邻位置配合后面要讲的选中逻辑交互体验会好很多。当然排序也意味着你的扇区顺序变了。如果产品方对扇区顺序有硬性要求可以牺牲交互体验不排序但要做好小扇区难以点击的心理准备。我一般会在拿到数据后先判断如果存在占比小于5%的扇区就排序如果所有扇区都比较大就保持原始顺序。4. 从3D饼图到环形图一个内径参数就够了4.1 internalDiameterRatio是环形的核心秘密环形图和饼图的差异用文字描述就是“中间挖掉一个洞”。在getParametricEquation里v的最小值控制的就是这个洞的半径。internalDiameterRatio传0时v从0开始整个圆形区域都是扇区的覆盖范围这就是完整的饼图internalDiameterRatio传0.6时v从0.6开始圆心半径0.6以内的区域就没有图形这就是环形图。实际调用时你只需要把getPie3D的第二个参数改一下// 3D饼图 const seriesData getPie3D(pieData, 0); // 3D环形图内径占外径的60% const seriesData getPie3D(pieData, 0.6);注意getParametricEquation里还有一个h参数控制扇区的高度比例。如果internalDiameterRatio比较大比如0.7以上建议把h适当调低一些因为环带越窄厚度太大看起来会很臃肿失去环形图该有的精致感。4.2 环形图中心放文字推荐的方案环形图最常见的用途就是把核心指标放在中间的洞里。2D环形图用title属性就能轻松实现但3D场景下title是平面文本它会一直正对着屏幕不跟随三维场景旋转。这样在初始视角没问题一旦用户拖动旋转中心文字和环形图就会脱节。我的方案是用graphic组件添加一个始终居中的文本图形这样文字始终保持在画布中心视觉上最稳const option { graphic: { type: text, left: center, top: center, style: { text: 总销售额\n1,285,600, textAlign: center, textVerticalAlign: middle, fontSize: 18, fontWeight: bold, fill: #333 } }, series: getPie3D(pieData, 0.6) };graphic组件的一个好处是它的位置是相对于整个canvas的不参与三维变换。另一个更灵活的方案是在环形图上层再叠加一个普通的2D饼图series把它的半径设置成0这样它只负责显示label和tooltip不绘制图形。这种做法适合需要每个扇区单独显示数值的场景但配置复杂度高一些性能也略有损耗。4.3 多环嵌套的扩展思路理解了internalDiameterRatio的原理之后多环嵌套就是水到渠成的事。比如你想做一个三层的环形图每一层展示不同维度的数据只需要生成三组seriesconst outerRing getPie3D(outerData, 0.5); const middleRing getPie3D(middleData, 0.35).map(s ({ ...s, parametricEquation: getParametricEquation(s.startRatio, s.endRatio, false, false, 0.3, 0.7, 0.5) })); const innerRing getPie3D(innerData, 0.2).map(s ({ ...s, parametricEquation: getParametricEquation(s.startRatio, s.endRatio, false, false, 0.3, 0.5, 0.35) }));多环嵌套的难点在于每个环的厚度和半径范围需要手动协调。最外层v从0.5到1中间层v从0.35到0.5最内层v从0.2到0.35这样三层环带刚好拼接成一个完整的圆盘。注意每个环的数据要单独计算总和和角度比例不能直接用同一个startRatio。5. 让图表真正好用悬浮放大、选中偏移与自动旋转5.1 通过mouseover和mouseout实现扇区高亮3D饼图做好之后交互就是决定它好不好用的关键。最基础的需求是鼠标悬停到某个扇区上时这个扇区要“鼓”起来。echarts-gl的pie3D对emphasis状态的支持不是特别完善用原生dispatchAction配合highlight类型在部分版本里有兼容问题。我实践下来最稳定可靠的方式是监听鼠标事件后重新生成对应扇区的parametricEquation把isHover参数从false改成true然后通过setOption更新。代码写起来是这样// 监听鼠标悬停 chart.on(mouseover, (params) { const seriesIndex params.seriesIndex; // 重新生成当前扇区的parametricEquation开启悬停放大 updatePie3D(seriesIndex, true); }); chart.on(mouseout, (params) { const seriesIndex params.seriesIndex; // 关闭悬停放大 updatePie3D(seriesIndex, false); }); function updatePie3D(seriesIndex, isHover) { const series chart.getOption().series; const currentSeries series[seriesIndex]; const k 0.3; const h 1; const newEquation getParametricEquation( currentSeries.startRatio, currentSeries.endRatio, currentSeries.selected || false, isHover, k, h, currentSeries.internalDiameterRatio || 0 ); chart.setOption({ series: { id: currentSeries.id, parametricEquation: newEquation } }); }注意这里我用id来定位series而不是重新传整个series数组。因为每次setOption如果都assign整个series数组会导致所有扇区的状态被重置高亮一个扇区其他扇区的label、颜色、透明度都会被刷新视觉上会闪动。给每个series加上唯一id就可以精准更新目标扇区。5.2 点击选中与取消选中的完整链路选中态比悬停态多了一个需求点击一次扇区扇区向外弹开再次点击它缩回去。而且同时只能有一个扇区处于选中状态。实现思路是先记住当前选中的seriesIndex然后在点击事件里处理三件事把上一次选中的扇区取消选中把当前点击的扇区设置为选中更新所有相关扇区的parametricEquation。let selectedIndex -1; chart.on(click, (params) { const seriesIndex params.seriesIndex; const series chart.getOption().series; // 先把所有扇区的选中状态都设为false const prevSelectedIndex selectedIndex; selectedIndex (prevSelectedIndex seriesIndex) ? -1 : seriesIndex; series.forEach((item, index) { const isSelected index selectedIndex; const newEquation getParametricEquation( item.startRatio, item.endRatio, isSelected, false, 0.3, 1, item.internalDiameterRatio || 0 ); chart.setOption({ series: { id: item.id, parametricEquation: newEquation } }); }); });这里有一个细节值得注意当扇区向外偏移时它的图形位置变了但label还是基于原始坐标计算的。echarts-gl的pie3D会自动把label绑定到扇区的几何中心所以偏移之后label会跟着走这一点倒是不用额外处理。5.3 自动旋转与用户交互后的智能停止大屏场景下3D饼图放到页面上之后产品通常要求它自己转起来。这个需求用viewControl.autoRotate就能实现const option { series: getPie3D(pieData, 0).map(s ({ ...s, viewControl: { alpha: 30, // 初始俯仰角 beta: 0, // 初始方位角 distance: 220, // 相机距离 center: [0, 0, 0], // 相机中心点 autoRotate: true, // 自动旋转 rotateSensitivity: 1, // 手动拖动灵敏度 zoomSensitivity: 1, // 缩放灵敏度 minAlpha: 5, // 最小俯仰角 maxAlpha: 90 // 最大俯仰角 } })) };注意每个series都需要配一个viewControl因为它们共用同一个三维场景但是配置是分散在各series里的。实际生效的规则是后配置的会覆盖先配置的所以你在map时给每个series加上相同的viewControl配置就行。自动旋转还有一个体验优化用户一旦手动拖动图形就应该停止自动旋转否则边拖它边自己转体验非常差。监听rotate事件chart.on(glrotate, () { // 用户手动旋转后关闭所有series的自动旋转 chart.setOption({ series: chart.getOption().series.map(s ({ id: s.id, viewControl: { autoRotate: false } })) }); });这个事件在echarts-gl的文档里叫glrotate触发条件就是相机视角发生变化。结合需求你可以把它做成“用户手动操作3秒之后再恢复自动旋转”给大屏演示增加一点智能感。6. 实战中的坑与视觉打磨6.1 图例点击后3D图形消失的解决方案这是我在真实项目里被问得最多的问题用getPie3D生成的series数组每个扇区都是独立的series所以ECharts的legend默认会把每个series当成一个图例项。点击图例的某个项时ECharts会执行默认的series隐藏操作把对应的series隐藏掉。问题来了因为每个series都只包含一个扇区隐藏一个series就等于挖掉饼图的一块整个3D饼图变得残缺不全跟2D饼图里图例点击收起某个数据项的效果完全不一样。各种方案都试过之后最省事的处理是给legend配置selectedMode并监听事件自己控制显示隐藏chart.on(legendselectchanged, (params) { // 阻止默认行为 params.event.event.preventDefault(); const series chart.getOption().series; series.forEach((item, index) { const shouldShow params.selected[item.name]; chart.setOption({ series: { id: item.id, data: [{ name: item.name, value: shouldShow ? item.data[0].value : 0, itemStyle: { color: item.data[0].itemStyle.color, opacity: shouldShow ? 1 : 0 } }] } }); }); });通过把对应扇区的透明度改成0而不是直接隐藏series饼图的形状不会断裂只是那个扇区“变透明了”。视觉上看起来就是图例收起时扇区消失但整体圆形轮廓还在。你可以在透明扇区上叠加一个浅灰色半透明底让用户知道这里本来有一个数据项只是被隐藏了。6.2 label位置漂浮、重叠与显示不全3D饼图的label问题比2D复杂得多因为图形本身会旋转而label是跟随三维坐标计算后投影到屏幕上的。最典型的现象是旋转到某个角度时label会跟扇区重叠、互相遮挡甚至跑到画面之外。我的习惯是调整初始视角让所有扇区的label在进入页面时都处于可读状态。首先用viewControl的alpha和beta控制俯仰角和方位角一般alpha设30度到45度、beta设0度时大部分场景的label都不会被遮挡。其次在label配置里加上富文本格式化控制换行和长度label: { formatter: (params) { const name params.name; const value params.value; // 超过6个字符换行 const nameArr name.length 6 ? name.slice(0, 6) \n name.slice(6) : name; return ${nameArr}\n${value}; }, textStyle: { fontSize: 12, lineHeight: 18 } }还可以利用distance参数把label往外推减少跟扇区表面的重叠label: { position: outside, distance: 3 }如果你对label的位置精度要求特别高比如要精确对齐到扇区中心方向可以给每个series单独配label的formatter在里面用params.percent做百分比信息展示而不是只展示原始数值。6.3 大屏适配与resize时的三维参数同步可视化大屏项目通常会用rem或者vw/vh做整体缩放这时候ECharts经常会遇到一个问题容器宽高变了但是canvas内部的文字和图形没有跟着缩放。2D图表通过chart.resize()能解决大部分问题但3D饼图多了一个维度——viewControl里的distance、alpha这些三维参数不会因为canvas尺寸变化而自动适配。我的做法是在resize事件里同时调整distancefunction handleResize() { const width chart.getWidth(); // 根据容器宽度动态调整相机距离 const newDistance width 600 ? 160 : width 1000 ? 220 : 280; chart.resize(); chart.setOption({ series: chart.getOption().series.map(s ({ id: s.id, viewControl: { distance: newDistance } })) }); } window.addEventListener(resize, handleResize);distance的值越大相机离图形越远3D饼图看起来就越小。如果容器宽度缩小了但distance不变饼图就会超出可视范围。这里我用宽度做分档动态计算简单实用。更精细的做法是用一个比例函数把distance跟容器宽度绑定成线性关系。6.4 echarts-gl体积与首屏性能优化echarts-gl的体积不小打包后大概会占用几百KB。如果项目里只有这一个页面需要3D饼图直接在主入口引入会拖慢首屏加载。我常用的优化手段是路由级懒加载// 路由配置 { path: /dashboard, component: () import(/views/Dashboard.vue) }然后在Dashboard.vue组件里再用动态import引入echarts-glonMounted(async () { const echarts await import(echarts); await import(echarts-gl); chart echarts.init(chartRef.value); // 初始化数据 });这样echarts-gl的代码只会在进入仪表盘页的时候才加载。如果首页本身不依赖3D图表这个优化能明显减少首屏网络请求体积。还有一个容易忽略的优化点shading参数。默认的lambert光照模式性能最好realistic模式带真实物理光照效果但对GPU的压力明显增大。如果你的大屏机器性能一般或者饼图旁边还有其他图表在动画强烈建议用lambert视觉上差异很小性能却稳得多。6.5 颜色与光源的搭配心得3D饼图跟2D饼图最大的视觉差别就在于光影。2D饼图的颜色是“平面的”3D饼图的颜色会因为光照产生明暗变化。如果你不给光源配置lambert模式下的默认光线虽然能用但扇区的立体感会差一些看起来灰蒙蒙的。我给3D饼图配光源的标准配置是环境光加平行光环境光保证暗面细节可见平行光负责塑造立体感light: { main: { intensity: 1.2, shadow: true, alpha: 40, beta: 20 }, ambient: { intensity: 0.3 } }颜色选择上3D饼图不适合用太浅的颜色因为光照后高光区域会发白浅色扇区的边界会看不清。我一般建议用中等饱和度和中等明度的颜色比如#5470c6、#91cc75、#fac858这一组ECharts默认色在3D场景下表现就很稳定。7. 数据量变大时的性能表现与实际限制7.1 扇区数量对渲染和交互的影响3D饼图跟2D饼图在性能上有一个很本质的区别每个扇区都是一个独立的网格体都要占据独立的渲染批次。2D饼图哪怕有20个扇区也只是同一个图形通过不同角度绘制出来的子路径渲染开销几乎可以忽略。但3D饼图每多一个扇区就多一个独立的series和独立的几何体GPU的draw call数量就多一次。我实测过在普通办公电脑上6个扇区的3D饼图帧率稳定在60帧但数据增加到12个时开启动画和高亮交互后帧率会掉到30帧附近卡顿感在旋转时尤其明显。超过15个扇区时悬停和点击的响应也会有明显延迟。如果数据确实很多我的建议是提前做数据归并把占比很小的扇区合并成一个“其他”类别。一般以占比2%到3%为阈值这样扇区数量能控制在8个以内视觉上比较清爽性能也跟得上。另一个思路是把小扇区做瘦身处理让它们共享同一个扇区组。7.2 大数据量下的精简方案有一种既不牺牲数据完整度、又能保住性能的方案是把小扇区合并但用副饼图展示明细信息类似2D饼图里的南丁格尔玫瑰图交互模式。但3D场景里再做副饼图会很乱我自己更推荐直接做成图例列表配合选中交互。扇区数量少的时候展示3D图形点击扇区后在图例区展示该扇区对应的明细数据列表这样主视觉保持简洁交互深度也不缺。还需要提的是tooltip。ECharts原生的tooltip在3D场景下触发机制和2D略有不同鼠标移动到扇区表面时只有命中了几何体上的三角面才能触发tooltip。扇区很小或者处于旋转状态时命中检测会有些不稳定。我一般在3D饼图上不依赖tooltip展示详情而是把label和legend用足详细信息留给点击事件后的侧面面板展示。7.3 低配置设备上的降级策略大屏项目经常要跑在很旧的电脑上甚至有一些客户现场是浏览器版本都很老的Windows主机。echarts-gl依赖WebGL如果设备不支持WebGL或者浏览器禁用了硬件加速整个画布都会空白。这种场景需要在代码里做降级检测function isWebGLAvailable() { try { const canvas document.createElement(canvas); return !!(window.WebGLRenderingContext ( canvas.getContext(webgl) || canvas.getContext(experimental-webgl) )); } catch (e) { return false; } } // 使用时降级到2D饼图 if (isWebGLAvailable()) { // 初始化3D饼图 } else { // 使用普通的2D饼图option }降级方案看着土但在真实项目中救了我好几次。3D饼图做得再炫客户机器跑不动就等于白做。提前准备好2D降级方案交付的时候能少挨很多骂。8. 写在最后的实践提醒3D饼图这个东西看着是锦上添花的视觉效果但真正落地的时候涉及的计算和兼容性细节比2D图表多很多。我前前后后在好几个项目里用过这套方案总结下来记住三件事版本必须锁匹配参数方程要理解再抄交互效果一定要实测。网上很多demo代码拷贝下来直接跑在Vue里不生效十有八九是echarts-gl引入顺序或者版本对不上而不是业务逻辑有问题。最后分享一个小细节数据量很小时3D饼图的转场动画可以用series的enterAnimation和updateAnimation做入场效果配合viewControl的初始alpha从0滑到30度视觉冲击力很强而且代码量很小。如果你在做大屏可视化这个入场细节能给演示环节加分不少。