diff --git a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts new file mode 100644 index 0000000000..ef6c04677e --- /dev/null +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -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), + suggestedMin: compareStart!.getTime(), + max: getSuggestedMax(dayDifference, compareEnd!), + display: false, + }; + } + return options; +} 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 1ae6d1f730..5bb965b052 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 @@ -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), - suggestedMin: compareStart!.getTime(), - suggestedMax: compareEnd!.getTime(), - display: false, - }; - } return options; } ); 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 78916e4e33..9da9393888 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 @@ -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), - suggestedMin: compareStart!.getTime(), - suggestedMax: compareEnd!.getTime(), - display: false, - }; - } return options; } ); 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 f190757029..e0be896b36 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 @@ -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), - suggestedMin: compareStart!.getTime(), - suggestedMax: compareEnd!.getTime(), - display: false, - }; - } return options; } ); 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 15942779f9..2398b146a9 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 @@ -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), - suggestedMin: compareStart!.getTime(), - suggestedMax: compareEnd!.getTime(), - display: false, - }; - } return options; } );