Show seconds on x axis when chart is zoomed a lot (#24043)

Show seconds on x axis when charts is zoomed a lot
This commit is contained in:
Petar Petrov 2025-02-03 14:27:33 +02:00 committed by Bram Kragten
parent 6eb43a7d61
commit 4698a63642
7 changed files with 126 additions and 134 deletions

View File

@ -1,5 +1,4 @@
import type { HassConfig } from "home-assistant-js-websocket"; import type { HassConfig } from "home-assistant-js-websocket";
import type { XAXisOption } from "echarts/types/dist/shared";
import type { FrontendLocaleData } from "../../data/translation"; import type { FrontendLocaleData } from "../../data/translation";
import { import {
formatDateMonth, formatDateMonth,
@ -7,56 +6,46 @@ import {
formatDateVeryShort, formatDateVeryShort,
formatDateWeekdayShort, formatDateWeekdayShort,
} from "../../common/datetime/format_date"; } 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, locale: FrontendLocaleData,
config: HassConfig, config: HassConfig,
dayDifference = 0 minutesDifference: number
) { ) {
return (value: number | Date) => { const dayDifference = minutesDifference / 60 / 24;
const date = new Date(value); const date = new Date(value);
if (dayDifference > 88) { if (dayDifference > 88) {
return date.getMonth() === 0 return date.getMonth() === 0
? `{bold|${formatDateMonthYear(date, locale, config)}}` ? `{bold|${formatDateMonthYear(date, locale, config)}}`
: formatDateMonth(date, locale, config); : formatDateMonth(date, locale, config);
} }
if (dayDifference > 35) { if (dayDifference > 35) {
return date.getDate() === 1 return date.getDate() === 1
? `{bold|${formatDateVeryShort(date, locale, config)}}` ? `{bold|${formatDateVeryShort(date, locale, config)}}`
: formatDateVeryShort(date, locale, config); : formatDateVeryShort(date, locale, config);
} }
if (dayDifference > 7) { if (dayDifference > 7) {
const label = formatDateVeryShort(date, locale, config); const label = formatDateVeryShort(date, locale, config);
return date.getDate() === 1 ? `{bold|${label}}` : label; return date.getDate() === 1 ? `{bold|${label}}` : label;
} }
if (dayDifference > 2) { if (dayDifference > 2) {
return formatDateWeekdayShort(date, locale, config); 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 // show only date for the beginning of the day
if ( return `{bold|${formatDateVeryShort(date, locale, config)}}`;
date.getHours() === 0 && }
date.getMinutes() === 0 && return formatTime(date, locale, config);
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,
};
} }

View File

@ -2,6 +2,7 @@ import type { PropertyValues } from "lit";
import { css, html, nothing, LitElement } from "lit"; import { css, html, nothing, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { classMap } from "lit/directives/class-map";
import { mdiRestart } from "@mdi/js"; import { mdiRestart } from "@mdi/js";
import type { EChartsType } from "echarts/core"; import type { EChartsType } from "echarts/core";
import type { DataZoomComponentOption } from "echarts/components"; import type { DataZoomComponentOption } from "echarts/components";
@ -12,6 +13,7 @@ import type {
YAXisOption, YAXisOption,
} from "echarts/types/dist/shared"; } from "echarts/types/dist/shared";
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import { differenceInMinutes } from "date-fns";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac"; 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 type { Themes } from "../../data/ws-themes";
import { themesContext } from "../../data/context"; import { themesContext } from "../../data/context";
import { getAllGraphColors } from "../../common/color/colors"; import { getAllGraphColors } from "../../common/color/colors";
import { formatTimeLabel } from "./axis-label";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@ -45,6 +48,10 @@ export class HaChartBase extends LitElement {
@state() private _isZoomed = false; @state() private _isZoomed = false;
@state() private _zoomRatio = 1;
@state() private _minutesDifference = 24 * 60;
private _modifierPressed = false; private _modifierPressed = false;
private _isTouchDevice = "ontouchstart" in window; private _isTouchDevice = "ontouchstart" in window;
@ -152,7 +159,10 @@ export class HaChartBase extends LitElement {
protected render() { protected render() {
return html` return html`
<div <div
class="chart-container" class=${classMap({
"chart-container": true,
"has-legend": !!this.options?.legend,
})}
style=${styleMap({ style=${styleMap({
height: this.height ?? `${this._getDefaultHeight()}px`, height: this.height ?? `${this._getDefaultHeight()}px`,
})} })}
@ -173,6 +183,14 @@ export class HaChartBase extends LitElement {
`; `;
} }
private _formatTimeLabel = (value: number | Date) =>
formatTimeLabel(
value,
this.hass.locale,
this.hass.config,
this._minutesDifference * this._zoomRatio
);
private async _setupChart() { private async _setupChart() {
if (this._loading) return; if (this._loading) return;
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement; const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
@ -199,6 +217,7 @@ export class HaChartBase extends LitElement {
this.chart.on("datazoom", (e: any) => { this.chart.on("datazoom", (e: any) => {
const { start, end } = e.batch?.[0] ?? e; const { start, end } = e.batch?.[0] ?? e;
this._isZoomed = start !== 0 || end !== 100; this._isZoomed = start !== 0 || end !== 100;
this._zoomRatio = (end - start) / 100;
}); });
this.chart.on("click", (e: ECElementEvent) => { this.chart.on("click", (e: ECElementEvent) => {
fireEvent(this, "chart-click", e); fireEvent(this, "chart-click", e);
@ -236,6 +255,45 @@ export class HaChartBase extends LitElement {
} }
private _createOptions(): ECOption { 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 = { const options = {
animation: !this._reducedMotion, animation: !this._reducedMotion,
darkMode: this._themes.darkMode ?? false, darkMode: this._themes.darkMode ?? false,
@ -244,6 +302,7 @@ export class HaChartBase extends LitElement {
}, },
dataZoom: this._getDataZoomConfig(), dataZoom: this._getDataZoomConfig(),
...this.options, ...this.options,
xAxis,
}; };
const isMobile = window.matchMedia( const isMobile = window.matchMedia(
@ -485,6 +544,9 @@ export class HaChartBase extends LitElement {
color: var(--primary-color); color: var(--primary-color);
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
} }
.has-legend .zoom-reset {
top: 64px;
}
`; `;
} }

View File

@ -4,7 +4,6 @@ import { property, state } from "lit/decorators";
import type { VisualMapComponentOption } from "echarts/components"; import type { VisualMapComponentOption } from "echarts/components";
import type { LineSeriesOption } from "echarts/charts"; import type { LineSeriesOption } from "echarts/charts";
import type { YAXisOption } from "echarts/types/dist/shared"; import type { YAXisOption } from "echarts/types/dist/shared";
import { differenceInDays } from "date-fns";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { getGraphColorByIndex } from "../../common/color/colors"; import { getGraphColorByIndex } from "../../common/color/colors";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
@ -18,7 +17,6 @@ import {
getNumberFormatOptions, getNumberFormatOptions,
formatNumber, formatNumber,
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { getTimeAxisLabelConfig } from "./axis-label";
import { measureTextWidth } from "../../util/text"; import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate"; import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
@ -206,7 +204,6 @@ export class StateHistoryChartLine extends LitElement {
changedProps.has("paddingYAxis") || changedProps.has("paddingYAxis") ||
changedProps.has("_yWidth") changedProps.has("_yWidth")
) { ) {
const dayDifference = differenceInDays(this.endTime, this.startTime);
const rtl = computeRTL(this.hass); const rtl = computeRTL(this.hass);
let minYAxis: number | ((values: { min: number }) => number) | undefined = let minYAxis: number | ((values: { min: number }) => number) | undefined =
this.minYAxis; this.minYAxis;
@ -231,23 +228,6 @@ export class StateHistoryChartLine extends LitElement {
type: "time", type: "time",
min: this.startTime, min: this.startTime,
max: this.endTime, 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: { yAxis: {
type: this.logarithmicScale ? "log" : "value", type: this.logarithmicScale ? "log" : "value",

View File

@ -8,7 +8,6 @@ import type {
TooltipFormatterCallback, TooltipFormatterCallback,
TooltipPositionCallbackParams, TooltipPositionCallbackParams,
} from "echarts/types/dist/shared"; } from "echarts/types/dist/shared";
import { differenceInDays } from "date-fns";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration"; import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
import { computeRTL } from "../../common/util/compute_rtl"; 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 { hex2rgb } from "../../common/color/convert-color";
import { measureTextWidth } from "../../util/text"; import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { getTimeAxisLabelConfig } from "./axis-label";
@customElement("state-history-chart-timeline") @customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement { export class StateHistoryChartTimeline extends LitElement {
@ -191,7 +189,6 @@ export class StateHistoryChartTimeline extends LitElement {
: 0; : 0;
const labelMargin = 5; const labelMargin = 5;
const rtl = computeRTL(this.hass); const rtl = computeRTL(this.hass);
const dayDifference = differenceInDays(this.endTime, this.startTime);
this._chartOptions = { this._chartOptions = {
xAxis: { xAxis: {
type: "time", type: "time",
@ -203,20 +200,6 @@ export class StateHistoryChartTimeline extends LitElement {
splitLine: { splitLine: {
show: false, 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: { yAxis: {
type: "category", type: "category",

View File

@ -30,7 +30,6 @@ import {
getNumberFormatOptions, getNumberFormatOptions,
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { getTimeAxisLabelConfig } from "./axis-label";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = { export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean", mean: "mean",
@ -216,7 +215,6 @@ export class StatisticsChart extends LitElement {
}; };
private _createOptions() { private _createOptions() {
const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {};
const dayDifference = this.daysToShow ?? 1; const dayDifference = this.daysToShow ?? 1;
let minYAxis: number | ((values: { min: number }) => number) | undefined = let minYAxis: number | ((values: { min: number }) => number) | undefined =
this.minYAxis; this.minYAxis;
@ -236,29 +234,32 @@ export class StatisticsChart extends LitElement {
} else if (this.logarithmicScale) { } else if (this.logarithmicScale) {
maxYAxis = ({ max }) => (max > 0 ? max * 1.05 : max * 0.95); 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 = { this._chartOptions = {
xAxis: { xAxis: {
type: "time", type: "time",
axisLabel: getTimeAxisLabelConfig( min: startTime,
this.hass.locale, max: endTime,
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,
}, },
yAxis: { yAxis: {
type: this.logarithmicScale ? "log" : "value", type: this.logarithmicScale ? "log" : "value",
@ -274,7 +275,6 @@ export class StatisticsChart extends LitElement {
max: this._clampYAxis(maxYAxis), max: this._clampYAxis(maxYAxis),
splitLine: { splitLine: {
show: true, show: true,
lineStyle: splitLineStyle,
}, },
}, },
legend: { legend: {
@ -348,6 +348,7 @@ export class StatisticsChart extends LitElement {
if (endTime > new Date()) { if (endTime > new Date()) {
endTime = new Date(); endTime = new Date();
} }
this.endTime = endTime;
let unit: string | undefined | null; let unit: string | undefined | null;

View File

@ -39,7 +39,6 @@ import { hardwareBrandsUrl } from "../../../util/brands-url";
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available"; import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import type { ECOption } from "../../../resources/echarts"; import type { ECOption } from "../../../resources/echarts";
import { getTimeAxisLabelConfig } from "../../../components/chart/axis-label";
const DATASAMPLES = 60; const DATASAMPLES = 60;
@ -153,13 +152,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
this._chartOptions = { this._chartOptions = {
xAxis: { xAxis: {
type: "time", type: "time",
axisLabel: getTimeAxisLabelConfig(this.hass.locale, this.hass.config),
splitLine: {
show: true,
},
axisLine: {
show: false,
},
}, },
yAxis: { yAxis: {
type: "value", type: "value",

View File

@ -10,7 +10,6 @@ import { formatNumber } from "../../../../../common/number/format_number";
import { formatDateVeryShort } from "../../../../../common/datetime/format_date"; import { formatDateVeryShort } from "../../../../../common/datetime/format_date";
import { formatTime } from "../../../../../common/datetime/format_time"; import { formatTime } from "../../../../../common/datetime/format_time";
import type { ECOption } from "../../../../../resources/echarts"; import type { ECOption } from "../../../../../resources/echarts";
import { getTimeAxisLabelConfig } from "../../../../../components/chart/axis-label";
export function getSuggestedMax(dayDifference: number, end: Date): number { export function getSuggestedMax(dayDifference: number, end: Date): number {
let suggestedMax = new Date(end); let suggestedMax = new Date(end);
@ -52,23 +51,9 @@ export function getCommonOptions(
const options: ECOption = { const options: ECOption = {
xAxis: { xAxis: {
id: "xAxisMain",
type: "time", type: "time",
min: start.getTime(), min: start,
max: getSuggestedMax(dayDifference, end), max: 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,
}, },
yAxis: { yAxis: {
type: "value", type: "value",