Showing gauges in UIbuilder

I've been terribly remis in not getting hotNipi's great gauge component out there after his generosity in sharing it. :frowning: Too much life getting in the way.

It is a HTML web component - I've made a few tweaks to it but it probably still needs a few more and some documentation - still it works (or it did when hotNipi sent it over :slight_smile:, I think I messed up the rotation calculation somewhere so that can look a bit odd - let me know if you find my error ) so presented here as is. Ultimately, I'll turn it into an npm published version so it can be used anywhere you want to use it but with some extra magic built in to make it easy to use with uibuilder.

hotnipi-gauge.js

/**
 * TODO:
 * - Max/Min not working correctly
 * - Add last update timestamp to LED title
 */
const componentName = 'gauge-hotnipi'
const className = 'GaugeHotnipi' // eslint-disable-line no-unused-vars

// just for syntax highlighting in VSCode (requires the lit-html extension)
function html(strings, ...keys) {
    return strings.map( (s, i) => {
        return s + (keys[i] || '')
    }).join('')
}

const template = document.createElement('template')
template.innerHTML = html`
    <style>
        :host{           
                --hng-needle-color: var(--needle-color,#e02f2b);
                --hng-zone-color-high: var(--zone-color-high,#ff5d4e75);
                --hng-zone-color-warn: var(--zone-color-warn,#ffb52d75);
                --hng-zone-color-normal: var(--zone-color-normal,#91ff4e55);
                --hng-zone-color-low: var(--zone-color-low,#4ec3ff85);
                --hng-needle-speed: var(--needle-speed,0.5);
            }
            .g-wrapper {
                display: grid;
                grid-template-rows: 1fr;
                width: 100%;
                height: 100%;
                align-content: center;
                align-items: center;
                justify-items: center;
            }
            .g-wrapper-label-0 .g-container {
                height: calc(100% - 6px);
            }        
            .g-container {
                position: relative;
                display: flex;
                justify-content: center;
                align-items: center;
                user-select: none;
                width: 100%;
                height: 100%;
            }        
            .g-body {
                position: relative;
                display: flex;
                align-content: center;
                align-items: center;
                justify-content: center;
                height: 98%;
                width: 98%;
                border-radius: 15%;
                box-shadow: 0px 5px 8px #00000030;
                background: linear-gradient(0deg, rgb(193, 193, 193) 0%, rgba(215, 215, 215, 1) 99%, rgba(236, 236, 236, 1) 100%);
            }
            .g-round{
                border-radius: 100%;
            }        
            .g-body::before {
                content: '';
                position: absolute;
                top: 0px;
                right: 0px;
                bottom: 0px;
                left: 0px;
                opacity: 0.1;
                border-radius: 15%;    
            }        
            .g-ring {
                position: relative;
                display: flex;
                justify-content: center;
                align-items: center;
                align-content: center;
                width: 94%;
                height: 94%;
                border-radius: 50%;
                background: linear-gradient(180deg, rgb(172, 172, 172) 0%, rgba(215, 215, 215, 1) 99%, rgba(236, 236, 236, 1) 100%);
            }        
            .quarter-top-right>.g-ring {
                width: 90%;
                height: 90%;
                border-radius: 15% 85% 15% 15%;
            }        
            .quarter-top-left>.g-ring {
                width: 90%;
                height: 90%;
                border-radius: 85% 15% 15% 15%;
            }        
            .g-plate {
                position: relative;
                overflow: hidden;
                width: 93%;
                height: 93%;
                border-radius: 50%;
                box-shadow: inset 0 0 15px #00000050;
                /*  background: radial-gradient(circle, rgb(196 205 209) 0%, rgb(177 183 186) 40%, rgb(191 193 194) 100%); */
            }    
            .g-led{
                position: absolute;
                left:48%;
                top:27%;
                border-radius: 2em;
                width: 0.5em;
                height: 0.5em;
                background-color: hsla(360 100% 50% / 100%);
                box-shadow: 0 0 4px 1px hwb(360 0% 49%), inset 0px -5px 6px -3px hsl(360 100% 30%);
                filter:saturate(0.05) brightness(3);        
            }
            .g-led.active{
                animation:blink 0.25s linear ;
                animation-iteration-count: infinite;        
            }
            @keyframes blink{
                50%{filter: none;}
            }
            .g-ticks {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                filter: drop-shadow(2px 4px 6px black);
            }        
            .g-tick {
                position: relative;
                left: 0;
                top: 50%;
                width: 100%;
                height: 1px;
                margin-bottom: -1px;
                background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 0) 2%, rgb(0 0 0 / 60%) 2%, rgb(0 0 0 / 60%) 10%, rgba(0, 0, 0, 0) 10%);
                transform: rotate(calc(calc(270deg / var(--ga-tick-count)) * var(--ga-tick) - calc(calc(270deg / var(--ga-tick-count)) + 45deg)));
            }        
            .g-tick.clock {
                transform: rotate(calc(calc(360deg / var(--ga-tick-count)) * var(--ga-tick) - calc(calc(360deg / var(--ga-tick-count)) +270deg)));
            }        
            .g-subtick {
                position: relative;
                left: 0;
                top: 50%;
                width: 100%;
                height: 1px;
                margin-bottom: -1px;
                background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 0) 2%, rgb(0 0 0 / 40%) 2%, rgb(0 0 0 / 40%) 6%, rgba(0, 0, 0, 0) 6%);
                transform: rotate(calc(calc(270deg / var(--ga-subtick-count)) * var(--ga-tick) - calc(calc(270deg / var(--ga-subtick-count)) + 45deg)));
            }   
            .g-subtick.clock {
                transform: rotate(calc(calc(360deg / var(--ga-subtick-count)) * var(--ga-tick) - calc(calc(360deg / var(--ga-subtick-count)) + 270deg)));
            }        
            .g-num {
                position: absolute;
                top: 50%;
                left: 50%;
                text-align: center;
                transform: translate(-50%, -50%) rotate(calc(calc(270deg / var(--ga-tick-count)) * var(--ga-tick) - calc(calc(270deg / var(--ga-tick-count)) + 45deg))) translate(calc(-1px * var(--container-size) * var(--gn-distance))) rotate(calc(calc(270deg / var(--ga-tick-count)) * var(--ga-tick) *-1 - calc(calc(270deg / var(--ga-tick-count))*-1 - 45deg)));
            }        
            .g-num.clock {
                transform: translate(-50%, -50%) rotate(calc(calc(360deg / var(--ga-tick-count)) * var(--ga-tick) - calc(calc(360deg / var(--ga-tick-count)) + 270deg))) translate(calc(-1px * var(--container-size) * var(--gn-distance))) rotate(calc(calc(360deg / var(--ga-tick-count)) * var(--ga-tick) *-1 - calc(calc(360deg / var(--ga-tick-count))*-1 - 270deg)));
            }        
            .g-nums {
                position: absolute;
                top: 0;
                width: 100%;
                height: 100%;
                color: #000000a1;
                font-size: calc(var(--digit-size) * 1%);
                font-weight: 500;
                filter: drop-shadow(2px 4px 10px black);
            }        
            .g-needle {
                position: absolute;
                left: 0;
                top: 49%;
                width: 100%;
                height: 2%;
                filter: drop-shadow(0px 1px 3px #00000080);
                background: linear-gradient(90deg, rgba(2, 0, 36, 0) 0, rgba(0, 0, 0, 0) 10%, var(--hng-needle-color) 10%, var(--hng-needle-color) 65%, rgba(0, 0, 0, 0) 65%);
                transform: rotate(calc(270deg * calc(var(--gauge-value, 0deg) / 100) - 45deg));
                transition: transform calc(1s * var(--hng-needle-speed));
            }        
            .g-needle-secondary {
                position: absolute;
                left: 0;
                top: 49%;
                width: 100%;
                height: 2%;
                filter: drop-shadow(0px 1px 3px #00000080);
                background: linear-gradient(90deg, rgba(2, 0, 36, 0) 0, rgba(0, 0, 0, 0) 15%, var(--hng-needle-color-secondary) 15%, var(--hng-needle-color-secondary) 50%, rgba(0, 0, 0, 0) 50%);
                transform: rotate(calc(270deg * calc(var(--gauge-value-secondary, 0deg) / 100) - 45deg));
                transition: transform  calc(1s * var(--hng-needle-speed));
            }
            .g-needle.hour {
                background: linear-gradient(90deg, rgba(2, 0, 36, 0) 0, rgba(0, 0, 0, 0) 20%, var(--hng-needle-color) 20%, var(--hng-needle-color) 50%, rgba(0, 0, 0, 0) 50%);
                transition: unset;
                transform: rotate(var(--time-hour));
            }        
            .g-needle.minute {
                top: 49.25%;
                height: 1.5%;
                background: linear-gradient(90deg, rgba(2, 0, 36, 0) 0, rgba(0, 0, 0, 0) 15%, var(--hng-needle-color) 15%, var(--hng-needle-color) 50%, rgba(0, 0, 0, 0) 50%);
                transition: unset;
                transform: rotate(var(--time-minute));
            }        
            .g-needle.second {
                top: 49.5%;
                height: 0.5%;
                background: linear-gradient(90deg, rgba(2, 0, 36, 0) 0, rgba(0, 0, 0, 0) 10%, var(--hng-needle-color) 10%, var(--hng-needle-color) 50%, rgba(0, 0, 0, 0) 50%);
                transform: rotate(var(--time-second));
                transition: unset;
            }        
            .g-needle-ring {
                position: absolute;
                width: calc(var(--container-size) * 2.5%);
                height: calc(var(--container-size) * 2.5%);
                top: 50%;
                left: 50%;
                border-radius: 50%;
                box-shadow: 0 1px 4px #0000009c;
                background: linear-gradient(#e0e0e0, #b4b4b4);
                transform: translate(-50%, -50%);
            }    
            .g-val {
                position: absolute;
                text-align: center;
                left: 50%;
                bottom: 0%;
                width: 80px;
                font-family: monospace;
                font-size: calc(var(--container-size) * 50%);
                color: #000000a1;
                filter: drop-shadow(2px 3px 2px #00000050);
                transform: translateX(-50%);
            }
            .g-val-ring {
                position: absolute;
                right: 0%;
                top: 0%;
                width: calc(calc(var(--container-size) * 7%) / calc(var(--container-size)/4));
                height: calc(calc(var(--container-size) * 6%) / calc(var(--container-size)/4));
                border-radius: 50%;
                background: linear-gradient(180deg, rgba(78, 78, 78, 1) 0%, rgba(215, 215, 215, 1) 99%, rgba(236, 236, 236, 1) 100%);
            }
            .g-val-plate {
                position: absolute;
                right: 0%;
                top: 0%;
                width: 90%;
                height: 90%;
                border-radius: 50%;
                background: #e4e9ee;
                box-shadow: inset 0 0 15px #000000a3;
                transform: translate(-5%, 5%);
            }
            .g-text {
                position: absolute;
                left: 50%;           
                width: 100%;
                font-family: monospace;
                font-size: calc(var(--container-size) * 20%);
                text-align: center;
                color: #000000a1;
                filter: drop-shadow(2px 3px 2px #00000080);
                transform: translateX(-50%);
            }        
            .g-label {
                top: 35%;
            }
            .g-unit {
                top: 62%;
            }
            .g-multi{
                top: 69%;
                font-size: calc(var(--container-size) * 27%);
            }        
            .g-rivets {
                position: absolute;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
            }        
            .g-rivet {
                position: absolute;
                width: 4%;
                height: 4%;
                border: 1px solid rgba(255, 255, 255, 0.1);
                border-radius: 50px;
                box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.596), -1px -1px 5px rgba(0, 0, 0, 0.2);           
            }        
            .g-rivet:nth-child(1) {
                top: 3%;
                left: 3%;
            }        
            .g-rivet:nth-child(2) {
                top: 3%;
                right: 3%;
            }        
            .g-rivet:nth-child(3) {
                bottom: 3%;
                left: 3%;
            }        
            .g-rivet:nth-child(4) {
                bottom: 3%;
                right: 3%;
            }
            .g-zone {
                position: absolute;
                width: 48%;
                height: 48%;
                top: 2%;
                left: 50%;
                box-sizing: border-box;
                border-radius: 0 100% 0 0;
                border-color: var(--hng-zone-color-normal);
                border-top: calc(var(--container-size) * 2.5px) solid;
                border-right: calc(var(--container-size) * 2.5px) solid;
                transform-origin: bottom left;
            }        
            .g-zone-1 {
                clip-path: polygon(0% 0%, 100% 0%, 50% 0%, 0% 100%);
            }
            .g-zone-2 {
                clip-path: polygon(0% 0%, 100% 0%, 100% 25%, 0% 100%);
            }
            .g-zone-3 {
                clip-path: polygon(0% 0%, 100% 0%, 100% 85%, 0% 100%)
            }
            .g-zone.high {
                border-color: var(--hng-zone-color-high);  
            }        
            .g-zone.warn {
                border-color: var(--hng-zone-color-warn);  
            }        
            .g-zone.normal {
                border-color: var(--hng-zone-color-normal);  
            }        
            .g-zone.low {
                border-color: var(--hng-zone-color-low);
            }
    </style>
    
`

