I’m trying to recreate the Dashboard 2.0 ui-gauge-tank with a custom unit (i need somthing else than %). I used the Vue component i found on Dashboard 2.0’s github, and adapted it to change the unit. It works well but I can’t get the wave animation working correctly…
I’m using node-red v4.0.9, NodeJS v22.14.0 and Dashboard 2.0 V1.22.1
Here’s my flow :
[
{
"id": "7c0a6281fad8692c",
"type": "tab",
"label": "Flux 1",
"disabled": false,
"info": "",
"env": []
},
{
"id": "c26cf52ff92bf8dc",
"type": "inject",
"z": "7c0a6281fad8692c",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "15",
"payloadType": "num",
"x": 170,
"y": 200,
"wires": [
[
"77736be83ff7aa82"
]
]
},
{
"id": "77736be83ff7aa82",
"type": "function",
"z": "7c0a6281fad8692c",
"name": "function 2",
"func": "msg.min = 4nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 380,
"y": 200,
"wires": [
[
"442aaa386a504831"
]
]
},
{
"id": "f2bc295826c5e605",
"type": "inject",
"z": "7c0a6281fad8692c",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "15",
"payloadType": "num",
"x": 150,
"y": 260,
"wires": [
[
"d0ccf216830a748d"
]
]
},
{
"id": "442aaa386a504831",
"type": "ui-template",
"z": "7c0a6281fad8692c",
"group": "fb6501fa7d8b6b7b",
"page": "",
"ui": "",
"name": "custom-tank",
"order": 1,
"width": "3",
"height": "3",
"head": "",
"format": "<template>n <div v-resize="onResize" class="nrdb-ui-gauge-tank--container">n <label class="nrdb-ui-gauge-title"> CUSTOM-TANK</label>nn <divn class="nrdb-ui-gauge-tank"n :style="{'--gauge-fill': color, '--gauge-fill-pc': pc + '%', 'color': color}"n >n <div class="nrdb-ui-gauge-tank--center">n <div ref="fill" class="nrdb-ui-gauge-tank--fill"></div>n <svg class="WaveBG" :style="`bottom: 0; height: ${svgBottom}px`" :viewBox="`0 ${amplitude_setting} 1000 ${Math.min(100, svgScaleRatio * svgBottom)}`" preserveAspectRatio="xMinYMin meet">n <path :d="waves[0]">n <animaten dur="5s"n repeatCount="indefinite"n attributeName="d"n :values="`${waves[0]}; ${waves[1]}; ${waves[0]};`"n />n </path>n </svg>n <svg class="Wave" :style="`bottom: 0; height: ${svgBottom}px;`" :viewBox="`0 ${amplitude_setting} 1000 ${Math.min(100, svgScaleRatio * svgBottom)}`" preserveAspectRatio="xMinYMin meet">n <path :d="waves[0]">n <animaten dur="5s"n repeatCount="indefinite"n attributeName="d"n :values="`${waves[0]}; ${waves[1]}; ${waves[0]};`"n />n </path>n </svg>nn <div ref="labels" class="nrdb-ui-gauge-tank-labels">n <label class="nrdb-ui-gauge-tank--label" >{{ this.value }} T</label>n </div>n </div>n </div>nn </div>n</template>n<script>nexport default {nn data() {n return {n labelLineHeight: 0,n svgBottom: 0,n amplitude_setting: 15,n svgScaleRatio: 1,n minivalue : 0,n maxivalue : 50n }n },n watch: {n value: function () {n this.$nextTick(() => {n // react to the fill element being updatedn this.updateMask()n })n }n },n computed: {nn color: function ()n {n if (this.value <= this.min) {return "#FF0000"}n return "#00FF00"n }n ,n pc: function () {n if (typeof this.value !== 'undefined') {n return Math.max(0, Math.min(Math.round((this.value - this.minivalue) / (this.maxivalue - this.minivalue) * 100), 100))n } else {nn return 0n }n },n clipId: function () {n return `clip-${this.id}`n },n waves: function () {n const amplitude = this.amplitude_setting * this.svgScaleRation const svgBottom = this.svgBottomn const svgScaleRatio = this.svgScaleRationn return [n `M750,${amplitude} c -125,0 -125,-${amplitude} -250,-${amplitude} s -125,${amplitude} -250,${amplitude} S 125,0, 0,0 v${svgScaleRatio * (svgBottom + amplitude)}h1000 V0 c-125,0 -125,${amplitude} -250,${amplitude}Z`,n `M750,0 c -125,0 -125,${amplitude} -250,${amplitude} S 375,0 250,0, S 125,${amplitude}, 0,${amplitude} v${svgScaleRatio * svgBottom}h1000 V${amplitude} c-125,0 -125-${amplitude} -250-${amplitude}Z`n ]n }n },n methods: {n updateMask () {n const h = this.$refs.fill?.clientHeight || 0n const w = this.$refs.fill?.clientWidth || 0nn this.labelLineHeight =`${this.$refs.labels.clientHeight}px`nn if (h >= 0 && this.pc !== 0) {n this.svgBottom = hn this.svgScaleRatio = w !== 0 ? 1000 / w : 1n } else {n this.svgBottom = 0n this.svgScaleRatio = 1n }n },n onResize () {n this.updateMask()n }n },n mounted () {nn this.$nextTick(() => {n this.updateMask()n })nn this.$socket.on('msg-input:' + this.id, (msg) => {n this.value = msg.payloadn this.min = msg.minn })n },n unmounted() {nn }n}nn</script>n<style scoped>n.Wave,n.WaveBG {n width: 200%;n position: absolute;n overflow: visible;n animation-name: swell;n animation-fill-mode: forwards;n animation-iteration-count: infinite;n animation-timing-function: linear;n fill: var(--gauge-fill);n bottom: var(--gauge-fill-pc);n}nn.WaveBG {n opacity: 0.4;n /* offset the animation so that the wave's standing nodes never overlap */n animation-duration: 1.5s;n animation-delay: 1.2s;n}n.Wave {n animation-duration: 2s;n}nn@keyframes swell {n 0% {n transform: scaleX(2) translateX(25%) translateY(1px);n }n 100% {n transform: scaleX(2) translateX(-25%) translateY(1px);n }n}nn.nrdb-ui-gauge-tank--container {n display: flex;n flex-direction: column;n container: tank-container / size;n}n.nrdb-ui-gauge-tank {n --tank-margin: 6px;n --tank-padding: 3px;n --tank-radius: 6px;n --tank-border: 4px;n}nn@container tank-container (min-width: 75px) and (min-height: 75px) {n .nrdb-ui-gauge-tank {n --tank-margin: 12px;n --tank-radius: 12px;n --tank-border: 8px;n --tank-padding: 6px;n }n}nn.nrdb-ui-gauge-tank {n border-radius: var(--tank-radius);n border-width: var(--tank-border);n padding: var(--tank-padding);n border-color: var(--gauge-fill);n border-style: solid;n flex-grow: 1;n position: relative;n}n.nrdb-ui-gauge-tank--fill {n background-color: transparent;n}n.nrdb-ui-gauge-tank-labels {n position: relative;n width: 100%;n height: 100%;n display: flex;n container-type: size;n}n.nrdb-ui-gauge-tank label {n font-weight: bold;n display:flex;n resize: both;n font-size: min(2.5rem, 50cqmin);n position: absolute;n z-index: 2;n width: 100%;n height: 100%;n text-align: center;n justify-content: center;n align-items: center;n color: black;n --text-border: 1px;n --text-border-inv: calc(-1 * 1px);n --text-border-color: white;n text-shadow: var(--text-border) var(--text-border-inv) var(--text-border-color),n var(--text-border-inv) var(--text-border-inv) var(--text-border-color),n var(--text-border-inv) var(--text-border) var(--text-border-color),n var(--text-border) var(--text-border) var(--text-border-color);n}nnnrdb-ui-gauge-title {n font-weight: bold;n display:flex;n resize: both;n font-size: min(2.5rem, 50cqmin);n position: absolute;n z-index: 2;n width: 100%;n height: 100%;n text-align: center;n justify-content: center;n align-items: center;n color: black;nn}nn.nrdb-ui-gauge-tank--center {n display: flex;n justify-content: center;n align-items: center;n height: 100%;n position: relative;n overflow: hidden;n}nn/* text mask */n.nrdb-ui-gauge-tank svg.mask {n top: 0;n}n.nrdb-ui-gauge-tank svg.mask,n.nrdb-ui-gauge-tank--fill {n position: absolute;n left: 0;n}n.nrdb-ui-gauge-tank--fill {n bottom: 0;n height: var(--gauge-fill-pc);n width: 100%;n}n</style>",
"storeOutMessages": true,
"passthru": false,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 610,
"y": 200,
"wires": [
[]
]
},
{
"id": "c6cc4137fbb3cdda",
"type": "ui-template",
"z": "7c0a6281fad8692c",
"group": "",
"page": "d30cf1a71bcee0b6",
"ui": "",
"name": "Dasboard 2.0 CSS",
"order": 0,
"width": 0,
"height": 0,
"head": "",
"format": ".nrdb-ui-text {n background-color: #eeeeee !important;n}nn.nrdb-ui-gauge {n background-color: #eeeeee !important;n}nn.nrdb-ui-gauge-title {n font-size: larger;n background-color: aqua;n}nn.valid.valid-label label {n color: black;n}nn.valid.dark-text span {n color: black;n}nn.valid.valid-text span {n color: green;n}nn.invalid.invalid-label label {n color: black;n}nn.invalid.invalid-text span {n color: red;n}nn",
"storeOutMessages": true,
"passthru": false,
"resendOnRefresh": true,
"templateScope": "page:style",
"className": "",
"x": 190,
"y": 100,
"wires": [
[]
]
},
{
"id": "d0ccf216830a748d",
"type": "ui-gauge",
"z": "7c0a6281fad8692c",
"name": "tank",
"group": "fb6501fa7d8b6b7b",
"order": 2,
"width": "3",
"height": "3",
"gtype": "gauge-tank",
"gstyle": "needle",
"title": "TANK",
"units": "Bar",
"icon": "",
"prefix": "",
"suffix": "",
"segments": [
{
"from": "0",
"color": "#ff0000"
},
{
"from": "4",
"color": "#00ff40"
}
],
"min": 0,
"max": "50",
"sizeThickness": 16,
"sizeGap": 4,
"sizeKeyThickness": 8,
"styleRounded": true,
"styleGlow": false,
"className": "custom-tank",
"x": 590,
"y": 260,
"wires": []
},
{
"id": "fb6501fa7d8b6b7b",
"type": "ui-group",
"name": "Group Name",
"page": "fd51264148764b4f",
"width": "12",
"height": "1",
"order": 1,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "fd51264148764b4f",
"type": "ui-page",
"name": "Page Name",
"ui": "955d24bee66399fa",
"path": "/exemple",
"icon": "home",
"layout": "grid",
"theme": "4cbed8b158df0175",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 1,
"className": "",
"visible": "true",
"disabled": "false"
},
{
"id": "955d24bee66399fa",
"type": "ui-base",
"name": "UI Name",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default",
"showReconnectNotification": true,
"notificationDisplayTime": "1",
"showDisconnectNotification": true
},
{
"id": "4cbed8b158df0175",
"type": "ui-theme",
"name": "Default Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0094CE",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "default",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
}
]
Thanks in advance for your help!