-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Gauge improvements #30368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Gauge improvements #30368
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"; | ||
|
|
@@ -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(); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -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() { | ||
|
|
@@ -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"; | ||
|
|
||
| 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)` })} | ||
| > | ||
|
MindFreeze marked this conversation as resolved.
Outdated
|
||
| ` | ||
| : svg` | ||
| <path | ||
|
|
@@ -179,7 +186,8 @@ export class HaGauge extends LitElement { | |
| /> | ||
| ` | ||
| } | ||
|
|
||
| </svg> | ||
| <svg class="text"> | ||
| <text | ||
| class="value-text" | ||
| x="0" | ||
|
|
@@ -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}` | ||
|
silamon marked this conversation as resolved.
Outdated
|
||
| ); | ||
| } | ||
|
|
||
| private _getSegmentLabel() { | ||
| if (this.levels) { | ||
| [...this.levels].sort((a, b) => a.level - b.level); | ||
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.