diff --git a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts index 5e2d072f4c..2ab09b7f28 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -39,7 +39,7 @@ import { getSuggestedPeriod } from "../../../../../data/energy"; /** * Energy chart data point tuple: - * [0] displayX - midpoint of the period, used for bar positioning + * [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 */ @@ -228,8 +228,8 @@ function formatTooltip( if (!params[0]?.value) { return ""; } - // displayX is the period midpoint; originalStart has the real date for display. - // Gap-filled entries lack originalStart, so find the first real one. + // 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; @@ -393,11 +393,24 @@ 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() + diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts index 769cfb5eed..5f6fafef21 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts @@ -16,6 +16,7 @@ import type { } from "../../../../data/energy"; import { getEnergyDataCollection, + getSuggestedPeriod, getSummedData, computeConsumptionData, validateEnergyCollectionKey, @@ -399,6 +400,7 @@ export class HuiEnergyDevicesDetailGraphCard this._start, this._compareStart! ); + const period = getSuggestedPeriod(this._start, this._end); const untrackedConsumption: BarSeriesOption["data"] = []; const sortedTimes = Object.keys(consumptionData.used_total).sort( @@ -407,7 +409,7 @@ export class HuiEnergyDevicesDetailGraphCard // Only start timestamps available here, so estimate midpoint from the gap // between the first two entries. Assumes uniform period spacing. const periodOffset = - sortedTimes.length >= 2 + (period === "hour" || period === "5minute") && sortedTimes.length >= 2 ? (Number(sortedTimes[1]) - Number(sortedTimes[0])) / 2 : 0; sortedTimes.forEach((time) => { @@ -466,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); @@ -509,6 +512,7 @@ export class HuiEnergyDevicesDetailGraphCard computeStatMidpoint( point.start, point.end, + period, compare ? compareTransform : undefined ), point.change - sumChildren, diff --git a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts index c00f9baf3b..3c21382f07 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts @@ -16,6 +16,7 @@ import type { } from "../../../../data/energy"; import { getEnergyDataCollection, + getSuggestedPeriod, validateEnergyCollectionKey, } from "../../../../data/energy"; import type { Statistics, StatisticsMetaData } from "../../../../data/recorder"; @@ -267,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; @@ -291,6 +293,7 @@ export class HuiEnergyGasGraphCard computeStatMidpoint( point.start, point.end, + period, compare ? compareTransform : undefined ), point.change, diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index 382520c3cc..25dda71a0e 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -289,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; @@ -314,6 +315,7 @@ export class HuiEnergySolarGraphCard computeStatMidpoint( point.start, point.end, + period, compare ? compareTransform : undefined ), point.change, @@ -413,15 +415,19 @@ export class HuiEnergySolarGraphCard if (forecastsData) { const solarForecastData: LineSeriesOption["data"] = []; - // Only start timestamps available for forecasts, so estimate midpoint - // from the gap between the first two entries. Assumes uniform spacing. - const forecastTimes = Object.keys(forecastsData) - .map(Number) - .sort((a, b) => a - b); - const forecastOffset = - forecastTimes.length >= 2 - ? (forecastTimes[1] - forecastTimes[0]) / 2 - : 0; + // 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) + forecastOffset, diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index 512cae5ea6..871d078b53 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -24,6 +24,7 @@ import type { import { computeConsumptionData, getEnergyDataCollection, + getSuggestedPeriod, getSummedData, validateEnergyCollectionKey, } from "../../../../data/energy"; @@ -483,10 +484,14 @@ 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 = - uniqueKeys.length >= 2 ? (uniqueKeys[1] - uniqueKeys[0]) / 2 : 0; + (period === "hour" || period === "5minute") && uniqueKeys.length >= 2 + ? (uniqueKeys[1] - uniqueKeys[0]) / 2 + : 0; const compareTransform = getCompareTransform( this._start, diff --git a/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts index 04b2f1e2d4..14ac832731 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts @@ -15,6 +15,7 @@ import type { } from "../../../../data/energy"; import { getEnergyDataCollection, + getSuggestedPeriod, validateEnergyCollectionKey, } from "../../../../data/energy"; import type { Statistics, StatisticsMetaData } from "../../../../data/recorder"; @@ -267,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; @@ -291,6 +293,7 @@ export class HuiEnergyWaterGraphCard computeStatMidpoint( point.start, point.end, + period, compare ? compareTransform : undefined ), point.change,