Gauge's for Dashboard 2.0 made with ui_template

There are a few ways, but based on how the above example is setup, you could add to the watch:

 watch: {
        msg: function(){    
            if(this.msg.payload !== undefined){  
                const v = this.validate(this.msg.payload)                
                if(v === null){
                    return
                }         
                this.value = v
                this.changeLine()              
            }
            if(this.msg.icon) {
                this.icon = icon
            }
        }
    },

then just include an icon property set to "mdi-something" in the msg you send to the template.

Hi,
I try to work with wind compass in Dashboard 2.0 and I would like
to set the color speed text according the wind speed (this.speed).
Is possible to set the color text in style with a variable set in the script?

xxx

Yes.

image
.

image

Where red line is, make your logic as you wish.

2 Likes

@Steve-Mcl Thanks for your hint. This is the watch method that I am now using:

    watch: {
        msg: function(){    
            if(this.msg.payload !== undefined){  
                const v = this.validate(this.msg.payload)                
                if(v === null){
                    return
                }         
                this.value = v
                if(this.type != "artless"){
                    this.lit()
                }
                else{
                    this.changeLine()
                }
            }
            if(this.msg.icon) {
                this.icon = this.msg.icon
            }
        }
    },

Can anybody give me a hint regarding the icon colour?

Thank you very much .....

gggg

Look at the post one above yours.

@Colin @arturv2000
Made another try to make the scale divisions configurable. See if that does the trick for you.

Code is HERE
<template>
    <div class="hn-sng">
        <div class="label">{{label}}</div>
        <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
            <g>
                <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector"
                    stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
            </g>
            <g>
                <path class="tick-minor" :style="{'stroke-dasharray':minor }" pathLength="496" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90" ></path>
                <path ref="arc" class="tick-major" :style="{'stroke-dasharray':major }" pathLength="248" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
            </g>
            <g>
                <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" y="-37"
                    :style="`rotate: ${item.r}deg;`">{{item.n}}</text>
            </g>
            <g>
                <text class="measurement" y="48" x="50%" text-anchor="middle">{{measurement}}</text>
                <text class="unit" y="75" x="50%" text-anchor="middle">{{unit}}</text>
                <text class="value" y="90" x="50%" text-anchor="middle">{{formattedValue}}</text>
                <text v-if="showUpdate" ref="update" class="update" y="100" x="0"
                    text-anchor="left">{{formattedUpdate}}</text>
            </g>
            <g ref="o-needle" class="o-needle">
                <path d="M 0,0 -1.5,0 -0.15,-40 0.15,-40 1.5,0 z"></path>
                <circle cx="0" cy="0" r="3"></circle>
            </g>
        </svg>
    </div>
