react + ts封装echarts基础组件

302 阅读5分钟

最近在学习react,所以就写了个react+ts的项目。由于其中的大屏模块涉及到了大量echarts的使用,因此封装了一个echarts基础组件

直接上代码

第一部分 引入模块

import * as echarts from 'echarts/core';
import React, { FC } from 'react';
import {
  DatasetComponent,
  DatasetComponentOption,
  DataZoomComponent,
  DataZoomComponentOption,
  GridComponent,
  GridComponentOption,
  LegendComponent,
  LegendComponentOption,
  TitleComponent,
  TitleComponentOption,
  ToolboxComponent,
  ToolboxComponentOption,
  TooltipComponent,
  TooltipComponentOption,
  MarkLineComponent,
  PolarComponent,
  AxisPointerComponentOption,
  AriaComponentOption,
  AriaComponent,
  AxisPointerComponent,
  BrushComponent,
  BrushComponentOption,
  CalendarComponent,
  CalendarComponentOption,
  VisualMapComponent,
} from 'echarts/components';
import {
  BarChart,
  BarSeriesOption,
  LineChart,
  LineSeriesOption,
  PieChart,
  PictorialBarChart,
  GaugeChart,
  PieSeriesOption,
  PictorialBarSeriesOption,
  GaugeSeriesOption,
  MapChart,
} from 'echarts/charts';
import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared';
import { UniversalTransition } from 'echarts/features';
import { SVGRenderer } from 'echarts/renderers';
import { useEffect, useRef } from 'react';
import { useSize } from 'ahooks';
import { debounce, isArray, isNumber, isPlainObject, merge } from 'lodash-es';
import { getScale } from '@/utils/global';
import { Empty } from 'antd';

echarts.use([
  MapChart,
  PieChart,
  BarChart,
  LineChart,
  GaugeChart,
  GridComponent,
  TitleComponent,
  TooltipComponent,
  LegendComponent,
  DataZoomComponent,
  SVGRenderer,
  ToolboxComponent,
  MarkLineComponent,
  PolarComponent,
  PictorialBarChart,
  DatasetComponent,
  UniversalTransition,
  AriaComponent,
  BrushComponent,
  CalendarComponent,
  AxisPointerComponent,
  VisualMapComponent,
]);

export type MyChartOption = echarts.ComposeOption<
  | DatasetComponentOption
  | DataZoomComponentOption
  | GridComponentOption
  | LegendComponentOption
  | TitleComponentOption
  | ToolboxComponentOption
  | TooltipComponentOption
  | LineSeriesOption
  | BarSeriesOption
  | PieSeriesOption
  | PictorialBarSeriesOption
  | GaugeSeriesOption
  | AxisPointerComponentOption
  | AriaComponentOption
  | BrushComponentOption
  | CalendarComponentOption
  | XAXisOption
  | YAXisOption
>;

export interface MyChartProps {
  option: MyChartOption;
  isTransform?: boolean;
  isMap?: boolean;
  json?: any[];
  mapName?: string;
}

export type symbolType = string | [string, string] | undefined;

这里提几个注意的点,就是XAXisOption, YAXisOption是文档中没有的,但是我在实际使用时一直提示说XAXisOption, YAXisOption类型不匹配,因此找了好久才找到这两个类型。还有就是在使用echarts画地图时,要注意引入MapChart, VisualMapComponent这两个部分。

第二部分 封装组件

基本的思路如下:

  1. 提供默认配置,在初始化时会和传入的配置合并

  2. 判断是否为空,为空则不生成echarts

  3. 转换Formatter, 属于定制功能,可要可不要,用来统一echarts中tooltip的效果

  4. transformKeys转换,即将一些属性,比如字体,根据大屏的比例进行一定程度的缩放

  5. onTransformOptions对传入的options进行转换,比如上面的转换Formatter和transformKeys就在这个方法里调用,也可以不调用

  6. initChart初始化echarts

    1. 判断是否已经有了,有则销毁
    2. 判断是否有传入option或者挂载的节点生成了,有一个没有则不生成echarts
    3. 判断是否为空,为空则返回
    4. 合并默认配置和传入的option
    5. 判断是否为地图,为地图则调用registerMap方法
    6. 监听数据变化,重置echarts
    7. 监听窗口大小,重置echarts大小

