Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions gallery/src/pages/lovelace/gauge-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ const CONFIGS = [
entity: sensor.not_working
`,
},
{
heading: "Lower minimum",
config: `
- type: gauge
entity: sensor.brightness_high
needle: true
severity:
green: 0
yellow: 0.45
red: 0.9
min: -0.05
name: " "
max: 1.9
unit: GBP/h`,
},
];

@customElement("demo-lovelace-gauge-card")
Expand Down
203 changes: 117 additions & 86 deletions src/components/ha-gauge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { PropertyValues } from "lit";
import { css, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
Expand Down Expand Up @@ -54,6 +54,7 @@ export class HaGauge extends LitElement {
this._angle = getAngle(this.value, this.min, this.max);
}
this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
});
}

Expand All @@ -70,6 +71,7 @@ export class HaGauge extends LitElement {
}
this._angle = getAngle(this.value, this.min, this.max);
this._segment_label = this._getSegmentLabel();
this._rescaleSvg();
}

protected render() {
Expand All @@ -88,87 +90,92 @@ export class HaGauge extends LitElement {
/>


${
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
? (() => {
const sortedLevels = [...this.levels].sort(
(a, b) => a.level - b.level
);

if (
sortedLevels.length > 0 &&
sortedLevels[0].level !== this.min
) {
sortedLevels.unshift({
level: this.min,
stroke: "var(--info-color)",
});
}

return sortedLevels.map((level, i, arr) => {
const startLevel = level.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 isFirst = i === 0;
const isLast = i === arr.length - 1;
const linecap = "butt";
Comment thread
silamon marked this conversation as resolved.
Outdated

if (isFirst) {
return svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
/>
`;
}

if (isLast) {
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);

return svg`
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}" />
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}" />
`;
}

return svg`
<path
class="level"
stroke="${level.stroke}"
style="stroke-linecap: ${linecap}"
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
></path>
`;
});
})()
: ""
}

${
this.needle
? svg`
<line
class="needle"
x1="-35.0"
y1="0"
x2="-45.0"
y2="0"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
/>
<path
class="needle"
d="M -36,-2 L -44,-1 A 1,1,0,0,0,-44,1 L -36,2 A 2,2,0,0,0,-36,-2 Z"

style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
>
Comment thread
MindFreeze marked this conversation as resolved.
Outdated
`
: svg`
<path
Expand All @@ -179,7 +186,8 @@ export class HaGauge extends LitElement {
/>
`
}

</svg>
<svg class="text">
<text
class="value-text"
x="0"
Expand All @@ -204,6 +212,18 @@ export class HaGauge extends LitElement {
`;
}

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}`
Comment thread
silamon marked this conversation as resolved.
Outdated
);
}

private _getSegmentLabel() {
if (this.levels) {
[...this.levels].sort((a, b) => a.level - b.level);
Expand All @@ -224,32 +244,43 @@ export class HaGauge extends LitElement {
.levels-base {
fill: none;
stroke: var(--primary-background-color);
stroke-width: 8;
stroke-linecap: round;
stroke-width: 6;
stroke-linecap: butt;
}

.level {
fill: none;
stroke-width: 8;
stroke-width: 6;
stroke-linecap: butt;
}

.value {
fill: none;
stroke-width: 8;
stroke-width: 6;
stroke: var(--gauge-color);
stroke-linecap: round;
stroke-linecap: butt;
transition: stroke-dashoffset 1s ease 0s;
}

.needle {
stroke: var(--primary-text-color);
stroke-width: 2;
fill: var(--primary-text-color);
stroke: var(--card-background-color);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about a transparency for this stroke? Just an idea not sure if it looks nice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's going to be a good idea. The full white (or black depending on theme) behind it makes the contrast better to see where the needle exactly is. We've already received a lot of feedback that this is important for some users.

@Mariusthvdb Mariusthvdb Mar 30, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to give needle it its own variable? so we can theme independently from text-color/background-color? might seem niche, but given the many different uses for a gauge, it would be a welcome option to be able to differentiate

color: var(--primary-text-color);
stroke-width: 1;
stroke-linecap: round;
transform-origin: 0 0;
transition: all 1s ease 0s;
}

.text {
position: absolute;
max-height: 40%;
max-width: 55%;
left: 50%;
bottom: 10%;
transform: translate(-50%, 0%);
}

.value-text {
font-size: var(--ha-font-size-l);
fill: var(--primary-text-color);
Expand Down
4 changes: 2 additions & 2 deletions src/panels/lovelace/cards/hui-gauge-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,8 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {

.title {
width: 100%;
font-size: var(--ha-font-size-l);
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-expanded);
padding: 0px 0px var(--ha-space-2) 0px;
margin: 0;
text-align: center;
box-sizing: border-box;
Expand All @@ -318,6 +317,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {

ha-gauge {
width: 100%;
max-width: 250px;
}
`;
}
Expand Down
Loading