完整3D餅圖項目下載 https://download.csdn.net/download/weixin_54645059/91716476 只有一個vue文件 直接下滑到完整代碼就闊以
本文介紹了如何使用ECharts和ECharts-GL插件實現3D餅圖效果,并提出了數值顯示未解決的問題。主要包含以下內容:
安裝所需插件:yarn add echarts
和 yarn add echarts-gl
提供了完整的Vue3組件代碼,包括3D餅圖的配置和渲染邏輯
核心功能:
1. 支持自定義餅圖高度和空心比例
2. 實現3D餅圖的參數曲面方程計算
3. 包含鼠標點擊和滑動特效
4. 圖例的選中狀態后 顯示完整的餅圖( selectedMode: true
)
作者尚未研究明白如何在每個模塊直接顯示數值,并希望有解決方案的人能給予反饋。該實現通過計算數據百分比和構建3D曲面參數方程來渲染餅圖,但標簽顯示功能目前被注釋掉,需要進一步優化。
效果圖
始終保持圓 isCircle.value = false
isCircle.value = true
安裝插件
yarn add echarts
yarn add echarts-gl
直接上完整代碼
<template><div class="container"><div class="chartsGl" id="charts"></div></div>
</template><script setup>
import * as echarts from "echarts";
import "echarts-gl";
import { nextTick, onMounted, ref } from "vue";
const pieHeight = ref(20); // 餅圖高度 0 自適應高度 其他值固定高度
const hollow = ref(0); // 空心的大小 0 實心 大于0 小于1 (空心的大小根據數據的大小來決定)onMounted(() => {init();
});
const optionData = ref([{ id: 0, name: "文案1", num: 50, itemStyle: { color: "#2196f3" } },{ id: 1, name: "文案2", num: 10, itemStyle: { color: "#0ce4d1" } },{ id: 2, name: "文案3", num: 20, itemStyle: { color: "#fbc02d" } },{ id: 3, name: "文案4", num: 30, itemStyle: { color: "#ff5252" } },{ id: 4, name: "文案5", num: 40, itemStyle: { color: "#ff9800" } },{ id: 5, name: "文案6", num: 100, itemStyle: { color: "#333" } },
]);const option = ref(null);
const init = () => {const total = optionData.value.reduce((sum, item) => sum + item.num, 0);// 計算每個項的百分比并添加到對象中optionData.value.forEach((item) => ((item.value = item.num), (item.bfb = ((item.value / total) * 100).toFixed(2) + "%")));//構建3d餅狀圖let myChart = echarts.init(document.getElementById("charts"));// 傳入數據生成 option ; getPie3D(數據,透明的空心占比(調節中間空心范圍的0就是普通餅1就很鏤空))option.value = getPie3D(optionData.value, hollow.value);//將配置項設置進去myChart.setOption(option.value);// 是否需要label指引線,如果要就添加一個透明的2d餅狀圖并調整角度使得labelLine和3d的餅狀圖對齊,并再次 setOption// option.value.series.push({// name: "pie2d",// type: "pie",// // color: ["#fbc02d", "#2196f3", "#0ce4d1"],// labelLine: {// // length: 15, // 延長線第一段的長度// // length2: 0, //延長線長度// // maxSurfaceAngle: 80,// },// startAngle: -53, //起始角度,支持范圍[0, 360]。// clockwise: false, //餅圖的扇區是否是順時針排布。上述這兩項配置主要是為了對齊3d的樣式// radius: ["20%", "20%"],// center: ["35%", "44%"],// data: optionData.value,// label: {// alignTo: "edge",// formatter: "{name|{b}}}",// size: 30,// // minMargin: 5,// // edgeDistance: 10,// // lineHeight: 15,// rich: {// time: {// // fontSize: 10,// color: "#999",// },// },// },// labelLine: {// // show: true,// },// });// myChart.setOption(option.value);//鼠標移動上去特效效果bindListen(myChart);
};
const getPie3D = (pieData, internalDiameterRatio) => {let series = [];let sumValue = 0;let startValue = 0;let endValue = 0;let legendData = [];let legendBfb = [];let k = 1 - internalDiameterRatio;// 為每一個餅圖數據,生成一個 series-surface(參數曲面) 配置for (let i = 0; i < pieData.length; i++) {sumValue += pieData[i].value;let 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: k,},//設置餅圖在容器中的位置(目前沒發現啥用)// center: ["50%", "100%"],};//曲面的顏色、不透明度等樣式。if (typeof pieData[i].itemStyle != "undefined") {let itemStyle = {};typeof pieData[i].itemStyle.color != "undefined" ? (itemStyle.color = pieData[i].itemStyle.color) : null;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,也就是實現每一個扇形。legendData = [];legendBfb = [];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].pieData.value = pieHeight.value > 0 ? pieHeight.value : series[i].pieData.value;series[i].parametricEquation = getParametricEquation(series[i].pieData.startRatio, series[i].pieData.endRatio, false, false, k, series[i].pieData.value);startValue = endValue;let bfb = fomatFloat(series[i].pieData.value / sumValue, 4);legendData.push({name: series[i].name,value: bfb,});legendBfb.push({name: series[i].name,value: bfb,});}series = series.sort((a, b) => {return a.pieData.id - b.pieData.id;});//(第二個參數可以設置你這個環形的高低程度)let boxHeight = getHeight3D(series, 16); //通過傳參設定3d餅/環的高度// 準備待返回的配置項,把準備好的 legendData、series 傳入。let option = {color: ["#1C9FFD", "#0FF0FF", "#EBC542"],//圖例組件 //右側展示的文案legend: {selectedMode: true,data: legendData,//圖例列表的布局朝向。orient: "vertical",// right: 10,// top: 140,top: "middle",right: "5%",//圖例文字每項之間的間隔itemGap: 15,textStyle: {color: "#333",},show: true,icon: "rect",// 這個可以顯示百分比那種(可以根據你想要的來配置)formatter: function (param) {let item = optionData.value.filter((item) => item.name == param)[0];return `${item.name}: ${item.bfb}`;},},//移動上去提示的文本內容(我沒來得及改 你們可以根據需求改)tooltip: {formatter: (params) => {if (params.seriesName !== "mouseoutSeries" && params.seriesName !== "pie2d") {return (`名稱: ${params.seriesName}<br/>` + `值: ${optionData.value[params.seriesIndex].value}<br/>` + `百分比: ${optionData.value[params.seriesIndex].bfb}`);}},},itemStyle: {borderRadius: 5,},//這個可以變形xAxis3D: {min: -1,max: 1,},yAxis3D: {min: -1,max: 1,},zAxis3D: {min: -1,max: 1,},//此處是修改樣式的重點grid3D: {show: false,boxHeight: boxHeight, //圓環的高度//這是餅圖的位置top: "middle",left: "-15%",viewControl: {//3d效果可以放大、旋轉等,請自己去查看官方配置alpha: 34, //角度(這個很重要 調節角度的)distance: 300, //調整視角到主體的距離,類似調整zoom(這是整體大小)rotateSensitivity: true, //設置為0無法旋轉zoomSensitivity: 0, //設置為0無法縮放panSensitivity: 0, //設置為0無法平移autoRotate: false, //自動旋轉},},series: series,};return option;
};//獲取3d丙圖的最高扇區的高度
const getHeight3D = (series, height) => {if (pieHeight.value > 0) return pieHeight.value;series.sort((a, b) => {return b.pieData.value - a.pieData.value;});return (height * 35) / series[0].pieData.value;
};// 生成扇形的曲面參數方程,用于 series-surface.parametricEquation
const 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(默認值 1/3)k = typeof k !== "undefined" ? k : 1 / 3;// 計算選中效果分別在 x 軸、y 軸方向上的位移(未選中,則位移均為 0)let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;// 計算高亮效果的放大比例(未高亮,則比例為 1)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;},};
};//這是一個自定義計算的方法
const fomatFloat = (num, n) => {var f = parseFloat(num);if (isNaN(f)) {return false;}f = Math.round(num * Math.pow(10, n)) / Math.pow(10, n); // n 冪var s = f.toString();var rs = s.indexOf(".");//判定如果是整數,增加小數點再補0if (rs < 0) {rs = s.length;s += ".";}while (s.length <= rs + n) {s += "0";}return s;
};
const bindListen = (myChart) => {let selectedIndex = "";let hoveredIndex = "";// 監聽點擊事件,實現選中效果(單選)myChart.on("click", function (params) {// 從 option.series 中讀取重新渲染扇形所需的參數,將是否選中取反。let isSelected = !option.value.series[params.seriesIndex].pieStatus.selected;let isHovered = option.value.series[params.seriesIndex].pieStatus.hovered;let k = option.value.series[params.seriesIndex].pieStatus.k;let startRatio = option.value.series[params.seriesIndex].pieData.startRatio;let endRatio = option.value.series[params.seriesIndex].pieData.endRatio;// 如果之前選中過其他扇形,將其取消選中(對 option 更新)if (selectedIndex !== "" && selectedIndex !== params.seriesIndex) {option.value.series[selectedIndex].parametricEquation = getParametricEquation(option.value.series[selectedIndex].pieData.startRatio,option.value.series[selectedIndex].pieData.endRatio,false,false,k,option.value.series[selectedIndex].pieData.value);option.value.series[selectedIndex].pieStatus.selected = false;}// 對當前點擊的扇形,執行選中/取消選中操作(對 option 更新)option.value.series[params.seriesIndex].parametricEquation = getParametricEquation(startRatio,endRatio,isSelected,isHovered,k,option.value.series[params.seriesIndex].pieData.value);option.value.series[params.seriesIndex].pieStatus.selected = isSelected;// 如果本次是選中操作,記錄上次選中的扇形對應的系列號 seriesIndexisSelected ? (selectedIndex = params.seriesIndex) : null;// 使用更新后的 option,渲染圖表myChart.setOption(option.value);});// 監聽 mouseover,近似實現高亮(放大)效果myChart.on("mouseover", function (params) {// 準備重新渲染扇形所需的參數let isSelected;let isHovered;let startRatio;let endRatio;let k;// 如果觸發 mouseover 的扇形當前已高亮,則不做操作if (hoveredIndex === params.seriesIndex) {return;// 否則進行高亮及必要的取消高亮操作} else {// 如果當前有高亮的扇形,取消其高亮狀態(對 option 更新)if (hoveredIndex !== "") {// 從 option.series 中讀取重新渲染扇形所需的參數,將是否高亮設置為 false。isSelected = option.value.series[hoveredIndex].pieStatus.selected;isHovered = false;startRatio = option.value.series[hoveredIndex].pieData.startRatio;endRatio = option.value.series[hoveredIndex].pieData.endRatio;k = option.value.series[hoveredIndex].pieStatus.k;// 對當前點擊的扇形,執行取消高亮操作(對 option 更新)option.value.series[hoveredIndex].parametricEquation = getParametricEquation(startRatio,endRatio,isSelected,isHovered,k,option.value.series[hoveredIndex].pieData.value);option.value.series[hoveredIndex].pieStatus.hovered = isHovered;// 將此前記錄的上次選中的扇形對應的系列號 seriesIndex 清空hoveredIndex = "";}// 如果觸發 mouseover 的扇形不是透明圓環,將其高亮(對 option 更新)if (params.seriesName !== "mouseoutSeries" && params.seriesName !== "pie2d") {// 從 option.series 中讀取重新渲染扇形所需的參數,將是否高亮設置為 true。isSelected = option.value.series[params.seriesIndex].pieStatus.selected;isHovered = true;startRatio = option.value.series[params.seriesIndex].pieData.startRatio;endRatio = option.value.series[params.seriesIndex].pieData.endRatio;k = option.value.series[params.seriesIndex].pieStatus.k;// 對當前點擊的扇形,執行高亮操作(對 option 更新)option.value.series[params.seriesIndex].parametricEquation = getParametricEquation(startRatio,endRatio,isSelected,isHovered,k,option.value.series[params.seriesIndex].pieData.value + 5);option.value.series[params.seriesIndex].pieStatus.hovered = isHovered;// 記錄上次高亮的扇形對應的系列號 seriesIndexhoveredIndex = params.seriesIndex;}// 使用更新后的 option,渲染圖表myChart.setOption(option.value);}});// 修正取消高亮失敗的 bugmyChart.on("globalout", function () {// 準備重新渲染扇形所需的參數let isSelected;let isHovered;let startRatio;let endRatio;let k;if (hoveredIndex !== "") {// 從 option.series 中讀取重新渲染扇形所需的參數,將是否高亮設置為 true。isSelected = option.value.series[hoveredIndex].pieStatus.selected;isHovered = false;k = option.value.series[hoveredIndex].pieStatus.k;startRatio = option.value.series[hoveredIndex].pieData.startRatio;endRatio = option.value.series[hoveredIndex].pieData.endRatio;// 對當前點擊的扇形,執行取消高亮操作(對 option 更新)option.value.series[hoveredIndex].parametricEquation = getParametricEquation(startRatio,endRatio,isSelected,isHovered,k,option.value.series[hoveredIndex].pieData.value);option.value.series[hoveredIndex].pieStatus.hovered = isHovered;// 將此前記錄的上次選中的扇形對應的系列號 seriesIndex 清空hoveredIndex = "";}// 使用更新后的 option,渲染圖表myChart.setOption(option.value);});myChart.on("legendselectchanged", function (event) {const { selected } = event; // 獲取當前所有圖例的選中狀態(key:圖例name,value:是否選中)const k = 1 - hollow.value; // 復用空心比例配置optionData.value.forEach((item) => (item.value = item.num));// 1. 篩選選中的原始數據(未選中則排除,選中為空時默認顯示全部)const selectedData = Object.keys(selected).length ? optionData.value.filter((item) => selected[item.name]) : [...optionData.value]; // 無選中時顯示全部// 2. 重新計算篩選后數據的總數值(用于計算扇形比例)const newSumValue = selectedData.reduce((sum, item) => sum + item.value, 0);if (newSumValue === 0) return; // 避免無數據時出錯// 3. 重置起始比例,遍歷選中數據計算新的startRatio/endRatiolet startValue = 0;const dataRatioMap = new Map(); // 存儲{圖例name: {startRatio, endRatio}}selectedData.forEach((item) => {const startRatio = startValue / newSumValue;const endRatio = (startValue + item.value) / newSumValue;dataRatioMap.set(item.name, { startRatio, endRatio });startValue += item.value;});// 4. 遍歷所有3D扇形系列,更新配置(顯示/隱藏、比例、3D形態)option.value.series.forEach((seriesItem) => {const seriesName = seriesItem.name;const isShow = selected[seriesName] ?? true; // 未選中項默認隱藏const pieData = optionData.value.find((item) => item.name === seriesName); // 關聯原始數據const ratioInfo = dataRatioMap.get(seriesName) || { startRatio: 0, endRatio: 0 }; // 未選中項比例置0// 4.1 更新扇形基礎信息(比例、高度)seriesItem.pieData = {...pieData,startRatio: ratioInfo.startRatio,endRatio: ratioInfo.endRatio,value: pieHeight.value > 0 ? pieHeight.value : pieData.value, // 復用高度配置};// 4.2 更新3D參數方程(核心:控制扇形的3D形態和位置)seriesItem.parametricEquation = getParametricEquation(ratioInfo.startRatio, // 新起始比例ratioInfo.endRatio, // 新結束比例seriesItem.pieStatus.selected, // 保留原點擊選中狀態seriesItem.pieStatus.hovered, // 保留原hover狀態k, // 空心比例seriesItem.pieData.value // 扇形高度);// 4.3 隱藏未選中的扇形(通過設置z軸范圍使其不可見)if (!isShow) {seriesItem.parametricEquation.z = () => -2; // 低于可視范圍(原配置z軸max=1)}});option.value.series[1].value = 100;// 5. 重渲染圖表,顯示完整3D餅圖myChart.setOption(option.value);});
};
</script>
<style scoped lang="scss">
//餅圖(外面的容器)
.container {width: 500px;height: 400px;background: #fff;
}
//餅圖的大小
.chartsGl {width: 500px;height: 400px;
}
</style>