# Gauge's for Dashboard 2.0 made with ui\_template

**URL:** <https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955>\
**Category:** Dashboard\
**Tags:** dashboard-2\
**Created:** [27 February 2024 13:43 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955 "2024-02-27T13:43:09Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [27 February 2024 13:43 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/1 "2024-02-27T13:43:09Z")

</div>

Type linear, round, vertical, or artless.

> **Code is HERE**
>
> TEMPLATE:
> 
> ```auto
> <template>
> <div>
> <template v-if="type === 'round'">
> <div ref="hng" class="round-led-level" :style="`--size:${size}; --shadow:${shadow}; --ledsize:${ledSize};`">
> <header>
> <div class="round-led-level-text">
> <span class="round-led-level-label">{{label}}</span>
> </div>
> </header>
> <div class="round-led-level-graph">
> <div class="round-led-level-stripe" :class="{'led-level-flat': flat }">
> <div v-for="(color, index) in colors" :key="index" class="round-led-level-led"
> :ref="'dot-' + index">
> </div>
> </div>
> <div class="round-led-level-centered-text">
> <span class="round-led-level-value">{{formattedValue}}</span>
> <span class="round-led-level-unit">{{unit}}</span>
> </div>
> <div class="round-led-level-limits">
> <span>{{min}}</span>
> <span>{{max}}</span>
> </div>
> </div>
> <div>
> </template>
> <template v-if="type === 'linear'">
> <div ref="hng" class="led-level" :style="`--shadow:${shadow};`">
> <div class="led-level-text">
> <span class="led-level-label">{{label}}</span>
> <span class="led-level-value">{{formattedValue}}<span class="led-level-unit">{{unit}}</span></span>
> </div>
> <div class="led-level-stripe" :class="{'led-level-flat': flat }">
> <div v-for="(color, index) in colors" :key="index" class="led-level-led" :ref="'dot-' + index">
> </div>
> </div>
> <div class="led-level-limits">
> <span>{{min}}</span>
> <span>{{max}}</span>
> </div>
> <div>
> </template>
> <template v-if="type === 'vertical'">
> <div ref="hng" class="led-level-vertical" :style="`--shadow:${shadow}; --size:${size};`">
> <header>
> <div class="round-led-level-text">
> <span class="round-led-level-label">{{label}}</span>
> </div>
> </header>
> <div class="led-level-vertical-content">
> <div class="led-level-stripe" :class="{'led-level-flat': flat }">
> <div v-for="(color, index) in colors" :key="index" class="led-level-led" :ref="'dot-' + index">
> </div>
> </div>
> <div class="led-level-limits">
> <span>{{max}}</span>
> <span>{{min}}</span>
> </div>
> <div class="round-led-level-centered-text">
> <span class="round-led-level-value">{{formattedValue}}</span>
> <span class="round-led-level-unit">{{unit}}</span>
> </div>
> </div>
> </div>
> 
> </template>
> <template v-if="type === 'artless'">
> <div ref="hng" :class="icon ? 'ag-wrapper-2' : 'ag-wrapper-1'" :style="`--line-color:${colors[0]};`">
> <div v-if="icon" class="ag-icon">
> <v-icon aria-hidden="false">{{icon}}</v-icon>
> </div>
> <div class="ag-content">
> <div class="ag-text">
> <span class="ag-label">{{label}}</span>
> <span class="ag-value">{{formattedValue}}<span class="ag-unit">{{unit}}</span></span>
> </div>
> <div class="ag-track" ref="agLine">
> <div class="ag-track-background"></div>
> <div class="ag-track-foreground" :style="{'width': linesize +'%'}"></div>
> </div>
> <div class="ag-limits">
> <span class="ag-min">{{min}}</span>
> <span class="ag-max">{{max}}</span>
> </div>
> </div>
> </div>
> </template>
> </div>
> </template>
> 
> <script>
> export default {
> data(){
> return {
> //Define me here
> type:"artless", // Gauge type. "artless", "linear", "vertical" or "round"                                          
> label:"Artless", // The label
> icon:"mdi-account", // (type: artless) (optional) the icon
> zeroCross:false,// (type: artless) line changes color depending on value being positive or negative (at least 2 colors must be defined and min must be 0)
> min:0, // Smallest expected value
> max:100, // Highest expected value
> unit:"cm³",// The unit of the measurement
> dim:0.3, //(type: round, linear, vertical) How dim is led when not glowing
> shadow:0, //(type: round, linear, vertical) Led shadow intensity (too much makes graphics muddy, 0 removes shadows)
> filterFunction:"brightness", // (type: round, linear, vertical) "brightness" for dark themes, "opacity" for light themes  
> animate:true, // Animating led's is not most performant thing in the world.                          
>             
> // Define colors
> 
> // For type "round", "vertical" and "linear" the count of colors equals count of led's.
> // For type "artless" the line changes color based on percentage of value turned index of colors array.  
> // For type "round" the led size depends on how many colors is defined. About 20 is optimal.
> // Color can be defined as:
> // HEX - "#FF00FF" 
> // RGB - rgb(0,65,88)
> // named color - "red"
> // or depend on some defined CSS variable      
> colors:[
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "#0fb60f",                   
> "red",
> "red",
> "red",
> "red",
> ],            
>             
> //no need to change those
> value:0,
> previousValue:0,
> size:100,           
> inited:false
> }
> },
> 
>    
> methods: {        
> getElement: function(name,base){        
> if(base){
> return this.$refs[name]
> }
> return this.$refs[name][0]
> },
> validate(data){
> let ret
> if(typeof data !== "number"){
> ret = parseFloat(data)
> if(isNaN(ret)){
> console.log("BAD DATA! gauge type:",this.type, "id:",this.id,"data:",data)
> return null
> }   
> }
> else{
> ret = data
> }            
> return ret
> },
> 
> full: function(){
> return Math.floor(this.colors.length*this.percentage/100)
> },
> half: function (){
> let p = this.colors.length*this.percentage/100;
> p -= this.full()
> p *= .5
> p += this.dim;
> return p;
> },
> filter: function(amount){
> let f
> switch(amount){
> case "full":{
> f = this.filterFunction == "brightness" ? "brightness(1.1)" : "opacity(1)";
> break
> }
> case "half":{
> f = this.filterFunction == "brightness" ? "brightness(" +this.half()+")" : "opacity(" +this.half()+")";
> break
> }
> default:{
> f = this.filterFunction == "brightness" ? "brightness(" +this.dim+")" : "opacity(" +this.dim+")";
> break
> }
> }
> return f
> },
> 
> lit: function(){
> if(this.inited == false){
> return
> }
> const down = this.previousValue > this.value
> 
> let time = .01            
> this.colors.forEach((color,i) => {
> let dot = this.getElement("dot-"+i);
> if(!dot){
> console.log("lit() no dots found")
> return
> }                
> if(i<this.full()){
> dot.style.filter=this.filter("full");                   
> }
> else if(i==this.full()){
> dot.style.filter= this.filter("half");                   
> }
> else{
> dot.style.filter= this.filter("dim");                   
> }
> if(down){
> time = (this.colors.length - i) * .12                    
> }else{
> time = i * 0.08
> }
> dot.style.transition = this.animate ? "filter "+time+"s" : "unset";
> })
> this.previousValue = this.value
> },
> changeLine:function(){
> const line = this.getElement("agLine",true);
> if(!line){
> console.log("no line found")
> return            
> }
>            
> let c = Math.floor(this.colors.length * this.percentage / 100)
> if(c >= this.colors.length){
> c = this.colors.length - 1
> }
> if(c < 0){
> c = 0
> }
> if(this.zeroCross){
> c = this.value > 0 ? 1 : 0
> }
> line.style.setProperty('--line-color',this.colors[c])
> 
> },
> onResize:function(){            
> let g = this.getElement("hng",true)
> if(!g){
> return
> }           
> this.$nextTick(() => {
> let last = this.size 
> let changed = this.type == "vertical" ? g.clientHeight : g.clientWidth;   
> if(Math.abs(last - changed) < 3){
> return
> }
> this.size = changed
> g.style.setProperty('--size',this.size);            
> if(this.type == "round"){
> this.updateLayout()
> }
> })
>                               
> },
> updateLayout:function(){
> let angle;
> const step = 270 / this.colors.length;
> const radius = (this.size - (this.size*0.1))/2
> const s = this.ledSize / -2;
> const outline = this.filterFunction == "opacity" ? "black" : "white";        
> this.colors.forEach((c,i) => {
> let dot = this.getElement("dot-"+i);
> if(!dot){
> console.log("round init() no dots found")
> return
> }
> dot.style.backgroundColor = c
> dot.style.outlineColor="color-mix(in srgb, "+c+", "+outline+" 35%)";
> dot.style.transition = "filter 0.1s";
> dot.style.setProperty('--dot',i);
> angle = ((i+1)*step) * Math.PI / 180;
> dot.style.left = s + radius * Math.cos(angle) + 'px';
> dot.style.top = s + radius * Math.sin(angle) + 'px';
> dot.style.transform = 'translate('+s+'px, '+s+'px)'; 
> dot.style.rotate = (angle - 0.08)+"rad"               
> }
> )
> }
> },
> watch: {
> msg: function(){    
> if(this.msg.payload !== undefined){  
> const v = this.validate(this.msg.payload)                
> if(v === null){
> return
> }         
> this.value = v
> if(this.type != "artless"){
> this.lit()
> }
> else{
> this.changeLine()
> } 
> }
> }
> },
> computed: {
> formattedValue: function () {
> return this.value.toFixed(2)
> },
> percentage: function(){
> return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);
> },
> linesize:function(){
> if(this.zeroCross){
> return Math.floor(((Math.abs(this.value) - this.min) / (this.max - this.min)) * 100);           
> }
> else{
> return Math.max(0,this.percentage)
> }
> },                
> ledSize:function(){
> const s = 4.71239 * ((this.size - (this.size*0.3))/2)            
> return s / this.colors.length
> },
> flat:function(){
> return this.shadow == 0
> }
> 
> },
> mounted(){
> const outline = this.filterFunction == "opacity" ? "black" : "white";
> if(this.type == "round"){
> let g = this.getElement("hng",true)
> if(!g){
> return
> }            
> this.resizeObserver = new ResizeObserver((entries) => {
> this.onResize()
> });
> this.resizeObserver.observe(g);           
>             
> setTimeout(()=>{
> this.onResize()
> },20)
> }
> else if(this.type == "linear"){
>             
> this.colors.forEach((c,i) => {
> let dot = this.getElement("dot-"+i);
> if(!dot){
> console.log("linear init() no dots found")
> return
> }
> dot.style.outlineColor="color-mix(in srgb, "+c+", "+outline+" 35%)";
> dot.style.backgroundColor = c               
> dot.style.transition = "filter 0.1s";
> }
> )
> }
> else if(this.type == "vertical"){
> let g = this.getElement("hng",true)
> if(!g){
> return
> }
> this.resizeObserver = new ResizeObserver((entries) => {
> this.onResize()
> });
> this.resizeObserver.observe(g);                
> setTimeout(()=>{
> this.onResize()
> },20)
> this.colors.forEach((c,i) => {
> let dot = this.getElement("dot-"+i);
> if(!dot){
> console.log("linear init() no dots found")
> return
> }
> dot.style.outlineColor="color-mix(in srgb, "+c+", "+outline+" 35%)";
> dot.style.backgroundColor = c
> dot.style.transition = "filter 0.1s";
> })
> }
> else if(this.type == "artless"){
> const line = this.getElement("agLine",true);
> line.style.setProperty('--line-color',this.colors[0])
> if(this.animate == true){                
> if(!line){
> console.log("artless init() no line found")
> return
> }
> line.style.transition = "width 0.5s";
> }
> }        
>        
> this.inited = true;
> },
> unmounted () {
> if(this.resizeObserver){
> this.resizeObserver.disconnect();
> this.resizeObserver = null;            
> }
> }
> }
> </script>
> 
> ```
> 
> CSS:
> 
> ```auto
> .led-level{
> display: grid;
> grid-template-rows: 1.3em minmax(3px, 1fr) .7em;
> gap: 2px;
> height: 100%;
> }
> .led-level-stripe{
> display: flex;
> gap:2px;
> }    
> .led-level-led {
> --s:var(--shadow,0.2);
> --shadowColor:rgba(0,0,0,var(--s));
> background: #ffffff;
> width: 100%;
> height: 100%;
> outline:1px solid;
> outline-offset:-1px;
> border-radius: 0px;
> box-shadow: inset 0px 0px 10px 0px var(--shadowColor), 0px 0px 3px 0px var(--shadowColor);
> filter: brightness(0.4);
> }
> .led-level-text{
> font-size: 1.25em;
> line-height: 1em;
> align-self: end;
> display: flex;
> flex-wrap: wrap;
> justify-content: space-between;
> user-select: none;
> }
> .led-level-value{       
> font-weight:bold;
> }
> .led-level-unit{
> font-size:.75em;
> font-weight:normal;
> padding-inline-start: 0.15em;
> }
> .led-level-limits{
> display: flex;
> justify-content: space-between;
> font-size: .75em;
> line-height: .75em;
> align-content: flex-end;
> flex-wrap: wrap;
> user-select: none;
> }
> .round-led-level, .led-level-vertical{
> display: grid;
> grid-template-rows: 1em 1fr;
> width:100%;
> height: 100%;
> aspect-ratio: 1/1;
> position: relative;
> margin: auto;
> }
> .round-led-level-graph{
> position: relative;
> aspect-ratio: 1;
> }
> .round-led-level-stripe{
> display: block;
> position: absolute;
> left: 50%;
> top: 56%;
> rotate: 135deg;  
> }    
> .round-led-level-led {
> --s:var(--shadow,0.2);
> --shadowColor:rgba(0,0,0,var(--s));
> background: #ffffff;
> position: absolute;
> width: calc(var(--ledsize) * 1px);
> aspect-ratio: 1/1;
> border-radius: 4px;
> outline:1px solid;
> outline-offset:-1px;
> box-shadow: inset 0px 0px calc(var(--ledsize) / 3 * 1px) 0px var(--shadowColor), 0px 0px calc(var(--ledsize) / 7 * 1px) 0px var(--shadowColor);
> filter: brightness(0.4);
> transform-origin: center center;
> }
> .round-led-level-text{    
> font-size: clamp(0.5em,calc(var(--size) * .1 * 1px),1.25em);
> line-height: 1rem;
> text-align: center;
> user-select: none;
> white-space: nowrap;
> }
> .round-led-level-centered-text{
> position: absolute;
> inset: 0;
> font-size: 1rem;
> line-height: 1;
> display: grid;
> text-align: center;
> grid-template-rows: 1.5fr 1fr;
> gap: 0.1em;
> user-select: none;   
> align-items: center;
> }
> .round-led-level-value{       
> font-weight:bold;
> font-size: calc(var(--size) * .15 * 1px);
> align-self: end;
> }
> .round-led-level-unit{
> font-size:calc(var(--size) * .1 * 1px);
> font-weight:normal;
> align-self: start;
> padding-inline-start: 0.15em;
> }
> .round-led-level-limits{
> position: absolute;
> inset:0; 
> display: flex;
> justify-content: space-between;
> font-size: calc(var(--size) * .06 * 1px);
> line-height: calc(var(--size) * .06 * 1px);
> align-content: flex-end;
> flex-wrap: wrap;
> padding-inline:1em;
> user-select: none;
> }
> .ag-wrapper-2 {
> display: grid;
> grid-template-columns: 3em 1fr;
> gap:1em;
> }
> .ag-wrapper-1 {
> display: grid;
> grid-template-columns: 1fr;   
> }
> .ag-icon{
> font-size: 2em;
> display: flex;
> flex-direction: column;
> justify-content: center;        
> }
> .ag-content{
> display: grid;
> grid-template-rows: 1fr 7px 0.75em;
> gap: 2px;
> }
> .ag-text{
> font-size: 1.25em;
> line-height: 1em;
> align-self: end;
> display: flex;
> flex-wrap: wrap;
> justify-content: space-between;
> user-select: none;
> }
> .ag-value{       
> font-weight:bold;
> }
> .ag-unit{
> font-size:.75em;
> font-weight:normal;
> padding-inline-start: 0.15em;
> }
> .ag-limits{
> display: flex;
> justify-content: space-between;
> font-size: .75em;
> line-height: .75em;
> align-content: center;
> flex-wrap: wrap;
> user-select: none;
> }
> 
> .ag-track{
> position:relative;
> display:flex;
> align-items: center;
> width: 100%;
> border-radius: 6px;
> }
> 
> .ag-track-background{
> position:absolute;
> background: var(--line-color,rgb(var(--v-theme-primary)));
> opacity: 0.45;
> width: 100%;
> height: 50%;
> border-radius:inherit;
> }
> .ag-track-foreground{
> position:absolute;
> background-color: var(--line-color,rgb(var(--v-theme-primary)));
> width: 50%;
> height: 100%;
> max-width: 100%;
> border-radius:inherit;
> transition:inherit;
> }
> .led-level-vertical{
> gap:calc(var(--size) * .1 * 1px);
> aspect-ratio: var(--aspect-ratio);
> }
> .led-level-vertical-content{
> display:grid;
> grid-template-columns: 1fr auto 4fr;
> }
> .led-level-vertical .round-led-level-centered-text{
> grid-template-rows: 2.5fr 1fr;
> padding-inline-start: 1em;
> }
> .led-level-vertical .round-led-level-value{
> font-size: calc(var(--size) * .2 * 1px);
> }
> .led-level-vertical .led-level-stripe{
> flex-direction: column-reverse;
> }
> .led-level-vertical .led-level-limits{
> display: flex;
> justify-content: space-between;
> font-size: calc(var(--size) * .075 * 1px);
> line-height: calc(var(--size) * .075 * 1px);
> align-content: flex-start;
> align-items: start;
> flex-wrap: wrap;
> user-select: none;
> flex-direction: column;
> padding-inline-start: 0.4em;
> }
> .led-level-flat div{
> box-shadow: unset;
> }
> 
> ```

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/c/7/c747cd5b2c38e9a5a6bf9388d9399e1bd43ed3e6.png)

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [27 February 2024 13:44 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/2 "2024-02-27T13:44:13Z")

