Newer
Older
robot_dog_patrol_front / src / components / Echart / PieChart3D.vue
<script lang="ts" setup name="PieChart">
import * as echarts from 'echarts'
import 'echarts-gl'
import type { ECharts } from 'echarts'
import { init, number } from 'echarts'
import type { Ref } from 'vue'
import type { ECBasicOption } from 'echarts/types/dist/shared'
import type { pieDataI, pieOption, pieSeriesOption } from './echart-interface'
import tdTheme from './theme.json' // 引入默认主题
const props = defineProps({
  /**
   * id
   */
  id: {
    type: String,
    default: 'chart',
  },
  /**
   * 加载中
   */
  loading: {
    type: Boolean,
    default: false,
  },
  /**
   * 展示每项标题文字lebel
   */
  showEmphasis: {
    type: Boolean,
    default: true,
  },

  /**
   * 标题
   */
  title: {
    type: String,
    default: '',
  },
  /**
   * 图例是否显示
   */
  legend: {
    type: Object,
    default: () => {
      return {
        show: true,
        // color: '#fff',
        orient: 'vertical',
        color: 'rgba(255, 255, 255, 1)',
        type: 'scroll',
        right: '0%',
        top: 'center',
        icon: 'circle',
        itemWidth: 12,
        itemHeight: 12,
        textStyle: {
          fontSize: 14,
          color: 'rgba(255, 255, 255, 1)',
        },
      }
    },
  },
  /**
   * 文本标签是否显示
   */
  labelShow: {
    type: Boolean,
    default: true,
  },
  /**
   * 图表宽
   */
  width: {
    type: String,
    default: '100%',
  },
  /**
   * 图表高
   */
  height: {
    type: String,
    default: '100%',
  },
  /**
   * 饼图的半径[内半径,外半径]
   */
  radius: {
    type: Array,
    default: () => {
      return ['30%', '45%']
    },
  },
  /**
   * 距离容器右侧距离,默认0,为legend在右侧时做准备
   */
  right: {
    type: [Number, String],
    default: 0,
  },
  /**
   * 是否要展示成玫瑰图,或玫瑰图类型:radius|area
   */
  roseType: {
    type: [String, Boolean],
    default: false,
  },
  /**
   * 显示值类型:percentage百分比|value数值
   */
  valueType: {
    type: String,
    default: 'percentage',
  },
  /**
   * 数据,格式为 [{name:'xxx',value:0},...]
   */
  data: {
    type: Array<pieDataI>,
    default: () => { return [] },
  },
  /**
   * 颜色
   */
  colors: {
    type: Array,
    default: () => { return ['#3d7eff', '#caddff'] },
  },
  /**
   * 网格配置
   */
  grid: {
    type: Object,
    default: () => {
      return {
        top: '10%',
        left: '5%',
        right: '50%',
        bottom: 20,
        containLabel: true, // 是否包含坐标轴的刻度标签
      }
    },
  },
  /**
   * 标签文字颜色
   */
  fontColor: {
    type: String,
    default: '#000000',
  },
  /**
   * 标签格式化
   */
  labelFormatter: {
    type: String,
    default: '',
  },
  /**
   * 标签位置, outside饼图扇区外侧, inside饼图扇区内部, center饼图中心位置
   */
  labelPosition: {
    type: String,
    default: 'center',
  },
  /**
   * 悬停格式化
   */
  hoverFormatter: {
    type: String,
    default: '{b}<br/>数量:{c}<br/>占比:{d}%',
  },

})

// 图表对象
let chart: ECharts
const chartRef: Ref<HTMLElement | null> = ref(null)

