Linear gauge as another gauge option

For now I'm using some template as a linear gauge, a gauge style that for me is missing as standard on the palette.

Is very useful to monitor the level of something like for instance in my case light level and could be also great being able to adjust color for temperature or something like this.

The main point is that linear gauge is able to display same valuable information that any other kind of the available gauges but takes much less space on the dashboard.

As example:

image

Regards

Sorry, just spotted this. Is there a helper library that will show this?

There is this on the flows site

https://flows.nodered.org/flow/1538793825685bd8f08e4aea16ca2c74

I started over to remake my home automation dashboard. I like gauges but as their graphics cant be customized so I decided to go for something similar to this topic. It's actually not that complicated thing to do (if targeted to dedicated task). It still "work in progress" so I don't want to share the flow in this stadium in public. But I'm happy to share basics and help to get started with.

Here's the visual outcome of mine as example.
image

3 Likes

I especially like the look of the vertical elements on the right side, very neat.
Are you using a plug-in library, or drawn in the flow?

ui_template and drawing with svg elements

I'll look forward to seeing how you achieved it, if/when you do share :wink:

1 Like

There is couple of things to do for example I want them to be able to be rendered in different widget sizes without major modification needed. And as it rely on properly feed data, this part should be optimized and simplified also. So it takes some time but not long I hope :wink:

2 Likes

As promised, a little example how to achieve it using default set nodes ("node_red_dashboard" is needed).
Feel free to use and modify it by your needs. The colors I used look fine on medium gray background.

I separated configuring part from data preparing part into different functions just for clarity of example.
Parameters can be configured using Change node also

You can get rid of shadows by just deleting the filter properties in ui_template :slight_smile: Shadows harm rendering for sure and not recommended for performance critical situations.

I added comments about basic stuff, I hope it makes this thing kind of clear.