</div>

Compass

> **Code is HERE**
>
> ```auto
> <template>
> <div class="windrose" v-resize="onResize">
> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" width="100%" height="100%" viewBox="0 0 500 500">
> <g>
> <circle cx="250" cy="250" r="205" class="ring" />
> <circle cx="250" cy="250" r="205" class="tick major" pathLength="720"/>
> <circle cx="250" cy="250" r="200" class="tick mid" pathLength="720"/>
> <circle cx="250" cy="250" r="195" class="tick minor" pathLength="720"/>                
> </g>
> <g>
> <text transform="matrix(1 0 0 1 385 268)" class="letters">E</text>
> <text transform="matrix(1 0 0 1 233 420)" class="letters">S</text>
> <text transform="matrix(1 0 0 1 233 112)" class="letters">N</text>
> <text transform="matrix(1 0 0 1 82 268)" class="letters">W</text>
> </g>
> </svg>
> <div class="txt">
> <div>{{formattedDirection}}</div>
> <div class="small">DIRECTION</div>
> <div class="small">SPEED</div>
> <div>{{formattedSpeed}}</div>
> </div>
> <div ref="needle" class="needle" :style="{'rotate': direction +'deg'}"></div>
> </div>
> </template>
> 
> <script>
> export default {       
> data() {           
> return {              
> direction:0,
> speed:0,
> timeout:null
> }
> },
> watch: {
> msg: function(){           
> if(this.msg?.payload != undefined){
> if(this.msg.payload.direction != undefined){
> this.direction = this.shortWayRotation(this.direction, this.msg.payload.direction)
> if(this.msg.speed == undefined){
> this.msg.speed = this.speed
> }                                            
> }
> if(this.msg.payload.speed != undefined){
> this.speed = this.msg.payload.speed 
> if(this.msg.payload.direction == undefined){
> this.msg.payload.direction = this.direction;
> }                                          
> }
> this.send({payload:this.msg.payload})
> }
> }
> }
> ,
> computed: {
> formattedSpeed:function(){
> return this.speed + "m/s" 
> },
> formattedDirection:function(){
> let r = (this.direction + 360) % 360;
> if(r<0){
> r += 360;
> }
> return r + "°"
> }
> 
> },
> methods: {
> 
> onResize(){
> console.log('this',this)
> },
>             
> shortWayRotation:function(frm, to){               
> let delta = ((((to - frm) % 360) + 540) % 360) - 180;
> return (frm + delta);
> },
> manageAnimations:function(event){
>            
> if(document.hidden){                   
> if(this.$refs.needle){                        
> this.$refs.needle.style.transition = "rotate 0s"
> }
> }
> else{                   
> if(this.$refs.needle){
> if(this.timeout){
> clearTimeout(this.timeout)
> this.timeout = null
> }
> this.timeout = setTimeout(()=>{
> this.$refs.needle.style.transition = "rotate .5s"
> },40)
> 
>                         
> } 
> }
> }
>             
>             
> },        
> mounted() {
> document.addEventListener("visibilitychange", this.manageAnimations);
> window.addEventListener("blur", this.manageAnimations);
> window.addEventListener("focus", this.manageAnimations);      
>             
> },
>         
> unmounted() {
> if(this.timeout){
> clearTimeout(this.timeout)
> this.timeout = null
> }
> document.removeEventListener("visibilitychange", this.manageAnimations);
> window.removeEventListener("blur", this.manageAnimations);
> window.removeEventListener("focus", this.manageAnimations);
> }
> }
> </script>
> <style>    
> div:has(>.windrose){
> align-items: center;
> }
> .windrose{
> position:relative;
> width:100%;       
> aspect-ratio: 1;
> container-type: inline-size;
> }
> .tick {
> fill: none;
> stroke: currentColor;       
> }
> .major{
> stroke-dasharray:1 89;
> stroke-dashoffset:0.5;
> stroke-width:40px;
> }
> .mid{
> stroke-dasharray:0 45 1 44 0 45 1 44;
> stroke-dashoffset:0.5;
> stroke-width:30px;
> }
> .minor{
> stroke-dasharray:0 5 1 4 1 4 1 4 1 4 1 4 1 4 1 4 1 4;
> stroke-dashoffset:0.5;
> stroke-width:20px;
> }
> .letters {
> font-size: 48px;
> fill:currentColor;
> }
> .ring{
> fill:none;
> stroke:currentColor;
> stroke-width:40;
> opacity:0.1;
> }
> .needle{
> position: absolute;
> inset: 0;
> left: calc(50% - 1px);
> width: 2px;
> height: 100%;
> transform-origin: center;
> transition: rotate .5s;
> }
>     
> .needle:before, .needle:after{
> content: "▼";
> color: red;
> position: absolute;
> top: 1%;
> text-align: center;
> font-size: clamp(0.5rem, 10cqi, 2rem);
> transform: translateX(calc(-50% + 1px));
> }
> .needle:before{
> color:currentColor;
> font-size: calc(clamp(0.5rem, 10cqi, 2rem) + 4px);
> }
> .txt{
> position:absolute;
> inset:0;
> display: grid;
> place-content: center;
> width: 100%;
> text-align: center;        
> font-size: clamp(0.5rem, 12cqi, 3rem);
> font-weight:700;
> line-height:1.2em;
> }
> .txt .small{
> font-size: clamp(0.2rem, 6cqi, 1.5rem);
> line-height: 1em;
> font-weight:500;
> }
> </style>
> 
> ```

