带label的3D饼图(threejs)

发布于:2025-04-08 ⋅ 阅读:(32) ⋅ 点赞:(0)

3D饼图

在这里插入图片描述

使用three.js实现,选择threejs的原因:label需要实际的显示在具体的饼对应的模块上

“three”: “^0.127.0”,

<template>
  <div>

    <div ref="chartContainer" class="chart-container"></div>

    <div class="flex-space-between chart-legend">
      <div v-for="(item, index) in lableData" :key="index" class="chart-item">
        <div :style="{ color: item.color }" class="tc fs-16 chart-item-value">{{ (100 * (item.value /
          chartData)).toFixed(0) }}%
        </div>
        <div class="chart-item-label mgt-4 tc">{{ item.label }}</div>
      </div>
    </div>
  </div>

</template>

<script>
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import fontData from './FangSong_Regular.json'
export default {
  name: 'ThreeDPieChart',
  computed: {
    chartData() {
      // 总和
      return this.config.data.reduce((sum, item) => sum + item.value, 0);
    },

  },
  data() {
    return {
      lableData:[
      { label: '未完成', value: 20,color:'#FCC943'},
      { label: '未下发', value: 45,color:'#459CFF' },
      { label: '已完成', value: 100 ,color:'#7EC512'},
      ],
      config: {
        data: [
          { label: '已完成', value: 100 },
          { label: '未下发', value: 45 },
          { label: '未完成', value: 20 }
        ],
        colors: ['#459CFF', '#A2DC4A', '#F7DF42'],
        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() {

      console.log('chushih')
      if (!this.$refs.chartContainer) return;

      const containerWidth = this.$refs.chartContainer.clientWidth;
      const containerHeight = this.$refs.chartContainer.clientHeight;
      const outR = Math.min(containerWidth, containerHeight) / 2;
      const innerR = outR * 0.6;

      // 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.8);
      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, 800, 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; // 更精确的旋转
      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 / totalValue) * 100).toFixed(0)}%`,
          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-container {
  width: 100%;
  margin-top: 15px;
  height: 150px;
  padding: 0;
  overflow: hidden;
  background: url(~@/assets/images/taskManagement/piebg.png) center bottom 40%/200px  87px no-repeat;
}

.chart-legend {
  padding: 0 15px;

  .mgt-4 {
    margin-top: 4px;
  }
}
</style>

网站公告

今日签到

点亮在社区的每一天
去签到