Echarts实现3D饼状图

功能:

1、实心饼状图

效果:

组件代码:pie_3Dstyle03_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: "Pie3DStyle03Chart",
  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);
      }

      const series = this.getPie3D(this.optionData, 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 } },
        };
      });

      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, // 🔴 2D层仅负责显示labelLine,必须静默,不拦截鼠标
      });

      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) => {
            // 只处理 3D surface 触发的事件
            if (params.seriesType !== "surface") {
              return "";
            }
            if (
              params.seriesName === "mouseoutSeries" ||
              params.seriesName === "basePlate"
            ) {
              return "";
            }

            // 🔴 关键修复:直接通过闭包读取当前作用域 initCharts 中的 series 局部变量
            let currentSeries = series[params.seriesIndex];
            if (currentSeries && currentSeries.pieData) {
              let val = currentSeries.pieData.value; // 当前组件的数据源没有 val 字段,直接取 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) {
                target = 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: false,
          },
        },
        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">
      <Pie3DStyle03Chart :optionData="myChartData03" />
    </div>

数据源:

html 复制代码
   myChartData03: [
        {
          name: "柴油",
          value: 5,
          itemStyle: {
            color: "rgba(34, 196, 255,1)",
          },
        },
        {
          name: "汽油",
          value: 13,
          itemStyle: {
            color: "rgba(170, 255, 0,1)",
          },
        },
        {
          name: "甲醇",
          value: 38,
          itemStyle: {
            color: "rgba(187, 170, 241,1)",
          },
        },
      ],

样式:

html 复制代码
.chart-container {
    width: 400px;
    height: 400px;
  }
相关推荐
智码看视界1 小时前
老梁聊全栈系列:Vue2与Vue3核心区别及学习路线指南
前端·vue.js·学习
qq_363066931 小时前
react 使用web component导出静态html报告
前端·react.js·html·页面导出
weixin_457763081 小时前
展示youtube的视频
前端·javascript·html
雨翼轻尘1 小时前
03_HTML进阶标签与CSS入门
前端·css·html·入门·进阶标签
云水一下1 小时前
Vue.js从零到精通系列(六):组合式函数与逻辑复用——打造自己的 Hooks 工具箱
前端·javascript·vue.js
IT_陈寒1 小时前
Java的ArrayList扩容把我坑惨了,原来是这样搞的
前端·人工智能·后端
snow@li1 小时前
Charles:软件能力深度解析 / 跨平台 HTTP/HTTPS 代理调试工具 / 客户端与互联网之间的中间人代理 / 拦截、查看、篡改所有网络流量
前端
Pearson1 小时前
特大pdf文件在线预览技术方案
javascript·nginx·pdf
UXbot1 小时前
移动端UI设计工具选型指南:iOS与Android设计标准支持对比
android·前端·低代码·ios·交互·团队开发·ui设计