From 607175706b1c77cde4a20d37e0e2482f98cfd8dc Mon Sep 17 00:00:00 2001 From: Till Date: Thu, 12 Oct 2023 18:29:04 +0200 Subject: [PATCH] Add date range picker to energy period selector (#14337) --- cast/src/receiver/layout/hc-main.ts | 1 - src/common/datetime/calc_date.ts | 20 +- src/common/datetime/format_date.ts | 23 +- src/components/chart/chart-date-adapter.ts | 4 +- src/components/date-range-picker.ts | 12 +- src/components/ha-date-range-picker.ts | 98 ++- src/data/energy.ts | 44 +- src/panels/energy/ha-panel-energy.ts | 129 +++- .../energy/strategies/energy-view-strategy.ts | 18 +- .../energy/hui-energy-date-selection-card.ts | 22 +- .../cards/energy/hui-energy-gas-graph-card.ts | 6 +- .../energy/hui-energy-solar-graph-card.ts | 6 +- .../energy/hui-energy-usage-graph-card.ts | 6 +- .../energy/hui-energy-water-graph-card.ts | 6 +- .../components/hui-energy-period-selector.ts | 711 ++++++++++++------ src/translations/en.json | 10 +- 16 files changed, 785 insertions(+), 331 deletions(-) diff --git a/cast/src/receiver/layout/hc-main.ts b/cast/src/receiver/layout/hc-main.ts index 7287b024667f..020b0595dad6 100644 --- a/cast/src/receiver/layout/hc-main.ts +++ b/cast/src/receiver/layout/hc-main.ts @@ -260,7 +260,6 @@ export class HcMain extends HassElement { { strategy: { type: "energy", - show_date_selection: true, }, }, ], diff --git a/src/common/datetime/calc_date.ts b/src/common/datetime/calc_date.ts index 5a4da09a29e1..bd1fef2887e2 100644 --- a/src/common/datetime/calc_date.ts +++ b/src/common/datetime/calc_date.ts @@ -5,12 +5,15 @@ import { FrontendLocaleData, TimeZone } from "../../data/translation"; const calcZonedDate = ( date: Date, tz: string, - fn: (date: Date, options?: any) => Date, + fn: (date: Date, options?: any) => Date | number | boolean, options? ) => { const inputZoned = utcToZonedTime(date, tz); const fnZoned = fn(inputZoned, options); - return zonedTimeToUtc(fnZoned, tz); + if (fnZoned instanceof Date) { + return zonedTimeToUtc(fnZoned, tz) as Date; + } + return fnZoned; }; export const calcDate = ( @@ -21,5 +24,16 @@ export const calcDate = ( options? ) => locale.time_zone === TimeZone.server - ? calcZonedDate(date, config.time_zone, fn, options) + ? (calcZonedDate(date, config.time_zone, fn, options) as Date) + : fn(date, options); + +export const calcDateProperty = ( + date: Date, + fn: (date: Date, options?: any) => boolean | number, + locale: FrontendLocaleData, + config: HassConfig, + options? +) => + locale.time_zone === TimeZone.server + ? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean) : fn(date, options); diff --git a/src/common/datetime/format_date.ts b/src/common/datetime/format_date.ts index ffb48bc64e73..5f94177db045 100644 --- a/src/common/datetime/format_date.ts +++ b/src/common/datetime/format_date.ts @@ -37,6 +37,23 @@ const formatDateMem = memoizeOne( }) ); +// Aug 10, 2021 +export const formatDateShort = ( + dateObj: Date, + locale: FrontendLocaleData, + config: HassConfig +) => formatDateShortMem(locale, config.time_zone).format(dateObj); + +const formatDateShortMem = memoizeOne( + (locale: FrontendLocaleData, serverTimeZone: string) => + new Intl.DateTimeFormat(locale.language, { + year: "numeric", + month: "short", + day: "numeric", + timeZone: locale.time_zone === "server" ? serverTimeZone : undefined, + }) +); + // 10/08/2021 export const formatDateNumeric = ( dateObj: Date, @@ -102,13 +119,13 @@ const formatDateNumericMem = memoizeOne( ); // Aug 10 -export const formatDateShort = ( +export const formatDateVeryShort = ( dateObj: Date, locale: FrontendLocaleData, config: HassConfig -) => formatDateShortMem(locale, config.time_zone).format(dateObj); +) => formatDateVeryShortMem(locale, config.time_zone).format(dateObj); -const formatDateShortMem = memoizeOne( +const formatDateVeryShortMem = memoizeOne( (locale: FrontendLocaleData, serverTimeZone: string) => new Intl.DateTimeFormat(locale.language, { day: "numeric", diff --git a/src/components/chart/chart-date-adapter.ts b/src/components/chart/chart-date-adapter.ts index f348823a29f6..c9f9003e7188 100644 --- a/src/components/chart/chart-date-adapter.ts +++ b/src/components/chart/chart-date-adapter.ts @@ -39,7 +39,7 @@ import { formatDate, formatDateMonth, formatDateMonthYear, - formatDateShort, + formatDateVeryShort, formatDateWeekdayDay, formatDateYear, } from "../../common/datetime/format_date"; @@ -128,7 +128,7 @@ _adapters._date.override({ this.options.config ); case "day": - return formatDateShort( + return formatDateVeryShort( new Date(time), this.options.locale, this.options.config diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts index 182d3343ee25..b60ce14763e3 100644 --- a/src/components/date-range-picker.ts +++ b/src/components/date-range-picker.ts @@ -31,6 +31,10 @@ const Component = Vue.extend({ type: Boolean, default: true, }, + openingDirection: { + type: String, + default: "right", + }, disabled: { type: Boolean, default: false, @@ -66,7 +70,7 @@ const Component = Vue.extend({ props: { "time-picker": this.timePicker, "auto-apply": this.autoApply, - opens: "right", + opens: this.openingDirection, "show-dropdowns": false, "time-picker24-hour": this.twentyfourHours, disabled: this.disabled, @@ -126,9 +130,9 @@ class DateRangePickerElement extends WrappedElement { ${dateRangePickerStyles} .calendars { display: flex; + flex-wrap: nowrap !important; } .daterangepicker { - left: 0px !important; top: auto; box-shadow: var(--ha-card-box-shadow, none); background-color: var(--card-background-color); @@ -252,6 +256,10 @@ class DateRangePickerElement extends WrappedElement { direction: ltr; text-align: left; } + .vue-daterange-picker{ + min-width: unset !important; + display: block !important; + } `; const shadowRoot = this.shadowRoot!; shadowRoot.appendChild(style); diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts index 557d139ae331..8bfe2cdd7d58 100644 --- a/src/components/ha-date-range-picker.ts +++ b/src/components/ha-date-range-picker.ts @@ -15,6 +15,7 @@ import { CSSResultGroup, html, LitElement, + nothing, PropertyValues, TemplateResult, } from "lit"; @@ -29,6 +30,7 @@ import { HomeAssistant } from "../types"; import "./date-range-picker"; import "./ha-svg-icon"; import "./ha-textfield"; +import "./ha-icon-button"; export interface DateRangePickerRanges { [key: string]: [Date, Date]; @@ -54,7 +56,21 @@ export class HaDateRangePicker extends LitElement { @property({ type: String }) private _rtlDirection = "ltr"; + @property({ type: Boolean }) private minimal = false; + + @property() private _openingDirection = "right"; + protected willUpdate() { + // set dialog opening direction based on position + const datePickerPosition = this.getBoundingClientRect().x; + if (datePickerPosition > (2 * window.innerWidth) / 3) { + this._openingDirection = "left"; + } else if (datePickerPosition < window.innerWidth / 3) { + this._openingDirection = "right"; + } else { + this._openingDirection = "center"; + } + if (!this.hasUpdated && this.ranges === undefined) { const today = new Date(); const weekStartsOn = firstWeekdayIndex(this.hass.locale); @@ -133,41 +149,61 @@ export class HaDateRangePicker extends LitElement {
- - - + ${!this.minimal + ? html` + + ` + : html``}
${this.ranges ? html`
` - : ""} + : nothing} `; } - private _handleView(ev: CustomEvent): void { - this._period = ev.detail.value; - const today = startOfToday(); - const start = - !this._startDate || - isWithinInterval(today, { - start: this._startDate, - end: this._endDate || endOfToday(), - }) - ? today - : this._startDate; + private _simpleRange(): string { + if (differenceInDays(this._endDate!, this._startDate!) === 0) { + return "day"; + } + if ( + (calcDateProperty( + this._startDate!, + isFirstDayOfMonth, + this.hass.locale, + this.hass.config + ) as boolean) && + (calcDateProperty( + this._endDate!, + isLastDayOfMonth, + this.hass.locale, + this.hass.config + ) as boolean) + ) { + if ( + (calcDateProperty( + this._endDate!, + differenceInMonths, + this.hass.locale, + this.hass.config, + this._startDate! + ) as number) === 0 + ) { + return "month"; + } + if ( + (calcDateProperty( + this._endDate!, + differenceInMonths, + this.hass.locale, + this.hass.config, + this._startDate! + ) as number) === 2 && + this._startDate!.getMonth() % 3 === 0 + ) { + return "quarter"; + } + } + if ( + calcDateProperty( + this._startDate!, + isFirstDayOfMonth, + this.hass.locale, + this.hass.config + ) && + calcDateProperty( + this._endDate!, + isLastDayOfMonth, + this.hass.locale, + this.hass.config + ) && + calcDateProperty( + this._endDate!, + differenceInMonths, + this.hass.locale, + this.hass.config, + this._startDate! + ) === 11 + ) { + return "year"; + } + return "other"; + } - const weekStartsOn = firstWeekdayIndex(this.hass.locale); + private _updateCollectionPeriod() { + const energyCollection = getEnergyDataCollection(this.hass, { + key: this.collectionKey, + }); + energyCollection.setPeriod(this._startDate!, this._endDate!); + energyCollection.refresh(); + } - this._setDate( - this._period === "day" - ? calcDate(start, startOfDay, this.hass.locale, this.hass.config) - : this._period === "week" - ? calcDate(start, startOfWeek, this.hass.locale, this.hass.config, { - weekStartsOn, - }) - : this._period === "month" - ? calcDate(start, startOfMonth, this.hass.locale, this.hass.config) - : calcDate(start, startOfYear, this.hass.locale, this.hass.config) + private _dateRangeChanged(ev) { + const weekStartsOn = firstWeekdayIndex(this.hass.locale); + this._startDate = calcDate( + ev.detail.startDate, + startOfDay, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } + ); + this._endDate = calcDate( + ev.detail.endDate, + endOfDay, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } ); + + this._updateCollectionPeriod(); } private _pickToday() { - const weekStartsOn = firstWeekdayIndex(this.hass.locale); + if (!this._startDate) return; + + const range = this._simpleRange(); + const today = new Date(); + if (range === "month") { + this._startDate = calcDate( + today, + startOfMonth, + this.hass.locale, + this.hass.config + ); + this._endDate = calcDate( + today, + endOfMonth, + this.hass.locale, + this.hass.config + ); + } else if (range === "quarter") { + this._startDate = calcDate( + today, + startOfQuarter, + this.hass.locale, + this.hass.config + ); + this._endDate = calcDate( + today, + endOfQuarter, + this.hass.locale, + this.hass.config + ); + } else if (range === "year") { + this._startDate = calcDate( + today, + startOfYear, + this.hass.locale, + this.hass.config + ); + this._endDate = calcDate( + today, + endOfYear, + this.hass.locale, + this.hass.config + ); + } else { + const weekStartsOn = firstWeekdayIndex(this.hass.locale); + const weekStart = calcDate( + this._endDate!, + startOfWeek, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } + ); + const weekEnd = calcDate( + this._endDate!, + endOfWeek, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } + ); - this._setDate( - this._period === "day" - ? calcDate(new Date(), startOfDay, this.hass.locale, this.hass.config) - : this._period === "week" - ? calcDate( - new Date(), - startOfWeek, + // Check if a single week is selected + if ( + this._startDate.getTime() === weekStart.getTime() && + this._endDate!.getTime() === weekEnd.getTime() + ) { + // Pick current week + this._startDate = calcDate( + today, + startOfWeek, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } + ); + this._endDate = calcDate( + today, + endOfWeek, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } + ); + } else { + // Custom date range + const difference = calcDateProperty( + this._endDate!, + differenceInDays, + this.hass.locale, + this.hass.config, + this._startDate + ) as number; + this._startDate = calcDate( + calcDate( + today, + subDays, this.hass.locale, this.hass.config, - { - weekStartsOn, - } - ) - : this._period === "month" - ? calcDate(new Date(), startOfMonth, this.hass.locale, this.hass.config) - : calcDate(new Date(), startOfYear, this.hass.locale, this.hass.config) - ); + difference + ), + startOfDay, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } + ); + this._endDate = calcDate( + today, + endOfDay, + this.hass.locale, + this.hass.config, + { + weekStartsOn, + } + ); + } + } + + this._updateCollectionPeriod(); } private _pickPrevious() { - const newStart = - this._period === "day" - ? addDays(this._startDate!, -1) - : this._period === "week" - ? addWeeks(this._startDate!, -1) - : this._period === "month" - ? addMonths(this._startDate!, -1) - : addYears(this._startDate!, -1); - this._setDate(newStart); + this._shift(false); } private _pickNext() { - const newStart = - this._period === "day" - ? addDays(this._startDate!, 1) - : this._period === "week" - ? addWeeks(this._startDate!, 1) - : this._period === "month" - ? addMonths(this._startDate!, 1) - : addYears(this._startDate!, 1); - this._setDate(newStart); + this._shift(true); } - private _setDate(startDate: Date) { - const weekStartsOn = firstWeekdayIndex(this.hass.locale); + private _shift(forward: boolean) { + if (!this._startDate) return; - const endDate = - this._period === "day" - ? calcDate(startDate, endOfDay, this.hass.locale, this.hass.config) - : this._period === "week" - ? calcDate(startDate, endOfWeek, this.hass.locale, this.hass.config, { - weekStartsOn, - }) - : this._period === "month" - ? calcDate(startDate, endOfMonth, this.hass.locale, this.hass.config) - : calcDate(startDate, endOfYear, this.hass.locale, this.hass.config); + let start: Date; + let end: Date; + if ( + (calcDateProperty( + this._startDate, + isFirstDayOfMonth, + this.hass.locale, + this.hass.config + ) as boolean) && + (calcDateProperty( + this._endDate!, + isLastDayOfMonth, + this.hass.locale, + this.hass.config + ) as boolean) + ) { + // Shift date range with respect to month/year selection + const difference = + ((calcDateProperty( + this._endDate!, + differenceInMonths, + this.hass.locale, + this.hass.config, + this._startDate + ) as number) + + 1) * + (forward ? 1 : -1); + start = calcDate( + this._startDate, + addMonths, + this.hass.locale, + this.hass.config, + difference + ); + end = calcDate( + calcDate( + this._endDate!, + addMonths, + this.hass.locale, + this.hass.config, + difference + ), + endOfMonth, + this.hass.locale, + this.hass.config + ); + } else { + // Shift date range by period length + const difference = + ((calcDateProperty( + this._endDate!, + differenceInDays, + this.hass.locale, + this.hass.config, + this._startDate + ) as number) + + 1) * + (forward ? 1 : -1); + start = calcDate( + this._startDate, + addDays, + this.hass.locale, + this.hass.config, + difference + ); + end = calcDate( + this._endDate!, + addDays, + this.hass.locale, + this.hass.config, + difference + ); + } - const energyCollection = getEnergyDataCollection(this.hass, { - key: this.collectionKey, - }); - energyCollection.setPeriod(startDate, endDate); - energyCollection.refresh(); + this._startDate = start; + this._endDate = end; + + this._updateCollectionPeriod(); } private _updateDates(energyData: EnergyData): void { this._compare = energyData.startCompare !== undefined; this._startDate = energyData.start; this._endDate = energyData.end || endOfToday(); - const dayDifference = differenceInDays(this._endDate, this._startDate); - this._period = - dayDifference < 1 - ? "day" - : dayDifference === 6 - ? "week" - : dayDifference > 26 && dayDifference < 31 // 28, 29, 30 or 31 days in a month - ? "month" - : dayDifference === 364 || dayDifference === 365 // Leap year - ? "year" - : undefined; } - private _toggleCompare() { - this._compare = !this._compare; + private _toggleCompare(ev: CustomEvent) { + if (ev.detail.source !== "interaction") { + return; + } + this._compare = ev.detail.selected; const energyCollection = getEnergyDataCollection(this.hass, { key: this.collectionKey, }); @@ -305,74 +607,35 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { return css` .row { display: flex; - justify-content: flex-end; - } - :host([narrow]) .row { - flex-direction: column-reverse; + align-items: center; } - .label { + :host .time-handle { display: flex; justify-content: flex-end; align-items: center; - font-size: 20px; } - .period { + :host([narrow]) .time-handle { + margin-left: auto; + } + .label { display: flex; - flex-wrap: wrap; - justify-content: flex-end; align-items: center; + justify-content: flex-end; + font-size: 20px; + margin-left: auto; } - :host([narrow]) .period { - margin-bottom: 8px; + :host([narrow]) .label { + margin-left: unset; } mwc-button { margin-left: 8px; - } - ha-icon-button { - margin-left: 4px; - --mdc-icon-size: 20px; - } - ha-icon-button.active::before, - mwc-button.active::before { - top: 0; - left: 0; - width: 100%; - height: 100%; - position: absolute; - background-color: currentColor; - opacity: 0; - pointer-events: none; - content: ""; - transition: - opacity 15ms linear, - background-color 15ms linear; - opacity: var(--mdc-icon-button-ripple-opacity, 0.12); - } - ha-icon-button.active::before { - border-radius: 50%; - } - .compare { - position: relative; - } - :host { + flex-shrink: 0; --mdc-button-outline-color: currentColor; --primary-color: currentColor; --mdc-theme-primary: currentColor; --mdc-theme-on-primary: currentColor; --mdc-button-disabled-outline-color: var(--disabled-text-color); --mdc-button-disabled-ink-color: var(--disabled-text-color); - --mdc-icon-button-ripple-opacity: 0.2; - } - ha-icon-button { - --mdc-icon-button-size: 28px; - } - ha-button-toggle-group { - padding-left: 8px; - padding-inline-start: 8px; - direction: var(--direction); - } - mwc-button { - flex-shrink: 0; } `; } diff --git a/src/translations/en.json b/src/translations/en.json index aa5862f09855..3077dd44e4fb 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -528,11 +528,15 @@ "start_date": "Start date", "end_date": "End date", "select": "Select", + "select_date_range": "Select time period", "ranges": { "today": "Today", "yesterday": "Yesterday", "this_week": "This week", - "last_week": "Last week" + "last_week": "Last week", + "this_quarter": "This quarter", + "this_month": "This month", + "this_year": "This year" } }, "relative_time": { @@ -5232,10 +5236,6 @@ }, "energy_period_selector": { "today": "Today", - "day": "Day", - "week": "Week", - "month": "Month", - "year": "Year", "previous": "Previous", "next": "Next", "compare": "Compare data"