3D饼图,带背景图和自定义图例(threejs)

因为项目里同时有echarts的地图,地图需要弹跳动画,还有2d饼图和3d饼图.这里有一个坑,动画必须要ECharts 5.3.0+,而地图弹跳动画 → 需要 ECharts 5.3.0+ → 但 5.3.0+ 又和 echarts-gl 不兼容 → 3D 饼图出不来。所以这里用的是threejs,效果如下

先需要下载threejs

c 复制代码
npm install three
c 复制代码
<template>
  <div class="chart_3dPie_box">
    <div ref="chartContainer" class="chart-container" />
    <div class="total_num">
      <div class="num">{{ chartData }}</div>
      <div class="text">告警总数</div>
    </div>
    <div class="flex-space-between chart-legend">
      <div v-for="(item, index) in lableData" :key="index" class="chart-item">
        <div class="chart_item_label flex_center">
          <div
            :style="{ backgroundColor: item.color }"
            class="chart_item_color"
          />
          {{ item.label }}
        </div>
        <div :style="{ color: item.color }" class="chart_item_value">
          {{ item.value }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import fontData from "../../../../assets/font/DINPro-Regular.otf";
export default {
  name: "ThreeDPieChart",
  computed: {
    chartData() {
      // 总和
      return this.config.data.reduce((sum, item) => sum + item.value, 0);
    },
  },
  data() {
    return {
      lableData: [
        { label: "设备异常", value: 70, color: "#FFCC26" },
        { label: "水质异常", value: 45, color: "#00FFFF" },
        { label: "电气异常", value: 20, color: "#FF4747" },
        { label: "采集异常", value: 56, color: "#4BFF64" },
        { label: "工艺异常", value: 13, color: "#FFFC19" },
      ],
      config: {
        data: [
          { label: "电气异常", value: 100 },
          { label: "水质异常", value: 45 },
          { label: "电气异常", value: 20 },
          { label: "采集异常", value: 56 },
          { label: "工艺异常", value: 13 },
        ],
        colors: ["#FFCC26", "#00FFFF", "#FF4747", "#4BFF64", "#FFFC19"],
        height: 10,
        heightFactor: 4,
      },
      renderer: null,
      scene: null,
      camera: null,
      controls: null,
      font: null,
      animationId: null,
    };
  },
  mounted() {
    this.initChart();
    window.addEventListener("resize", this.handleResize);
  },
  beforeDestroy() {
    this.cleanup();
  },
  methods: {
    initChart() {
      if (!this.$refs.chartContainer) return;

      const containerWidth = this.$refs.chartContainer.clientWidth;
      const containerHeight = this.$refs.chartContainer.clientHeight;
      const outR = Math.min(containerWidth, containerHeight) * 0.58; // 调整饼图的大小
      const innerR = outR * 0.7; // 内圈大小

      // 1. 初始化渲染器(启用抗锯齿和更高的阴影质量)
      this.renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true, // 允许透明背景
      });
      this.renderer.setSize(containerWidth, containerHeight);
      this.renderer.shadowMap.enabled = true;
      this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 更柔和的阴影
      this.renderer.outputEncoding = THREE.sRGBEncoding; // 更好的颜色渲染
      this.$refs.chartContainer.appendChild(this.renderer.domElement);

      // 2. 创建场景(设置适当的背景色)
      this.scene = new THREE.Scene();

      // 添加光源
      const light1 = new THREE.PointLight(0xfff3e0, 0.5);
      light1.position.set(0, 1200, 2160);
      this.scene.add(light1);

      // 环境光(调整强度解决颜色变暗)
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
      this.scene.add(ambientLight);

      // 4. 创建相机
      this.camera = new THREE.OrthographicCamera(
        containerWidth / -2,
        containerWidth / 2,
        containerHeight / 2,
        containerHeight / -2,
        1,
        2000
      );

      // 特写镜头:相机距离拉近
      this.camera.position.set(0, 1000, 1200);
      this.camera.lookAt(0, 0, 0);

      // 5. 控制器设置
      this.controls = new OrbitControls(this.camera, this.renderer.domElement);

      // 6. 加载字体(添加加载状态提示)
      this.loadFont(outR, innerR);
    },

    loadFont(outR, innerR) {
      const fontLoader = new THREE.FontLoader();
      this.font = fontLoader.parse(fontData);
      this.createPieChart(outR, innerR);
    },

    createPieChart(outR, innerR) {
      const group = new THREE.Group();
      group.rotation.x = -Math.PI / 2; // 更精确的旋转
      group.position.y = 10; // 移动饼图的位置,向下移动 30 个单位

      this.scene.add(group);

      const totalValue = this.config.data.reduce(
        (sum, item) => sum + item.value,
        0
      );

      let startAngle = 0;
      this.config.data.forEach((item, index) => {
        const angleLength = (item.value / totalValue) * Math.PI * 2; // 使用弧度制更精确
        const height =
          this.config.height +
          (item.value / totalValue) *
            this.config.height *
            this.config.heightFactor;

        // 使用更鲜艳的颜色
        const color = new THREE.Color(this.config.colors[index]);
        color.convertSRGBToLinear(); // 确保颜色正确渲染

        this.createPieSegment(
          group,
          outR,
          innerR,
          height,
          startAngle,
          angleLength,
          color,
          item.value,
          item.label // 添加标签显示
        );

        startAngle += angleLength;
      });

      this.animate();
    },

    createPieSegment(
      group,
      outR,
      innerR,
      height,
      startAngle,
      angleLength,
      color,
      text,
      label
    ) {
      // 1. 创建形状
      const shape = new THREE.Shape();
      shape.absarc(0, 0, outR, startAngle, startAngle + angleLength, false);
      shape.lineTo(
        Math.cos(startAngle + angleLength) * innerR,
        Math.sin(startAngle + angleLength) * innerR
      );
      shape.absarc(0, 0, innerR, startAngle + angleLength, startAngle, true);

      // 2. 挤出设置
      const extrudeSettings = {
        curveSegments: 100,
        steps: 2,
        depth: height,
        bevelEnabled: true,
        bevelThickness: 1,
        bevelSize: 0,
        bevelOffset: 0,
        bevelSegments: 1,
      };

      // 3. 创建网格(使用更亮的材质)
      const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
      const material = new THREE.MeshPhongMaterial({
        color: color,
        shininess: 20,
        roughness: 0.6,
      });

      const mesh = new THREE.Mesh(geometry, material);
      group.add(mesh);

      // 4. 添加文本(如果字体已加载)
      if (this.font) {
        this.addTextToSegment(
          mesh,
          outR,
          innerR,
          height,
          startAngle,
          angleLength,
          text
        );
      }

      // 5. 添加标签(可选)
      this.addLabelToSegment(group, outR, startAngle, angleLength, label);
    },

    addTextToSegment(
      mesh,
      outR,
      innerR,
      height,
      startAngle,
      angleLength,
      text
    ) {
      try {
        // 计算文本位置和角度
        const midAngle = startAngle + angleLength / 2;
        const radius = (outR + innerR) / 2;

        // 创建文本几何体
        const textGeometry = new THREE.TextGeometry(text, {
          font: this.font,
          size: 11,
          height: 2,
          curveSegments: 12,
          bevelEnabled: false,
        });

        // 计算文本居中
        textGeometry.computeBoundingBox();
        const textWidth =
          textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x;

        // 创建文本材质(更醒目的颜色)
        const textMaterial = new THREE.MeshPhongMaterial({
          color: 0xffffff,
        });

        const textMesh = new THREE.Mesh(textGeometry, textMaterial);

        // 定位和旋转文本
        textMesh.position.set(
          Math.cos(midAngle) * radius - textWidth / 2,
          Math.sin(midAngle) * radius - 10,
          height + 0
        );

        textMesh.rotation.set(
          120, // X轴旋转90度使文字立起来
          0, // Y轴不需要旋转
          0 // Z轴旋转使文字朝向圆心
        );

        // textMesh.rotation.z = midAngle + Math.PI / 2;
        // textMesh.rotation.x = Math.PI / 2;

        mesh.add(textMesh);
      } catch (error) {
        console.error("创建文本失败:", error);
      }
    },

    addLabelToSegment(group, radius, startAngle, angleLength, label) {
      // 创建简单的标签(使用CSS2DRenderer或Three.js精灵)
      // 这里简化为控制台输出
      console.log(`Segment Label: ${label}`);
    },

    animate() {
      this.animationId = requestAnimationFrame(this.animate);
      this.controls.update();
      this.renderer.render(this.scene, this.camera);
    },

    handleResize() {
      if (!this.renderer || !this.camera || !this.$refs.chartContainer) return;

      const width = this.$refs.chartContainer.clientWidth;
      const height = this.$refs.chartContainer.clientHeight;

      this.camera.left = width / -2;
      this.camera.right = width / 2;
      this.camera.top = height / 2;
      this.camera.bottom = height / -2;
      this.camera.updateProjectionMatrix();

      this.renderer.setSize(width, height);
    },

    cleanup() {
      window.removeEventListener("resize", this.handleResize);
      if (this.animationId) {
        cancelAnimationFrame(this.animationId);
      }
      if (
        this.renderer &&
        this.$refs.chartContainer &&
        this.$refs.chartContainer.contains(this.renderer.domElement)
      ) {
        this.$refs.chartContainer.removeChild(this.renderer.domElement);
      }

      // 释放资源
      if (this.scene) {
        while (this.scene.children.length > 0) {
          this.scene.remove(this.scene.children[0]);
        }
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.chart_3dPie_box {
  position: relative;
  .chart-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 507px;
    height: 343px;
    padding: 0;
    overflow: hidden;
    //   background: url(~@/assets/images/dz_img.png) center bottom 40%/200px  87px no-repeat;
    background: url("~@/assets/images/dz_img.png") no-repeat center center;
    background-size: 100%/200px 100%;
  }

  .total_num {
    width: 507px;
    height: 343px;
    position: absolute;
    left: 0;
    top: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    .num {
      position: absolute;
      top: 88px;
      font-family: SourceHanSansCN-Bold;
      font-weight: bold;
      font-size: 54px;
      color: #ffffff;
    }
    .text {
      position: absolute;
      bottom: 142px;
      font-family: Source Han Sans CN;
      font-weight: 400;
      font-size: 32px;
      color: #b0e8ff;
    }
  }

  .chart-legend {
    position: absolute;
    top: 0;
    right: 0;
    width: calc(50% - 121px);
    padding: 36px 0;
    display: flex;
    flex-direction: column;
    // gap: 14px;

    .chart-item {
      width: 100%;
      padding-right: 94px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      .chart_item_color {
        width: 24px;
        height: 24px;
        margin-right: 19px;
      }
      .chart_item_label {
        font-family: Source Han Sans CN;
        font-weight: 400;
        font-size: 32px;
        color: #d1deee;
      }
      .chart_item_value {
        font-family: SourceHanSansCN-Bold;
        font-weight: bold;
        font-size: 34px;
        color: #ffcc26;
      }
    }
  }
}
</style>
相关推荐
楚Y6同学2 小时前
QT之下拉框自动填充功能
开发语言·c++·qt·qt开发技巧·串口下拉填充·网口下拉填充
Full Stack Developme2 小时前
Hutool DFA 教程
开发语言·c#
xyq20242 小时前
Bootstrap 滚动监听
开发语言
IT_陈寒2 小时前
SpringBoot自动配置的坑差点没把我埋了
前端·人工智能·后端
光影少年2 小时前
高级前端需要学习那些东西?
前端·人工智能·学习·aigc·ai编程
mjhcsp2 小时前
根号快速计算牛顿迭代法
开发语言·c++·算法·迭代法
jiayong232 小时前
第 41 课:任务详情抽屉里的快速筛选联动
开发语言·前端·javascript·vue.js·学习
momo(激进版)2 小时前
常用的skills安装记录
前端