diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts
index 90b6f0361b0e..8962eed28a78 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts
@@ -2,16 +2,22 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
+import { mdiChartDonut, mdiChartBar } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
-import type { BarSeriesOption } from "echarts/charts";
+import type { BarSeriesOption, PieSeriesOption } from "echarts/charts";
+import { PieChart } from "echarts/charts";
import type { ECElementEvent } from "echarts/types/dist/shared";
import { filterXSS } from "../../../../common/util/xss";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import type { EnergyData } from "../../../../data/energy";
-import { getEnergyDataCollection } from "../../../../data/energy";
+import {
+ computeConsumptionData,
+ getEnergyDataCollection,
+ getSummedData,
+} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
getStatisticLabel,
@@ -26,6 +32,8 @@ import type { ECOption } from "../../../../resources/echarts";
import "../../../../components/ha-card";
import { fireEvent } from "../../../../common/dom/fire_event";
import { measureTextWidth } from "../../../../util/text";
+import "../../../../components/ha-icon-button";
+import { storage } from "../../../../common/decorators/storage";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -36,10 +44,20 @@ export class HuiEnergyDevicesGraphCard
@state() private _config?: EnergyDevicesGraphCardConfig;
- @state() private _chartData: BarSeriesOption[] = [];
+ @state() private _chartData: (BarSeriesOption | PieSeriesOption)[] = [];
@state() private _data?: EnergyData;
+ @state()
+ @storage({
+ key: "energy-devices-graph-chart-type",
+ state: true,
+ subscribe: false,
+ })
+ private _chartType: "bar" | "pie" = "bar";
+
+ private _compoundStats: string[] = [];
+
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
@@ -76,9 +94,16 @@ export class HuiEnergyDevicesGraphCard
return html`
- ${this._config.title
- ? html``
- : ""}
+
@@ -97,71 +123,86 @@ export class HuiEnergyDevicesGraphCard
}
private _renderTooltip(params: any) {
- const deviceName = filterXSS(this._getDeviceName(params.value[1]));
+ const deviceName = filterXSS(this._getDeviceName(params.name));
const title = `
${deviceName}
`;
const value = `${formatNumber(
params.value[0] as number,
this.hass.locale,
- params.value[0] < 0.1 ? { maximumFractionDigits: 3 } : undefined
+ params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
)} kWh`;
return `${title}${params.marker} ${params.seriesName}: ${value}`;
}
- private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => {
- const isMobile = window.matchMedia(
- "all and (max-width: 450px), all and (max-height: 500px)"
- ).matches;
- return {
- xAxis: {
- type: "value",
- name: "kWh",
- },
- yAxis: {
- type: "category",
- inverse: true,
- triggerEvent: true,
- // take order from data
- data: data[0]?.data?.map((d: any) => d.value[1]),
- axisLabel: {
- formatter: this._getDeviceName.bind(this),
- overflow: "truncate",
- fontSize: 12,
- margin: 5,
- width: Math.min(
- isMobile ? 100 : 200,
- Math.max(
- ...(data[0]?.data?.map(
- (d: any) =>
- measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5
- ) || [])
- )
- ),
+ private _createOptions = memoizeOne(
+ (
+ data: (BarSeriesOption | PieSeriesOption)[],
+ chartType: "bar" | "pie"
+ ): ECOption => {
+ const options: ECOption = {
+ grid: {
+ top: 5,
+ left: 5,
+ right: 40,
+ bottom: 0,
+ containLabel: true,
+ },
+ tooltip: {
+ show: true,
+ formatter: this._renderTooltip.bind(this),
},
- },
- grid: {
- top: 5,
- left: 5,
- right: 40,
- bottom: 0,
- containLabel: true,
- },
- tooltip: {
- show: true,
- formatter: this._renderTooltip.bind(this),
- },
- };
- });
+ xAxis: { show: false },
+ yAxis: { show: false },
+ };
+ if (chartType === "bar") {
+ const isMobile = window.matchMedia(
+ "all and (max-width: 450px), all and (max-height: 500px)"
+ ).matches;
+ options.xAxis = {
+ show: true,
+ type: "value",
+ name: "kWh",
+ };
+ options.yAxis = {
+ show: true,
+ type: "category",
+ inverse: true,
+ triggerEvent: true,
+ // take order from data
+ data: data[0]?.data?.map((d: any) => d.name),
+ axisLabel: {
+ formatter: this._getDeviceName.bind(this),
+ overflow: "truncate",
+ fontSize: 12,
+ margin: 5,
+ width: Math.min(
+ isMobile ? 100 : 200,
+ Math.max(
+ ...(data[0]?.data?.map(
+ (d: any) =>
+ measureTextWidth(this._getDeviceName(d.name), 12) + 5
+ ) || [])
+ )
+ ),
+ },
+ };
+ }
+ return options;
+ }
+ );
private _getDeviceName(statisticId: string): string {
+ const suffix = this._compoundStats.includes(statisticId)
+ ? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_graph.untracked")})`
+ : "";
return (
- this._data?.prefs.device_consumption.find(
+ (this._data?.prefs.device_consumption.find(
(d) => d.stat_consumption === statisticId
)?.name ||
- getStatisticLabel(
- this.hass,
- statisticId,
- this._data?.statsMetadata[statisticId]
- )
+ getStatisticLabel(
+ this.hass,
+ statisticId,
+ this._data?.statsMetadata[statisticId]
+ )) + suffix
);
}
@@ -169,60 +210,105 @@ export class HuiEnergyDevicesGraphCard
const data = energyData.stats;
const compareData = energyData.statsCompare;
- const chartData: NonNullable = [];
- const chartDataCompare: NonNullable = [];
+ const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> =
+ [];
+ const chartDataCompare: NonNullable<
+ (BarSeriesOption | PieSeriesOption)["data"]
+ > = [];
- const datasets: BarSeriesOption[] = [
+ const datasets: (BarSeriesOption | PieSeriesOption)[] = [
{
- type: "bar",
+ type: this._chartType,
+ radius: [compareData ? "50%" : "40%", "70%"],
+ universalTransition: true,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage"
),
itemStyle: {
- borderRadius: [0, 4, 4, 0],
+ borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4,
},
data: chartData,
barWidth: compareData ? 10 : 20,
cursor: "default",
- },
+ minShowLabelAngle: 15,
+ label:
+ this._chartType === "pie"
+ ? {
+ formatter: ({ name }) => this._getDeviceName(name),
+ }
+ : undefined,
+ } as BarSeriesOption | PieSeriesOption,
];
if (compareData) {
datasets.push({
- type: "bar",
+ type: this._chartType,
+ radius: ["30%", "50%"],
+ universalTransition: true,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage"
),
itemStyle: {
- borderRadius: [0, 4, 4, 0],
+ borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4,
},
data: chartDataCompare,
barWidth: 10,
cursor: "default",
- });
+ label: this._chartType === "pie" ? { show: false } : undefined,
+ emphasis:
+ this._chartType === "pie"
+ ? {
+ focus: "series",
+ blurScope: "global",
+ }
+ : undefined,
+ } as BarSeriesOption | PieSeriesOption);
}
const computedStyle = getComputedStyle(this);
- const exclude = this._config?.hide_compound_stats
- ? energyData.prefs.device_consumption
- .map((d) => d.included_in_stat)
- .filter(Boolean)
- : [];
+ this._compoundStats = energyData.prefs.device_consumption
+ .map((d) => d.included_in_stat)
+ .filter(Boolean) as string[];
- energyData.prefs.device_consumption.forEach((device, id) => {
- if (exclude.includes(device.stat_consumption)) {
- return;
- }
- const value =
+ const devices = energyData.prefs.device_consumption;
+ const devicesTotals: Record = {};
+ devices.forEach((device) => {
+ devicesTotals[device.stat_consumption] =
device.stat_consumption in data
? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0
: 0;
- const color = getGraphColorByIndex(id, computedStyle);
+ });
+ const devicesTotalsCompare: Record = {};
+ if (compareData) {
+ devices.forEach((device) => {
+ devicesTotalsCompare[device.stat_consumption] =
+ device.stat_consumption in compareData
+ ? calculateStatisticSumGrowth(
+ compareData[device.stat_consumption]
+ ) || 0
+ : 0;
+ });
+ }
+ devices.forEach((device, idx) => {
+ let value = devicesTotals[device.stat_consumption];
+ if (!this._config?.hide_compound_stats) {
+ const childSum = devices.reduce((acc, d) => {
+ if (d.included_in_stat === device.stat_consumption) {
+ return acc + devicesTotals[d.stat_consumption];
+ }
+ return acc;
+ }, 0);
+ value -= Math.min(value, childSum);
+ } else if (this._compoundStats.includes(device.stat_consumption)) {
+ return;
+ }
+ const color = getGraphColorByIndex(idx, computedStyle);
chartData.push({
- id,
- value: [value, device.stat_consumption],
+ id: device.stat_consumption,
+ value: [value, device.stat_consumption] as any,
+ name: device.stat_consumption,
itemStyle: {
color: color + "7F",
borderColor: color,
@@ -230,16 +316,24 @@ export class HuiEnergyDevicesGraphCard
});
if (compareData) {
- const compareValue =
+ let compareValue =
device.stat_consumption in compareData
? calculateStatisticSumGrowth(
compareData[device.stat_consumption]
) || 0
: 0;
+ const compareChildSum = devices.reduce((acc, d) => {
+ if (d.included_in_stat === device.stat_consumption) {
+ return acc + devicesTotalsCompare[d.stat_consumption];
+ }
+ return acc;
+ }, 0);
+ compareValue -= Math.min(compareValue, compareChildSum);
chartDataCompare.push({
- id,
- value: [compareValue, device.stat_consumption],
+ id: device.stat_consumption,
+ value: [compareValue, device.stat_consumption] as any,
+ name: device.stat_consumption,
itemStyle: {
color: color + "32",
borderColor: color + "7F",
@@ -249,11 +343,62 @@ 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;
+ }
- chartData.length = Math.min(
- this._config?.max_devices || Infinity,
- chartData.length
- );
+ 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 totalUsed = consumption.total.used_total;
+ const showUntracked =
+ "from_grid" in summedData ||
+ "solar" in summedData ||
+ "from_battery" in summedData;
+ const untracked = showUntracked
+ ? totalUsed -
+ chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
+ : 0;
+ datasets.push({
+ type: "pie",
+ radius: ["0%", compareData ? "30%" : "40%"],
+ name: this.hass.localize(
+ "ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage"
+ ),
+ data: [totalUsed],
+ label: {
+ show: true,
+ position: "center",
+ color: computedStyle.getPropertyValue("--secondary-text-color"),
+ fontSize: computedStyle.getPropertyValue("--ha-font-size-l"),
+ lineHeight: 24,
+ fontWeight: "bold",
+ formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`,
+ },
+ cursor: "default",
+ itemStyle: {
+ 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) }
+ )
+ : "",
+ },
+ });
+ }
this._chartData = datasets;
await this.updateComplete;
@@ -268,11 +413,26 @@ export class HuiEnergyDevicesGraphCard
fireEvent(this, "hass-more-info", {
entityId: e.detail.value as string,
});
+ } else if (
+ e.detail.seriesType === "pie" &&
+ e.detail.event?.target?.type === "tspan" // label
+ ) {
+ fireEvent(this, "hass-more-info", {
+ entityId: (e.detail.data as any).id as string,
+ });
}
}
+ private _handleChartTypeChange(): void {
+ this._chartType = this._chartType === "pie" ? "bar" : "pie";
+ this._getStatistics(this._data!);
+ }
+
static styles = css`
.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
padding-bottom: 0;
}
.content {
@@ -284,6 +444,11 @@ export class HuiEnergyDevicesGraphCard
ha-chart-base {
--chart-max-height: none;
}
+ ha-icon-button {
+ transform: rotate(90deg);
+ color: var(--secondary-text-color);
+ cursor: pointer;
+ }
`;
}
diff --git a/src/translations/en.json b/src/translations/en.json
index d49ff1e4f8ed..49bd3bd90725 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -7029,7 +7029,11 @@
},
"energy_devices_graph": {
"energy_usage": "Energy usage",
- "previous_energy_usage": "Previous energy usage"
+ "previous_energy_usage": "Previous energy usage",
+ "total_energy_usage": "Total energy usage",
+ "change_chart_type": "Change chart type",
+ "untracked": "untracked",
+ "includes_untracked": "Includes {num} kWh of untracked energy"
},
"energy_devices_detail_graph": {
"untracked_consumption": "Untracked consumption",