diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 214de197d8..e8903dd6c9 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -572,6 +572,7 @@ export class StatisticsChart extends LitElement { let firstSum: number | null | undefined = null; stats.forEach((stat) => { const startDate = new Date(stat.start); + const endDate = new Date(stat.end); if (prevDate === startDate) { return; } @@ -601,10 +602,25 @@ export class StatisticsChart extends LitElement { dataValues.push(val); }); if (!this._hiddenStats.has(statistic_id)) { - pushData(startDate, new Date(stat.end), dataValues); + pushData( + startDate, + endDate.getTime() < endTime.getTime() ? endDate : endTime, + dataValues + ); } }); + // Close out the last stat segment at prevEndTime + const lastEndTime = prevEndTime; + const lastValues = prevValues; + if (lastEndTime && lastValues) { + statDataSets.forEach((d, i) => { + d.data!.push( + this._transformDataValue([lastEndTime, ...lastValues[i]!]) + ); + }); + } + // Append current state if viewing recent data const now = new Date(); // allow 10m of leeway for "now", because stats are 5 minute aggregated @@ -619,16 +635,6 @@ export class StatisticsChart extends LitElement { isFinite(currentValue) && !this._hiddenStats.has(statistic_id) ) { - // First, close out the last stat segment at prevEndTime - const lastEndTime = prevEndTime; - const lastValues = prevValues; - if (lastEndTime && lastValues) { - statDataSets.forEach((d, i) => { - d.data!.push( - this._transformDataValue([lastEndTime, ...lastValues[i]!]) - ); - }); - } // Then push the current state at now statTypes.forEach((type, i) => { const val: (number | null)[] = []; 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 16aadc563e..9bf0560aa6 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -1,8 +1,9 @@ import type { HassConfig } from "home-assistant-js-websocket"; import { - differenceInMonths, subHours, differenceInDays, + differenceInMonths, + differenceInCalendarMonths, differenceInYears, startOfYear, addMilliseconds, @@ -12,6 +13,7 @@ import { addHours, startOfDay, addDays, + subDays, } from "date-fns"; import type { BarSeriesOption, @@ -33,10 +35,22 @@ import { filterXSS } from "../../../../../common/util/xss"; import type { StatisticPeriod } from "../../../../../data/recorder"; import { getSuggestedPeriod } from "../../../../../data/energy"; -export function getSuggestedMax(period: StatisticPeriod, end: Date): Date { +// Number of days of padding when showing time axis in months +const MONTH_TIME_AXIS_PADDING = 5; + +export function getSuggestedMax( + period: StatisticPeriod, + end: Date, + noRounding: boolean +): Date { + // Maximum period depends on whether plotting a line chart or discrete bars. + // - For line charts we must be plotting all the way to end of a given period, + // otherwise we cut off the last period of data. + // - For bar charts we need to round down to the start of the final bars period + // to avoid unnecessary padding of the chart. let suggestedMax = new Date(end); - if (period === "5minute") { + if (noRounding || period === "5minute") { return suggestedMax; } suggestedMax.setMinutes(0, 0, 0); @@ -82,17 +96,44 @@ export function getCommonOptions( detailedDailyData = false ): ECOption { const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData); + const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData); const compare = compareStart !== undefined && compareEnd !== undefined; const showCompareYear = compare && start.getFullYear() !== compareStart.getFullYear(); - const options: ECOption = { + const monthTimeAxis: ECOption = { + xAxis: { + type: "time", + min: subDays(start, MONTH_TIME_AXIS_PADDING), + max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING), + axisLabel: { + formatter: { + year: "{yearStyle|{MMMM} {yyyy}}", + month: "{MMMM}", + }, + rich: { + yearStyle: { + fontWeight: "bold", + }, + }, + }, + // For shorter month ranges, force splitting to ensure time axis renders + // as whole month intervals. Limit the number of forced ticks to 6 months + // (so a max calendar difference of 5) to reduce clutter. + splitNumber: Math.min(differenceInCalendarMonths(end, start), 5), + }, + }; + const normalTimeAxis: ECOption = { xAxis: { type: "time", min: start, - max: getSuggestedMax(suggestedPeriod, end), + max: suggestedMax, }, + }; + + const options: ECOption = { + ...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis), yAxis: { type: "value", name: unit, diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 383f8bc238..6817d96d04 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -332,7 +332,11 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { .maxYAxis=${this._config.max_y_axis} .startTime=${this._energyStart} .endTime=${this._energyEnd && this._energyStart - ? getSuggestedMax(this._period!, this._energyEnd) + ? getSuggestedMax( + this._period!, + this._energyEnd, + (this._config.chart_type ?? "line") === "line" + ) : undefined} .fitYData=${this._config.fit_y_data || false} .hideLegend=${this._config.hide_legend || false}