diff --git a/src/components/chart/axis-label.ts b/src/components/chart/axis-label.ts index d779269e33..1f8f9a8154 100644 --- a/src/components/chart/axis-label.ts +++ b/src/components/chart/axis-label.ts @@ -1,5 +1,4 @@ import type { HassConfig } from "home-assistant-js-websocket"; -import type { XAXisOption } from "echarts/types/dist/shared"; import type { FrontendLocaleData } from "../../data/translation"; import { formatDateMonth, @@ -7,56 +6,46 @@ import { formatDateVeryShort, formatDateWeekdayShort, } from "../../common/datetime/format_date"; -import { formatTime } from "../../common/datetime/format_time"; +import { + formatTime, + formatTimeWithSeconds, +} from "../../common/datetime/format_time"; -export function getLabelFormatter( +export function formatTimeLabel( + value: number | Date, locale: FrontendLocaleData, config: HassConfig, - dayDifference = 0 + minutesDifference: number ) { - return (value: number | Date) => { - const date = new Date(value); - if (dayDifference > 88) { - return date.getMonth() === 0 - ? `{bold|${formatDateMonthYear(date, locale, config)}}` - : formatDateMonth(date, locale, config); - } - if (dayDifference > 35) { - return date.getDate() === 1 - ? `{bold|${formatDateVeryShort(date, locale, config)}}` - : formatDateVeryShort(date, locale, config); - } - if (dayDifference > 7) { - const label = formatDateVeryShort(date, locale, config); - return date.getDate() === 1 ? `{bold|${label}}` : label; - } - if (dayDifference > 2) { - return formatDateWeekdayShort(date, locale, config); - } + const dayDifference = minutesDifference / 60 / 24; + const date = new Date(value); + if (dayDifference > 88) { + return date.getMonth() === 0 + ? `{bold|${formatDateMonthYear(date, locale, config)}}` + : formatDateMonth(date, locale, config); + } + if (dayDifference > 35) { + return date.getDate() === 1 + ? `{bold|${formatDateVeryShort(date, locale, config)}}` + : formatDateVeryShort(date, locale, config); + } + if (dayDifference > 7) { + const label = formatDateVeryShort(date, locale, config); + return date.getDate() === 1 ? `{bold|${label}}` : label; + } + if (dayDifference > 2) { + return formatDateWeekdayShort(date, locale, config); + } + if (minutesDifference && minutesDifference < 5) { + return formatTimeWithSeconds(date, locale, config); + } + if ( + date.getHours() === 0 && + date.getMinutes() === 0 && + date.getSeconds() === 0 + ) { // show only date for the beginning of the day - if ( - date.getHours() === 0 && - date.getMinutes() === 0 && - date.getSeconds() === 0 - ) { - return `{bold|${formatDateVeryShort(date, locale, config)}}`; - } - return formatTime(date, locale, config); - }; -} - -export function getTimeAxisLabelConfig( - locale: FrontendLocaleData, - config: HassConfig, - dayDifference?: number -): XAXisOption["axisLabel"] { - return { - formatter: getLabelFormatter(locale, config, dayDifference), - rich: { - bold: { - fontWeight: "bold", - }, - }, - hideOverlap: true, - }; + return `{bold|${formatDateVeryShort(date, locale, config)}}`; + } + return formatTime(date, locale, config); } diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 85a2e4a538..32e7f729eb 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -2,6 +2,7 @@ import type { PropertyValues } from "lit"; import { css, html, nothing, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; +import { classMap } from "lit/directives/class-map"; import { mdiRestart } from "@mdi/js"; import type { EChartsType } from "echarts/core"; import type { DataZoomComponentOption } from "echarts/components"; @@ -12,6 +13,7 @@ import type { YAXisOption, } from "echarts/types/dist/shared"; import { consume } from "@lit-labs/context"; +import { differenceInMinutes } from "date-fns"; import { fireEvent } from "../../common/dom/fire_event"; import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; @@ -21,6 +23,7 @@ import { listenMediaQuery } from "../../common/dom/media_query"; import type { Themes } from "../../data/ws-themes"; import { themesContext } from "../../data/context"; import { getAllGraphColors } from "../../common/color/colors"; +import { formatTimeLabel } from "./axis-label"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; @@ -45,6 +48,10 @@ export class HaChartBase extends LitElement { @state() private _isZoomed = false; + @state() private _zoomRatio = 1; + + @state() private _minutesDifference = 24 * 60; + private _modifierPressed = false; private _isTouchDevice = "ontouchstart" in window; @@ -152,7 +159,10 @@ export class HaChartBase extends LitElement { protected render() { return html`
+ formatTimeLabel( + value, + this.hass.locale, + this.hass.config, + this._minutesDifference * this._zoomRatio + ); + private async _setupChart() { if (this._loading) return; const container = this.renderRoot.querySelector(".chart") as HTMLDivElement; @@ -199,6 +217,7 @@ export class HaChartBase extends LitElement { this.chart.on("datazoom", (e: any) => { const { start, end } = e.batch?.[0] ?? e; this._isZoomed = start !== 0 || end !== 100; + this._zoomRatio = (end - start) / 100; }); this.chart.on("click", (e: ECElementEvent) => { fireEvent(this, "chart-click", e); @@ -236,6 +255,45 @@ export class HaChartBase extends LitElement { } private _createOptions(): ECOption { + let xAxis = this.options?.xAxis; + if (xAxis && !Array.isArray(xAxis) && xAxis.type === "time") { + if (xAxis.max && xAxis.min) { + this._minutesDifference = differenceInMinutes( + xAxis.max as Date, + xAxis.min as Date + ); + } + const dayDifference = this._minutesDifference / 60 / 24; + let minInterval: number | undefined; + if (dayDifference) { + minInterval = + dayDifference >= 89 // quarter + ? 28 * 3600 * 24 * 1000 + : dayDifference > 2 + ? 3600 * 24 * 1000 + : undefined; + } + xAxis = { + ...xAxis, + axisLabel: { + formatter: this._formatTimeLabel, + rich: { + bold: { + fontWeight: "bold", + }, + }, + hideOverlap: true, + ...xAxis.axisLabel, + }, + axisLine: { + show: false, + }, + splitLine: { + show: true, + }, + minInterval, + } as XAXisOption; + } const options = { animation: !this._reducedMotion, darkMode: this._themes.darkMode ?? false, @@ -244,6 +302,7 @@ export class HaChartBase extends LitElement { }, dataZoom: this._getDataZoomConfig(), ...this.options, + xAxis, }; const isMobile = window.matchMedia( @@ -485,6 +544,9 @@ export class HaChartBase extends LitElement { color: var(--primary-color); border: 1px solid var(--divider-color); } + .has-legend .zoom-reset { + top: 64px; + } `; } diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 01bd1916b1..be9e71e845 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -4,7 +4,6 @@ import { property, state } from "lit/decorators"; import type { VisualMapComponentOption } from "echarts/components"; import type { LineSeriesOption } from "echarts/charts"; import type { YAXisOption } from "echarts/types/dist/shared"; -import { differenceInDays } from "date-fns"; import { styleMap } from "lit/directives/style-map"; import { getGraphColorByIndex } from "../../common/color/colors"; import { computeRTL } from "../../common/util/compute_rtl"; @@ -18,7 +17,6 @@ import { getNumberFormatOptions, formatNumber, } from "../../common/number/format_number"; -import { getTimeAxisLabelConfig } from "./axis-label"; import { measureTextWidth } from "../../util/text"; import { fireEvent } from "../../common/dom/fire_event"; import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate"; @@ -206,7 +204,6 @@ export class StateHistoryChartLine extends LitElement { changedProps.has("paddingYAxis") || changedProps.has("_yWidth") ) { - const dayDifference = differenceInDays(this.endTime, this.startTime); const rtl = computeRTL(this.hass); let minYAxis: number | ((values: { min: number }) => number) | undefined = this.minYAxis; @@ -231,23 +228,6 @@ export class StateHistoryChartLine extends LitElement { type: "time", min: this.startTime, max: this.endTime, - axisLabel: getTimeAxisLabelConfig( - this.hass.locale, - this.hass.config, - dayDifference - ), - axisLine: { - show: false, - }, - splitLine: { - show: true, - }, - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, }, yAxis: { type: this.logarithmicScale ? "log" : "value", diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 4069473b62..39a22c827c 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -8,7 +8,6 @@ import type { TooltipFormatterCallback, TooltipPositionCallbackParams, } from "echarts/types/dist/shared"; -import { differenceInDays } from "date-fns"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration"; import { computeRTL } from "../../common/util/compute_rtl"; @@ -22,7 +21,6 @@ import { luminosity } from "../../common/color/rgb"; import { hex2rgb } from "../../common/color/convert-color"; import { measureTextWidth } from "../../util/text"; import { fireEvent } from "../../common/dom/fire_event"; -import { getTimeAxisLabelConfig } from "./axis-label"; @customElement("state-history-chart-timeline") export class StateHistoryChartTimeline extends LitElement { @@ -191,7 +189,6 @@ export class StateHistoryChartTimeline extends LitElement { : 0; const labelMargin = 5; const rtl = computeRTL(this.hass); - const dayDifference = differenceInDays(this.endTime, this.startTime); this._chartOptions = { xAxis: { type: "time", @@ -203,20 +200,6 @@ export class StateHistoryChartTimeline extends LitElement { splitLine: { show: false, }, - axisLine: { - show: false, - }, - axisLabel: getTimeAxisLabelConfig( - this.hass.locale, - this.hass.config, - dayDifference - ), - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, }, yAxis: { type: "category", diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 2dd3fd68c5..62090d23ac 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -30,7 +30,6 @@ import { getNumberFormatOptions, } from "../../common/number/format_number"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; -import { getTimeAxisLabelConfig } from "./axis-label"; export const supportedStatTypeMap: Record = { mean: "mean", @@ -216,7 +215,6 @@ export class StatisticsChart extends LitElement { }; private _createOptions() { - const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {}; const dayDifference = this.daysToShow ?? 1; let minYAxis: number | ((values: { min: number }) => number) | undefined = this.minYAxis; @@ -236,29 +234,32 @@ export class StatisticsChart extends LitElement { } else if (this.logarithmicScale) { maxYAxis = ({ max }) => (max > 0 ? max * 1.05 : max * 0.95); } + const endTime = this.endTime ?? new Date(); + let startTime = this.startTime; + + if (!startTime) { + // Calculate default start time based on dayDifference + startTime = new Date( + endTime.getTime() - dayDifference * 24 * 3600 * 1000 + ); + + // Check chart data for earlier points + this._chartData.forEach((series) => { + if (!Array.isArray(series.data)) return; + series.data.forEach((point) => { + const timestamp = Array.isArray(point) ? point[0] : point.value?.[0]; + if (new Date(timestamp) < startTime!) { + startTime = new Date(timestamp); + } + }); + }); + } + this._chartOptions = { xAxis: { type: "time", - axisLabel: getTimeAxisLabelConfig( - this.hass.locale, - this.hass.config, - dayDifference - ), - min: this.startTime, - max: this.endTime, - axisLine: { - show: false, - }, - splitLine: { - show: true, - lineStyle: splitLineStyle, - }, - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, + min: startTime, + max: endTime, }, yAxis: { type: this.logarithmicScale ? "log" : "value", @@ -274,7 +275,6 @@ export class StatisticsChart extends LitElement { max: this._clampYAxis(maxYAxis), splitLine: { show: true, - lineStyle: splitLineStyle, }, }, legend: { @@ -348,6 +348,7 @@ export class StatisticsChart extends LitElement { if (endTime > new Date()) { endTime = new Date(); } + this.endTime = endTime; let unit: string | undefined | null; diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 004bcb0cd1..12c14def9a 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -39,7 +39,6 @@ import { hardwareBrandsUrl } from "../../../util/brands-url"; import { showhardwareAvailableDialog } from "./show-dialog-hardware-available"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import type { ECOption } from "../../../resources/echarts"; -import { getTimeAxisLabelConfig } from "../../../components/chart/axis-label"; const DATASAMPLES = 60; @@ -153,13 +152,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { this._chartOptions = { xAxis: { type: "time", - axisLabel: getTimeAxisLabelConfig(this.hass.locale, this.hass.config), - splitLine: { - show: true, - }, - axisLine: { - show: false, - }, }, yAxis: { type: "value", 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 3619c731d3..76dde725aa 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -10,7 +10,6 @@ import { formatNumber } from "../../../../../common/number/format_number"; import { formatDateVeryShort } from "../../../../../common/datetime/format_date"; import { formatTime } from "../../../../../common/datetime/format_time"; import type { ECOption } from "../../../../../resources/echarts"; -import { getTimeAxisLabelConfig } from "../../../../../components/chart/axis-label"; export function getSuggestedMax(dayDifference: number, end: Date): number { let suggestedMax = new Date(end); @@ -52,23 +51,9 @@ export function getCommonOptions( const options: ECOption = { xAxis: { - id: "xAxisMain", type: "time", - min: start.getTime(), - max: getSuggestedMax(dayDifference, end), - axisLabel: getTimeAxisLabelConfig(locale, config, dayDifference), - axisLine: { - show: false, - }, - splitLine: { - show: true, - }, - minInterval: - dayDifference >= 89 // quarter - ? 28 * 3600 * 24 * 1000 - : dayDifference > 2 - ? 3600 * 24 * 1000 - : undefined, + min: start, + max: end, }, yAxis: { type: "value",