fix-3D饼图

This commit is contained in:
沈鸿 2025-05-15 14:54:07 +08:00
parent 05249ceca6
commit 60c31db526
7 changed files with 419 additions and 44 deletions

2
components.d.ts vendored
View File

@ -42,8 +42,6 @@ declare module 'vue' {
CustomScrollTitle: typeof import('./src/components/custom-scroll-title/index.vue')['default'] CustomScrollTitle: typeof import('./src/components/custom-scroll-title/index.vue')['default']
CustomTableOperate: typeof import('./src/components/custom-table-operate/index.vue')['default'] CustomTableOperate: typeof import('./src/components/custom-table-operate/index.vue')['default']
CustomTableTree: typeof import('./src/components/custom-table-tree/index.vue')['default'] CustomTableTree: typeof import('./src/components/custom-table-tree/index.vue')['default']
IndexCo: typeof import('./src/components/custom-echart-line/index-co.vue')['default']
IndexRe: typeof import('./src/components/new-hyaline-cake/index-re.vue')['default']
NewHyalineCake: typeof import('./src/components/custom-echart-hyaline-cake/new-hyaline-cake.vue')['default'] NewHyalineCake: typeof import('./src/components/custom-echart-hyaline-cake/new-hyaline-cake.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

View File

@ -74,10 +74,10 @@ function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h
k = typeof k !== 'undefined' ? k : 1 / 3; k = typeof k !== 'undefined' ? k : 1 / 3;
// //
const offsetX = Math.cos(midRadian) * props.option.itemGap; const offsetX = isSelected ? Math.cos(midRadian) * props.option.itemGap : 0;
const offsetY = Math.sin(midRadian) * props.option.itemGap; const offsetY = isSelected ? Math.sin(midRadian) * props.option.itemGap : 0;
// 1 // 1
const hoverRate = isHovered ? 1.08 : 1; const hoverRate = isHovered ? 1.05 : 1;
// parametric // parametric
return { return {
@ -88,7 +88,7 @@ function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h
step: Math.PI / 32, step: Math.PI / 32,
}, },
v: { v: {
// v // v 0 - 2π
min: 0, min: 0,
max: Math.PI * 2, max: Math.PI * 2,
step: Math.PI / 20, step: Math.PI / 20,
@ -124,6 +124,7 @@ function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h
return Math.sin(u) * h * 0.1; return Math.sin(u) * h * 0.1;
} }
// z v // z v
// Zhvalue
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1; return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
}, },
}; };
@ -374,67 +375,95 @@ function handleClick(params) {
// click 使 // click 使
emit('click', params); emit('click', params);
} }
//
window.debugZValues = {
current: null,
history: [],
};
function handleMouseover(params) { function handleMouseover(params) {
const optionVal = chartOption.value; if (params.seriesName === 'mouseoutSeries') return;
const chart = getInstance();
const optionVal = chart.getOption(); //
const series = optionVal.series; const series = optionVal.series;
const idx = params.seriesIndex; const idx = params.seriesIndex;
const hh = series[idx].parametricEquation.z();
window.debugZValues.current = hh;
window.debugZValues.history.push({
event: 'mouseover',
series: series[idx].name,
zValue: hh,
time: new Date().toISOString(),
});
console.log('当前Z值:', hh, '历史记录:', window.debugZValues.history);
//
console.log(
'[移入] 当前所有扇形状态',
series.map((s) => ({
name: s.name,
hovered: s.pieStatus?.hovered,
selected: s.pieStatus?.selected,
height: s.parametricEquation?.z(Math.PI, Math.PI),
}))
);
// //
if (params.seriesName === 'mouseoutSeries') { if (hoveredIndex !== null && hoveredIndex !== idx) {
return;
}
//
if (hoveredIndex === idx) {
return;
}
console.log('hoveredIndexOne :>> ', hoveredIndex);
console.log('idx= :>> ', idx);
//
if (hoveredIndex !== null) {
const prev = series[hoveredIndex]; const prev = series[hoveredIndex];
const isSelected = prev.pieStatus.selected; prev.pieStatus.hovered = false; //
prev.parametricEquation = getParametricEquation( prev.parametricEquation = getParametricEquation(
//
prev.pieData.startRatio, prev.pieData.startRatio,
prev.pieData.endRatio, prev.pieData.endRatio,
isSelected, prev.pieStatus.selected,
false, false, // isHovered=false
prev.pieStatus.k, prev.pieStatus.k,
prev.pieData.value prev.pieData.value
); );
prev.pieStatus.hovered = false;
hoveredIndex = null;
} }
// //
const isSelected = series[idx].pieStatus.selected; const current = series[idx];
const startRatio = series[idx].pieData.startRatio; current.pieStatus.hovered = true;
const endRatio = series[idx].pieData.endRatio; current.parametricEquation = getParametricEquation(
const k = series[idx].pieStatus.k; current.pieData.startRatio,
const h = series[idx].pieData.value; current.pieData.endRatio,
series[idx].parametricEquation = getParametricEquation(startRatio, endRatio, isSelected, true, k, h); current.pieStatus.selected,
series[idx].pieStatus.hovered = true; true, // isHovered=true
hoveredIndex = idx; current.pieStatus.k,
console.log('hoveredIndexTwo :>> ', hoveredIndex); current.pieData.value
);
setOptions(optionVal); hoveredIndex = idx;
chart.setOption({ series }); // series
} }
function handleGlobalout() { function handleGlobalout() {
if (hoveredIndex !== null) { if (hoveredIndex !== null) {
const optionVal = chartOption.value; const chart = getInstance();
const optionVal = chart.getOption();
const series = optionVal.series; const series = optionVal.series;
const prev = series[hoveredIndex]; const prev = series[hoveredIndex];
const isSelected = prev.pieStatus.selected;
//
console.warn('[修复前] 异常状态', {
name: prev.name,
z: prev.parametricEquation.z(Math.PI, Math.PI),
equation: prev.parametricEquation,
});
//
prev.pieStatus.hovered = false;
prev.parametricEquation = getParametricEquation( prev.parametricEquation = getParametricEquation(
prev.pieData.startRatio, prev.pieData.startRatio,
prev.pieData.endRatio, prev.pieData.endRatio,
isSelected, prev.pieStatus.selected,
false, false, // isHovered=false
prev.pieStatus.k, prev.pieStatus.k,
prev.pieData.value prev.pieData.value
); );
prev.pieStatus.hovered = false;
hoveredIndex = null; hoveredIndex = null;
setOptions(optionVal); chart.setOption({ series }, { replaceMerge: 'series' }); // series
} }
} }
// //

