Gauge's for Dashboard 2.0 made with ui_template

Hi,
I realized this dashboard with 3 vertical gauge.
I modified CSS node to change the size of value but the changes take effect only on the first gauge and not for the other two ....

Where is the problem?

xxx

Hard to say without seeing how you did it.

I understand ....
On the top of your CSS node I added this code for the color of text node
and this is the problem ....

.my-colored-text-widget.label-white label{
    color:rgb(250, 249, 249);
    font-size:20px;
}
.my-colored-text-widget.label-green label{
    color:rgb(173, 255, 47);
    font-size:20px;
}
.my-colored-text-widget.label-red label{
    color:rgba(255, 0, 0);
    font-size:20px;
}

.my-colored-text-widget.text-green span{
    color:rgb(173, 255, 47);
    font-size:24px;
}
.my-colored-text-widget.text-red span{
    color:rgba(255, 0, 0);
    font-size:24px;
}
.my-colored-text-widget.text-orange span{
    color:rgb(255, 145, 0);
    font-size:24px;
}
.my-colored-text-widget.text-white span{
    color:rgb(250, 249, 249);
    font-size:24px;
}
.my-colored-text-widget.text-grey span {
    color: rgb(219, 213, 213);
    font-size: 20px;
}

Without this code I have no problem but I have not the color in text node ....
How can I do to repair?

I can't understand. Those are not related.

I took majority of your improvements and included.
One thing (the division calculations) still I didn't cos my version shows a bit more accurate. But I'm not against the logic you have provided.

CODE is HERE
<script>
    export default{
        data(){
            return {
                //define me here
                min:0,
                max:1.4,
                unit:"kg",
                label:"Yet another gauge",
                measurement:"Plums",
                updateLabel:"Last update:",
                divisors:{major:7,minor:4},
                sectors:[{start:0,end:0.4,color:"skyblue"},{start:0.4,end:0.75,color:"green"},{start:0.75,end:1.4,color:"red"}],
                showUpdate:true,
                allowOverflow:true,
                valueDecimalPlaces: 2,
                majorDecimalPlaces:1,
                
                //no need to change
                value:0,
                updateTime:0                             
            }
        }
    }
