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