echarts特殊饼图样式组件:渐变,空心圆环、有边框,有间隔,带图例

128 阅读2分钟

image.png

定义组件ChartPie

<div class="chart-pie" :class="{ 'no-title': !title }">
  <div class="chart-pie-title" v-if="title">
    {{ title }}
  </div>
  <Chart v-on="$listeners" v-bind="$attrs" :option="mergedOption" ref="chartBase" />
  <slot name="legend" :data="data" :percents="percents" :colors="colors">
    <table class="legend-table" v-if="legend">
      <tbody>
        <tr v-for="(item, index) in data" :key="item[nameKey]" class="legend">
          <td>
            <div class="dot" :style="{ backgroundColor: colors[index] }"/>
          </td>
          <td class="name">
            <XText font-size="48px">{{ item[nameKey] }}</XText>
          </td>
          <td class="align-right">
            <p>
              <Num font-size="56px">{{ item[valueKey] }}</Num>
              <Unit v-if="unit" font-size="40px">{{ unit }}</Unit>
            </p>
          </td>
          <td v-if="percent" class="align-right">
            <Num font-size="56px" style="margin: 0;">{{ percents[index] }}%</Num>
          </td>
        </tr>
      </tbody>
    </table>
  </slot>
</div>

import XText from 'XText'
import Unit from 'Unit'
import Num from 'Num'
import Chart from 'Chart'

const TinyColor = window.tinycolor.TinyColor // 颜色操作与转换的开源库

export default Vue.extend({
  inheritAttrs: false,
  components: {
    XText,
    Unit,
    Num,
    Chart,
  },
  props: {
    data: {
      type: Array,
      required: true
    },
    title: {
      type: String,
      default: undefined
    },
    option: {
      type: Object,
      default: () => ({})
    },
    legend: {
      type: Boolean,
      default: true
    },
    unit: {
      type: String,
      default: undefined
    },
    percent: {
      type: Boolean,
      default: false
    },
    nameKey: {
      type: String,
      default: 'name'
    },
    valueKey: {
      type: String,
      default: 'value'
    },
    percentRoundPrecision: {
      default: 2,
      type: Number
    }
  },
  data() {
    return {
      isMounted: false,
    }
  },
  mounted() {
    this.isMounted = true
  },
  computed: {
    percents() {
      if (!this.percent) {
        return []
      }
      const key = this.valueKey
      const data = this.data.map(d => Number(d[key]))
      const sum = _.sum(data)
      return data.map(d => (d / sum * 100).toFixed(this.percentRoundPrecision))
    },
    defaultColors() {
      return [
        '#52D680',
        '#CE7DEB',
        '#48CFF1',
        '#1E95E3',
        '#BADB3C',
        '#87D04D',
        '#EFBF59',
        '#2F7AE6',
        '#7991FF',
        '#1CC143',
        '#E17940',
        '#D89B38',
        '#12CFCD',
        '#85AACA',
      ]
    },
    colors() {
      return this.option.color || this.defaultColors
    },
    mergedOption() {
      if (!this.isMounted) {
        return
      }
      const chart = this.$refs.chartBase.$refs.chart
      if (!chart) {
        return
      }
      const centerX = chart.getWidth() / 2
      const centerY = chart.getHeight() / 2
      const color = this.colors
      const nameKey = this.nameKey
      const valueKey = this.valueKey

      const baseOption = {
        tooltip: {},
        color,
        series: {
          type: 'pie',
          radius: ['45%', '90%'],
          label: { show: false },
          labelLine: { show: false },
          padAngle: 2,
          data: this.data.map((d, i) => {
            const outerColor = color[i]
            const innerColor = new TinyColor(outerColor).setAlpha(0.4).toString()

            return {
              name: d[nameKey],
              value: d[valueKey],
              itemStyle: {
                color: {
                  type: 'radial',
                  x: centerX,
                  y: centerY,
                  r: Math.min(centerX, centerY),
                  colorStops: [
                    { offset: 0, color: outerColor },
                    { offset: 0, color: innerColor },
                    { offset: 0.8, color: innerColor },
                    { offset: 0.8, color: outerColor },
                    { offset: 1, color: outerColor },
                  ],
                  global: true
                }
              }
            }
          })
        },
      }
      return _.merge(baseOption, this.option)
    },
  },
})

