import { FunctionComponent, useEffect } from 'react'; import { useCallback, useReducer, useRef } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import defaultsDeep from 'lodash/defaultsDeep'; import * as echarts from 'echarts/core'; import { EChartsInitOpts } from 'echarts/types/dist/echarts'; import { EChartsOption } from 'echarts/types/dist/option'; import { TooltipOption } from 'echarts/types/dist/shared'; import { getLineSeries } from '../Line'; import { getSankeySeries } from '../Sankey'; import { ThemeDefinition } from '../themes/Theme'; import { getMutationObserver } from '../utils/observe'; import { getClassName } from '../utils/styles'; import { getTheme } from '../utils/theme'; import { getLegendTooltip, getSankeyTooltip } from '../utils/tooltip'; import { ThemeColor } from '../themes/ThemeColor'; /** * See https://echarts.apache.org/en/option.html#tooltip * * @public * @beta */ export interface TooltipOptionProps extends TooltipOption { /** * The destination label shown in the tooltip -- for Sankey only */ destinationLabel?: string; /** * The source label shown in the tooltip -- for Sankey only */ sourceLabel?: string; } /** * See https://echarts.apache.org/en/option.html * * @public * @beta */ export interface ChartsOptionProps extends EChartsOption { /** * Tooltip component -- see https://echarts.apache.org/en/option.html#tooltip */ tooltip?: TooltipOptionProps | TooltipOptionProps[]; } /** * This component is based on the Apache ECharts chart library. It provides additional functionality, custom * components, and theming for PatternFly. This provides a collection of React based components you can use to build * PatternFly patterns with consistent markup, styling, and behavior. * * See https://echarts.apache.org/en/api.html#echarts * * @public * @beta */ export interface ChartsProps { /** * The className prop specifies a class name that will be applied to outermost element */ className?: string; /** * Specify height explicitly, in pixels */ height?: number; /** * The id prop specifies an ID that will be applied to outermost element. */ id?: string; /** * Flag indicating to use the legend tooltip (default). This may be overridden by the `option.tooltip` property. */ isLegendTooltip?: boolean; /** * Flag indicating to use the SVG renderer (default). This may be overridden by the `opts.renderer` property. */ isSvgRenderer?: boolean; /** * This creates a Mutation Observer to watch the given DOM selector. * * When the pf-v6-theme-dark selector is added or removed, this component will be notified to update its computed * theme styles. However, if the dark theme is not updated dynamically (e.g., via a toggle), there is no need to add * this Mutation Observer. * * Note: Don't provide ".pf-v6-theme-dark" as the node selector as it won't exist in the page for light theme. * The underlying querySelectorAll() function needs to find the element the dark theme selector will be added to. * * See https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Locating_DOM_elements_using_selectors * * @propType string * @example * @example * @example */ nodeSelector?: string; /** * ECharts uses this object to configure its properties; for example, series, title, and tooltip * * See https://echarts.apache.org/en/option.html */ option?: ChartsOptionProps; /** * Optional chart configuration * * See https://echarts.apache.org/en/api.html#echarts.init */ opts?: EChartsInitOpts; /** * The theme prop specifies a theme to use for determining styles and layout properties for a component. Any styles or * props defined in theme may be overwritten by props specified on the component instance. * * See https://echarts.apache.org/handbook/en/concepts/style/#theme */ theme?: ThemeDefinition; /** * Specifies the theme color. Valid values are 'blue', 'green', 'multi', etc. * * Note: Not compatible with theme prop * * @example themeColor={ChartThemeColor.blue} */ themeColor?: string; /** * Specify width explicitly, in pixels */ width?: number; } export const Charts: FunctionComponent = ({ className, height, id, isLegendTooltip = true, isSvgRenderer = true, nodeSelector, option, opts, theme, themeColor, width, ...rest }: ChartsProps) => { const containerRef = useRef(null); const echart = useRef(null); const [update, forceUpdate] = useReducer((x) => x + 1, 0); const getSize = () => ({ ...(height && { height: `${height}px` }), ...(width && { width: `${width}px` }) }); const getTooltip = useCallback( (series: any[], tooltipType: string, isSkeleton: boolean, echart) => { // Skeleton should not have any interactions if (isSkeleton) { return undefined; } else if (tooltipType === 'sankey') { return getSankeyTooltip(series, option); } else if (tooltipType === 'legend') { return getLegendTooltip(series, option, echart); } return option.tooltip; }, [option] ); const getSeries = useCallback( (chartTheme: ThemeDefinition, isSkeleton: boolean) => { let tooltipType; const series: any = cloneDeep(option?.series); const newSeries = []; series.map((serie: any) => { switch (serie.type) { case 'sankey': tooltipType = 'sankey'; // Overrides legend tooltip newSeries.push(getSankeySeries(serie, chartTheme, isSkeleton)); break; case 'line': if (!tooltipType) { tooltipType = 'legend'; } newSeries.push(getLineSeries(serie, chartTheme, isSkeleton)); break; default: newSeries.push(serie); break; } }); return { series, tooltipType }; }, [option?.series] ); useEffect(() => { const isSkeleton = themeColor === ThemeColor.skeleton; const chartTheme = theme ? theme : getTheme(themeColor); const renderer = isSvgRenderer ? 'svg' : 'canvas'; echart.current = echarts.init( containerRef.current, chartTheme, defaultsDeep(opts, { height, renderer, width }) // height and width are necessary here for unit tests ); const { series, tooltipType } = getSeries(chartTheme, isSkeleton); echart.current?.setOption({ ...option, ...(isLegendTooltip && { tooltip: getTooltip(series, tooltipType, isSkeleton, echart.current) }), series }); return () => { echart.current?.dispose(); }; }, [ containerRef, getSeries, getTooltip, height, isLegendTooltip, isSvgRenderer, option, opts, theme, themeColor, update, width ]); // Resize observer useEffect(() => { echart.current?.resize(); }, [height, width]); // Dark theme observer useEffect(() => { let observer = () => {}; observer = getMutationObserver(nodeSelector, () => { forceUpdate(); }); return () => { observer(); }; }, [nodeSelector]); return
; }; Charts.displayName = 'Charts';