优雅地使用Echarts:探索基于Hooks的数据可视化方案

1,395 阅读5分钟

前言

最近在一个react项目中需要使用到echarts图表,但是直接把echartsdemo实例直接搬运到项目上又有点不优雅,所以我用react+hooks的方式二次封装了hook和组件方便以后项目直接使用,也就了这一篇文章

如果您想看Vue版本的echarts封装,可以看看如何在Vue3中更优雅的使用echart图表这一篇文章,主要讲述了在Vue3中如何结合compositionApi来使用echarts这个图表组件库

普通的使用echarts

我们先来看一下,我们普通的使用hooks和写图表需要怎么做的

import {useEffect, useRef} from 'react'
import * as echarts from 'echarts';
import {EChartsType} from 'echarts';

type EChartsOption = echarts.EChartsOption;

function App() {
    // echart实例
    let chartInstance: EChartsType | null = null
    // 获取dom元素
    const el = useRef<HTMLDivElement | null>(null)
    useEffect(() => {
        // 初始化数据
        chartInstance = echarts.init(el.current as unknown as HTMLDivElement)
        const option: EChartsOption = {
            xAxis: {
                type: 'category',
                data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
            },
            yAxis: {
                type: 'value'
            },
            series: [
                {
                    data: [150, 230, 224, 218, 135, 147, 260],
                    type: 'line'
                }
            ]
        };
        // 设置图表内容
        chartInstance.setOption(option);
        // 注销实例
        return () => {
            chartInstance?.dispose()
        }
    }, [])
    return (
        <>
            <div>
                {/*图表元素*/}
                <div style={{height: '500px', width: '500px'}} ref={el}></div>
            </div>

        </>
    )
}

20230606212748

从上面的代码中,我们简单的将echarts中的折线图案例放到了一个react-hook项目中

嗯.. 从目前来看很简单的吧,但是在业务上我们还需要加载图表时的加载状态、当页面大小改变时执行一些操作,使得代码变得更加的复杂不好维护,而且图表数量越多也会使得代码耦合度越高

这个时候,我们可以利用react16.8之后的hook featureecharts进行二次封装,把一些常用的功能进行封装复用,有效的减少代码的耦合度

先看效果

hooks封装

在封装这个hooks之前,我们首先要确定我们需要什么样子的功能

  • 自动生成一个echarts实例
  • 提供一个设置option的钩子,供图标数据更新
  • 在渲染图标之前,显示loading效果
export default function useEcharts(el: MutableRefObject<HTMLElement | null>) {
    // echarts实例
    let instance: EChartsType | null = null

    const initChart = (el: HTMLElement) => {
        if (!el) return
        instance = echarts.init(el as HTMLElement)
    }
    
    useEffect(() => {
        if (instance !== null) return
        initChart(el.current as HTMLElement)
    }, [el])
    

    const setOption = (option:ECBasicOption) => {
        hideLoading()
        instance?.setOption(option)
    }

    const showLoading = () => {
        instance?.showLoading()
    }
    const hideLoading = () => {
        instance?.hideLoading()
    }

    return {
        setOption,
        showLoading,
        hideLoading
    }
}

经过上面简单的代码封装,现在我们在组件中使用图表只需要给hook传入一个元素作为渲染图表元素,更新图表时使用hook导出的setOption即可,如果您还想显示loading效果,你可以使用showLoadinghideLoading方法

在组件时使用

// 获取dom元素
const el = useRef<HTMLDivElement | null>(null)
// 传递元素给useEcharts
const {setOption} = useEcharts(el)
useEffect(() => {
    const option: EChartsOption = {
        xAxis: {
            type: 'category',
            data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        },
        yAxis: {
            type: 'value'
        },
        series: [
            {
                data: [150, 230, 224, 218, 135, 147, 260],
                type: 'line'
            }
        ]
    };
    // 设置图表内容
    setOption(option);
}, [])

额外封装

重置图表大小

当页面大小发生变化时,图表元素的宽高可能也会发生改变