</script>
<template>
    <div class="hn-sng">
        <div class="label">{{label}}</div>
        <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
            <g>
                <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector"
                    stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
            </g>
            <g>
                <path class="tick-minor" :style="{'stroke-dasharray':minor }" pathLength="492" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90" ></path>
                <path ref="arc" class="tick-major" :style="{'stroke-dasharray':major }" pathLength="246" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
            </g>
            <g>
                <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" y="-37"
                    :style="`rotate: ${item.r}deg;`">{{item.n}}</text>
            </g>
            <g>
                <text class="measurement" y="48" x="50%" text-anchor="middle">{{measurement}}</text>
                <text class="unit" y="75" x="50%" text-anchor="middle">{{unit}}</text>
                <text class="value" y="90" x="50%" text-anchor="middle">{{formattedValue}}</text>
                <text v-if="showUpdate" ref="update" class="update" y="100" x="0"
                    text-anchor="left">{{formattedUpdate}}</text>
            </g>
            <g ref="o-needle" class="o-needle">
                <path d="M 0,0 -1.5,0 -0.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{       
        methods:{
            getElement: function(name,base){
                if(base){
                    return this.$refs[name]
                }
                return this.$refs[name][0]
            },
            validate: function(data){
                let ret                
                if(typeof data !== "number"){
                    ret = parseFloat(data)
                    if(isNaN(ret)){
                        console.log("BAD DATA! gauge id:",this.id,"data:",data)
                        return null
                    }
                }                    
                else{
                    ret = data
                }                
                return ret
            },
            range:function (n, p, r) {           
                if (p.maxIn > p.minIn) {
                    n = Math.min(n, p.maxIn)
                    n = Math.max(n, p.minIn)
                } else {
                    n = Math.min(n, p.minIn)
                    n = Math.max(n, p.maxIn)
                }
                if(r){
                    return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);
                }
                return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;
            },
            generateNumbers:function(min,max){               
               
                let nums = []
                let t = (max - min) / this.divisors.major;
                let i = min;
                let ro = -123
                let rp = 246 / this.divisors.major
               
                for (let e = 0; e < this.divisors.major + 1; ++e){
                    if(this.min > this.max){
                        nums.unshift({n: i.toFixed(this.majorDecimalPlaces),r: ro + (e * rp) })
                    }
                    else{
                        nums.push({n: i.toFixed(this.majorDecimalPlaces),r: ro + (e * rp) })
                    }
                    
                    i = parseFloat(i + t);                   
                }
                return nums 
            },
            sectorData:function(full){  

                let ret = []
                this.sectors.forEach((sector,idx) => {
                    let sec = {name:'sector-'+idx,color:sector.color}
                    const params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}
                    const start = this.range(sector.start,params,false)
                    const end = this.range(sector.end,params,false)
                    const pos = Math.min(start, end)
                    const span = Math.max(start, end) - pos
                    sec.css = `0 ${pos} ${span} var(--dash)`
                    ret.push(sec)
                })
                return ret
            },
            rotation:function(v){
                const overflow = this.allowOverflow ? (this.max-this.min) * 0.1 : 0
                const maxRotation = this.allowOverflow ? (123 * 1.2) : 123
                const min = this.min - overflow
                const max = this.max + overflow
                const params = {minIn:min, maxIn:max, minOut:-maxRotation, maxOut:maxRotation};
                if (v === null) {
                    v = Math.min(min, max)
                }
                return this.range(v,params,false) +'deg'          
            }

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

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

        },
        mounted(){
           
            const dal = this.getElement('arc',true).getTotalLength()
            const sec = this.sectorData(dal)              
            const gauge = this.getElement('hn-gauge',true)
            gauge.style.setProperty('--dash',dal)
           
            sec.forEach(s =>{
                const sector = this.getElement(s.name,false)
                sector.style.setProperty("stroke-dasharray",s.css)
                sector.style.setProperty("stroke",s.color)
            })
            this.getElement('o-needle',true).style.rotate = this.rotation(null)
            this.$nextTick(() => {
                this.getElement('o-needle',true).style.transition= "rotate .5s"
            })
            

        }

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

    .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: 0.5;
        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: unset;
    }

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

It seems you are right. That should not be the case. I will investigate.

If you can take mine, keep the "magic numbers" of mine but apply your logic of creating divisions, it may come out pretty nice?

I worked the numbers out from first principles so it should be correct. I need to know what I have done wrong.

See here

<path class="tick-minor" :style="{'stroke-dasharray':minor }" pathLength="492" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90" ></path>
                <path ref="arc" class="tick-major" :style="{'stroke-dasharray':major }" pathLength="246" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>

I do define pathLength
For major -> 246 This sis same as for normal length of needle rotation
For minor -> 492 is double of that.
Those can actually be arbitrary numbers but if to base on available (measureable) rotation, you can think more clear what it is.

So the 123 (half) or 246 are solid base for all calculations.

The choice of 246 is that overall shape of gauge leaves just enough free space for additions above (label) and bellow (updated text). Also the value has more space in between arc legs

Actually - all of that was just a math again...

CODE
<script>
    export default{
        data(){
            return {
                //define me here
                min:0,
                max:1.4,
                unit:"kg",
                label:"Yet another gauge",
                measurement:"Plums",
                updateLabel:"Last update:",               
                majorDivision: 0.2, // number of input units for each (numbered) major division
                minorDivision: 0.05, // number of input units for each minor division
                sectors:[{start:0,end:0.4,color:"skyblue"},{start:0.4,end:0.75,color:"green"},{start:0.75,end:1.4,color:"red"}],
                showUpdate:true,
                allowOverflow:true,
                valueDecimalPlaces: 2,
                majorDecimalPlaces:1,
                
                //no need to change
                value:0,
                updateTime:0                             
            }
        }
    }
