diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index ccd628260a..898b5ec34f 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -7,12 +7,15 @@ import type { EChartsType } from "echarts/core"; import type { DataZoomComponentOption } from "echarts/components"; import { ResizeController } from "@lit-labs/observers/resize-controller"; import type { XAXisOption, YAXisOption } from "echarts/types/dist/shared"; +import { consume } from "@lit-labs/context"; import { fireEvent } from "../../common/dom/fire_event"; import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; import "../ha-icon-button"; import type { ECOption } from "../../resources/echarts"; import { listenMediaQuery } from "../../common/dom/media_query"; +import type { Themes } from "../../data/ws-themes"; +import { themesContext } from "../../data/context"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; @@ -31,6 +34,10 @@ export class HaChartBase extends LitElement { @property({ attribute: "external-hidden", type: Boolean }) public externalHidden = false; + @state() + @consume({ context: themesContext, subscribe: true }) + _themes!: Themes; + @state() private _isZoomed = false; private _modifierPressed = false; @@ -110,6 +117,10 @@ export class HaChartBase extends LitElement { if (!this.hasUpdated || !this.chart) { return; } + if (changedProps.has("_themes")) { + this._setupChart(); + return; + } if (changedProps.has("data")) { this.chart.setOption( { series: this.data }, @@ -163,9 +174,15 @@ export class HaChartBase extends LitElement { const container = this.renderRoot.querySelector(".chart") as HTMLDivElement; this._loading = true; try { + if (this.chart) { + this.chart.dispose(); + } const echarts = (await import("../../resources/echarts")).default; - this.chart = echarts.init(container); + this.chart = echarts.init( + container, + this._themes.darkMode ? "dark" : "light" + ); this.chart.on("legendselectchanged", (params: any) => { if (this.externalHidden) { const isSelected = params.selected[params.name]; @@ -207,17 +224,10 @@ export class HaChartBase extends LitElement { } private _createOptions(): ECOption { - const darkMode = this.hass.themes?.darkMode ?? false; - const xAxis = Array.isArray(this.options?.xAxis) - ? this.options?.xAxis - : [this.options?.xAxis]; - const yAxis = Array.isArray(this.options?.yAxis) - ? this.options?.yAxis - : [this.options?.yAxis]; - // we should create our own theme but this is a quick fix for now - const splitLineStyle = darkMode ? { color: "#333" } : {}; + const darkMode = this._themes.darkMode ?? false; const options = { + backgroundColor: "transparent", animation: !this._reducedMotion, darkMode, aria: { @@ -225,32 +235,13 @@ export class HaChartBase extends LitElement { }, dataZoom: this._getDataZoomConfig(), ...this.options, - xAxis: xAxis.map((axis) => - axis - ? { - ...axis, - splitLine: axis.splitLine - ? { - ...axis.splitLine, - lineStyle: splitLineStyle, - } - : undefined, - } - : undefined - ) as XAXisOption[], - yAxis: yAxis.map((axis) => - axis - ? { - ...axis, - splitLine: axis.splitLine - ? { - ...axis.splitLine, - lineStyle: splitLineStyle, - } - : undefined, - } - : undefined - ) as YAXisOption[], + legend: this.options?.legend + ? { + // we should create our own theme but this is a quick fix for now + inactiveColor: darkMode ? "#444" : "#ccc", + ...this.options.legend, + } + : undefined, }; const isMobile = window.matchMedia( diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index a9c49582c8..3e7c42ec00 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -192,7 +192,10 @@ export class StateHistoryChartLine extends LitElement { max: this.fitYData ? this.maxYAxis : undefined, position: rtl ? "right" : "left", scale: true, - nameGap: 3, + nameGap: 2, + nameTextStyle: { + align: "left", + }, splitLine: { show: true, lineStyle: splitLineStyle, diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 4dae7ce189..1612c0d03b 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -154,10 +154,6 @@ export class StateHistoryChartTimeline extends LitElement { }; public willUpdate(changedProps: PropertyValues) { - if (!this.hasUpdated) { - this._createOptions(); - } - if ( changedProps.has("startTime") || changedProps.has("endTime") || @@ -171,10 +167,12 @@ export class StateHistoryChartTimeline extends LitElement { } if ( + !this.hasUpdated || changedProps.has("startTime") || changedProps.has("endTime") || changedProps.has("showNames") || - changedProps.has("paddingYAxis") + changedProps.has("paddingYAxis") || + changedProps.has("_yWidth") ) { this._createOptions(); } @@ -183,9 +181,11 @@ export class StateHistoryChartTimeline extends LitElement { private _createOptions() { const narrow = this.narrow; const showNames = this.chunked || this.showNames; + const maxInternalLabelWidth = narrow ? 70 : 165; const labelWidth = showNames - ? Math.max(narrow ? 70 : 170, this.paddingYAxis) + ? Math.max(this.paddingYAxis, this._yWidth) : 0; + const labelMargin = 5; const rtl = computeRTL(this.hass); const dayDifference = differenceInDays(this.endTime, this.startTime); this._chartOptions = { @@ -193,6 +193,12 @@ export class StateHistoryChartTimeline extends LitElement { type: "time", min: this.startTime, max: this.endTime, + axisTick: { + show: true, + lineStyle: { + opacity: 0.4, + }, + }, axisLabel: getTimeAxisLabelConfig( this.hass.locale, this.hass.config, @@ -217,12 +223,14 @@ export class StateHistoryChartTimeline extends LitElement { }, axisLabel: { show: showNames, - width: labelWidth, + width: labelWidth - labelMargin, overflow: "truncate", - margin: 5, - // @ts-ignore this is a valid option + margin: labelMargin, formatter: (label: string) => { - const width = Math.min(measureTextWidth(label, 12) + 5, labelWidth); + const width = Math.min( + measureTextWidth(label, 12) + labelMargin, + maxInternalLabelWidth + ); if (width > this._yWidth) { this._yWidth = width; fireEvent(this, "y-width-changed", { diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index 0c20746206..b909fc1d09 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -2,7 +2,6 @@ import type { PropertyValues } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, eventOptions, property, state } from "lit/decorators"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; -import { styleMap } from "lit/directives/style-map"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { restoreScroll } from "../../common/decorators/restore-scroll"; import type { @@ -136,10 +135,7 @@ export class StateHistoryCharts extends LitElement { return html``; } if (!Array.isArray(item)) { - return html`
+ return html`
`; } @@ -281,7 +277,8 @@ export class StateHistoryCharts extends LitElement { static styles = css` :host { - display: block; + display: flex; + flex-direction: column; /* height of single timeline chart = 60px */ min-height: 60px; } @@ -302,6 +299,7 @@ export class StateHistoryCharts extends LitElement { .entry-container { width: 100%; + flex: 1; } .entry-container:hover { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 78fafaf434..efd150f91e 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -96,6 +96,8 @@ export class StatisticsChart extends LitElement { @state() private _chartOptions?: ECOption; + @state() private _hiddenStats = new Set(); + private _computedStyle?: CSSStyleDeclaration; protected shouldUpdate(changedProps: PropertyValues): boolean { @@ -107,7 +109,8 @@ export class StatisticsChart extends LitElement { changedProps.has("statisticsData") || changedProps.has("statTypes") || changedProps.has("chartType") || - changedProps.has("hideLegend") + changedProps.has("hideLegend") || + changedProps.has("_hiddenStats") ) { this._generateData(); } @@ -120,7 +123,8 @@ export class StatisticsChart extends LitElement { changedProps.has("maxYAxis") || changedProps.has("fitYData") || changedProps.has("logarithmicScale") || - changedProps.has("hideLegend") + changedProps.has("hideLegend") || + changedProps.has("_legendData") ) { this._createOptions(); } @@ -160,10 +164,23 @@ export class StatisticsChart extends LitElement { .options=${this._chartOptions} .height=${this.height} style=${styleMap({ height: this.height })} + external-hidden + @dataset-hidden=${this._datasetHidden} + @dataset-unhidden=${this._datasetUnhidden} > `; } + private _datasetHidden(ev: CustomEvent) { + this._hiddenStats.add(ev.detail.name); + this.requestUpdate("_hiddenStats"); + } + + private _datasetUnhidden(ev: CustomEvent) { + this._hiddenStats.delete(ev.detail.name); + this.requestUpdate("_hiddenStats"); + } + private _renderTooltip(params: any) { return params .map((param, index: number) => { @@ -219,6 +236,10 @@ export class StatisticsChart extends LitElement { yAxis: { type: this.logarithmicScale ? "log" : "value", name: this.unit, + nameGap: 2, + nameTextStyle: { + align: "left", + }, position: computeRTL(this.hass) ? "right" : "left", // @ts-ignore scale: this.chartType !== "bar", @@ -247,13 +268,6 @@ export class StatisticsChart extends LitElement { appendTo: document.body, formatter: this._renderTooltip.bind(this), }, - // scales: { - // x: { - // ticks: { - // source: this.chartType === "bar" ? "data" : undefined, - // }, - // }, - // }, }; } @@ -282,8 +296,8 @@ export class StatisticsChart extends LitElement { let colorIndex = 0; const statisticsData = Object.entries(this.statisticsData); - const totalDataSets: LineSeriesOption[] = []; - const legendData: string[] = []; + const totalDataSets: typeof this._chartData = []; + const legendData: { name: string; color: string }[] = []; const statisticIds: string[] = []; let endTime: Date; @@ -334,7 +348,7 @@ export class StatisticsChart extends LitElement { // The datasets for the current statistic const statDataSets: (LineSeriesOption | BarSeriesOption)[] = []; - const statLegendData: string[] = []; + const statLegendData: { name: string; color: string }[] = []; const pushData = ( start: Date, @@ -403,7 +417,7 @@ export class StatisticsChart extends LitElement { ? type === "mean" : displayedLegend === false; if (showLegend) { - statLegendData.push(name); + statLegendData.push({ name, color }); } displayedLegend = displayedLegend || showLegend; } @@ -415,8 +429,7 @@ export class StatisticsChart extends LitElement { name: name ? `${name} (${this.hass.localize( `ui.components.statistics_charts.statistic_types.${type}` - )}) - ` + )})` : this.hass.localize( `ui.components.statistics_charts.statistic_types.${type}` ), @@ -472,7 +485,9 @@ export class StatisticsChart extends LitElement { } dataValues.push(val); }); - pushData(startDate, new Date(stat.end), dataValues); + if (!this._hiddenStats.has(name)) { + pushData(startDate, new Date(stat.end), dataValues); + } }); // Concat two arrays @@ -484,8 +499,22 @@ export class StatisticsChart extends LitElement { this.unit = unit; } + legendData.forEach(({ name, color }) => { + // Add an empty series for the legend + totalDataSets.push({ + id: name + "-legend", + name: name, + color, + type: this.chartType, + data: [], + }); + }); + this._chartData = totalDataSets; - this._legendData = legendData; + if (legendData.length !== this._legendData.length) { + // only update the legend if it has changed or it will trigger options update + this._legendData = legendData.map(({ name }) => name); + } this._statisticIds = statisticIds; } diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts index f6a75d082b..9f130c9829 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-detail-graph-card.ts @@ -326,14 +326,12 @@ export class HuiEnergyDevicesDetailGraphCard Object.keys(consumptionData.total).forEach((time) => { const value = consumptionData.total[time] - (totalDeviceConsumption[time] || 0); - if (value > 0) { - const dataPoint = [Number(time), value]; - if (compare) { - dataPoint[2] = dataPoint[0]; - dataPoint[0] += compareOffset; - } - untrackedConsumption.push(dataPoint); + const dataPoint = [Number(time), value]; + if (compare) { + dataPoint[2] = dataPoint[0]; + dataPoint[0] += compareOffset; } + untrackedConsumption.push(dataPoint); }); const dataset: BarSeriesOption = { type: "bar", diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index ac22ad0c97..b77360cef8 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -64,7 +64,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { getGridOptions(): LovelaceGridOptions { return { columns: 12, - rows: 6, min_columns: 6, min_rows: this._config?.entities?.length || 1, }; @@ -244,6 +243,9 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { start_date: now.toISOString(), })}`; + const columns = this._config.grid_options?.columns ?? 12; + const narrow = Number.isNaN(columns) || Number(columns) < 12; + return html` ${this._config.title @@ -281,7 +283,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { .minYAxis=${this._config.min_y_axis} .maxYAxis=${this._config.max_y_axis} .fitYData=${this._config.fit_y_data || false} - height="100%" + .height=${this._config.grid_options?.rows + ? "100%" + : undefined} + .narrow=${narrow} > `}
diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 0376d3811b..1ecde26862 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -131,7 +131,6 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { getGridOptions(): LovelaceGridOptions { return { columns: 12, - rows: 5, min_columns: 6, min_rows: 3, }; @@ -279,7 +278,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { .hideLegend=${this._config.hide_legend || false} .logarithmicScale=${this._config.logarithmic_scale || false} .daysToShow=${this._config.days_to_show || DEFAULT_DAYS_TO_SHOW} - height="100%" + .height=${this._config.grid_options?.rows ? "100%" : undefined} > diff --git a/src/state/logging-mixin.ts b/src/state/logging-mixin.ts index a019a9d7b6..79e07ccd63 100644 --- a/src/state/logging-mixin.ts +++ b/src/state/logging-mixin.ts @@ -29,11 +29,21 @@ export const loggingMixin = >( return; } if ( - !__DEV__ && - (ev.message.includes("ResizeObserver loop limit exceeded") || - ev.message.includes( - "ResizeObserver loop completed with undelivered notifications" - )) + // !__DEV__ && + ev.message.includes("ResizeObserver loop limit exceeded") || + ev.message.includes( + "ResizeObserver loop completed with undelivered notifications" + ) || + (ev.error.stack.includes("echarts") && + (ev.message.includes( + "Cannot read properties of undefined (reading 'hostedBy')" + ) || + ev.message.includes( + "Cannot read properties of undefined (reading 'scale')" + ) || + ev.message.includes( + "Cannot read properties of null (reading 'innerHTML')" + ))) ) { ev.preventDefault(); ev.stopImmediatePropagation(); diff --git a/src/util/text.ts b/src/util/text.ts index e51b2eef0c..b332c2ec0a 100644 --- a/src/util/text.ts +++ b/src/util/text.ts @@ -21,5 +21,5 @@ export function measureTextWidth( } context.font = `${fontSize}px ${fontFamily}`; - return context.measureText(text).width; + return Math.ceil(context.measureText(text).width); }