From f910063dbe97bd0ec4a9e4dc292d3cabb63768c9 Mon Sep 17 00:00:00 2001 From: "Mr.Dr.Professor Patrick" Date: Tue, 9 Jul 2024 14:18:12 +0200 Subject: [PATCH] fix(Highcharts plugin): fix onRender trigger behaviour (#502) * fix(Highcharts plugin): fix onRender trigger behaviour * fix: handle nullable entries --- .../scatter/PerformanceIssue.stories.tsx | 1 + .../components/HighchartsComponent.tsx | 15 +++-- .../renderer/components/HighchartsReact.tsx | 28 ++++++++ .../renderer/components/useElementSize.ts | 67 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 src/plugins/highcharts/renderer/components/useElementSize.ts diff --git a/src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx b/src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx index 68798423..d4549335 100644 --- a/src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx +++ b/src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx @@ -52,6 +52,7 @@ const Template: Story = () => { type="highcharts" data={widgetData} onLoad={action('onLoad')} + onRender={action('onRender')} /> ); diff --git a/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx b/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx index 1711c035..0c9c1536 100644 --- a/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx +++ b/src/plugins/highcharts/renderer/components/HighchartsComponent.tsx @@ -129,13 +129,7 @@ export class HighchartsComponent extends React.PureComponent { } componentDidUpdate() { - const needRenderCallback = - this.props.onRender && !this.state.isError && !this.props.splitTooltip; - if (needRenderCallback) { - this.props.onRender?.({ - renderTime: getChartPerformanceDuration(this.getId()), - }); - + if (this.needRenderCallback()) { const widget = this.chartComponent.current ? this.chartComponent.current.chart : null; if (this.state.callback && widget) { @@ -169,6 +163,7 @@ export class HighchartsComponent extends React.PureComponent { constructorType={options?.useHighStock ? 'stockChart' : 'chart'} containerProps={{className: 'chartkit-graph'}} ref={this.chartComponent} + onRender={this.needRenderCallback() && this.props.onRender} /> ); } @@ -226,4 +221,10 @@ export class HighchartsComponent extends React.PureComponent { window.requestAnimationFrame(this.reflow); } } + + private needRenderCallback() { + const {splitTooltip, onRender} = this.props; + const {isError} = this.state; + return !splitTooltip && onRender && !isError; + } } diff --git a/src/plugins/highcharts/renderer/components/HighchartsReact.tsx b/src/plugins/highcharts/renderer/components/HighchartsReact.tsx index 782f4539..4bbee046 100644 --- a/src/plugins/highcharts/renderer/components/HighchartsReact.tsx +++ b/src/plugins/highcharts/renderer/components/HighchartsReact.tsx @@ -2,8 +2,14 @@ import React from 'react'; +import afterFrame from 'afterframe'; import Highcharts from 'highcharts'; +import type {ChartKitProps} from '../../../../types'; +import {measurePerformance} from '../../../../utils'; + +import {useElementSize} from './useElementSize'; + interface HighchartsReactRefObject { chart: Highcharts.Chart | null | undefined; container: React.RefObject; @@ -16,6 +22,7 @@ interface HighchartsReactProps { highcharts?: typeof Highcharts; options: Highcharts.Options; callback?: Highcharts.ChartCallbackFunction; + onRender?: ChartKitProps['onRender']; } const useIsomorphicLayoutEffect = @@ -25,8 +32,13 @@ export const HighchartsReact: React.ForwardRefExoticComponent< React.PropsWithoutRef & React.RefAttributes > = React.memo( React.forwardRef(function HighchartsReact(props: HighchartsReactProps, ref) { + const {onRender} = props; const containerRef = React.useRef(null); const chartRef = React.useRef(); + const {width, height} = useElementSize(containerRef); + const performanceMeasure = React.useRef | null>( + measurePerformance(), + ); useIsomorphicLayoutEffect(() => { function createChart() { @@ -83,6 +95,22 @@ export const HighchartsReact: React.ForwardRefExoticComponent< [], ); + React.useLayoutEffect(() => { + if (width && height) { + if (!performanceMeasure.current) { + performanceMeasure.current = measurePerformance(); + } + + afterFrame(() => { + const renderTime = performanceMeasure.current?.end(); + if (typeof renderTime === 'number') { + onRender?.({renderTime}); + } + performanceMeasure.current = null; + }); + } + }, [width, height, onRender]); + return
; }), ); diff --git a/src/plugins/highcharts/renderer/components/useElementSize.ts b/src/plugins/highcharts/renderer/components/useElementSize.ts new file mode 100644 index 00000000..21b323a7 --- /dev/null +++ b/src/plugins/highcharts/renderer/components/useElementSize.ts @@ -0,0 +1,67 @@ +import React from 'react'; + +import debounce from 'lodash/debounce'; +import round from 'lodash/round'; + +const RESIZE_DEBOUNCE = 200; +const ROUND_PRESICION = 2; + +export interface UseElementSizeResult { + width: number; + height: number; +} + +export function useElementSize( + ref: React.MutableRefObject | null, + // can be used, when it is needed to force reassign observer to element + // in order to get correct measures. might be related to below + // https://github.com/WICG/resize-observer/issues/65 + key?: string, +) { + const [size, setSize] = React.useState({ + width: 0, + height: 0, + }); + + React.useLayoutEffect(() => { + if (!ref?.current) { + return undefined; + } + + const handleResize: ResizeObserverCallback = (entries) => { + if (!Array.isArray(entries)) { + return; + } + + const entry = entries[0]; + + if (entry && entry.borderBoxSize) { + const borderBoxSize = entry.borderBoxSize[0] + ? entry.borderBoxSize[0] + : (entry.borderBoxSize as unknown as ResizeObserverSize); + // ...but old versions of Firefox treat it as a single item + // https://github.com/mdn/dom-examples/blob/main/resize-observer/resize-observer-text.html#L88 + + setSize({ + width: round(borderBoxSize.inlineSize, ROUND_PRESICION), + height: round(borderBoxSize.blockSize, ROUND_PRESICION), + }); + } else if (entry) { + const target = entry.target as HTMLElement; + setSize({ + width: round(target.offsetWidth, ROUND_PRESICION), + height: round(target.offsetHeight, ROUND_PRESICION), + }); + } + }; + + const observer = new ResizeObserver(debounce(handleResize, RESIZE_DEBOUNCE)); + observer.observe(ref.current); + + return () => { + observer.disconnect(); + }; + }, [ref, key]); + + return size; +}