diff --git a/src/core/chart-api/index.tsx b/src/core/chart-api/index.tsx index b2cfca19..ac03f1b0 100644 --- a/src/core/chart-api/index.tsx +++ b/src/core/chart-api/index.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef } from "react"; import type Highcharts from "highcharts"; import { fireNonCancelableEvent } from "../../internal/events"; -import { ReadonlyAsyncStore } from "../../internal/utils/async-store"; +import AsyncStore, { ReadonlyAsyncStore } from "../../internal/utils/async-store"; import { getChartSeries, getSeriesData } from "../../internal/utils/highcharts"; import { Writeable } from "../../internal/utils/utils"; import { CoreChartProps } from "../interfaces"; @@ -64,6 +64,8 @@ export function useChartAPI( // Run cleanup code when the component unmounts. useEffect(() => () => api.onChartDestroy(), [api]); + api.startRendering(); + return api; } @@ -81,6 +83,7 @@ export class ChartAPI { private chartExtraLegend = new ChartExtraLegend(this.context); private chartExtraNodata = new ChartExtraNodata(this.context); private chartExtraAxisTitles = new ChartExtraAxisTitles(this.context); + private renderingReadyStore = new AsyncStore(false); constructor( settings: ChartExtraContext.Settings, @@ -98,6 +101,14 @@ export class ChartAPI { return !!this.context.chartOrNull; } + public get renderingReady() { + return this.renderingReadyStore as ReadonlyAsyncStore; + } + + public startRendering() { + this.renderingReadyStore.set(() => false); + } + // The Highcharts options to be merged with the rest of the configuration defined in the chart-core. public getOptions() { // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -115,6 +126,7 @@ export class ChartAPI { chartAPI.handleDestroyedPoints(); chartAPI.resetColorCounter(); chartAPI.showMarkersForIsolatedPoints(); + chartAPI.renderingReadyStore.set(() => true); }; const onChartClick: Highcharts.ChartClickCallbackFunction = function () { chartAPI.chartExtraPointer.onChartClick(this.hoverPoint); diff --git a/src/core/chart-container.tsx b/src/core/chart-container.tsx index 08e6a5cc..48bd61c4 100644 --- a/src/core/chart-container.tsx +++ b/src/core/chart-container.tsx @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import clsx from "clsx"; -import { useResizeObserver } from "@cloudscape-design/component-toolkit/internal"; +import { useResizeObserver, useStableCallback } from "@cloudscape-design/component-toolkit/internal"; import * as Styles from "../internal/chart-styles"; +import { ReadonlyAsyncStore } from "../internal/utils/async-store"; import { DebouncedCall } from "../internal/utils/utils"; import styles from "./styles.css.js"; @@ -38,6 +39,7 @@ interface ChartContainerProps { chartHeight?: number; chartMinHeight?: number; chartMinWidth?: number; + renderingReady: ReadonlyAsyncStore; } export function ChartContainer({ @@ -57,8 +59,9 @@ export function ChartContainer({ chartHeight, chartMinHeight, chartMinWidth, + renderingReady, }: ChartContainerProps) { - const { refs, measures } = useContainerQueries(); + const { refs, measures } = useContainerQueries(renderingReady); // The vertical axis title is rendered above the chart, and is technically not a part of the chart plot. // However, we want to include it to the chart's height computations as it does belong to the chart logically. @@ -125,15 +128,29 @@ export function ChartContainer({ ); } +interface MeasuresState { + chart: number; + header: number; + footer: number; +} + // This hook combines 3 resize observer and does a small optimization to batch their updates in a single set-state. -function useContainerQueries() { - const [measuresState, setMeasuresState] = useState({ ready: false, chart: 0, header: 0, footer: 0 }); - const measuresRef = useRef({ ready: false, chart: 0, header: 0, footer: 0 }); +function useContainerQueries(renderingReady: ReadonlyAsyncStore) { + const [measuresState, setMeasuresState] = useState({ chart: 0, header: 0, footer: 0 }); + const measuresRef = useRef({ chart: 0, header: 0, footer: 0 }); const measureDebounce = useRef(new DebouncedCall()).current; + const queueMeasure = useStableCallback(() => + measureDebounce.call(() => { + if (renderingReady.get()) { + setMeasuresState({ ...measuresRef.current }); + } + }, 0), + ); const setMeasure = (type: "chart" | "header" | "footer", value: number) => { measuresRef.current[type] = value; - measureDebounce.call(() => setMeasuresState({ ...measuresRef.current }), 0); + queueMeasure(); }; + useEffect(() => renderingReady.subscribe((state) => state, queueMeasure), [renderingReady, queueMeasure]); const chartMeasureRef = useRef(null); const getChart = useCallback(() => chartMeasureRef.current, []); diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index adf28c27..4a345361 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -106,6 +106,7 @@ export function InternalCoreChart({ legendPosition, verticalAxisTitlePlacement, legendBottomMaxHeight: legendOptions?.bottomMaxHeight, + renderingReady: api.renderingReady, }; // AWSUI-61678