</script>
<template>
    <div class="hn-sng">
        <div class="label">{{label}}</div>
        <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
            <g>
                <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector"
                    stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
            </g>
            <g>
                <path class="tick-minor" :style="{'stroke-dasharray':minor }" pathLength="492" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90" ></path>
                <path ref="arc" class="tick-major" :style="{'stroke-dasharray':major }" pathLength="246" stroke-width="5" d="M 10 90 A 47.5 47.5 25 1 1 90 90"></path>
            </g>
            <g>
                <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" y="-37"
                    :style="`rotate: ${item.r}deg;`">{{item.n}}</text>
            </g>
            <g>
                <text class="measurement" y="48" x="50%" text-anchor="middle">{{measurement}}</text>
                <text class="unit" y="75" x="50%" text-anchor="middle">{{unit}}</text>
                <text class="value" y="90" x="50%" text-anchor="middle">{{formattedValue}}</text>
                <text v-if="showUpdate" ref="update" class="update" y="100" x="0"
                    text-anchor="left">{{formattedUpdate}}</text>
            </g>
            <g ref="o-needle" class="o-needle">
                <path d="M 0,0 -1.5,0 -0.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{       
        methods:{
            getElement: function(name,base){
                if(base){
                    return this.$refs[name]
                }
                return this.$refs[name][0]
            },
            validate: function(data){
                let ret                
                if(typeof data !== "number"){
                    ret = parseFloat(data)
                    if(isNaN(ret)){
                        console.log("BAD DATA! gauge id:",this.id,"data:",data)
                        return null
                    }
                }                    
                else{
                    ret = data
                }                
                return ret
            },
            range:function (n, p, r) {           
                if (p.maxIn > p.minIn) {
                    n = Math.min(n, p.maxIn)
                    n = Math.max(n, p.minIn)
                } else {
                    n = Math.min(n, p.minIn)
                    n = Math.max(n, p.maxIn)
                }
                if(r){
                    return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);
                }
                return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;
            },
            generateNumbers:function(min,max){               
               
                let nums = []
                let t = (max - min) / this.divisors.major;
                let i = min;
                let ro = -123
                let rp = 246 / this.divisors.major
               
                for (let e = 0; e < this.divisors.major + 1; ++e){
                    if(this.min > this.max){
                        nums.unshift({n: i.toFixed(this.majorDecimalPlaces),r: ro + (e * rp) })
                    }
                    else{
                        nums.push({n: i.toFixed(this.majorDecimalPlaces),r: ro + (e * rp) })
                    }
                    
                    i = parseFloat(i + t);                   
                }
                return nums 
            },
            sectorData:function(full){  

                let ret = []
                this.sectors.forEach((sector,idx) => {
                    let sec = {name:'sector-'+idx,color:sector.color}
                    const params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}
                    const start = this.range(sector.start,params,false)
                    const end = this.range(sector.end,params,false)
                    const pos = Math.min(start, end)
                    const span = Math.max(start, end) - pos
                    sec.css = `0 ${pos} ${span} var(--dash)`
                    ret.push(sec)
                })
                return ret
            },
            rotation:function(v){
                const overflow = this.allowOverflow ? (this.max-this.min) * 0.1 : 0
                const maxRotation = this.allowOverflow ? (123 * 1.2) : 123
                const min = this.min - overflow
                const max = this.max + overflow
                const params = {minIn:min, maxIn:max, minOut:-maxRotation, maxOut:maxRotation};
                if (v === null) {
                    v = Math.min(min, max)
                }
                return this.range(v,params,false) +'deg'          
            }

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

                return line
            },
            offset:function(){
                return -1
            },
            divisors:function(){
                let span
                if(this.max > this.min){
                    span = this.max - this.min
                }
                else{
                    span = this.min - this.max                
                }
                const ma = span / this.majorDivision
                const mi = span / ma / this.minorDivision
                return {major:ma,minor:mi}
            }

        },
        mounted(){
           
            const dal = this.getElement('arc',true).getTotalLength()
            const sec = this.sectorData(dal)              
            const gauge = this.getElement('hn-gauge',true)
            gauge.style.setProperty('--dash',dal)
           
            sec.forEach(s =>{
                const sector = this.getElement(s.name,false)
                sector.style.setProperty("stroke-dasharray",s.css)
                sector.style.setProperty("stroke",s.color)
            })
            this.getElement('o-needle',true).style.rotate = this.rotation(null)
            this.$nextTick(() => {
                this.getElement('o-needle',true).style.transition= "rotate .5s"
            })
            

        }

    }
