1
mirror of https://github.com/home-assistant/frontend synced 2024-09-30 15:52:53 +02:00

Fix trailing energy gaps, refactor chart options (#19117)

* Fix trailing gap on energy graph cards

* Use date methods instead of unix time calculation

* Fix trailing energy gaps, refactor chart options

---------

Co-authored-by: Till Fleisch <till@fleisch.dev>
This commit is contained in:
karwosts 2023-12-27 01:53:16 -08:00 committed by GitHub
parent bded31b311
commit df54687de1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 231 additions and 520 deletions

View File

@ -0,0 +1,168 @@
import { ChartOptions } from "chart.js";
import { HassConfig } from "home-assistant-js-websocket";
import {
addHours,
subHours,
differenceInDays,
differenceInHours,
} from "date-fns/esm";
import { FrontendLocaleData } from "../../../../../data/translation";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../../common/number/format_number";
import { formatDateVeryShort } from "../../../../../common/datetime/format_date";
import { formatTime } from "../../../../../common/datetime/format_time";
export function getSuggestedMax(dayDifference: number, end: Date): number {
let suggestedMax = new Date(end);
// Sometimes around DST we get a time of 0:59 instead of 23:59 as expected.
// Correct for this when showing days/months so we don't get an extra day.
if (dayDifference > 2 && suggestedMax.getHours() === 0) {
suggestedMax = subHours(suggestedMax, 1);
}
suggestedMax.setMinutes(0, 0, 0);
if (dayDifference > 35) {
suggestedMax.setDate(1);
}
if (dayDifference > 2) {
suggestedMax.setHours(0);
}
return suggestedMax.getTime();
}
export function getCommonOptions(
start: Date,
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit?: string,
compareStart?: Date,
compareEnd?: Date
): ChartOptions {
const dayDifference = differenceInDays(end, start);
const compare = compareStart !== undefined && compareEnd !== undefined;
if (compare && dayDifference <= 35) {
const difference = differenceInHours(end, start);
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
// If the compare period doesn't match the main period, adjust them to match
if (differenceCompare > difference) {
end = addHours(end, differenceCompare - difference);
} else if (difference > differenceCompare) {
compareEnd = addHours(compareEnd!, difference - differenceCompare);
}
}
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
max: getSuggestedMax(dayDifference, end),
adapters: {
date: {
locale,
config,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour",
},
},
y: {
stacked: true,
type: "linear",
title: {
display: true,
text: unit,
},
ticks: {
beginAtZero: true,
callback: (value) => formatNumber(Math.abs(value), locale),
},
},
},
plugins: {
tooltip: {
position: "nearest",
filter: (val) => val.formattedValue !== "0",
itemSort: function (a, b) {
return b.datasetIndex - a.datasetIndex;
},
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${
compare ? `${formatDateVeryShort(date, locale, config)}: ` : ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,
config
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
locale
)} ${unit}`,
},
},
filler: {
propagate: false,
},
legend: {
display: false,
labels: {
usePointStyle: true,
},
},
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 50,
},
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
};
if (compare) {
options.scales!.xAxisCompare = {
...(options.scales!.x as Record<string, any>),
suggestedMin: compareStart!.getTime(),
max: getSuggestedMax(dayDifference, compareEnd!),
display: false,
};
}
return options;
}

View File

@ -4,14 +4,7 @@ import {
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
} from "date-fns";
import { endOfToday, isToday, startOfToday } from "date-fns";
import { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@ -31,12 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
@ -56,6 +44,7 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyGasGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions } from "./common/energy-chart-options";
@customElement("hui-energy-gas-graph-card")
export class HuiEnergyGasGraphCard
@ -159,105 +148,23 @@ export class HuiEnergyGasGraphCard
compareStart?: Date,
compareEnd?: Date
): ChartOptions => {
const dayDifference = differenceInDays(end, start);
const compare = compareStart !== undefined && compareEnd !== undefined;
if (compare) {
const difference = differenceInHours(end, start);
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
// If the compare period doesn't match the main period, adjust them to match
if (differenceCompare > difference) {
end = addHours(end, differenceCompare - difference);
} else if (difference > differenceCompare) {
compareEnd = addHours(compareEnd!, difference - differenceCompare);
}
}
const commonOptions = getCommonOptions(
start,
end,
locale,
config,
unit,
compareStart,
compareEnd
);
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
suggestedMax: end.getTime(),
adapters: {
date: {
locale,
config,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour",
},
offset: true,
},
y: {
stacked: true,
type: "linear",
title: {
display: true,
text: unit,
},
ticks: {
beginAtZero: true,
},
},
},
...commonOptions,
plugins: {
...commonOptions.plugins,
tooltip: {
position: "nearest",
filter: (val) => val.formattedValue !== "0",
itemSort: function (a, b) {
return b.datasetIndex - a.datasetIndex;
},
...commonOptions.plugins!.tooltip,
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,
config
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
locale
)} ${unit}`,
...commonOptions.plugins!.tooltip!.callbacks,
footer: (contexts) => {
if (contexts.length < 2) {
return [];
@ -278,33 +185,8 @@ export class HuiEnergyGasGraphCard
},
},
},
filler: {
propagate: false,
},
legend: {
display: false,
labels: {
usePointStyle: true,
},
},
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 50,
},
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
};
if (compare) {
options.scales!.xAxisCompare = {
...(options.scales!.x as Record<string, any>),
suggestedMin: compareStart!.getTime(),
suggestedMax: compareEnd!.getTime(),
display: false,
};
}
return options;
}
);

