diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 9434e1a84c..80036a1bf8 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -63,6 +63,9 @@ export class HaChartBase extends LitElement { @property({ attribute: "small-controls", type: Boolean }) public smallControls?: boolean; + @property({ attribute: "hide-reset-button", type: Boolean }) + public hideResetButton?: boolean; + // extraComponents is not reactive and should not trigger updates public extraComponents?: any[]; @@ -215,7 +218,7 @@ export class HaChartBase extends LitElement { ${this._renderLegend()}
- ${this._isZoomed + ${this._isZoomed && !this.hideResetButton ? html` { - const { start, end } = e.batch?.[0] ?? e; - this._isZoomed = start !== 0 || end !== 100; - this._zoomRatio = (end - start) / 100; - if (this._isTouchDevice) { - // zooming changes the axis pointer so we need to hide it - this.chart?.dispatchAction({ - type: "hideTip", - from: "datazoom", - }); - } + this._handleDataZoomEvent(e); }); this.chart.on("click", (e: ECElementEvent) => { fireEvent(this, "chart-click", e); }); + if (!this.options?.dataZoom) { this.chart.getZr().on("dblclick", this._handleClickZoom); } @@ -868,10 +863,60 @@ export class HaChartBase extends LitElement { }); }; + public zoom(start: number, end: number, silent = false) { + this.chart?.dispatchAction({ + type: "dataZoom", + start, + end, + silent, + }); + } + private _handleZoomReset() { this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); } + private _handleDataZoomEvent(e: any) { + const zoomData = e.batch?.[0] ?? e; + let start = typeof zoomData.start === "number" ? zoomData.start : 0; + let end = typeof zoomData.end === "number" ? zoomData.end : 100; + + if ( + start === 0 && + end === 100 && + zoomData.startValue !== undefined && + zoomData.endValue !== undefined + ) { + const option = this.chart!.getOption(); + const xAxis = option.xAxis?.[0] ?? option.xAxis; + + if (xAxis?.min && xAxis?.max) { + const axisMin = new Date(xAxis.min).getTime(); + const axisMax = new Date(xAxis.max).getTime(); + const axisRange = axisMax - axisMin; + + start = Math.max( + 0, + Math.min(100, ((zoomData.startValue - axisMin) / axisRange) * 100) + ); + end = Math.max( + 0, + Math.min(100, ((zoomData.endValue - axisMin) / axisRange) * 100) + ); + } + } + + this._isZoomed = start !== 0 || end !== 100; + this._zoomRatio = (end - start) / 100; + if (this._isTouchDevice) { + this.chart?.dispatchAction({ + type: "hideTip", + from: "datazoom", + }); + } + fireEvent(this, "chart-zoom", { start, end }); + } + private _legendClick(ev: any) { if (!this.chart) { return; @@ -1024,5 +1069,9 @@ declare global { "dataset-hidden": { id: string }; "dataset-unhidden": { id: string }; "chart-click": ECElementEvent; + "chart-zoom": { + start: number; + end: number; + }; } } diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index e95d67ef07..1c7ad4a147 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -66,6 +66,9 @@ export class StateHistoryChartLine extends LitElement { @property({ attribute: "expand-legend", type: Boolean }) public expandLegend?: boolean; + @property({ attribute: "hide-reset-button", type: Boolean }) + public hideResetButton?: boolean; + @state() private _chartData: LineSeriesOption[] = []; @state() private _entityIds: string[] = []; @@ -94,7 +97,9 @@ export class StateHistoryChartLine extends LitElement { style=${styleMap({ height: this.height })} @dataset-hidden=${this._datasetHidden} @dataset-unhidden=${this._datasetUnhidden} + @chart-zoom=${this._handleDataZoom} .expandLegend=${this.expandLegend} + .hideResetButton=${this.hideResetButton} > `; } @@ -192,6 +197,19 @@ export class StateHistoryChartLine extends LitElement { this._hiddenStats.delete(ev.detail.id); } + public zoom(start: number, end: number) { + const chartBase = this.shadowRoot!.querySelector("ha-chart-base")!; + chartBase.zoom(start, end, true); + } + + private _handleDataZoom(ev: CustomEvent) { + fireEvent(this, "chart-zoom-with-index", { + start: ev.detail.start ?? 0, + end: ev.detail.end ?? 100, + chartIndex: this.chartIndex, + }); + } + public willUpdate(changedProps: PropertyValues) { if ( changedProps.has("data") || diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 9a9d01e95a..8969ec1cf6 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -51,6 +51,9 @@ export class StateHistoryChartTimeline extends LitElement { @property({ attribute: false, type: Number }) public chartIndex?; + @property({ attribute: "hide-reset-button", type: Boolean }) + public hideResetButton?: boolean; + @state() private _chartData: CustomSeriesOption[] = []; @state() private _chartOptions?: ECOption; @@ -68,6 +71,8 @@ export class StateHistoryChartTimeline extends LitElement { .data=${this._chartData as ECOption["series"]} small-controls @chart-click=${this._handleChartClick} + @chart-zoom=${this._handleDataZoom} + .hideResetButton=${this.hideResetButton} > `; } @@ -256,6 +261,19 @@ export class StateHistoryChartTimeline extends LitElement { }; } + public zoom(start: number, end: number) { + const chartBase = this.shadowRoot!.querySelector("ha-chart-base")!; + chartBase.zoom(start, end, true); + } + + private _handleDataZoom(ev: CustomEvent) { + fireEvent(this, "chart-zoom-with-index", { + start: ev.detail.start ?? 0, + end: ev.detail.end ?? 100, + chartIndex: this.chartIndex, + }); + } + private _generateData() { const computedStyles = getComputedStyle(this); let stateHistory = this.data; diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index 7e89aad514..f2938301b6 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -1,7 +1,8 @@ import type { PropertyValues } from "lit"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, eventOptions, property, state } from "lit/decorators"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; +import { mdiRestart } from "@mdi/js"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { restoreScroll } from "../../common/decorators/restore-scroll"; import type { @@ -11,6 +12,10 @@ import type { } from "../../data/history"; import { loadVirtualizer } from "../../resources/virtualizer"; import type { HomeAssistant } from "../../types"; +import type { StateHistoryChartLine } from "./state-history-chart-line"; +import type { StateHistoryChartTimeline } from "./state-history-chart-timeline"; +import "../ha-fab"; +import "../ha-svg-icon"; import "./state-history-chart-line"; import "./state-history-chart-timeline"; @@ -29,6 +34,11 @@ const chunkData = (inputArray: any[], chunks: number) => declare global { interface HASSDomEvents { "y-width-changed": { value: number; chartIndex: number }; + "chart-zoom-with-index": { + start: number; + end: number; + chartIndex: number; + }; } } @@ -84,6 +94,10 @@ export class StateHistoryCharts extends LitElement { @state() private _chartCount = 0; + @state() private _hasZoomedCharts = false; + + private _isSyncing = false; + // @ts-ignore @restoreScroll(".container") private _savedScrollPos?: number; @@ -115,19 +129,36 @@ export class StateHistoryCharts extends LitElement { // eslint-disable-next-line lit/no-this-assign-in-render this._chartCount = combinedItems.length; - return this.virtualize - ? html`
- - -
` - : html`${combinedItems.map((item, index) => - this._renderHistoryItem(item, index) - )}`; + + +
` + : html`${combinedItems.map((item, index) => + this._renderHistoryItem(item, index) + )}`} + ${this._hasZoomedCharts + ? html` + + ` + : nothing} + `; } private _renderHistoryItem: RenderItemFunction< @@ -156,8 +187,10 @@ export class StateHistoryCharts extends LitElement { .maxYAxis=${this.maxYAxis} .fitYData=${this.fitYData} @y-width-changed=${this._yWidthChanged} + @chart-zoom-with-index=${this._handleTimelineSync} .height=${this.virtualize ? undefined : this.height} .expandLegend=${this.expandLegend} + hide-reset-button > `; } @@ -175,6 +208,8 @@ export class StateHistoryCharts extends LitElement { .chartIndex=${index} .clickForMoreInfo=${this.clickForMoreInfo} @y-width-changed=${this._yWidthChanged} + @chart-zoom-with-index=${this._handleTimelineSync} + hide-reset-button > `; }; @@ -264,6 +299,66 @@ export class StateHistoryCharts extends LitElement { this._maxYWidth = Math.max(...Object.values(this._childYWidths), 0); } + private _handleTimelineSync( + e: CustomEvent + ) { + if (this._isSyncing) { + return; + } + + const { start, end, chartIndex } = e.detail; + + this._hasZoomedCharts = start !== 0 || end !== 100; + this._syncZoomToAllCharts(start, end, chartIndex); + } + + private _syncZoomToAllCharts( + start: number, + end: number, + sourceChartIndex?: number + ) { + this._isSyncing = true; + + requestAnimationFrame(() => { + const chartComponents = this.renderRoot.querySelectorAll( + "state-history-chart-line, state-history-chart-timeline" + ) as unknown as (StateHistoryChartLine | StateHistoryChartTimeline)[]; + + chartComponents.forEach((chartComponent, index) => { + if (index === sourceChartIndex) { + return; + } + + if ("zoom" in chartComponent) { + chartComponent.zoom(start, end); + } + }); + + this._isSyncing = false; + }); + } + + private _handleGlobalZoomReset() { + this._hasZoomedCharts = false; + this._isSyncing = true; + + requestAnimationFrame(() => { + const chartComponents = this.renderRoot.querySelectorAll( + "state-history-chart-line, state-history-chart-timeline" + ); + + chartComponents.forEach((chartComponent: any) => { + const chartBase = + chartComponent.renderRoot?.querySelector("ha-chart-base"); + + if (chartBase && chartBase.chart) { + chartBase.zoom(0, 100); + } + }); + this._isSyncing = false; + }); + } + private _isHistoryEmpty(): boolean { const historyDataEmpty = !this.historyData || @@ -345,6 +440,11 @@ export class StateHistoryCharts extends LitElement { state-history-chart-line { width: 100%; } + .reset-button { + position: fixed; + bottom: calc(24px + var(--safe-area-inset-bottom)); + right: calc(24px + var(--safe-area-inset-bottom)); + } `; }