1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-24 20:55:49 +00:00

Add legend to energy pie chart (#27697)

* Add legend to energy pie chart

* Update src/components/chart/ha-chart-base.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* resize fix

* some fixes

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Petar Petrov
2025-10-29 16:50:57 +02:00
committed by GitHub
parent 7b188759e3
commit 25d9fc94b2
3 changed files with 156 additions and 32 deletions

View File

@@ -35,6 +35,7 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300;
const RESIZE_ANIMATION_DURATION = 250;
export type CustomLegendOption = ECOption["legend"] & {
type: "custom";
@@ -205,6 +206,15 @@ export class HaChartBase extends LitElement {
}
if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() };
if (
this._compareCustomLegendOptions(
changedProps.get("options"),
this.options
)
) {
// custom legend changes may require a resize to layout properly
this._shouldResizeChart = true;
}
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig();
}
@@ -296,7 +306,7 @@ export class HaChartBase extends LitElement {
itemStyle = {
color: dataset?.color as string,
...(dataset?.itemStyle as { borderColor?: string }),
itemStyle,
...itemStyle,
};
const color = itemStyle?.color as string;
const borderColor = itemStyle?.borderColor as string;
@@ -508,6 +518,7 @@ export class HaChartBase extends LitElement {
);
}
});
this.requestUpdate("_hiddenDatasets");
}
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
@@ -958,11 +969,31 @@ export class HaChartBase extends LitElement {
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize();
this.chart?.resize({
animation: this._reducedMotion
? undefined
: { duration: RESIZE_ANIMATION_DURATION },
});
this._shouldResizeChart = false;
}
};
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
): boolean {
const oldLegends = ensureArray(
oldOptions?.legend || []
) as LegendComponentOption[];
const newLegends = ensureArray(
newOptions?.legend || []
) as LegendComponentOption[];
return (
oldLegends.some((l) => l.show && l.type === "custom") !==
newLegends.some((l) => l.show && l.type === "custom")
);
}
static styles = css`
:host {
display: block;

View File

@@ -35,6 +35,8 @@ import { measureTextWidth } from "../../../../util/text";
import "../../../../components/ha-icon-button";
import { storage } from "../../../../common/decorators/storage";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { getEnergyColor } from "./common/color";
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -49,6 +51,8 @@ export class HuiEnergyDevicesGraphCard
@state() private _data?: EnergyData;
@state() private _legendData: NonNullable<CustomLegendOption["data"]> = [];
@state()
@storage({
key: "energy-devices-graph-chart-type",
@@ -57,6 +61,14 @@ export class HuiEnergyDevicesGraphCard
})
private _chartType: "bar" | "pie" = "bar";
@state()
@storage({
key: "energy-devices-pie-hidden-stats",
state: true,
subscribe: false,
})
private _hiddenStats: string[] = [];
@state() private _isMobile = false;
private _compoundStats: string[] = [];
@@ -121,10 +133,16 @@ export class HuiEnergyDevicesGraphCard
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._createOptions(this._chartData, this._chartType)}
.height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`}
@chart-click=${this._handleChartClick}
.options=${this._createOptions(
this._chartData,
this._chartType,
this._legendData
)}
.height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`}
.extraComponents=${[PieChart]}
@chart-click=${this._handleChartClick}
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
</div>
</ha-card>
@@ -145,7 +163,8 @@ export class HuiEnergyDevicesGraphCard
private _createOptions = memoizeOne(
(
data: (BarSeriesOption | PieSeriesOption)[],
chartType: "bar" | "pie"
chartType: "bar" | "pie",
legendData: typeof this._legendData
): ECOption => {
const options: ECOption = {
grid: {
@@ -161,6 +180,7 @@ export class HuiEnergyDevicesGraphCard
},
xAxis: { show: false },
yAxis: { show: false },
legend: { type: "custom", show: false },
};
if (chartType === "bar") {
options.xAxis = {
@@ -191,6 +211,18 @@ export class HuiEnergyDevicesGraphCard
),
},
};
} else {
options.legend = {
type: "custom",
show: true,
data: legendData,
selected: legendData
.filter((d) => d.id && this._hiddenStats.includes(d.id))
.reduce((acc, d) => {
acc[d.id!] = false;
return acc;
}, {}),
};
}
return options;
}
@@ -354,23 +386,12 @@ export class HuiEnergyDevicesGraphCard
}
});
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
datasets.forEach((dataset) => {
dataset.data!.length = Math.min(
this._config?.max_devices || Infinity,
dataset.data!.length
);
});
if (this._chartType === "pie") {
const { summedData } = getSummedData(energyData);
const { consumption } = computeConsumptionData(summedData);
const { summedData, compareSummedData } = getSummedData(energyData);
const { consumption, compareConsumption } = computeConsumptionData(
summedData,
compareSummedData
);
const totalUsed = consumption.total.used_total;
const showUntracked =
"from_grid" in summedData ||
@@ -380,6 +401,47 @@ export class HuiEnergyDevicesGraphCard
? totalUsed -
chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
: 0;
if (untracked > 0) {
const color = getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
false,
false,
"--history-unknown-color"
);
chartData.push({
id: "untracked",
value: [untracked, "untracked"] as any,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
),
itemStyle: {
color: color + "7F",
borderColor: color,
},
});
if (compareData) {
const compareUntracked =
compareConsumption!.total.used_total -
chartDataCompare.reduce(
(acc: number, d: any) => acc + d.value[0],
0
);
if (compareUntracked > 0) {
chartDataCompare.push({
id: "untracked",
value: [compareUntracked, "untracked"] as any,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
),
itemStyle: {
color: color + "32",
borderColor: color + "7F",
},
});
}
}
}
datasets.push({
type: "pie",
radius: ["0%", compareData ? "30%" : "40%"],
@@ -401,17 +463,36 @@ export class HuiEnergyDevicesGraphCard
color: "rgba(0, 0, 0, 0)",
},
tooltip: {
formatter: () =>
untracked > 0
? this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.includes_untracked",
{ num: formatNumber(untracked, this.hass.locale) }
)
: "",
show: false,
},
});
}
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (
this._config?.max_devices &&
chartData.length > this._config.max_devices
) {
chartData.splice(this._config.max_devices);
}
this._legendData = chartData.map((d) => ({
...d,
name: this._getDeviceName(d.name),
}));
// filter out hidden stats in place
for (let i = chartData.length - 1; i >= 0; i--) {
if (this._hiddenStats.includes((chartData[i] as any).id)) {
chartData.splice(i, 1);
}
}
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
this._chartData = datasets;
await this.updateComplete;
}
@@ -440,6 +521,18 @@ export class HuiEnergyDevicesGraphCard
this._getStatistics(this._data!);
}
private _datasetHidden(ev: CustomEvent<{ id: string }>) {
this._hiddenStats = [...this._hiddenStats, ev.detail.id];
this._getStatistics(this._data!);
}
private _datasetUnhidden(ev: CustomEvent<{ id: string }>) {
this._hiddenStats = this._hiddenStats.filter(
(stat) => stat !== ev.detail.id
);
this._getStatistics(this._data!);
}
static styles = css`
.card-header {
display: flex;

View File

@@ -7076,10 +7076,10 @@
"energy_devices_graph": {
"energy_usage": "Energy usage",
"previous_energy_usage": "Previous energy usage",
"total_energy_usage": "Total energy usage",
"total_energy_usage": "Total",
"change_chart_type": "Change chart type",
"untracked": "untracked",
"includes_untracked": "Includes {num} kWh of untracked energy"
"untracked_consumption": "Untracked consumption",
"untracked": "untracked"
},
"energy_devices_detail_graph": {
"untracked_consumption": "Untracked consumption",