View File

@ -5,9 +5,7 @@ import {
ScatterDataPoint,
} from "chart.js";
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
@ -31,12 +29,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
@ -57,6 +50,7 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions } from "./common/energy-chart-options";
@customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard
@ -156,104 +150,23 @@ export class HuiEnergySolarGraphCard
compareStart?: Date,
compareEnd?: Date
): ChartOptions => {
const dayDifference = differenceInDays(end, start);
const compare = compareStart !== undefined && compareEnd !== undefined;
if (compare) {
const difference = differenceInHours(end, start);
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
// If the compare period doesn't match the main period, adjust them to match
if (differenceCompare > difference) {
end = addHours(end, differenceCompare - difference);
} else if (difference > differenceCompare) {
compareEnd = addHours(compareEnd!, difference - differenceCompare);
}
}
const commonOptions = getCommonOptions(
start,
end,
locale,
config,
"kWh",
compareStart,
compareEnd
);
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
suggestedMax: end.getTime(),
adapters: {
date: {
locale,
config,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour",
},
},
y: {
stacked: true,
type: "linear",
title: {
display: true,
text: "kWh",
},
ticks: {
beginAtZero: true,
},
},
},
...commonOptions,
plugins: {
...commonOptions.plugins,
tooltip: {
position: "nearest",
filter: (val) => val.formattedValue !== "0",
itemSort: function (a, b) {
return b.datasetIndex - a.datasetIndex;
},
...commonOptions.plugins!.tooltip,
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,
config
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
locale
)} kWh`,
...commonOptions.plugins!.tooltip!.callbacks,
footer: (contexts) => {
const production_contexts = contexts.filter(
(c) => c.dataset?.stack === "solar"
@ -277,15 +190,6 @@ export class HuiEnergySolarGraphCard
},
},
},
filler: {
propagate: false,
},
legend: {
display: false,
labels: {
usePointStyle: true,
},
},
},
elements: {
line: {
@ -297,17 +201,7 @@ export class HuiEnergySolarGraphCard
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
};
if (compare) {
options.scales!.xAxisCompare = {
...(options.scales!.x as Record<string, any>),
suggestedMin: compareStart!.getTime(),
suggestedMax: compareEnd!.getTime(),
display: false,
};
}
return options;
}
);

View File

@ -4,14 +4,7 @@ import {
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
} from "date-fns/esm";
import { endOfToday, isToday, startOfToday } from "date-fns/esm";
import { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@ -31,12 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
@ -51,6 +39,7 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyUsageGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions } from "./common/energy-chart-options";
interface ColorSet {
base: string;
@ -155,81 +144,21 @@ export class HuiEnergyUsageGraphCard
compareStart?: Date,
compareEnd?: Date
): ChartOptions => {
const dayDifference = differenceInDays(end, start);
const compare = compareStart !== undefined && compareEnd !== undefined;
if (compare) {
const difference = differenceInHours(end, start);
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
// If the compare period doesn't match the main period, adjust them to match
if (differenceCompare > difference) {
end = addHours(end, differenceCompare - difference);
} else if (difference > differenceCompare) {
compareEnd = addHours(compareEnd!, difference - differenceCompare);
}
}
const commonOptions = getCommonOptions(
start,
end,
locale,
config,
"kWh",
compareStart,
compareEnd
);
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
suggestedMax: end.getTime(),
adapters: {
date: {
locale,
config,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour",
},
},
y: {
stacked: true,
type: "linear",
title: {
display: true,
text: "kWh",
},
ticks: {
beginAtZero: true,
callback: (value) => formatNumber(Math.abs(value), locale),
},
},
},
...commonOptions,
plugins: {
...commonOptions.plugins,
tooltip: {
position: "nearest",
filter: (val) => val.formattedValue !== "0",
...commonOptions.plugins!.tooltip,
itemSort: function (a: any, b: any) {
if (a.raw?.y > 0 && b.raw?.y < 0) {
return -1;
@ -243,26 +172,7 @@ export class HuiEnergyUsageGraphCard
return a.datasetIndex - b.datasetIndex;
},
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,
config
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
Math.abs(context.parsed.y),
locale
)} kWh`,
...commonOptions.plugins!.tooltip!.callbacks,
footer: (contexts) => {
let totalConsumed = 0;
let totalReturned = 0;
@ -292,33 +202,8 @@ export class HuiEnergyUsageGraphCard
},
},
},
filler: {
propagate: false,
},
legend: {
display: false,
labels: {
usePointStyle: true,
},
},
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 50,
},
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
};
if (compare) {
options.scales!.xAxisCompare = {
...(options.scales!.x as Record<string, any>),
suggestedMin: compareStart!.getTime(),
suggestedMax: compareEnd!.getTime(),
display: false,
};
}
return options;
}
);