class GaugeHotnipi extends HTMLElement {
    //#region ---- Class Variables ----
    config = {
        /** Minimum scale number */
        min: 0,
        /** Maximum scale number */
        max: 100,
        /** @type {"rect"|"round"} Gauge shape */
        shape: 'rect',
        /** Show the corner "rivets" */
        rivets: true,
        /** Show the indicator LED - lights when a value is received */
        led: true,
        /** @type {Array<number>} */
        scales: [],
        measurement: '',
        unit: '',
        multiplier: 0,
        digits: { size: 100, distance: 14 },
        zones: [],
    }

    /** @type {number|undefined} */
    lastValue
    //#endregion ---- ---- -----

    constructor() {
        super()

        const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true })
        shadow.append(template.content.cloneNode(true))

        this.dispatchEvent(new Event(`${componentName}:construction`, { bubbles: true, composed: true }))

        this.wrapper = document.createElement('div')
        shadow.appendChild(this.wrapper)
    }

    draw() { // eslint-disable-line sonarjs/cognitive-complexity

        // scales from min - max
        const gap = ((this.config.max - this.config.min) / 10)
        let n = this.config.min

        this.config.scales = []
        for (let i = 0; i < 11; ++i) {
            this.config.scales.push(n)
            n = parseFloat((n + gap).toFixed(2))
        }

        const use = this.config.multiplier
        if (use && use != 0) { // eslint-disable-line eqeqeq
            this.config.scales = this.config.scales.map(n => parseFloat((n / use).toFixed(2)))
        }

        // clear
        this.wrapper.replaceChildren()

        // init wrapper
        this.wrapper.style.width = '100%'
        this.wrapper.style.height = '100%'

        // create gauge
        this.gauge = document.createElement('div')

        this.gauge.setAttribute(
            'style',
            `--gauge-value:0;--container-size:${this.size / 50};--gn-distance:${this.config.digits.distance};--digit-size:${this.config.digits.size};--ga-tick-count:10;--ga-subtick-count:100;`
        )
        this.gauge.style.width = '100%'
        this.gauge.style.height = '100%'

        // body
        const body = document.createElement('div')
        body.className = 'g-body'
        if (this.config.shape === 'round') {
            body.classList.add('g-round')
        }
        this.gauge.appendChild(body)

        // ring
        const ring = document.createElement('div')
        ring.className = 'g-ring'
        body.appendChild(ring)

        // rivets
        if (this.config.rivets && this.config.shape === 'rect') {
            const rivets = document.createElement('div')
            rivets.className = 'g-rivets'
            ring.appendChild(rivets)
            for (let i = 0; i < 4; ++i) {
                const rivet = document.createElement('div')
                rivet.className = 'g-rivet'
                rivets.appendChild(rivet)
            }
        }

        // plate
        const plate = document.createElement('div')
        plate.className = 'g-plate'
        ring.appendChild(plate)

        // zones
        if (this.config.zones.length > 0) {
            let zone, cl
            this.config.zones.forEach(z => {
                zone = document.createElement('div')
                cl = 'g-zone '
                cl += `g-zone-${z.cover} `
                cl += z.type
                zone.className = cl
                zone.style.rotate = `${z.rotate}deg`
                plate.appendChild(zone)
            })
        }

        // led
        if (this.config.led) {
            this.led = document.createElement('div')
            this.led.className = 'g-led'
            plate.appendChild(this.led)
        }

        // ticks
        const ticks = document.createElement('div')
        ticks.className = 'g-ticks'
        plate.appendChild(ticks)

        for (let i = 1; i < 12; i++) {
            const tick = document.createElement('div')
            tick.className = 'g-tick'
            tick.setAttribute('style', '--ga-tick:' + i)
            ticks.appendChild(tick)
        }

        for (let i = 2; i < 101; i++) {
            const is = i.toString()
            if (is.charAt(is.length - 1) == '1') {
                continue
            }
            const tick = document.createElement('div')
            tick.className = 'g-subtick'
            tick.setAttribute('style', '--ga-tick:' + i)
            ticks.appendChild(tick)
        }

        // numbers

        const numbers = document.createElement('div')
        numbers.className = 'g-nums'
        plate.appendChild(numbers)
        for (let i = 1; i < 12; i++) {
            const num = document.createElement('div')
            num.className = 'g-num'
            num.setAttribute('style', '--ga-tick:' + i)
            num.textContent = (this.config.scales[i - 1]).toString()
            numbers.appendChild(num)
        }

        // measurement field
        if (this.config.measurement) {
            const label = document.createElement('div')
            label.className = 'g-text g-label'
            label.textContent = this.config.measurement
            plate.appendChild(label)
        }

        // unit
        if (this.config.unit) {
            const label = document.createElement('div')
            label.className = 'g-text g-unit'
            label.textContent = this.config.unit
            plate.appendChild(label)
        }
        // multiplier
        if (this.config.multiplier) {
            const label = document.createElement('div')
            label.className = 'g-text g-multi'
            label.textContent = 'x' + this.config.multiplier
            plate.appendChild(label)
        }

        // needle
        const needle = document.createElement('div')
        needle.className = 'g-needle'
        plate.appendChild(needle)

        const needleRing = document.createElement('div')
        needleRing.className = 'g-needle-ring'
        plate.appendChild(needleRing)

        // valueField
        this.valueField = document.createElement('div')
        this.valueField.className = 'g-val'
        plate.appendChild(this.valueField)

        this.wrapper.appendChild(this.gauge)

    }

    connectedCallback() {
        this.draw()
    }

    removeBlink() {
        this.led.classList.remove('active')
        this.delay = null
    }

    setValue(value) {
        this.lastValue = value
        if (!this.valueField) {
            return
        }

        const t = this.config.multiplier ? (value / this.config.multiplier).toFixed(1) : value.toFixed(1)
        this.valueField.textContent = t
        const v = ((value - this.config.min) / (this.config.max - this.config.min)) * 100
        this.gauge.style.setProperty('--gauge-value', v)

        // blink led
        if (this.config.led) {
            if (this.delay != null) {
                clearTimeout(this.delay)
                this.removeBlink()
            }
            this.led.classList.add('active')
            this.delay = setTimeout(() => this.removeBlink(), 800)
        }
    }

    update(value) {
        this.setAttribute('gauge-value', value)
    }

    static get observedAttributes() {
        return ['min', 'max', 'shape', 'multiplier', 'measurement', 'unit', 'rivets', 'digits', 'led', 'zones', 'gauge-value']
    }

    attributeChangedCallback(name, from, to) { // eslint-disable-line sonarjs/cognitive-complexity
        if (from !== to) {
            if (name === 'gauge-value') {
                this.setValue(Number(to))
                return
            }
            if (this.config.hasOwnProperty(name)) {
                switch (name) {
                    case 'min':{
                        to = parseFloat(to)
                        if (isNaN(to)) {
                            to = 0
                        }
                        break
                    }
                    case 'max':{
                        to = parseFloat(to)
                        if (isNaN(to)) {
                            to = 100
                        }
                        break
                    }
                    case 'multiplier':{
                        to = parseFloat(to)
                        if (isNaN(to) || to === 0) {
                            to = false
                        }
                        break
                    }
                    case 'led':
                    case 'rivets':{
                        to = to == 'true' ? true : false
                    }
                    case 'digits':{ // eslint-disable-line no-fallthrough
                        try {
                            to = JSON.parse(to)
                        }
                        catch (error) {
                            console.log(error)
                            to = this.config.digits
                        }
                        break
                    }
                    case 'zones':{
                        try {
                            to = JSON.parse(to)
                        }
                        catch (error) {
                            console.log(error)
                            to = this.config.zones
                        }
                        break
                    }
                    default:
                        break
                }
                this.config[name] = to
            }
        }
        this.size = this.wrapper.getBoundingClientRect().width
        this.draw()
        if (this.lastValue) {
            this.setValue(this.lastValue)
        }
    } // --- End of attributeChangedCallback ---

} // ---- End of class definition ----

