Echarts实现自定旋转3D饼状图

功能:

  • 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;
  }
相关推荐
meilindehuzi_a2 小时前
深入理解 JavaScript 的同步与异步机制:从单线程设计到 Promise 核心应用
开发语言·javascript·ecmascript
如烟花的信页2 小时前
加速乐cookie逆向分析
javascript·爬虫·python·js逆向
永远的WEB小白2 小时前
css改变svg图标的颜色
前端·javascript·css
ikoala2 小时前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
code_pgf2 小时前
3D点云目标检测(PointPillars)部署pipeline
人工智能·目标检测·3d
赵庆明老师3 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
颂love3 小时前
Vue的两大生态以及组件通信
前端·javascript·vue.js·typescript
光影少年3 小时前
js单线程,为什在node环境下的js可以处理高并发请求?
前端·javascript·掘金·金石计划
爱凤的小光4 小时前
Cog3DRangeImagePlaneEstimatorTool完全指南
3d·visionpro