You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

319 lines
7.6 KiB

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