基本思路就是这么些,下面是具体代码:

// 默认配置
const defaultOptions = {
  tooltip: {
    textStyle: {
      fontSize: 14,
    },
  },
  legend: {
    itemWidth: 25,
    itemHeight: 14,
    pageIconColor: 'currentColor',
    textStyle: { fontSize: 14, color: 'currentColor' },
    pageTextStyle: { fontSize: 14, color: 'currentColor' },
  },
  xAxis: {
    nameGap: 15,
    nameTextStyle: {
      fontSize: 14,
    },
    axisLabel: {
      fontSize: 14,
    },
  },
  yAxis: {
    nameGap: 15,
    nameTextStyle: {
      fontSize: 14,
    },
    axisLabel: {
      fontSize: 14,
    },
  },
};

const MyChart: React.FC<MyChartProps> = ({ option, isTransform, isMap = false, json = [], mapName = '' }) => {
  const cRef = useRef<HTMLDivElement>(null); // 挂载的dom
  const cInstance = useRef<echarts.ECharts>(); // echarts实例

// 判断是否为空
  const isEmpty = () => {
    if (!option) return false;
    if (!isArray(option.series)) return !option.series?.data?.length;
    return !option.series.length || !option.series[0].data?.length;
  };

// 转换tooltip的Formatter  属于是定制了  可自由更改  也可删除
  const transformFormatter = (value: Array<{ key: string; unit?: string; default?: string }>) => () => {
    return (params: any) => {
      const style = 'padding-left:0.2rem;text-align:right;font-weight:900;';
      const title = `<p style="font-size:1.1em;line-height:1.5;">${params[0].name}</p>`;
      const content = params.map((item: { marker: any; seriesName: any; data: { [x: string]: any } }) => {
        const items = [
          `<td>${item.marker}</td>`,
          `<td>${item.seriesName}</td>`,
          ...value.map((prop) => `<td style="${style}">${item.data[prop.key] ?? prop.default}${prop.unit || ''}</td>`),
        ];
        return `<tr>${items.join('')}</tr>`;
      });
      return `${title}<table>${content.join('')}</table>`;
    };
  };

// transformKeys 是用来按照一定比例缩放这些属性 保证能在大屏中正常显示
  const transformKeys = ['fontSize', 'itemWidth', 'itemHeight', 'right', 'bottom', 'top', 'left', 'width', 'shadowBlur'];
  
 // 转换options  
  const onTransformOptions = (options: any) => {
    // 不转换
    if (!isTransform) return options;
    // 数组
    if (isArray(options)) {
      options.forEach(onTransformOptions);
      return options;
    }
    // 非对象【字符串、函数】
    if (!isPlainObject(options)) return options;
    // 对象
    for (const key in options) {
      if (key === 'formatter' && isArray(options[key])) {
        options[key] = transformFormatter(options[key]);
      } else if (transformKeys.includes(key) && isNumber(options[key])) {
        options[key] = getScale(options[key]);
      } else {
        options[key] = onTransformOptions(options[key]);
      }
    }
    return options;
  };

// 初始化initChart
  const initChart = () => {
    if (cInstance.current) cInstance.current.dispose(); // 判断是否有了 
    if (!cRef || !option) return; // 判断是否生成
    if (isEmpty()) return; // 判断是否为空

    cInstance.current = echarts.init(cRef.current);
    const options = merge({}, defaultOptions, option); // 将传入的options合并默认配置
    for (const key in defaultOptions) {
      if (!option[key]) delete options[key];
    }
    
    // 是否为地图
    if (isMap) {
      // @ts-expect-error: TS1234 because the library definition is wrong
      echarts.registerMap(mapName, json); // MapInput  GeoJSONSourceInput
    }
    cInstance.current.setOption(onTransformOptions(options));

    const series = isArray(option.series) ? option.series : [option.series];
    const seriesIndex = series.findIndex((item) => item && item.type === 'pie');
    if (seriesIndex >= 0) {
      let index = 0;
      cInstance.current.dispatchAction({ type: 'highlight', seriesIndex, dataIndex: index });
      cInstance.current.on('mouseover', function (e) {
        if (index === e.dataIndex) return;
        cInstance.current && cInstance.current.dispatchAction({ type: 'downplay', seriesIndex, dataIndex: index });
        cInstance.current && cInstance.current.dispatchAction({ type: 'highlight', seriesIndex, dataIndex: e.dataIndex });
        index = e.dataIndex;
      });
    }
  };

  // 监听数据变化,重置图表
  useEffect(() => {
    initChart();
  }, [cRef, option]);

  // 监听窗口大小变化,重置图表大小
  const resize = debounce(() => {
    cInstance.current && cInstance.current.resize();
  }, 200);
  const size = useSize(cRef);
  useEffect(() => {
    resize();
  }, [size]);

  return (
    !isEmpty() ? <div ref={cRef} style={{ width: '100%', height: '100%' }} /> : (
      <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
    )
  );
};

