I wanted to do something in DB2, similar to MQTT explorer. I want to point to any messages (for now, I am just using my MQTT traffic), specify a topic and payload attribute and it will plot the values in a chart:
Here, the top is showing power readings from a Shelly 3EM where the payload is the value itself, and in the second chart it displays all values where the payload is an object and it has a brightness attribute.
So far it is working fine, and in the flow I just duplicated the nodes for the two
ui-text-inputs, ui-chart and a function node:
[{"id":"fe09dc7743685138","type":"ui-text-input","z":"9f85e288f238e9a9","group":"e65d837f5784300c","name":"Attirbute name 1","label":"Attirbute name","order":2,"width":0,"height":0,"topic":"__path","topicType":"str","mode":"text","tooltip":"Type the path of the attribute (within msg.payload), use * if the payload is the value","delay":300,"passthru":true,"sendOnDelay":true,"sendOnBlur":true,"sendOnEnter":true,"className":"compact-input","clearable":false,"sendOnClear":false,"icon":"","iconPosition":"left","iconInnerPosition":"inside","x":200,"y":480,"wires":[["9f9fb08b4762eb68"]]},{"id":"9f9fb08b4762eb68","type":"function","z":"9f85e288f238e9a9","name":"Extract data","func":"let path = context.get(\"path\");\nlet topic = context.get(\"topic\");\nlet count = context.get(\"count\") || 0;\n\n// incoming message is a new payload filter\nif (msg.topic === \"__path\") {\n path = msg.payload;\n if (path === \"\") {\n path = undefined;\n context.set(\"path\");\n } else {\n context.set(\"path\", path);\n }\n context.set(\"count\");\n return [{topic:\"reset\", payload: []}]; // reset the chart\n}\n\n// incoming message is a new topic filter\nif (msg.topic === \"__topic\") {\n topic = msg.payload;\n if (topic === \"\") {\n topic = undefined;\n context.set(\"topic\");\n } else {\n context.set(\"topic\", topic);\n }\n context.set(\"count\");\n return [{ topic: \"reset\", payload: [] }]; // reset the chart\n}\n\nif ((path === undefined) || (topic === undefined)) {\n node.status({ fill: \"grey\", shape: \"ring\", text: \"No filter specified\" });\n return;\n}\n\nif (count === 0) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"Waiting for message...\" });\n} else {\n node.status({ fill: \"green\", shape: \"ring\", text: count + \" messages\" });\n}\n\n/**\n * Helper to get nested property\n * @param {Object} obj - The object to search\n * @param {string} path - The path string (e.g. \"a.b.c\")\n */\nfunction getNestedValue(obj, path) {\n return path.split('.').reduce((acc, part) => {\n return (acc && acc[part] !== undefined) ? acc[part] : undefined;\n }, obj);\n}\n\nif (topic !== \"*\") {\n if (msg.topic !== topic) {\n // there is a topic filter, but it does not match, ignore\n return null;\n } \n}\nif (path === \"*\") {\n // path filter is *, payload is the value\n count++;\n context.set(\"count\", count);\n return msg;\n} else {\n // Safety Check: Ensure payload is an object before trying to traverse it\n if (typeof msg.payload !== 'object' || msg.payload === null) {\n return null; \n }\n\n // extract the value from the payload\n const extractedValue = getNestedValue(msg.payload, path);\n\n if (extractedValue !== undefined) {\n // Standard Dashboard 2.0 Chart format\n msg.payload = extractedValue;\n msg.topic = path; // Useful for identifying the line in the chart\n count++;\n context.set(\"count\", count);\n return msg;\n } else {\n return null; // Don't send anything to the chart if path is invalid\n }\n}\n \n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":480,"wires":[["d411130d3391173b"]]},{"id":"bac541c1ebec429c","type":"link in","z":"9f85e288f238e9a9","name":"link in 1","links":["683988880c70ac61"],"x":265,"y":440,"wires":[["9f9fb08b4762eb68"]]},{"id":"d411130d3391173b","type":"ui-chart","z":"9f85e288f238e9a9","group":"e65d837f5784300c","name":"Chart inspector 1","label":"","order":3,"chartType":"line","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"auto","xmin":"","xmax":"","yAxisLabel":"","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"","ymax":"","bins":10,"action":"append","stackSeries":false,"pointShape":"circle","pointRadius":4,"showLegend":false,"removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"100","colors":["#ff0000","#4400ff","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#666666"],"textColorDefault":true,"gridColor":["#e5e5e5"],"gridColorDefault":true,"width":"2","height":"2","className":"","interpolation":"linear","x":690,"y":480,"wires":[[]]},{"id":"76cbb0110ee62973","type":"ui-text-input","z":"9f85e288f238e9a9","group":"e65d837f5784300c","name":"Topic 1","label":"Topic","order":1,"width":0,"height":0,"topic":"__topic","topicType":"str","mode":"text","tooltip":"Type the topic name, or use * for any topic","delay":300,"passthru":true,"sendOnDelay":true,"sendOnBlur":true,"sendOnEnter":true,"className":"compact-input","clearable":false,"sendOnClear":false,"icon":"","iconPosition":"left","iconInnerPosition":"inside","x":240,"y":520,"wires":[["9f9fb08b4762eb68"]]},{"id":"5c859bf27ac155b9","type":"ui-template","z":"9f85e288f238e9a9","group":"","page":"19d5dd7a7b9f3134","ui":"","name":"Small text styles","order":0,"width":0,"height":0,"head":"","format":"/* 1. Target the outer container to reduce total footprint */\n.compact-input.nrdb-ui-text-field {\n height: 34px !important;\n min-height: 34px !important;\n margin-bottom: 2px !important;\n}\n\n/* 2. Shrink the actual border/background box (The Boundary) */\n.compact-input .v-field {\n --v-field-padding-start: 8px;\n --v-field-padding-end: 8px;\n height: 34px !important;\n min-height: 34px !important;\n border-radius: 4px !important; /* Optional: makes it look tighter */\n}\n\n/* 3. Align the text and control the internal height */\n.compact-input .v-field__input {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n min-height: 34px !important;\n height: 34px !important;\n line-height: 34px !important;\n font-size: 0.85rem !important;\n}\n\n/* 4. Force the label out of the way or shrink it */\n.compact-input .v-field-label {\n font-size: 0.75rem !important;\n top: 50% !important;\n transform: translateY(-50%) !important;\n}\n\n/* 5. Eliminate the 'details' slot (extra space at bottom) */\n.compact-input .v-input__details {\n display: none !important;\n grid-template-areas: none !important;\n padding: 0 !important;\n}","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"page:style","className":"","x":480,"y":400,"wires":[[]]},{"id":"e65d837f5784300c","type":"ui-group","name":"Chart Inspector","page":"19d5dd7a7b9f3134","width":"2","height":1,"order":4,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"19d5dd7a7b9f3134","type":"ui-page","name":"Message Inspector","ui":"cb79bc4520925e32","path":"/mi","icon":"home","layout":"grid","theme":"0d92c765bfad87e6","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":"cb79bc4520925e32","type":"ui-base","name":"My UI","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control","ui-text"],"showPathInSidebar":false,"headerContent":"page","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":5,"showDisconnectNotification":true,"allowInstall":true},{"id":"0d92c765bfad87e6","type":"ui-theme","name":"Basic Blue Theme","colors":{"surface":"#4d58ff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px","density":"default"}}]
But what is I want all this to be dynamic, having a plus button to add a set of text-inputs and charts and set values, delete a set, etc.
I know it would be possible to do this in a ui-template by dynamically create chart.js objects, but that is really beyond my capabilities. Of course I tried Gemini to sort this out, and it almost got there, but the charts were not really working, and no matter how much I fed back the errors it starting going tangent and never fixed the issue.
Ignore the colors and the visuals for the time being, but this is what Gemini coocked up so far:
I can create chart sets, specify the topic and payload filter, it is able to handle that, get the data selected (you can see the correct numbers above the chart), but the value is not being displayed. And I see various errors in F12 which seems to suggest that either created the chart objects or accessing them is the issue. I am hoping somebody is willing to look at this, because it looks very close to a working solution:
[{"id":"baeb7b43db249d9a","type":"comment","z":"9f85e288f238e9a9","name":"Dynamic chart","info":"","x":110,"y":1260,"wires":[]},{"id":"1b6d9c2d1098f072","type":"link in","z":"9f85e288f238e9a9","name":"link in 5","links":["683988880c70ac61"],"x":175,"y":1340,"wires":[["5d9a2e652a2cccb4"]]},{"id":"5d9a2e652a2cccb4","type":"function","z":"9f85e288f238e9a9","name":"Processing","func":"let inspectors = context.get(\"inspectors\") || [];\n\n// 1. HANDLE UI UPDATES\nif (msg.topic === \"update_filters\") {\n inspectors = msg.payload;\n context.set(\"inspectors\", inspectors);\n return null;\n}\n\n// 2. PROCESS INCOMING DATA\nif (inspectors.length === 0) return null;\n\nfunction getNestedValue(obj, path) {\n if (!path || path === \"*\") return obj;\n return path.split('.').reduce((acc, part) => {\n return (acc && acc[part] !== undefined) ? acc[part] : undefined;\n }, obj);\n}\n\nconst outputs = [];\n\ninspectors.forEach((ins) => {\n // NEW RULE: Skip if both filters are still wildcards to prevent flooding\n if (ins.topic === \"*\" && ins.path === \"*\") return;\n\n // Topic Filter\n if (ins.topic !== \"*\" && msg.topic !== ins.topic) return;\n\n let val;\n if (ins.path === \"*\") {\n val = msg.payload;\n } else {\n if (typeof msg.payload !== 'object' || msg.payload === null) return;\n val = getNestedValue(msg.payload, ins.path);\n }\n\n // Only send numeric or simple string data to the UI to keep it light\n if (val !== undefined && (typeof val === 'number' || typeof val === 'string')) {\n outputs.push({\n payload: val,\n topic: `inspector_${ins.id}`,\n inspectorId: ins.id\n });\n }\n});\n\nreturn outputs.length > 0 ? { payload: outputs } : null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":1340,"wires":[["4bb31d5e615dc827","b5183b12600d8b98"]]},{"id":"4bb31d5e615dc827","type":"ui-template","z":"9f85e288f238e9a9","group":"8bafe82e5d9f07ba","page":"","ui":"","name":"","order":1,"width":0,"height":0,"head":"","format":"<template>\n <div class=\"inspector-app\">\n <div class=\"header\">\n <span class=\"title\">Visual Inspectors</span>\n <!-- Button is disabled until Chart.js is ready -->\n <v-btn icon=\"mdi-plus\" :color=\"libReady ? 'green' : 'grey'\" @click=\"addInspector\" :disabled=\"!libReady\"\n size=\"x-small\"></v-btn>\n </div>\n\n <div v-for=\"(ins, index) in inspectors\" :key=\"ins.id\" class=\"inspector-card\">\n <div class=\"inputs\">\n <v-text-field v-model=\"ins.topic\" label=\"Topic\" density=\"compact\" @change=\"sync\" hide-details\n class=\"small-input\"></v-text-field>\n <v-text-field v-model=\"ins.path\" label=\"Path\" density=\"compact\" @change=\"sync\" hide-details\n class=\"small-input\"></v-text-field>\n <v-btn icon=\"mdi-close\" variant=\"text\" color=\"grey\" @click=\"removeInspector(index)\" size=\"x-small\">\n </v-btn>\n </div>\n\n <div class=\"stats-row\">\n <span class=\"current-val\">{{ currentValues[ins.id] ?? '...' }}</span>\n </div>\n\n <div class=\"chart-container\">\n <canvas :id=\"'chart-' + ins.id\"></canvas>\n </div>\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data() {\n return {\n inspectors: [],\n currentValues: {},\n charts: {},\n libReady: false\n }\n },\n mounted() {\n this.loadChartJs();\n },\n methods: {\n loadChartJs() {\n if (window.Chart) {\n this.libReady = true;\n return;\n }\n const script = document.createElement('script');\n script.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\n script.async = true;\n script.onload = () => { this.libReady = true; };\n document.head.appendChild(script);\n },\n addInspector() {\n const id = Date.now();\n this.inspectors.push({ id: id, topic: '', path: '' });\n this.currentValues[id] = 'Waiting...';\n this.sync();\n \n // Wait for DOM to create the canvas element\n this.$nextTick(() => {\n this.initChart(id);\n });\n },\n initChart(id) {\n const canvas = document.getElementById('chart-' + id);\n if (!canvas || !window.Chart) return;\n\n const ctx = canvas.getContext('2d');\n this.charts[id] = new Chart(ctx, {\n type: 'line',\n data: {\n labels: Array(100).fill(''),\n datasets: [{\n data: [],\n borderColor: '#ff0000',\n borderWidth: 2,\n pointRadius: 2,\n tension: 0.1,\n fill: false\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n animation: false,\n scales: {\n x: { display: false },\n y: { grid: { color: '#333' }, ticks: { color: '#888', font: { size: 10 } } }\n },\n plugins: { legend: { display: false } }\n }\n });\n },\n removeInspector(index) {\n const id = this.inspectors[index].id;\n if (this.charts[id]) {\n this.charts[id].destroy();\n delete this.charts[id];\n }\n this.inspectors.splice(index, 1);\n delete this.currentValues[id];\n this.sync();\n },\n sync() {\n this.send({ topic: 'update_filters', payload: JSON.parse(JSON.stringify(this.inspectors)) });\n }\n },\n watch: {\n msg: {\n handler(newMsg) {\n if (newMsg?.payload && Array.isArray(newMsg.payload)) {\n newMsg.payload.forEach(item => {\n const id = item.inspectorId;\n this.currentValues[id] = item.payload;\n const chart = this.charts[id];\n if (chart) {\n const data = chart.data.datasets[0].data;\n data.push(item.payload);\n if (data.length > 100) data.shift();\n chart.update('none');\n }\n });\n }\n },\n deep: true\n }\n }\n}\n</script>\n\n<style>\n /* ... (Style section remains the same as previous) ... */\n .inspector-app {\n color: white;\n min-width: 300px;\n }\n\n .header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 12px;\n }\n\n .inspector-card {\n background: #1a1a1a;\n border: 1px solid #333;\n padding: 10px;\n margin-bottom: 12px;\n border-radius: 4px;\n }\n\n .inputs {\n display: flex;\n gap: 8px;\n margin-bottom: 8px;\n }\n\n .small-input .v-field__input {\n font-size: 0.8rem !important;\n }\n\n .chart-container {\n height: 100px;\n background: #000;\n }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":540,"y":1340,"wires":[["5d9a2e652a2cccb4","cec7aeaaf7123591"]]},{"id":"b5183b12600d8b98","type":"debug","z":"9f85e288f238e9a9","name":"debug 38","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":570,"y":1280,"wires":[]},{"id":"cec7aeaaf7123591","type":"debug","z":"9f85e288f238e9a9","name":"debug 39","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":750,"y":1340,"wires":[]},{"id":"8bafe82e5d9f07ba","type":"ui-group","name":"Dynamic chart","page":"19d5dd7a7b9f3134","width":"4","height":1,"order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"19d5dd7a7b9f3134","type":"ui-page","name":"Message Inspector","ui":"cb79bc4520925e32","path":"/mi","icon":"home","layout":"grid","theme":"0d92c765bfad87e6","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":"cb79bc4520925e32","type":"ui-base","name":"My UI","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control","ui-text"],"showPathInSidebar":false,"headerContent":"page","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":5,"showDisconnectNotification":true,"allowInstall":true},{"id":"0d92c765bfad87e6","type":"ui-theme","name":"Basic Blue Theme","colors":{"surface":"#4d58ff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px","density":"default"}}]