// Self-register the HTML tag
customElements.define(componentName, GaugeHotnipi)

Example usage.

hgauge.html

<!doctype html>
<html lang="en"><head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" href="../uibuilder/images/node-blue.ico">

    <title>Blank template - Node-RED uibuilder</title>
    <meta name="description" content="Node-RED uibuilder - Blank template">

    <!-- Your own CSS (defaults to loading uibuilders css)-->
    <link type="text/css" rel="stylesheet" href="./hgauge.css" media="all">
    <link type="text/css" rel="stylesheet" href="./hotnipi-gauge-style.css" media="all">

    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->
    <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
    <script defer src="./hgauge.js">/* OPTIONAL: Put your custom code in that */</script> 
    <script defer src="./hotnipi-gauge.js">/* OPTIONAL: Put your custom code in that */</script> 
    <!-- #endregion -->

</head><body class="uib">
    
    <h1 class="with-subtitle">uibuilder Blank Template</h1>
    <div role="doc-subtitle">Using the IIFE library.</div>

    <div id="more"><!-- '#more' is used as a parent for dynamic HTML content in examples --></div>
    <div style="position:relative;width:200px;height:200px"><!-- 'gauge sizes are relative to container so width and height must be somewhere up in DOM tree -->
        
        <!-- hot-nipi-gauge attributes:            
            "min" - (number, mandatory) min value  
            "max" - (number, mandatory) max value
            "shape" - (string, optional) shape of the gauge. "round" makes gauge round shape and removes rivets, "rect" is default
            "multiplier" - (number, optional) multiplier for all values. scale numbers and value are divided by that, gauge shows multiplier on plate (fe: x100)
            "measurement" - (string, optional) the name of the measurement (temperature, humidity ...)
            "unit" - (string, optional) the unit of the measurement
            "rivets" - (boolean, optional) show/hide rivets. defaults to true
            "digits" - (json string, optional) size and placement of the scale digits.  '{"size":100,"distance":14}' size is treated as percentage, distance is arbitrary number around 15.
            "led" - (boolean, optional) shows small led which blinks couple of times when update is received.
            "zones" - (json string, optional) configuration of zones. An array of objects where:
                "type" (string) - color choice. acceptable values "low", "normal", "warn", "high"
                "cover" (number) - size of zone. acceptable values 1, 2 3. (1 covers space between major ticks)
                "rotate" (number) - to find correct value, try with 0 and manually rotate to desired position using browser developer tools. When position found, adjust the code. 
                
                
            All attributes can be changed at runtime, forces redraw.
            fe: 
            document.getElementById('gauge').setAttribute('digits',JSON.stringify({size:80,distance:15}))
            document.getElementById('gauge').setAttribute('max',1200)                    
        -->
        
        <gauge-hotnipi id="hgauge1"
            --width="100%" height="100%"
            min="0" max="500"
            multiplier="10"
            zones='[{"type":"warn","cover":1,"rotate":55},{"type":"high","cover":2,"rotate":82}]'
            measurement="Pressure"
            digits='{"size":100,"distance":14}'
            unit="PSI"
            rivets="true"
            led="true"
            gauge-value="0"
        ></gauge-hotnipi>
    </div>
    
