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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user