</template>
<script>
    export default{
        data(){
            return {
                //define me here
                min:-50,
                max:50,
                unit:"kg",
                label:"Yet another gauge",
                measurement:"Plums",
                updateLabel:"Last update:",
                divisors:{major:10,minor:10},
                sectors:[{start:-50,end:0,color:"#63cc25"},{start:10,end:20,color:"orange"},{start:20,end:50,color:"red"}],
                showUpdate:true,
                
                //no need to change
                value:0,
                updateTime:0                             
            }
        },
        methods:{
            getElement: function(name,base){
                if(base){
                    return this.$refs[name]
                }
                return this.$refs[name][0]
            },
            validate: function(data){
                let ret                
                if(typeof data !== "number"){
                    ret = parseFloat(data)
                    if(isNaN(ret)){
                        console.log("BAD DATA! gauge id:",this.id,"data:",data)
                        return null
                    }
                }                    
                else{
                    ret = data
                }                
                return ret
            },
            range:function (n, p, r) {           
                if (n < p.minIn) {
                    n=p.minIn;
                }
                if (n > p.maxIn) {
                    n = p.maxIn;
                }
                if(r){
                    return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);
                }
                return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;
            },
            generateNumbers:function(min,max){               
               
                let nums = []
                let t = (max - min) / this.divisors.major;
                let i = min;
                let ro = -123
                let rp = 246 / this.divisors.major
               
                for (let e = 0; e < this.divisors.major + 1; ++e){
                    nums.push({n: i,r: ro + (e * rp) })
                    i = parseFloat((i + t).toFixed(2));                   
                }
                return nums 
            },
            sectorData:function(full){               
                let params, pos, span, sec, str
                let ret = [] 
                this.sectors.forEach((sector,idx) => {
                    sec = {name:'sector-'+idx,color:sector.color}
                    params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}                   
                    pos = this.range(sector.start,params,false)
                    params = {minIn:0, maxIn:this.max-this.min, minOut:0, maxOut:full}
                    span = this.range(sector.end - sector.start,params,false)                    
                    sec.css = "0 "+pos+" "+span+" var(--dash)"
                    ret.push(sec)
                })
                return ret
            },
            rotation:function(v){
                const params = {minIn:this.min, maxIn:this.max, minOut:-123, maxOut:123};
                return this.range(v,params,false) +'deg'          
            }

        },
        watch: {
            msg: function(){
                if(this.msg.payload != undefined){                   
                    const v = this.validate(this.msg.payload)                   
                    if(v == null){
                        return
                    }
                    this.value = v
                    this.updateTime = Date.now()                    
                    this.getElement('o-needle',true).style.rotate = this.rotation(this.value)                    
                }
            }
        },
        computed: {
            formattedValue: function () {
                return this.value.toFixed(1)
            },
            formattedUpdate:function (){
                return this.updateLabel+" "+this.time
            },
            time:function(){
                return new Intl.DateTimeFormat('default', {hour: 'numeric', minute: 'numeric',second: 'numeric'}).format(this.updateTime);
            },
            numbers:function(){
                return this.generateNumbers(this.min,this.max)
            },
            major:function(){
                return "1," + ((246 / this.divisors.major) - 1)           
            },
            minor:function(){
                let step = 492 / this.divisors.major / this.divisors.minor
                let line = "0," + step + ","
                step --
                for(let i=0;i<this.divisors.minor-1;++i){
                    line += "1,"+step+","
                }
                line = line.slice(0, -1);

                return line
            },
            offset:function(){
                return -1
            }

        },
        mounted(){
           
            const dal = this.getElement('arc',true).getTotalLength()
            const sec = this.sectorData(dal)              
            const gauge = this.getElement('hn-gauge',true)
            gauge.style.setProperty('--dash',dal)
           
            sec.forEach(s =>{
                const sector = this.getElement(s.name,false)
                sector.style.setProperty("stroke-dasharray",s.css)
                sector.style.setProperty("stroke",s.color)
            })            

        }

    }
</script>
<style>
    .hn-sng {
        position: relative;
    }

    .hn-sng .label {
        position: absolute;
        font-size: 1rem;
        color: currentColor;
        text-align: center;
        width: 100%;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }

    .hn-sng .value {
        fill: currentColor;
        font-weigth: 600;
    }

    .hn-sng .unit {
        fill: currentColor;
        font-size: 0.4rem;
    }

    .hn-sng .measurement {
        fill: currentColor;
        font-size: 0.5rem;
    }

    .hn-sng .update {
        fill: currentColor;
        fill-opacity: 0.6;
        font-size: 0.35rem;
    }

    .hn-sng .num {
        transform-origin: center 64%;
        transform: translate(50%, 64%);
        fill: currentColor;
        fill-opacity: 0.6;
        font-size: .35rem;
    }

    .hn-sng .tick-minor {       
        stroke-dashoffset: -2;
        fill: none;
        stroke: currentColor;
        stroke-opacity: 0.6;
    }

    .hn-sng .tick-major {
        stroke-dashoffset: -1;
        fill: none;
        stroke: currentColor;
    }

    .hn-sng .sector {
        fill: none;
        stroke: transparent;
    }
    .hn-sng .sector:first-of-type {
        stroke-dashoffset:-0.5;
    }

    .hn-sng .o-needle {
        transform-origin: center 64%;
        transform: translate(50%, 64%);
        transition: rotate .5s;
    }

    .hn-sng .o-needle path,
    .hn-sng .o-needle circle {
        fill: currentColor;
    }
</style>

image

5 Likes

Liking the 'last update' time addition, that's really useful to ensure that data is not stale.

1 Like

Just it is time of last msg which can be also replay when you open the dashboard so it isn't absolute check for equipment or communication quality.

1 Like

Yes, I've just spotted that, but it's OK for data that is frequently updated.

1 Like

But nothing prevents you to send that last update as property of msg.payload and then it indicates more actual thing. Just some slight changes needed about what the payload structure should be and stuff around it.

