From c52217c1ce9c9a138e28af3bbcc26e5b5bd02196 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 18 Feb 2025 08:57:07 +0200 Subject: [PATCH] Make part of the chart rendering async for large datasets (#24260) --- src/components/chart/ha-chart-base.ts | 41 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 78902bc018..deb89d24d2 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -6,7 +6,6 @@ import type { DataZoomComponentOption } from "echarts/components"; import type { EChartsType } from "echarts/core"; import type { ECElementEvent, - SetOptionOpts, XAXisOption, YAXisOption, } from "echarts/types/dist/shared"; @@ -25,6 +24,7 @@ import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; import "../ha-icon-button"; import { formatTimeLabel } from "./axis-label"; +import { ensureArray } from "../../common/array/ensure-array"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; @@ -68,12 +68,16 @@ export class HaChartBase extends LitElement { private _listeners: (() => void)[] = []; + private _originalZrFlush?: () => void; + public disconnectedCallback() { super.disconnectedCallback(); while (this._listeners.length) { this._listeners.pop()!(); } this.chart?.dispose(); + this.chart = undefined; + this._originalZrFlush = undefined; } public connectedCallback() { @@ -86,7 +90,7 @@ export class HaChartBase extends LitElement { listenMediaQuery("(prefers-reduced-motion)", (matches) => { if (this._reducedMotion !== matches) { this._reducedMotion = matches; - this.chart?.setOption({ animation: !this._reducedMotion }); + this._setChartOptions({ animation: !this._reducedMotion }); } }) ); @@ -96,7 +100,7 @@ export class HaChartBase extends LitElement { if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) { this._modifierPressed = true; if (!this.options?.dataZoom) { - this.chart?.setOption({ dataZoom: this._getDataZoomConfig() }); + this._setChartOptions({ dataZoom: this._getDataZoomConfig() }); } } }; @@ -105,7 +109,7 @@ export class HaChartBase extends LitElement { if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) { this._modifierPressed = false; if (!this.options?.dataZoom) { - this.chart?.setOption({ dataZoom: this._getDataZoomConfig() }); + this._setChartOptions({ dataZoom: this._getDataZoomConfig() }); } } }; @@ -131,10 +135,8 @@ export class HaChartBase extends LitElement { return; } let chartOptions: ECOption = {}; - const chartUpdateParams: SetOptionOpts = { lazyUpdate: true }; if (changedProps.has("data")) { chartOptions.series = this.data; - chartUpdateParams.replaceMerge = ["series"]; } if (changedProps.has("options")) { chartOptions = { ...chartOptions, ...this._createOptions() }; @@ -142,7 +144,7 @@ export class HaChartBase extends LitElement { chartOptions.dataZoom = this._getDataZoomConfig(); } if (Object.keys(chartOptions).length > 0) { - this.chart.setOption(chartOptions, chartUpdateParams); + this._setChartOptions(chartOptions); } } @@ -509,6 +511,31 @@ export class HaChartBase extends LitElement { return Math.max(this.clientWidth / 2, 200); } + private _setChartOptions(options: ECOption) { + if (!this.chart) { + return; + } + if (!this._originalZrFlush) { + const dataSize = ensureArray(this.data).reduce( + (acc, series) => acc + (series.data as any[]).length, + 0 + ); + if (dataSize > 10000) { + // for large datasets zr.flush takes 30-40% of the render time + // so we delay it a bit to avoid blocking the main thread + const zr = this.chart.getZr(); + this._originalZrFlush = zr.flush.bind(zr); + zr.flush = () => { + setTimeout(() => { + this._originalZrFlush?.(); + }, 10); + }; + } + } + const replaceMerge = options.series ? ["series"] : []; + this.chart.setOption(options, { replaceMerge }); + } + private _handleZoomReset() { this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); }