猫晨 2024-06-19 11:02 采纳率: 25%
浏览 32

echarts,3D饼图

我使用echarts创建了一个3D饼图,因为饼图的特殊性,很难添加标注和指示线,所以我选择的思路是在3D饼图上面增加一个2D饼图,并让2D饼图和3D饼图的顶部重合,并隐藏2D饼图的扇区,只保留2D饼图的标注和指示线,从而实现功能。
目前的问题是我通过transform属性要旋转2D饼图,让它和3D饼图拟合的时候遇到了困难,无论如何也很难让它和3D饼图的顶部重合在一起。
下面是我已经实现的代码,以及当前的效果:

img



<template>
  <div class="age_distribution">
    <div id="age" style="width: 100%; height: 400px;"></div>
    <div id="age2D" style="width: 100%; height: 400px;"></div>
  </div>
</template>

<script>
import * as echarts from 'echarts'
import 'echarts-gl'

export default {
  props: ['title'],
  data() {
    return {}
  },
  mounted() {
    this.getPeople()
  },
  methods: {
    getPeople() {
      var myChart = echarts.init(document.getElementById('age'))

      var myChart2D = echarts.init(document.getElementById('age2D'))

      // 生成3D饼图的参数方程
      function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
        const midRatio = (startRatio + endRatio) / 2
        const startRadian = startRatio * Math.PI * 2
        const endRadian = endRatio * Math.PI * 2
        const midRadian = midRatio * Math.PI * 2
        if (startRatio === 0 && endRatio === 1) {
          isSelected = false
        }
        k = typeof k !== 'undefined' ? k : 1 / 3
        const offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0
        const offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0
        const 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(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(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(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 optionData = [
        { name: '18岁以下', value: 7140 },
        { name: '19-30岁', value: 8991 },
        { name: '31-40岁', value: 37455 },
        { name: '41-50岁', value: 25490 },
        { name: '51-60岁', value: 7161 }
      ]

      function getPie3D(pieData, internalDiameterRatio) {
        const series = []
        let sumValue = 0
        let startValue = 0
        let endValue = 0
        const legendData = []
        const k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3
        for (let i = 0; i < pieData.length; i += 1) {
          sumValue += pieData[i].value
          const seriesItem = {
            name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,
            radius: ['40%', '60%'],
            type: 'surface',
            parametric: true,
            wireframe: { show: false },
            pieData: pieData[i],
            pieStatus: { selected: false, hovered: false, k },
            label: {
              show: false
            }
          }
          if (typeof pieData[i].itemStyle !== 'undefined') {
            const { itemStyle } = pieData[i]
            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)
        }
        for (let i = 0; i < series.length; i += 1) {
          endValue = startValue + series[i].pieData.value
          series[i].pieData.startRatio = startValue / sumValue
          series[i].pieData.endRatio = endValue / sumValue
          series[i].parametricEquation = getParametricEquation(
            series[i].pieData.startRatio,
            series[i].pieData.endRatio,
            false,
            false,
            k,
            10
          )
          startValue = endValue
          legendData.push(series[i].name)
        }
        const option = {
          title: {
            show: false
          },
          legend: {
            show: true,
            type: 'scroll',
            right: 50,
            top: 60,
            orient: 'vertical',
            icon: 'rect',
            itemHeight: 12,
            itemWidth: 12,
            itemGap: 10,
            data: legendData,
            textStyle: {
              color: '#709DD9',
              fontSize: 12,
              fontWeight: '400'
            }
          },
          color: ['#9f76f2', '#2a9dff', '#fac924', '#5ce5ff', '#6573f3'],
          tooltip: {
            formatter: params => {
              if (params.seriesName !== 'mouseoutSeries') {
                return `${params.marker}${params.seriesName}${pieData[params.seriesIndex].value}人`
              }
              return ''
            }
          },
          xAxis3D: { min: -1, max: 1 },
          yAxis3D: { min: -1, max: 1 },
          zAxis3D: { min: -1, max: 1 },
          grid3D: {
            show: false,
            boxHeight: 15,
            top: '5%',
            left: "center",
            viewControl: {
              alpha: 40,
              beta: 30,
              rotateSensitivity: 1,
              zoomSensitivity: 0,
              panSensitivity: 0,
              autoRotateSpeed: 50,
              autoRotate: false, // 停止旋转
              distance: 300
            }
          },
          series
        }
        return option
      }

      const option3D = getPie3D(optionData, 0)

      const option2D = {
      series: [{
        type: 'pie',
        radius: ['40%', '60%'],
        label: {
          show: true,
          position: 'outside',
          formatter: '{b}: {c}人 ({d}%)',
          textStyle: {
            fontSize: 12,
            color: '#fff'
          }
        },
        labelLine: {
          show: true,
          lineStyle: {
            color: '#845EC2'
          },
          smooth: 0.2,
          length: 10,
          length2: 20
        },
        data: optionData,
        itemStyle: {
          color: 'rgba(0, 0, 0, 1)' // 设置扇区颜色为透明
        },
        z: 10 // 确保2D饼图在3D饼图上方
      }]
    };
     myChart2D.setOption(option2D);

      myChart.setOption(option3D)
      // myChart.setOption(option2D)

      window.addEventListener('resize', function () {
        myChart.resize()
        myChart2D.resize()
      })
    }
  }
}
</script>

<style scoped>
.age_distribution {
  position: relative;
  width: 100%;
  height: 400px;
  border: 1px solid red;
}
.age_distribution .title {
  position: absolute;
  top: -7px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 17px;
  font-family: PingFang SC, PingFang SC;
  color: #ffffff;
  letter-spacing: 2px;
}

/* 在样式中添加2D图的旋转、缩放和平移以对齐3D图 */
.age_distribution #age2D {
  position: absolute;
  top: 0;
  left: 0;
  transform: rotateX(-10deg) rotateY(60deg);
  /* 根据3D图的旋转角度调整 */
  transform-origin: center;
  z-index: 10;
}

</style>


  • 写回答

1条回答 默认 最新

  • 专家-赤兔[在线] 优质创作者: 编程框架技术领域 2024-06-19 11:02
    关注

    引自免费微信小程序:皆我百晓生

    为了使2D饼图与3D饼图顶部完美对齐,关键在于理解并调整transform属性中的旋转角度。在你的场景中,由于3D饼图的视角是固定的(通过viewControl设置),你需要根据这个视角来调整2D饼图的transform属性。

    在你的代码中,你尝试了使用rotateXrotateY来对齐2D饼图,但是这可能不够精确,因为你还需要考虑到3D饼图的实际旋转角度和透视效果。为了更精确地对齐,可以考虑以下步骤:

    1. 确定3D饼图的实际视角:首先,从3D饼图的viewControl配置中获取alphabeta值,这些值分别对应于俯仰角和偏航角。
    2. 应用相同的视角到2D饼图:将获取的alphabeta值应用于2D饼图的CSS transform属性中的rotateXrotateY

    为了实现这一点,可以在初始化图表之后,读取3D饼图的viewControl配置,并将其应用于2D饼图的CSS transform属性。下面是如何修改你的代码来实现这一点:

    // ... 初始化图表代码 ...
    
    // 获取3D饼图的viewControl配置
    const viewControl = myChart.getOption().grid3D.viewControl;
    
    // 应用于2D饼图的transform属性
    document.getElementById('age2D').style.transform = `rotateX(${viewControl.alpha}deg) rotateY(${viewControl.beta}deg)`;
    

    然而,上述代码在实际操作中可能会遇到一些问题,因为getOption()返回的配置可能不是实时更新的,特别是当用户交互时。为了确保实时更新,可以监听echarts实例的事件,比如updated事件,然后重新计算并应用transform

    // 监听3D饼图的更新事件
    myChart.on('updated', function () {
      const viewControl = this.getOption().grid3D.viewControl;
      document.getElementById('age2D').style.transform = `rotateX(${viewControl.alpha}deg) rotateY(${viewControl.beta}deg)`;
    });
    

    这样,每当3D饼图的视角发生变化时,2D饼图的transform属性也会相应地进行调整,从而保持两者的对齐。

    请注意,由于CSS transform的限制,可能需要进行一些微调才能达到完美的视觉对齐。这可能包括调整transform-origin的值,或者在transform属性中加入缩放和平移操作。此外,由于3D透视效果的影响,简单的角度匹配可能不足以完全消除所有对齐误差,因此可能需要额外的视觉调整。

    最后,记得在窗口大小改变时也更新transform属性,以保持图表的正确显示。这可以通过在window.addEventListener('resize', function () { ... })中加入更新transform的逻辑来实现。

    评论 编辑记录

报告相同问题?

问题事件

  • 创建了问题 6月19日