mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-29 04:06:35 +00:00
Rewrite gauge (#6407)
This commit is contained in:
parent
d7d8dd8986
commit
41370be2b8
@ -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",
|
||||||
|
@ -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
130
src/components/ha-gauge.ts
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -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}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user