Make entities on the energy now sankey graph clickable (#28998)

* enhancement: make entities on the energy now sankey graph clickable to show details

Signed-off-by: Jason Madigan <jason@jasonmadigan.com>

* add a test

Signed-off-by: Jason Madigan <jason@jasonmadigan.com>

* format

---------

Signed-off-by: Jason Madigan <jason@jasonmadigan.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Jason Madigan 2026-01-15 06:44:03 +00:00 committed by GitHub
parent 5be7bad176
commit f8d65cc0ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 1 deletions

View File

@ -2,9 +2,13 @@ import { customElement, property, state } from "lit/decorators";
import { LitElement, html, css } from "lit";
import type { EChartsType } from "echarts/core";
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
import type { CallbackDataParams } from "echarts/types/src/util/types";
import type {
CallbackDataParams,
ECElementEvent,
} from "echarts/types/src/util/types";
import memoizeOne from "memoize-one";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { fireEvent } from "../../common/dom/fire_event";
import SankeyChart from "../../resources/echarts/components/sankey/install";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts/echarts";
@ -21,6 +25,7 @@ export interface Node {
label?: string;
color?: string;
passThrough?: boolean;
entityId?: string;
}
export interface Link {
source: string;
@ -83,6 +88,7 @@ export class HaSankeyChart extends LitElement {
.options=${options}
height="100%"
.extraComponents=${[SankeyChart]}
@chart-click=${this._handleChartClick}
></ha-chart-base>`;
}
@ -103,6 +109,22 @@ export class HaSankeyChart extends LitElement {
return null;
};
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
const detail = ev.detail;
// Only handle node clicks (not links)
if (detail.dataType !== "node") {
return;
}
const nodeId = (detail.data as Record<string, any>)?.id;
if (!nodeId) {
return;
}
const node = this.data.nodes.find((n) => n.id === nodeId);
if (node?.entityId) {
fireEvent(this, "node-click", { node });
}
};
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
@ -294,4 +316,7 @@ declare global {
interface HTMLElementTagNameMap {
"ha-sankey-chart": HaSankeyChart;
}
interface HASSDomEvents {
"node-click": { node: Node };
}
}

View File

@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
import {
formatPowerShort,
@ -326,6 +327,7 @@ class HuiPowerSankeyCard
color: getGraphColorByIndex(idx, computedStyle),
index: 4,
parent: effectiveParent,
entityId: device.stat_rate,
};
if (node.parent) {
parentLinks[node.id] = node.parent;
@ -461,6 +463,7 @@ class HuiPowerSankeyCard
.data=${{ nodes, links }}
.vertical=${vertical}
.valueFormatter=${this._valueFormatter}
@node-click=${this._handleNodeClick}
></ha-sankey-chart>`
: html`${this.hass.localize(
"ui.panel.lovelace.cards.energy.no_data"
@ -475,6 +478,13 @@ class HuiPowerSankeyCard
${formatPowerShort(this.hass, value)}
</div>`;
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
const { node } = ev.detail;
if (node.entityId) {
fireEvent(this, "hass-more-info", { entityId: node.entityId });
}
}
/**
* Compute real-time power data from current entity states.
* Similar to computeConsumptionData but for instantaneous power.

View File

@ -0,0 +1,37 @@
import { describe, it, expect } from "vitest";
import type { Node } from "../../../../../../src/components/chart/ha-sankey-chart";
describe("hui-power-sankey-card", () => {
describe("node click handling", () => {
it("should identify device nodes as clickable via entityId", () => {
const nodes: Node[] = [
{ id: "solar", value: 1000, index: 0, label: "Solar" },
{ id: "home", value: 1500, index: 1, label: "Home" },
{
id: "sensor.device1",
value: 200,
index: 4,
label: "Device 1",
entityId: "sensor.device1",
},
];
const clickableNodes = nodes.filter((n) => n.entityId);
expect(clickableNodes).toHaveLength(1);
expect(clickableNodes[0].entityId).toBe("sensor.device1");
});
it("should not make source/area/floor nodes clickable", () => {
const nodes: Node[] = [
{ id: "solar", value: 1000, index: 0 },
{ id: "grid", value: 500, index: 0 },
{ id: "home", value: 1500, index: 1 },
{ id: "floor_1", value: 800, index: 2 },
{ id: "area_kitchen", value: 400, index: 3 },
];
const clickableNodes = nodes.filter((n) => n.entityId);
expect(clickableNodes).toHaveLength(0);
});
});
});