![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/d/3/d3ea9dc17736f564a7fc1c6da5ec236d4509745e.png)

The payload for compass must be combined to have speed and direction properties. (previously was topic = "direction") This is changed to have porper wakeup on replayMessage.

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [27 February 2024 15:15 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/3 "2024-02-27T15:15:21Z")

</div>

And as already got questions how to configure - this part of code needs to be found in template

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/5/e/5e5db8486ed58e9b9bfe78f8c345236887e0005c.png)

---

<div class="post-metadata">

**Author:** ![joepavitt](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/joepavitt/32/59722_2.png) [@joepavitt](https://discourse.nodered.org/u/joepavitt)\
**Post date:** [29 February 2024 12:04 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/4 "2024-02-29T12:04:13Z")

</div>

For what it's worth, trying to use the `vertical` style resulted in a no-show for the gauge, I needed to add a `min-height` here at the end of the CSS:

```css
.led-level-flat div {
    box-shadow: unset;
    min-height: 4px;
}

```

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [29 February 2024 12:36 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/5 "2024-02-29T12:36:11Z")

</div>

But have you tried to give it a bit more vertical space to live in?  
 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/2/b/2b7df21586c69f66c7d9434c3d3b79956be284f5.png)

Cos if something defines itself to be vertical...

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [5 March 2024 07:19 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/6 "2024-03-05T07:19:47Z")