1 Like

@hotNipi
be patient, but dashboard 2 is very difficult for me ...
I would like to set one vertical gauge for humidity value ...
I copied your code but I have only this component ....

What should I do to show the vertical gauge?

Post nr 3 in this thread shows configuration options.
That is if you copied stuff from first post.

Found!
Thank you again ...(I will not disturb you anymore)

Meanwhile I have made some further mods

  1. Allowed scale min to be greater than scale max

image

  1. When an invalid payload is received, instead of ignoring it, show --- and put the needle off the end. I like this because you can add a trigger node before it that sends an invalid message if the inputs stop coming in so it is obvious if the inputs fail. Also this display is the default before any messages are received.

image

@hotNipi would you like me to merge either or both of these into your version?

Click here to see code
<!-- Gauge Template CDL v1.2  
  Based on original work by @HotNipi 
-->
 <template>
    <div class="hn-sng">
        <div class="label">{{label}}</div>
        <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
            <g>
                <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>                
            </g>
            <g>
                <path class="tick-minor" stroke-width="5" d="M 10 90 A 47.5 47.5 0 1 1 90 90" :style="tickStyle(this.minorDivision, 0.5)"></path>
                <path ref="arc" class="tick-major" stroke-width="5" d="M 10 90 A 47.5 47.5 0 1 1 90 90" :style="tickStyle(this.majorDivision, 2)"></path>
                
            </g>         
            <g>
                <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" y="-37" :style="`rotate: ${item.r}deg;`">{{item.n}}</text>
            </g>
            <g>
                <text class="measurement" y="48" x="50%" text-anchor="middle">{{measurement}}</text>
                <text class="unit" y="75" x="50%" text-anchor="middle">{{unit}}</text>
                <text class="value" y="90" x="50%" text-anchor="middle">{{formattedValue}}</text>
            </g>
            <g ref="o-needle" class="o-needle">
                <path d="M 0,0 -1.5,0 -0.15,-43 0.15,-43 1.5,0 z"></path>
                <circle cx="0" cy="0" r="3"></circle>
            </g>
        </svg>
    </div>