View File

@ -23,6 +23,7 @@ const state = reactive({
containLabel: true, containLabel: true,
}, },
tooltip: { tooltip: {
className: 'custom-tooltip-container',
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
type: 'shadow', type: 'shadow',

View File

@ -22,11 +22,12 @@ const state = reactive({
containLabel: true, containLabel: true,
}, },
tooltip: { tooltip: {
className: 'custom-tooltip-container', //
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
type: 'shadow', type: 'shadow',
}, },
backgroundColor: 'rgba(0,0,0,0.6);', backgroundColor: 'rgba(0,0,0,0.5);',
borderColor: '#35d0c0', borderColor: '#35d0c0',
formatter: (data) => { formatter: (data) => {
const params = data[0]; const params = data[0];

View File

@ -15,10 +15,11 @@ const props = defineProps({
const state = reactive({ const state = reactive({
option: { option: {
k: 0.5,
opacity: 1, opacity: 1,
itemGap: 0.1, itemGap: 0.1,
legendSuffix: '万亩', legendSuffix: '万亩',
itemHeight: 500, itemHeight: 200,
grid3D: { grid3D: {
show: false, show: false,
boxHeight: 1, boxHeight: 1,
@ -31,7 +32,7 @@ const state = reactive({
rotateSensitivity: 10, //0 rotateSensitivity: 10, //0
zoomSensitivity: 10, //0 zoomSensitivity: 10, //0
panSensitivity: 10, //0 panSensitivity: 10, //0
autoRotate: true, // autoRotate: false, //
autoRotateAfterStill: 2, //, autoRotate autoRotateAfterStill: 2, //, autoRotate
}, },
}, },

View File

@ -27,6 +27,7 @@ const state = reactive({
containLabel: true, containLabel: true,
}, },
tooltip: { tooltip: {
className: 'custom-tooltip-container',
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
type: 'shadow', type: 'shadow',

View File

@ -0,0 +1,344 @@
let selectedIndex = '';
let hoveredIndex = '';
option = getPie3D(
[
{
name: 'cc',
value: 47,
itemStyle: {
color: '#f77b66',
},
},
{
name: 'aa',
value: 44,
itemStyle: {
color: '#3edce0',
},
},
{
name: 'bb',
value: 32,
itemStyle: {
color: '#f94e76',
},
},
{
name: 'ee',
value: 16,
itemStyle: {
color: '#018ef1',
},
},
{
name: 'dd',
value: 23,
itemStyle: {
color: '#9e60f9',
},
},
],
0.59
);
// 生成扇形的曲面参数方程
function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
// 计算
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) {
// eslint-disable-next-line no-param-reassign
isSelected = false;
}
// 通过扇形内径/外径的值,换算出辅助参数 k默认值 1/3
// eslint-disable-next-line no-param-reassign
k = typeof k !== 'undefined' ? k : 1 / 3;
// 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0
const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
// 计算高亮效果的放大比例(未高亮,则比例为 1
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(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) * h * 0.1;
}
// 当前图形的高度是Z根据h每个value的值决定的
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
},
};
}
// 生成模拟 3D 饼图的配置项
function getPie3D(pieData, internalDiameterRatio) {
const series = [];
// 总和
let sumValue = 0;
let startValue = 0;
let endValue = 0;
const legendData = [];
const k =
typeof internalDiameterRatio !== 'undefined'
? (1 - internalDiameterRatio) / (1 + internalDiameterRatio)
: 1 / 3;
// 为每一个饼图数据,生成一个 series-surface 配置
for (let i = 0; i < pieData.length; i += 1) {
sumValue += pieData[i].value;
const 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,
},
};
if (typeof pieData[i].itemStyle !== 'undefined') {
const { itemStyle } = pieData[i];
// eslint-disable-next-line no-unused-expressions
typeof pieData[i].itemStyle.color !== 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : null;
// eslint-disable-next-line no-unused-expressions
typeof pieData[i].itemStyle.opacity !== 'undefined'
? (itemStyle.opacity = pieData[i].itemStyle.opacity)
: null;
seriesItem.itemStyle = itemStyle;
}
series.push(seriesItem);
}
// 使用上一次遍历时,计算出的数据和 sumValue调用 getParametricEquation 函数,
// 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation也就是实现每一个扇形。
console.log(series);
for (let i = 0; i < series.length; i += 1) {
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,
// 我这里做了一个处理使除了第一个之外的值都是10
series[i].pieData.value === series[0].pieData.value ? 35 : 10
);
startValue = endValue;
legendData.push(series[i].name);
}
// 准备待返回的配置项,把准备好的 legendData、series 传入。
const option = {
// animation: false,
tooltip: {
formatter: (params) => {
if (params.seriesName !== 'mouseoutSeries') {
return `${
params.seriesName
}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${
params.color
};"></span>${option.series[params.seriesIndex].pieData.value}`;
}
return '';
},
},
xAxis3D: {
min: -1,
max: 1,
},
yAxis3D: {
min: -1,
max: 1,
},
zAxis3D: {
min: -1,
max: 1,
},
grid3D: {
show: false,
boxHeight: 5,
top: '-20%',
viewControl: {
// 3d效果可以放大、旋转等请自己去查看官方配置
alpha: 35,
// beta: 30,
rotateSensitivity: 1,
zoomSensitivity: 0,
panSensitivity: 0,
autoRotate: true,
distance: 150,
},
// 后处理特效可以为画面添加高光、景深、环境光遮蔽SSAO、调色等效果。可以让整个画面更富有质感。
postEffect: {
// 配置这项会出现锯齿,请自己去查看官方配置有办法解决
enable: false,
bloom: {
enable: true,
bloomIntensity: 0.1,
},
SSAO: {
enable: true,
quality: 'medium',
radius: 2,
},
// temporalSuperSampling: {
// enable: true,
// },
},
},
series,
};
return option;
}
// 修正取消高亮失败的 bug
// 监听 mouseover近似实现高亮放大效果
myChart.on('mouseover', function (params) {
// 准备重新渲染扇形所需的参数
let isSelected;
let isHovered;
let startRatio;
let endRatio;
let k;
let i;
// 如果触发 mouseover 的扇形当前已高亮,则不做操作
if (hoveredIndex === params.seriesIndex) {
return;
// 否则进行高亮及必要的取消高亮操作
} else {
// 如果当前有高亮的扇形,取消其高亮状态(对 option 更新)
if (hoveredIndex !== '') {
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 false。
isSelected = option.series[hoveredIndex].pieStatus.selected;
isHovered = false;
startRatio = option.series[hoveredIndex].pieData.startRatio;
endRatio = option.series[hoveredIndex].pieData.endRatio;
k = option.series[hoveredIndex].pieStatus.k;
i = option.series[hoveredIndex].pieData.value === option.series[0].pieData.value ? 35 : 10;
// 对当前点击的扇形,执行取消高亮操作(对 option 更新)
option.series[hoveredIndex].parametricEquation = getParametricEquation(
startRatio,
endRatio,
isSelected,
isHovered,
k,
i
);
option.series[hoveredIndex].pieStatus.hovered = isHovered;
// 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
hoveredIndex = '';
}
// 如果触发 mouseover 的扇形不是透明圆环,将其高亮(对 option 更新)
if (params.seriesName !== 'mouseoutSeries') {
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
isSelected = option.series[params.seriesIndex].pieStatus.selected;
isHovered = true;
startRatio = option.series[params.seriesIndex].pieData.startRatio;
endRatio = option.series[params.seriesIndex].pieData.endRatio;
k = option.series[params.seriesIndex].pieStatus.k;
// 对当前点击的扇形,执行高亮操作(对 option 更新)
option.series[params.seriesIndex].parametricEquation = getParametricEquation(
startRatio,
endRatio,
isSelected,
isHovered,
k,
option.series[params.seriesIndex].pieData.value + 5
);
option.series[params.seriesIndex].pieStatus.hovered = isHovered;
// 记录上次高亮的扇形对应的系列号 seriesIndex
hoveredIndex = params.seriesIndex;
}
// 使用更新后的 option渲染图表
myChart.setOption(option);
}
});
// 修正取消高亮失败的 bug
myChart.on('globalout', function () {
if (hoveredIndex !== '') {
// 从 option.series 中读取重新渲染扇形所需的参数,将是否高亮设置为 true。
isSelected = option.series[hoveredIndex].pieStatus.selected;
isHovered = false;
k = option.series[hoveredIndex].pieStatus.k;
startRatio = option.series[hoveredIndex].pieData.startRatio;
endRatio = option.series[hoveredIndex].pieData.endRatio;
// 对当前点击的扇形,执行取消高亮操作(对 option 更新)
i = option.series[hoveredIndex].pieData.value === option.series[0].pieData.value ? 35 : 10;
option.series[hoveredIndex].parametricEquation = getParametricEquation(
startRatio,
endRatio,
isSelected,
isHovered,
k,
i
);
option.series[hoveredIndex].pieStatus.hovered = isHovered;
// 将此前记录的上次选中的扇形对应的系列号 seriesIndex 清空
hoveredIndex = '';
}
// 使用更新后的 option渲染图表
myChart.setOption(option);
});