// 监听数据变化
watch(
  [() => props.data], ([newNums], [oldNums]) => {
    refreshChart()
  },
  {
    immediate: true,
    deep: true,
  },
)
// 构建option
function buildOption() {
  const colors = ['#7652FC', '#6993FF', '#9BD342', '#FFD900', '#AA4CD3', '#00ADEF']
  function getParametricEquation(
    startRatio: number,
    endRatio: number,
    isSelected: boolean,
    isHovered: boolean,
    k: number,
    h: number,
  ) {
    // 计算
    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;
    // }
    isSelected = false
    // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
    k = typeof k !== 'undefined' ? k : 1 / 3

    // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
    const offsetX = isSelected ? Math.sin(midRadian) * 0.1 : 0
    const offsetY = isSelected ? Math.cos(midRadian) * 0.1 : 0

    // 计算高亮效果的放大比例(未高亮,则比例为 1)
    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
      },
    }
  }
  // 生成模拟 3D 饼图的配置项
  function getPie3D(pieData: any, internalDiameterRatio: any) {
    const series = []
    let sumValue = 0
    let startValue = 0
    let endValue = 0
    const legendData = []
    const k
      = typeof internalDiameterRatio !== 'undefined'
        ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio)
        : 1 / 3

    // 为每一个饼图数据,生成一个 series-surface 配置
    for (let i = 0; i < pieData.length; i++) {
      sumValue += pieData[i].value

      const seriesItem = {
        name:
          typeof pieData[i].name === 'undefined'
            ? `series${i}`
            : pieData[i].name,
        type: 'surface',
        parametric: true,
        wireframe: {
          show: false,
        },
        pieData: pieData[i],
        pieStatus: {
          selected: false,
          hovered: false,
          k: 1 / 10,
        },
      } as any

      if (typeof pieData[i].itemStyle != 'undefined') {
        const itemStyle = {} as any

        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)
    }

    // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
    // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
    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 = getParametricEquation(
        series[i].pieData.startRatio,
        series[i].pieData.endRatio,
        false,
        false,
        k,
        series[i].pieData.value,
      )

      startValue = endValue

      legendData.push(series[i].name)
    }

    // // 补充一个透明的圆环,用于支撑高亮功能的近似实现。
    // series.push({
    //   name: "mouseoutSeries",
    //   type: "surface",
    //   parametric: true,
    //   wireframe: {
    //     show: false,
    //   },
    //   itemStyle: {
    //     opacity: 0.1,
    //     color: "#E1E8EC",
    //   },
    //   parametricEquation: {
    //     u: {
    //       min: 0,
    //       max: Math.PI * 2,
    //       step: Math.PI / 20,
    //     },
    //     v: {
    //       min: 0,
    //       max: Math.PI,
    //       step: Math.PI / 20,
    //     },
    //     x: function (u, v) {
    //       return ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * 2;
    //     },
    //     y: function (u, v) {
    //       return ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * 2;
    //     },
    //     z: function (u, v) {
    //       return Math.cos(v) > 0 ? -0.5 : -5;
    //     },
    //   },
    // });
    // // // 补充一个透明的圆环,用于支撑高亮功能的近似实现。
    // series.push({
    //   name: "mouseoutSeries",
    //   type: "surface",
    //   parametric: true,
    //   wireframe: {
    //     show: false,
    //   },
    //   itemStyle: {
    //     opacity: 0.1,
    //     color: "#E1E8EC",
    //   },
    //   parametricEquation: {
    //     u: {
    //       min: 0,
    //       max: Math.PI * 2,
    //       step: Math.PI / 20,
    //     },
    //     v: {
    //       min: 0,
    //       max: Math.PI,
    //       step: Math.PI / 20,
    //     },
    //     x: function (u, v) {
    //       return ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * 2;
    //     },
    //     y: function (u, v) {
    //       return ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * 2;
    //     },
    //     z: function (u, v) {
    //       return Math.cos(v) > 0 ? -5 : -7;
    //     },
    //   },
    // });
    // series.push({
    //   name: "mouseoutSeries",
    //   type: "surface",

    //   parametric: true,
    //   wireframe: {
    //     show: false,
    //   },
    //   itemStyle: {
    //     opacity: 0.1,
    //     color: "#E1E8EC",
    //   },
    //   parametricEquation: {
    //     u: {
    //       min: 0,
    //       max: Math.PI * 2,
    //       step: Math.PI / 20,
    //     },
    //     v: {
    //       min: 0,
    //       max: Math.PI,
    //       step: Math.PI / 20,
    //     },
    //     x: function (u, v) {
    //       return (
    //         ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * 2.2
    //       );
    //     },
    //     y: function (u, v) {
    //       return (
    //         ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * 2.2
    //       );
    //     },
    //     z: function (u, v) {
    //       return Math.cos(v) > 0 ? -7 : -7;
    //     },
    //   },
    // });
    // return series;
    return series
  }
  const series = getPie3D(props.data.map((item: any, index: number) => ({
    name: item.name,
    value: Number(item.value),
    itemStyle: {
      color: colors[index],
      opacity: 0.5,
    },
  })), 0.5)
  series.push({
    name: 'pie2d',
    type: 'pie',
    label: {
      show: false,
      opacity: 1,
      lineHeight: 24,
      textStyle: {
        fontSize: 28,
        color: '#fff',
      },
    },
    labelLine: {
      normal: {
        show: false,
        length: 30,
        length2: 200,
        lineStyle: {
          // color: '#e6e6e6'
          width: 4,
        },
      },
    },
    startAngle: 300, // 起始角度,支持范围[0, 360]。
    clockwise: false, // 饼图的扇区是否是顺时针排布。上述这两项配置主要是为了对齐3d的样式
    radius: ['36%', '46%'],
    center: ['0', '50%'],
    data: props.data.map((item: any, index: number) => ({
      name: item.name,
      value: Number(item.value),
      itemStyle: {
        color: colors[index],
        opacity: 0,
      },
    })),
    itemStyle: {
      opacity: 0,
    },
  })
  const option: pieOption = {
    grid: props.grid, // 网格
    legend: {
      show: true,
      orient: 'vertical',
      data: props.data.map((item: any) => item.name),
      top: 'center',
      icon: 'circle',
      type: 'scroll',
      itemWidth: 24,
      itemHeight: 14,
      right: '0%',
      textStyle: {
        color: '#fff',
        fontSize: 12,
        rich: {
          name: {
            // width: '45%',
            fontSize: 12,
            color: '#B0D8DF',
            fontFamily: 'Source Han Sans CN',
          },
          value: {
            width: 46,
            fontSize: 12,
            padding: [0, 0, 0, 5],
            color: '#fff',
            fontFamily: 'Source Han Sans CN',
          },
          A: {
            fontSize: 12,
            color: '#B0D8DF',
            fontFamily: 'Source Han Sans CN',
          },
          rate: {
            width: 60,
            fontSize: 12,
            padding: [0, 5, 0, 10],
            color: '#10DD24',
            fontFamily: 'Source Han Sans CN',
          },
          B: {
            fontSize: 12,
            color: '#B0D8DF',
            fontFamily: 'Source Han Sans CN',
          },
        },
      },
      formatter(name: any) {
        let total = 0
        let target
        for (let i = 0; i < props.data.length; i++) {
          total += props.data[i].value
          if (props.data[i].name === name) {
            target = props.data[i].value
          }
        }
        const arr = [
          `{name|${name}}`,
          // "{rate|" + ((target / total) * 100).toFixed(1) + "}{B|%}",
        ]
        return arr.join('  ')
      },
    }, // 图例
    // animation: true,
    tooltip: {
      show: true,
      trigger: 'item',
      confine: true,
      backgroundColor: 'rgba(2,53,80, 0.6)',
      // borderColor: "rgba(64, 180, 176, 0.6)",
      textStyle: {
        color: 'rgba(255, 255, 255, 1)',
        fontSize: 16,
      },
      formatter: (params) => {
        if (
          params.seriesName !== 'mouseoutSeries'
          && params.seriesName !== 'pie2d'
        ) {
          return `${params.seriesName
            }</br>${option.series[params.seriesIndex].pieData.value
            }`
        }
      },
    },
    xAxis3D: {
      min: -1,
      max: 1,
    },
    yAxis3D: {
      min: -1,
      max: 1,
    },
    zAxis3D: {
      min: -1,
      max: 1,
    },
    grid3D: {
      show: false,
      boxHeight: 5,
      left: '5%',
      top: '0%',
      width: '50%',
      // environment: "rgba(255,255,255,0)",
      viewControl: {
        distance: 240,
        alpha: 55,
        beta: 70,
        autoRotate: false, // 自动旋转
        rotateSensitivity: 1,
        zoomSensitivity: 0,
        panSensitivity: 0,
      },
    },
    series,
  }
  console.log(option.series, '123')
  return option
}
// 初始化图表
function initChart() {
  chart = init(chartRef.value as HTMLElement, tdTheme)
  chart.setOption({})
}

// 刷新图表
function refreshChart() {
  if (chart) {
    const option = buildOption()
    chart.setOption(option as unknown as ECBasicOption, true)
    chart.resize()
  }
}

window.addEventListener('resize', () => {
  chart.resize()
})

onMounted(() => {
  initChart()
})
</script>

<template>
  <div :id="id" ref="chartRef" v-loading="loading" class="chart" :style="{ height, width }" />
</template>

<style lang="scss" scoped>
.chart {
  width: 100%;
  height: 100%;
}
</style>