diff --git a/package.json b/package.json index 2dc08e8e86..8452d18e26 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/webcomponentsjs": "2.8.0", "app-datepicker": "5.1.1", - "chart.js": "3.3.2", + "chart.js": "4.3.3", "comlink": "4.4.1", "core-js": "3.32.1", "cropperjs": "1.6.0", diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 389fd8dab6..7d620b3a7f 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -15,13 +15,20 @@ import { HomeAssistant } from "../../types"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; -interface Tooltip extends TooltipModel { +export interface ChartResizeOptions { + aspectRatio?: number; + height?: number; + width?: number; +} + +interface Tooltip + extends Omit, "tooltipPosition" | "hasValue" | "getProps"> { top: string; left: string; } @customElement("ha-chart-base") -export default class HaChartBase extends LitElement { +export class HaChartBase extends LitElement { public chart?: Chart; @property({ attribute: false }) public hass!: HomeAssistant; @@ -45,14 +52,6 @@ export default class HaChartBase extends LitElement { @state() private _hiddenDatasets: Set = new Set(); - private _releaseCanvas() { - // release the canvas memory to prevent - // safari from running out of memory. - if (this.chart) { - this.chart.destroy(); - } - } - public disconnectedCallback() { this._releaseCanvas(); super.disconnectedCallback(); @@ -65,6 +64,36 @@ export default class HaChartBase extends LitElement { } } + public updateChart = ( + mode: + | "resize" + | "reset" + | "none" + | "hide" + | "show" + | "default" + | "active" + | undefined + ): void => { + this.chart?.update(mode); + }; + + public resize = (options?: ChartResizeOptions): void => { + if (options?.aspectRatio && !options.height) { + options.height = Math.round( + (options.width ?? this.clientWidth) / options.aspectRatio + ); + } else if (options?.aspectRatio && !options.width) { + options.width = Math.round( + (options.height ?? this.clientHeight) * options.aspectRatio + ); + } + this.chart?.resize( + options?.width ?? this.clientWidth, + options?.height ?? this.clientHeight + ); + }; + protected firstUpdated() { this._setupChart(); this.data.datasets.forEach((dataset, index) => { @@ -80,14 +109,11 @@ export default class HaChartBase extends LitElement { if (!this.hasUpdated || !this.chart) { return; } - if (changedProps.has("plugins")) { + if (changedProps.has("plugins") || changedProps.has("chartType")) { this.chart.destroy(); this._setupChart(); return; } - if (changedProps.has("chartType")) { - this.chart.config.type = this.chartType; - } if (changedProps.has("data")) { if (this._hiddenDatasets.size) { this.data.datasets.forEach((dataset, index) => { @@ -131,55 +157,70 @@ export default class HaChartBase extends LitElement { ` : ""}
- - ${this._tooltip - ? html`
-
${this._tooltip.title}
- ${this._tooltip.beforeBody - ? html`
- ${this._tooltip.beforeBody} -
` - : ""} -
-
    - ${this._tooltip.body.map( - (item, i) => - html`
  • -
    - ${item.lines.join("\n")} -
  • ` - )} -
-
- ${this._tooltip.footer.length - ? html`` - : ""} -
` - : ""} +
+ + ${this._tooltip + ? html`
+
${this._tooltip.title}
+ ${this._tooltip.beforeBody + ? html`
+ ${this._tooltip.beforeBody} +
` + : ""} +
+
    + ${this._tooltip.body.map( + (item, i) => + html`
  • +
    + ${item.lines.join("\n")} +
  • ` + )} +
+
+ ${this._tooltip.footer.length + ? html`` + : ""} +
` + : ""} +
`; } @@ -213,6 +254,7 @@ export default class HaChartBase extends LitElement { private _createOptions() { return { + maintainAspectRatio: false, ...this.options, plugins: { ...this.options?.plugins, @@ -233,10 +275,10 @@ export default class HaChartBase extends LitElement { return [ ...(this.plugins || []), { - id: "afterRenderHook", - afterRender: (chart) => { + id: "resizeHook", + resize: (chart) => { const change = chart.height - (this._chartHeight ?? 0); - if (!this._chartHeight || change > 0 || change < -12) { + if (!this._chartHeight || change > 12 || change < -12) { // hysteresis to prevent infinite render loops this._chartHeight = chart.height; } @@ -288,21 +330,13 @@ export default class HaChartBase extends LitElement { }; } - public updateChart = ( - mode: - | "resize" - | "reset" - | "none" - | "hide" - | "show" - | "normal" - | "active" - | undefined - ): void => { + private _releaseCanvas() { + // release the canvas memory to prevent + // safari from running out of memory. if (this.chart) { - this.chart.update(mode); + this.chart.destroy(); } - }; + } static get styles(): CSSResultGroup { return css` @@ -310,11 +344,14 @@ export default class HaChartBase extends LitElement { display: block; position: var(--chart-base-position, relative); } - .chartContainer { + .animationContainer { overflow: hidden; height: 0; transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); } + .chartContainer { + position: relative; + } canvas { max-height: var(--chart-max-height, 400px); } diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index 11c655b497..3474c1eb5b 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -1,6 +1,6 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { html, LitElement, PropertyValues } from "lit"; -import { property, state } from "lit/decorators"; +import { property, query, state } from "lit/decorators"; import { getGraphColorByIndex } from "../../common/color/colors"; import { fireEvent } from "../../common/dom/fire_event"; import { computeRTL } from "../../common/util/compute_rtl"; @@ -11,14 +11,18 @@ import { } from "../../common/number/format_number"; import { LineChartEntity, LineChartState } from "../../data/history"; import { HomeAssistant } from "../../types"; -import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; +import { + ChartResizeOptions, + HaChartBase, + MIN_TIME_BETWEEN_UPDATES, +} from "./ha-chart-base"; const safeParseFloat = (value) => { const parsed = parseFloat(value); return isFinite(parsed) ? parsed : null; }; -class StateHistoryChartLine extends LitElement { +export class StateHistoryChartLine extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public data: LineChartEntity[] = []; @@ -47,6 +51,12 @@ class StateHistoryChartLine extends LitElement { private _chartTime: Date = new Date(); + @query("ha-chart-base") private _chart?: HaChartBase; + + public resize = (options?: ChartResizeOptions): void => { + this._chart?.resize(options); + }; + protected render() { return html` { + this._chart?.resize(options); + }; + protected render() { return html` { + this._charts?.forEach( + (chart: StateHistoryChartLine | StateHistoryChartTimeline) => + chart.resize(options) + ); + }; + protected render() { if (!isComponentLoaded(this.hass, "history")) { return html`
diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index f414eb7871..22422da297 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -12,7 +12,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property, state, query } from "lit/decorators"; import memoizeOne from "memoize-one"; import { getGraphColorByIndex } from "../../common/color/colors"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; @@ -31,6 +31,7 @@ import { } from "../../data/recorder"; import type { HomeAssistant } from "../../types"; import "./ha-chart-base"; +import type { ChartResizeOptions, HaChartBase } from "./ha-chart-base"; export const supportedStatTypeMap: Record = { mean: "mean", @@ -42,7 +43,7 @@ export const supportedStatTypeMap: Record = { }; @customElement("statistics-chart") -class StatisticsChart extends LitElement { +export class StatisticsChart extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public statisticsData?: Statistics; @@ -75,8 +76,14 @@ class StatisticsChart extends LitElement { @state() private _chartOptions?: ChartOptions; + @query("ha-chart-base") private _chart?: HaChartBase; + private _computedStyle?: CSSStyleDeclaration; + public resize = (options?: ChartResizeOptions): void => { + this._chart?.resize(options); + }; + protected shouldUpdate(changedProps: PropertyValues): boolean { return changedProps.size > 1 || !changedProps.has("hass"); } diff --git a/src/components/chart/timeline-chart/const.ts b/src/components/chart/timeline-chart/const.ts index ac5f234272..88e29e7863 100644 --- a/src/components/chart/timeline-chart/const.ts +++ b/src/components/chart/timeline-chart/const.ts @@ -1,3 +1,8 @@ +import type { + BarControllerChartOptions, + BarControllerDatasetOptions, +} from "chart.js"; + export interface TimeLineData { start: Date; end: Date; diff --git a/src/components/chart/timeline-chart/textbar-element.ts b/src/components/chart/timeline-chart/textbar-element.ts index 1f11a8f433..9b766b0bb4 100644 --- a/src/components/chart/timeline-chart/textbar-element.ts +++ b/src/components/chart/timeline-chart/textbar-element.ts @@ -16,7 +16,7 @@ export interface TextBaroptions extends BarOptions { export class TextBarElement extends BarElement { static id = "textbar"; - draw(ctx) { + draw(ctx: CanvasRenderingContext2D) { super.draw(ctx); const options = this.options as TextBaroptions; const { x, y, base, width, text } = ( diff --git a/src/components/chart/timeline-chart/timeline-controller.ts b/src/components/chart/timeline-chart/timeline-controller.ts index 6b6ce7c41b..5aeadc6e00 100644 --- a/src/components/chart/timeline-chart/timeline-controller.ts +++ b/src/components/chart/timeline-chart/timeline-controller.ts @@ -2,6 +2,95 @@ import { BarController, BarElement } from "chart.js"; import { TimeLineData } from "./const"; import { TextBarProps } from "./textbar-element"; +function borderProps(properties) { + let reverse; + let start; + let end; + let top; + let bottom; + if (properties.horizontal) { + reverse = properties.base > properties.x; + start = "left"; + end = "right"; + } else { + reverse = properties.base < properties.y; + start = "bottom"; + end = "top"; + } + if (reverse) { + top = "end"; + bottom = "start"; + } else { + top = "start"; + bottom = "end"; + } + return { start, end, reverse, top, bottom }; +} + +function setBorderSkipped(properties, options, stack, index) { + let edge = options.borderSkipped; + const res = {}; + + if (!edge) { + properties.borderSkipped = res; + return; + } + + if (edge === true) { + properties.borderSkipped = { + top: true, + right: true, + bottom: true, + left: true, + }; + return; + } + + const { start, end, reverse, top, bottom } = borderProps(properties); + + if (edge === "middle" && stack) { + properties.enableBorderRadius = true; + if ((stack._top || 0) === index) { + edge = top; + } else if ((stack._bottom || 0) === index) { + edge = bottom; + } else { + res[parseEdge(bottom, start, end, reverse)] = true; + edge = top; + } + } + + res[parseEdge(edge, start, end, reverse)] = true; + properties.borderSkipped = res; +} + +function parseEdge(edge, a, b, reverse) { + if (reverse) { + edge = swap(edge, a, b); + edge = startEnd(edge, b, a); + } else { + edge = startEnd(edge, a, b); + } + return edge; +} + +function swap(orig, v1, v2) { + return orig === v1 ? v2 : orig === v2 ? v1 : orig; +} + +function startEnd(v, start, end) { + return v === "start" ? start : v === "end" ? end : v; +} + +function setInflateAmount( + properties, + { inflateAmount }: { inflateAmount?: string | number }, + ratio +) { + properties.inflateAmount = + inflateAmount === "auto" ? (ratio === 1 ? 0.33 : 0) : inflateAmount; +} + function parseValue(entry, item, vScale, i) { const startValue = vScale.parse(entry.start, i); const endValue = vScale.parse(entry.end, i); @@ -97,7 +186,7 @@ export class TimelineController extends BarController { bars: BarElement[], start: number, count: number, - mode: "reset" | "resize" | "none" | "hide" | "show" | "normal" | "active" + mode: "reset" | "resize" | "none" | "hide" | "show" | "default" | "active" ) { const vScale = this._cachedMeta.vScale!; const iScale = this._cachedMeta.iScale!; @@ -114,15 +203,15 @@ export class TimelineController extends BarController { for (let index = start; index < start + count; index++) { const data = dataset.data[index] as TimeLineData; - // @ts-ignore const y = vScale.getPixelForValue(this.index); - // @ts-ignore const xStart = iScale.getPixelForValue(data.start.getTime()); - // @ts-ignore const xEnd = iScale.getPixelForValue(data.end.getTime()); const width = xEnd - xStart; + const parsed = this.getParsed(index); + const stack = (parsed._stacks || {})[vScale.axis]; + const height = 10; const properties: TextBarProps = { @@ -145,7 +234,10 @@ export class TimelineController extends BarController { backgroundColor: data.color, }; } + const options = properties.options || bars[index].options; + setBorderSkipped(properties, options, stack, index); + setInflateAmount(properties, options, 1); this.updateElement(bars[index], index, properties as any, mode); } } diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 080d10cb1e..8a9e45369a 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -10,8 +10,8 @@ import { mdiPencilOutline, } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; -import { css, html, LitElement, nothing, PropertyValues } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { LitElement, PropertyValues, css, html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import { cache } from "lit/directives/cache"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../common/dom/fire_event"; @@ -38,15 +38,17 @@ import { haStyleDialog } from "../../resources/styles"; import "../../state-summary/state-card-content"; import { HomeAssistant } from "../../types"; import { - computeShowHistoryComponent, - computeShowLogBookComponent, DOMAINS_WITH_MORE_INFO, EDITABLE_DOMAINS_WITH_ID, EDITABLE_DOMAINS_WITH_UNIQUE_ID, + computeShowHistoryComponent, + computeShowLogBookComponent, } from "./const"; import "./controls/more-info-default"; import "./ha-more-info-history-and-logbook"; +import type { MoreInfoHistoryAndLogbook } from "./ha-more-info-history-and-logbook"; import "./ha-more-info-info"; +import type { MoreInfoInfo } from "./ha-more-info-info"; import "./ha-more-info-settings"; import "./more-info-content"; @@ -91,6 +93,9 @@ export class MoreInfoDialog extends LitElement { @state() private _infoEditMode = false; + @query("ha-more-info-info, ha-more-info-history-and-logbook") + private _history?: MoreInfoInfo | MoreInfoHistoryAndLogbook; + public showDialog(params: MoreInfoDialogParams) { this._entityId = params.entityId; if (!this._entityId) { @@ -263,6 +268,7 @@ export class MoreInfoDialog extends LitElement { ; + @query("statistics-chart, state-history-charts") private _chart?: + | StateHistoryCharts + | StatisticsChart; + + public resize = (options?: ChartResizeOptions): void => { + if (this._chart) { + this._chart.resize(options); + } + }; + protected render() { if (!this.entityId) { return nothing; } - return html` ${isComponentLoaded(this.hass, "history") + return html`${isComponentLoaded(this.hass, "history") ? html`
${this.hass.localize("ui.dialogs.more_info_control.history")} diff --git a/src/dialogs/more-info/ha-more-info-info.ts b/src/dialogs/more-info/ha-more-info-info.ts index cf9fa430aa..1df9ef1401 100644 --- a/src/dialogs/more-info/ha-more-info-info.ts +++ b/src/dialogs/more-info/ha-more-info-info.ts @@ -1,7 +1,8 @@ import { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, query } from "lit/decorators"; import { computeDomain } from "../../common/entity/compute_domain"; +import { ChartResizeOptions } from "../../components/chart/ha-chart-base"; import { ExtEntityRegistryEntry } from "../../data/entity_registry"; import type { HomeAssistant } from "../../types"; import { @@ -12,6 +13,7 @@ import { DOMAINS_WITH_MORE_INFO, } from "./const"; import "./ha-more-info-history"; +import type { MoreInfoHistory } from "./ha-more-info-history"; import "./ha-more-info-logbook"; import "./more-info-content"; @@ -25,6 +27,13 @@ export class MoreInfoInfo extends LitElement { @property({ attribute: false }) public editMode?: boolean; + @query("ha-more-info-history") + private _history?: MoreInfoHistory; + + public resize(options?: ChartResizeOptions) { + this._history?.resize(options); + } + protected render() { const entityId = this.entityId; const stateObj = this.hass.states[entityId] as HassEntity | undefined; diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index cdf7431129..96f69ed914 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -19,7 +19,7 @@ import { numberFormatToLocale, } from "../../../../common/number/format_number"; import "../../../../components/chart/ha-chart-base"; -import type HaChartBase from "../../../../components/chart/ha-chart-base"; +import type { HaChartBase } from "../../../../components/chart/ha-chart-base"; import "../../../../components/ha-card"; import { EnergyData, getEnergyDataCollection } from "../../../../data/energy"; import { diff --git a/yarn.lock b/yarn.lock index 9abc302ea1..3452e95561 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2039,6 +2039,13 @@ __metadata: languageName: node linkType: hard +"@kurkle/color@npm:^0.3.0": + version: 0.3.2 + resolution: "@kurkle/color@npm:0.3.2" + checksum: 79e97b31f8f6efb28c69d373f94b0c7480226fe8ec95221f518ac998e156444a496727ce47de6d728eb5c3369288e794cba82cae34253deb0d472d3bfe080e49 + languageName: node + linkType: hard + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.4 resolution: "@leichtgewicht/ip-codec@npm:2.0.4" @@ -6570,10 +6577,12 @@ __metadata: languageName: node linkType: hard -"chart.js@npm:3.3.2": - version: 3.3.2 - resolution: "chart.js@npm:3.3.2" - checksum: 0aaebab52cb5eacfc969210f7c304cad7e20b03a37b33f4e0332b5eeb9319e2929b085e0a8654ec33752566a55982ffe6bb3f2ba620c2a9b3bc71806ca7b9cb7 +"chart.js@npm:4.3.3": + version: 4.3.3 + resolution: "chart.js@npm:4.3.3" + dependencies: + "@kurkle/color": ^0.3.0 + checksum: 548605fc0a0a64fdc591a41159a2be86c1b408339d6e57b566c04dc157331b003db285a6139801d1700596acde8f0521bc6a156da29388025581619db6600073 languageName: node linkType: hard @@ -9720,7 +9729,7 @@ __metadata: babel-loader: 9.1.3 babel-plugin-template-html-minifier: 4.1.0 chai: 4.3.8 - chart.js: 3.3.2 + chart.js: 4.3.3 comlink: 4.4.1 core-js: 3.32.1 cropperjs: 1.6.0