这个时候,我们需要对图表的大小进行调整,这个echarts已经提供了ApiechartsInstance.resize

我们在hooks中添加一个resize方法,用于重置图表大小并导出resize方法

hooks的第二个对象参数里接收一个autoResize参数,用于判断当元素大小变动时是否resize图表

这里代码就不贴全啦,只贴核心代码

export interface UseEchartsOption {
    autoResize?: boolean
}
import {bind} from "size-sensor";
// 初始化图表时
const resize = (opt: ResizeOpts = {}) => {
    try {
        instance?.resize(opt || defaultResizeOpt as ResizeOpts);
    } catch (e) {
        console.warn(e);
    }
}

const initChart = (el: HTMLElement) => {
    const {
        autoResize = true
    } = opt
    // 添加以下代码
    if (autoResize) {
        bind(el, () => {
            resize();
        });
    }
}

size-sensor包,是用于检测DOM元素的尺寸变化的,当DOM元素的大小变化,执行一些用户操

主题设定

考虑到用户可能会使用到echarts的主题设置功能,我们也要把主题属性提供给用户操作

同样,在option中新增接收一个theme属性,在初始化的时候传入在init函数

export interface UseEchartsOption {
    // ...
    theme?: string
}
const initChart = (el: HTMLElement) => {
    if (!el) return
    // 默认情况下,theme值为default
    const {autoResize = true, theme = 'default'} = opt
    instance = echarts.init(el as HTMLElement, theme)
    if (autoResize) {
        bind(el, () => {
            resize();
        });
    }
}

渲染器

echarts默认的使用canvas元素进行渲染的,其实它还提供了另外一种渲染方式,那就是SVG模式

当用户想要使用SVG模式渲染时,目前hook是还没有实现这一块功能的,我们在option中新增接收renderer作为渲染模式选择的参数

export interface UseEchartsOption {
    // ...
    renderer?: 'SVG'|'CANVAS'
}
const initChart = (el: HTMLElement) => {
    if (!el) return
    const {
        autoResize = true, renderer = 'canvas', theme = 'default'
    } = opt
    instance = echarts.init(el as HTMLElement, theme, {
        renderer: renderer as RendererType,
    })
}

遇到个小问题

在封装这里的时候,遇到个问题,我不能直接写{renderer},而是renderer: renderer as RendererTyperenderer我默认是给的一个字符串的canvas,但是typescript却给我一个警告说类型不匹配,有点神奇~

所以我只能把renderer断言为RendererType类型解决它

// RendererType的类型也是字符串,不太明白为什么不能这样子写
// 有大佬讲解下么  谢谢
export declare type RendererType = 'canvas' | 'svg';

注销事件

当用户在离开页面时,我们需要把图表实例注销和元素绑定的事件进行注销,在hooks添加一个dispose方法,用于注销一系列的事件。

const dispose=()=>{
    const ele=el.current
    if (instance && !instance.isDisposed()) {
        try {
            clear(ele);
        } catch (e) {
            console.warn(e);
        }
        instance.dispose();
    }
}
useEffect(()=>{
    return ()=>{
        dispose();
    }
},[])

到了这里,封装的hook基本上就完成啦,但是我觉得还不够~

接下来我还要再封装一个组件 😀😀

hook的完整代码:useEcharts.ts

这里就不直接贴出来了 因为太占地方了有点影响阅读体验 📄

组件封装

除了用hook来进行封装,我们可以再进行组件封装,把图表封装在一个组件里面

之后,我们需要渲染一个图表的时候,只需要引入这个组件并传入一个option即可

function App() {
    const [option, setOption] = useState<EChartsOption>({
        xAxis: {
            type: 'category',
            data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        },
        yAxis: {
            type: 'value'
        },
        series: [
            {
                data: [15, 230, 224, 218, 135, 147, 260],
                type: 'line'
            }
        ]
    })
    return (
        <>
            <EchartsCmp option={option} width={500} height={500}></EchartsCmp>
        </>
    )
}

export default App

项目仓库地址

echarts-react-hooks