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 { 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);
}

View File

@ -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`
<div
class="chart-container"
class=${classMap({
"chart-container": true,
"has-legend": !!this.options?.legend,
})}
style=${styleMap({
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() {
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;
}
`;
}

View File

@ -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",

View File

@ -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",

View File

@ -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<StatisticType, StatisticType> = {
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;

View File

@ -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",

View File

@ -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",