1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00

Improve energy dashboard monthly/this-quarter chart time axes (#29435)

* Add splitNumber option to monthly ECharts

When there are a small number of bars (<=3) for monthly data, set the splitNumber parameter to force the date x-axis to show whole months.

* Add axis tick fomratting for short months

This ensures that the month format is consistent between 2/3 month and longer ranges.

* Avoid calling getSuggestedMax twice

* Fix another case of power chart cutting off last hour of data

The previous fix only solved the problem for 5-minute data, not hourly or daily. This should solve the issue regardless, and allows the energy chart to have other line-based plots in the future.

* Update other uses of getSuggestedMax()

* Fix statistics-chart Last Period Rendering

1. When appending the "current state" value, if the current time intersects with the final period, we can end up with the chart folding back on itself. This is fixed by ensuring for the final period we push the earlier of the statistic end time and the display end time (which is in turn limited to now).

2. Always close off the last data point at the chart end time. Otherwise for line charts, the final period doesn't get rendered.

* Remove unused monthStyle formatter.

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Rename getSuggestedMax function parameter in energy chart

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Document magic numbers in montly energy chart

* Make padding a constant for clarity.
* Explain the purpose of splitNumber.

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Tom Carpenter
2026-02-09 11:05:42 +00:00
committed by GitHub
parent c41d7ff923
commit 8e860cb17d
3 changed files with 68 additions and 17 deletions

View File

@@ -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)[] = [];

View File

@@ -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,

View File

@@ -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}