[{"id":"66684c36.a1fb94","type":"inject","z":"828c92a8.6bb45","name":"","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"onceDelay":0.1,"x":370,"y":280,"wires":[["fdc73f50.1ac08"]]},{"id":"fdc73f50.1ac08","type":"function","z":"828c92a8.6bb45","name":"Generate some data","func":"var lastupdate = global.get(\"lastupdate\") || 0;\nif(lastupdate++ > 99){\n lastupdate = 0;\n}\nglobal.set(\"lastupdate\",lastupdate);\nmsg.payload = lastupdate\nreturn msg;","outputs":1,"noerr":0,"x":550,"y":280,"wires":[["313749d2.75fae6","818a6bf4.ddc6e8","4be2825.29c377c","85ebd007.f2145","c096a17b.058b7","5769d1e9.05358","b7f58518.7e0968"]]},{"id":"ae422917.1413b8","type":"ui_template","z":"828c92a8.6bb45","group":"2accfba2.147474","name":"svg level vertical 2x3","order":2,"width":"2","height":"3","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n x=\"0\" \n ng:attr:y=\"{{$index * 4}}px\"\n width=\"12\" \n height=\"2\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-ver-value\"\n text-anchor=\"middle\" alignment-baseline=\"middle\"\n x=55%\n y=50%>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=15\n ng:attr:y={{msg.minpos}}>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"hanging\"\n x=15\n y=1 >\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1130,"y":320,"wires":[[]]},{"id":"81337426.09e378","type":"ui_template","z":"828c92a8.6bb45","group":"e19b4f66.fbbeb","name":"svg level horizontal 2x1","order":4,"width":"2","height":"1","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.2 0 0 0 0 0 0.2 0 0 0 0 0 0.2 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n y=80% \n ng:attr:x=\"{{$index * 4}}px\"\n width=\"2\"\n height=\"6\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-hor-value\"\n text-anchor=\"middle\" alignment-baseline=\"hanging\"\n x=50%\n y=0>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=0\n y=85%>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"end\" alignment-baseline=\"ideographic\"\n x=100%\n y=85%>\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1140,"y":480,"wires":[[]]},{"id":"3d2df1f1.8c42fe","type":"ui_template","z":"828c92a8.6bb45","group":"efcad470.381fb8","name":"Custom element style","order":0,"width":0,"height":0,"format":"<style>\n #level-hor-value {\n font-size: 1.4em;\n fill:#d0d0d0;\n }\n #level-minmax {\n font-size: 60%;\n fill:#d0d0d0;\n }\n #level-3x3-stripe {\n fill: #404040;\n }\n #level-ver-value {\n font-size: 1.8em;\n fill:#d0d0d0;\n }\n #chart-hour {\n font-size: 60%;\n fill:#d0d0d0;\n }\n #chart-stripe {\n fill: #404040;\n }\n\n</style>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"global","x":850,"y":180,"wires":[[]]},{"id":"76e613ef.e1bb0c","type":"inject","z":"828c92a8.6bb45","name":"init","topic":"init","payload":"create","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":430,"y":180,"wires":[["8961d767.2bad38"]]},{"id":"8961d767.2bad38","type":"function","z":"828c92a8.6bb45","name":"set site properties","func":"var unit = {\"h\":48,w:48}; // site sizes 1x1 wdget size\nvar colormap = {on:\"#47d147\",off:\"dimgrey\",warn:\"#ff9933\", high:\"#ff5c33\"} // green, gray, orange, red \nvar site = {\"unit\":unit,\"widgetcolors\":colormap}; //merge to store to global as one variable\nglobal.set(\"site\",site);\nreturn msg;","outputs":1,"noerr":0,"x":610,"y":180,"wires":[[]]},{"id":"c1f35432.aeb748","type":"ui_template","z":"828c92a8.6bb45","group":"2accfba2.147474","name":"svg level vertical 2x4","order":3,"width":"2","height":"4","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n x=\"0\" \n ng:attr:y=\"{{$index * 4}}px\"\n width=\"12\" \n height=\"2\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-ver-value\"\n text-anchor=\"middle\" alignment-baseline=\"middle\"\n x=55%\n y=50%>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=15\n ng:attr:y={{msg.minpos}}>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"hanging\"\n x=15\n y=1 >\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1130,"y":360,"wires":[[]]},{"id":"3978cc74.8f9694","type":"ui_template","z":"828c92a8.6bb45","group":"2accfba2.147474","name":"svg level vertical 2x5","order":4,"width":"2","height":"5","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n x=\"0\" \n ng:attr:y=\"{{$index * 4}}px\"\n width=\"12\" \n height=\"2\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-ver-value\"\n text-anchor=\"middle\" alignment-baseline=\"middle\"\n x=55%\n y=50%>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=15\n ng:attr:y={{msg.minpos}}>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"hanging\"\n x=15\n y=1 >\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1130,"y":400,"wires":[[]]},{"id":"9845d23a.797d6","type":"ui_template","z":"828c92a8.6bb45","group":"2accfba2.147474","name":"svg level vertical 2x2","order":1,"width":"2","height":"2","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n x=\"0\" \n ng:attr:y=\"{{$index * 4}}px\"\n width=\"12\" \n height=\"2\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-ver-value\"\n text-anchor=\"middle\" alignment-baseline=\"middle\"\n x=55%\n y=50%>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=15\n ng:attr:y={{msg.minpos}}>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"hanging\"\n x=15\n y=1 >\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1130,"y":280,"wires":[[]]},{"id":"313749d2.75fae6","type":"function","z":"828c92a8.6bb45","name":"Configure","func":"msg.value = msg.payload; //expected to be a number\nmsg.min = 0; //expected minimum value of incoming data\nmsg.max = 100; //expected maximum value of incoming data\nmsg.valuesuffix = \"%\" //kind of data \nmsg.direction = \"vertical\"; //widget look\nmsg.units = 2; //how many units widget takes in direction \n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":280,"wires":[["7925330e.07bb5c"]]},{"id":"818a6bf4.ddc6e8","type":"function","z":"828c92a8.6bb45","name":"Configure","func":"msg.value = msg.payload; //expected to be a number\nmsg.min = 0; //expected minimum value of incoming data\nmsg.max = 100; //expected maximum value of incoming data\nmsg.valuesuffix = \"%\" //kind of data \nmsg.direction = \"vertical\"; //widget look\nmsg.units = 3; //how many units widget takes in direction \n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":320,"wires":[["136cbf9c.efa6"]]},{"id":"4be2825.29c377c","type":"function","z":"828c92a8.6bb45","name":"Configure","func":"msg.value = msg.payload; //expected to be a number\nmsg.min = 0; //expected minimum value of incoming data\nmsg.max = 100; //expected maximum value of incoming data\nmsg.valuesuffix = \"%\" //kind of data \nmsg.direction = \"vertical\"; //widget look\nmsg.units = 4; //how many units widget takes in direction \n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":360,"wires":[["713cf384.d53dec"]]},{"id":"85ebd007.f2145","type":"function","z":"828c92a8.6bb45","name":"Configure","func":"msg.value = msg.payload; //expected to be a number\nmsg.min = 0; //expected minimum value of incoming data\nmsg.max = 100; //expected maximum value of incoming data\nmsg.valuesuffix = \"%\" //kind of data \nmsg.direction = \"vertical\"; //widget look\nmsg.units = 5; //how many units widget takes in direction \n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":400,"wires":[["1e353951.383ed7"]]},{"id":"c096a17b.058b7","type":"function","z":"828c92a8.6bb45","name":"Configure","func":"msg.value = msg.payload; //expected to be a number\nmsg.min = 0; //expected minimum value of incoming data\nmsg.max = 100; //expected maximum value of incoming data\nmsg.valuesuffix = \"%\" //kind of data \nmsg.direction = \"horizontal\"; //widget look\nmsg.units = 2; //how many units widget takes in direction \n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":480,"wires":[["d8cda4f7.3cf358"]]},{"id":"7925330e.07bb5c","type":"function","z":"828c92a8.6bb45","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.floor(unit * msg.units / 4) - 2; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+1, minout:0, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// for vertical, the last stripe will not be at the bottom exactly.\n// for adjusting min value position.\nmsg.minpos = count*4; \nreturn msg;","outputs":1,"noerr":0,"x":960,"y":280,"wires":[["9845d23a.797d6"]]},{"id":"136cbf9c.efa6","type":"function","z":"828c92a8.6bb45","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.floor(unit * msg.units / 4) - 2; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+1, minout:0, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// for vertical, the last stripe will not be at the bottom exactly.\n// for adjusting min value position.\nmsg.minpos = count*4; \nreturn msg;","outputs":1,"noerr":0,"x":960,"y":320,"wires":[["ae422917.1413b8"]]},{"id":"713cf384.d53dec","type":"function","z":"828c92a8.6bb45","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.floor(unit * msg.units / 4) - 2; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+1, minout:0, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// for vertical, the last stripe will not be at the bottom exactly.\n// for adjusting min value position.\nmsg.minpos = count*4; \nreturn msg;","outputs":1,"noerr":0,"x":960,"y":360,"wires":[["c1f35432.aeb748"]]},{"id":"1e353951.383ed7","type":"function","z":"828c92a8.6bb45","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.floor(unit * msg.units / 4) - 2; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+1, minout:0, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// for vertical, the last stripe will not be at the bottom exactly.\n// for adjusting min value position.\nmsg.minpos = count*4; \nreturn msg;","outputs":1,"noerr":0,"x":960,"y":400,"wires":[["3978cc74.8f9694"]]},{"id":"d8cda4f7.3cf358","type":"function","z":"828c92a8.6bb45","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.floor(unit * msg.units / 4) - 2; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+1, minout:0, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// for vertical, the last stripe will not be at the bottom exactly.\n// for adjusting min value position.\nmsg.minpos = count*4; \nreturn msg;","outputs":1,"noerr":0,"x":960,"y":480,"wires":[["81337426.09e378"]]},{"id":"fb78f9a5.209888","type":"ui_template","z":"828c92a8.6bb45","group":"e19b4f66.fbbeb","name":"svg level horizontal 3x1","order":4,"width":"3","height":"1","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n y=80% \n ng:attr:x=\"{{$index * 4}}px\"\n width=\"2\"\n height=\"6\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-hor-value\"\n text-anchor=\"middle\" alignment-baseline=\"hanging\"\n x=50%\n y=0>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=0\n y=85%>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"end\" alignment-baseline=\"ideographic\"\n x=100%\n y=85%>\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1140,"y":520,"wires":[[]]},{"id":"5769d1e9.05358","type":"function","z":"828c92a8.6bb45","name":"Configure","func":"msg.value = msg.payload; //expected to be a number\nmsg.min = 0; //expected minimum value of incoming data\nmsg.max = 100; //expected maximum value of incoming data\nmsg.valuesuffix = \"%\" //kind of data \nmsg.direction = \"horizontal\"; //widget look\nmsg.units = 4; //how many units widget takes in direction \n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":520,"wires":[["bdd01ad3.6a39c8"]]},{"id":"bdd01ad3.6a39c8","type":"function","z":"828c92a8.6bb45","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.floor(unit * msg.units / 4) - 2; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+1, minout:0, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// for vertical, the last stripe will not be at the bottom exactly.\n// for adjusting min value position.\nmsg.minpos = count*4; \nreturn msg;","outputs":1,"noerr":0,"x":960,"y":520,"wires":[["fb78f9a5.209888"]]},{"id":"f82fcdc6.76ca8","type":"ui_template","z":"828c92a8.6bb45","group":"e19b4f66.fbbeb","name":"svg level horizontal 4x1","order":4,"width":"4","height":"1","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n y=80% \n ng:attr:x=\"{{$index * 4}}px\"\n width=\"2\"\n height=\"6\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-hor-value\"\n text-anchor=\"middle\" alignment-baseline=\"hanging\"\n x=50%\n y=0>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=0\n y=85%>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"end\" alignment-baseline=\"ideographic\"\n x=100%\n y=85%>\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1140,"y":560,"wires":[[]]},{"id":"b7f58518.7e0968","type":"function","z":"828c92a8.6bb45","name":"Configure","func":"msg.value = msg.payload; //expected to be a number\nmsg.min = 0; //expected minimum value of incoming data\nmsg.max = 100; //expected maximum value of incoming data\nmsg.valuesuffix = \"%\" //kind of data \nmsg.direction = \"horizontal\"; //widget look\nmsg.units = 4; //how many units widget takes in direction \n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":560,"wires":[["80d29778.56a648"]]},{"id":"80d29778.56a648","type":"function","z":"828c92a8.6bb45","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.floor(unit * msg.units / 4) - 2; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+1, minout:0, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// for vertical, the last stripe will not be at the bottom exactly.\n// for adjusting min value position.\nmsg.minpos = count*4; \nreturn msg;","outputs":1,"noerr":0,"x":960,"y":560,"wires":[["f82fcdc6.76ca8"]]},{"id":"2accfba2.147474","type":"ui_group","z":"","name":"VERITCAL","tab":"ee954aaa.1cbb18","order":2,"disp":true,"width":"8","collapse":false},{"id":"e19b4f66.fbbeb","type":"ui_group","z":"","name":"HORIZONTAL","tab":"ee954aaa.1cbb18","disp":true,"width":"6","collapse":false},{"id":"efcad470.381fb8","type":"ui_group","z":"","name":"SITE","tab":"b5e9809.d91cc8","disp":true,"width":"6","collapse":false},{"id":"ee954aaa.1cbb18","type":"ui_tab","z":"","name":"WIDGETS","icon":"dashboard","order":1},{"id":"b5e9809.d91cc8","type":"ui_tab","z":"","name":"MAIN","icon":"dashboard","order":1}]

3 Likes

Nice work!
The flow draws upon some of the more advanced features of node-RED/javascript, which unfortunately are above my pay grade :tired_face:
It would be really good if it could be wrapped up into a single library node, but I can see why that probably isn't possible.

Well it is possible for sure. It's just matter of where to draw the line. If the "Thing" already does "This", then why it shouldn't do "that". And this way it is never ending story. That's why I try to do my "things" by myself and targeting them to do the dedicated tasks.

Well everyone starts from some point. Hunger is the best cook. Try, fail, ask, fail again and learn from every step. And never ever give up. There is nothing you can't achieve. There is only new and more advanced targets to reach in every step on the everlasting learning curve.

3 Likes

linear

Hi hotNipi,
Being new to NodeRed I was impressed with your Dashboard design skills. I like the image example you post and beg you if you can also share the code for the Graphics block, upper rigth side of this image. I'm just beggining with a personal project for a data logger at home, recording temperatures in several points and found this representation higly adecuate for this pourpose.
Thanks in advance
Javier

Well I can share those things also. Takes a little time to prepare one to be shareable cos I have those things too deeply tied into my current solution. Let's see if I can do it tomorrow. Or if you are interested with specific thing - point it out :slight_smile: Less things for me to struggle with.

This is where I am right now with my dashboard design

7 Likes

Thats a work of art - i want it !!! Looks great

You have to template this !!

Craig

1 Like

Hi hotNipi I have been using some of your Linear gauges for a while now, however whilst they are brilliant when using the ui Dashboard 2.9.8

image

when I upgraded to 2.13.2 the gauge is no longer centered, its like justified left I guess its because the ui templates choices have changed, I am now using "Angular theme in ui_template" but it does not matter which choice I make its the same result.

image

I tried adjusting the settings in the template node and managed to move of "level-minmax" text-anchor="start" and end positions and I can move the gauge bars up and down but I am unable effect the left starting position I think its ng:attr:x but Im totally stuck please advise.

<rect ng-repeat="obj in msg.colors track by $index"
y=80% // up~down
ng:attr:x="{{$index * 4}}px"
width="2"
height="6"
style="fill:{{msg.colors[$index]}}" filter="url(#level-filter)"
/>

ng:attr:x="{{($index * 4)+10}}px" might help as quick fix but it's not correct solution.
I'm at work at the moment and cant really check it out. But I'll check it at the evening
. I haven't updated dashboard for my working unit so I never run into this. But I will check out If something needs or can be improved.
Actually I should start to create proper UI node for this but yeah, too busy for that at the moment.

aha yeh that fixes it, thanks a lot, I can now keep using it. cheers

Did a bit adjustments to fit better into angular theme.

In the Generate node the line where you can fine-tune stripe count is this.

// how many stripes should be drawn according to calculated widget size
var count = Math.ceil(unit * msg.units / 4) + 1;

It might depend on your global site settings and I'm not very sure if I can manage this properly. That was and is the main problem of the whole thing. But you can do trial & error to figure it out.

And here's the changed bits

[{"id":"dc6b2ff2.de8dc","type":"ui_template","z":"3c409e2.49e6162","group":"3635d22a.45b01e","name":"svg level horizontal 2x1","order":4,"width":"2","height":"1","format":"<svg width=100% height=100%>\n <defs>\n <filter id=\"level-filter\" x=\"0\" y=\"0\" width=\"200%\" height=\"200%\">\n <feOffset result=\"offOut\" in=\"SourceGraphic\" dx=\"1\" dy=\"1\" />\n <feColorMatrix result=\"matrixOut\" in=\"offOut\" type=\"matrix\"\n values=\"0.2 0 0 0 0 0 0.2 0 0 0 0 0 0.2 0 0 0 0 0 1 0\" />\n <feGaussianBlur result=\"blurOut\" in=\"matrixOut\" stdDeviation=\"5\" />\n <feBlend in=\"SourceGraphic\" in2=\"blurOut\" mode=\"normal\" />\n </filter>\n </defs>\n <rect ng-repeat=\"obj in msg.colors track by $index\" \n y=80% \n ng:attr:x=\"{{$index * 4}}px\"\n width=\"2\"\n height=\"6\" \n style=\"fill:{{msg.colors[$index]}}\" filter=\"url(#level-filter)\"\n />\n <text id=\"level-hor-value\"\n text-anchor=\"middle\" alignment-baseline=\"hanging\"\n x=50%\n y=0>\n {{msg.value}}{{msg.valuesuffix}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"start\" alignment-baseline=\"ideographic\"\n x=0\n y=84%>\n {{msg.min}}\n </text>\n <text id=\"level-minmax\"\n text-anchor=\"end\" alignment-baseline=\"ideographic\"\n ng:attr:x={{msg.minpos}}\n y=84%>\n {{msg.max}}\n </text>\n</svg> ","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":900,"y":500,"wires":[[]]},{"id":"db36182a.67f278","type":"function","z":"3c409e2.49e6162","name":"Generate","func":"//drawing of stripes will be done on exact pixels to avoid blur\n//calculated count of stripes will not fill 100% of area.\n\n\n// get global variables\nvar unit = global.get(\"site\").unit.h;\nvar colormap = global.get(\"site\").widgetcolors;\n// declare local variables\nvar c;\nvar colors = [];\n\n// how many stripes should be drawn according to calculated widget size\nvar count = Math.ceil(unit * msg.units / 4) + 1; \n\n// parameters for range incoming values into size of widget\nvar params = {minin:msg.min, maxin:msg.max+0.01, minout:1, maxout:count};\n\n// range\nfunction range(n,params){\n var divisor = params.maxin - params.minin;\n n = n > params.maxin ? params.maxin - 0.01 : n;\n n = n < params.minin ? params.minin : n;\n n = ((n - params.minin) % divisor + divisor) % divisor + params.minin;\n n = ((n - params.minin) / (params.maxin - params.minin) * (params.maxout - params.minout)) + params.minout;\n return n;\n}\nvar rangedvalue = Math.round(range(msg.value,params));\n\n// determine color for each stripe\nfor(var i=0; i<count; i++){\n c = rangedvalue <= i ? colormap.off : ( i < (count-5) ? colormap.on : ( i < (count-2) ? colormap.warn : colormap.high));\n colors.push(c);\n}\n//vertical starts drawing from top. colors needed to be in reverse order \nif(msg.direction === \"vertical\"){\n colors.reverse();\n}\n\n// set msg parameters\nmsg.colors = colors;\n\n// the last stripe will not be at the end exactly.\n// for adjusting boundary value position.\nmsg.minpos = count*4; \nif(msg.direction === \"horizontal\"){\n msg.minpos -= 2; \n}\n\nreturn msg;","outputs":1,"noerr":0,"x":710,"y":500,"wires":[["dc6b2ff2.de8dc"]]},{"id":"3635d22a.45b01e","type":"ui_group","z":"","name":"HORIZONTAL","tab":"2431693f.26dba6","disp":true,"width":"6","collapse":false},{"id":"2431693f.26dba6","type":"ui_tab","z":"","name":"WIDGETS","icon":"dashboard","order":1}]

1 Like

Hey @hotNipi,

That looks damn good!! Have you ever considered to convert it to a UI widget node? Think it would become a bestseller :wink:
Bart

[EDIT] Could it be somehow related to node-red-contrib-ui-level?