echarts实现3D饼状图

56 阅读2分钟
<template>
  <div class="alarm-statistics-wrapper">
    <div ref="alarmPie" class="alarm-pie"></div>
  </div>
</template>

<script>
import 'echarts-gl'
export default {
  name: 'AlarmStatistics',
  props: {
    data: {
      type: Array,
      default: () => ([
        { name: '待处置', value: 120, color: '#02ABF9' },
        { name: '处置中', value: 160, color: '#FFAE3A' },
        { name: '已处置', value: 200, color: '#0CC2C2' },
      ])
    },
    innerRatio: { type: Number, default: 0.78 }, // 空心比
    heightPx: { type: Number, default: 26 },     // 3D环厚度基准
    viewAlpha: { type: Number, default: 40 },
    viewDistance: { type: Number, default: 180 }
  },
  data() {
    return {
      chart: null,
      option: null
    }
  },
  mounted() {
    this.initChart()
    window.addEventListener('resize', this.onResize)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.onResize)
    this.chart && this.chart.dispose()
  },
  methods: {
    onResize() { this.chart && this.chart.resize() },
    initChart() {
      this.chart && this.chart.dispose()
      this.chart = this.$echarts.init(this.$refs.alarmPie)
      this.option = this.build3DPie(this.data, this.innerRatio)
      this.chart.setOption(this.option)
      // 添加透明 2D 饼用于 label
      this.addLabelLayer()
      this.chart.setOption(this.option)
      this.bindListen(this.chart)
    },
    build3DPie(pieData, internalDiameterRatio) {
      let series = []
      let sumValue = 0
      let startValue = 0
      let endValue = 0
      let legendData = []
      let legendPercent = []
      const k = 1 - internalDiameterRatio
      const sorted = [...pieData].sort((a, b) => b.value - a.value)
      sorted.forEach((item, i) => {
        sumValue += item.value
        series.push({
          name: item.name || `item${i}`,
            type: 'surface',
          parametric: true,
          wireframe: { show: false },
          pieData: item,
          pieStatus: { selected: false, hovered: false, k },
          itemStyle: { color: item.color }
        })
      })
      series.forEach(s => {
        endValue = startValue + s.pieData.value
        s.pieData.startRatio = startValue / sumValue
        s.pieData.endRatio = endValue / sumValue
        s.parametricEquation = this.getParametricEquation(
          s.pieData.startRatio,
          s.pieData.endRatio,
          false,
          false,
          k,
          s.pieData.value
        )
        startValue = endValue
        const pct = this.fmtFloat(s.pieData.value / sumValue, 4)
        legendData.push({ name: s.name, value: pct })
        legendPercent.push({ name: s.name, value: pct })
      })
      const boxHeight = this.getHeight3D(series, this.heightPx)
      return {
        legend: {
          data: legendData.map(l => l.name),
          top: 8,
          left: 8,
          itemWidth: 12,
          itemHeight: 12,
          textStyle: { color: '#A1E2FF', fontSize: 12 },
          icon: 'circle',
          formatter: name => {
            const item = legendPercent.find(i => i.name === name)
            return `${name}  ${(item.value * 100).toFixed(2)}%`
          }
        },
        tooltip: {
          formatter: p => {
            if (p.seriesName !== 'pie2d') {
              const slice = series[p.seriesIndex]
              const percent = ((slice.pieData.endRatio - slice.pieData.startRatio) * 100).toFixed(2)
              return `${p.seriesName}<br/>
                <span style="display:inline-block;width:10px;height:10px;background:${p.color};margin-right:5px;border-radius:2px;"></span>${percent}%`
            }
          },
          backgroundColor: 'rgba(5,35,70,.85)',
          borderWidth: 0,
          padding: [6, 10]
        },
        xAxis3D: { min: -1, max: 1 },
        yAxis3D: { min: -1, max: 1 },
        zAxis3D: { min: -1, max: 1 },
        grid3D: {
          show: false,
          boxHeight,
          viewControl: {
            alpha: this.viewAlpha,
            distance: this.viewDistance,
            rotateSensitivity: 0,
            zoomSensitivity: 0,
            panSensitivity: 0,
            autoRotate: false
          }
        },
        series
      }
    },
    addLabelLayer() {
      this.option.series.push({
        name: 'pie2d',
        type: 'pie',
        radius: ['10%', '75%'],
        center: ['50%', '50%'],
        startAngle: -20,
        clockwise: false,
        silent: true,
        hoverAnimation: false,
        labelLine: { show: true, length: 38, length2: 18, lineStyle: { color: '#6EC9FF' } },
        label: {
          show: true,
          rich: {
            name: { color: '#fff', fontSize: 12, lineHeight: 16 },
            val: { color: '#6FE2FF', fontSize: 12, fontWeight: 600 }
          },
          formatter: p => `{name|${p.name}}\n{val|${p.value}}`
        },
        data: this.data.map(d => ({
          name: d.name,
          value: d.value,
          itemStyle: { color: 'rgba(0,0,0,0)' },
          tooltip: { show: false }
        }))
      })
    },
    getHeight3D(series, height) {
      const sorted = [...series].sort((a, b) => b.pieData.value - a.pieData.value)
      return (height * 25) / sorted[0].pieData.value
    },
    getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, h) {
      const midRatio = (startRatio + endRatio) / 2
      const startR = startRatio * Math.PI * 2
      const endR = endRatio * Math.PI * 2
      const midR = midRatio * Math.PI * 2
      if (startRatio === 0 && endRatio === 1) isSelected = false
      k = k || 1 / 3
      const offsetX = isSelected ? Math.cos(midR) * 0.1 : 0
      const offsetY = isSelected ? Math.sin(midR) * 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 < startR) return offsetX + Math.cos(startR) * (1 + Math.cos(v) * k) * hoverRate
          if (u > endR) return offsetX + Math.cos(endR) * (1 + Math.cos(v) * k) * hoverRate
          return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate
        },
        y: (u, v) => {
          if (u < startR) return offsetY + Math.sin(startR) * (1 + Math.cos(v) * k) * hoverRate
          if (u > endR) return offsetY + Math.sin(endR) * (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
        }
      }
    },
    fmtFloat(num, n) {
      const f = Math.round(parseFloat(num) * Math.pow(10, n)) / Math.pow(10, n)
      let s = f.toString()
      let rs = s.indexOf('.')
      if (rs < 0) { rs = s.length; s += '.' }
      while (s.length <= rs + n) s += '0'
      return parseFloat(s)
    },
    bindListen(chart) {
      let selectedIndex = ''
      let hoveredIndex = ''
      chart.on('click', params => {
        if (params.seriesName === 'pie2d') return
        const slice = this.option.series[params.seriesIndex]
        const isSelected = !slice.pieStatus.selected
        const { hovered, k } = slice.pieStatus
        const { startRatio, endRatio, value } = slice.pieData
        if (selectedIndex !== '' && selectedIndex !== params.seriesIndex) {
          const prev = this.option.series[selectedIndex]
          prev.parametricEquation = this.getParametricEquation(prev.pieData.startRatio, prev.pieData.endRatio, false, false, prev.pieStatus.k, prev.pieData.value)
          prev.pieStatus.selected = false
        }
        slice.parametricEquation = this.getParametricEquation(startRatio, endRatio, isSelected, hovered, k, value)
        slice.pieStatus.selected = isSelected
        selectedIndex = isSelected ? params.seriesIndex : ''
        chart.setOption(this.option)
      })
      chart.on('mouseover', params => {
        if (params.seriesName === 'pie2d') return
        if (hoveredIndex === params.seriesIndex) return
        if (hoveredIndex !== '') {
          const prev = this.option.series[hoveredIndex]
            prev.parametricEquation = this.getParametricEquation(prev.pieData.startRatio, prev.pieData.endRatio, prev.pieStatus.selected, false, prev.pieStatus.k, prev.pieData.value)
          prev.pieStatus.hovered = false
          hoveredIndex = ''
        }
        const cur = this.option.series[params.seriesIndex]
        cur.parametricEquation = this.getParametricEquation(cur.pieData.startRatio, cur.pieData.endRatio, cur.pieStatus.selected, true, cur.pieStatus.k, cur.pieData.value + 5)
        cur.pieStatus.hovered = true
        hoveredIndex = params.seriesIndex
        chart.setOption(this.option)
      })
      chart.on('globalout', () => {
        if (hoveredIndex === '') return
        const cur = this.option.series[hoveredIndex]
        cur.parametricEquation = this.getParametricEquation(cur.pieData.startRatio, cur.pieData.endRatio, cur.pieStatus.selected, false, cur.pieStatus.k, cur.pieData.value)
        cur.pieStatus.hovered = false
        hoveredIndex = ''
        chart.setOption(this.option)
      })
    }
  },
  watch: {
    data: {
      deep: true,
      handler() { this.initChart() }
    }
  }
}
</script>

<style scoped>
.alarm-statistics-wrapper {
  width:100%;
  height:100%;
  display:flex;
  flex-direction:column;
}
.alarm-pie {
  width:100%;
  height:100%;
  min-height:240px;
}
</style>