mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 19:26:36 +00:00
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:
parent
e1bda9b57d
commit
cc48ae82d6
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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", {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -29,11 +29,21 @@ export const loggingMixin = <T extends Constructor<HassBaseEl>>(
|
||||
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();
|
||||
|
@ -21,5 +21,5 @@ export function measureTextWidth(
|
||||
}
|
||||
|
||||
context.font = `${fontSize}px ${fontFamily}`;
|
||||
return context.measureText(text).width;
|
||||
return Math.ceil(context.measureText(text).width);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user