ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用ECharts与echarts-gl实现3D立体环形图:默认选项与surface参数方程

2026/9/30 16:05:08 拓冰建站 浏览量
用ECharts与echarts-gl实现3D立体环形图:默认选项与surface参数方程 前几天有朋友问我数据大屏上那种带厚度的立体环形占比图到底怎么做能不能直接用 ECharts 出效果。我第一反应是ECharts 自身的饼图只能做平面的二维环形图要真正带高度、有厚度的立体效果必须请出 echarts-gl 这个扩展库。折腾了一整天后我用一个比较取巧但很稳定的方案——用 echarts-gl 的 surface 参数曲面去拼 3D 环形扇区全程没有写一行 Shader效果却足够撑起大屏。这篇文章就把这个方案的完整思路、可复现代码和踩过的坑一次性写清楚尤其是标题里提到的“默认选项”我们完全可以用最少的配置先让 ECharts 和 ECharts-gl 的默认能力把 3D 立体环形图跑起来再逐步精调。1. 需求拆解3D环形图到底是“饼图”还是“建模”1.1 一句话描述这个需求项目标题很简单“Echarts与Echarts-gl构建3D立体环形图展示默认选项”。翻译成项目语言就是在页面上用 ECharts 和 ECharts-gl 配合画一个能转动、有厚度、看起来像甜甜圈的环形占比图并且先用库的默认配置把效果跑出来不做复杂的自定义美化。这个“默认选项”很关键。很多人拿到一个新图表需求第一反应是找现成的大段配置结果光样式就调了两天。我的习惯正好相反先只用默认参数把图形完整渲染出来确认技术路线可行再逐步加坐标轴、光照、交互、配色。所以这篇文章也会按这个节奏来写前面大量代码都是“能跑”的版本不是“最好看”的版本。1.2 需求背后的三个关键词拆开标题核心词有三个ECharts国内数据可视化场景里用得最多的 2D 图表库生态成熟社区案例非常多。ECharts-glECharts 官方推出的 3D 扩展库底层基于 WebGL能在不改变 ECharts 使用习惯的前提下渲染 3D 图形。3D 立体环形图本质是“环形占比图”的立体版本常见于数据大屏、汇报演示、驾驶舱等场景用来展示不同模块的占比关系。这三个词放在一起说明这不是一个纯 3D 建模需求而是一个“在图表框架内快速获得 3D 视觉表现”的需求。因此选型不需要上重型引擎ECharts 加 echarts-gl 正好能覆盖。1.3 技术可行性预判刚开始我也按“饼图 3D 化”的思路找了一圈发现 echarts-gl 官方并没有提供pie3D这样的系列。ECharts 社区里流传的 3D 环形图方案基本都是用surface系列去拼的。把每个数据扇区看作一个独立的曲面多个曲面环形排列就组合成了完整的 3D 环形占比图。这个方案有两个明显优势第一surface是 echarts-gl 的内置系列稳定性有保障第二参数曲面的形状可控性极强只要给出数学公式就能生成完整的环形扇区。所以技术路线一开始就定下来用surfaceparametricEquation参数方程把 3D 环形图做出来。2. 技术选型ECharts ECharts-gl 为什么合适2.1 ECharts-gl 在 ECharts 生态里的位置ECharts 负责传统的 2D 图表echarts-gl 则是它的 3D 扩展包。二者共用同一套setOption配置体系使用体验非常接近。echarts-gl 里比较常用的系列有bar3D、scatter3D、surface、lines3D、map3D等每个系列都能直接嵌套在 ECharts 的 option 中。安装方式也很简单用 CDN先引入echarts.min.js再引入echarts-gl.min.js。用 npm执行npm install echarts echarts-gl然后在代码里import * as echarts from echarts和import echarts-gl。版本方面我实测比较稳的组合是echarts5.4.3加echarts-gl2.0.9。太老的版本配合 ECharts 5 可能会出现 API 兼容问题。2.2 为什么不用 Three.js 硬撸有人会问“3D 环形图用 Three.js 不是更简单吗”确实Three.js 能做得更精细但成本也更高。数据可视化项目的核心是数据表达不是建模精度。使用 Three.js 需要自己处理场景、相机、灯光、坐标系、鼠标交互、窗口自适应、事件绑定等一整套机制开发周期会明显拉长。ECharts-gl 的价值在于它保留了 ECharts 的使用习惯比如grid3D负责 3D 场景viewControl负责视角控制tooltip负责交互提示。我们用一套配置就能获得交互完整的 3D 图形这对大屏项目来说性价比极高。2.3 没有 pie3D就从 surface 入手既然 echarts-gl 没有原生的 3D 饼图系列我们就用surface系列自己造一个。surface本质是参数曲面能根据数学公式生成连续几何体。把环形分成多个扇区每个扇区一个surface实例再用数据占比控制扇区角度就可以得到非常自然的立体环形占比图。同样能做环形效果的还有bar3D思路是把一个个 3D 柱子绕中心排列成环形但那样看起来是“环形柱状图”不是连续的“环形占比图”。两种效果我都测试过最终选了surface因为它在视觉上更接近甜甜圈也更符合“3D 立体环形图”的直觉。3. 搭建基础页面先把3D场景跑起来3.1 引入依赖与最小页面写代码之前先准备一个最小可运行页面。这个页面里什么都不做只初始化图表确保 CDN 依赖没有冲突。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleECharts-GL 3D环形图/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script srchttps://cdn.jsdelivr.net/npm/echarts-gl2.0.9/dist/echarts-gl.min.js/script style #chart { width: 800px; height: 600px; margin: 0 auto; background: #0f1423; } /style /head body div idchart/div script var chart echarts.init(document.getElementById(chart)); console.log(ECharts version:, echarts.version); console.log(ECharts-GL loaded:, !!echarts.gl); /script /body /html如果你打开页面能在控制台看到版本信息说明依赖已经正常加载。这里我特意给容器加了深色背景因为 3D 场景在深色背景下立体感更明显也更符合大屏视觉风格。3.2 基础 option三个 3D 坐标轴3D 系列和 2D 图表最大的区别是它需要完整的 3D 坐标系。在 echarts-gl 里这个坐标系由xAxis3D、yAxis3D、zAxis3D和grid3D共同组成。先给一个最基础的配置chart.setOption({ grid3D: {}, xAxis3D: { type: value }, yAxis3D: { type: value }, zAxis3D: { type: value } });这段代码运行后页面上会出现一个带三维坐标轴的空场景。如果不写grid3D某些版本可能不会正常渲染 3D 系列所以我习惯先显式声明。之后我们要画的圆环会落在这个 3D 坐标系里。3.3 默认选项到底默认了什么“默认选项”并不是一个玄学词。ECharts-gl 在grid3D、viewControl、light等模块上都有默认值。比如viewControl控制 3D 场景的视角默认视角一般是斜上方用户可以用鼠标拖拽旋转。light.main是主光源默认会有一个平行光或点光源保证物体有明暗面。light.ambient是环境光默认存在用来提亮暗部。wireframe是曲面线框surface系列默认不显示线框。表里是我实际体验下来比较重要的默认项参考配置项默认值参考作用说明viewControl.alpha约 40 度俯仰角影响立体感viewControl.beta0 度方位角改变观察方向viewControl.distance约 200相机距离影响画面缩放viewControl.autoRotatefalse是否自动旋转light.main.intensity约 1主光源强度light.ambient.intensity约 0.4环境光强度series.wireframe.showfalse是否显示曲面线框不同版本数值可能有差异但思路一致我们完全可以在不设置这些配置的情况下利用默认值先渲染出立体环形图。接下来就直接进入核心实现。4. 一步一步画出3D环形图4.1 圆环面参数方程一个数学公式搞定立体环要做 3D 环形图首先要会画 3D 圆环。圆环面有一个非常经典的三维参数方程简单说就是用两个角度参数u和v扫出一个环面。u表示绕环形中心的旋转角度范围是 0 到2π。v表示绕圆环截面中心的角度范围也是 0 到2π。R是主半径决定圆环整体大小。r是截面半径决定圆环的粗细。对应的方程是x (R r * cos(v)) * cos(u) y (R r * cos(v)) * sin(u) z r * sin(v)可以想象成一条橡皮筋绕成一个圆圈橡皮筋本身又有一个粗细。R控制圆圈半径r控制橡皮筋粗细。在 echarts-gl 的surface系列里这个方程写起来非常直观。先画一个完整的 3D 圆环chart.setOption({ grid3D: { show: false }, xAxis3D: { type: value }, yAxis3D: { type: value }, zAxis3D: { type: value }, series: [{ type: surface, parametric: true, wireframe: { show: false }, parametricEquation: { u: { min: 0, max: Math.PI * 2, step: Math.PI / 180 }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 90 }, x: function (u, v) { var R 30; var r 8; return (R r * Math.cos(v)) * Math.cos(u); }, y: function (u, v) { var R 30; var r 8; return (R r * Math.cos(v)) * Math.sin(u); }, z: function (u, v) { return r * Math.sin(v); } } }] });打开页面你应该能看到一个银白色的 3D 甜甜圈。它本身没有任何数据含义但这就是后续所有扇区的基础形状。4.2 把占比数据映射到扇区完整圆环只有一个几何体无法表达数据占比。要做成“环形图”必须把数据按比例切成多个扇区每个扇区由一个独立的surface系列渲染。具体做法把整个圆周2π按数据比例切成多段角度区间每段角度对应一个扇区。例如有四条数据占比分别是 30%、26%、24%、20%那么角度区间就按value / total * 2 * Math.PI依次切分。下面这段代码是核心逻辑var data [ { name: 运营中心, value: 30 }, { name: 产品中心, value: 26 }, { name: 技术中心, value: 24 }, { name: 市场中心, value: 20 } ]; var palette [#5470c6, #91cc75, #fac858, #ee6666]; var total data.reduce(function (sum, item) { return sum item.value; }, 0); var R 30; var r 8; var currentAngle 0; var series data.map(function (item, index) { var start currentAngle; var angleLength (item.value / total) * Math.PI * 2; currentAngle angleLength; // 终点多加 0.02 弧度避免相邻扇区衔接处出现裂缝 var end currentAngle 0.02; return { name: item.name, type: surface, parametric: true, wireframe: { show: false }, itemStyle: { color: palette[index], opacity: 1 }, parametricEquation: { u: { min: start, max: end, step: Math.PI / 180 }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 90 }, x: function (u, v) { return (R r * Math.cos(v)) * Math.sin(u); }, y: function (u, v) { return (R r * Math.cos(v)) * Math.cos(u); }, z: function (u, v) { return r * Math.sin(v); } } }; });这里每个surface系列只负责绘制一段圆环曲面多个曲面拼在一起就是一个按数据占比切分的 3D 立体环形图。视觉上很像把一块甜甜圈蛋糕切成了几份每一份代表一类数据。4.3 完整示例默认选项下的3D立体环形图把前面的代码合到一起就是一个可以直接运行的完整页面。这个版本刻意没有设置viewControl、light、label等配置全部走 ECharts-gl 的默认渲染逻辑验证“默认选项”效果。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleECharts-GL 3D环形图 - 默认选项/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script srchttps://cdn.jsdelivr.net/npm/echarts-gl2.0.9/dist/echarts-gl.min.js/script style #chart { width: 800px; height: 600px; margin: 0 auto; background: #0f1423; } /style /head body div idchart/div script var chart echarts.init(document.getElementById(chart)); var data [ { name: 运营中心, value: 30 }, { name: 产品中心, value: 26 }, { name: 技术中心, value: 24 }, { name: 市场中心, value: 20 } ]; var palette [#5470c6, #91cc75, #fac858, #ee6666]; var total data.reduce(function (sum, item) { return sum item.value; }, 0); var R 30; var r 8; var currentAngle 0; var series data.map(function (item, index) { var start currentAngle; var angleLength (item.value / total) * Math.PI * 2; currentAngle angleLength; var end currentAngle 0.02; return { name: item.name, type: surface, parametric: true, wireframe: { show: false }, itemStyle: { color: palette[index], opacity: 1 }, parametricEquation: { u: { min: start, max: end, step: Math.PI / 180 }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 90 }, x: function (u, v) { return (R r * Math.cos(v)) * Math.sin(u); }, y: function (u, v) { return (R r * Math.cos(v)) * Math.cos(u); }, z: function (u, v) { return r * Math.sin(v); } } }; }); chart.setOption({ grid3D: { show: false }, xAxis3D: { type: value }, yAxis3D: { type: value }, zAxis3D: { type: value }, series: series }); /script /body /html运行后你会得到一个可拖拽旋转的 3D 环形占比图。各个扇区颜色不同切分清晰默认视角下立体感已经比较明显。这一步验证了“ECharts 与 ECharts-gl 构建 3D 立体环形图”的可行性也展示了“默认选项”到底长什么样。5. 默认效果之外交互与样式优化5.1 viewControl 视角、光照和坐标轴隐藏默认效果虽然能跑但大屏项目通常还希望有自动旋转、合适的相机距离、更精致的光照。这时我们可以继续在grid3D里配置viewControl。我常用的配置是grid3D: { show: false, boxWidth: 80, boxDepth: 80, boxHeight: 40, viewControl: { autoRotate: true, autoRotateSpeed: 8, distance: 180, alpha: 25, beta: 0 } }说明一下alpha是俯仰角角度越小越接近俯视beta是方位角控制左右旋转distance是相机到物体的距离。配合自动旋转大屏上会显得更有科技感。光照方面默认的主光源和环境光基本够用但颜色偏暗时我会手动调light: { main: { intensity: 1.5, shadow: true }, ambient: { intensity: 0.6 } }shadow开启后阴影会明显增强立体感但也会增加渲染压力。数据量大或者机器性能一般时建议关掉。5.2 tooltip 提示和标签展示3D 环形图如果没有提示信息用户根本分不清每个扇区代表什么。最简单的方式是给series配置name然后使用 ECharts 的tooltip。我在项目中习惯维护一个valueMap然后在 formatter 里把名称和数值拼出来var valueMap {}; data.forEach(function (item) { valueMap[item.name] item.value; }); chart.setOption({ tooltip: { trigger: item, formatter: function (params) { var name params.seriesName; var value valueMap[name]; return name br/占比 value %; }, backgroundColor: rgba(13,20,40,0.8), textStyle: { color: #fff } } });这里也顺带解决了 ECharts tooltip 的一个经典问题提示内容过长时不会换行。如果你想让多行内容正常换行可以在tooltip里加一个extraCssText: white-space: normal;这样提示框内容就会按 HTML 自然的换行逻辑展示不需要手动加br/。这个细节在大屏信息很多时特别实用。5.3 自适应大屏和 rem 适配问题大屏项目里 rem 适配几乎是标配但这里有一个非常容易踩的坑postcss-pxtorem只会转换 CSS 里的像素单位对 canvas 内部绘制的 ECharts 内容完全无效。ECharts 图表内部的字体、线条、标记大小都是在 canvas 初始化时根据容器像素尺寸计算出来的。所以你会看到 DOM 元素随 rem 缩放得很完美但图表内部的文字大小纹丝不动。解决办法是不要依赖 pxtorem 去处理图表尺寸而是直接用chart.resize()监听窗口变化window.addEventListener(resize, function () { chart.resize(); });如果大屏设计稿是 1920 宽容器按 rem 设置了响应式宽度那么在resize时最好重新计算容器尺寸再调用chart.resize()。另一方面ECharts 的textStyle.fontSize可以直接设置成基于当前屏幕宽度计算出来的像素值不要写成纯固定 rem 值。这个坑在 Vue 3 项目中尤其常见很多朋友反馈“pxtorem 对 ECharts 没起到效果”其实就是混淆了 DOM 渲染和 canvas 渲染。6. 常见问题与排查实录6.1 页面只有坐标轴没有曲面连着踩过两次这个坑原因基本都是同一个parametric没有设置为true或者parametricEquation写错了位置。surface系列要使用参数方程必须同时满足三个条件type: surfaceparametric: trueparametricEquation与series平级而不是嵌套在其他子配置里。如果只写了parametricEquation而忘了parametric: trueecharts-gl 会认为你想用data渲染普通曲面而你又没有传data结果自然是空场景。排查时可以打开浏览器控制台看是否有报错提示。6.2 扇区之间有裂缝当你把多个扇区拼接成一个环形时相邻曲面之间偶尔会出现一条细线或者半个像素宽的裂缝。这不是真正的破面而是 WebGL 在抗锯齿时对边缘像素做了透明处理导致两个曲面交界处的颜色没有完全覆盖。解决办法是在计算扇区终点时加一个小的重叠量比如currentAngle 0.02。这样相邻扇区会有轻微重叠边缘裂缝就被覆盖掉了。注意重叠量不要太大否则扇区颜色会互相侵染反而显得脏。6.3 颜色发黑或整个环不可区分3D 图形的颜色表现和光照关系非常密切。如果扇区颜色比预期暗很多优先检查light.main.intensity和light.ambient.intensity。默认光照在普通场景下没问题但深色背景下往往需要额外提高环境光强度。还有一种情况是曲面法线方向反了导致光照计算在背面。此时可以尝试调整v方向的范围或修改z函数的正负。不过在我们这个圆环方程里正常写法不会出现法线反向多出现在二次开发改方程时。6.4 Vue/React 项目里初始化失效在 Vue 3 或 React 中最典型的报错是“Initialize failed: invalid dom”。原因很简单初始化时容器还没挂载或者容器宽度高度为 0。解决方式也很固定在onMounted或useEffect里执行nextTick后再init。如果是异步加载数据甚至可以等数据返回后再创建实例。组件销毁时记得调用chart.dispose()不然会有内存泄漏风险切路由之后连 canvas 都可能会残留。6.5 体积和性能优化echarts-gl 本身包含 WebGL 渲染层体积不算小。如果你项目对首屏体积敏感建议不要全量引入而是用 CDN 的 gzip 版本或者在需要 3D 图表的页面里动态加载脚本。还有一点surface的step决定了曲面精度。步长越小曲面越光滑但顶点数也越多会明显增加渲染开销。像圆环这种形状u.step用Math.PI / 90、v.step用Math.PI / 90已经足够了不要盲目追求Math.PI / 360。7. 扩展同一条路还能做哪些3D可视化7.1 3D柱状图与3D散点图学会了surface其实等于打开了一扇门。echarts-gl 里还有bar3D、scatter3D、lines3D等系列都用同一个grid3D坐标系。比如常见的 3D 柱状图写法比surface更简单series: [{ type: bar3D, data: [ [0, 0, 5], [1, 0, 8], [2, 0, 3], [0, 1, 6], [1, 1, 9], [2, 1, 4] ], shading: lambert, label: { show: true } }]data里的前两个维度是 x 和 y第三个维度是柱子高度。配合shading: lambert柱子会有明暗面立体感立刻出来。很多大屏里的 3D 城市数据展示用的就是这种能力。7.2 3D地图与多系列组合除了基础图形echarts-gl 还可以结合地图 GeoJSON 做 3D 地图。比如用map3D系列加载中国地图数据再配置viewControl实现旋转、缩放视觉冲击力非常强。社区里搜索“echarts中国地图”“echarts-gl示例”能找到大量类似实现。不过我的建议是先从简单的surface和bar3D练手把grid3D、viewControl、light之间的关系摸清楚再上地图和复杂组合。3D 可视化的核心难点往往不是“画出来”而是“调得好看又不卡”。这篇文章里所有的实现本质上都是同一个套路定义几何形状映射数据属性控制光源和视角。只要这个套路通了未来再做 3D 漏斗图、3D 金字塔甚至自定义波形图就只是改参数方程的问题了。我个人在实际操作中最想强调的一点是先让默认选项把效果跑起来再逐步加配置。这个习惯帮我少踩了很多坑。很多新手一上来就复制一大段现成配置结果不知道哪行配置改坏了最后连图形都渲染不出来。先用默认的surface跑出完整圆环确认坐标轴、光照、交互都正常再按数据切扇区最后才调色和视角。每一步改动都比较小出了问题能立刻定位。如果你也想做 3D 环形图建议不要跳过这个步骤尤其要把parametricEquation的手感练熟。