</div>

Couldn't sleep this night too well, spent couple of night hours...

![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/7/6/760ecfdc0151e6a6b5424b5eeabf6048ea3eab75.png)

> **Code is HERE**
>
> ```auto
> <script>
> export default{
> data(){
> return {
> //define me here
> min:0,
> max:100,
> label:"Gauge",
> measurement:"Temperature",
> unit:"°C",
> majorDivision:10, // number of input units for each (numbered) major division
> minorDivision: 2, // number of input units for each minor division
> sectors:[{start:0,end:10,color:"skyblue"},{start:70,end:90,color:"#e99200"},{start:90,end:100,color:"red"}],
> valueDecimalPlaces: 1,
> majorDecimalPlaces:0,
> allowOverflow:true, // needle can travel over scale boundaries
> showUpdate:true,
> updateLabel:"Last update:",               
>                 
> //no need to change
> value:0,
> updateTime:0               
> }
> }
> }
> </script>
> <template>
> <div class="hn-sng">
> <div class="label">{{label}}</div>
> <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
> <g>
> <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector"
> stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
> </g>
> <g>
> <path class="tick-minor" :style="{'stroke-dasharray':minor }" pathLength="492" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90" ></path>
> <path ref="arc" class="tick-major" :style="{'stroke-dasharray':major }" pathLength="246" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
> </g>
> <g>
> <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" y="-37"
> :style="`rotate: ${item.r}deg;`">{{item.n}}</text>
> </g>
> <g>
> <text class="measurement" y="48" x="50%" text-anchor="middle">{{measurement}}</text>
> <text class="unit" y="75" x="50%" text-anchor="middle">{{unit}}</text>
> <text class="value" y="90" x="50%" text-anchor="middle">{{formattedValue}}</text>
> <text v-if="showUpdate" ref="update" class="update" y="100" x="0"
> text-anchor="left">{{formattedUpdate}}</text>
> </g>
> <g ref="o-needle" class="o-needle">
> <path d="M 0,0 -1.5,0 -0.25,-43 0.25,-43 1.5,0 z"></path>
> <circle cx="0" cy="0" r="3"></circle>
> </g>
> </svg>
> </div>
> </template>
> 
> <script>
> export default{ 
> methods:{
> getElement: function(name,base){
> if(base){
> return this.$refs[name]
> }
> return this.$refs[name][0]
> },
> validate: function(data){
> let ret                
> if(typeof data !== "number"){
> ret = parseFloat(data)
> if(isNaN(ret)){
> console.log("BAD DATA! gauge id:",this.id,"data:",data)
> return null
> }
> }                    
> else{
> ret = data
> }                
> return ret
> },
> range:function (n, p, r) {           
> if (p.maxIn > p.minIn) {
> n = Math.min(n, p.maxIn)
> n = Math.max(n, p.minIn)
> } else {
> n = Math.min(n, p.minIn)
> n = Math.max(n, p.maxIn)
> }
> if(r){
> return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);
> }
> return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;
> },
> generateNumbers:function(min,max){               
>                
> let nums = []
> let t = (max - min) / this.divisors.major;
> let i = min;
> let ro = -123
> let rp = 246 / this.divisors.major
>                
> for (let e = 0; e < this.divisors.major + 1; ++e){
> if(this.min > this.max){
> nums.unshift({n: i.toFixed(this.majorDecimalPlaces),r: ro + (e * rp) })
> }
> else{
> nums.push({n: i.toFixed(this.majorDecimalPlaces),r: ro + (e * rp) })
> }
>                     
> i = parseFloat(i + t);                   
> }
> return nums 
> },
> sectorData:function(full){  
> 
> let ret = []
> this.sectors.forEach((sector,idx) => {
> let sec = {name:'sector-'+idx,color:sector.color}
> const params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}
> const start = this.range(sector.start,params,false)
> const end = this.range(sector.end,params,false)
> const pos = Math.min(start, end)
> const span = Math.max(start, end) - pos
> sec.css = `0 ${pos} ${span} var(--dash)`
> ret.push(sec)
> })
> return ret
> },
> rotation:function(v){
> const overflow = this.allowOverflow ? (this.max-this.min) * 0.1 : 0
> const maxRotation = this.allowOverflow ? (123 * 1.2) : 123
> const min = this.min - overflow
> const max = this.max + overflow
> const params = {minIn:min, maxIn:max, minOut:-maxRotation, maxOut:maxRotation};
> if (v === null) {
> v = Math.min(min, max)
> }
> return this.range(v,params,false) +'deg'          
> }
> 
> },
> watch: {
> msg: function(){
> if(this.msg.payload != undefined){
> this.value = this.validate(this.msg.payload)
> this.updateTime = Date.now()                    
> this.getElement('o-needle',true).style.rotate = this.rotation(this.value)                    
> }
> }
> },
> computed: {            
> formattedValue: function () {
> return this.value !== null ? this.value.toFixed(this.valueDecimalPlaces) : "---"
> },
> formattedUpdate:function (){
> return this.updateLabel+" "+this.time
> },
> time:function(){
> return new Intl.DateTimeFormat('default', {hour: 'numeric', minute: 'numeric',second: 'numeric'}).format(this.updateTime);
> },
> numbers:function(){
> return this.generateNumbers(this.min,this.max)
> },
> major:function(){
> return "2," + ((246 / this.divisors.major)-2)           
> },
> minor:function(){
> let step = 492 / this.divisors.major / this.divisors.minor
> let line = "0," + step + ","
> step --
> for(let i=0;i<this.divisors.minor-1;++i){
> line += "1,"+step+","
> }
> line = line.slice(0, -1);
> 
> return line
> },
> offset:function(){
> return -1
> },
> divisors:function(){
> let span
> if(this.max > this.min){
> span = this.max - this.min
> }
> else{
> span = this.min - this.max                
> }
> const mad = this.majorDivision <= 0 ? 1 : this.majorDivision;
> const mid = this.minorDivision <= 0 ? 1 : this.minorDivision;
> const ma = span / mad
> const mi = span / ma / mid
> return {major:ma,minor:mi}
> }
> 
> },
> mounted(){
>            
> const dal = this.getElement('arc',true).getTotalLength()
> const sec = this.sectorData(dal)              
> const gauge = this.getElement('hn-gauge',true)
> gauge.style.setProperty('--dash',dal)
>            
> sec.forEach(s =>{
> const sector = this.getElement(s.name,false)
> sector.style.setProperty("stroke-dasharray",s.css)
> sector.style.setProperty("stroke",s.color)
> })
> this.getElement('o-needle',true).style.rotate = this.rotation(null)
> this.$nextTick(() => {
> this.getElement('o-needle',true).style.transition= "rotate .5s"
> })
> }
> }
> </script>
> <style>
> .hn-sng {
> position: relative;
> user-select: none;
> }
> 
> .hn-sng .label {
> position: absolute;
> font-size: 1rem;
> color: currentColor;
> text-align: center;
> width: 100%;
> overflow: hidden;
> white-space: nowrap;
> text-overflow: ellipsis;
> }
> 
> .hn-sng .value {
> fill: currentColor;
> font-weigth: 600;
> }
> 
> .hn-sng .unit {
> fill: currentColor;
> font-size: 0.4rem;
> }
> 
> .hn-sng .measurement {
> fill: currentColor;
> font-size: 0.5rem;
> }
> 
> .hn-sng .update {
> fill: currentColor;
> fill-opacity: 0.6;
> font-size: 0.35rem;
> }
> 
> .hn-sng .num {
> transform-origin: center 64.15%;
> transform: translate(50%, 64.15%);
> fill: currentColor;
> fill-opacity: 0.6;
> font-size: .35rem;
> }
> 
> .hn-sng .tick-minor {       
> stroke-dashoffset: 0;
> fill: none;
> stroke: currentColor;
> stroke-opacity: 0.6;
> }
> 
> .hn-sng .tick-major {
> stroke-dashoffset: 0.75;
> fill: none;
> stroke: currentColor;
> }
> 
> .hn-sng .sector {
> fill: none;
> stroke: transparent;
> }
> .hn-sng .sector:first-of-type {
> stroke-dashoffset:-0.5;
> }
> 
> .hn-sng .o-needle {
> transform-origin: center 64.15%;
> transform: translate(50%, 64.15%);
> transition: unset;
> }
> 
> .hn-sng .o-needle path,
> .hn-sng .o-needle circle {
> fill: currentColor;
> }
> </style>
> 
> ```

