From 41370be2b8d5cf0ea65f856da0e5fcf6ca9127a8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 16 Jul 2020 17:42:14 +0200 Subject: [PATCH] Rewrite gauge (#6407) --- package.json | 1 - src/components/ha-card.ts | 2 +- src/components/ha-gauge.ts | 130 ++++++++++++++++++ src/panels/lovelace/cards/hui-gauge-card.ts | 128 ++++++----------- .../lovelace/cards/hui-thermostat-card.ts | 23 ++-- yarn.lock | 5 - 6 files changed, 189 insertions(+), 100 deletions(-) create mode 100644 src/components/ha-gauge.ts diff --git a/package.json b/package.json index 7b4ada4548..9f61f7eba7 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,6 @@ "resize-observer-polyfill": "^1.5.1", "roboto-fontface": "^0.10.0", "superstruct": "^0.6.1", - "svg-gauge": "^1.0.6", "unfetch": "^4.1.0", "vue": "^2.6.11", "vue2-daterange-picker": "^0.5.1", diff --git a/src/components/ha-card.ts b/src/components/ha-card.ts index 8b2cbd8292..1c8fc43c97 100644 --- a/src/components/ha-card.ts +++ b/src/components/ha-card.ts @@ -9,7 +9,7 @@ import { } from "lit-element"; @customElement("ha-card") -class HaCard extends LitElement { +export class HaCard extends LitElement { @property() public header?: string; @property({ type: Boolean, reflect: true }) public outlined = false; diff --git a/src/components/ha-gauge.ts b/src/components/ha-gauge.ts new file mode 100644 index 0000000000..7b9869500c --- /dev/null +++ b/src/components/ha-gauge.ts @@ -0,0 +1,130 @@ +import { + LitElement, + svg, + customElement, + css, + property, + internalProperty, + PropertyValues, +} from "lit-element"; +import { styleMap } from "lit-html/directives/style-map"; +import { afterNextRender } from "../common/util/render-status"; + +const getAngle = (value: number, min: number, max: number) => { + const percentage = getValueInPercentage(normalize(value, min, max), min, max); + return (percentage * 180) / 100; +}; + +const normalize = (value: number, min: number, max: number) => { + if (value > max) return max; + if (value < min) return min; + return value; +}; + +const getValueInPercentage = (value: number, min: number, max: number) => { + const newMax = max - min; + const newVal = value - min; + return (100 * newVal) / newMax; +}; + +@customElement("ha-gauge") +export class Gauge extends LitElement { + @property({ type: Number }) public min = 0; + + @property({ type: Number }) public max = 100; + + @property({ type: Number }) public value = 45; + + @property() public label = ""; + + @internalProperty() private _angle = 0; + + @internalProperty() private _updated = false; + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + // Wait for the first render for the initial animation to work + afterNextRender(() => { + this._updated = true; + this._angle = getAngle(this.value, this.min, this.max); + this._rescale_svg(); + }); + } + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (!this._updated || !changedProperties.has("value")) { + return; + } + this._angle = getAngle(this.value, this.min, this.max); + this._rescale_svg(); + } + + protected render() { + return svg` + + + + + + + ${this.value} ${this.label} + + `; + } + + private _rescale_svg() { + // Set the viewbox of the SVG containing the value to perfectly + // fit the text + // That way it will auto-scale correctly + const svgRoot = this.shadowRoot!.querySelector(".text")!; + const box = svgRoot.querySelector("text")!.getBBox()!; + svgRoot.setAttribute( + "viewBox", + `${box.x} ${box!.y} ${box.width} ${box.height}` + ); + } + + static get styles() { + return css` + :host { + position: relative; + } + .dial { + fill: none; + stroke: var(--primary-background-color); + stroke-width: 15; + } + .value { + fill: none; + stroke-width: 15; + stroke: var(--gauge-color); + transition: all 1000ms ease 0s; + transform-origin: 50% 100%; + } + .gauge { + display: block; + } + .text { + position: absolute; + max-height: 40%; + max-width: 55%; + left: 50%; + bottom: -6%; + transform: translate(-50%, 0%); + } + .value-text { + font-size: 50px; + fill: var(--primary-text-color); + text-anchor: middle; + } + `; + } +} diff --git a/src/panels/lovelace/cards/hui-gauge-card.ts b/src/panels/lovelace/cards/hui-gauge-card.ts index 7bc23a4121..9f4ab7a9f3 100644 --- a/src/panels/lovelace/cards/hui-gauge-card.ts +++ b/src/panels/lovelace/cards/hui-gauge-card.ts @@ -1,5 +1,4 @@ import { HassEntity } from "home-assistant-js-websocket/dist/types"; -import Gauge from "svg-gauge"; import { css, CSSResult, @@ -10,7 +9,6 @@ import { internalProperty, PropertyValues, TemplateResult, - query, } from "lit-element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; @@ -24,6 +22,8 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { GaugeCardConfig } from "./types"; +import "../../../components/ha-gauge"; +import { styleMap } from "lit-html/directives/style-map"; export const severityMap = { red: "var(--label-badge-red)", @@ -68,10 +68,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { @internalProperty() private _config?: GaugeCardConfig; - @internalProperty() private _gauge?: any; - - @query("#gauge") private _gaugeElement!: HTMLDivElement; - public getCardSize(): number { return 2; } @@ -84,7 +80,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { throw new Error("Invalid Entity"); } this._config = { min: 0, max: 100, ...config }; - this._initGauge(); } protected render(): TemplateResult { @@ -118,7 +113,18 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { return html` -
+
${this._config.name || computeStateName(stateObj)}
@@ -130,13 +136,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { return hasConfigOrEntityChanged(this, changedProps); } - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - if (!this._gauge) { - this._initGauge(); - } - } - protected updated(changedProps: PropertyValues): void { super.updated(changedProps); if (!this._config || !this.hass) { @@ -156,66 +155,38 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { ) { applyThemesOnElement(this, this.hass.themes, this._config.theme); } - const oldState = oldHass?.states[this._config.entity]; - const stateObj = this.hass.states[this._config.entity]; - if (oldState?.state !== stateObj.state) { - this._gauge.setValueAnimated(stateObj.state, 1); - } } - private _initGauge() { - if (!this._gaugeElement || !this._config || !this.hass) { - return; + private _computeSeverity(numberValue: number): string { + const sections = this._config!.severity; + + if (!sections) { + return severityMap.normal; } - if (this._gauge) { - this._gaugeElement.removeChild(this._gaugeElement.lastChild!); - this._gauge = undefined; - } - this._gauge = Gauge(this._gaugeElement, { - min: this._config.min, - max: this._config.max, - dialStartAngle: 180, - dialEndAngle: 0, - viewBox: "0 0 100 55", - label: (value) => `${Math.round(value)} - ${ - this._config!.unit || - this.hass?.states[this._config!.entity].attributes - .unit_of_measurement || - "" - }`, - color: (value) => { - const sections = this._config!.severity; - if (!sections) { - return severityMap.normal; - } + const sectionsArray = Object.keys(sections); + const sortable = sectionsArray.map((severity) => [ + severity, + sections[severity], + ]); - const sectionsArray = Object.keys(sections); - const sortable = sectionsArray.map((severity) => [ - severity, - sections[severity], - ]); - - for (const severity of sortable) { - if (severityMap[severity[0]] == null || isNaN(severity[1])) { - return severityMap.normal; - } - } - sortable.sort((a, b) => a[1] - b[1]); - - if (value >= sortable[0][1] && value < sortable[1][1]) { - return severityMap[sortable[0][0]]; - } - if (value >= sortable[1][1] && value < sortable[2][1]) { - return severityMap[sortable[1][0]]; - } - if (value >= sortable[2][1]) { - return severityMap[sortable[2][0]]; - } + for (const severity of sortable) { + if (severityMap[severity[0]] == null || isNaN(severity[1])) { return severityMap.normal; - }, - }); + } + } + sortable.sort((a, b) => a[1] - b[1]); + + if (numberValue >= sortable[0][1] && numberValue < sortable[1][1]) { + return severityMap[sortable[0][0]]; + } + if (numberValue >= sortable[1][1] && numberValue < sortable[2][1]) { + return severityMap[sortable[1][0]]; + } + if (numberValue >= sortable[2][1]) { + return severityMap[sortable[2][0]]; + } + return severityMap.normal; } private _handleClick(): void { @@ -244,29 +215,20 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { outline: none; background: var(--divider-color); } - #gauge { + + ha-gauge { + --gauge-color: var(--label-badge-blue); width: 100%; max-width: 300px; } - .dial { - stroke: #ccc; - stroke-width: 15; - } - .value { - stroke-width: 15; - } - .value-text { - fill: var(--primary-text-color); - font-size: var(--gauge-value-font-size, 1.1em); - transform: translate(0, -5px); - font-family: inherit; - } + .name { text-align: center; line-height: initial; color: var(--primary-text-color); width: 100%; font-size: 15px; + margin-top: 8px; } `; } diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index ea254bd21f..04a7a8a1ac 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -12,6 +12,7 @@ import { PropertyValues, svg, TemplateResult, + query, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import { UNIT_F } from "../../../common/const"; @@ -33,6 +34,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { ThermostatCardConfig } from "./types"; +import type { HaCard } from "../../../components/ha-card"; const modeIcons: { [mode in HvacMode]: string } = { auto: "hass:calendar-sync", @@ -77,6 +79,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { @internalProperty() private _setTemp?: number | number[]; + @query("ha-card") private _card?: HaCard; + public getCardSize(): number { return 5; } @@ -290,18 +294,17 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { // That way it will auto-scale correctly // This is not done to the SVG containing the current temperature, because // it should not be centered on the text, but only on the value - if (this.shadowRoot && this.shadowRoot.querySelector("ha-card")) { - (this.shadowRoot.querySelector( - "ha-card" - ) as LitElement).updateComplete.then(() => { - const svgRoot = this.shadowRoot!.querySelector("#set-values"); - const box = svgRoot!.querySelector("g")!.getBBox(); - svgRoot!.setAttribute( + const card = this._card; + if (card) { + card.updateComplete.then(() => { + const svgRoot = this.shadowRoot!.querySelector("#set-values")!; + const box = svgRoot.querySelector("g")!.getBBox()!; + svgRoot.setAttribute( "viewBox", - `${box!.x} ${box!.y} ${box!.width} ${box!.height}` + `${box.x} ${box!.y} ${box.width} ${box.height}` ); - svgRoot!.setAttribute("width", `${box!.width}`); - svgRoot!.setAttribute("height", `${box!.height}`); + svgRoot.setAttribute("width", `${box.width}`); + svgRoot.setAttribute("height", `${box.height}`); }); } } diff --git a/yarn.lock b/yarn.lock index 50621e3fee..9701db610f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11305,11 +11305,6 @@ sver-compat@^1.5.0: es6-iterator "^2.0.1" es6-symbol "^3.1.1" -svg-gauge@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/svg-gauge/-/svg-gauge-1.0.6.tgz#1e84a366b1cce5b95dab3e33f41fdde867692d28" - integrity sha512-gRkznVhtS18eOM/GMPDXAvrLZOpqzNVDg4bFAPAEjiDKd1tZHFIe8Bwt3G6TFg/H+pFboetPPI+zoV+bOL26QQ== - symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"