# 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:** 7

<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: [17 April 2024 14:47 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/121 "2024-04-17T14:47:39Z")

</div>

> [@thebaldgeek](#):
>
> I miss sparklines

Wont be long (they are in labs): [Sparklines — Vuetify](https://vuetifyjs.com/en/components/sparklines/)

---

<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: [17 April 2024 15:34 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/122 "2024-04-17T15:34:36Z")

</div>

> [@thebaldgeek](#):
>
> to give them a little more 'breathing' room between each gauge?

The space between them is the gap what is defined by dashboard.  
Widget takes 100% available space.

If you don't want to mess with provided styles for all gauges in all situations but just in this card or dedicated widgets only - add a class to them to override appearance of widget

```auto
.my-level .led-level{
    padding-inline:5px;
}

```

where `my-level` is that classname you wrote where you wont it to be applied.

Otherwise to add left and right padding for all of them

```auto
.led-level{
    padding-inline:5px;
}

```

Whole widget can be made responsive by all means but only for dedicated use cases. It takes to break the layout at those exact sizes you as the creator of the page will define.

---

<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: [17 April 2024 16:00 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/123 "2024-04-17T16:00:58Z")

</div>

The responsiveness of DB2 is not magical resolver of every possible use cases. It still requires to make some compromises like placing and sizing your widgets so that they look OK on all your screens. That means you can't have that many widgets in one row if they don't fit with the smallest screen you are using.

But saying so - you have the source code of the level - you can make it to be multiple of them in one container which you can control and then make your own placement/sizing rules inside that container so it fits always.

---

<div class="post-metadata">

### Author: ![E1cid](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/e1cid/32/77971_2.png) [@E1cid](https://discourse.nodered.org/u/E1cid)
#### Post date: [8 May 2024 17:27 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/124 "2024-05-08T17:27:17Z")

</div>

8 posts were split to a new topic: [Dash 2.0 Sparkline](https://discourse.nodered.org/t/dash-2-0-sparkline/87814)

---

<div class="post-metadata">

### Author: ![Wolfe](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/wolfe/32/108255_2.png) [@Wolfe](https://discourse.nodered.org/u/Wolfe)
#### Post date: [25 May 2024 08:37 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/125 "2024-05-25T08:37:01Z")

</div>

That's really cool.

Does anyone has an idea how to implement this in Grafana, too ?

---

<div class="post-metadata">

### Author: ![Giamma](https://avatars.discourse-cdn.com/v4/letter/g/e47774/32.png) [@Giamma](https://discourse.nodered.org/u/Giamma)
#### Post date: [7 July 2024 05:42 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/126 "2024-07-07T05:42:01Z")

</div>

@hotNipi

It's correct or it's a typo?

 ![Screenshot_20240707_073747](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/1/9/196043f4a4e6e470c10d6a389f5ff55ca99f41a0.png)

---

<div class="post-metadata">

### Author: ![BartButenaers](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/bartbutenaers/32/10476_2.png) [@BartButenaers](https://discourse.nodered.org/u/BartButenaers)
#### Post date: [7 July 2024 06:24 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/127 "2024-07-07T06:24:02Z")

</div>

Hi @Giamma,  
I had the same doubt as you last week, because I hadn't seen it before in my life. It is used for [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining).  
Bart

---

<div class="post-metadata">

### Author: ![Giamma](https://avatars.discourse-cdn.com/v4/letter/g/e47774/32.png) [@Giamma](https://discourse.nodered.org/u/Giamma)
#### Post date: [7 July 2024 06:35 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/128 "2024-07-07T06:35:32Z")

</div>

Ok, thank you ....

---

<div class="post-metadata">

### Author: ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)
#### Post date: [7 July 2024 10:26 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/129 "2024-07-07T10:26:04Z")

</div>

Now that newer versions of node.js, it is safe to use that in runtime code.

However, you probably shouldn't use it for front-end browser code since it only started being supported early 2020. iOS support came with v13.4 in March 2020. So if there is a chance that your users might have older devices, best avoided.

> **["optional chaining" | Can I use... Support tables for HTML5, CSS3, etc](https://caniuse.com/?search=optional%20chaining)**
>
> "Can I use" provides up-to-date browser support tables for support of front-end web technologies on desktop and mobile web browsers.

I target my front-end code for early 2019 as a rule as this seems to cover all but the oldest devices. One way around this issue is to use a build-tool such as ESBUILD to produce production, minified versions of front-end code. It lets you write current generation code but can build to any previous JavaScript or browser version.

---

<div class="post-metadata">

### Author: ![Giamma](https://avatars.discourse-cdn.com/v4/letter/g/e47774/32.png) [@Giamma](https://discourse.nodered.org/u/Giamma)
#### Post date: [7 July 2024 15:48 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/130 "2024-07-07T15:48:47Z")

</div>

Thank you ...

---

<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: [1 August 2024 10:54 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/131 "2024-08-01T10:54:09Z")

</div>

I was kindly asked for a bit redesign for horizontal level (add icon, spread dots evenly, make it more responsive, get rid of filter usage ...) A bit dedicated design or so. But I'll share anyway 😃

It uses container query's for responsiveness. Those are set to solid values so the break points may need adjustments if you plan to use it on your dashboard. (break points can't be dynamically adjustable)

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

> **Code**
>
> ```auto
> <template>
> <div class="led-level">
> <div :class="icon ? 'led-level-grid-2':'led-level-grid-1'">
> <div v-if="icon" class="led-level-icon">
> <v-icon aria-hidden="false">{{icon}}</v-icon>
> </div>
> <div class="led-level-content">
> <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">
> <div v-for="(color, index) in colors" :key="index" class="led-level-led" :ref="'dot-' + index"></div>
> </div>
> <div class="led-level-limits">
> <span class="led-level-limit">{{min}}</span>
> <span v-for="(tick, index) in ticks" :key="index" class="led-level-limit" :ref="'tick-' + index">{{tick}}</span>
> <span class="led-level-limit">{{max}}</span>
> </div>
> </div>
> </div>
> </div>
> </template>
> <script>
> export default {
> data(){
> return {
> //Define me here
>                                                
> label:"Level long name", // The label
> icon:"mdi-account", //mdi-account (optional) the icon           
> min:0, // Smallest expected value
> max:100, // Highest expected value
> unit:"cm³",// The unit of the measurement
> dim:0.2, //How dim is led when not glowing 
>             
> animate:true, // Animating led's is not most performant thing in the world. 
> 
> colors:[ 
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300",
> "#00e300", 
> "#ffa916", 
> "#ffa916", 
> "#ffa916", 
> "#ff4c16",
> "#ff4c16"],
>                 
> ticks:[50,75], //optional tick values          
>             
> //no need to change those
> value:0,
> previousValue:0,         
> 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! 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;
> },
>         
> tickPosition: function(tv){
>         
> return Math.floor(((tv - this.min) / (this.max - this.min)) * 100)+'%';
> },
>         
> 
> lit: function(){
> if(this.inited == false){
> return
> }
> const down = this.previousValue > this.value
> let time = down ? 1 : 0.2
> let step = down ? 0.12 : 0.06
> 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.opacity = 1;
> time += step
> }
> else if(i==this.full()){                    
> dot.style.opacity = this.half()                                   
> }
> else{                  
> dot.style.opacity = this.dim                                  
> }
> if(down){                  
> time -= step                                 
> }
> else{
> time -= step
> }
> dot.style.transition = this.animate && time > 0 ? "opacity "+time+"s" : "unset";
> })
> this.previousValue = this.value
> }    
> },
> watch: {
> msg: function(){    
> if(this.msg.payload !== undefined){  
> const v = this.validate(this.msg.payload)                
> if(v === null){
> return
> }         
> this.value = v                
> this.lit()              
> }
> }
> },
> computed: {
> formattedValue: function () {
> return this.value.toFixed(2)
> },
> percentage: function(){
> return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);
> }
> },
> mounted(){         
> this.colors.forEach((c,i) => {
> let dot = this.getElement("dot-"+i);
> if(!dot){                        
> return
> }                    
> dot.style.backgroundColor = c                    
> }
> );
> this.ticks.forEach((t,i) => {
> let tick = this.getElement("tick-"+i);
> if(!tick){                        
> return
> }                    
> tick.style.left = this.tickPosition(t)
> }
> ); 
> 
> this.inited = true;
> },
> unmounted () {
>        
> }
> }
> </script>
> 
> ```

> **CSS**
>
> ```auto
> .led-level{
> container-type: inline-size;
> container-name: led-level;
> --break-point:160;
> }
> 
> .led-level-grid-2 {    
> display:grid;    
> grid-template-columns: 2rem auto;
> gap:1rem;   
> height: 100%;
> }
> .led-level-grid-1 {
> display:grid;    
> grid-template-columns: 1fr;
> gap:1rem;   
> height: 100%;
> }
> @container led-level (max-width: 160px) { 
> .led-level-grid-2{
> grid-template-columns: 1rem auto;
> }
> .led-level-icon .v-icon{
> font-size: calc(var(--v-icon-size-multiplier)* 2em) !important;
> }
> }
> .led-level-icon{    
> display:flex;
> justify-content: center;
> align-items: center;    
> }
> .led-level-icon .v-icon{
> font-size: calc(var(--v-icon-size-multiplier)* 3em);
> }
> .led-level-content {
> container-type: inline-size;
> container-name: level-content;     
> display: grid;
> grid-template-rows: 1.3em minmax(3px, 1fr) .7em;
> gap: 2px;
> height: 100%;
> }
> .led-level-text {
> font-size: 1.25em;
> line-height: 1em;
> align-self: end;
> display: flex;    
> justify-content: space-between;
> user-select: none;
> overflow: auto;
> }
> .led-level-label{
> font-size: 1rem;
> text-overflow: ellipsis;
> white-space: nowrap;
> overflow: hidden;
> }
> .led-level-value {
> font-weight: bold;
> }
> .led-level-unit {
> font-size: .75em;
> font-weight: normal;
> padding-inline-start: 0.15em;
> }
> .led-level-stripe {
> display: flex;    
> }
> .led-level-led {
> background: #ffffff;
> width: 100%;
> height: 100%;    
> border-left: 1px solid rgb(var(--v-theme-group-background));
> border-right: 1px solid rgb(var(--v-theme-group-background));
> border-radius: 0px;    
> }
> @container level-content (max-width: 150px) {
> .led-level-led:nth-child(even){
> display:none;
> }
> .led-level-limit:not(:first-child):not(:last-child){
> display:none;
> }
> }
> @container level-content (max-width: 130px) {
> .led-level-label{
> display:none;
> }
> .led-level-text{
> justify-content:flex-end;
> }
> }
> 
> .led-level-limits {
> display: block;
> position: relative;
> font-size: .75em;
> line-height: 1;   
> user-select: none;
> }
> .led-level-limit{
> position:absolute;
> transform:translate(-50%,0);
> }
> .led-level-limit:first-child{
> left:0;
> transform: translate(0, 0);
> }
> .led-level-limit:last-child{
> right:0;
> transform: translate(0, 0);
> }
> 
> ```

---

<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: [2 October 2024 19:17 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/132 "2024-10-02T19:17:35Z")

</div>

What about this one? Responsive and stuff... 😃

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

Yes, you'll need to have dark background for it. Long way to be a widget ...

> **CODE**
>
> ```auto
> <template>
> <div class="fg-body">
> <div class="fg-display">
> <div class="fg-backplate">
> <div ref="display" class="fg-numbers" >
> <div v-for="(n, index) in numbers" :key="index" class="fg-num">{{n}}            
> </div>
> </div>
> <div class="fg-needle"></div>
> </div>    
> </div>
> </template>
> 
> <script>
> export default {
> data() {           
> return {
> min:0,
> max:100,
> value:0
> }
> },
>        
> computed: {
> numbers:function(){
> var t = (this.max - this.min) / 10;               
> let i = this.min;
> let r = []
> for (let e = 0; e < 11; ++e){
> r.push(i),                    
> i = parseFloat((i + t).toFixed(2));
> }
> return r 
> }
>             
> },
> methods: {
> range :function (n, p, a, r) {
> if (a == "clamp") {
> if (n < p.minin) { 
> n=p.minin; 
> } 
> if (n> p.maxin) {
> n = p.maxin;
> }
> }
> if (a == "roll") {
> let d = p.maxin - p.minin;
> n = ((n - p.minin) % d + d) % d + p.minin;
> }
> let v = ((n - p.minin) / (p.maxin - p.minin) * (p.maxout - p.minout)) + p.minout;
> if (r) {
> v = Math.round(v);
> }
> return v
> },
> percentage: function(){
> let p = 100 - ((this.value - this.min) / (this.max - this.min)) * 100
> let params = {minin:0, maxin:100, minout:2.5, maxout:97.5};
> 
> return this.range(p,params)
> },
> 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
> },
> move(){
> const display = this.$refs.display;
> if(!display){
> return
> }
> display.style.left = this.percentage()+"%" 
> }
>          
> },
> watch: {           
> msg: function(){    
> if(this.msg.payload !== undefined){  
> const v = this.validate(this.msg.payload)                
> if(v === null){
> return
> }         
> this.value = v
> this.move()
> }
> }
>             
> },
> mounted() {
>             
> },
> unmounted() {
>            
> }
> }
> </script>
> <style>
> .fg-body {
> position: relative;
> width: 100%;
> height: 100%;
> display: flex;
> align-content: center;
> flex-wrap: wrap;
> }
> 
> .fg-numbers {
> position:relative;
> display: flex;
> flex-direction: row;
> justify-content: space-between;
> height: 100%;
> font-size: 1.25rem;
> word-wrap: normal;
> align-content: center;
> flex-wrap: wrap;
> transform: translateX(-50%);
> transition: left 1s ease-in-out;
> }
> 
> .fg-backplate {
> width: 600px;
> flex-shrink: 0;
> position: relative;        
> }
> 
> .fg-numbers:before {
> content: "";
> position: absolute;
> right: 0%;
> width: 100%;
> top: 0px;
> height: 25%;
>        
> background: linear-gradient(to right,
> #00000050,
> #00000050 50%,
> transparent 50%,
> transparent);
> background-size: 0.6% 100%;
> }
> 
> .fg-numbers:after {
> content: "Gauges by hotNipi ®";
> position: absolute;
> font-size: 9px;
> left: -90px;
> top: 9px;
> color: #00000080;
> }
> 
> .fg-display {
> width: 100%;
> height: 100%;
> display:flex;
> justify-content: center;
> position: relative;
> box-sizing: border-box;
> overflow: hidden;
> border: 3px solid black;
> background-color: #cfcfcf;
> outline: 1px solid #4d4d4d;
> outline-offset: -2px;
> border-radius: 6px;
> box-shadow: inset 0 0 9px 1px black;
> }
> 
> .fg-display:before,
> .fg-display:after {
> content: "";
> position: absolute;
> inset: 0;
> 
> }
> 
> .fg-display:before {
> background: linear-gradient(270deg, #000000b0, transparent 20%);
> }
> 
> .fg-display:after {
> background: linear-gradient(90deg, #000000b0, transparent 20%);
> }
> 
> .fg-num {
> position: relative;
> text-align: center;
> top: 3px;
> width: 30px;
> color: black;        
> }
> 
> .fg-needle {
> position: absolute;
> left: calc(50% - 1px);
> top: 1px;
> width: 2px;
> height: 21px;
> background-color: #eb1e1e;
> box-shadow: 0px 0px 7px 2px #0000008c;
> }
> </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: [2 October 2024 20:10 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/133 "2024-10-02T20:10:08Z")

</div>

As always. Super nice.

---

<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: [4 October 2024 15:30 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/134 "2024-10-04T15:30:45Z")

</div>

A call to try it and maybe you have ideas to make it better smarter fancier ...  
It is going to be a widget. `edgewise-meter`

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

Just copy the code into the ui\_template and go.

> **CODE**
>
> ```auto
> <template>   
> <div class="fg-wrapper" :style="styles">
> <div v-if="label" class="fg-label">{{label}}</div>
> <div class="fg-body" :class="{'fg-dark': dark, 'fg-light': !dark}">
> <div ref="display" class="fg-display">
> <div class="fg-backplate">
> <div ref="plate" class="fg-numbers" >
> <div v-for="(n, index) in numbers" :key="index" class="fg-num">{{n}}</div>                                    
> </div>
> <div v-if="sizeError" class="fg-size-error fg-blonk">
> <p>The configured size is too small. The digits cannot be placed in exact locations.</p>
> </div>
> </div>
> <div ref="needle" class="fg-needle"></div>
> </div>    
> </div>
> </div>
> </template>
> 
> <script>
> export default {
> data() {           
> return {              
> min:0,
> max:10,
> count:20,// (even int) not all min - max - count combinations can be nicely divided to make a scale. real widget will help with that.   
> size:800,// adjust space between digits. too small will show runtime error
> dark:false, // turn on if the group background is dark 
> sectors:[{start:0,color:"lightblue"},{start:3,color:"transparent"},{start:7,color:"#ffc55c"},{start:9,color:"#fc5b5b"}],
> label:"T-out °C",
> smallDigits:true,                
> logo:"",
> 
> //no need to change 
> value:0,
> sizeError:false,
> }
> },
>        
> computed: {
>             
> numbers:function(){
> var t = (this.max - this.min) / this.count;               
> let i = this.min;
> let r = []
> for (let e = 0; e < this.count+1; ++e){
> r.push(i),                    
> i = parseFloat((i + t).toFixed(2));
> }
> return r 
> },
> styles:function() {
> return {                   
> "--logo": this.logo ? JSON.stringify(this.logo) : undefined,
> "--size": this.size,
> "--small-digits":this.smallDigits ? "small" : "inherit",
> "--gradient":this.sectorGradient(),
> "grid-template-columns":this.label ? "auto 1fr" : "1fr",
> }
> }            
> },
> methods: {
> sectorGradient: function(){
> let gradient = "repeating-linear-gradient(to right, #00000040, #00000040 40%, transparent 40%, transparent)"
>               
> if(this.sectors.length == 0){
> return gradient
> }
> 
> gradient = gradient.concat(', linear-gradient(to right,')
> let pos
> let total = this.sectors.length -1
>               
> this.sectors.forEach((s,i) => {
> if(i == 0){
> if(s.start == this.min){
> gradient = gradient.concat(s.color,", ")
> if(i + 1 <= total){
> pos = this.position(this.sectors[i+1].start).toString()
> gradient = gradient.concat(s.color," ",pos,"%, ")
> }
> }
> else{
> gradient = gradient.concat('transparent, transparent ')
> pos = this.position(s.start).toString()
> gradient = gradient.concat(pos,"%, ")
> if(i + 1 <= total){
> pos = this.position(s.start).toString()
> gradient = gradient.concat(s.color," ",pos,"%, ")
> pos = this.position(this.sectors[i+1].start).toString()
> gradient = gradient.concat(s.color," ",pos,"%, ")
> }
> }
> }
> else if(i == total){
> pos = this.position(s.start).toString()
> gradient = gradient.concat(s.color," ",pos,"%, ")
> gradient = gradient.concat(s.color)
> } 
> else{
> pos = this.position(s.start).toString()
> gradient = gradient.concat(s.color," ",pos,"%, ")
> if(i + 1 <= total){
> pos = this.position(this.sectors[i+1].start).toString()
> gradient = gradient.concat(s.color," ",pos,"%, ")
> }
> }
> })
> gradient = gradient.concat(")")
> return gradient
> },
> range :function (n, p, a, r) {
> if (a == "clamp") {
> if (n < p.minin) { 
> n=p.minin; 
> } 
> if (n> p.maxin) {
> n = p.maxin;
> }
> }
> if (a == "roll") {
> let d = p.maxin - p.minin;
> n = ((n - p.minin) % d + d) % d + p.minin;
> }
> let v = ((n - p.minin) / (p.maxin - p.minin) * (p.maxout - p.minout)) + p.minout;
> if (r) {
> v = Math.round(v);
> }
> return v
> },
> position: function(target){
> const v = target ?? this.value
> const l = this.size/(this.count+1)/2
> const d = l * 100 / this.size                
> let p = ((v - this.min) / (this.max - this.min)) * 100
> p = target ? p : 100 - p
> 
> const params = {minin:0, maxin:100, minout:d, maxout:100-d};
> return this.range(p,params)
> },
>             
>             
> 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
> },
> move(){
> const plate = this.$refs.plate;
> const display = this.$refs.display;
> if(!display || !plate){
> return
> }
> plate.style.left = this.position()+"%"  
> }
>          
> },
> watch: {           
> msg: function(){    
> if(this.msg.payload !== undefined){  
> const v = this.validate(this.msg.payload)                
> if(v === null){
> return
> }         
> this.value = v
> this.move()
> }
> }
>             
> },
> mounted() {
> setTimeout(()=>{
> const plate = this.$refs.plate;
> const needle = this.$refs.needle
> if(!plate || !needle){
> return
> }
> let w = 0                
> Array.from(plate.children).forEach(n => {
> w += parseFloat(getComputedStyle(n).getPropertyValue('width'))                   
> })
> if(w - this.size > 0.2){ 
> this.sizeError = true             
> plate.classList.add('fg-blink')
> needle.classList.add('fg-blink')
> 
> }
> },100)           
>            
> 
> },
> unmounted() {
>            
> }
> }
> </script>
> <style>
> 
> .fg-light{
> --mix:20%;
> --fg-shadow:rgba(var(--v-theme-on-group-background),var(--mix));
> --fg-background:rgb(var(--v-theme-group-background));
> --fg-outline:rgba(var(--v-theme-on-group-background),40%);
> }
> .fg-dark{
> --mix: 75%;
> --fg-shadow: rgba(var(--v-theme-group-background), var(--mix));
> --fg-background: rgb(var(--v-theme-on-group-background));
> --fg-outline: rgba(var(--v-theme-group-background), 100%);
> }
> .fg-wrapper{
> display: grid;   
> gap: 1rem;
> align-items: center;
> height: 100%;
> }
> .fg-label {
> min-width: 1rem;
> overflow: hidden;
> white-space: nowrap;
> text-overflow: ellipsis;
> }
> .fg-body {   
> position: relative;
> min-width: 3rem;
> width: 100%;
> height: 100%;
> display: flex;
> align-content: center;       
> }
> 
> .fg-body.fg-light {    
> filter: drop-shadow(0px 2px 4px var(--fg-shadow));
> }
> 
> .fg-display {
> width: 100%;
> height: 100%;
> display:flex;
> justify-content: center;
> position: relative;
> box-sizing: border-box;
> overflow: hidden;
> border: 3px solid var(--fg-shadow);
> background-color: var(--fg-background);
> outline: 1px solid var(--fg-outline);
> outline-offset: -2px;
> border-radius: 6px;
> box-shadow: inset 0 0 9px 1px var(--fg-shadow);
> transition: background-color 1s;
> }
> 
> .fg-display::after {
> content: "";
> position: absolute;
> inset: 0;
> background: linear-gradient(90deg, var(--fg-shadow), transparent 30%, transparent 70%, var(--fg-shadow));
> }
> 
> .fg-backplate {
> width: calc(var(--size) * 1px);
> flex-shrink: 0;
> position: relative;        
> }
> 
> .fg-numbers {
> position:relative;
> display: flex;
> flex-direction: row;
> justify-content: space-between;
> height: 100%;
> font-size: 1.25rem;
> word-wrap: normal;
> align-content: center;   
> transform: translateX(-50%);
> transition: left 1s ease-in-out;
> }
> 
> .fg-numbers :nth-child(even){
> font-size: var(--small-digits);
> }
> 
> .fg-numbers:after {
> content: "";
> position: absolute;
> right: 0%;
> width: 100%;
> top: 0px;
> height: 25%;    
> background: var(--gradient,repeating-linear-gradient(to right, #00000040, #00000040 40%, transparent 40%, transparent));
> background-size: 4px, 100%;
> }
> .fg-numbers:before {
> content: var(--logo,"Made in Estonia hotNipi ® Gauges");
> position: absolute;
> font-size: xx-small;   
> text-align: end;
> inset: 0;
> width: 20ch;
> translate: -120%;
> color: var(--fg-outline);
> }
> 
> .fg-num {
> position: relative;
> text-align: center;   
> width: 100%;
> color: black;
> align-self: flex-end;
> border-bottom: 3px solid transparent;          
> }
> 
> .fg-needle {
> position: absolute;
> left: calc(50% - 0.5px);
> top: 0;
> width: 1px;
> height: 100%;
> background-color: #eb1e1e;
> box-shadow: 0px 0px 7px 2px var(--fg-shadow);
> }
> .fg-dark .fg-needle {
> box-shadow: 0px 0px 12px 1px var(--fg-shadow);
> }
> .fg-size-error {
> position: absolute;
> inset: 0;
> font-size: x-small;
> color:red;
> text-align: center;
> display: flex; 
> justify-content:center;
> }
> .fg-size-error > p{
> max-width: 140px;
> }
> 
> @keyframes blink {
> 50% {
> opacity: 0.0;
> }
> }
> .fg-blink{   
> animation: blink 4s step-start 0s infinite;
> }
> .fg-blonk{    
> animation: blink 4s step-start 2s infinite;
> }
> 
> </style>
> 
> ```

---

<div class="post-metadata">

### Author: ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)
#### Post date: [4 October 2024 16:29 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/135 "2024-10-04T16:29:25Z")

</div>

For fun, I just asked ChatGPT 4o the following question:

> Create a web component that outputs a visible gauge that looks like the attached image. The output must scale and be responsive to page sizes and layouts and user font size preferences. The gauge's scale changes left and right, the needle stays static in the centre. The gauge mimics a physical edgewise meter. There should be attributes to set the maximum and minimum ranges, an optional label and the HTML output should be reasonably accessible (at least WCAG 2.2 A).

It has given me an answer - no idea whether it will work and it won't let me share it I'm afraid. I'll give it a go and see if it makes any sense at all.

🧙‍♂️

---

<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: [4 October 2024 16:36 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/136 "2024-10-04T16:36:30Z")

</div>

It actually is simple thing to create with plain html/css but what makes it complex is the configuration options and forced container system where it will be used. So no doubt the AI can make some plain and straight thing. I'll stay away. Making things from scratch is fun.

---

<div class="post-metadata">

### Author: ![jbudd](https://avatars.discourse-cdn.com/v4/letter/j/5f8ce5/32.png) [@jbudd](https://discourse.nodered.org/u/jbudd)
#### Post date: [4 October 2024 16:57 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/137 "2024-10-04T16:57:01Z")

</div>

It's a lovely gauge, I can almost see the moving coil hauling the scale backwards and forwards!  
"Made in Estonia" is a nice touch too. 😀

I see that your .fg-numbers div has an ease-in-out transition, but it looks to me rather abrupt.

What do you think of a bezier transition to simulate moving a significant weight, with a little overrun?

![default](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/1/c/1c2591fa195d2362b2cf9f0fe047da86e0cfa908.gif)  
Default

![bezier](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/c/1/c1401cb314b3ccadf2fe26c056306362fabe4dc8.gif)  
transition: left 1s cubic-bezier( 0.6, 0.06, 0.132, 1.15 );

I would like the numbers to fade a little out of focus at the sides. I tried to come up with a frosted glass overlay to do this but can't work out how to fade the effect in and out.

---

<div class="post-metadata">

### Author: ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)
#### Post date: [4 October 2024 17:17 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/138 "2024-10-04T17:17:39Z")

</div>

> [@hotNipi](#):
>
> Making things from scratch is fun.

I like the creativity but I'm lazy enough to be happy if something does some heavy lifting for me so I can concentrate on the design rather than the raw engineering. The architects mindset perhaps. 🙂

I very much doubt that ChatGPT will come at all close to something finely engineered like your gauges though @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: [4 October 2024 17:22 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/139 "2024-10-04T17:22:22Z")

</div>

> [@jbudd](#):
>
> "Made in Estonia"

It is 😉 , but I doubt the one deliberately don't use the "logo" option ..

> [@jbudd](#):
>
> ease-in-out transition, but it looks to me rather abrupt

It is matter of taste kind of. I don't think it should be something the user should be able to easily configure but as CSS overrides are fee and easy to use , I think that is enough of freedom for that case.

> [@jbudd](#):
>
> numbers to fade a little out of focus at the sides

I tried. And it is possible and stuff. But I have a friend with pretty good understanding of digital art and all that flew out the window.  
Key points :

1. Always readable
2. Muddy look is no no
3. Easy to render
4. If it doesn't make it better it makes it worse
5. Less is more

There was more 😃

> [@jbudd](#):
>
> the moving coil hauling the scale backwards and forwards!

Just needs a bit of screeching sound to be perfect ..

---

<div class="post-metadata">

### Author: ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)
#### Post date: [4 October 2024 18:21 UTC](https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/140 "2024-10-04T18:21:32Z")

</div>

> [@hotNipi](#):
>
> I'll stay away. Making things from scratch is fun.

Well, you will doubtless be glad to know that your creativity and engineering skills are still safe for now - I'm not sure just yet what it produced but it certainly does not work! 😊

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

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