vue3 3d饼图

完整3D饼图项目下载 https://download.csdn.net/download/weixin_54645059/91716476 只有一个vue文件 直接下滑到完整代码就阔以

本文介绍了如何使用ECharts和ECharts-GL插件实现3D饼图效果,并提出了数值显示未解决的问题。主要包含以下内容:

安装所需插件:yarn add echartsyarn add echarts-gl
提供了完整的Vue3组件代码,包括3D饼图的配置和渲染逻辑
核心功能:
1. 支持自定义饼图高度和空心比例
2. 实现3D饼图的参数曲面方程计算
3. 包含鼠标点击和滑动特效
4. 图例的选中状态后 显示完整的饼图( selectedMode: true )
作者尚未研究明白如何在每个模块直接显示数值,并希望有解决方案的人能给予反馈。该实现通过计算数据百分比和构建3D曲面参数方程来渲染饼图,但标签显示功能目前被注释掉,需要进一步优化。

效果图

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1b0314b006dd407fbb8b23f98ddd8bff.png

始终保持圆 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(".");
  //判定如果是整数,增加小数点再补0
  if (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;
    // 如果本次是选中操作,记录上次选中的扇形对应的系列号 seriesIndex
    isSelected ? (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;
        // 记录上次高亮的扇形对应的系列号 seriesIndex
        hoveredIndex = params.seriesIndex;
      }
      // 使用更新后的 option,渲染图表
      myChart.setOption(option.value);
    }
  });
  // 修正取消高亮失败的 bug
  myChart.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/endRatio
    let 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>


### 如何在 Vue 中使用 ECharts 绘制 3D 要在 Vue 中实现 ECharts 的 3D 功能,可以按照以下方法操作: #### 安装依赖 首先,在项目中安装 `echarts` 和其扩展库 `echarts-gl` 来支持 3D 表绘制。 ```bash npm install echarts echarts-gl --save ``` #### 页面配置 在需要展示 3D 的组件中引入必要的模块并初始化表实例。以下是完整的代码示例: ```javascript <template> <div ref="chartRef" style="width: 100%; height: 500px;"></div> </template> <script> import * as echarts from &#39;echarts&#39;; require(&#39;echarts-gl&#39;); // 加载用于 3D 可视化的插件 export default { name: "Echarts3DPie", data() { return { chartInstance: null, }; }, mounted() { this.initChart(); }, beforeDestroy() { if (this.chartInstance) { window.removeEventListener(&#39;resize&#39;, this.resizeHandler); this.chartInstance.dispose(); // 销毁实例释放资源 } }, methods: { initChart() { const chartDom = this.$refs.chartRef; this.chartInstance = echarts.init(chartDom); const option = { backgroundColor: &#39;#fff&#39;, tooltip: {}, series: [ { type: &#39;pieGL&#39;, radius: [&#39;40%&#39;, &#39;70%&#39;], label: { show: false }, itemStyle: { borderColor: &#39;#fff&#39;, borderWidth: 2, }, data: [ { value: 40, name: &#39;分类A&#39; }, { value: 60, name: &#39;分类B&#39; }, { value: 80, name: &#39;分类C&#39; }, { value: 100, name: &#39;分类D&#39; }, ], }, ], }; this.chartInstance.setOption(option); // 设置选项数据 window.addEventListener(&#39;resize&#39;, this.resizeHandler = () => { this.chartInstance && this.chartInstance.resize(); }); }, }, }; </script> <style scoped> /* 自定义样式 */ </style> ``` 上述代码实现了如下功能: - 使用 `ref` 获取 DOM 节点作为容器。 - 初始化 ECharts 实例,并加载 `echarts-gl` 插件以启用 3D 功能[^1]。 - 添加事件监听器来动态调整表大小[^2]。 #### 关键参数说明 - **type**: 设定为 `&#39;pieGL&#39;` 表明这是一个基于 WebGL 渲染的 3D 。 - **radius**: 控制内外半径的比例范围。 - **data**: 提供具体的数据集,每项包含 `value` 值和对应的名称 `name`。 通过以上设置即可成功渲染一个基础版的 3D 。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值