Fix for charts with identically named entities (#26166)

* Fix for charts with identically named entities

* lint

* type fix
This commit is contained in:
Petar Petrov 2025-07-15 20:01:58 +03:00 committed by GitHub
parent aee9e4b0a5
commit 72a12a4ba4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 39 deletions

View File

@ -35,6 +35,15 @@ const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6; const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300; const DOUBLE_TAP_TIME = 300;
export type CustomLegendOption = ECOption["legend"] & {
type: "custom";
data?: {
id?: string;
name: string;
itemStyle?: Record<string, any>;
}[];
};
@customElement("ha-chart-base") @customElement("ha-chart-base")
export class HaChartBase extends LitElement { export class HaChartBase extends LitElement {
public chart?: EChartsType; public chart?: EChartsType;
@ -219,16 +228,18 @@ export class HaChartBase extends LitElement {
if (!this.options?.legend || !this.data) { if (!this.options?.legend || !this.data) {
return nothing; return nothing;
} }
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption; const legend = ensureArray(this.options.legend).find(
if (!legend.show || legend.type !== "custom") { (l) => l.show && l.type === "custom"
) as CustomLegendOption | undefined;
if (!legend) {
return nothing; return nothing;
} }
const datasets = ensureArray(this.data); const datasets = ensureArray(this.data);
const items: LegendComponentOption["data"] = const items =
legend.data || legend.data ||
((datasets datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name)) .filter((d) => (d.data as any[])?.length && (d.id || d.name))
.map((d) => d.name ?? d.id) || []) as string[]); .map((d) => ({ id: d.id, name: d.name }));
const isMobile = window.matchMedia( const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)" "all and (max-width: 450px), all and (max-height: 500px)"
@ -249,25 +260,29 @@ export class HaChartBase extends LitElement {
} }
let itemStyle: Record<string, any> = {}; let itemStyle: Record<string, any> = {};
let name = ""; let name = "";
let id = "";
if (typeof item === "string") { if (typeof item === "string") {
name = item; name = item;
const dataset = datasets.find( id = item;
(d) => d.id === item || d.name === item
);
itemStyle = {
color: dataset?.color as string,
...(dataset?.itemStyle as { borderColor?: string }),
};
} else { } else {
name = item.name ?? ""; name = item.name ?? "";
id = item.id ?? name;
itemStyle = item.itemStyle ?? {}; itemStyle = item.itemStyle ?? {};
} }
const dataset =
datasets.find((d) => d.id === id) ??
datasets.find((d) => d.name === id);
itemStyle = {
color: dataset?.color as string,
...(dataset?.itemStyle as { borderColor?: string }),
itemStyle,
};
const color = itemStyle?.color as string; const color = itemStyle?.color as string;
const borderColor = itemStyle?.borderColor as string; const borderColor = itemStyle?.borderColor as string;
return html`<li return html`<li
.name=${name} .id=${id}
@click=${this._legendClick} @click=${this._legendClick}
class=${classMap({ hidden: this._hiddenDatasets.has(name) })} class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
.title=${name} .title=${name}
> >
<div <div
@ -645,7 +660,7 @@ export class HaChartBase extends LitElement {
| YAXisOption | YAXisOption
| undefined; | undefined;
const series = ensureArray(this.data).map((s) => { const series = ensureArray(this.data).map((s) => {
const data = this._hiddenDatasets.has(String(s.name ?? s.id)) const data = this._hiddenDatasets.has(String(s.id ?? s.name))
? undefined ? undefined
: s.data; : s.data;
if (data && s.type === "line") { if (data && s.type === "line") {
@ -740,13 +755,13 @@ export class HaChartBase extends LitElement {
if (!this.chart) { if (!this.chart) {
return; return;
} }
const name = ev.currentTarget?.name; const id = ev.currentTarget?.id;
if (this._hiddenDatasets.has(name)) { if (this._hiddenDatasets.has(id)) {
this._hiddenDatasets.delete(name); this._hiddenDatasets.delete(id);
fireEvent(this, "dataset-unhidden", { name }); fireEvent(this, "dataset-unhidden", { id });
} else { } else {
this._hiddenDatasets.add(name); this._hiddenDatasets.add(id);
fireEvent(this, "dataset-hidden", { name }); fireEvent(this, "dataset-hidden", { id });
} }
this.requestUpdate("_hiddenDatasets"); this.requestUpdate("_hiddenDatasets");
} }
@ -881,8 +896,8 @@ declare global {
"ha-chart-base": HaChartBase; "ha-chart-base": HaChartBase;
} }
interface HASSDomEvents { interface HASSDomEvents {
"dataset-hidden": { name: string }; "dataset-hidden": { id: string };
"dataset-unhidden": { name: string }; "dataset-unhidden": { id: string };
"chart-click": ECElementEvent; "chart-click": ECElementEvent;
} }
} }

View File

@ -111,7 +111,7 @@ export class StateHistoryChartLine extends LitElement {
this._chartData.forEach((dataset, index) => { this._chartData.forEach((dataset, index) => {
if ( if (
dataset.tooltip?.show === false || dataset.tooltip?.show === false ||
this._hiddenStats.has(dataset.name as string) this._hiddenStats.has(dataset.id as string)
) )
return; return;
const param = params.find( const param = params.find(
@ -185,11 +185,11 @@ export class StateHistoryChartLine extends LitElement {
}; };
private _datasetHidden(ev: CustomEvent) { private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name); this._hiddenStats.add(ev.detail.id);
} }
private _datasetUnhidden(ev: CustomEvent) { private _datasetUnhidden(ev: CustomEvent) {
this._hiddenStats.delete(ev.detail.name); this._hiddenStats.delete(ev.detail.id);
} }
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {

View File

@ -31,6 +31,7 @@ import {
} from "../../data/recorder"; } from "../../data/recorder";
import type { ECOption } from "../../resources/echarts"; import type { ECOption } from "../../resources/echarts";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base"; import "./ha-chart-base";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = { export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
@ -96,7 +97,7 @@ export class StatisticsChart extends LitElement {
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = []; @state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
@state() private _legendData: string[] = []; @state() private _legendData: NonNullable<CustomLegendOption["data"]> = [];
@state() private _statisticIds: string[] = []; @state() private _statisticIds: string[] = [];
@ -183,12 +184,12 @@ export class StatisticsChart extends LitElement {
} }
private _datasetHidden(ev: CustomEvent) { private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name); this._hiddenStats.add(ev.detail.id);
this.requestUpdate("_hiddenStats"); this.requestUpdate("_hiddenStats");
} }
private _datasetUnhidden(ev: CustomEvent) { private _datasetUnhidden(ev: CustomEvent) {
this._hiddenStats.delete(ev.detail.name); this._hiddenStats.delete(ev.detail.id);
this.requestUpdate("_hiddenStats"); this.requestUpdate("_hiddenStats");
} }
@ -199,8 +200,8 @@ export class StatisticsChart extends LitElement {
: ""; : "";
return params return params
.map((param, index: number) => { .map((param, index: number) => {
if (rendered[param.seriesName]) return ""; if (rendered[param.seriesIndex]) return "";
rendered[param.seriesName] = true; rendered[param.seriesIndex] = true;
const statisticId = this._statisticIds[param.seriesIndex]; const statisticId = this._statisticIds[param.seriesIndex];
const stateObj = this.hass.states[statisticId]; const stateObj = this.hass.states[statisticId];
@ -367,6 +368,7 @@ export class StatisticsChart extends LitElement {
const statisticsData = Object.entries(this.statisticsData); const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: typeof this._chartData = []; const totalDataSets: typeof this._chartData = [];
const legendData: { const legendData: {
id: string;
name: string; name: string;
color?: ZRColor; color?: ZRColor;
borderColor?: ZRColor; borderColor?: ZRColor;
@ -540,6 +542,7 @@ export class StatisticsChart extends LitElement {
: displayedLegend === false; : displayedLegend === false;
if (showLegend) { if (showLegend) {
statLegendData.push({ statLegendData.push({
id: statistic_id,
name, name,
color: series.color as ZRColor, color: series.color as ZRColor,
borderColor: series.itemStyle?.borderColor, borderColor: series.itemStyle?.borderColor,
@ -584,7 +587,7 @@ export class StatisticsChart extends LitElement {
} }
dataValues.push(val); dataValues.push(val);
}); });
if (!this._hiddenStats.has(name)) { if (!this._hiddenStats.has(statistic_id)) {
pushData(startDate, new Date(stat.end), dataValues); pushData(startDate, new Date(stat.end), dataValues);
} }
}); });
@ -598,10 +601,10 @@ export class StatisticsChart extends LitElement {
this.unit = unit; this.unit = unit;
} }
legendData.forEach(({ name, color, borderColor }) => { legendData.forEach(({ id, name, color, borderColor }) => {
// Add an empty series for the legend // Add an empty series for the legend
totalDataSets.push({ totalDataSets.push({
id: name + "-legend", id: id,
name: name, name: name,
color, color,
itemStyle: { itemStyle: {
@ -616,7 +619,7 @@ export class StatisticsChart extends LitElement {
this._chartData = totalDataSets; this._chartData = totalDataSets;
if (legendData.length !== this._legendData.length) { if (legendData.length !== this._legendData.length) {
// only update the legend if it has changed or it will trigger options update // only update the legend if it has changed or it will trigger options update
this._legendData = legendData.map(({ name }) => name); this._legendData = legendData.map(({ id, name }) => ({ id, name }));
} }
this._statisticIds = statisticIds; this._statisticIds = statisticIds;
} }

View File

@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { BarSeriesOption } from "echarts/charts"; import type { BarSeriesOption } from "echarts/charts";
import type { LegendComponentOption } from "echarts/components";
import { getGraphColorByIndex } from "../../../../common/color/colors"; import { getGraphColorByIndex } from "../../../../common/color/colors";
import { getEnergyColor } from "./common/color"; import { getEnergyColor } from "./common/color";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
@ -39,6 +38,7 @@ import {
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import type { ECOption } from "../../../../resources/echarts"; import type { ECOption } from "../../../../resources/echarts";
import { formatNumber } from "../../../../common/number/format_number"; import { formatNumber } from "../../../../common/number/format_number";
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
const UNIT = "kWh"; const UNIT = "kWh";
@ -55,7 +55,7 @@ export class HuiEnergyDevicesDetailGraphCard
@state() private _data?: EnergyData; @state() private _data?: EnergyData;
@state() private _legendData?: LegendComponentOption["data"]; @state() private _legendData?: CustomLegendOption["data"];
@state() private _start = startOfToday(); @state() private _start = startOfToday();
@ -149,12 +149,12 @@ export class HuiEnergyDevicesDetailGraphCard
); );
private _datasetHidden(ev) { private _datasetHidden(ev) {
this._hiddenStats = [...this._hiddenStats, ev.detail.name]; this._hiddenStats = [...this._hiddenStats, ev.detail.id];
} }
private _datasetUnhidden(ev) { private _datasetUnhidden(ev) {
this._hiddenStats = this._hiddenStats.filter( this._hiddenStats = this._hiddenStats.filter(
(stat) => stat !== ev.detail.name (stat) => stat !== ev.detail.id
); );
} }
@ -314,6 +314,7 @@ export class HuiEnergyDevicesDetailGraphCard
datasets.push(...processedData); datasets.push(...processedData);
this._legendData = processedData.map((d) => ({ this._legendData = processedData.map((d) => ({
id: d.id as string,
name: d.name as string, name: d.name as string,
itemStyle: { itemStyle: {
color: d.color as string, color: d.color as string,
@ -330,6 +331,7 @@ export class HuiEnergyDevicesDetailGraphCard
); );
datasets.push(untrackedData); datasets.push(untrackedData);
this._legendData.push({ this._legendData.push({
id: untrackedData.id as string,
name: untrackedData.name as string, name: untrackedData.name as string,
itemStyle: { itemStyle: {
color: untrackedData.color as string, color: untrackedData.color as string,