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?
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?
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.
<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...
<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
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.
<!-- 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>
Fantastic work !
(both of you)
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