Fixes for echarts (#23906)

* show negative untracked energy again

* fix chart cards height

* fix timeline label width

* fix statistics chart legend

* fix layout of chart cards

* tweak timeline chart labels

* timeline label tweak

* css tweak

* fix legend colors in statistics chart

* dark mode fix

* fix for y axis with a long name

* listen for darkMode changes and update charts

* legend tweak for darkMode

* dark mode tweak

* hide insignificant echarts errors for now
This commit is contained in:
Petar Petrov 2025-01-28 16:20:34 +02:00 committed by GitHub
parent e1bda9b57d
commit cc48ae82d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 129 additions and 88 deletions

View File

@ -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
legend: this.options?.legend
? {
...axis,
splitLine: axis.splitLine
? {
...axis.splitLine,
lineStyle: splitLineStyle,
// we should create our own theme but this is a quick fix for now
inactiveColor: darkMode ? "#444" : "#ccc",
...this.options.legend,
}
: undefined,
}
: undefined
) as XAXisOption[],
yAxis: yAxis.map((axis) =>
axis
? {
...axis,
splitLine: axis.splitLine
? {
...axis.splitLine,
lineStyle: splitLineStyle,
}
: undefined,
}
: undefined
) as YAXisOption[],
};
const isMobile = window.matchMedia(

View File

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

View File

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

View File

@ -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`<div
class="entry-container"
style=${styleMap({ height: this.height })}
>
return html`<div class="entry-container">
<state-history-chart-line
.hass=${this.hass}
.unit=${item.unit}
@ -157,7 +153,7 @@ export class StateHistoryCharts extends LitElement {
.maxYAxis=${this.maxYAxis}
.fitYData=${this.fitYData}
@y-width-changed=${this._yWidthChanged}
.height=${this.height}
.height=${this.virtualize ? undefined : this.height}
></state-history-chart-line>
</div> `;
}
@ -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 {

View File

@ -96,6 +96,8 @@ export class StatisticsChart extends LitElement {
@state() private _chartOptions?: ECOption;
@state() private _hiddenStats = new Set<string>();
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}
></ha-chart-base>
`;
}
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);
});
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;
}

View File

@ -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 dataset: BarSeriesOption = {
type: "bar",

View File

@ -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`
<ha-card>
${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}
></state-history-charts>
`}
</div>

View File

@ -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}
></statistics-chart>
</div>
</ha-card>

View File

@ -29,11 +29,21 @@ export const loggingMixin = <T extends Constructor<HassBaseEl>>(
return;
}
if (
!__DEV__ &&
(ev.message.includes("ResizeObserver loop limit exceeded") ||
// !__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();

View File

@ -21,5 +21,5 @@ export function measureTextWidth(
}
context.font = `${fontSize}px ${fontFamily}`;
return context.measureText(text).width;
return Math.ceil(context.measureText(text).width);
}