Files
admin-vben5/apps/web-antd/src/utils/pie3d.ts
2025-07-10 17:52:54 +08:00

264 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}