import * as echarts from 'echarts' import 'echarts-gl' interface Pie3DData { name: string value: number itemStyle?: { color?: string opacity?: number } startRatio?: number endRatio?: number } interface Pie3DOptions { data: Pie3DData[] height?: number hoverHeightScale?: number selectOffset?: number distance?: number boxHeight?: number radius?: number } interface ParametricEquation { u: { min: number; max: number; step: number } v: { min: number; max: number; step: number } x: (u: number, v: number) => number y: (u: number, v: number) => number z: (u: number, v: number) => number } interface SeriesItem { name: string type: string parametric: boolean wireframe: { show: boolean } pieData?: Pie3DData pieStatus?: { selected: boolean; hovered: boolean; k: number } itemStyle?: any parametricEquation?: ParametricEquation } /** * 渲染3D饼图,支持点击选中(外移)、鼠标悬停高亮(升高)、高亮修正,无label/legend。 * @param dom - 容器DOM * @param options - 配置项 * options.data: [{name, value, itemStyle}] * options.height: 饼图厚度 * options.hoverHeightScale: 高亮时高度放大倍数 * options.selectOffset: 选中时外移距离(默认0.1) * options.distance: 视角距离 * options.boxHeight: grid3D.boxHeight * options.radius: 半径缩放 * @returns echartsInstance */ export function renderPie3DChart(dom: HTMLElement, options: Pie3DOptions): echarts.ECharts | undefined { if (!dom) return const myChart = echarts.init(dom) const hoverHeightScale = options.hoverHeightScale || 1.2 const selectOffset = options.selectOffset || 0.1 const distance = options.distance || 120 const gridBoxHeight = options.boxHeight || 10 const R = options.radius || 0.8 const fixedH = 100 // 生成扇形的曲面参数方程 function getParametricEquation(startRatio: number, endRatio: number, isSelected: boolean, isHovered: boolean, k: number, h: number): ParametricEquation { const midRatio = (startRatio + endRatio) / 2 const startRadian = startRatio * Math.PI * 2 const endRadian = endRatio * Math.PI * 2 const midRadian = midRatio * Math.PI * 2 if (startRatio === 0 && endRatio === 1) isSelected = false k = typeof k !== 'undefined' ? k : 1 const offsetX = isSelected ? Math.cos(midRadian) * selectOffset : 0 const offsetY = isSelected ? Math.sin(midRadian) * selectOffset : 0 const 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: number, v: number) { if (u < startRadian) return offsetX + R * Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate if (u > endRadian) return offsetX + R * Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate return offsetX + R * Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate }, y: function(u: number, v: number) { if (u < startRadian) return offsetY + R * Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate if (u > endRadian) return offsetY + R * Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate return offsetY + R * Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate }, z: function(u: number, v: number) { if (u < -Math.PI * 0.5) return Math.sin(u) if (u > Math.PI * 2.5) return Math.sin(u) * h * .1 return Math.sin(v) > 0 ? 1 * h * .1 : -1 } } } // 生成series function getPie3D(pieData: Pie3DData[]): SeriesItem[] { const series: SeriesItem[] = [] let sumValue = 0 let startValue = 0 let endValue = 0 const k = 1 // 实心饼图 for (let i = 0; i < pieData.length; i++) { sumValue += pieData[i].value const seriesItem: 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 (pieData[i].itemStyle) { const itemStyle: any = {} const itemStyleObj: any = pieData[i].itemStyle if (itemStyleObj.color) itemStyle.color = itemStyleObj.color if (itemStyleObj.opacity !== undefined) itemStyle.opacity = itemStyleObj.opacity seriesItem.itemStyle = itemStyle } series.push(seriesItem) } for (let i = 0; i < series.length; i++) { endValue = startValue + series[i].pieData!.value series[i].pieData!.startRatio = startValue / sumValue series[i].pieData!.endRatio = endValue / sumValue series[i].parametricEquation = getParametricEquation( series[i].pieData!.startRatio!, series[i].pieData!.endRatio!, false, false, k, fixedH // 所有块厚度一样 ) startValue = endValue } // 透明圆环用于高亮修正 series.push({ name: 'mouseoutSeries', type: 'surface', parametric: true, wireframe: { show: false }, itemStyle: { opacity: 0 }, parametricEquation: { u: { min: 0, max: Math.PI * 2, step: Math.PI / 20 }, v: { min: 0, max: Math.PI, step: Math.PI / 20 }, x: function(u: number, v: number) { return Math.sin(v) * Math.sin(u) + Math.sin(u) }, y: function(u: number, v: number) { return Math.sin(v) * Math.cos(u) + Math.cos(u) }, z: function(u: number, v: number) { return Math.cos(v) > 0 ? 0.1 : -0.1 } } }) return series } const series = getPie3D(options.data) // 不加pie2d,不加legend,不加label const option = { backgroundColor: 'rgba(0,0,0,0)', legend: undefined, tooltip: undefined, xAxis3D: { min: -1, max: 1 }, yAxis3D: { min: -1, max: 1 }, zAxis3D: { min: -1, max: 1 }, grid3D: { show: false, boxHeight: gridBoxHeight, viewControl: { alpha: 35, distance: distance, rotateSensitivity: 0, zoomSensitivity: 0, panSensitivity: 0, autoRotate: false } }, series: series } myChart.setOption(option, true) // 选中/高亮交互 let hoveredIndex = '' // // 点击选中(外移) // myChart.on('click', function(params) { // if (!series[params.seriesIndex] || series[params.seriesIndex].type !== 'surface') return; // let isSelected = !series[params.seriesIndex].pieStatus.selected; // let isHovered = series[params.seriesIndex].pieStatus.hovered; // let k = series[params.seriesIndex].pieStatus.k; // let startRatio = series[params.seriesIndex].pieData.startRatio; // let endRatio = series[params.seriesIndex].pieData.endRatio; // // 取消之前选中 // if (selectedIndex !== '' && selectedIndex !== params.seriesIndex) { // let prev = series[selectedIndex]; // prev.parametricEquation = getParametricEquation( // prev.pieData.startRatio, prev.pieData.endRatio, false, prev.pieStatus.hovered, k, prev.pieData.value // ); // prev.pieStatus.selected = false; // } // // 当前选中/取消 // series[params.seriesIndex].parametricEquation = getParametricEquation( // startRatio, endRatio, isSelected, isHovered, k, series[params.seriesIndex].pieData.value // ); // series[params.seriesIndex].pieStatus.selected = isSelected; // isSelected ? selectedIndex = params.seriesIndex : selectedIndex = ''; // myChart.setOption({ series }); // }); // 悬停高亮(升高) myChart.on('mouseover', function(params: any) { if (!series[params.seriesIndex] || !series[params.seriesIndex].pieData) return if (hoveredIndex === params.seriesIndex) return // 取消之前高亮 if (hoveredIndex !== '') { const prev = series[parseInt(hoveredIndex)] if (!prev || !prev.pieData) return prev.parametricEquation = getParametricEquation( prev.pieData.startRatio!, prev.pieData.endRatio!, prev.pieStatus!.selected, false, prev.pieStatus!.k, fixedH ) prev.pieStatus!.hovered = false hoveredIndex = '' } // 当前高亮 const cur = series[params.seriesIndex] cur.parametricEquation = getParametricEquation( cur.pieData!.startRatio!, cur.pieData!.endRatio!, cur.pieStatus!.selected, true, cur.pieStatus!.k, fixedH * hoverHeightScale ) cur.pieStatus!.hovered = true hoveredIndex = params.seriesIndex.toString() myChart.setOption({ series }) }) // 全局移出,修正高亮残留 myChart.on('globalout', function() { if (hoveredIndex !== '') { const prev = series[parseInt(hoveredIndex)] if (!prev || !prev.pieData) return prev.parametricEquation = getParametricEquation( prev.pieData.startRatio!, prev.pieData.endRatio!, prev.pieStatus!.selected, false, prev.pieStatus!.k, fixedH ) prev.pieStatus!.hovered = false hoveredIndex = '' myChart.setOption({ series }) } }) return myChart }