封装一个的 ECharts 折线图 Hook—— useLineEcharts

400 阅读4分钟

摘要

本文最适合平时不怎么接触echarts图表,但是偶尔有一些简单图表需求或者没有接触过echarts的 jym 食用,可以帮助你在项目中快速接入echarts,不用看密密麻麻的文档,就可以快速复原ui效果了,大大提升开发效率。

背景

最近两月公司比较忙,很久了没有总结了,这个月有个关于图表的需求,由于之前接过echarts感觉应该很快就能写完,但是由于距离上次接触有段时间了,很多都忘记了,排期时排少了,只能狠狠加班了。

echarts直接使用还是很简单,复杂的点在于ui的复原,复原ui就需要看着那密密麻麻的文档,这里我就打算写一个当前项目专用的hook,因为一般一个项目下图表风格都是一样的,不会有什么变动;

最后整整花了一天的时间成功封装了一个hook,这样以后再有折线图需求就可以快速实现,即使未来有其他项目也需要图表,我也可以进行二次改动快速实现新的ui,最为新项目的hook。

左边ui,右边实现,复原度也高达95%了

image.png

在组件中如何使用

我期望这个hook的使用是这样的,通过参数传数据,导出一个ref,绑定到一个需要渲染的div上,数据变化div重新渲染。图表的样式有hook内置好,

 const { chartRef } = useLineEcharts({
     //数据
 })
 
 <div ref={chartRef}  />

代码片段

点击运行查看实际效果

//关键代码
  const chartOption = useMemo(() => {
    return {
      titleText: chartData?.indicatorsDesc,
      xAxisData: chartData?.time,
      seriesData: [
        {
          name: '今日',
          data: chartData?.today,
          lineColor: '#3D70FF',
        },
        {
          name: '昨日',
          data: chartData?.yesterday,
          lineColor: '#42C4D8',
        },
        {
          name: '7天前',
          data: chartData?.sevenDaysAgo,
          lineColor: '#C1C1C1',
          lineType: 'dashed' as const,
        },
      ],
      Tooltip: {
        titleFormatter: formatterTitle,
        valueFormatter: formatterVal,
      },
    };
  }, [chartData]);

  const { chartRef } = useLineEcharts(chartOption);

Hook 概览

  • 职责:负责 ECharts 实例的初始化、设置 option、销毁,暴露 chartRef 以供绑定 DOM。
  • tooltip的属性中是不支持设置background属性的,只能设置backgroundColor,但是在css中backgroundColor是不能设置渐变色的,这里我利用了一个bug来实现的(也是偶然发现的)。
iimport { useEffect, useRef } from 'react';
import * as echarts from 'echarts';

// https://echarts.apache.org/zh/option.html#title
interface UseLineEchartsProps {
  /**
   * 图表标题文本
   */
  titleText?: string;
  /**
   * x轴数据
   */
  xAxisData: string[];
  /**
   * 系列数据数组
   * lineColor 系列线条颜色,若未指定则按默认颜色列表顺序取用
   */
  seriesData: Array<{
    name: string; // 系列名称
    data: number[] | string[]; // 数据
    lineColor?: string; // 线条颜色
    lineType?: 'solid' | 'dashed'; // 线条类型 实线 | 虚线
    yAxisIndex?: number; // 显示的y轴位置  0 左侧 1 右侧 默认0
  }>;
  /**
   * 提示框格式化函数
   */
  Tooltip?: {
    // 标题格式化
    titleFormatter?: (data: any) => string;
    // 值格式化
    valueFormatter?: (value: any) => string;
  };
}

// 默认颜色列表
const DEFAULT_COLOR = [
  '#3D70FF',
  '#42C4D8',
  '#C1C1C1',
  '#8bcff1',
  '#d393e6',
  '#FF2842',
  '#079933',
];

/**
 * useLineEcharts
 * 该hooks用于封装ECharts折线图的初始化与配置
 */
