1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-18 07:56:44 +01:00

Redesign gauge card (#29981)

* Redesign gauge card

* Fix calculation

* New design, code needs optimization (ai)
This commit is contained in:
Simon Lamon
2026-03-11 10:22:44 +01:00
committed by GitHub
parent d61de4d2df
commit 6f6ba71e61
3 changed files with 147 additions and 98 deletions

View File

@@ -54,6 +54,19 @@ const CONFIGS = [
needle: true needle: true
`, `,
}, },
{
heading: "Rendering needle and severity levels",
config: `
- type: gauge
entity: sensor.brightness_high
name: Brightness High
needle: true
severity:
red: 75
green: 0
yellow: 50
`,
},
{ {
heading: "Setting severity levels", heading: "Setting severity levels",
config: ` config: `

View File

@@ -44,16 +44,16 @@ export class HaGauge extends LitElement {
@state() private _updated = false; @state() private _updated = false;
@state() private _segment_label? = ""; @state() private _segment_label?: string = "";
protected firstUpdated(changedProperties: PropertyValues) { protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
// Wait for the first render for the initial animation to work
afterNextRender(() => { afterNextRender(() => {
this._updated = true; this._updated = true;
this._angle = getAngle(this.value, this.min, this.max); if (this.needle) {
this._angle = getAngle(this.value, this.min, this.max);
}
this._segment_label = this._getSegmentLabel(); this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
}); });
} }
@@ -70,70 +70,121 @@ export class HaGauge extends LitElement {
} }
this._angle = getAngle(this.value, this.min, this.max); this._angle = getAngle(this.value, this.min, this.max);
this._segment_label = this._getSegmentLabel(); this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
} }
protected render() { protected render() {
const arcRadius = 40;
const arcLength = Math.PI * arcRadius;
const valueAngle = getAngle(this.value, this.min, this.max);
const strokeOffset = arcLength * (1 - valueAngle / 180);
return svg` return svg`
<svg viewBox="-50 -50 100 50" class="gauge"> <svg viewBox="-50 -50 100 60" class="gauge">
${ <path
!this.needle || !this.levels class="levels-base"
? svg`<path
class="dial"
d="M -40 0 A 40 40 0 0 1 40 0" d="M -40 0 A 40 40 0 0 1 40 0"
></path>` />
: ""
}
${
this.levels
? [...this.levels]
.sort((a, b) => a.level - b.level)
.map((level, i, arr) => {
const startLevel = i === 0 ? this.min : arr[i].level;
const endLevel = i + 1 < arr.length ? arr[i + 1].level : this.max;
const startAngle = getAngle(startLevel, this.min, this.max);
const endAngle = getAngle(endLevel, this.min, this.max);
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
const x1 = -arcRadius * Math.cos((startAngle * Math.PI) / 180);
const y1 = -arcRadius * Math.sin((startAngle * Math.PI) / 180);
const x2 = -arcRadius * Math.cos((endAngle * Math.PI) / 180);
const y2 = -arcRadius * Math.sin((endAngle * Math.PI) / 180);
const firstSegment = i === 0;
const lastSegment = i === arr.length - 1;
const paths: TemplateResult[] = [];
if (firstSegment) {
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: round"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
/>
`);
} else if (lastSegment) {
const offsetAngle = 0.5;
const midAngle = endAngle - offsetAngle;
const xm = -arcRadius * Math.cos((midAngle * Math.PI) / 180);
const ym = -arcRadius * Math.sin((midAngle * Math.PI) / 180);
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}"
/>
`);
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: round"
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}"
/>
`);
} else {
paths.push(svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
/>
`);
}
return paths;
})
: ""
}
${
this.levels
? this.levels
.sort((a, b) => a.level - b.level)
.map((level, idx) => {
let firstPath: TemplateResult | undefined;
if (idx === 0 && level.level !== this.min) {
const angle = getAngle(this.min, this.min, this.max);
firstPath = svg`<path
stroke="var(--info-color)"
class="level"
d="M
${0 - 40 * Math.cos((angle * Math.PI) / 180)}
${0 - 40 * Math.sin((angle * Math.PI) / 180)}
A 40 40 0 0 1 40 0
"
></path>`;
}
const angle = getAngle(level.level, this.min, this.max);
return svg`${firstPath}<path
stroke="${level.stroke}"
class="level"
d="M
${0 - 40 * Math.cos((angle * Math.PI) / 180)}
${0 - 40 * Math.sin((angle * Math.PI) / 180)}
A 40 40 0 0 1 40 0
"
></path>`;
})
: ""
}
${ ${
this.needle this.needle
? svg`<path ? svg`
class="needle" <line
d="M -25 -2.5 L -47.5 0 L -25 2.5 z" class="needle"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })} x1="-35.0"
> y1="0"
x2="-45.0"
y2="0"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
/>
`
: svg`
<path
class="value"
d="M -40 0 A 40 40 0 0 1 40 0"
stroke-dasharray="${arcLength}"
style=${styleMap({ strokeDashoffset: `${strokeOffset}` })}
/>
` `
: svg`<path
class="value"
d="M -40 0 A 40 40 0 1 0 40 0"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
>`
} }
</path>
</svg> <text
<svg class="text"> class="value-text"
<text class="value-text"> x="0"
y="-10"
dominant-baseline="middle"
text-anchor="middle"
>
${ ${
this._segment_label this._segment_label
? this._segment_label ? this._segment_label
@@ -147,24 +198,13 @@ export class HaGauge extends LitElement {
: ` ${this.label}` : ` ${this.label}`
} }
</text> </text>
</svg>`; </svg>
} `;
private _rescaleSvg() {
// 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}`
);
} }
private _getSegmentLabel() { private _getSegmentLabel() {
if (this.levels) { if (this.levels) {
this.levels.sort((a, b) => a.level - b.level); [...this.levels].sort((a, b) => a.level - b.level);
for (let i = this.levels.length - 1; i >= 0; i--) { for (let i = this.levels.length - 1; i >= 0; i--) {
if (this.value >= this.levels[i].level) { if (this.value >= this.levels[i].level) {
return this.levels[i].label; return this.levels[i].label;
@@ -178,40 +218,38 @@ export class HaGauge extends LitElement {
:host { :host {
position: relative; position: relative;
} }
.dial {
.levels-base {
fill: none; fill: none;
stroke: var(--primary-background-color); stroke: var(--primary-background-color);
stroke-width: 15; stroke-width: 10;
} stroke-linecap: round;
.value {
fill: none;
stroke-width: 15;
stroke: var(--gauge-color);
transition: all 1s ease 0s;
}
.needle {
fill: var(--primary-text-color);
transition: all 1s ease 0s;
} }
.level { .level {
fill: none; fill: none;
stroke-width: 15; stroke-width: 10;
stroke-linecap: butt;
} }
.gauge {
display: block; .value {
fill: none;
stroke-width: 10;
stroke: var(--gauge-color);
stroke-linecap: round;
transition: all 1s ease 0s;
} }
.text {
position: absolute; .needle {
max-height: 40%; stroke: var(--primary-text-color);
max-width: 55%; stroke-width: 2;
left: 50%; stroke-linecap: round;
bottom: -6%; transform-origin: 0 0;
transform: translate(-50%, 0%); transition: all 1s ease 0s;
} }
.value-text { .value-text {
font-size: 50px;
fill: var(--primary-text-color); fill: var(--primary-text-color);
text-anchor: middle;
direction: ltr; direction: ltr;
} }
`; `;

View File

@@ -303,7 +303,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
ha-gauge { ha-gauge {
width: 100%; width: 100%;
max-width: 250px;
} }
.name { .name {
@@ -312,7 +311,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
color: var(--primary-text-color); color: var(--primary-text-color);
width: 100%; width: 100%;
font-size: var(--ha-font-size-m); font-size: var(--ha-font-size-m);
margin-top: 8px;
} }
`; `;
} }