Rewrite gauge (#6407)

This commit is contained in:
Bram Kragten 2020-07-16 17:42:14 +02:00 committed by GitHub
parent d7d8dd8986
commit 41370be2b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 189 additions and 100 deletions

View File

@ -108,7 +108,6 @@
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1", "superstruct": "^0.6.1",
"svg-gauge": "^1.0.6",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",

View File

@ -9,7 +9,7 @@ import {
} from "lit-element"; } from "lit-element";
@customElement("ha-card") @customElement("ha-card")
class HaCard extends LitElement { export class HaCard extends LitElement {
@property() public header?: string; @property() public header?: string;
@property({ type: Boolean, reflect: true }) public outlined = false; @property({ type: Boolean, reflect: true }) public outlined = false;

130
src/components/ha-gauge.ts Normal file
View File

@ -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`
<svg viewBox="0 0 100 50" class="gauge">
<path
class="dial"
d="M 10 50 A 40 40 0 0 1 90 50"
></path>
<path
class="value"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
d="M 90 50.001 A 40 40 0 0 1 10 50"
></path>
</svg>
<svg class="text">
<text class="value-text">
${this.value} ${this.label}
</text>
</svg>`;
}
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;
}
`;
}
}

View File

@ -1,5 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket/dist/types"; import { HassEntity } from "home-assistant-js-websocket/dist/types";
import Gauge from "svg-gauge";
import { import {
css, css,
CSSResult, CSSResult,
@ -10,7 +9,6 @@ import {
internalProperty, internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
query,
} from "lit-element"; } from "lit-element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_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 { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { GaugeCardConfig } from "./types"; import type { GaugeCardConfig } from "./types";
import "../../../components/ha-gauge";
import { styleMap } from "lit-html/directives/style-map";
export const severityMap = { export const severityMap = {
red: "var(--label-badge-red)", red: "var(--label-badge-red)",
@ -68,10 +68,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
@internalProperty() private _config?: GaugeCardConfig; @internalProperty() private _config?: GaugeCardConfig;
@internalProperty() private _gauge?: any;
@query("#gauge") private _gaugeElement!: HTMLDivElement;
public getCardSize(): number { public getCardSize(): number {
return 2; return 2;
} }
@ -84,7 +80,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
throw new Error("Invalid Entity"); throw new Error("Invalid Entity");
} }
this._config = { min: 0, max: 100, ...config }; this._config = { min: 0, max: 100, ...config };
this._initGauge();
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@ -118,7 +113,18 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
return html` return html`
<ha-card @click=${this._handleClick} tabindex="0"> <ha-card @click=${this._handleClick} tabindex="0">
<div id="gauge"></div> <ha-gauge
.min=${this._config.min!}
.max=${this._config.max!}
.value=${state}
.label=${this._config!.unit ||
this.hass?.states[this._config!.entity].attributes
.unit_of_measurement ||
""}
style=${styleMap({
"--gauge-color": this._computeSeverity(state),
})}
></ha-gauge>
<div class="name"> <div class="name">
${this._config.name || computeStateName(stateObj)} ${this._config.name || computeStateName(stateObj)}
</div> </div>
@ -130,13 +136,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
return hasConfigOrEntityChanged(this, changedProps); return hasConfigOrEntityChanged(this, changedProps);
} }
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (!this._gauge) {
this._initGauge();
}
}
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
super.updated(changedProps); super.updated(changedProps);
if (!this._config || !this.hass) { if (!this._config || !this.hass) {
@ -156,66 +155,38 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
) { ) {
applyThemesOnElement(this, this.hass.themes, this._config.theme); 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() { private _computeSeverity(numberValue: number): string {
if (!this._gaugeElement || !this._config || !this.hass) { const sections = this._config!.severity;
return;
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) { const sectionsArray = Object.keys(sections);
return severityMap.normal; const sortable = sectionsArray.map((severity) => [
} severity,
sections[severity],
]);
const sectionsArray = Object.keys(sections); for (const severity of sortable) {
const sortable = sectionsArray.map((severity) => [ if (severityMap[severity[0]] == null || isNaN(severity[1])) {
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]];
}
return severityMap.normal; 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 { private _handleClick(): void {
@ -244,29 +215,20 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
outline: none; outline: none;
background: var(--divider-color); background: var(--divider-color);
} }
#gauge {
ha-gauge {
--gauge-color: var(--label-badge-blue);
width: 100%; width: 100%;
max-width: 300px; 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 { .name {
text-align: center; text-align: center;
line-height: initial; line-height: initial;
color: var(--primary-text-color); color: var(--primary-text-color);
width: 100%; width: 100%;
font-size: 15px; font-size: 15px;
margin-top: 8px;
} }
`; `;
} }

View File

@ -12,6 +12,7 @@ import {
PropertyValues, PropertyValues,
svg, svg,
TemplateResult, TemplateResult,
query,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { UNIT_F } from "../../../common/const"; import { UNIT_F } from "../../../common/const";
@ -33,6 +34,7 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types"; import { LovelaceCard, LovelaceCardEditor } from "../types";
import { ThermostatCardConfig } from "./types"; import { ThermostatCardConfig } from "./types";
import type { HaCard } from "../../../components/ha-card";
const modeIcons: { [mode in HvacMode]: string } = { const modeIcons: { [mode in HvacMode]: string } = {
auto: "hass:calendar-sync", auto: "hass:calendar-sync",
@ -77,6 +79,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
@internalProperty() private _setTemp?: number | number[]; @internalProperty() private _setTemp?: number | number[];
@query("ha-card") private _card?: HaCard;
public getCardSize(): number { public getCardSize(): number {
return 5; return 5;
} }
@ -290,18 +294,17 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
// That way it will auto-scale correctly // That way it will auto-scale correctly
// This is not done to the SVG containing the current temperature, because // 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 // it should not be centered on the text, but only on the value
if (this.shadowRoot && this.shadowRoot.querySelector("ha-card")) { const card = this._card;
(this.shadowRoot.querySelector( if (card) {
"ha-card" card.updateComplete.then(() => {
) as LitElement).updateComplete.then(() => { const svgRoot = this.shadowRoot!.querySelector("#set-values")!;
const svgRoot = this.shadowRoot!.querySelector("#set-values"); const box = svgRoot.querySelector("g")!.getBBox()!;
const box = svgRoot!.querySelector("g")!.getBBox(); svgRoot.setAttribute(
svgRoot!.setAttribute(
"viewBox", "viewBox",
`${box!.x} ${box!.y} ${box!.width} ${box!.height}` `${box.x} ${box!.y} ${box.width} ${box.height}`
); );
svgRoot!.setAttribute("width", `${box!.width}`); svgRoot.setAttribute("width", `${box.width}`);
svgRoot!.setAttribute("height", `${box!.height}`); svgRoot.setAttribute("height", `${box.height}`);
}); });
} }
} }

View File

@ -11305,11 +11305,6 @@ sver-compat@^1.5.0:
es6-iterator "^2.0.1" es6-iterator "^2.0.1"
es6-symbol "^3.1.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: symbol-observable@^1.1.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"