mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-04 09:54:00 +00:00
Compare commits
7 Commits
keyboard-i
...
fix-30291
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec43671550 | ||
|
|
3363347200 | ||
|
|
4ab42d7325 | ||
|
|
37b83af087 | ||
|
|
ed3cf9918b | ||
|
|
d670467bc1 | ||
|
|
a4d9e0c7f5 |
@@ -37,6 +37,18 @@ import { filterXSS } from "../../../../../common/util/xss";
|
||||
import type { StatisticPeriod } from "../../../../../data/recorder";
|
||||
import { getSuggestedPeriod } from "../../../../../data/energy";
|
||||
|
||||
/**
|
||||
* Energy chart data point tuple:
|
||||
* [0] displayX - bar position (midpoint for sub-daily periods, start otherwise)
|
||||
* [1] value - the energy value
|
||||
* [2] originalStart - original period start timestamp, used for tooltips
|
||||
*/
|
||||
export type EnergyDataPoint = [
|
||||
displayX: number,
|
||||
value: number,
|
||||
originalStart: number,
|
||||
];
|
||||
|
||||
// Number of days of padding when showing time axis in months
|
||||
const MONTH_TIME_AXIS_PADDING = 5;
|
||||
|
||||
@@ -216,9 +228,8 @@ function formatTooltip(
|
||||
if (!params[0]?.value) {
|
||||
return "";
|
||||
}
|
||||
// when comparing the first value is offset to match the main period
|
||||
// and the real date is in the third value
|
||||
// find the first param with the real date to handle gap-filled entries
|
||||
// displayX may be shifted from the period start (see EnergyDataPoint);
|
||||
// originalStart has the real date for display. Gap-filled entries lack it.
|
||||
const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2];
|
||||
const date = new Date(origDate ?? params[0].value?.[0]);
|
||||
let period: string;
|
||||
@@ -382,6 +393,34 @@ export function fillLineGaps(datasets: LineSeriesOption[]) {
|
||||
return datasets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the display x-position for an energy bar chart data point.
|
||||
* For sub-daily periods (hour/5minute), returns the midpoint to center bars
|
||||
* between ticks. For daily or longer periods, returns the start timestamp.
|
||||
*/
|
||||
export function computeStatMidpoint(
|
||||
start: number,
|
||||
end: number,
|
||||
period: string,
|
||||
compareTransform?: (ts: Date) => Date
|
||||
): number {
|
||||
const center = period === "hour" || period === "5minute";
|
||||
if (!center) {
|
||||
if (compareTransform) {
|
||||
return compareTransform(new Date(start)).getTime();
|
||||
}
|
||||
return start;
|
||||
}
|
||||
if (compareTransform) {
|
||||
return (
|
||||
(compareTransform(new Date(start)).getTime() +
|
||||
compareTransform(new Date(end)).getTime()) /
|
||||
2
|
||||
);
|
||||
}
|
||||
return (start + end) / 2;
|
||||
}
|
||||
|
||||
export function getCompareTransform(start: Date, compareStart?: Date) {
|
||||
if (!compareStart) {
|
||||
return (ts: Date) => ts;
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
getEnergyDataCollection,
|
||||
getSuggestedPeriod,
|
||||
getSummedData,
|
||||
computeConsumptionData,
|
||||
validateEnergyCollectionKey,
|
||||
@@ -32,6 +33,8 @@ import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyDevicesDetailGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import {
|
||||
computeStatMidpoint,
|
||||
type EnergyDataPoint,
|
||||
fillDataGapsAndRoundCaps,
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
@@ -389,31 +392,36 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
|
||||
processedData.forEach((device) => {
|
||||
device.data.forEach((datapoint) => {
|
||||
totalDeviceConsumption[datapoint[compare ? 2 : 0]] =
|
||||
(totalDeviceConsumption[datapoint[compare ? 2 : 0]] || 0) +
|
||||
datapoint[1];
|
||||
totalDeviceConsumption[datapoint[2]] =
|
||||
(totalDeviceConsumption[datapoint[2]] || 0) + datapoint[1];
|
||||
});
|
||||
});
|
||||
const compareTransform = getCompareTransform(
|
||||
this._start,
|
||||
this._compareStart!
|
||||
);
|
||||
const period = getSuggestedPeriod(this._start, this._end);
|
||||
|
||||
const untrackedConsumption: BarSeriesOption["data"] = [];
|
||||
Object.keys(consumptionData.used_total)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.forEach((time) => {
|
||||
const ts = Number(time);
|
||||
const value =
|
||||
consumptionData.used_total[time] -
|
||||
(totalDeviceConsumption[time] || 0);
|
||||
const dataPoint: number[] = [ts, value];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(ts)).getTime();
|
||||
}
|
||||
untrackedConsumption.push(dataPoint);
|
||||
});
|
||||
const sortedTimes = Object.keys(consumptionData.used_total).sort(
|
||||
(a, b) => Number(a) - Number(b)
|
||||
);
|
||||
// Only start timestamps available here, so estimate midpoint from the gap
|
||||
// between the first two entries. Assumes uniform period spacing.
|
||||
const periodOffset =
|
||||
(period === "hour" || period === "5minute") && sortedTimes.length >= 2
|
||||
? (Number(sortedTimes[1]) - Number(sortedTimes[0])) / 2
|
||||
: 0;
|
||||
sortedTimes.forEach((time) => {
|
||||
const ts = Number(time);
|
||||
const value =
|
||||
consumptionData.used_total[time] - (totalDeviceConsumption[time] || 0);
|
||||
const dataPoint: EnergyDataPoint = [ts + periodOffset, value, ts];
|
||||
if (compare) {
|
||||
dataPoint[0] = compareTransform(new Date(ts)).getTime() + periodOffset;
|
||||
}
|
||||
untrackedConsumption.push(dataPoint);
|
||||
});
|
||||
// random id to always add untracked at the end
|
||||
const order = Date.now();
|
||||
const dataset: BarSeriesOption = {
|
||||
@@ -460,6 +468,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
this._start,
|
||||
this._compareStart!
|
||||
);
|
||||
const period = getSuggestedPeriod(this._start, this._end);
|
||||
|
||||
devices.forEach((source, idx) => {
|
||||
const order = sorted_devices.indexOf(source.stat_consumption);
|
||||
@@ -499,11 +508,16 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
cStats?.find((cStat) => cStat.start === point.start)?.change || 0;
|
||||
});
|
||||
|
||||
const dataPoint = [point.start, point.change - sumChildren];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(point.start)).getTime();
|
||||
}
|
||||
const dataPoint: EnergyDataPoint = [
|
||||
computeStatMidpoint(
|
||||
point.start,
|
||||
point.end,
|
||||
period,
|
||||
compare ? compareTransform : undefined
|
||||
),
|
||||
point.change - sumChildren,
|
||||
point.start,
|
||||
];
|
||||
consumptionData.push(dataPoint);
|
||||
prevStart = point.start;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
getEnergyDataCollection,
|
||||
getSuggestedPeriod,
|
||||
validateEnergyCollectionKey,
|
||||
} from "../../../../data/energy";
|
||||
import type { Statistics, StatisticsMetaData } from "../../../../data/recorder";
|
||||
@@ -27,6 +28,8 @@ import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyGasGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import {
|
||||
computeStatMidpoint,
|
||||
type EnergyDataPoint,
|
||||
fillDataGapsAndRoundCaps,
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
@@ -265,6 +268,7 @@ export class HuiEnergyGasGraphCard
|
||||
this._start,
|
||||
this._compareStart!
|
||||
);
|
||||
const period = getSuggestedPeriod(this._start, this._end);
|
||||
|
||||
gasSources.forEach((source, idx) => {
|
||||
let prevStart: number | null = null;
|
||||
@@ -285,14 +289,16 @@ export class HuiEnergyGasGraphCard
|
||||
if (prevStart === point.start) {
|
||||
continue;
|
||||
}
|
||||
const dataPoint: (Date | string | number)[] = [
|
||||
point.start,
|
||||
const dataPoint: EnergyDataPoint = [
|
||||
computeStatMidpoint(
|
||||
point.start,
|
||||
point.end,
|
||||
period,
|
||||
compare ? compareTransform : undefined
|
||||
),
|
||||
point.change,
|
||||
point.start,
|
||||
];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(point.start));
|
||||
}
|
||||
gasConsumptionData.push(dataPoint);
|
||||
prevStart = point.start;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import type { LovelaceCard } from "../../types";
|
||||
import type { EnergySolarGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import {
|
||||
computeStatMidpoint,
|
||||
type EnergyDataPoint,
|
||||
fillDataGapsAndRoundCaps,
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
@@ -287,6 +289,7 @@ export class HuiEnergySolarGraphCard
|
||||
this._start,
|
||||
this._compareStart!
|
||||
);
|
||||
const period = getSuggestedPeriod(this._start, this._end);
|
||||
|
||||
solarSources.forEach((source, idx) => {
|
||||
let prevStart: number | null = null;
|
||||
@@ -308,14 +311,16 @@ export class HuiEnergySolarGraphCard
|
||||
if (prevStart === point.start) {
|
||||
continue;
|
||||
}
|
||||
const dataPoint: (Date | string | number)[] = [
|
||||
point.start,
|
||||
const dataPoint: EnergyDataPoint = [
|
||||
computeStatMidpoint(
|
||||
point.start,
|
||||
point.end,
|
||||
period,
|
||||
compare ? compareTransform : undefined
|
||||
),
|
||||
point.change,
|
||||
point.start,
|
||||
];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(point.start));
|
||||
}
|
||||
solarProductionData.push(dataPoint);
|
||||
prevStart = point.start;
|
||||
}
|
||||
@@ -410,8 +415,24 @@ export class HuiEnergySolarGraphCard
|
||||
|
||||
if (forecastsData) {
|
||||
const solarForecastData: LineSeriesOption["data"] = [];
|
||||
// Only center forecast points for sub-daily periods to align with bars.
|
||||
// Only start timestamps available, so estimate midpoint from the gap
|
||||
// between the first two entries. Assumes uniform spacing.
|
||||
let forecastOffset = 0;
|
||||
if (period === "hour" || period === "5minute") {
|
||||
const forecastTimes = Object.keys(forecastsData)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
forecastOffset =
|
||||
forecastTimes.length >= 2
|
||||
? (forecastTimes[1] - forecastTimes[0]) / 2
|
||||
: 0;
|
||||
}
|
||||
for (const [time, value] of Object.entries(forecastsData)) {
|
||||
solarForecastData.push([Number(time), value / 1000]);
|
||||
solarForecastData.push([
|
||||
Number(time) + forecastOffset,
|
||||
value / 1000,
|
||||
]);
|
||||
}
|
||||
|
||||
if (solarForecastData.length) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
import {
|
||||
computeConsumptionData,
|
||||
getEnergyDataCollection,
|
||||
getSuggestedPeriod,
|
||||
getSummedData,
|
||||
validateEnergyCollectionKey,
|
||||
} from "../../../../data/energy";
|
||||
@@ -36,6 +37,7 @@ import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyUsageGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import {
|
||||
type EnergyDataPoint,
|
||||
fillDataGapsAndRoundCaps,
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
@@ -482,6 +484,15 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
const uniqueKeys = summedData.timestamps;
|
||||
|
||||
// Only center bars for sub-daily periods (hour/5min).
|
||||
// Only start timestamps available here, so estimate midpoint from the gap
|
||||
// between the first two entries. Assumes uniform period spacing.
|
||||
const period = getSuggestedPeriod(this._start, this._end);
|
||||
const periodOffset =
|
||||
(period === "hour" || period === "5minute") && uniqueKeys.length >= 2
|
||||
? (uniqueKeys[1] - uniqueKeys[0]) / 2
|
||||
: 0;
|
||||
|
||||
const compareTransform = getCompareTransform(
|
||||
this._start,
|
||||
this._compareStart!
|
||||
@@ -493,15 +504,16 @@ export class HuiEnergyUsageGraphCard
|
||||
// Process chart data.
|
||||
for (const key of uniqueKeys) {
|
||||
const value = source[key] || 0;
|
||||
const dataPoint = [
|
||||
new Date(key),
|
||||
const dataPoint: EnergyDataPoint = [
|
||||
key + periodOffset,
|
||||
value && ["to_grid", "to_battery"].includes(type)
|
||||
? -1 * value
|
||||
: value,
|
||||
key,
|
||||
];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(dataPoint[0] as Date);
|
||||
dataPoint[0] =
|
||||
compareTransform(new Date(key)).getTime() + periodOffset;
|
||||
}
|
||||
points.push(dataPoint);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
getEnergyDataCollection,
|
||||
getSuggestedPeriod,
|
||||
validateEnergyCollectionKey,
|
||||
} from "../../../../data/energy";
|
||||
import type { Statistics, StatisticsMetaData } from "../../../../data/recorder";
|
||||
@@ -26,6 +27,8 @@ import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyWaterGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import {
|
||||
computeStatMidpoint,
|
||||
type EnergyDataPoint,
|
||||
fillDataGapsAndRoundCaps,
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
@@ -265,6 +268,7 @@ export class HuiEnergyWaterGraphCard
|
||||
this._start,
|
||||
this._compareStart!
|
||||
);
|
||||
const period = getSuggestedPeriod(this._start, this._end);
|
||||
|
||||
waterSources.forEach((source, idx) => {
|
||||
let prevStart: number | null = null;
|
||||
@@ -285,14 +289,16 @@ export class HuiEnergyWaterGraphCard
|
||||
if (prevStart === point.start) {
|
||||
continue;
|
||||
}
|
||||
const dataPoint: (Date | string | number)[] = [
|
||||
point.start,
|
||||
const dataPoint: EnergyDataPoint = [
|
||||
computeStatMidpoint(
|
||||
point.start,
|
||||
point.end,
|
||||
period,
|
||||
compare ? compareTransform : undefined
|
||||
),
|
||||
point.change,
|
||||
point.start,
|
||||
];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(point.start));
|
||||
}
|
||||
waterConsumptionData.push(dataPoint);
|
||||
prevStart = point.start;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user