</template>
<script>
export default{
        data(){
            return {
                //define settings here
                // Min and max scale values.  Max may be less that min.
                min:0,
                max:1.4,
                //min:0,
                //max:-1.4,
                majorDivision: 0.2,       // number of input units for each (numbered) major division
                minorDivision: 0.05,       // number of input units for each minor division
                unit:"Ā°C",
                label:"Hot Water",
                measurement:"Temperature",
                valueDecimalPlaces: 2,    // number of decimal places to show in the value display
                majorDecimalPlaces: 1,    // number of decimal places to show on the scale
                // Coloured sectors around the scale.  Sectors can be in any order and it makes no difference if 
                // start and end are reversed.
                //  Any gaps are left at background colour
                sectors:[{start:0,end:0.4,color:"skyblue"},{start:0.4,end:0.75,color:"green"},{start:0.75,end:1.4,color:"red"}],
                //sectors:[{start:0,end:-0.4,color:"skyblue"},{start:-0.4,end:-0.75,color:"green"},{start:-0.75,end:-1.4,color:"red"}],
                
                //no need to change
                value: null
            }
        },
        methods:{
            getElement: function(name,base){
                if(base){
                    return this.$refs[name]
                }
                return this.$refs[name][0]
            },
            validate: function(data){
                let ret                
                if(typeof data !== "number"){
                    ret = parseFloat(data)
                    if(isNaN(ret)){
                        console.log("BAD DATA! gauge id:",this.id,"data:",data)
                        ret = null
                    }
                }                    
                else{
                    ret = data
                }                
                return ret
            },
            range:function (n, p, r) {
                // clamp n to be within input range
                if (p.maxIn > p.minIn) {
                    n = Math.min(n, p.maxIn)
                    n = Math.max(n, p.minIn)
                } else {
                    n = Math.min(n, p.minIn)
                    n = Math.max(n, p.maxIn)
                }
                if(r){
                    return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);
                }
                return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;
            },
            generateNumbers:function(min,max,majorDivision){    
                let minDegrees, maxDegrees, startValue
                if (max > min) {
                    minDegrees = 237.36
                    maxDegrees = 482.64
                    startValue = min    
                } else {
                    minDegrees = 482.64
                    maxDegrees = 237.36
                    startValue = max              
                }
                // Calculate number of major divisions, adding on a bit and rounding down in case last one is just off the end
                const numDivs = Math.floor(Math.abs(max-min) / majorDivision + 0.1)
                const degRange = maxDegrees-minDegrees
                const degPerDiv = degRange * majorDivision/Math.abs(max-min)
                let nums = []
                for (let div=0; div<=numDivs; div++) {
                    let degrees = div*degPerDiv + minDegrees
                    const n = (startValue + div * majorDivision).toFixed(this.majorDecimalPlaces)
                    nums.push({r: degrees, n: n})
                }
                return nums 
            },
            sectorData:function(full){               
                let ret = []
                this.sectors.forEach((sector,idx) => {
                    let sec = {name:'sector-'+idx,color:sector.color}
                    const params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}
                    const start = this.range(sector.start,params,false)
                    const end = this.range(sector.end,params,false)
                    const pos = Math.min(start, end)
                    const span = Math.max(start, end) - pos
                    sec.css = `0 ${pos} ${span} var(--dash)`
                    ret.push(sec)
                    console.log(`sec: ${JSON.stringify(sec)}`)
                })
                return ret
            },
            rotation:function(v){
                // allow pointer to go 10% off ends of scale
                const overflow = (this.max-this.min)*0.1
                const maxAngle = 122.64 * 1.2       // 1.2 as this is only half the range, so need 20%
                const min = this.min - overflow
                const max = this.max + overflow
                const params = {minIn:min, maxIn:max, minOut:-maxAngle, maxOut:maxAngle};
                if (v === null) {
                    v = Math.min(min, max)
                }
                return `${this.range(v,params,false)}deg`
            },
            tickStyle: function(division, width) {
                // division is the number of input units per tick
                // width is the width (length?) of the tick in svg units

                // total arc length in svg units
                const arcLength = 203.356
                // length in user units
                const range = Math.abs(this.max - this.min)
                const tickPeriod = division/range * arcLength
                // marker is width wide, so gap is tickPeriod-width
                // stroke-dashoffset sets the first tick to half width
                return `stroke-dasharray: ${width} ${tickPeriod-width}; stroke-dashoffset: ${width/2};`
            },
        },
        watch: {
            msg: function(){
                // allow undefined payload through as it will show the invalid data state
                const v = this.validate(this.msg.payload)                   
                // v is null if payload is invalid, this is coped with then it is displayed
                this.value = v
                this.getElement('o-needle',true).style.rotate = this.rotation(this.value)    
            }
        },
        computed: {
            formattedValue: function () {
                // Show --- for the value until a valid value is recevied
                return this.value !== null  ?  this.value.toFixed(this.valueDecimalPlaces)  :  "---"
            },
            numbers:function(){
                return this.generateNumbers(this.min,this.max,this.majorDivision)
            },
        },
        mounted(){
           
            const dal = this.getElement('arc',true).getTotalLength()
            const sec = this.sectorData(dal)              
            const gauge = this.getElement('hn-gauge',true)
            gauge.style.setProperty('--dash',dal)
            sec.forEach(s =>{
                const sector = this.getElement(s.name,false)
                sector.style.setProperty("stroke-dasharray",s.css)
                sector.style.setProperty("stroke",s.color)
            })
            // initialise the needle off the bottom
            this.getElement('o-needle',true).style.rotate = this.rotation(null) 
        }

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

Edit: Somehow I lost the hub from the needle. I have put it back in now.

2 Likes

I'll do. Just not right now. Too much spring :blossom::seedling: knocks to come in.

1 Like

You are right there, much to do outside.

I can't say that I am very keen on that method. To me it seems more user friendly to specify the divisions in the user's units rather than how many divisions there should be along the arc.

Ask the arbitrary user about the user units. Therm is unknown thus I can't see the value.

I don't think I know any :frowning:

Sorry, I don't understand.

On a side note I have discovered that one can split the export script into two sections so one can have in the ui-template

<script>
    export default{
        data(){
            return {
                //define settings here
                // Min and max scale values.  Max may be less that min.
                min:0,
                max:1.4,
                ...
            }
        },
    }      
</script>

<template>
    all the template stuff
</template>    

<script>
    export default{
      methods, computed, watch etc
    }
</script>

Which I think makes it easier to configure, as all the config is at the start.