// less code here
.chart-pie {
  width: 100%;
  height: 100%;
  flex: 1;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    'title .'
    'chart legend';
  align-items: center;
  gap: 20px;

  &.no-title {
    grid-template-columns: auto 1fr;
    grid-template-rows: 1fr;
    grid-template-areas:
      'chart legend';
  }

  &-title {
    font-size: 54px;
    color: white;
    font-weight: 500;
    justify-self: center;
    grid-area: title;
    font-family: PingFangSC;
  }

  .chart {
    aspect-ratio: 1 / 1;
    grid-area: chart;
  }

  .legend-table {
    grid-area: legend;
    white-space: nowrap;
    line-height: 1;
    border-collapse: collapse;

    td {
      padding: var(--legend-padding, 10px);
    }

    .dot {
      width: 24px;
      height: 24px;
      border-radius: 50%;
    }

    .name {
      width: 100%;
    }

    .align-right {
      text-align: right;
    }
  }
}
  • 其中用到的组件有Chart如下,其它组件(XText,Unit,Num)略
<VChart
  class="chart"
  ref="chart"
  :option="mergedOption"
  :loading-options="loadingOptions"
  v-bind="$attrs"
  v-on="$listeners"
/>
const defaultXAxisOption = {
  axisLabel: {
    interval: 0,
    textStyle: {
      fontSize: 64,
      color: '#CCCCCC',
    },
  },
  axisLine: {
    lineStyle: {
      color: '#4B636A',
      width: 4,
    },
  },
  axisTick: {
    show: false,
  },
}

const defaultYAxisOption = {
  nameTextStyle: {
    color: '#CCCCCC',
    fontSize: 64,
  },
  splitLine: {
    lineStyle: {
      type: 'dashed',
    },
  },
  axisTick: {
    show: false,
  },
  axisLine: {
    show: false,
  },
  axisLabel: {
    textStyle: {
      color: '#CCCCCC',
      fontSize: 64,
    },
  },
}

const defaultLegendOption = {
  lineStyle: {
    width: 10,
  },
  textStyle: {
    color: '#ffffff',
    fontSize: 54,
    padding: [0, 0, 0, 30],
  },
  itemHeight: 40,
  itemWidth: 100,
  itemGap: 60,
}

const defaultTooltipOption = {
  textStyle: {
    fontSize: 64,
    color: '#fff',
  },
  appendToBody: true,
  backgroundColor: '#0C3E5F',
  borderColor: '#0CB6FF',
  borderWidth: 4,
  padding: 20,
}

export default Vue.extend({
  components: {
    VChart: window.VueECharts
  },
  inheritAttrs: false,
  props: {
    autoresize: {
      type: Boolean,
      default: false
    },
    option: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      resizeObserver: undefined
    }
  },
  mounted() {
    if (this.autoresize) {
      this.enableAutoResize()
    }
  },
  beforeDestroy() {
    this.$refs.chart?.dispose()
    this.resizeObserver?.disconnect()
  },
  computed: {
    mergedOption() {
      const { tooltip, xAxis, yAxis, legend } = this.option

      const baseOption = {
        xAxis: this.copySingleOrArray(xAxis, defaultXAxisOption),
        yAxis: this.copySingleOrArray(yAxis, defaultYAxisOption),
        legend: this.copySingleOrArray(legend, defaultLegendOption),
        tooltip: this.copySingleOrArray(tooltip, defaultTooltipOption),
      }
      _.merge(baseOption, this.option)
      this.setTooltipClass(baseOption.tooltip)
      return baseOption
    },
    loadingOptions() {
      return {
        maskColor: 'rgba(0, 0, 0, 0.6)',
        textColor: 'white',
        color: 'white',
        fontSize: '86px',
        fontFamily: 'PingFangSC',
        spinnerRadius: 30,
        lineWidth: 10,
        text: ''
      }
    }
  },
  methods: {
    enableAutoResize() {
      this.resizeObserver = new ResizeObserver(() => {
        this.$refs.chart?.resize()
      })
      this.resizeObserver.observe(this.$el)
    },
    copySingleOrArray(customOption, defaultOption) {
      if (!customOption) {
        return undefined
      } else if (Array.isArray(customOption)) {
        const result = []
        let size = customOption.length
        while (size--) {
          result.push(_.cloneDeep(defaultOption))
        }
        return result
      }
      return _.cloneDeep(defaultOption)
    },
    setTooltipClass(tooltip) {
      if (!tooltip) {
        return
      }
      const items = Array.isArray(tooltip) ? tooltip : [tooltip]
      for (const item of items) {
        const classes = ['chart-tooltip']
        if (item.className) {
          classes.push(item.className)
        }
        item.className = classes.join(' ')
      }
    }
  }
})

// less code here
.chart {
  flex: 1;
  overflow: hidden;

  &-tooltip {
    span {
      &:first-of-type {
        border-radius: 50% !important;
        height: 64px !important;
        width: 64px !important;
        vertical-align: top;
        border: 12px solid transparent;
        background-clip: content-box;
      }
    }
  }
}

使用ChartPie组件的方法

<ChartPie title="学历结构" :data="educationChartData" unit="人" />

数据结构

[{
    "unit": "人",
    "name": "专科",
    "value": 0
}]