Uibuilder - example pure SVG gauge created dynamically

Really can't genuinely call this "zero" code. However, it is certainly low-code. You will need v6.3.0 (just published) to try this example because that release now allows not just HTML to be created dynamically but SVG images as well.

Simply use this single inject node to create a very simplistic gauge display. It has some parameters you can change but it is certainly not a feature-complete example.

[{"id":"8116c9abacbaf474","type":"inject","z":"56443195ea782ac2","name":"svg gauge","props":[{"p":"gaugeId","v":"gauge-1","vt":"str"},{"p":"radius","v":"200","vt":"num"},{"p":"topic","vt":"str"},{"p":"payload"},{"p":"_ui","v":"(\t  $strokeWidth := radius * 0.2;\t  $innerRadius := radius - $strokeWidth / 2;\t  $circumference := $innerRadius * 2 * 3.14;\t  $arc := $circumference * (270 / 360);\t  $dashArray := $arc & \" \" & $circumference;\t  $transform := \"rotate(135, \" & radius & \", \" & radius & \")\";\t  $percentNormalized := $min([$max([payload, 0]), 100]);\t  $offset := $arc - ($percentNormalized / 100) * $arc;\t\t[\t  {\t    \"method\": \"replace\",\t    \"components\": [\t      {\t        \"type\": \"svg\",\t        \"id\": gaugeId,\t        \"parent\": \"#more\",\t        \"position\": \"last\",\t        \"attributes\": {\t          \"xmlns\": \"http://www.w3.org/2000/svg\",\t          \"xmlns:xlink\": \"http://www.w3.org/1999/xlink\",\t          \"height\": radius * 2,\t          \"width\": radius * 2\t        },\t        \"components\": [\t          {\t            \"type\": \"circle\",\t            \"attributes\": {\t              \"class\": \"gauge_base\",\t              \"cx\": radius,\t              \"cy\": radius,\t              \"fill\": \"transparent\",\t              \"r\": $innerRadius,\t              \"stroke\": \"var(--surface5)\",\t              \"stroke-width\": $strokeWidth,\t              \"stroke-dasharray\": $dashArray,\t              \"transform\": $transform,\t              \"stroke-linecap\": \"round\"\t            }\t          },\t          {\t            \"type\": \"circle\",\t            \"attributes\": {\t              \"class\": \"gauge_pct\",\t              \"cx\": radius,\t              \"cy\": radius,\t              \"fill\": \"transparent\",\t              \"r\": $innerRadius,\t              \"stroke\": \"var(--primary-bg)\",\t              \"stroke-width\": $strokeWidth,\t              \"stroke-dasharray\": $dashArray,\t              \"stroke-dashoffset\": $offset,\t              \"transform\": $transform,\t              \"stroke-linecap\": \"round\",\t              \"style\": \"transition: \\\"stroke-dasharray 0.5s;\\\"\"\t            }\t          }\t        ]\t      }\t    ]\t  }\t]\t)","vt":"jsonata"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"svg-gauge","payload":"$random() * 100","payloadType":"jsonata","x":760,"y":920,"wires":[["278d2031ac38b84a"]],"info":"Based on this REACT example:\r\nhttps://www.fullstacklabs.co/blog/creating-an-svg-gauge-component-from-scratch"}]

We made good use of JSONata here so that the gauge really is generated dynamically. The payload is also dynamic. If you use the uib-brand.css file, the display also adapts to light/dark settings.

Keep track of this thread if you like as I will doubtless expand the example further.

Eventually, this will be incorporated into a zero-code node for ease of use.

Animation1

2 Likes

And here is a slightly more complete and interesting version.

This one has a second inject that only updates the gauge value. The main inject has also been updated to allow you to change the max/min values.

[{"id":"fc6e0ea1c25b3e22","type":"inject","z":"56443195ea782ac2","name":"Upd value","props":[{"p":"id","v":"gauge-1","vt":"str"},{"p":"radius","v":"200","vt":"num"},{"p":"payload"},{"p":"_ui","v":"(\t  $strokeWidth := radius * 0.2;\t  $innerRadius := radius - $strokeWidth / 2;\t  $circumference := $innerRadius * 2 * 3.14;\t  $arc := $circumference * (270 / 360);\t  $percentNormalized := $min([$max([payload, 0]), 100]);\t  $offset := $arc - ($percentNormalized / 100) * $arc;\t  \t  [\t   {\t      \"method\": \"update\",\t      \"selector\": \"#\" & id & \" .gauge_pct\",\t      \"attributes\": {\t        \"stroke-dashoffset\": $offset\t      }\t   },\t   {\t      \"method\": \"update\",\t      \"selector\": \"#\" & id & \" text\",\t      \"slot\": payload\t   }\t  ]\t)","vt":"jsonata"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$round($random() * 100, 0)","payloadType":"jsonata","x":740,"y":960,"wires":[["278d2031ac38b84a"]],"info":"Based on this REACT example:\r\nhttps://www.fullstacklabs.co/blog/creating-an-svg-gauge-component-from-scratch"},{"id":"2939dc058b40b58c","type":"inject","z":"56443195ea782ac2","name":"svg gauge","props":[{"p":"topic","vt":"str"},{"p":"gaugeId","v":"gauge-1","vt":"str"},{"p":"radius","v":"200","vt":"num"},{"p":"min","v":"0","vt":"num"},{"p":"max","v":"100","vt":"num"},{"p":"payload"},{"p":"_ui","v":"(\t  $strokeWidth := radius * 0.2;\t  $innerRadius := radius - $strokeWidth / 2;\t  $circumference := $innerRadius * 2 * 3.14;\t  $arc := $circumference * (270 / 360);\t  $dashArray := $arc & \" \" & $circumference;\t  $transform := \"rotate(135, \" & radius & \", \" & radius & \")\";\t  $percentNormalized := $min( [ $max( [ payload, min ] ), max ] );\t  $offset := $arc - ($percentNormalized / 100) * $arc;\t\t[\t  {\t    \"method\": \"replace\",\t    \"components\": [\t      {\t        \"type\": \"svg\",\t        \"id\": gaugeId,\t        \"parent\": \"#more\",\t        \"position\": \"last\",\t        \"attributes\": {\t          \"xmlns\": \"http://www.w3.org/2000/svg\",\t          \"xmlns:xlink\": \"http://www.w3.org/1999/xlink\",\t          \"height\": radius * 2,\t          \"width\": radius * 2\t        },\t        \"components\": [\t          {\t            \"type\": \"title\",\t            \"slot\": \"Dynamic Gauge\"\t          },\t          {\t            \"type\": \"defs\",\t            \"components\": [\t              {\t                \"type\": \"style\",\t                \"slot\": \"text {fill: var(--text1);}\"\t              },\t              {\t                \"type\": \"circle\",\t                \"id\": gaugeId & \"-circ\",\t                \"attributes\": {\t                  \"cx\": radius,\t                  \"cy\": radius,\t                  \"fill\": \"transparent\",\t                  \"r\": $innerRadius,\t                  \"stroke-width\": $strokeWidth,\t                  \"stroke-dasharray\": $dashArray,\t                  \"transform\": $transform,\t                  \"stroke-linecap\": \"round\"\t                }\t              }\t            ]\t          },\t          {\t            \"type\": \"circle\",\t            \"attributes\": {\t              \"class\": \"gauge_base\",\t              \"stroke\": \"var(--surface5)\",\t                  \"cx\": radius,\t                  \"cy\": radius,\t                  \"fill\": \"transparent\",\t                  \"r\": $innerRadius,\t                  \"stroke-width\": $strokeWidth,\t                  \"stroke-dasharray\": $dashArray,\t                  \"transform\": $transform,\t                  \"stroke-linecap\": \"round\"\t            }\t          },\t          {\t            \"type\": \"circle\",\t            \"attributes\": {\t              \"class\": \"gauge_pct\",\t              \"stroke\": \"var(--primary-bg)\",\t              \"stroke-dashoffset\": $offset,\t              \"transition\": \"stroke-dasharray 0.5s;\",\t                  \"cx\": radius,\t                  \"cy\": radius,\t                  \"fill\": \"transparent\",\t                  \"r\": $innerRadius,\t                  \"stroke-width\": $strokeWidth,\t                  \"stroke-dasharray\": $dashArray,\t                  \"transform\": $transform,\t                  \"stroke-linecap\": \"round\"\t            }\t          },\t          {\t            \"type\": \"text\",\t            \"attributes\": {\t              \"x\": \"50%\",\t              \"y\": \"50%\",\t              \"text-anchor\": \"middle\"\t            },\t            \"slot\": payload\t          }\t        ]\t      }\t    ]\t  }\t]\t)","vt":"jsonata"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"svg-gauge","payload":"$round($random() * 100, 0)","payloadType":"jsonata","x":740,"y":920,"wires":[["278d2031ac38b84a"]],"info":"Based on this REACT example:\r\nhttps://www.fullstacklabs.co/blog/creating-an-svg-gauge-component-from-scratch"}]

One thing to note though - ideally, to create 2 circles - the orange "bar" is a circle the same size/shape as the underlying grey bar just cut off - you would use a template (which you will find in the inject). However, you reference the template with a <use> tag and that, like the svg tag itself, seems to need to be created differently. While I fixed the svg tag creation in the client library, looks like I will need to add a work-around for the use tag as well.

So I guess a small bug-fix release of uibuilder will be on its way soon. smcgann99 also spotted another issue where the built-in docs aren't working correctly so I'll be fixing that.

In the meantime though, this example works just fine even though it isn't quite as efficient as it should be.

image

One last version - I'm a bit bored with it now :slight_smile:

[{"id":"2939dc058b40b58c","type":"inject","z":"56443195ea782ac2","name":"svg gauge config/value","props":[{"p":"topic","vt":"str"},{"p":"config","v":"{\"id\":\"gauge-1\",\"min\":0,\"max\":100,\"text\":{\"caption\":\"My Gauge\",\"pre\":\"\",\"post\":\"%\"},\"visual\":{\"radius\":200,\"rotate\":135,\"end\":270,\"thick\":0.3,\"rounded\":false}}","vt":"json"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"svg-gauge","payload":"$round($random() * 100, 0)","payloadType":"jsonata","x":620,"y":920,"wires":[["39e3f54508dbf9d1"]]},{"id":"0796aae05135ba8c","type":"inject","z":"56443195ea782ac2","name":"180 gauge config/value","props":[{"p":"topic","vt":"str"},{"p":"config","v":"{\"id\":\"gauge-3\",\"min\":0,\"max\":100,\"text\":{\"caption\":\"180 deg gauge\",\"pre\":\"\",\"post\":\"%\"},\"visual\":{\"radius\":200,\"rotate\":180,\"end\":180,\"thick\":0.3,\"rounded\":false}}","vt":"json"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"svg-gauge","payload":"$round($random() * 100, 0)","payloadType":"jsonata","x":620,"y":960,"wires":[["39e3f54508dbf9d1"]]},{"id":"39e3f54508dbf9d1","type":"function","z":"56443195ea782ac2","name":"Output Gauge","func":"const conf = msg.config\n\nconst {sin, tan, max, min} = Math\n\nconst radius = conf.visual.radius ?? 200\n\nconf.calc = {\n  // Thickness of the bar\n  strokeWidth:  radius * conf.visual.thick,\n  // Make sure the value is within bounds\n  percentNormalized:  min( max(msg.payload, conf.min), conf.max),\n  // 1/2 the rotational gap in the bar ends in degrees\n  halfGap: (360 - conf.visual.end) / 2,\n  // Rotates the bars to the correct starting angle\n  transform:  `rotate(${conf.visual.rotate}, ${radius}, ${radius})`,\n}\n// inner radius\nconf.calc.innerRadius = radius - conf.calc.strokeWidth / 2,\n// circumference of the inner circle\nconf.calc.circumference = conf.calc.innerRadius * 2 * 3.14,\n// Length of the value bar arc\nconf.calc.arc = conf.calc.circumference * (conf.visual.end / 360),\n// Where does the value bar end?\nconf.calc.offset = conf.calc.arc - (conf.calc.percentNormalized / 100) * conf.calc.arc,\n// Length of the value bar arc and the value gap arc for stroke-dasharray\nconf.calc.dashArray = `${conf.calc.arc} ${conf.calc.circumference}`\n// Horizontal distance from centre-line to start of the bar gap (e.g. 1/2 the gap)\nconf.calc.xOffset = radius * sin(conf.calc.halfGap)\n// Vertical distance from top to start/end of the bar gaps\nconf.calc.yOffset = (radius * 2) - (radius - (conf.calc.xOffset * sin(90 - conf.calc.halfGap)))\n\nmsg._ui = [\n  {\n    \"method\": \"replace\",\n    \"components\": [\n      {\n        \"type\": \"svg\",\n        \"id\": conf.id,\n        \"parent\": conf.parent ?? 'body',\n        \"position\": conf.position ?? \"last\",\n        \"attributes\": {\n          \"xmlns\": \"http://www.w3.org/2000/svg\",\n          \"xmlns:xlink\": \"http://www.w3.org/1999/xlink\",\n          \"height\": conf.calc.yOffset + 25, // radius * 2,\n          \"width\": radius * 2,\n        },\n        \"components\": [\n          {\n            \"type\": \"title\",\n            \"slot\": conf.text.title ?? \"Dynamic Gauge\",\n          },\n          {\n            \"type\": \"defs\",\n            \"components\": [\n              {\n                \"type\": \"style\",\n                \"slot\": \"text {fill: var(--text1); font-size: larger;}\",\n              },\n              {\n                \"type\": \"circle\",\n                \"id\": conf.id + \"-circ\",\n                \"attributes\": {\n                  \"cx\": radius,\n                  \"cy\": radius,\n                  \"fill\": \"transparent\",\n                  \"r\": conf.calc.innerRadius,\n                  \"stroke-width\": conf.calc.strokeWidth,\n                  \"stroke-dasharray\": conf.calc.dashArray,\n                  \"transform\": conf.calc.transform,\n                  \"stroke-linecap\": conf.visual.rounded ? \"round\" : \"butt\",\n                },\n              },\n            ],\n          },\n          {\n            \"type\": \"circle\",\n            \"attributes\": {\n              \"class\": \"gauge_base\",\n              \"stroke\": \"var(--surface5)\",\n              \"cx\": radius,\n              \"cy\": radius,\n              \"fill\": \"transparent\",\n              \"r\": conf.calc.innerRadius,\n              \"stroke-width\": conf.calc.strokeWidth,\n              \"stroke-dasharray\": conf.calc.dashArray,\n              \"transform\": conf.calc.transform,\n              \"stroke-linecap\": conf.visual.rounded ? \"round\" : \"butt\",\n            },\n          },\n          {\n            \"type\": \"circle\",\n            \"attributes\": {\n              \"class\": \"gauge_pct\",\n              \"stroke\": \"var(--primary-bg)\",\n              \"stroke-dashoffset\": conf.calc.offset,\n              \"transition\": \"stroke-dasharray 0.5s;\",\n              \"cx\": radius,\n              \"cy\": radius,\n              \"fill\": \"transparent\",\n              \"r\": conf.calc.innerRadius,\n              \"stroke-width\": conf.calc.strokeWidth,\n              \"stroke-dasharray\": conf.calc.dashArray,\n              \"transform\": conf.calc.transform,\n              \"stroke-linecap\": conf.visual.rounded ? \"round\" : \"butt\",\n            },\n          },\n          {\n            \"type\": \"text\",\n            \"attributes\": {\n              \"x\": \"50%\",\n              \"y\": \"50%\",\n              \"text-anchor\": \"middle\",\n              \"dominant-baseline\": \"hanging\",\n            },\n            \"slot\": conf.text.caption,\n          },\n          {\n            \"type\": \"text\",\n            \"attributes\": {\n              \"x\": \"50%\",\n              \"y\": \"45%\",\n              \"text-anchor\": \"middle\",\n              \"dominant-baseline\": \"middle\",\n            },\n            \"slot\": `${conf.text.pre}${msg.payload}${conf.text.post}`,\n          },\n          {\n            \"type\": \"text\",\n            \"attributes\": {\n              \"x\": radius - conf.calc.xOffset,\n              \"y\": conf.calc.yOffset,\n              \"text-anchor\": \"middle\",\n              \"dominant-baseline\": \"hanging\",\n            },\n            \"slot\": `${conf.text.pre}${conf.min}${conf.text.post}`,\n          },\n          {\n            \"type\": \"text\",\n            \"attributes\": {\n              \"x\": radius + conf.calc.xOffset,\n              \"y\": conf.calc.yOffset,\n              \"text-anchor\": \"middle\",\n              \"dominant-baseline\": \"hanging\",\n            },\n            \"slot\": `${conf.text.pre}${conf.max}${conf.text.post}`,\n          },\n        ],\n      },\n    ],\n  },\n]\n\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":860,"y":920,"wires":[["65a49fda3ad54925"]],"info":"Based on this REACT example:\r\nhttps://www.fullstacklabs.co/blog/creating-an-svg-gauge-component-from-scratch\r\n\r\nBy default, arc starts in x-axis direction (90 deg).\r\n\r\nOriginal arc/thickness:\r\nrotate=135, endArc=270, Thick=0.2\r\n?? Should reduce height?\r\n\r\n180 version:\r\nrotate=180, endArc=180\r\nNeed to reduce height\r\n\r\n360 version:\r\nrotate=90, endArc=360\r\n\r\n----\r\n## To Do\r\n\r\n* BG, empty and filled colours\r\n"}]

Still not quite perfect but it has a lot more flexibility than the last version. Even the height is calculated dynamically now.

2 Likes