</body></html>

hgauge.js

/** The simplest use of uibuilder client library
 * See the docs if the client doesn't start on its own.
 */

uibuilder.onChange('msg', (msg) => {
    console.log(msg)
    if (msg.topic === 'hgauge1') {
        console.log(msg)
        let g = document.getElementById('hgauge1')
        console.log(msg)
        if (g) {
            g.update(msg.payload)
        }
    }
})

hgauge.css

/* Load defaults from `<userDir>/node_modules/node-red-contrib-uibuilder/front-end/uib-brand.css`
 * This version auto-adjusts for light/dark browser settings but might not be as complete.
 */
@import url("../uibuilder/uib-brand.css");

/* Box sizing rules */

/* @namespace ct "http://gionkunz.github.com/chartist-js/ct"; */

/* *,
*::before,
*::after {
  box-sizing: border-box;
} */



/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
/* ul[role='list'],
ol[role='list'] {
  list-style: none;
} */

/* A elements that don't have a class get default styles */
/* a:not([class]) {
  text-decoration-skip-ink: auto;
} */

/* Make images easier to work with */
/* img,
picture {
  max-width: 100%;
  display: block;
} */

/* Inherit fonts for inputs and buttons */
/* input,
button,
textarea,
select {
  font: inherit;
} */

/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
/* @media (prefers-reduced-motion: reduce) {
  html:focus-within {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
} */