export const useLineEcharts = ({
  titleText,
  xAxisData,
  seriesData,
  Tooltip = {
    titleFormatter: (data) => data,
    valueFormatter: (value) => value,
  },
}: UseLineEchartsProps) => {
  // chartRef用于绑定ECharts实例的DOM节点
  const chartRef = useRef<HTMLDivElement>(null);
  const chartInstance = useRef<echarts.ECharts | null>(null);

  useEffect(() => {
    if (chartRef.current) {
      // 初始化ECharts实例
      chartInstance.current = echarts.init(chartRef.current);
      // 组装每个系列的颜色,若未指定则按默认颜色列表顺序取用
      const color: string[] = seriesData?.map(
        (item, index) => item.lineColor || DEFAULT_COLOR[index],
      );

      // ECharts配置项
      let option = {
        // color: 图表全局调色盘,对应每个系列的颜色
        color,
        // 标题组件
        title: {
          text: titleText, // 标题文本
          padding: [10, 0, 0, 0], // 标题内边距
          textStyle: {
            fontSize: 16,
          },
        },
        // 网格组件,控制图表绘图区的位置
        grid: {
          left: 5,
          right: 16,
          bottom: 5,
          containLabel: true, // 保证坐标轴标签不被裁剪
        },
        // 提示框组件
        tooltip: {
          trigger: 'axis', // 触发类型,坐标轴触发
          axisPointer: {
            type: 'line', // 指示器类型为直线
            lineStyle: {
              color: '#4275FF', // 指示线颜色
            },
          },
          // 设置提示框背景色(利用小bug实现渐变背景)
          backgroundColor:
            '#fff;background:linear-gradient( #EBF1FF 7%, #ffffff 44%);',

          // 提示框内容格式化
          formatter: (data) => {
            const _title = Tooltip?.titleFormatter?.(data[0].name);

            const strArr = data.map((item) => {
              const _val = Tooltip?.valueFormatter?.(item.value);
              const isHasName = item.seriesName.length > 0;
              const jcv = isHasName ? 'space-between' : 'start';
              const mr = isHasName ? '10px' : '0px';

              return `<div style="display:inline-flex; align-items: center; justify-content: ${jcv}; width: 100%;"><div>${item.marker}<div style="display: inline-block;margin-right: ${mr};"> ${item.seriesName}</div> </div><div style="font-weight: bold; text-align: right;">${_val}</div></div>`;
            });

            return `<div>${_title}<br/>${strArr.join('<br/>')}</div>`;
          },
        },

        // 图例组件(右上角的提示)
        legend: {
          show: true, // 显示图例
          top: 8, // 距离顶部8px
          right: 0, // 靠右
          itemWidth: 8, // 图例标记宽度
          itemStyle: {
            opacity: 0, // 图例标记透明(只显示文字)
          },
          textStyle: {
            color: '#757A86', // 图例文字颜色
          },
          lineStyle: {
            cap: 'round', // 图例线条端点样式
          },
          // 图例数据
          data: seriesData.map((item) => ({
            name: item.name,
            lineStyle: {
              type: item.lineType,
              dashOffset: 6,
            },
          })),
        },

        // 区域缩放组件,支持内部缩放
        dataZoom: [
          {
            type: 'inside', // 内置型缩放
            start: 0, // 默认起始百分比
            end: 100, // 默认结束百分比
          },
        ],

        // x轴配置
        xAxis: {
          type: 'category', // 类目轴
          axisTick: {
            show: false, // 不显示刻度线
          },
          offset: 0, // 轴线偏移‘
          axisLine: {
            lineStyle: {
              color: '#F0F0F0', // 轴线颜色
            },
          },
          axisLabel: {
            color: '#616161', // 标签文字颜色
            showMinLabel: true, // 显示最小标签
            showMaxLabel: true, // 显示最大标签
          },

          splitLine: {
            lineStyle: {
              color: '#F0F0F0', // 分割线颜色
            },
          },
          data: xAxisData, // x轴数据
        },

        // y轴配置
        yAxis: [
          {
            offset: 5, // 轴线偏移
            type: 'value', // 数值轴
            position: 'left', // 轴线在左侧
            axisLabel: {
              color: '#616161', // 标签文字颜色
            },
          },
          {
            offset: 0, // 轴线偏移
            type: 'value', // 数值轴
            position: 'right', // 轴线在右侧
            axisLabel: {
              color: '#616161', // 标签文字颜色
            },
          },
        ],

        // 折线图列表,每个对象对应一条折线
        series: seriesData.map((item) => {
          return {
            type: 'line', // 折线图
            name: item.name, // 系列名称
            showSymbol: false, // 不显示拐点标记
            smooth: true, // 平滑曲线
            lineStyle: {
              width: 1.5, // 线条宽度
              type: item.lineType, // 线条类型
              color: item.lineColor, // 线条颜色
            },
            yAxisIndex: item.yAxisIndex || 0,
            data: item.data?.map((it) => Number(String(it).replace(/%+$/, ''))), // 数据
          };
        }),
      };

      // 设置ECharts配置项
      chartInstance.current.setOption(option);
    }
    // 组件卸载时销毁ECharts实例,防止内存泄漏
    return () => {
      chartInstance.current?.dispose();
    };
  }, [xAxisData, seriesData, titleText, Tooltip]);

  // 返回chartRef,需绑定到div元素
  return {
    chartRef,
  };
};


最后

总之,就是对折线图进行一层ui上的封装,一来方便了项目下未来折线图的需求;二来以后其他项目在做图表就不需要看密密麻麻的文档,可以直接在这个hook上改动,可以大幅提高开发效率。