效果
安装echarts
npm install echarts
npm install echarts-gl
3d饼图组件:
<template><div style="width: 100%; height: 100%" ref="echart"></div>
</template><script setup>
import { reactive, ref, onMounted, watch } from 'vue'
import * as echarts from 'echarts'
import 'echarts-gl'const boxHeight = ref([])
const legendData = ref([])
const echart = ref()const props = defineProps({optionsData: []
})const echartInit = () => {var myChart = echarts.init(echart.value)const series = getPie3D(props.optionsData, 0.7)series.push({center: ['10%', '90%'],name: 'pie2d',type: 'pie',label: {show: false,opacity: 1,fontSize: 12,lineHeight: 10,textStyle: {fontSize: 12,color: '#fff',},},labelLine: {length: 30,length2: 30,},startAngle: -30, //起始角度,支持范围[0, 360]。clockwise: false, //饼图的扇区是否是顺时针排布。上述这两项配置主要是为了对齐3d的样式radius: ['40%', '60%'],//data: props.optionsData,data: props.optionsData.map(item => {item.itemStyle.opacity = 0return item}),itemStyle: {opacity: 0,}})// 准备待返回的配置项,把准备好的 legendData、series 传入。let option = {legend: {show: true,tooltip: {show: true,},orient: 'vertical',//data: ['待办', '已办', '未处理'],data: legendData.value,top: 'center',// 设置图例为矩形itemWidth: 14, // 图例标记的宽度,默认为14itemHeight: 14, // 图例标记的高度,默认为12itemStyle: {// 设置边框圆角,可以设置为 0 实现正方形borderRadius: 5},itemGap: 12,right: '2%',textStyle: {color: '#fff',fontSize: 12,},formatter: (param)=> {let item = legendData.value.filter(item => item.name == param)[0];let bfs = fomatFloat(item.value * 100, 2) + "%";let v = item.value2;//return `${item.name} ${bfs}`;return `${item.name} ${v}`;}},animation: true,tooltip: {formatter: (params) => {if (params.seriesName !== 'mouseoutSeries' &¶ms.seriesName !== 'pie2d') {return `${params.seriesName}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>${option.series[params.seriesIndex].pieData.value + '台'}`}},textStyle: {fontSize: 12,},},title: {x: 'center',top: '20',textStyle: {color: '#fff',fontSize: 12,},},// backgroundColor: '#FFF',labelLine: {show: false,lineStyle: {color: '#7BC0CB',},normal: {show: false,length: 10,length2: 10,},},label: {show: false,position: 'outside',formatter: '{b} \n{d}%',textStyle: {color: '#fff',fontSize: '14px',},},xAxis3D: {min: -1,max: 1,},yAxis3D: {min: -1,max: 1,},zAxis3D: {min: -1,max: 1,},grid3D: {show: false,//boxHeight: 0.01,boxHeight: boxHeight.value,//top: '30%',bottom: '50%',left: '-18%',// environment: "rgba(255,255,255,0)",viewControl: {distance: 180,//这个数值越大图就越小alpha: 25,//倾斜角度beta: 60,//起始渲染角度autoRotate: false, // 自动旋转rotateSensitivity: 1,//旋转灵敏度,鼠标按住不放可进行角度偏移zoomSensitivity: 1,//缩放灵敏度,鼠标滚轮可修改大小panSensitivity: 0,// 平移操作的灵敏度,值越大越灵敏。默认为1,设置为0后无法平移。支持使用数组分别设置横向和纵向的平移灵敏度},},series: series,}// 使用刚指定的配置项和数据显示图表。myChart.setOption(option)}function fomatFloat(num, n) {var f = parseFloat(num);if (isNaN(f)) {return false;}f = Math.round(num * Math.pow(10, n)) / Math.pow(10, n); // n 幂 var s = f.toString();var rs = s.indexOf('.');//判定如果是整数,增加小数点再补0if (rs < 0) {rs = s.length;s += '.';}while (s.length <= rs + n) {s += '0';}return s;
}// 获取3d饼图的最高扇区的高度
function getHeight3D(series, height) {series.sort((a, b) => {return (b.pieData.value - a.pieData.value);})return height * 25 / series[0].pieData.value;
}
function getParametricEquation(startRatio,endRatio,isSelected,isHovered,k,height,
) {// 计算let midRatio = (startRatio + endRatio) / 2let startRadian = startRatio * Math.PI * 2let endRadian = endRatio * Math.PI * 2let midRadian = midRatio * Math.PI * 2// 如果只有一个扇形,则不实现选中效果。if (startRatio === 0 && endRatio === 1) {isSelected = false}// 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)k = typeof k !== 'undefined' ? k : 1 / 3// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0// 计算高亮效果的放大比例(未高亮,则比例为 1)let hoverRate = isHovered ? 1.05 : 1// 返回曲面参数方程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: function (u, v) {if (u < startRadian) {return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate}if (u > endRadian) {return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate}return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate},y: function (u, v) {if (u < startRadian) {return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate}if (u > endRadian) {return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate}return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate},z: function (u, v) {if (u < -Math.PI * 0.5) {return Math.sin(u)}if (u > Math.PI * 2.5) {return Math.sin(u)}return Math.sin(v) > 0 ? 1 * height : -1},}}// 生成模拟 3D 饼图的配置项function getPie3D(pieData, internalDiameterRatio) {let series = []let sumValue = 0let startValue = 0let endValue = 0//let legendData = []let k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3pieData.sort((a, b) => {return (b.value - a.value);});// 为每一个饼图数据,生成一个 series-surface 配置for (let i = 0; i < pieData.length; i++) {sumValue += pieData[i].valuelet seriesItem = {name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,type: 'surface',parametric: true,wireframe: {show: false,},pieData: pieData[i],pieStatus: {selected: false,hovered: false,k: k,},}if (typeof pieData[i].itemStyle != 'undefined') {let itemStyle = {}typeof pieData[i].itemStyle.color != 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : nulltypeof pieData[i].itemStyle.opacity != 'undefined' ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : nullseriesItem.itemStyle = itemStyle}series.push(seriesItem)}// 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。for (let i = 0; i < series.length; i++) {endValue = startValue + series[i].pieData.valueseries[i].pieData.startRatio = startValue / sumValueseries[i].pieData.endRatio = endValue / sumValueseries[i].parametricEquation = getParametricEquation(series[i].pieData.startRatio,series[i].pieData.endRatio,false,false,k,series[i].pieData.value,)boxHeight.value = getHeight3D(series, 2);//通过传参设定3d饼/环的高度,26代表26pxstartValue = endValue//legendData.value.push(series[i].name)let bfb = fomatFloat(series[i].pieData.value / sumValue, 4);legendData.value.push({name: series[i].name,value: bfb,value2: series[i].pieData.value});}return series}watch(() => [props.optionsData],() => {echartInit()}
)onMounted(() => {echartInit()
})</script>
vue中的使用:
<My3DPie :optionsData="optionsData" />
vue的js引入组件
// 传入数据生成 optionconst optionsData = ref([{name: '饮料',value: 36,itemStyle: {opacity: 0.8,color: '#14EC92',},},{name: '小食品',value: 29,itemStyle: {opacity: 0.8,color: '#42A2D8',},}])