> 技术文档 > Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码

Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码


文章目录

  • 前言
  • 一、准备工作
    • 1. 所需工具
    • 2. 引入依赖
      • 方式一:CDN 快速引入
      • 方式二:npm 本地安装(推荐)
  • 二、实现原理解析
  • 三、echarts-gl 3D插件 使用回顾
    • grid3D 常用通用属性:
    • series 常用通用属性:
    • surface(曲面图)常用专属属性:
    • 快速示例:绘制球体
  • 四、代码实战
    • 4.1 封装一个echarts通用组件
    • 4.2 实现3D饼图主体
    • 4.2 添加指示线和标签
    • 完整代码:
      • 控制参数说明:
      • 通过调整参数实现3D环形图:
  • 五、优化
  • 总结

前言

在数据可视化场景中,3D 饼图凭借立体效果和空间层次感,能让数据展示更具视觉冲击力。ECharts 作为优秀的数据可视化库,通过 echarts-gl 插件轻松支持 3D 图表渲染。本文将详细阐述如何利用 ECharts实现3D饼图、3D环形图

Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码

Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码


一、准备工作

1. 所需工具

ECharts 核心库:版本需 ≥5.0(支持新特性)
echarts-gl 插件:专门处理 3D 渲染的扩展库

2. 引入依赖

方式一:CDN 快速引入

<!-- 引入 ECharts 核心库 --><script src=\"https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js\"></script><!-- 引入 echarts-gl 3D 插件 --><script src=\"https://cdn.jsdelivr.net/npm/echarts-gl@2.0.8/dist/echarts-gl.min.js\"></script>

方式二:npm 本地安装(推荐)

npm install echarts echarts-gl --save
import * as echarts from \'echarts\';import \'echarts-gl\';

二、实现原理解析

3D饼图的实现主要通过echarts-gl 3D插件去实现,分为3部分。首先通过 自定义曲面图 (type: “surface”)绘制3D饼图主体,再者通过三维折线图( type: “line3D”)绘制指示线,最后通过三维散点图(type: “scatter3D”)绘制标签

三、echarts-gl 3D插件 使用回顾

echarts-gl 3D插件使用跟echarts类似,包括xAxis3D、yAxis3D、zAxis3D、grid3D、series等配置项,可以发现就是在echarts基础上后缀加了3D

grid3D 常用通用属性:

  • viewControl 用于鼠标的旋转,缩放等视角控制,是个对象

    viewControl 的属性:

  • distance :默认视角距离主体的距离,值越大主体显示越小

  • alpha :视角绕 x 轴,即上下旋转的角度

  • beta :视角绕 y 轴,即左右旋转的角度

  • zoomSensitivity 缩放操作的灵敏度,值越大越灵敏,设置0可禁用缩放功能

  • rotateSensitivity 旋转操作的灵敏度,值越大越灵敏 ,设置0可禁用旋转功能

  • panSensitivity 平移操作的灵敏度,值越大越灵敏,设置0可禁用平移功能

  • autoRotate 是否开启视角绕物体的自动旋转查看


series 常用通用属性:

  • type :图表类型,支持surface、bar3D、line3D等值
  • itemStyle :样式包括颜色和透明度。
  • name:系列名称,用于 tooltip 的显示,legend 的图例筛选
  • data:图表项数据

surface(曲面图)常用专属属性:

series :

  • wireframe:曲面图的网格线设置
  • equation:曲面的函数表达式。如果需要展示的是函数曲面,可以不设置 data,通过 equation 去声明函数表达式
  • parametric:是否为参数曲面。
  • parametricEquation:曲面的参数方程。在data没被设置的时候,可以通过 parametricEquation 去声明参数参数方程。在 parametric 为true时有效。

其中parametricEquation属性是实现3D饼图关键,通过曲面的参数方程(parametricEquation)可以绘制复杂的 3D 曲面和曲线比如球体、圆柱体等 ,通过它 我们可以自定义绘制3D扇形图,最终由多个3D扇形组合成3D饼图。

参数方程(parametricEquation是个对象)通常使用两个参数( u 和 v)在特定区间内生成点的坐标 (x, y, z)。

参数方程形式:

parametricEquation:{ u: { min: 0, max: 2 * Math.PI, step: 0.1 }, // u 参数范围 v: { min: 0, max: Math.PI, step: 0.1 }, // v 参数范围 x: function(u, v) { /* 返回 x 坐标 */ }, y: function(u, v) { /* 返回 y 坐标 */ }, z: function(u, v) { /* 返回 z 坐标 */ }}

(1)参数范围 (u 和 v):

min 和 max 定义参数的取值范围。
step 控制参数步长,影响曲面的细分精度(值越小,曲面越精细,但性能开销越大)。

(2)坐标方程 (x, y, z)
定义曲面形状的核心部分。例如:

  • 圆柱体:
x: function(u, v) { return Math.cos(u); },y: function(u, v) { return Math.sin(u); },z: function(u, v) { return v; }
  • 环面
x: function(u, v) { return (2 + Math.cos(v)) * Math.cos(u);},y: function(u, v) { return (2 + Math.cos(v)) * Math.sin(u);},z: function(u, v) { return Math.sin(v);}

快速示例:绘制球体

option = { xAxis3D: { type: \'value\' }, yAxis3D: { type: \'value\' }, zAxis3D: { type: \'value\' }, grid3D: {}, series: [{ type: \'surface\', parametric: true, // 启用参数方程模式 parametricEquation: { u: { min: 0, max: 2 * Math.PI, step: 0.1 }, // u 参数范围 v: { min: 0, max: Math.PI, step: 0.1 }, // v 参数范围 x: function(u, v) { return Math.sin(v) * Math.cos(u); // x 坐标方程 }, y: function(u, v) { return Math.sin(v) * Math.sin(u); // y 坐标方程 }, z: function(u, v) { return Math.cos(v); // z 坐标方程 } }, itemStyle: { color: \'#2290ff\', // 曲面颜色 opacity: 0.7 } }]};

更多属性请查看官方文档


四、代码实战

以vue3为代码为示例,实现3D饼图、环形图

4.1 封装一个echarts通用组件

echarts.vue

<template><div class=\"echarts-box\"><div ref=\"echartRef\" class=\"charts\" ></div></div></template><script setup>import { ref, onMounted, onBeforeUnmount, watch, nextTick, markRaw } from \'vue\';import * as echarts from \'echarts\';import \'echarts-gl\';const props = defineProps({// 图表配置data: {type: Object,default: () => {},},});const echartRef = ref();let dom = null;//设置图表配置const setOptions = (options) => {//清除画布dom && dom.clear();//重新渲染dom && dom.setOption(options);};watch(() => props.data,(val) => {nextTick(() => {//默认关闭动画setOptions({animation: false,...val});});},{ deep: true, immediate: true });const emits= defineEmits([\'click\'])onMounted(() => {//初始化dom = markRaw(echarts.init(echartRef.value));//点击事件 dom.on(\'click\', (param)=> {emits(\'click\',param) } )});onBeforeUnmount(() => {//离开销毁echarts.dispose(dom);dom = null;});defineExpose({setOptions,});</script><style lang=\"scss\" scoped>.echarts-box {width: 100%;height: 100%;box-sizing: border-box;}.charts {width: 100%;height: 100%;}</style>

上述代码封装了一个echarts通用组件,只需传入data图表配置数据就会重新渲染,需要注意的是组件默认继承父元素的宽高(100%),所以父元素需要设置尺寸。

4.2 实现3D饼图主体

3D饼图主体不包含指示线和标签

demo.vue

<template> <div class=\"container\"> <div class=\"echarts-view\"> <Echarts :data=\"options\" /> </div> </div></template><script setup>import { ref, computed } from \"vue\";import Echarts from \"./echarts.vue\";//数据const data = ref([ { name: \"已完成\", value: 25, color: \"#D13DF2\" }, { name: \"申请中\", value: 45, color: \"#6442ee\" }, { name: \"已撤销\", value: 12, color: \"#6DCDE6\" }, { name: \"审核中\", value: 7, color: \"#2F54F3\" }, { name: \"已驳回\", value: 3, color: \"#8E56E0\" },]);// 图表配置const options = computed(() => { //总数 let total = data.value.reduce((a, b) => a + b.value, 0); //当前累加值 let sumValue = 0; //辅助参数,控制饼图半径,(0-1)范围内控制环形大小,值越小环形内半径越大 let k = 1; //series配置(每个扇形) let series = data.value.map((item) => { //当前扇形起始位置占饼图比例 let startRatio = sumValue / total; //值累加 sumValue += item.value; //当前扇形结束位置占饼图比例 let endRatio = sumValue / total; return { name: item.name ?? null, type: \"surface\", //曲面图 itemStyle: { color: item.color ?? null, //颜色 }, wireframe: { show: false, //不显示网格线 }, pieData: item, //数据 //饼图状态 pieStatus: { k, //辅助参数 startRatio, //起始位置比例 endRatio, //结束位置比例 value: item.value, //数值 }, parametric: true, //参数曲面 //曲面的参数方程 parametricEquation: getParametricEquation( startRatio, endRatio, k, item.value ), }; }); //返回配置 return { //提示框 tooltip: { formatter: (params) => { if ( params.seriesName !== \"mouseoutSeries\" && params.seriesName !== \"pie2d\" ) { return `${ params.seriesName }
<span style=\"display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:
${ params.color };\">
${series[params.seriesIndex].pieData.value}`; } return \"\"; }, }, xAxis3D: { min: -1, max: 1, }, yAxis3D: { min: -1, max: 1, }, zAxis3D: { min: -1, max: 1, }, // grid3D: { show: false, //不显示坐标系 boxHeight: 15, //饼图高度 // 用于鼠标的旋转,缩放等视角控制 viewControl: { alpha: 30, //视角 distance: 300, //距离,值越大饼图越小 rotateSensitivity: 0, //禁止旋转 zoomSensitivity: 0, //禁止缩放 panSensitivity: 0, //禁止平移 autoRotate: false, //禁止自动旋转 }, }, series, };});/** * 获取面的参数方程 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} k 辅助参数,控制饼图半径 * @param {*} value 数值 */const getParametricEquation = (startRatio, endRatio, k, value) => { const startRadian = startRatio * Math.PI * 2; const endRadian = endRatio * Math.PI * 2; k = typeof k === \"number\" && !isNaN(k) ? k : 1 / 3; //默认1/3 // 返回曲面参数方程 return { u: { min: -Math.PI, max: Math.PI * 3, step: Math.PI / 32, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x(u, v) { if (u < startRadian) { return Math.cos(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.cos(endRadian) * (1 + Math.cos(v) * k); } return Math.cos(u) * (1 + Math.cos(v) * k); }, y(u, v) { if (u < startRadian) { return Math.sin(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.sin(endRadian) * (1 + Math.cos(v) * k); } return Math.sin(u) * (1 + Math.cos(v) * k); }, z(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * value * 0.1; } // 扇形高度根据value值计算 return Math.sin(v) > 0 ? value * 0.1 : -1; }, };};</script><style scoped>.container { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; background-color: #203598; align-items: center;}.echarts-view { height: 700px; width: 1200px;}</style>

运行效果:

Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码

4.2 添加指示线和标签

............// 图表配置const options = computed(() => { //总数 let total = data.value.reduce((a, b) => a + b.value, 0); //当前累加值 let sumValue = 0; //辅助参数,控制饼图半径,(0-1)范围内控制环形大小,值越小环形内半径越大 let k = 1;//series配置(每个扇形)let series =.... .... ...../////////////新增///////////////添加指示线 series.forEach((item, index) => { let { itemStyle: { color }, pieStatus: { startRatio, endRatio, value }, } = item; addLabelLine(series, startRatio, endRatio, value, k, index, color); });/////////////////// return { .... .... .... }})///////////////新增////////////////////////添加label指示线/** * @param {*} series 配置 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} value 数值 * @param {*} k 辅助参数,饼图半径相关 * @param {*} i 在series中索引 * @param {*} color 指示线颜色 */const addLabelLine = ( series, startRatio, endRatio, value, k, i, color = \"#fff\") => { //计算扇形中心弧度 const midRadian = (startRatio + endRatio) * Math.PI; // 计算起点位置坐标(扇形边缘中心) const radius = 1 + k; // 外径 const posX = Math.cos(midRadian) * radius; //x坐标 const posY = Math.sin(midRadian) * radius; //y坐标 const posZ = 0.1 * value; //z坐标 let flag = (midRadian >= 0 && midRadian <= Math.PI / 2) || (midRadian >= (3 * Math.PI) / 2 && midRadian <= Math.PI * 2) ? 1 : -1; //计算拐点坐标 let turningPosArr = [ posX * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ, ]; //计算结束位置坐标 let endPosArr = [ posX * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ * 3, ]; //添加label+指示线 series.push( // 指示线 { type: \"line3D\", lineStyle: { color: \"#fff\",//线颜色 width:2,//线宽 }, data: [[posX, posY, posZ], turningPosArr, endPosArr], }, //label { type: \"scatter3D\", label: { show: true, distance: 0, position: \"center\", textStyle: { color: \"#fff\",//文字颜色 backgroundColor: \"rgba(0,0,0,0)\", //透明背景 fontSize: 18,//文字尺寸 fontWeight :\'bold\', //文字加粗 padding: 5, }, formatter: \"{b}\", }, symbolSize: 0, data: [ { name: series[i].name + \":\" + value+\'个\', value: endPosArr, }, ], } );};//////////////////////////////////

运行效果:

Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码

完整代码:

demo.vue

<template> <div class=\"container\"> <div class=\"echarts-view\"> <Echarts :data=\"options\" /> </div> </div></template><script setup>import { ref, computed } from \"vue\";import Echarts from \"./echarts.vue\";//数据const data = ref([ { name: \"已完成\", value: 25, color: \"#D13DF2\" }, { name: \"申请中\", value: 45, color: \"#6442ee\" }, { name: \"已撤销\", value: 12, color: \"#6DCDE6\" }, { name: \"审核中\", value: 7, color: \"#2F54F3\" }, { name: \"已驳回\", value: 3, color: \"#8E56E0\" },]);// 图表配置const options = computed(() => { //总数 let total = data.value.reduce((a, b) => a + b.value, 0); //当前累加值 let sumValue = 0; //辅助参数,控制饼图半径,(0-1)范围内控制环形大小,值越小环形内半径越大 let k = 1; //series配置(每个扇形) let series = data.value.map((item) => { //当前扇形起始位置占饼图比例 let startRatio = sumValue / total; //值累加 sumValue += item.value; //当前扇形结束位置占饼图比例 let endRatio = sumValue / total; return { name: item.name ?? null, type: \"surface\", //曲面图 itemStyle: { color: item.color ?? null, //颜色 }, wireframe: { show: false, //不显示网格线 }, pieData: item, //数据 //饼图状态 pieStatus: { k, //辅助参数 startRatio, //起始位置比例 endRatio, //结束位置比例 value: item.value, //数值 }, parametric: true, //参数曲面 //曲面的参数方程 parametricEquation: getParametricEquation( startRatio, endRatio, k, item.value ), }; }); //添加指示线 series.forEach((item, index) => { let { itemStyle: { color }, pieStatus: { startRatio, endRatio, value }, } = item; addLabelLine(series, startRatio, endRatio, value, k, index, color); }); //返回配置 return { //提示框 tooltip: { formatter: (params) => { if ( params.seriesName !== \"mouseoutSeries\" && params.seriesName !== \"pie2d\" ) { return `${ params.seriesName }
<span style=\"display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:
${ params.color };\">
${series[params.seriesIndex].pieData.value}`; } return \"\"; }, }, xAxis3D: { min: -1, max: 1, }, yAxis3D: { min: -1, max: 1, }, zAxis3D: { min: -1, max: 1, }, // grid3D: { show: false, //不显示坐标系 boxHeight: 15, //饼图高度 // 用于鼠标的旋转,缩放等视角控制 viewControl: { alpha: 30, //视角 distance: 300, //距离,值越大饼图越小 rotateSensitivity: 0, //禁止旋转 zoomSensitivity: 0, //禁止缩放 panSensitivity: 0, //禁止平移 autoRotate: false, //禁止自动旋转 }, }, series, };});/** * 获取面的参数方程 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} k 辅助参数,控制饼图半径 * @param {*} value 数值 */const getParametricEquation = (startRatio, endRatio, k, value) => { const startRadian = startRatio * Math.PI * 2; const endRadian = endRatio * Math.PI * 2; k = typeof k === \"number\" && !isNaN(k) ? k : 1 / 3; //默认1/3 // 返回曲面参数方程 return { u: { min: -Math.PI, max: Math.PI * 3, step: Math.PI / 32, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x(u, v) { if (u < startRadian) { return Math.cos(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.cos(endRadian) * (1 + Math.cos(v) * k); } return Math.cos(u) * (1 + Math.cos(v) * k); }, y(u, v) { if (u < startRadian) { return Math.sin(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.sin(endRadian) * (1 + Math.cos(v) * k); } return Math.sin(u) * (1 + Math.cos(v) * k); }, z(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * value * 0.1; } // 扇形高度根据value值计算 return Math.sin(v) > 0 ? value * 0.1 : -1; }, };};//添加label指示线/** * @param {*} series 配置 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} value 数值 * @param {*} k 辅助参数,饼图半径相关 * @param {*} i 在series中索引 * @param {*} color 指示线颜色 */const addLabelLine = ( series, startRatio, endRatio, value, k, i, color = \"#fff\") => { //计算扇形中心弧度 const midRadian = (startRatio + endRatio) * Math.PI; // 计算起点位置坐标(扇形边缘中心) const radius = 1 + k; // 外径 const posX = Math.cos(midRadian) * radius; //x坐标 const posY = Math.sin(midRadian) * radius; //y坐标 const posZ = 0.1 * value; //z坐标 let flag = (midRadian >= 0 && midRadian <= Math.PI / 2) || (midRadian >= (3 * Math.PI) / 2 && midRadian <= Math.PI * 2) ? 1 : -1; //计算拐点坐标 let turningPosArr = [ posX * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ, ]; //计算结束位置坐标 let endPosArr = [ posX * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ * 3, ]; //添加label+指示线 series.push( // 指示线 { type: \"line3D\", lineStyle: { color: \"#fff\",//线颜色 width:2,//线宽 }, data: [[posX, posY, posZ], turningPosArr, endPosArr], }, //label { type: \"scatter3D\", label: { show: true, distance: 0, position: \"center\", textStyle: { color: \"#fff\",//文字颜色 backgroundColor: \"rgba(0,0,0,0)\", //透明背景 fontSize: 18,//文字尺寸 fontWeight :\'bold\', //文字加粗 padding: 5, }, formatter: \"{b}\", }, symbolSize: 0, data: [ { name: series[i].name + \":\" + value+\'个\', value: endPosArr, }, ], } );};</script><style scoped>.container { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; background-color: #203598; align-items: center;}.echarts-view { height: 700px; width: 1200px;}</style>

控制参数说明:

代码中几个比较重要的控制参数需要注意,根据实际需求修改:

  • k(第28行):辅助参数,用来控制饼图大小,一般设置(0-1)范围,当值为小于1时会出现环形,值越小环形内半径越大
  • boxHeight(第113行):饼图高度主要控制参数(不是唯一影响参数,高度视觉效果还受到距离影响)
  • viewControl (第115行):用于鼠标的旋转,缩放等视角控制,其中 alpha控制3D视角角度,distance控制眼睛与3D饼图距离,值越大饼图越显小。rotateSensitivity开启鼠标旋转功能,zoomSensitivity开启鼠标缩放功能。

通过调整参数实现3D环形图:

k=0.2 //半径相关boxHeight:8 //高度distance: 180, //距离rotateSensitivity: 1, //开启鼠标控制旋转autoRotate: true, //开启自动旋转

运行效果:

Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码

3D环形图完整代码:

demo.vue

<template> <div class=\"container\"> <div class=\"echarts-view\"> <Echarts :data=\"options\" /> </div> </div></template><script setup>import { ref, computed } from \"vue\";import Echarts from \"./echarts.vue\";//数据const data = ref([ { name: \"已完成\", value: 25, color: \"#D13DF2\" }, { name: \"申请中\", value: 45, color: \"#6442ee\" }, { name: \"已撤销\", value: 12, color: \"#6DCDE6\" }, { name: \"审核中\", value: 7, color: \"#2F54F3\" }, { name: \"已驳回\", value: 3, color: \"#8E56E0\" },]);// 图表配置const options = computed(() => { //总数 let total = data.value.reduce((a, b) => a + b.value, 0); //当前累加值 let sumValue = 0; //辅助参数,控制饼图半径,(0-1)范围内控制环形大小,值越小环形内半径越大 let k = 0.2; //series配置(每个扇形) let series = data.value.map((item) => { //当前扇形起始位置占饼图比例 let startRatio = sumValue / total; //值累加 sumValue += item.value; //当前扇形结束位置占饼图比例 let endRatio = sumValue / total; return { name: item.name ?? null, type: \"surface\", //曲面图 itemStyle: { color: item.color ?? null, //颜色 }, wireframe: { show: false, //不显示网格线 }, pieData: item, //数据 //饼图状态 pieStatus: { k, //辅助参数 startRatio, //起始位置比例 endRatio, //结束位置比例 value: item.value, //数值 }, parametric: true, //参数曲面 //曲面的参数方程 parametricEquation: getParametricEquation( startRatio, endRatio, k, item.value ), }; }); //添加指示线 series.forEach((item, index) => { let { itemStyle: { color }, pieStatus: { startRatio, endRatio, value }, } = item; addLabelLine(series, startRatio, endRatio, value, k, index, color); }); //返回配置 return { //提示框 tooltip: { formatter: (params) => { if ( params.seriesName !== \"mouseoutSeries\" && params.seriesName !== \"pie2d\" ) { return `${ params.seriesName }
<span style=\"display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:
${ params.color };\">
${series[params.seriesIndex].pieData.value}`; } return \"\"; }, }, xAxis3D: { min: -1, max: 1, }, yAxis3D: { min: -1, max: 1, }, zAxis3D: { min: -1, max: 1, }, // grid3D: { show: false, //不显示坐标系 boxHeight: 8, //饼图高度 // 用于鼠标的旋转,缩放等视角控制 viewControl: { alpha: 30, //视角 distance: 180, //距离,值越大饼图越小 rotateSensitivity: 1, //禁止旋转 zoomSensitivity: 0, //禁止缩放 panSensitivity: 0, //禁止平移 autoRotate: true, //禁止自动旋转 }, }, series, };});/** * 获取面的参数方程 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} k 辅助参数,控制饼图半径 * @param {*} value 数值 */const getParametricEquation = (startRatio, endRatio, k, value) => { const startRadian = startRatio * Math.PI * 2; const endRadian = endRatio * Math.PI * 2; k = typeof k === \"number\" && !isNaN(k) ? k : 1 / 3; //默认1/3 // 返回曲面参数方程 return { u: { min: -Math.PI, max: Math.PI * 3, step: Math.PI / 32, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x(u, v) { if (u < startRadian) { return Math.cos(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.cos(endRadian) * (1 + Math.cos(v) * k); } return Math.cos(u) * (1 + Math.cos(v) * k); }, y(u, v) { if (u < startRadian) { return Math.sin(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.sin(endRadian) * (1 + Math.cos(v) * k); } return Math.sin(u) * (1 + Math.cos(v) * k); }, z(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * value * 0.1; } // 扇形高度根据value值计算 return Math.sin(v) > 0 ? value * 0.1 : -1; }, };};//添加label指示线/** * @param {*} series 配置 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} value 数值 * @param {*} k 辅助参数,饼图半径相关 * @param {*} i 在series中索引 * @param {*} color 指示线颜色 */const addLabelLine = ( series, startRatio, endRatio, value, k, i, color = \"#fff\") => { //计算扇形中心弧度 const midRadian = (startRatio + endRatio) * Math.PI; // 计算起点位置坐标(扇形边缘中心) const radius = 1 + k; // 外径 const posX = Math.cos(midRadian) * radius; //x坐标 const posY = Math.sin(midRadian) * radius; //y坐标 const posZ = 0.1 * value; //z坐标 let flag = (midRadian >= 0 && midRadian <= Math.PI / 2) || (midRadian >= (3 * Math.PI) / 2 && midRadian <= Math.PI * 2) ? 1 : -1; //计算拐点坐标 let turningPosArr = [ posX * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ, ]; //计算结束位置坐标 let endPosArr = [ posX * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ * 3, ]; //添加label+指示线 series.push( // 指示线 { type: \"line3D\", lineStyle: { color: \"#fff\",//线颜色 width:2,//线宽 }, data: [[posX, posY, posZ], turningPosArr, endPosArr], }, //label { type: \"scatter3D\", label: { show: true, distance: 0, position: \"center\", textStyle: { color: \"#fff\",//文字颜色 backgroundColor: \"rgba(0,0,0,0)\", //透明背景 fontSize: 18,//文字尺寸 fontWeight :\'bold\', //文字加粗 padding: 5, }, formatter: \"{b}\", }, symbolSize: 0, data: [ { name: series[i].name + \":\" + value+\'个\', value: endPosArr, }, ], } );};</script><style scoped>.container { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; background-color: #203598; align-items: center;}.echarts-view { height: 700px; width: 1200px;}</style>

五、优化

上述代码看似没问题,绘制的3D效果也ok。仔细测试会发现当把高度参数调好后,多次绘制不同数据,当数据范围变化过大时,会造成高度过高情况,甚至超出父元素区域。

把上述示例“已完成”数值设置为1000观察效果:

const data = ref([ { name: \"已完成\", value: 1000, color: \"#D13DF2\" }, { name: \"申请中\", value: 45, color: \"#6442ee\" }, { name: \"已撤销\", value: 12, color: \"#6DCDE6\" }, { name: \"审核中\", value: 7, color: \"#2F54F3\" }, { name: \"已驳回\", value: 3, color: \"#8E56E0\" },]);

运行效果:
Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码
饼图高度已经完全超出父元素。

究其原因,可以看下3D饼图高度控制相关代码:

 z(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * value * 0.1; } // 扇形高度根据value值计算 return Math.sin(v) > 0 ? value * 0.1 : -1; },

z参数表示z轴控制也即高度方向,返回值就是高度相关,注意到 value * 0.1,value为该扇形属性对应数值,乘以0.1缩放系数,理论上value可以无限大,就会造成饼图高度可以无限高。

所以优化重点是控制最大高度使得饼图无法无限增高,我们能想到动态控制缩放系数(0.1)。

这边优化没有固定方案可以是多样化的,只要能限制实际场景中最大值扇形不超出可视区域即可。例如:可采用类似css媒体查询根据数值区间枚举定义缩放系数或barHeight值。

枚举定义还是比较麻烦要一个个测算出数值来。这里推荐一种比较简单的折中方法:
使得每次重新渲染3D饼图中最大扇形高度固定,其他扇形按原值比例渲染。假设缩放系数为scale,饼图中最大值为maxValue,也就是保证每次渲染scale*maxValue=固定高度值,返回的扇形最高高度就能固定。

所以我们就能得出 scale(动态缩放系数)=固定高度值/maxValue,maxValue能从数据中找出来,而固定高度值怎么获取呢?

我们先模拟一份任意数据,scale默认像示例代码使用0.1,调整boxHeight值使得饼图最高的高度是我们想要的效果,从而固定高度值(0.1*maxValue)就能算出来了。

例如:模拟的数据

const data = ref([ { name: \"已完成\", value: 10, color: \"#D13DF2\" }, { name: \"申请中\", value: 45, color: \"#6442ee\" }, { name: \"已撤销\", value: 12, color: \"#6DCDE6\" }, { name: \"审核中\", value: 7, color: \"#2F54F3\" }, { name: \"已驳回\", value: 3, color: \"#8E56E0\" },]);

固定高度值=45*0.1=4.5,所以动态缩放系数scale=4.5/maxValue

//最大值const maxValue = computed(() => { return Math.max(...data.value.map((item) => item.value));//找出最大值});//高度缩放系数const scaleZ = computed(() => { return 45*0.1/maxValue.value;});
 z(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * value * scaleZ.value;//乘以缩放系数 } // 扇形高度根据value值计算 return Math.sin(v) > 0 ? value * scaleZ.value : -1;//乘以缩放系数 },

label和指示器同理修改为动态系数

 // 计算起点位置坐标(扇形边缘中心) const radius = 1 + k; // 外径 const posX = Math.cos(midRadian) * radius; //x坐标 const posY = Math.sin(midRadian) * radius; //y坐标 const posZ = value*scaleZ.value; //z坐标

修改后已完成值1000运行效果:

{ name: \"已完成\", value: 1000, color: \"#D13DF2\" },

Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码
可以看出高度正常

完整代码:

<template> <div class=\"container\"> <div class=\"echarts-view\"> <Echarts :data=\"options\" /> </div> </div></template><script setup>import { ref, computed } from \"vue\";import Echarts from \"./components/echarts.vue\";//数据const data = ref([ { name: \"已完成\", value: 1000, color: \"#D13DF2\" }, { name: \"申请中\", value: 45, color: \"#6442ee\" }, { name: \"已撤销\", value: 12, color: \"#6DCDE6\" }, { name: \"审核中\", value: 7, color: \"#2F54F3\" }, { name: \"已驳回\", value: 3, color: \"#8E56E0\" },]);//数据中最大值const maxValue = computed(() => { return Math.max(...data.value.map((item) => item.value));});//高度动态缩放系数const scaleZ = computed(() => { return 45*0.1/maxValue.value;});// 图表配置const options = computed(() => { //总数 let total = data.value.reduce((a, b) => a + b.value, 0); //当前累加值 let sumValue = 0; //辅助参数,控制饼图半径,(0-1)范围内控制环形大小,值越小环形内半径越大 let k = 1; //series配置(每个扇形) let series = data.value.map((item) => { //当前扇形起始位置占饼图比例 let startRatio = sumValue / total; //值累加 sumValue += item.value; //当前扇形结束位置占饼图比例 let endRatio = sumValue / total; return { name: item.name ?? null, type: \"surface\", //曲面图 itemStyle: { color: item.color ?? null, //颜色 }, wireframe: { show: false, //不显示网格线 }, pieData: item, //数据 //饼图状态 pieStatus: { k, //辅助参数 startRatio, //起始位置比例 endRatio, //结束位置比例 value: item.value, //数值 }, parametric: true, //参数曲面 //曲面的参数方程 parametricEquation: getParametricEquation( startRatio, endRatio, k, item.value ), }; }); //添加指示线 series.forEach((item, index) => { let { itemStyle: { color }, pieStatus: { startRatio, endRatio, value }, } = item; addLabelLine(series, startRatio, endRatio, value, k, index, color); }); //返回配置 return { //提示框 tooltip: { formatter: (params) => { if ( params.seriesName !== \"mouseoutSeries\" && params.seriesName !== \"pie2d\" ) { return `${ params.seriesName }
<span style=\"display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:
${ params.color };\">
${series[params.seriesIndex].pieData.value}`; } return \"\"; }, }, xAxis3D: { min: -1, max: 1, }, yAxis3D: { min: -1, max: 1, }, zAxis3D: { min: -1, max: 1, }, // grid3D: { show: false, //不显示坐标系 boxHeight: 15, //饼图高度 // 用于鼠标的旋转,缩放等视角控制 viewControl: { alpha: 30, //视角 distance: 300, //距离,值越大饼图越小 rotateSensitivity: 1, //禁止旋转 zoomSensitivity: 0, //禁止缩放 panSensitivity: 0, //禁止平移 autoRotate: false, //禁止自动旋转 }, }, series, };});/** * 获取面的参数方程 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} k 辅助参数,控制饼图半径 * @param {*} value 数值 */const getParametricEquation = (startRatio, endRatio, k, value) => { const startRadian = startRatio * Math.PI * 2; const endRadian = endRatio * Math.PI * 2; k = typeof k === \"number\" && !isNaN(k) ? k : 1 / 3; //默认1/3 // 返回曲面参数方程 return { u: { min: -Math.PI, max: Math.PI * 3, step: Math.PI / 32, }, v: { min: 0, max: Math.PI * 2, step: Math.PI / 20, }, x(u, v) { if (u < startRadian) { return Math.cos(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.cos(endRadian) * (1 + Math.cos(v) * k); } return Math.cos(u) * (1 + Math.cos(v) * k); }, y(u, v) { if (u < startRadian) { return Math.sin(startRadian) * (1 + Math.cos(v) * k); } if (u > endRadian) { return Math.sin(endRadian) * (1 + Math.cos(v) * k); } return Math.sin(u) * (1 + Math.cos(v) * k); }, z(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u); } if (u > Math.PI * 2.5) { return Math.sin(u) * value * scaleZ.value; } // 扇形高度根据value值计算 return Math.sin(v) > 0 ? value * scaleZ.value : -1; }, };};//添加label指示线/** * @param {*} series 配置 * @param {*} startRatio 扇形起始位置比例 * @param {*} endRatio 扇形结束位置比例 * @param {*} value 数值 * @param {*} k 辅助参数,饼图半径相关 * @param {*} i 在series中索引 * @param {*} color 指示线颜色 */const addLabelLine = ( series, startRatio, endRatio, value, k, i, color = \"#fff\") => { //计算扇形中心弧度 const midRadian = (startRatio + endRatio) * Math.PI; // 计算起点位置坐标(扇形边缘中心) const radius = 1 + k; // 外径 const posX = Math.cos(midRadian) * radius; //x坐标 const posY = Math.sin(midRadian) * radius; //y坐标 const posZ = value*scaleZ.value; //z坐标 let flag = (midRadian >= 0 && midRadian <= Math.PI / 2) || (midRadian >= (3 * Math.PI) / 2 && midRadian <= Math.PI * 2) ? 1 : -1; //计算拐点坐标 let turningPosArr = [ posX * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.1 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ, ]; //计算结束位置坐标 let endPosArr = [ posX * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posY * 1.2 + i * 0.1 * flag + (flag < 0 ? -0.2 : 0), posZ * 3, ]; //添加label+指示线 series.push( // 指示线 { type: \"line3D\", lineStyle: { color: \"#fff\", //线颜色 width: 2, //线宽 }, data: [[posX, posY, posZ], turningPosArr, endPosArr], }, //label { type: \"scatter3D\", label: { show: true, distance: 0, position: \"center\", textStyle: { color: \"#fff\", //文字颜色 backgroundColor: \"rgba(0,0,0,0)\", //透明背景 fontSize: 18, //文字尺寸 fontWeight: \"bold\", //文字加粗 padding: 5, }, formatter: \"{b}\", }, symbolSize: 0, data: [ { name: series[i].name + \":\" + value + \"个\", value: endPosArr, }, ], } );};</script><style scoped>.container { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; background-color: #203598; align-items: center;}.echarts-view { height: 700px; width: 1200px;}</style>

总结

通过echarts-gl 3D插件灵活应用我们成功实现了 基础版3D 饼图、3D环形图。你可以根据实际需求,进一步调整图表的样式和参数,创造出更加美观、实用、符合实际需求的可视化效果。