echarts通用封装(折线图为例)

385 阅读2分钟

因为工作需要使用大量echarts折线图和饼图,为了减少重复代码量以及方便使用,打算将echarts图封装成组件,这里以折线图为例,其他类型的图类似,甚至可以直接把本次封装的折线图作为其他类型使用,因为这次打算做一个通用封装。

基本要求

  • 高度宽度可自定义方便调整不同容器中的图表样式
  • 封装y轴名,单位位于英文括号内
  • 封装series数据,可以直接将数据绑定在组件上
  • 内置一条警戒线,单位取y轴名的单位
  • 可自定义options覆盖内置封装(这是最关键的一点)

封装步骤

完整代码见:项目仓库

先看一下基本效果图: 效果图.png

<template>
    <div ref="myChart" :style="{ height: height, width: width }"></div>
</template>
<script>
export default {
  name: 'chart-line',
  props: {
    width: {
      type: String,
      default: '100%'
    },
    height: {
      type: String,
      default: '100%'
    },
    // y轴名
    label: {
      type: String,
      default: ''
    },
    data: {
      type: Array,
      default: function () {
        return [];
      }
    },
    threshold: {
      type: Number
    },
    options: {
      type: Object
    }
  },
  watch: {
    data: {
      deep: true,
      handler(val) {
        this.resizeHandler();
        this.render(val);
      }
    }
  },
  mounted() {
    this.initChart();
    window.addEventListener('resize', this.resizeHandler, true);
  },
  data() {
    return {
      chart: null,
      defaultOptions: {
        // data中数据大于1条时展示图例
        legend: {
          show: this.data.length > 1
        },
        grid: {
          containLabel: true
        },
        xAxis: {
          type: 'time',
          // x轴不显示刻度
          axisTick: {
            show: false
          }
        },
        yAxis: {
          type: 'value',
          name: this.label
          // 单位是 人 时y轴刻度最小间隔为1
          // minInterval: this.label.match(/(([^)]+))/)[1] === '人' ? 1 : undefined
        },
        tooltip: {
          trigger: 'axis',
          //  tooltip 框限制在图表的区域内
          confine: true
        }
      },
      // 内置一条markLine
      defaultMarkLine: {
        data: [
          {
            yAxis: this.threshold,
            label: {
              formatter: (e) => {
                return this.label.match(/(([^)]+))/)
                    ? `${e.value} ${this.label.match(/(([^)]+))/)[1]}`
                    : e.value;
              }
            }
          }
        ],
        symbol: 'none',
        label: {
          color: 'red'
        },
        lineStyle: {
          color: 'red',
          type: [5, 5]
        }
      }
    };
  },
  methods: {
    // 初始化渲染
    initChart() {
      this.chart = this.$echarts.init(this.$refs.myChart);
      this.render();
    },
    // 重复渲染
    render() {
      if (this.chart) {
        // 更新series数据
        this.defaultOptions['series'] = this.data.map((row) => {
          return {
            name: row.name,
            data: row.data,
            type: 'line',
            showSymbol: false
          };
        })
        // 设置阈值
        if (
            this.threshold &&
            this.defaultOptions.series.length > 0 &&
            this.defaultOptions.series[0].data &&
            this.defaultOptions.series[0].data.length > 0
        ) {
          this.defaultOptions.series[0]['markLine'] = this.defaultMarkLine;
        }
        // 合并自定义options
        this.options && this.recursiveMerge(this.defaultOptions, this.options);
        console.log('options', this.defaultOptions);
        this.chart.setOption(this.defaultOptions);
      }
    },
    resizeHandler() {
      this.chart && this.chart.resize();
    },
    // 递归合并
    recursiveMerge(base, extend) {
      if (!this.isPlainObject(base)) {
        return extend;
      }
      for (const key in extend) {
        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
          continue;
        }
        base[key] =
            this.isPlainObject(base[key]) && this.isPlainObject(extend[key])
                ? this.recursiveMerge(base[key], extend[key])
                : extend[key];
      }
      return base;
    },
    // 判断输入是否为对象
    isPlainObject(input) {
      return input && typeof input === 'object' && !Array.isArray(input);
    }
  },
  beforeDestroy() {
    if (!this.chart) {
      return;
    }
    window.removeEventListener('resize', this.resize);
    this.chart.dispose();
    this.chart = null;
  }
};
</script>

使用

首先给一个容器存放组件,设置label(y轴名),放一些测试数据,设置一个阈值:

<div class="chart-block">
  <chart-line label="连接时延(ms)" :data="testData" :threshold="30"></chart-line>
</div>

<script>
import ChartLine from "@/components/chart-line";

export default {
  name: 'module-preview',
  components: { ChartLine },
  data() {
    return {
      testData: [
        {
          name: '上海',
          data: [
            ['2022-08-06 17:20:20', 15],
            ['2022-08-06 17:30:20', 20],
            ['2022-08-06 17:40:20', 30],
            ['2022-08-06 17:50:20', 25],
            ['2022-08-06 18:00:20', 35],
            ['2022-08-06 18:10:20', 10],
          ]
        },
        {
          name: '北京',
          data: [
            ['2022-08-06 17:20:20', 25],
            ['2022-08-06 17:30:20', 10],
            ['2022-08-06 17:40:20', 20],
            ['2022-08-06 17:50:20', 15],
            ['2022-08-06 18:00:20', 30],
            ['2022-08-06 18:10:20', 35],
          ]
        }
      ]
    }
  }
};
</script>

<style scoped>
.chart-block {
  height: 300px;
}
</style>

如果这时候我想要对tooltip内容进行格式化或者想改变一下图例的样式又或者想让x轴显示刻度该怎么办呢? 直接在data中写一个自定义options就可以了:

<chart-line label="连接时延(ms)" :options="customOptions" :data="testData" :threshold="30"></chart-line>

customOptions: {
        legend: {
          icon: 'circle'
        },
        tooltip: {
          valueFormatter: (value) => value + ' 毫秒'
        },
        xAxis: {
          axisTick: {
            show: true
          }
        },
      }

效果如下: 合并.png

总结

大部分代码应该都是比较好理解的,比较关键的地方在于如果使用这个组件时提供了自定义options,图表生成前则会合并默认封装的defaultOptions和自定义的options,这样遇到少部分需要特殊处理的图表时,只需要提供一个小型的options就可以了,不用再进行新的封装。

由于封装的组件只进行了一些基本的测试,因此可能还存在一些问题,如果任何疑问欢迎交流讨论。