echarts动态图表组件实战

454 阅读2分钟

在前端开发中,数据可视化是一个至关重要的环节。Echarts 作为一款强大的图表库,为我们提供了丰富多样的图表类型和灵活的配置选项。在本文中,我将分享如何在 Vue 项目中构建一个可复用的 Echarts 组件,并在父组件中进行灵活使用。

子组件

<template>
  <div ref="elRef" :style="styles"></div>
</template>

<script setup>
import echarts from "./plugins/index.js";
import { useEchartsResizeAndDestory } from "@/hooks/web/EchartsResizeAndDestoryHooks.js";
defineOptions({ name: "EChart" });
const props = defineProps({
  options: {
    type: Object,
    required: true,
  },
  width: {
    type: String,
    default: "100%",
  },
  height: {
    type: String,
    default: "360px",
  },
});
console.log("props", props);
const elRef = ref();
const chartInstance = shallowRef();

const is = (val, type) => {
  return toString.call(val) === `[object ${type}]`;
};
const isString = (val) => {
  return is(val, "String");
};
const options = computed(() => {
  return props.options;
});

const styles = computed(() => {
  const width = isString(props.width) ? props.width : `${props.width}px`;
  const height = isString(props.height) ? props.height : `${props.height}px`;

  return {
    width,
    height,
  };
});
useEchartsResizeAndDestory(chartInstance, elRef);
const initChart = () => {
  const echartsContainer = elRef.value;
  if (echartsContainer && props.options) {
    console.log("开始渲染数据");
    chartInstance.value = echarts.init(echartsContainer);
    chartInstance.value.setOption(options.value);
  }
};

watch(
  () => options.value,
  (options) => {
    console.log("options", options);
    if (chartInstance.value) {
      chartInstance.value.setOption(options);
    }
  },
  { deep: true }
);

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

<style lang="scss" scoped></style>

import * as echarts from "echarts/core";

// 引入图表类型
import {
  BarChart,
  FunnelChart,
  GaugeChart,
  LineChart,
  MapChart,
  PictorialBarChart,
  PieChart,
  RadarChart,
} from "echarts/charts";

// 引入内置组件
import {
  AriaComponent,
  GridComponent,
  LegendComponent,
  ParallelComponent,
  PolarComponent,
  TitleComponent,
  ToolboxComponent,
  TooltipComponent,
  VisualMapComponent,
} from "echarts/components";
// 引入渲染器
import { CanvasRenderer } from "echarts/renderers";

echarts.use([
  LegendComponent,
  TitleComponent,
  TooltipComponent,
  ToolboxComponent,
  GridComponent,
  PolarComponent,
  AriaComponent,
  ParallelComponent,
  VisualMapComponent,
  BarChart,
  LineChart,
  PieChart,
  MapChart,
  CanvasRenderer,
  PictorialBarChart,
  RadarChart,
  GaugeChart,
  FunnelChart,
]);

export default echarts;
/**
 * @name: EchartsResizeAndDestoryHooks
 * @author: win10
 * @date: 2024/3/14 17:48
 * @description:EchartsResizeAndDestoryHooks
 * @update: 2024/3/14 17:48
 */
import { onBeforeUnmount, onMounted } from "vue";

/**
 *
 * @param {Ref} chartInstance - ECharts实例的Vue响应式引用
 * @param {Ref} echartsContainerRef - ECharts容器的Vue响应式引用
 * @returns {Object} - 包含销毁ECharts实例的方法
 */
export function useEchartsResizeAndDestory(chartInstance, echartsContainerRef) {
  /**
   * 挂载时监听容器大小变化
   */
  onMounted(() => {
    listenChartsResize();
  });
  /**
   * 卸载时销毁ECharts实例并停止监听容器变化
   */
  onBeforeUnmount(() => {
    destoryEcharts();
    destoryListenChartsResize();
  });

  /**
   * 创建实例用于监听容器大小变化
   */
  const observer = new ResizeObserver(() => {
    chartContainerResize();
  });

  /**
   * 调整图表容器
   * @returns
   */
  function chartContainerResize() {
    const echartsContainer = echartsContainerRef.value;
    if (!echartsContainer) return;

    if (chartInstance.value) {
      chartInstance.value.resize();
    }
  }

  /**
   * 开始监听图表容器大小变化
   * @returns
   */
  function listenChartsResize() {
    const echartsContainer = echartsContainerRef.value;
    if (!echartsContainer) return;

    observer.observe(echartsContainer);
  }
  /**
   * 停止监听图表容器大小变化
   */
  function destoryListenChartsResize() {
    observer.disconnect();
  }
  /**
   * 销毁ECharts实例
   */
  function destoryEcharts() {
    if (chartInstance.value) {
      chartInstance.value.dispose();
      chartInstance.value = undefined;
    }
  }

  return { destoryEcharts };
}

// 在Vue组件中的示例用法
// const { destoryEcharts } = useEchartsResizeAndDestory(registerThemeMethod, chartInstance, echartsContainerRef);

