|
|
/**
|
|
|
* 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}<br/>${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]
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|