---

<div class="post-metadata">

**Author:** ![Steve-Mcl](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/steve-mcl/32/4826_2.png) [@Steve-Mcl](https://discourse.nodered.org/u/Steve-Mcl)\
**Post date:** [5 March 2024 07:37 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/7 "2024-03-05T07:37:42Z")

</div>

We are pleased your sleep was disturbed, may you have many more restless nights @hotNipi

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [5 March 2024 08:05 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/8 "2024-03-05T08:05:54Z")

</div>

Heh, the code doesn't know anything about it's birth moment in spacetime...

---

<div class="post-metadata">

**Author:** ![Colin](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/colin/32/17040_2.png) [@Colin](https://discourse.nodered.org/u/Colin)\
**Post date:** [5 March 2024 08:38 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/9 "2024-03-05T08:38:53Z")

</div>

What theme settings do you use to get that dark theme?

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [5 March 2024 08:42 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/10 "2024-03-05T08:42:22Z")

</div>

I don't care too much about the theme as beauty but I do test for light and dark by just changing Pages and Groups background to some dark or some arbitrary light

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/3/d/3d68dc244e68aaf17b2a3a708ebf23fa4d5ba9ff.png)

---

<div class="post-metadata">

**Author:** ![Colin](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/colin/32/17040_2.png) [@Colin](https://discourse.nodered.org/u/Colin)\
**Post date:** [5 March 2024 09:08 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/11 "2024-03-05T09:08:36Z")

</div>

OK, thanks. I was hoping you had found a combination that works well with the core nodes.

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [5 March 2024 09:15 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/12 "2024-03-05T09:15:26Z")

</div>

No if you mean that using vuetify things in ui\_template may end with non-contrasting colors.  
But for my creatures I do at least try to make it kind of self adoptive so the user can stay at the side of low code world. (No need directly to dig into complicated CSS world at least..)

---

<div class="post-metadata">

**Author:** ![Paul-Reed](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/paul-reed/32/66906_2.png) [@Paul-Reed](https://discourse.nodered.org/u/Paul-Reed)\
**Post date:** [5 March 2024 09:27 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/13 "2024-03-05T09:27:11Z")

</div>

> [@hotNipi](#):
>
> Type linear, round, vertical, or artless.

Are the 'linear' and 'vertical' gauges still working?  
The other gauges are working fine for me, but neither the 'linear' and 'vertical' gauge display any visual line.

![test](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/4/3/4388bf069ade9cc73e891af366d225a12eb155fb.gif)

---

<div class="post-metadata">

**Author:** ![Paul-Reed](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/paul-reed/32/66906_2.png) [@Paul-Reed](https://discourse.nodered.org/u/Paul-Reed)\
**Post date:** [5 March 2024 09:32 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/14 "2024-03-05T09:32:58Z")

</div>

Oh hang on... just looking at the first post in this topic... the CSS code has also changed, and updating my existing CSS template resolves the problem

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [5 March 2024 09:39 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/15 "2024-03-05T09:39:12Z")

</div>

Even if I didn't put styles for new stuff into common CSS, it's still better practice to have one instance of your custom CSS in separate thing.  
I do it that (wrong) way just to have it as single package to share.

---

<div class="post-metadata">

**Author:** ![Paul-Reed](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/paul-reed/32/66906_2.png) [@Paul-Reed](https://discourse.nodered.org/u/Paul-Reed)\
**Post date:** [5 March 2024 09:48 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/16 "2024-03-05T09:48:16Z")

</div>

If we added the style css to the template containing the gauge code, would that css then just influence **that instance** of the gauge, or would it influence all other gauges (or other widgets) in the same group.  
(I'm aware that if it's separate that we can apply a class to isolate them)

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [5 March 2024 09:56 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/17 "2024-03-05T09:56:57Z")

</div>

Choose the scope for CSS as needed.

![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/7/d/7d912a004613640c34a9782f2bf8212e0bc3ad15.png)

If it is "All pages" - you can put widgets on any page of your dashboard - they can reach defined rules.  
With the Single page the CSS is targeted only for that page and widgets on other page can't reach it.  
You can have different look and feel for different pages.

Having scoped CSS for widget itself - if you have many widgets with absolutely same CSS rules - it just gives bunch of work for browser to parse it and leave latest defined rule to affect. If CSS rules differ widget by widget - Maybe reasonable, but mostly indicates that it can be done much better.. It's just not realistic that all of the rules are completely different ...

---

<div class="post-metadata">

**Author:** ![arturv2000](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/arturv2000/32/88724_2.png) [@arturv2000](https://discourse.nodered.org/u/arturv2000)\
**Post date:** [6 March 2024 18:46 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/18 "2024-03-06T18:46:53Z")

</div>

Hi

Excellent job, the gauge is really nice and "versatile".

One thing i noticed, the code in "msg: function(){" where the payload is processed, the "if" that checks the return value from "this.validate(this.msg.payload)" should be comparing the return with "null" instead of "(!v)", otherwise if the payload is '0' the needle position and text is not updated.

Should be something like this.

```auto
msg: function(){
        if(this.msg.payload != undefined){                   
            const v = this.validate(this.msg.payload)                   
            if(v == null){
                return
            }
            this.value = v                    
            this.getElement('o-needle',true).style.rotate = this.rotation(this.value)                    
        }
    }

```

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [6 March 2024 18:52 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/19 "2024-03-06T18:52:28Z")

</div>

Oops I did it again .

Here's the fixed code with a bit of bonus 🎁 🙃

> **Code HERE**
>
> ```auto
> <template>
> <div class="hn-sng">
> <div class="label">{{label}}</div>
> <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
> <g>
> <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>                
> </g>
> <g>
> <path class="tick-minor" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
> <path ref="arc" class="tick-major" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
> </g>            
> <g>
> <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" y="-37" :style="`rotate: ${item.r}deg;`">{{item.n}}</text>
> </g>
> <g>
> <text class="measurement" y="48" x="50%" text-anchor="middle">{{measurement}}</text>
> <text class="unit" y="75" x="50%" text-anchor="middle">{{unit}}</text>
> <text class="value" y="90" x="50%" text-anchor="middle">{{formattedValue}}</text>
> <text v-if="showUpdate" ref="update" class="update" y="100" x="0" text-anchor="left">{{formattedUpdate}}</text>
> </g>
> <g ref="o-needle" class="o-needle">
> <path d="M 0,0 -1.5,0 -0.15,-40 0.15,-40 1.5,0 z"></path>
> <circle cx="0" cy="0" r="3"></circle>
> </g>
> </svg>
> </div>
> </template>
> <script>
> export default{
> data(){
> return {
> //define me here
> min:-50,
> max:60,
> unit:"kg",
> label:"Yet another gauge",
> measurement:"Potatoes",
> updateLabel:"Last update:",
> sectors:[{start:-28,end:-6,color:"#63cc25"},{start:5,end:20,color:"orange"},{start:20,end:60,color:"red"}],
> showUpdate:true,
> //no need to change
> value:0,
> updateTime:0                             
> }
> },
> methods:{
> getElement: function(name,base){
> if(base){
> return this.$refs[name]
> }
> return this.$refs[name][0]
> },
> validate: function(data){
> let ret                
> if(typeof data !== "number"){
> ret = parseFloat(data)
> if(isNaN(ret)){
> console.log("BAD DATA! gauge id:",this.id,"data:",data)
> return null
> }
> }                    
> else{
> ret = data
> }                
> return ret
> },
> range:function (n, p, r) {           
> if (n < p.minIn) {
> n=p.minIn;
> }
> if (n > p.maxIn) {
> n = p.maxIn;
> }
> if(r){
> return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);
> }
> return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;
> },
> generateNumbers:function(min,max){               
> const nums = [{r:238,n:0},{r:262,n:0},{r:286,n:0},{r:310,n:0},{r:335,n:0},{r:360,n:0},{r:385,n:0},{r:409,n:0},{r:434,n:0},{r:458,n:0},{r:123,n:0}];
> let t = (max - min) / 10;
> let i = min;
> for (let e = 0; e < 11; ++e){
> nums[e].n = i
> i = parseFloat((i + t).toFixed(2));                   
> }
> return nums 
> },
> sectorData:function(full){               
> let params, pos, span, sec, str
> let ret = [] 
> this.sectors.forEach((sector,idx) => {
> sec = {name:'sector-'+idx,color:sector.color}
> params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}                   
> pos = this.range(sector.start,params,false)
> params = {minIn:0, maxIn:this.max-this.min, minOut:0, maxOut:full}
> span = this.range(sector.end - sector.start,params,false)                    
> sec.css = "0 "+pos+" "+span+" var(--dash)"
> ret.push(sec)
> })
> return ret
> },
> rotation:function(v){
> const params = {minIn:this.min, maxIn:this.max, minOut:-123, maxOut:123};
> return this.range(v,params,false) +'deg'          
> }
> 
> },
> watch: {
> msg: function(){
> if(this.msg.payload != undefined){                   
> const v = this.validate(this.msg.payload)                   
> if(v == null){
> return
> }
> this.value = v
> this.updateTime = Date.now()                    
> this.getElement('o-needle',true).style.rotate = this.rotation(this.value)                    
> }
> }
> },
> computed: {
> formattedValue: function () {
> return this.value.toFixed(1)
> },
> formattedUpdate:function (){
> return this.updateLabel+" "+this.time
> },
> time:function(){
> return new Intl.DateTimeFormat('default', {hour: 'numeric', minute: 'numeric',second: 'numeric'}).format(this.updateTime);
> },
> numbers:function(){
> return this.generateNumbers(this.min,this.max)
> }                
> },
> mounted(){
>            
> const dal = this.getElement('arc',true).getTotalLength()
> const sec = this.sectorData(dal)              
> const gauge = this.getElement('hn-gauge',true)
> gauge.style.setProperty('--dash',dal)
>            
> sec.forEach(s =>{
> const sector = this.getElement(s.name,false)
> sector.style.setProperty("stroke-dasharray",s.css)
> sector.style.setProperty("stroke",s.color)
> })            
> 
> }
> 
> }
> </script>
> <style>
>     
> .hn-sng{
> position:relative;
> }
> .hn-sng .label{
> position:absolute;
> font-size:1rem;
> color:currentColor;
> text-align:center;
> width:100%;
> overflow: hidden;
> white-space: nowrap;
> text-overflow: ellipsis;
> }
> .hn-sng .value {
> fill:currentColor;
> font-weigth:600;
> }
> .hn-sng .unit {
> fill:currentColor;
> font-size:0.4rem;
> }
> .hn-sng .measurement {
> fill:currentColor;
> font-size:0.5rem;
> }
> .hn-sng .update {
> fill:currentColor;
> fill-opacity:0.6;
> font-size:0.35rem;
> }
> .hn-sng .num{
> transform-origin: center 64%;
> transform: translate(50%, 64%);
> fill:currentColor;
> fill-opacity:0.6;
> font-size:.35rem;
> }
> .hn-sng .tick-minor{
> stroke-dasharray: 0.5 0 0 3.53;
> stroke-dashoffset:-0.5;
> fill:none;
> stroke:currentColor;
> stroke-opacity:0.6;
> }
> .hn-sng .tick-major{
> stroke-dasharray: 2 0 0 18.1;
> fill:none;
> stroke:currentColor;
> }
> .hn-sng .sector{
> fill:none;
> stroke:transparent;
> }
> .hn-sng .o-needle{
> transform-origin: center 64%;
> transform: translate(50%, 64%);
> transition:rotate .5s;
> }
> .hn-sng .o-needle path, .hn-sng .o-needle circle{
> fill:currentColor;
> }
> </style>
> 
> ```

---

<div class="post-metadata">

**Author:** ![Colin](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/colin/32/17040_2.png) [@Colin](https://discourse.nodered.org/u/Colin)\
**Post date:** [6 March 2024 20:43 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/20 "2024-03-06T20:43:05Z")

</div>

I have managed to learn enough about html svg and dynamic styling to make some adjustments to @hotNipi's gauge. If anyone is interested, I have:

1. Added the ability to specify the major and minor division lengths in input units. In the example below major is 5 and minor 1.
2. Allowed the pointer to travel a bit past the end points so it is more obvious if an out of range value is received.
3. Increased the length of the pointer a bit.
4. Ensured that the numbers round the scale are always integers, by adding `toFixed(0)`. Without that, sometimes one could end up with numbers with lots of decimal digits which completely mess it up.
5. Fixed the problem in Watch msg that has been pointed out. On a side note, instead of

```auto
                    if(v === null){
                        return
                    }

```

could one use just

```auto
v ?? return

```

I haven't tried it but I think it should work.

![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/1/4/1475b88e7f952013bc5b2346647e59e7a062da43.png)

```auto
<!-- Gauge based on original work by @HotNipi -->
 <template>
    <div class="hn-sng">
        <div class="label">{{label}}</div>
        <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
            <g>
                <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>                
            </g>
            <g>
                <path class="tick-minor" stroke-width="5" d="M 10 90 A 47.5 47.5 0 1 1 90 90" :style="tickStyle(this.minorDivision, 0.5)"></path>
                <path ref="arc" class="tick-major" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90" :style="tickStyle(this.majorDivision, 2)"></path>
            </g>            
            <g>
                <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" y="-37" :style="`rotate: ${item.r}deg;`">{{item.n}}</text>
            </g>
            <g>
                <text class="measurement" y="48" x="50%" text-anchor="middle">{{measurement}}</text>
                <text class="unit" y="75" x="50%" text-anchor="middle">{{unit}}</text>
                <text class="value" y="90" x="50%" text-anchor="middle">{{formattedValue}}</text>
            </g>
            <g ref="o-needle" class="o-needle">
                <path d="M 0,0 -1.5,0 -0.15,-43 0.15,-43 1.5,0 z"></path>
                <circle cx="0" cy="0" r="3"></circle>
            </g>
        </svg>
    </div>
</template>
<script>
export default{
        data(){
            return {
                //define me here
                min:40,
                max:60,
                majorDivision: 5, // number of input units for each (numbered) major division
                minorDivision: 1, // number of input units for each minor division
                unit:"°C",
                label:"Hot Water",
                measurement:"Temperature",
                sectors:[{start:20,end:46,color:"blue"},{start:46,end:52,color:"green"},{start:52,end:60,color:"red"}],
                
                //no need to change
                value:0                             
            }
        },
        methods:{
            getElement: function(name,base){
                if(base){
                    return this.$refs[name]
                }
                return this.$refs[name][0]
            },
            validate: function(data){
                let ret                
                if(typeof data !== "number"){
                    ret = parseFloat(data)
                    if(isNaN(ret)){
                        console.log("BAD DATA! gauge id:",this.id,"data:",data)
                        return null
                    }
                }                    
                else{
                    ret = data
                }                
                return ret
            },
            range:function (n, p, r) {           
                if (n < p.minIn) {
                    n=p.minIn;
                }
                if (n > p.maxIn) {
                    n = p.maxIn;
                }
                if(r){
                    return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);
                }
                return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;
            },
            generateNumbers:function(min,max,majorDivision){               
                const minDegrees = 237.36
                const maxDegrees = 482.64
                const degRange = maxDegrees-minDegrees
                const degPerDiv = degRange * majorDivision/(max-min)
                let nums = []
                for (let degrees=minDegrees; degrees<=maxDegrees; degrees+=degPerDiv) {
                    const n = ((degrees-minDegrees)/degRange * (max-min) + min).toFixed(0)
                    nums.push({r: degrees, n: n})
                }
                return nums 
            },
            sectorData:function(full){               
                let params, pos, span, sec, str
                let ret = [] 
                this.sectors.forEach((sector,idx) => {
                    sec = {name:'sector-'+idx,color:sector.color}
                    params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}                   
                    pos = this.range(sector.start,params,false)
                    params = {minIn:0, maxIn:this.max-this.min, minOut:0, maxOut:full}
                    span = this.range(sector.end - sector.start,params,false)                    
                    sec.css = "0 "+pos+" "+span+" var(--dash)"
                    ret.push(sec)
                })
                return ret
            },
            rotation:function(v){
                // allow pointer to go 10% off ends of scale
                const overflow = (this.max-this.min)*0.1
                const maxAngle = 122.64 * 1.2 // 1.2 as this is only half the range, so need 20%
                const min = this.min - overflow
                const max = this.max + overflow
                const params = {minIn:min, maxIn:max, minOut:-maxAngle, maxOut:maxAngle};
                return this.range(v,params,false) +'deg'          
            },
            tickStyle: function(division, width) {
                // division is the number of input units per tick
                // width is the width (length?) of the tick in svg units

                // total arc length in svg units
                const arcLength = 203.356
                // length in user units
                const range = this.max - this.min
                const tickPeriod = division/range * arcLength
                // marker is width wide, so gap is tickPeriod-width
                // stroke-dashoffset sets the first tick to half width
                return `stroke-dasharray: ${width} ${tickPeriod-width}; stroke-dashoffset: ${width/2};`
            },
        },
        watch: {
            msg: function(){
                if(this.msg.payload != undefined){                   
                    const v = this.validate(this.msg.payload)                   
                    if(v === null){
                        return
                    }
                    this.value = v
                    this.getElement('o-needle',true).style.rotate = this.rotation(this.value)                    
                }
            }
        },
        computed: {
            formattedValue: function () {
                return this.value.toFixed(1)
            },
            numbers:function(){
                return this.generateNumbers(this.min,this.max,this.majorDivision)
            },
        },
        mounted(){
           
            const dal = this.getElement('arc',true).getTotalLength()
            const sec = this.sectorData(dal)              
            const gauge = this.getElement('hn-gauge',true)
            gauge.style.setProperty('--dash',dal)
            sec.forEach(s =>{
                const sector = this.getElement(s.name,false)
                sector.style.setProperty("stroke-dasharray",s.css)
                sector.style.setProperty("stroke",s.color)
            })

        }

    }
</script>
<style>
    .hn-sng{
        position:relative;
    }
    .hn-sng .label{
        position:absolute;
        font-size:1rem;
        color:currentColor;
        text-align:center;
        width:100%;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
    .hn-sng .value {
        fill:currentColor;
    }
    .hn-sng .unit {
        fill:currentColor;
        font-size:0.4rem;
    }
    .hn-sng .measurement {
        fill:currentColor;
        font-size:0.5rem;
    }
    .hn-sng .num{
        transform-origin: center 64%;
        transform: translate(50%, 64%);
        fill:currentColor;
        fill-opacity:0.6;
        font-size:.35rem;
    }
    .hn-sng .tick-minor{
        fill:none;
        stroke:currentColor;
        stroke-opacity:0.6;
    }
    .hn-sng .tick-major{
        fill:none;
        stroke:currentColor;
    }
    .hn-sng .sector{        
        fill:none;
        stroke:transparent;      
    }    
    .hn-sng .o-needle{        
        transform-origin: center 64%;
        transform: translate(50%, 64%);
        transition:.5s;
    }    
    .hn-sng .o-needle path, .hn-sng .o-needle circle{
        fill:red;
    }
    
</style>

```

[Next page](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955.md?page=2)