</script>
<style scoped>
    .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: 0.5;
        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: unset;
    }

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

In fact it isn't 246, it is 245.28. That is derived from the fact that radius is 47.5 and the end points are 80 apart.

I have been looking at why mine is not accurate. The reason is that the needle is not exactly centred in the arc. The y axis transform at the bottom of the style section should be 64.383% not 64%. Then it lines up correctly.

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

Like you, I hate all these magic numbers that are so fiddly to work out accurately. I do have some thoughts on how to improve that, but I won't have much time for the next couple of weeks so it will have to wait.

Also 64.383 should be used in .num, though here the difference is not noticeable.

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

Tweaked a bit again, yes the center point for needle was off but for me not that much. Ended with .15
Also eliminated possibility to kill the browser by asking it to divide by zero.

But yeah, overall I don't think it needs to be more precise unless somebody labels it to be Rolex.
Updated code at original post. Gauge's for Dashboard 2.0 made with ui_template - #6 by hotNipi

2 Likes

Being a mathematician I feel a need to be accurate.

Having a lifetime as family member of mathematician I do feel sometimes as stupid as a chair but still, I can roughly predict how much time I still have in hands. More and more often the word waste takes over.

I have managed to find time to get rid of the magic numbers. I have done this by configuring the centre point, radius, start angle and end angle for the gauge. As well as getting rid of the magic numbers this makes it trivially easy to adjust the config of the gauge, should one wish to do so. So layouts such as these can easily be achieved without having to calculate lots of stuff.

image

Click here to see code
<!-- Gauge Template CDL v1.4.1
  Based on original work by @HotNipi 
-->
<script>
    export default{
        data(){
            let data = {
                //define settings here
                // Min and max scale values.  Max may be less that min.
                //min: -50,
                //max: 50,
                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"}],

                // The position and alignment of the gauge inside the 100x100 svg box for the widget can be changed by modifying the settings below
                // The origin of the svg box is the top left hand corner. The bottom right hand corner is 100,100
                // Obviously, if you move the gauge you may have to move the text fields also.
                // Take care with these settings, if you put silly values in the browser showing the dashboard may lock up. If this happens,
                // close the dashboard browser tab (which may take some time as it is locked up).
                arc: {
                    cx:50,              // the x and y coordinates of the centre of the gauge arc
                    cy: 64, 
                    radius: 47.5,       // the radius of the arc
                    startDegrees: -123, // the angle of the start and end points of the arc.  Zero is vertically up from the centre
                    endDegrees: 123,	// +ve values are clockwise
                },

                //don't change this
                value: null,
            }
            // calculate derived values
            // make sure startDegrees < endDegrees, but the difference is <= 360
            while (data.arc.startDegrees >= data.arc.endDegrees) {
                data.arc.startDegrees -= 360
            }
            while (data.arc.endDegrees - data.arc.startDegrees > 360) {
                data.arc.startDegrees += 360
            }
            const startRadians = data.arc.startDegrees * Math.PI/180
            const endRadians = data.arc.endDegrees * Math.PI/180
            data.arc.startx = data.arc.cx - data.arc.radius * Math.sin(startRadians-Math.PI)
            data.arc.starty = data.arc.cy + data.arc.radius * Math.cos(startRadians-Math.PI)
            data.arc.endx = data.arc.cx + data.arc.radius * Math.sin(Math.PI-endRadians)
            data.arc.endy = data.arc.cy + data.arc.radius * Math.cos(Math.PI-endRadians)
            data.arc.arcLength = 2 * Math.PI * data.arc.radius * (data.arc.endDegrees - data.arc.startDegrees)/360

            // sanity checks - probably there should be more of these
            this.majorDivision = this.majorDivision <= 0  ?  1  : this.majorDivision
            this.minorDivision = this.minorDivision <= 0  ?  1  : this.minorDivision
            //console.log({arc: data.arc})
            return data
        }
    }
</script>