在子组件中,我们首先导入了 Echarts 相关的依赖,并定义了组件的属性,包括图表的配置选项 options 、宽度 width 和高度 height 。通过计算属性 styles 来处理宽度和高度的样式。使用 useEchartsResizeAndDestory 钩子函数来处理图表的大小调整和销毁逻辑。在 initChart 方法中初始化图表,并通过 watch 监听 options 的变化来更新图表。同时使用钩子函数处理组件的挂载和卸载时的逻辑,包括监听容器大小变化、调整图表大小、销毁图表实例以及停止监听容器变化。

父组件

<template>
  <div class="echarts-container">
    <el-card shadow="never" class="mb-2">
      <template #header>
        <h3 class="font-bold">基础</h3>
      </template>
      <Echarts v-if="isShow" :options="options" />
    </el-card>
  </div>
</template>
<script setup>
import Echarts from "@/components/Echarts/index.vue";

const days = ref(null);
const networkDelayData = ref(null);
const packetLossRateData = ref(null);
const isShow = ref(false);

const generateRandomData = () => {
  days.value = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
  networkDelayData.value = days.value.map(() =>
    Math.floor(Math.random() * 300)
  );
  packetLossRateData.value = days.value.map(() =>
    Math.floor(Math.random() * 300)
  );
  isShow.value = true;
};
const options = computed(() => ({
  backgroundColor: "#fff",
  title: {
    textStyle: {
      fontSize: 12,
      fontWeight: 400,
    },
    left: "center",
    top: "5%",
  },
  legend: {
    icon: "circle",
    top: "5%",
    right: "5%",
    itemWidth: 6,
    itemGap: 20,
    textStyle: {
      color: "#556677",
    },
  },
  tooltip: {
    trigger: "axis",
    axisPointer: {
      label: {
        show: true,
        backgroundColor: "#fff",
        color: "#556677",
        borderColor: "rgba(0,0,0,0)",
        shadowColor: "rgba(0,0,0,0)",
        shadowOffsetY: 0,
      },
      lineStyle: {
        width: 0,
      },
    },
    backgroundColor: "#fff",
    textStyle: {
      color: "#5c6c7c",
    },
    padding: [10, 10],
    extraCssText: "box-shadow: 1px 0 2px 0 rgba(163,163,163,0.5)",
    formatter: function (params) {
      let tooltipText = "";
      params.forEach((param) => {
        if (param.seriesName === "网络延时") {
          tooltipText += `${param.seriesName}: ${param.value}ms<br>`;
        } else {
          tooltipText += `${param.seriesName}: ${param.value}%<br>`;
        }
      });
      return tooltipText;
    },
  },
  grid: {
    // top: "15%",
    bottom: 20,
  },
  xAxis: [
    {
      type: "category",
      data: days.value,
      axisLine: {
        show: false,
      },
      axisLabel: {
        show: true, // 也应该隐藏y轴标签
      },
      splitLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
    },
  ],
  yAxis: [
    {
      type: "value",
      show: true, // 添加这一行来隐藏这个y轴
      axisLine: {
        show: false,
      },
      axisLabel: {
        show: true, // 也应该隐藏y轴标签
        formatter: function (value) {
          return value + "%";
        },
      },
      splitLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
    },
    {
      type: "value",
      position: "right",
      show: true, // 同样添加这一行来隐藏右侧的y轴
      axisLine: {
        show: false,
      },
      axisLabel: {
        show: true, // 也应该隐藏y轴标签
        formatter: function (value) {
          return value + "%";
        },
      },
      splitLine: {
        show: false,
      },
      axisTick: {
        show: false,
      },
      splitLine: {
        show: false,
      },
    },
  ],
  series: [
    {
      name: "网络延时",
      type: "line",
      data: networkDelayData.value,
      barWidth: "40%",
      symbol: "circle",
      yAxisIndex: 0,
      showSymbol: false,
      lineStyle: {
        width: 1,
      },
    },

    {
      name: "丢包率",
      type: "line",
      data: packetLossRateData.value,
      symbolSize: 1,
      yAxisIndex: 1,
      symbol: "circle",
      showSymbol: false,
      lineStyle: {
        width: 1,
      },
    },
  ],
}));
onMounted(() => {
  setTimeout(() => {
    console.log("模拟网络延迟请求");
    generateRandomData();
  }, 3000);
});
</script>

<style lang="scss" scoped>
.echarts-container {
  position: relative;
  padding: 20px;
}
</style>

在父组件中,我们定义了一些数据,并通过计算属性 options 生成了图表的配置。通过条件渲染 v-if 来控制子组件的显示,并将配置传递给子组件。

为什么使用计算属性呢?

通过使用延迟函数模拟网络请求慢的场景。当子组件开始渲染的时候,父组件的数据并没有获获取到。会造成echarts渲染空白。使用计算属性能够根据依赖的数据进行自动更新。当依赖的数据如当依赖的数据(如 daysnetworkDelayData 、packetLossRateData 等)发生变化时,计算属性 options 会自动重新计算并更新图表的配置,确保图表能够及时反映数据的最新状态。同时计算属性具有缓存机制,如果依赖的数据没有发生变化,不会重复进行计算,提高了性能。