View File

@ -4,14 +4,7 @@ import {
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
} from "date-fns";
import { endOfToday, isToday, startOfToday } from "date-fns";
import { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@ -31,12 +24,7 @@ import {
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateVeryShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
@ -56,6 +44,7 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyWaterGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions } from "./common/energy-chart-options";
@customElement("hui-energy-water-graph-card")
export class HuiEnergyWaterGraphCard
@ -159,105 +148,23 @@ export class HuiEnergyWaterGraphCard
compareStart?: Date,
compareEnd?: Date
): ChartOptions => {
const dayDifference = differenceInDays(end, start);
const compare = compareStart !== undefined && compareEnd !== undefined;
if (compare) {
const difference = differenceInHours(end, start);
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
// If the compare period doesn't match the main period, adjust them to match
if (differenceCompare > difference) {
end = addHours(end, differenceCompare - difference);
} else if (difference > differenceCompare) {
compareEnd = addHours(compareEnd!, difference - differenceCompare);
}
}
const commonOptions = getCommonOptions(
start,
end,
locale,
config,
unit,
compareStart,
compareEnd
);
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
suggestedMax: end.getTime(),
adapters: {
date: {
locale,
config,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour",
},
offset: true,
},
y: {
stacked: true,
type: "linear",
title: {
display: true,
text: unit,
},
ticks: {
beginAtZero: true,
},
},
},
...commonOptions,
plugins: {
...commonOptions.plugins,
tooltip: {
position: "nearest",
filter: (val) => val.formattedValue !== "0",
itemSort: function (a, b) {
return b.datasetIndex - a.datasetIndex;
},
...commonOptions.plugins!.tooltip,
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${
compare
? `${formatDateVeryShort(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,
config
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
locale
)} ${unit}`,
...commonOptions.plugins!.tooltip!.callbacks,
footer: (contexts) => {
if (contexts.length < 2) {
return [];
@ -278,33 +185,8 @@ export class HuiEnergyWaterGraphCard
},
},
},
filler: {
propagate: false,
},
legend: {
display: false,
labels: {
usePointStyle: true,
},
},
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 50,
},
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
};
if (compare) {
options.scales!.xAxisCompare = {
...(options.scales!.x as Record<string, any>),
suggestedMin: compareStart!.getTime(),
suggestedMax: compareEnd!.getTime(),
display: false,
};
}
return options;
}
);