Type linear, round, vertical, or artless.
Code is HERE
TEMPLATE:
<template>
<div>
<template v-if="type === 'round'">
<div ref="hng" class="round-led-level" :style="`--size:${size}; --shadow:${shadow}; --ledsize:${ledSize};`">
<header>
<div class="round-led-level-text">
<span class="round-led-level-label">{{label}}</span>
</div>
</header>
<div class="round-led-level-graph">
<div class="round-led-level-stripe" :class="{'led-level-flat': flat }">
<div v-for="(color, index) in colors" :key="index" class="round-led-level-led"
:ref="'dot-' + index">
</div>
</div>
<div class="round-led-level-centered-text">
<span class="round-led-level-value">{{formattedValue}}</span>
<span class="round-led-level-unit">{{unit}}</span>
</div>
<div class="round-led-level-limits">
<span>{{min}}</span>
<span>{{max}}</span>
</div>
</div>
<div>
</template>
<template v-if="type === 'linear'">
<div ref="hng" class="led-level" :style="`--shadow:${shadow};`">
<div class="led-level-text">
<span class="led-level-label">{{label}}</span>
<span class="led-level-value">{{formattedValue}}<span class="led-level-unit">{{unit}}</span></span>
</div>
<div class="led-level-stripe" :class="{'led-level-flat': flat }">
<div v-for="(color, index) in colors" :key="index" class="led-level-led" :ref="'dot-' + index">
</div>
</div>
<div class="led-level-limits">
<span>{{min}}</span>
<span>{{max}}</span>
</div>
<div>
</template>
<template v-if="type === 'vertical'">
<div ref="hng" class="led-level-vertical" :style="`--shadow:${shadow}; --size:${size};`">
<header>
<div class="round-led-level-text">
<span class="round-led-level-label">{{label}}</span>
</div>
</header>
<div class="led-level-vertical-content">
<div class="led-level-stripe" :class="{'led-level-flat': flat }">
<div v-for="(color, index) in colors" :key="index" class="led-level-led" :ref="'dot-' + index">
</div>
</div>
<div class="led-level-limits">
<span>{{max}}</span>
<span>{{min}}</span>
</div>
<div class="round-led-level-centered-text">
<span class="round-led-level-value">{{formattedValue}}</span>
<span class="round-led-level-unit">{{unit}}</span>
</div>
</div>
</div>
</template>
<template v-if="type === 'artless'">
<div ref="hng" :class="icon ? 'ag-wrapper-2' : 'ag-wrapper-1'" :style="`--line-color:${colors[0]};`">
<div v-if="icon" class="ag-icon">
<v-icon aria-hidden="false">{{icon}}</v-icon>
</div>
<div class="ag-content">
<div class="ag-text">
<span class="ag-label">{{label}}</span>
<span class="ag-value">{{formattedValue}}<span class="ag-unit">{{unit}}</span></span>
</div>
<div class="ag-track" ref="agLine">
<div class="ag-track-background"></div>
<div class="ag-track-foreground" :style="{'width': linesize +'%'}"></div>
</div>
<div class="ag-limits">
<span class="ag-min">{{min}}</span>
<span class="ag-max">{{max}}</span>
</div>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
data(){
return {
//Define me here
type:"artless", // Gauge type. "artless", "linear", "vertical" or "round"
label:"Artless", // The label
icon:"mdi-account", // (type: artless) (optional) the icon
zeroCross:false,// (type: artless) line changes color depending on value being positive or negative (at least 2 colors must be defined and min must be 0)
min:0, // Smallest expected value
max:100, // Highest expected value
unit:"cm³",// The unit of the measurement
dim:0.3, //(type: round, linear, vertical) How dim is led when not glowing
shadow:0, //(type: round, linear, vertical) Led shadow intensity (too much makes graphics muddy, 0 removes shadows)
filterFunction:"brightness", // (type: round, linear, vertical) "brightness" for dark themes, "opacity" for light themes
animate:true, // Animating led's is not most performant thing in the world.
// Define colors
// For type "round", "vertical" and "linear" the count of colors equals count of led's.
// For type "artless" the line changes color based on percentage of value turned index of colors array.
// For type "round" the led size depends on how many colors is defined. About 20 is optimal.
// Color can be defined as:
// HEX - "#FF00FF"
// RGB - rgb(0,65,88)
// named color - "red"
// or depend on some defined CSS variable
colors:[
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"#0fb60f",
"red",
"red",
"red",
"red",
],
//no need to change those
value:0,
previousValue:0,
size:100,
inited:false
}
},
methods: {
getElement: function(name,base){
if(base){
return this.$refs[name]
}
return this.$refs[name][0]
},
validate(data){
let ret
if(typeof data !== "number"){
ret = parseFloat(data)
if(isNaN(ret)){
console.log("BAD DATA! gauge type:",this.type, "id:",this.id,"data:",data)
return null
}
}
else{
ret = data
}
return ret
},
full: function(){
return Math.floor(this.colors.length*this.percentage/100)
},
half: function (){
let p = this.colors.length*this.percentage/100;
p -= this.full()
p *= .5
p += this.dim;
return p;
},
filter: function(amount){
let f
switch(amount){
case "full":{
f = this.filterFunction == "brightness" ? "brightness(1.1)" : "opacity(1)";
break
}
case "half":{
f = this.filterFunction == "brightness" ? "brightness(" +this.half()+")" : "opacity(" +this.half()+")";
break
}
default:{
f = this.filterFunction == "brightness" ? "brightness(" +this.dim+")" : "opacity(" +this.dim+")";
break
}
}
return f
},
lit: function(){
if(this.inited == false){
return
}
const down = this.previousValue > this.value
let time = .01
this.colors.forEach((color,i) => {
let dot = this.getElement("dot-"+i);
if(!dot){
console.log("lit() no dots found")
return
}
if(i<this.full()){
dot.style.filter=this.filter("full");
}
else if(i==this.full()){
dot.style.filter= this.filter("half");
}
else{
dot.style.filter= this.filter("dim");
}
if(down){
time = (this.colors.length - i) * .12
}else{
time = i * 0.08
}
dot.style.transition = this.animate ? "filter "+time+"s" : "unset";
})
this.previousValue = this.value
},
changeLine:function(){
const line = this.getElement("agLine",true);
if(!line){
console.log("no line found")
return
}
let c = Math.floor(this.colors.length * this.percentage / 100)
if(c >= this.colors.length){
c = this.colors.length - 1
}
if(c < 0){
c = 0
}
if(this.zeroCross){
c = this.value > 0 ? 1 : 0
}
line.style.setProperty('--line-color',this.colors[c])
},
onResize:function(){
let g = this.getElement("hng",true)
if(!g){
return
}
this.$nextTick(() => {
let last = this.size
let changed = this.type == "vertical" ? g.clientHeight : g.clientWidth;
if(Math.abs(last - changed) < 3){
return
}
this.size = changed
g.style.setProperty('--size',this.size);
if(this.type == "round"){
this.updateLayout()
}
})
},
updateLayout:function(){
let angle;
const step = 270 / this.colors.length;
const radius = (this.size - (this.size*0.1))/2
const s = this.ledSize / -2;
const outline = this.filterFunction == "opacity" ? "black" : "white";
this.colors.forEach((c,i) => {
let dot = this.getElement("dot-"+i);
if(!dot){
console.log("round init() no dots found")
return
}
dot.style.backgroundColor = c
dot.style.outlineColor="color-mix(in srgb, "+c+", "+outline+" 35%)";
dot.style.transition = "filter 0.1s";
dot.style.setProperty('--dot',i);
angle = ((i+1)*step) * Math.PI / 180;
dot.style.left = s + radius * Math.cos(angle) + 'px';
dot.style.top = s + radius * Math.sin(angle) + 'px';
dot.style.transform = 'translate('+s+'px, '+s+'px)';
dot.style.rotate = (angle - 0.08)+"rad"
}
)
}
},
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()
}
}
}
},
computed: {
formattedValue: function () {
return this.value.toFixed(2)
},
percentage: function(){
return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);
},
linesize:function(){
if(this.zeroCross){
return Math.floor(((Math.abs(this.value) - this.min) / (this.max - this.min)) * 100);
}
else{
return Math.max(0,this.percentage)
}
},
ledSize:function(){
const s = 4.71239 * ((this.size - (this.size*0.3))/2)
return s / this.colors.length
},
flat:function(){
return this.shadow == 0
}
},
mounted(){
const outline = this.filterFunction == "opacity" ? "black" : "white";
if(this.type == "round"){
let g = this.getElement("hng",true)
if(!g){
return
}
this.resizeObserver = new ResizeObserver((entries) => {
this.onResize()
});
this.resizeObserver.observe(g);
setTimeout(()=>{
this.onResize()
},20)
}
else if(this.type == "linear"){
this.colors.forEach((c,i) => {
let dot = this.getElement("dot-"+i);
if(!dot){
console.log("linear init() no dots found")
return
}
dot.style.outlineColor="color-mix(in srgb, "+c+", "+outline+" 35%)";
dot.style.backgroundColor = c
dot.style.transition = "filter 0.1s";
}
)
}
else if(this.type == "vertical"){
let g = this.getElement("hng",true)
if(!g){
return
}
this.resizeObserver = new ResizeObserver((entries) => {
this.onResize()
});
this.resizeObserver.observe(g);
setTimeout(()=>{
this.onResize()
},20)
this.colors.forEach((c,i) => {
let dot = this.getElement("dot-"+i);
if(!dot){
console.log("linear init() no dots found")
return
}
dot.style.outlineColor="color-mix(in srgb, "+c+", "+outline+" 35%)";
dot.style.backgroundColor = c
dot.style.transition = "filter 0.1s";
})
}
else if(this.type == "artless"){
const line = this.getElement("agLine",true);
line.style.setProperty('--line-color',this.colors[0])
if(this.animate == true){
if(!line){
console.log("artless init() no line found")
return
}
line.style.transition = "width 0.5s";
}
}
this.inited = true;
},
unmounted () {
if(this.resizeObserver){
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
}
</script>
CSS:
.led-level{
display: grid;
grid-template-rows: 1.3em minmax(3px, 1fr) .7em;
gap: 2px;
height: 100%;
}
.led-level-stripe{
display: flex;
gap:2px;
}
.led-level-led {
--s:var(--shadow,0.2);
--shadowColor:rgba(0,0,0,var(--s));
background: #ffffff;
width: 100%;
height: 100%;
outline:1px solid;
outline-offset:-1px;
border-radius: 0px;
box-shadow: inset 0px 0px 10px 0px var(--shadowColor), 0px 0px 3px 0px var(--shadowColor);
filter: brightness(0.4);
}
.led-level-text{
font-size: 1.25em;
line-height: 1em;
align-self: end;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
user-select: none;
}
.led-level-value{
font-weight:bold;
}
.led-level-unit{
font-size:.75em;
font-weight:normal;
padding-inline-start: 0.15em;
}
.led-level-limits{
display: flex;
justify-content: space-between;
font-size: .75em;
line-height: .75em;
align-content: flex-end;
flex-wrap: wrap;
user-select: none;
}
.round-led-level, .led-level-vertical{
display: grid;
grid-template-rows: 1em 1fr;
width:100%;
height: 100%;
aspect-ratio: 1/1;
position: relative;
margin: auto;
}
.round-led-level-graph{
position: relative;
aspect-ratio: 1;
}
.round-led-level-stripe{
display: block;
position: absolute;
left: 50%;
top: 56%;
rotate: 135deg;
}
.round-led-level-led {
--s:var(--shadow,0.2);
--shadowColor:rgba(0,0,0,var(--s));
background: #ffffff;
position: absolute;
width: calc(var(--ledsize) * 1px);
aspect-ratio: 1/1;
border-radius: 4px;
outline:1px solid;
outline-offset:-1px;
box-shadow: inset 0px 0px calc(var(--ledsize) / 3 * 1px) 0px var(--shadowColor), 0px 0px calc(var(--ledsize) / 7 * 1px) 0px var(--shadowColor);
filter: brightness(0.4);
transform-origin: center center;
}
.round-led-level-text{
font-size: clamp(0.5em,calc(var(--size) * .1 * 1px),1.25em);
line-height: 1rem;
text-align: center;
user-select: none;
white-space: nowrap;
}
.round-led-level-centered-text{
position: absolute;
inset: 0;
font-size: 1rem;
line-height: 1;
display: grid;
text-align: center;
grid-template-rows: 1.5fr 1fr;
gap: 0.1em;
user-select: none;
align-items: center;
}
.round-led-level-value{
font-weight:bold;
font-size: calc(var(--size) * .15 * 1px);
align-self: end;
}
.round-led-level-unit{
font-size:calc(var(--size) * .1 * 1px);
font-weight:normal;
align-self: start;
padding-inline-start: 0.15em;
}
.round-led-level-limits{
position: absolute;
inset:0;
display: flex;
justify-content: space-between;
font-size: calc(var(--size) * .06 * 1px);
line-height: calc(var(--size) * .06 * 1px);
align-content: flex-end;
flex-wrap: wrap;
padding-inline:1em;
user-select: none;
}
.ag-wrapper-2 {
display: grid;
grid-template-columns: 3em 1fr;
gap:1em;
}
.ag-wrapper-1 {
display: grid;
grid-template-columns: 1fr;
}
.ag-icon{
font-size: 2em;
display: flex;
flex-direction: column;
justify-content: center;
}
.ag-content{
display: grid;
grid-template-rows: 1fr 7px 0.75em;
gap: 2px;
}
.ag-text{
font-size: 1.25em;
line-height: 1em;
align-self: end;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
user-select: none;
}
.ag-value{
font-weight:bold;
}
.ag-unit{
font-size:.75em;
font-weight:normal;
padding-inline-start: 0.15em;
}
.ag-limits{
display: flex;
justify-content: space-between;
font-size: .75em;
line-height: .75em;
align-content: center;
flex-wrap: wrap;
user-select: none;
}
.ag-track{
position:relative;
display:flex;
align-items: center;
width: 100%;
border-radius: 6px;
}
.ag-track-background{
position:absolute;
background: var(--line-color,rgb(var(--v-theme-primary)));
opacity: 0.45;
width: 100%;
height: 50%;
border-radius:inherit;
}
.ag-track-foreground{
position:absolute;
background-color: var(--line-color,rgb(var(--v-theme-primary)));
width: 50%;
height: 100%;
max-width: 100%;
border-radius:inherit;
transition:inherit;
}
.led-level-vertical{
gap:calc(var(--size) * .1 * 1px);
aspect-ratio: var(--aspect-ratio);
}
.led-level-vertical-content{
display:grid;
grid-template-columns: 1fr auto 4fr;
}
.led-level-vertical .round-led-level-centered-text{
grid-template-rows: 2.5fr 1fr;
padding-inline-start: 1em;
}
.led-level-vertical .round-led-level-value{
font-size: calc(var(--size) * .2 * 1px);
}
.led-level-vertical .led-level-stripe{
flex-direction: column-reverse;
}
.led-level-vertical .led-level-limits{
display: flex;
justify-content: space-between;
font-size: calc(var(--size) * .075 * 1px);
line-height: calc(var(--size) * .075 * 1px);
align-content: flex-start;
align-items: start;
flex-wrap: wrap;
user-select: none;
flex-direction: column;
padding-inline-start: 0.4em;
}
.led-level-flat div{
box-shadow: unset;
}