export default MyChart;

给个示例:

const ChinaMap: FC = () => {
  const [data, setData] = useState<any[]>([]);
  useEffect(() => {
    const fn = async () => {
      const response = await fetch('/JSON/china.json'); // 使用相对路径引用JSON文件
      const data = await response.json();
      setData(data);
    };
    fn();
  }, []);
  const option = {
    tooltip: {
      //鼠标hover是提示信息
      show: true,
      formatter: '{b}', //提示标签格式
      backgroundColor: '#ff7f50', //提示标签背景颜色
      textStyle: {
        color: '#fff',
        fontSize: '20',
      }, //提示标签字体颜色
    },
    visualMap: {
      //视觉映射组件()
      type: 'continuous', //连续型
      min: 0,
      max: 150,
      right: 120,
      bottom: 140,
      text: ['150', '0'], // 文本,默认为数值文本
      textGap: 10, //文本与图形之间的距离
      itemWidth: 40, //图形的宽
      itemHeight: 200, //图形的高
      calculable: true, //是否显示拖动手柄
      textStyle: {
        color: '#fff',
        fontSize: 25,
      }, //省份标签字体颜色
      //align:"left",
      //inverse: true, //反向
      inRange: {
        //地图颜色变化
        color: ['#3246FB', '#24DD57', '#FDD52C'],
      },
      // outOfRange:{
      // 	symbolSize: [100, 100]
      // }
    },
    series: [
      {
        type: 'map',
        color: 'red',
        mapType: 'china',
        roam: 'false', //是否开启缩放平移
        label: {
          //标签字体样式
          position: 'inside',
          show: true, //显示省份标签
          textStyle: {
            color: '#fff',
            fontSize: 20,
          }, //省份标签字体颜色
          emphasis: {
            //对应的鼠标悬浮效果
            show: true,
            textStyle: {
              color: '#800080',
            },
          },
        },
        itemStyle: {
          borderWidth: 2, //区域边框宽度
          borderColor: '#fff', //区域边框颜色
          //areaColor: "#ffefd5", //区域颜色
          fontSize: 30,
        },
        emphasis: {
          borderWidth: 2,
          borderColor: '#3246FB', // 边框颜色
          // areaColor: "#6AE5E5", // 区域颜色
        },
        data: CITIES.map((item) => {
          const value = Math.floor(Math.random() * 200);
          return {
            name: item,
            value,
          };
        }),
      },
    ],
  } as MyChartOption;
  return (
    <div className={styles.ChinaMap}>
      <MyChart option={option} isMap={true} json={data} mapName="china" />
    </div>
  );
};

image.png

如有任何问题,欢迎指正

这个项目目前用到技术点为: react + ts + echarts + 高德地图 + webrtc + Nest.js + next.js 我打算把这个项目打造成一个缝合怪,将所学的技术全都融合进去,做成一个大杂烩。预计花费一年时间,每个月出一个大版本,加入一些新东西。 这个react学习项目的地址是这里,欢迎大家前来围观。也可以加入到这个项目中来,一起学习进步,将这个项目打造成自己的开源项目。