import type { EChartsOption, SetOptionOpts } from "echarts" import { type ECharts, type EChartsInitOpts, use as echartsUse, init } from "echarts/core" import { useEffect, useRef, useState } from "react" import { type ChartEventProp, type ChartEventsProps, chartEvents } from "./types" export type ChartInitOptions = EChartsInitOpts & { /** * Declare the min value of data */ min?: number /** * Declare the max value of data */ max?: number } export type UseChartOptions = { /** * Group id for the chart instance * @see https://echarts.apache.org/en/api.html#echartsInstance.group */ group?: ECharts["group"] /** * Theme for the chart instance * @see https://echarts.apache.org/en/api.html#echarts.init */ theme?: Parameters[1] /** * Use options for the chart instance * @see https://echarts.apache.org/en/api.html#use */ use?: Parameters[0] /** * Event props for the chart instance * @see https://echarts.apache.org/en/api.html#events */ eventProps?: ChartEventsProps /** * Base option for the chart instance * @see https://echarts.apache.org/en/api.html#echartsInstance.setOption */ instanceOptions?: EChartsOption /** * Set option options for the chart instance * @see https://echarts.apache.org/en/api.html#echartsInstance.setOption */ instanceSetOptions?: SetOptionOpts /** * Init options for the chart instance * @see https://echarts.apache.org/en/api.html#echarts.init */ initOptions?: ChartInitOptions chartId?: string } export type OriginalEChartsOption = EChartsOption async function getGlobalUse() { const all = [ import("echarts/features"), import("echarts/charts"), import("echarts/components"), import("echarts/renderers"), ] const promise = await Promise.all(all.map(m => m.then(m => Object.values(m)))) return promise.flat() } const chartInstances: Record = {} export function useChart({ initOptions, theme, use, group, instanceOptions, instanceSetOptions, eventProps, chartId, }: UseChartOptions): [(node: T) => void, ECharts | null] { const containerRef = useRef(null) const echartsRef = useRef(null) const resizeObserverRef = useRef(null) const [started, setStarted] = useState(false) const echartsInstance = echartsRef.current async function setContainerRef(node: T | null) { if ( !node || (containerRef.current && echartsRef.current) || (chartId && chartInstances[chartId]) ) return containerRef.current = node echartsRef.current = await initECharts() resizeObserverRef.current = startResizeObserver() setStarted(true) } async function initECharts() { if (!containerRef.current) return null const useOpts: any = use ?? (await getGlobalUse()) echartsUse(useOpts) const instance = init(containerRef.current, theme, initOptions) if (chartId) { chartInstances[chartId] = instance } return instance } function startResizeObserver() { const resizeObserver = new ResizeObserver(() => { echartsRef.current?.resize() }) if (containerRef.current) resizeObserver.observe(containerRef.current) return resizeObserver } // biome-ignore lint/correctness/useExhaustiveDependencies: We need to clean up echarts and the resize observer useEffect(() => { return () => { if (chartId) { chartInstances[chartId]?.dispose() delete chartInstances[chartId] } echartsInstance?.dispose?.() if (containerRef.current) { resizeObserverRef.current?.unobserve(containerRef.current) resizeObserverRef.current?.disconnect() } } }, []) // biome-ignore lint/correctness/useExhaustiveDependencies: We need started to be true before we can set the group useEffect(() => { if (!echartsInstance) return if (group) echartsInstance.group = group }, [group, started, echartsInstance]) // biome-ignore lint/correctness/useExhaustiveDependencies: We need started to be true before we can set the option useEffect(() => { if (!echartsInstance) return echartsInstance.setOption(instanceOptions ?? {}, instanceSetOptions) }, [ // Currently we choose this approach to avoid unnecessary destructuring of instanceSetOptions ...Object.values(instanceOptions ?? {}), ...Object.values(instanceSetOptions ?? {}), // started, echartsInstance, ]) // biome-ignore lint/correctness/useExhaustiveDependencies: We need started to be true before we can set the option useEffect(() => { toggleChartEvents("on", echartsInstance, eventProps) return () => { toggleChartEvents("off", echartsInstance, eventProps) } }, [started, eventProps, echartsInstance]) return [setContainerRef, echartsRef.current] } function toggleChartEvents( action: "on" | "off", echartsInstance: ECharts | null, eventProps?: ChartEventsProps, ) { if (!eventProps || !echartsInstance) return for (const [event, handler] of Object.entries(eventProps)) { // @ts-expect-error We cannot cover all the event types echartsInstance[action](chartEvents[event as ChartEventProp], handler) } } export const getEchartsInstance = (chartId: string): ECharts | undefined => { return chartInstances[chartId] }