/** * ECharts-GL 三维环饼图(surface 参数方程) */ const MIN_SLICE_HEIGHT = 0.04 const MAX_SLICE_HEIGHT = 3.6 const getHeightScale = (value, maxValue) => { if (!maxValue || !value) { return MIN_SLICE_HEIGHT } const ratio = value / maxValue return MIN_SLICE_HEIGHT + (MAX_SLICE_HEIGHT - MIN_SLICE_HEIGHT) * ratio } export const getParametricEquation = ( startRatio, endRatio, isSelected, isHovered, k, height ) => { const startRadian = startRatio * Math.PI * 2 const endRadian = endRatio * Math.PI * 2 const midRadian = (startRadian + endRadian) / 2 const offsetX = isSelected ? Math.cos(midRadian) * 0.12 : 0 const offsetY = isSelected ? Math.sin(midRadian) * 0.12 : 0 const hoverRate = isHovered ? 1.06 : 1 const sliceHeight = height * hoverRate 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 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(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(u, v) { if (u < -Math.PI * 0.5) { return Math.sin(u) } if (u > Math.PI * 2.5) { return Math.sin(u) * sliceHeight } return Math.sin(v) > 0 ? sliceHeight : -1 } } } const PLACEHOLDER_SLICE_COUNT = 5 const PLACEHOLDER_SLICE_HEIGHT = 0.1 const LABEL_LAYOUT = { centerX: 50, centerY: 37, radiusX: 29, radiusY: 21, radialLen: 7, horizontalLen: 10 } const buildLabelGraphics = (sortedData, sumValue) => { if (!sortedData.length || !sumValue) { return [] } const { centerX, centerY, radiusX, radiusY, radialLen, horizontalLen } = LABEL_LAYOUT let startValue = 0 const graphics = [] sortedData.forEach((item) => { const endValue = startValue + item.value const midRatio = (startValue + endValue) / 2 / sumValue const midRadian = midRatio * Math.PI * 2 const cos = Math.cos(midRadian) const sin = Math.sin(midRadian) const onRight = cos >= 0 const anchorX = centerX + cos * radiusX const anchorY = centerY - sin * radiusY const radialX = cos * radialLen * 3.2 const radialY = -sin * radialLen * 2.4 const endX = radialX + (onRight ? horizontalLen * 3.6 : -horizontalLen * 3.6) const endY = radialY graphics.push({ type: 'group', left: `${anchorX}%`, top: `${anchorY}%`, z: 10, children: [ { type: 'polyline', shape: { points: [ [0, 0], [radialX, radialY], [endX, endY] ] }, style: { stroke: 'rgba(142, 207, 255, 0.55)', lineWidth: 1, fill: null }, silent: true }, { type: 'text', x: endX + (onRight ? 5 : -5), y: endY, style: { text: `${item.name}\n${item.percent}%`, fill: '#d8ecff', font: '13px Microsoft YaHei', textAlign: onRight ? 'left' : 'right', textVerticalAlign: 'middle' }, silent: true } ], silent: true }) startValue = endValue }) return graphics } const buildCenterGraphic = (text) => ([ { type: 'text', left: 'center', top: '44%', style: { text, fill: 'rgba(142, 207, 255, 0.88)', font: '14px Microsoft YaHei', textAlign: 'center', textVerticalAlign: 'middle' }, silent: true } ]) export const buildEmptyPie3DPlaceholder = (colors, config = {}) => { const pieData = Array.from({ length: PLACEHOLDER_SLICE_COUNT }, (_, index) => ({ name: `empty-${index}`, value: 1, percent: 20, color: colors[index % colors.length], opacity: 0.42 })) return buildPie3DOption({ ...config, pieData, placeholderMode: true }) } export const buildPie3DOption = ({ pieData, internalDiameterRatio = 0.58, boxHeight = 40, viewAlpha = 28, viewBeta = 48, viewDistance = 185, placeholderMode = false }) => { const series = [] let sumValue = 0 let startValue = 0 const validData = placeholderMode ? pieData : pieData.filter((item) => (Number(item.value) || 0) > 0) if (!validData.length) { return null } const sortedData = placeholderMode ? validData : [...validData].sort((a, b) => b.value - a.value) const maxValue = placeholderMode ? 1 : Math.max(...sortedData.map((item) => item.value), 1) const k = (1 - internalDiameterRatio) / (1 + internalDiameterRatio) sortedData.forEach((item) => { sumValue += item.value }) sortedData.forEach((item) => { const endValue = startValue + item.value const startRatio = startValue / sumValue const endRatio = endValue / sumValue const height = placeholderMode ? PLACEHOLDER_SLICE_HEIGHT : getHeightScale(item.value, maxValue) series.push({ name: item.name, type: 'surface', coordinateSystem: 'cartesian3D', parametric: true, progressive: 0, wireframe: { show: false }, shading: 'lambert', pieData: item, itemStyle: { color: item.color, opacity: item.opacity ?? (placeholderMode ? 0.42 : 0.96) }, parametricEquation: getParametricEquation( startRatio, endRatio, false, false, k, height ) }) startValue = endValue }) return { animation: false, tooltip: placeholderMode ? { show: false } : { trigger: 'item', formatter: (params) => { if (!params.seriesName) { return '' } const target = series.find((item) => item.name === params.seriesName) const data = target?.pieData if (!data) { return params.seriesName } return `${data.name}
${data.percent}% ${data.value}头` } }, xAxis3D: { min: -1.2, max: 1.2 }, yAxis3D: { min: -1.2, max: 1.2 }, zAxis3D: { min: -2, max: 4.5 }, grid3D: { show: false, boxHeight, top: '-12%', viewControl: { alpha: viewAlpha, beta: viewBeta, distance: viewDistance, rotateSensitivity: 0, zoomSensitivity: 0, panSensitivity: 0, autoRotate: false }, light: { main: { intensity: 1.2, shadow: false, alpha: 40, beta: 40 }, ambient: { intensity: 0.5 } } }, series, graphic: placeholderMode ? buildCenterGraphic('暂无统计数据') : buildLabelGraphics(sortedData, sumValue) } } export const enrichPieData = (items, colors) => { const total = items.reduce((sum, item) => sum + (Number(item.value) || 0), 0) return items.map((item, index) => { const value = Number(item.value) || 0 const percent = total === 0 ? 0 : Number(((value / total) * 100).toFixed(2)) return { name: item.name, value, percent, color: colors[index % colors.length] } }) }