功能:
- 1、实心饼状图
- 2、自定旋转
- 3、展示tooltip
效果:

组件代码:pie_3Dstyle04_chart.vue
html
<template>
<div class="content">
<div ref="eCharts" class="chart"></div>
</div>
</template>
<script>
import * as echarts from "echarts";
import "echarts-gl";
export default {
name: "Pie3DStyle04Chart",
props: {
optionData: {
type: Array,
default: () => [],
},
},
data() {
return {
myChart: null,
boxHeight: null,
};
},
mounted() {
this.$nextTick(() => {
if (this.optionData && this.optionData.length > 0) {
this.initCharts();
}
});
},
watch: {
optionData: {
handler(newVal) {
if (newVal && newVal.length > 0) {
this.$nextTick(() => {
this.initCharts();
});
}
},
deep: true,
},
},
beforeDestroy() {
if (this.myChart) {
this.myChart.dispose();
this.myChart = null;
}
},
methods: {
getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
let midRatio = (startRatio + endRatio) / 2;
let startRadian = startRatio * Math.PI * 2;
let endRadian = endRatio * Math.PI * 2;
let midRadian = midRatio * Math.PI * 2;
if (startRatio === 0 && endRatio === 1) {
isSelected = false;
}
k = typeof k !== "undefined" ? k : 1 / 3;
let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
let 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, 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: function (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: function (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;
return Math.sin(v) > 0 ? 1 * h * 0.1 : -1;
},
};
},
getPie3D(pieData, internalDiameterRatio) {
let rawData = JSON.parse(JSON.stringify(pieData));
let series = [];
let sumValue = 0;
let startValue = 0;
let endValue = 0;
let k = 1 - internalDiameterRatio;
rawData.sort((a, b) => {
return b.value - a.value;
});
for (let i = 0; i < rawData.length; i++) {
sumValue += rawData[i].value;
let seriesItem = {
name:
typeof rawData[i].name === "undefined"
? `series${i}`
: rawData[i].name,
type: "surface",
parametric: true,
wireframe: { show: false },
pieData: rawData[i],
pieStatus: { selected: false, hovered: false, k: k },
// center: ["10%", "50%"],
silent: false, // 🔴 3D层关闭交互,避免阻挡2D的tooltip
};
if (typeof rawData[i].itemStyle != "undefined") {
let itemStyle = {};
typeof rawData[i].itemStyle.color != "undefined"
? (itemStyle.color = rawData[i].itemStyle.color)
: null;
typeof rawData[i].itemStyle.opacity != "undefined"
? (itemStyle.opacity = rawData[i].itemStyle.opacity)
: null;
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 = this.getParametricEquation(
series[i].pieData.startRatio,
series[i].pieData.endRatio,
false,
false,
k,
series[i].pieData.value
);
startValue = endValue;
}
this.boxHeight = this.getHeight3D(series, 26);
return series;
},
getHeight3D(series, height) {
if (!series || series.length === 0) return 1;
series.sort((a, b) => {
return b.pieData.value - a.pieData.value;
});
return (height * 25) / series[0].pieData.value;
},
initCharts() {
if (!this.optionData || this.optionData.length === 0) return;
if (!this.myChart) {
this.myChart = echarts.init(this.$refs.eCharts);
}
// 🔴 核心修复:组件内部依赖 value 字段进行计算,需将外部的 val 字段映射为 value
const formatData = this.optionData.map((item) => {
return {
...item,
value: item.val !== undefined ? item.val : item.value,
};
});
const series = this.getPie3D(formatData, 0);
// 2D饼图必须使用和3D相同顺序的数据
// let pie2DData = series.map((item) => {
// return {
// name: item.pieData.name,
// value: item.pieData.value,
// itemStyle: item.pieData.itemStyle,
// };
// });
// pie2DData.forEach((item) => {
// item.label = {
// color: item.itemStyle.color,
// show: true,
// formatter: (params) => {
// return `{b|${params.name} \n}`;
// },
// rich: { b: { fontSize: 16, lineHeight: 20 } },
// };
// });
//3Dv饼图如果设置自动旋转的话这里就不要展示labelLine了,因为labelLine不会跟着旋转
// series.push({
// name: "pie2d",
// type: "pie",
// label: { opacity: 1, fontSize: 28, lineHeight: 20 },
// labelLine: { length: 20, length2: 50 },
// startAngle: 0,
// clockwise: false,
// radius: ["50%", "32%"],
// center: ["50%", "50%"],
// data: pie2DData,
// itemStyle: { opacity: 0 },
// silent: true,
// });
let legendData = this.optionData.map((item) => item.name);
let option = {
tooltip: {
show: true,
confine: true,
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderColor: "rgba(0, 0, 0, 0.7)",
textStyle: {
color: "#fff",
},
formatter: (params) => {
if (params.seriesType !== "surface") {
return "";
}
if (
params.seriesName === "mouseoutSeries" ||
params.seriesName === "basePlate"
) {
return "";
}
let currentSeries = series[params.seriesIndex];
if (currentSeries && currentSeries.pieData) {
let val = currentSeries.pieData.value;
let totalValue = 0;
series.forEach((item) => {
if (item.type === "surface" && item.pieData) {
totalValue += item.pieData.value;
}
});
let percent =
totalValue > 0 ? ((val / totalValue) * 100).toFixed(2) : 0;
let seriesColor =
(currentSeries.itemStyle && currentSeries.itemStyle.color) ||
params.color ||
"#A1E2FF";
return `<div style="font-size:14px; line-height: 1.5;">
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${seriesColor};vertical-align: middle;"></span>
<span style="color:${seriesColor}; font-weight: bold;">${params.seriesName}</span><br/>
<span style="padding-left:15px; color:#A1E2FF;">数量:${val}</span><br/>
<span style="padding-left:15px; color:#A1E2FF;">占比:${percent}%</span>
</div>`;
}
return "";
},
},
legend: {
data: legendData,
orient: "horizontal",
left: "center",
top: 10,
itemGap: 15,
textStyle: { color: "#A1E2FF" },
show: true,
icon: "circle",
formatter: (name) => {
var target;
for (var i = 0, l = this.optionData.length; i < l; i++) {
if (this.optionData[i].name == name) {
// 🔴 修复图例取值:兼容 val 和 value 字段
target =
this.optionData[i].val !== undefined
? this.optionData[i].val
: this.optionData[i].value;
}
}
return `${name}: ${target}`;
},
},
xAxis3D: { min: -1, max: 1 },
yAxis3D: { min: -1, max: 1 },
zAxis3D: { min: -1, max: 1 },
grid3D: {
show: false,
top: 0,
boxHeight: this.boxHeight,
viewControl: {
alpha: 22,
distance: 400,
rotateSensitivity: 0,
zoomSensitivity: 0,
panSensitivity: 0,
autoRotate: true,
},
},
series,
};
this.myChart?.setOption(option, true);
},
},
};
</script>
<style lang="scss" scoped>
.content {
position: relative;
.chart {
position: absolute;
height: 300px;
width: 500px;
}
}
</style>
页面中引用:
html
<div class="chart-container">
<Pie3DStyle04Chart :optionData="myChartData04" />
</div>
数据源:
html
myChartData04: [
{
name: "项目1",
val: 350,
itemStyle: {
color: "#1890ff",
},
},
{
name: "项目2",
val: 150,
itemStyle: {
color: "#19c9c7",
},
},
{
name: "项目3",
val: 100,
itemStyle: {
color: "#ff9900",
},
},
],
样式:
html
.chart-container {
width: 400px;
height: 400px;
}