Showing gauges in UIbuilder

i am using gauges from @hotNipi previous posts which are superb but wanted to use UIbuilder rather than the dashboard but am unable to get them to display properly i hoped someone could show me how to use the templates to display in UIbuilder .


Thanks in anticipation

I made easy to use web component easy to use with uibuilder, and gifted it to @TotallyInformation

Known that we are all too busy with life, the public version is not yet available but I think Julian can share it no matter the current state of widget readiness.(it is ready to use but just some small tweaks to improve it)

2 Likes

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;
  }
} */

And for good measure, this is hotNipi's original component file just in case you want to use the orginal:

class Gauge extends HTMLElement{
    static get observedAttributes() {
        return ["min", "max", "shape","multiplier","measurement","unit","rivets","digits","led","zones"];
    }
    constructor() {
        super();
        const styleString = `
        :host{           
            --needle-color: #e02f2b;
            --zone-color-high: #ff5d4e75;
            --zone-color-warn: #ffb52d75;
            --zone-color-normal: #91ff4e55;
            --zone-color-low: #4ec3ff85;
            --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:25%;
            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(--needle-color) 10%, var(--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(--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(--needle-color-secondary) 15%, var(--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(--needle-speed));
        }
        .g-needle.hour {
            background: linear-gradient(90deg, rgba(2, 0, 36, 0) 0, rgba(0, 0, 0, 0) 20%, var(--needle-color) 20%, var(--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(--needle-color) 15%, var(--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(--needle-color) 10%, var(--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(--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(--zone-color-high);  
        }
        
        .g-zone.warn {
            border-color: var(--zone-color-warn);  
        }
        
        .g-zone.normal {
            border-color: var(--zone-color-normal);  
        }
        
        .g-zone.low {
            border-color: var(--zone-color-low);
        }`


        const stylesheet = document.createElement('style');
        stylesheet.innerHTML = styleString;
        this.config = {min:0,max:100,shape:"rect",rivets:true,led:true,scales:[],measurement:"",unit:"",multiplier:0,digits:{size:100,distance:14},zones:[]}
        const shadow = this.attachShadow({mode: "open"});
        shadow.appendChild(stylesheet);
        this.wrapper = document.createElement("div");       
        shadow.appendChild(this.wrapper);        
    }
    draw() {

        //scales from min - max
        let i;
        let gap = ((this.config.max-this.config.min)/10)
        let n = this.config.min
        this.config.scales = []
        let use = this.config.multiplier
        for(i = 0;i<11;++i){
            this.config.scales.push(n)
            n = parseFloat((n+gap).toFixed(2))
        }
        if(use && use != 0){
            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
        let body = document.createElement("div")
        body.className = "g-body"
        if(this.config.shape == "round"){
            body.classList.add("g-round")
        }
        this.gauge.appendChild(body)

        //ring
        let ring = document.createElement("div")
        ring.className = "g-ring"
        body.appendChild(ring)

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

        //plate
        let 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
        let ticks = document.createElement("div")
        ticks.className = "g-ticks"
        plate.appendChild(ticks)

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

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

        //numbers

        let numbers = document.createElement("div")
        numbers.className = "g-nums"
        plate.appendChild(numbers)
        for (i=1; i < 12; i++) {
            let 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){
            let label = document.createElement("div")
            label.className = "g-text g-label"
            label.textContent = this.config.measurement
            plate.appendChild(label)
        }

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

        //needle
        let needle = document.createElement("div")
        needle.className = "g-needle"
        plate.appendChild(needle)

        let 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()       
    }
    attributeChangedCallback(name, from, to) {  
        if (from !== to) { 
            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":{
                        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()       
    }

    removeBlink(){
        this.led.classList.remove("active")
        this.delay = null
    }
    
    update(value) { 
        if(!this.valueField){
            return
        }

        console.log({value})

        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);
        console.log({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)
        }
    } 
 } 

 customElements.define("hot-nipi-gauge", Gauge);

Here is what it looks like:

image

Send this simple inject to the page to see it in action.

[{"id":"4da3d3e81c5562b1","type":"inject","z":"4da773028609f9c6","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"hgauge1","payload":"$formatInteger($random() * 1000, \"0\")","payloadType":"jsonata","x":550,"y":520,"wires":[["f68fb96806c42501"]]}]
1 Like

Wow!
Thanks guys ...
Will be working on it tomorrow :slight_smile:

Yeah, let us know how it goes so it we can learn from real life experiences.

1 Like

sorry @TotallyInformation i am not getting to grips with what goes where in uibuilder which index.js do i use from the above and not sure how to use the css file supplied? i know its me not grasping it properly and that you are busy , so anytime you can is it possible to help me? I am hoping to design a heating display system using uibuilder. i am presently using the gauges in dashboard and they look and respond perfectly but need more flexibility and wish to use uibuilder for the project.

Hope you can help

Len

Sorry, partly my fault because I was testing in a dedicated page under an existing uibuilder instance. So I wasn't using index.xxx

Take the files listed as hgauge.xxx and rename them as index.xxx. Leave the other js file as-is

In the resulting index.html file, change the line that says:

<script defer src="./hgauge.js">/* OPTIONAL: Put your custom code in that */</script> 

to be

<script defer src="./index.js">/* OPTIONAL: Put your custom code in that */</script> 

and now you will have a more standard config.

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.