<template>
    <div class="hn-sng">
        <div class="label">{{label}}</div>
        <svg ref="hn-gauge" width="100%" height="100%" viewBox="0 0 100 100">
            <g>
                <path v-for="(item, index) in sectors" :key="index" :ref="'sector-' + index" class="sector" stroke-width="5" :d="arcspec" ></path>                
            </g>
            <g>
                <path class="tick-minor" stroke-width="5" :d="arcspec" :style="tickStyle(this.minorDivision, 0.5)"></path>
                <path ref="arc" class="tick-major" stroke-width="5" :d="arcspec" :style="tickStyle(this.majorDivision, 1)"></path>
                
            </g>         
            <g>
                <text v-for="(item, index) in numbers" :key="index" class="num" text-anchor="middle" :y="`${10.5-this.arc.radius}`" 
                  :style="`rotate: ${item.r}deg; transform-origin: ${this.arc.cx}% ${this.arc.cy}%; transform: translate(${this.arc.cx}%, ${this.arc.cy}%)`">
                  {{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" v-html="needle">
            </g>
        </svg>
    </div>
</template>

<script>
    export default{
        methods:{
            getElement: function(name,base){
                if(base){
                    return this.$refs[name]
                }
                return this.$refs[name][0]
            },
            validate: function(data){
                let ret                
                if(typeof data !== "number"){
                    ret = parseFloat(data)
                    if(isNaN(ret)){
                        console.log("BAD DATA! gauge id:",this.id,"data:",data)
                        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 = this.arc.startDegrees
                    maxDegrees = this.arc.endDegrees
                    startValue = min    
                } else {
                    minDegrees = this.arc.endDegrees
                    maxDegrees = this.arc.startDegrees
                    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)
                })
                return ret
            },
            rotation:function(v){
                // allow pointer to go 10% off ends of scale, but not more than half way to the other end of the scale
                const deltaDeg = this.arc.endDegrees - this.arc.startDegrees
                const gapDeg = 360 - deltaDeg
                const overflowFactor = Math.min(0.1, gapDeg/2/deltaDeg)
                const overflow = (this.max-this.min)*overflowFactor
                const angleOverflow = (deltaDeg)*overflowFactor 
                const min = this.min - overflow
                const max = this.max + overflow
                const minAngle = this.arc.startDegrees - angleOverflow
                const maxAngle = this.arc.endDegrees + angleOverflow
                const params = {minIn:min, maxIn:max, minOut:minAngle, 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 = this.arc.arcLength
                // 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: {
            needle: function() {
                const cx = this.arc.cx
                const cy = this.arc.cy
                const length = this.arc.radius - 4.5
                return `<path d="M ${cx},${cy} ${cx-1.5},${cy} ${cx-0.15},${cy-length} ${cx+0.15},${cy-length} ${cx+1.5},${cy} z"></path> 
                  <circle cx="${this.arc.cx}" cy="${this.arc.cy}" r="3"></circle>`
            },
            arcspec: function() {
                const delta = this.arc.endDegrees - this.arc.startDegrees
                // if more than 180 deg sweep then large-arg-flag should be 1
                const largeArcFlag = delta > 180  ?  1  :  0

                return `M ${this.arc.startx} ${this.arc.starty} A ${this.arc.radius} ${this.arc.radius} 0 ${largeArcFlag} 1 ${this.arc.endx} ${this.arc.endy}`
            },
            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)
            })
            // set the needle centre of rotation
            this.getElement('o-needle',true).style["transform-origin"] = `${this.arc.cx}% ${this.arc.cy}%`
            // 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{
        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{        
        transition:.5s;
    }
    .hn-sng .o-needle path, .hn-sng .o-needle circle{
        fill:black;
    }
    
</style>
8 Likes

Fantastic work !
(both of you)

1 Like

Nice.

For sure - different layouts like quarter or full circle don't use available space wisely thus it maybe requires some additional options to get full out of it but as for most of the hard work is done it's a walk in the park to make it look however you want then ..

Can CSS transform-origin and translate only take parameters in pixels, not svg co-ordinates? It seems to me that is the case which does complicate things if one wants to change the size of the viewport.

This maybe can help then: transform-box - CSS: Cascading Style Sheets | MDN