From d92ac4b4b74eb3426313de997192e2441c8fd0e9 Mon Sep 17 00:00:00 2001 From: Louis Sautier Date: Mon, 30 Mar 2026 17:08:16 +0200 Subject: [PATCH] Add solo-select gesture to chart legend (#30395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add solo-select gesture to chart legend Ctrl+click (Cmd+click on Mac) or long-press (touch, 500ms) a legend item to solo-select it: - Solo-click any item → hide everything else, show only that item - Solo-click the only visible item → restore all There is no special "solo mode" — the gesture simply sets which items are hidden. Normal click/tap continues to toggle individual series, including after a solo action (e.g. solo a, then click b to add it). Closes https://github.com/orgs/home-assistant/discussions/1492 Co-Authored-By: Claude Opus 4.6 * Deduplicate legend parsing in _renderLegend and _getAllLegendIds Both methods parsed options.legend and filtered datasets identically. Extract the shared logic into a new _getLegendItems method. Co-Authored-By: Claude Opus 4.6 * Update src/components/chart/ha-chart-base.ts --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Petar Petrov --- src/components/chart/ha-chart-base.ts | 128 ++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 7 deletions(-) diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index eb484fa2b2..4d664b5eb2 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -91,6 +91,10 @@ export class HaChartBase extends LitElement { private _lastTapTime?: number; + private _longPressTimer?: ReturnType; + + private _longPressTriggered = false; + private _shouldResizeChart = false; private _resizeAnimationDuration?: number; @@ -128,6 +132,7 @@ export class HaChartBase extends LitElement { public disconnectedCallback() { super.disconnectedCallback(); + this._legendPointerCancel(); this._pendingSetup = false; while (this._listeners.length) { this._listeners.pop()!(); @@ -302,22 +307,31 @@ export class HaChartBase extends LitElement { `; } - private _renderLegend() { + private _getLegendItems() { if (!this.options?.legend || !this.data) { - return nothing; + return undefined; } const legend = ensureArray(this.options.legend).find( (l) => l.show && l.type === "custom" ) as CustomLegendOption | undefined; if (!legend) { - return nothing; + return undefined; } const datasets = ensureArray(this.data); - const items = + return ( legend.data || datasets .filter((d) => (d.data as any[])?.length && (d.id || d.name)) - .map((d) => ({ id: d.id, name: d.name })); + .map((d) => ({ id: d.id, name: d.name })) + ); + } + + private _renderLegend() { + const items = this._getLegendItems(); + if (!items) { + return nothing; + } + const datasets = ensureArray(this.data!); const isMobile = window.matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" @@ -362,6 +376,11 @@ export class HaChartBase extends LitElement { return html`
  • @@ -1022,11 +1041,52 @@ export class HaChartBase extends LitElement { fireEvent(this, "chart-zoom", { start, end }); } - private _legendClick(ev: any) { + // Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive) + private _legendPointerDown(ev: PointerEvent) { + // Mouse uses Ctrl/Cmd+click instead + if (ev.pointerType === "mouse") { + return; + } + const id = (ev.currentTarget as HTMLElement)?.id; + if (!id) { + return; + } + this._longPressTriggered = false; + this._longPressTimer = setTimeout(() => { + this._longPressTriggered = true; + this._longPressTimer = undefined; + this._soloLegend(id); + }, 500); + } + + private _legendPointerCancel() { + if (this._longPressTimer) { + clearTimeout(this._longPressTimer); + this._longPressTimer = undefined; + } + } + + private _legendContextMenu(ev: Event) { + if (this._longPressTimer || this._longPressTriggered) { + ev.preventDefault(); + } + } + + private _legendClick(ev: MouseEvent) { if (!this.chart) { return; } - const id = ev.currentTarget?.id; + if (this._longPressTriggered) { + this._longPressTriggered = false; + return; + } + const id = (ev.currentTarget as HTMLElement)?.id; + // Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere + const soloModifier = isMac ? ev.metaKey : ev.ctrlKey; + if (soloModifier) { + this._soloLegend(id); + return; + } if (this._hiddenDatasets.has(id)) { this._getAllIdsFromLegend(this.options, id).forEach((i) => this._hiddenDatasets.delete(i) @@ -1041,6 +1101,60 @@ export class HaChartBase extends LitElement { this.requestUpdate("_hiddenDatasets"); } + private _soloLegend(id: string) { + const allIds = this._getAllLegendIds(); + const clickedIds = this._getAllIdsFromLegend(this.options, id); + const otherIds = allIds.filter((i) => !clickedIds.includes(i)); + + const clickedIsOnlyVisible = + clickedIds.every((i) => !this._hiddenDatasets.has(i)) && + otherIds.every((i) => this._hiddenDatasets.has(i)); + + if (clickedIsOnlyVisible) { + // Already solo'd on this item — restore all series to visible + for (const hiddenId of [...this._hiddenDatasets]) { + this._hiddenDatasets.delete(hiddenId); + fireEvent(this, "dataset-unhidden", { id: hiddenId }); + } + } else { + // Solo: hide every other series, unhide clicked if it was hidden + for (const otherId of otherIds) { + if (!this._hiddenDatasets.has(otherId)) { + this._hiddenDatasets.add(otherId); + fireEvent(this, "dataset-hidden", { id: otherId }); + } + } + for (const clickedId of clickedIds) { + if (this._hiddenDatasets.has(clickedId)) { + this._hiddenDatasets.delete(clickedId); + fireEvent(this, "dataset-unhidden", { id: clickedId }); + } + } + } + this.requestUpdate("_hiddenDatasets"); + } + + private _getAllLegendIds(): string[] { + const items = this._getLegendItems(); + if (!items) { + return []; + } + const allIds = new Set(); + for (const item of items) { + const primaryId = + typeof item === "string" + ? item + : ((item.id as string) ?? (item.name as string) ?? ""); + for (const expandedId of this._getAllIdsFromLegend( + this.options, + primaryId + )) { + allIds.add(expandedId); + } + } + return [...allIds]; + } + private _toggleExpandedLegend() { this.expandLegend = !this.expandLegend; setTimeout(() => {