diff --git a/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/resize-debounce-chrome-linux.png b/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/resize-debounce-chrome-linux.png new file mode 100644 index 0000000000..91fff008e7 Binary files /dev/null and b/e2e/screenshots/all.test.ts-snapshots/baselines/test-cases/resize-debounce-chrome-linux.png differ diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 58d76123d0..c1f4855648 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -2353,6 +2353,9 @@ export type RenderChangeListener = (isRendered: boolean) => void; // @public (undocumented) export type Rendering = 'canvas' | 'svg'; +// @alpha +export type ResizeListener = () => void; + // @public (undocumented) export type RGB = number; @@ -2512,7 +2515,7 @@ export const Settings: (props: SFProps; +export const settingsBuildProps: BuildProps; // @public (undocumented) export type SettingsProps = ComponentProps; @@ -2555,6 +2558,10 @@ export interface SettingsSpec extends Spec, LegendSpec { onProjectionClick?: ProjectionClickListener; // (undocumented) onRenderChange?: RenderChangeListener; + // @alpha + onResize?: ResizeListener; + // (undocumented) + onWillRender?: WillRenderListener; orderOrdinalBinsBy?: OrderBy; pointBuffer: MarkBuffer; pointerUpdateDebounce?: number; @@ -3209,6 +3216,9 @@ export const WeightFn: Readonly<{ // @public (undocumented) export type WeightFn = $Values; +// @public +export type WillRenderListener = () => void; + // @alpha export const Wordcloud: FC>; diff --git a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap index fa47f466e3..3084b3130b 100644 --- a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap +++ b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap @@ -12,12 +12,12 @@ exports[`Chart should render the legend name test 1`] = ` - +
- +
diff --git a/packages/charts/src/components/chart_resizer.tsx b/packages/charts/src/components/chart_resizer.tsx index b3c4ac8f0a..57b7a2fe4a 100644 --- a/packages/charts/src/components/chart_resizer.tsx +++ b/packages/charts/src/components/chart_resizer.tsx @@ -11,24 +11,25 @@ import { connect } from 'react-redux'; import { Dispatch, bindActionCreators } from 'redux'; import ResizeObserver from 'resize-observer-polyfill'; +import { DEFAULT_RESIZE_DEBOUNCE } from '../specs/constants'; +import { ResizeListener } from '../specs/settings'; import { updateParentDimensions } from '../state/actions/chart_settings'; import { GlobalChartState } from '../state/chart_state'; import { getSettingsSpecSelector } from '../state/selectors/get_settings_spec'; import { isFiniteNumber } from '../utils/common'; import { debounce, DebouncedFunction } from '../utils/debounce'; -import { Dimensions } from '../utils/dimensions'; interface ResizerStateProps { resizeDebounce: number; + onResize?: ResizeListener; } interface ResizerDispatchProps { - updateParentDimensions(dimension: Dimensions): void; + updateParentDimensions: typeof updateParentDimensions; } type ResizerProps = ResizerStateProps & ResizerDispatchProps; - -const DEFAULT_RESIZE_DEBOUNCE = 200; +type ResizeFn = (entries: ResizeObserverEntry[]) => void; class Resizer extends React.Component { private initialResizeComplete = false; @@ -39,10 +40,7 @@ class Resizer extends React.Component { private animationFrameID: number; - private onResizeDebounced?: DebouncedFunction< - [entries: ResizeObserverEntry[]], - (entries: ResizeObserverEntry[]) => void - >; + private onResizeDebounced?: ResizeFn | DebouncedFunction, ResizeFn>; constructor(props: ResizerProps) { super(props); @@ -52,18 +50,27 @@ class Resizer extends React.Component { } componentDidMount() { - this.onResizeDebounced = debounce(this.onResize, this.props.resizeDebounce); + this.setupResizeDebounce(); if (this.containerRef.current) { this.ro.observe(this.containerRef.current as Element); } } + componentDidUpdate({ resizeDebounce }: Readonly): void { + if (resizeDebounce !== this.props.resizeDebounce) this.setupResizeDebounce(); + } + componentWillUnmount() { window.cancelAnimationFrame(this.animationFrameID); this.ro.disconnect(); } - onResize = (entries: ResizeObserverEntry[]) => { + setupResizeDebounce() { + this.onResizeDebounced = + this.props.resizeDebounce > 0 ? debounce(this.onResize, this.props.resizeDebounce) : this.onResize; + } + + onResize: ResizeFn = (entries) => { if (!Array.isArray(entries)) { return; } @@ -73,6 +80,7 @@ class Resizer extends React.Component { const { width, height } = entries[0].contentRect; this.animationFrameID = window.requestAnimationFrame(() => { this.props.updateParentDimensions({ width, height, top: 0, left: 0 }); + this.props.onResize?.(); }); }; @@ -99,9 +107,10 @@ const mapDispatchToProps = (dispatch: Dispatch): ResizerDispatchProps => ); const mapStateToProps = (state: GlobalChartState): ResizerStateProps => { - const { resizeDebounce } = getSettingsSpecSelector(state); + const { resizeDebounce, onResize } = getSettingsSpecSelector(state); return { resizeDebounce: isFiniteNumber(resizeDebounce) ? resizeDebounce : DEFAULT_RESIZE_DEBOUNCE, + onResize, }; }; diff --git a/packages/charts/src/components/chart_status.tsx b/packages/charts/src/components/chart_status.tsx index efac78d2a7..04dc1ec177 100644 --- a/packages/charts/src/components/chart_status.tsx +++ b/packages/charts/src/components/chart_status.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { RenderChangeListener } from '../specs'; +import { RenderChangeListener, WillRenderListener } from '../specs'; import { GlobalChartState } from '../state/chart_state'; import { globalSelectorCache } from '../state/create_selector'; import { getDebugStateSelector } from '../state/selectors/get_debug_state'; @@ -21,6 +21,7 @@ interface ChartStatusStateProps { rendered: boolean; renderedCount: number; onRenderChange?: RenderChangeListener; + onWillRender?: WillRenderListener; debugState: DebugState | null; } @@ -38,7 +39,9 @@ class ChartStatusComponent extends React.Component { } dispatchRenderChange = () => { - const { onRenderChange, rendered } = this.props; + const { onWillRender, onRenderChange, rendered } = this.props; + onWillRender?.(); + if (onRenderChange) { window.requestAnimationFrame(() => { onRenderChange(rendered); @@ -61,12 +64,13 @@ class ChartStatusComponent extends React.Component { } const mapStateToProps = (state: GlobalChartState): ChartStatusStateProps => { - const { onRenderChange, debugState } = getSettingsSpecSelector(state); + const { onWillRender, onRenderChange, debugState } = getSettingsSpecSelector(state); return { chartId: state.chartId, rendered: state.chartRendered, renderedCount: state.chartRenderedCount, + onWillRender, onRenderChange, debugState: debugState ? getDebugStateSelector(state) : null, }; diff --git a/packages/charts/src/specs/constants.ts b/packages/charts/src/specs/constants.ts index 2215c5cb6e..72608e35d0 100644 --- a/packages/charts/src/specs/constants.ts +++ b/packages/charts/src/specs/constants.ts @@ -127,6 +127,9 @@ export const TooltipStickTo = Object.freeze({ /** @public */ export type TooltipStickTo = $Values; +/** @internal */ +export const DEFAULT_RESIZE_DEBOUNCE = 10; + /** * Default legend config * @internal @@ -151,7 +154,7 @@ export const settingsBuildProps = buildSFProps()( rendering: 'canvas' as const, rotation: 0 as const, animateData: true, - resizeDebounce: 10, + resizeDebounce: DEFAULT_RESIZE_DEBOUNCE, debug: false, pointerUpdateTrigger: PointerUpdateTrigger.X, externalPointerEvents: { diff --git a/packages/charts/src/specs/settings.tsx b/packages/charts/src/specs/settings.tsx index ca7f173af7..ca1dc797eb 100644 --- a/packages/charts/src/specs/settings.tsx +++ b/packages/charts/src/specs/settings.tsx @@ -205,6 +205,11 @@ export type LegendItemListener = (series: SeriesIdentifier[]) => void; * @public */ export type PointerUpdateListener = (event: PointerEvent) => void; +/** + * Listener to be called when chart resizes + * @alpha + */ +export type ResizeListener = () => void; /** * Listener to be called when chart render state changes * @@ -212,6 +217,11 @@ export type PointerUpdateListener = (event: PointerEvent) => void; * @public */ export type RenderChangeListener = (isRendered: boolean) => void; +/** + * Listener to be called *before* chart renders + * @public + */ +export type WillRenderListener = () => void; /** @public */ export type BasicListener = () => undefined | void; /** @public */ @@ -510,7 +520,12 @@ export interface SettingsSpec extends Spec, LegendSpec { onElementOut?: BasicListener; onBrushEnd?: BrushEndListener; onPointerUpdate?: PointerUpdateListener; + /** + * @alpha subject to be removed in the future + */ + onResize?: ResizeListener; onRenderChange?: RenderChangeListener; + onWillRender?: WillRenderListener; onProjectionAreaChange?: ProjectionAreaChangeListener; /** diff --git a/packages/charts/src/state/chart_state.ts b/packages/charts/src/state/chart_state.ts index b45bf7c06c..e6c3e8536b 100644 --- a/packages/charts/src/state/chart_state.ts +++ b/packages/charts/src/state/chart_state.ts @@ -380,11 +380,11 @@ export const chartStoreReducer = (chartId: string, title?: string, description?: }, }; case CHART_RENDERED: - const count = state.chartRendered ? state.chartRenderedCount : state.chartRenderedCount + 1; + const chartRenderedCount = state.chartRendered ? state.chartRenderedCount : state.chartRenderedCount + 1; return { ...state, chartRendered: true, - chartRenderedCount: count, + chartRenderedCount, }; case UPDATE_PARENT_DIMENSION: return { @@ -401,6 +401,7 @@ export const chartStoreReducer = (chartId: string, title?: string, description?: parentDimensions: { ...action.dimensions, }, + chartRendered: false, }; case UPDATE_CHART_TITLES: return { diff --git a/storybook/stories/test_cases/20_highlighter_z_index.story.tsx b/storybook/stories/test_cases/10_highlighter_z_index.story.tsx similarity index 100% rename from storybook/stories/test_cases/20_highlighter_z_index.story.tsx rename to storybook/stories/test_cases/10_highlighter_z_index.story.tsx diff --git a/storybook/stories/test_cases/11_resize_debounce.story.tsx b/storybook/stories/test_cases/11_resize_debounce.story.tsx new file mode 100644 index 0000000000..12e9dcbee0 --- /dev/null +++ b/storybook/stories/test_cases/11_resize_debounce.story.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { action } from '@storybook/addon-actions'; +import { number } from '@storybook/addon-knobs'; +import moment from 'moment'; +import React from 'react'; + +import { Axis, Chart, BarSeries, Position, ScaleType, Settings } from '@elastic/charts'; +import { getRandomNumberGenerator } from '@elastic/charts/src/mocks/utils'; + +import { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; + +const rng = getRandomNumberGenerator(); + +const data: { t: number; values: { v: number; cat: string }[] }[] = []; +const end = moment(1699037055867); +const days = 3; +const maxCardinality = 100; +const start = end.clone().subtract(days, 'days'); +const hours = 6; +while (start.isBefore(end)) { + const values = Array.from({ length: maxCardinality }, (_, i) => ({ + v: rng(0, 100), + cat: `Category ${i + 1}`, + })); + data.push({ t: start.add(hours, 'hours').valueOf(), values }); +} + +export const Example: ChartsStory = (_, { title, description }) => { + const resizeDebounce = number('resizeDebounce (ms)', 10, { min: 0, step: 20 }); + const cardinality = number('cardinality', 100, { min: 1, max: maxCardinality }); + return ( +
+ + + + + + values.slice(0, cardinality).map(({ v, cat }) => ({ t, v, cat })))} + /> + +
+ ); +}; + +Example.parameters = { + markdown: `The \`resizeDebounce\` option on the \`Settings\` spec provides control over the eagerness of the chart to re-render upon resize. A value of \`0\` will remove the debounce altogether. +You can play with the cardinality and debounce time to see how the debouncing affects the chart render timing`, +}; diff --git a/storybook/stories/test_cases/test_cases.stories.tsx b/storybook/stories/test_cases/test_cases.stories.tsx index ab48aba620..0dff7160e5 100644 --- a/storybook/stories/test_cases/test_cases.stories.tsx +++ b/storybook/stories/test_cases/test_cases.stories.tsx @@ -19,4 +19,5 @@ export { Example as accessibilityCustomizations } from './6_a11y_custom_descript export { Example as rtlText } from './7_rtl_text.story'; export { Example as testPointsOutsideOfDomain } from './8_test_points_outside_of_domain.story'; export { Example as duplicateLabelsInPartitionLegend } from './9_duplicate_labels_in_partition_legend.story'; -export { Example as highlighterZIndex } from './20_highlighter_z_index.story'; +export { Example as highlighterZIndex } from './10_highlighter_z_index.story'; +export { Example as resizeDebounce } from './11_resize_debounce.story';