I wanted to create a chart which has both a bar and a line series. I got it working, with Gemini's help:
But since I am a boomer I don't immediately trust AI and wanted to ask the experts here.
So there is a very good video from Yaser in YT https://www.youtube.com/watch?v=KmVCIY4cpgg and he had an example how to create a chart in ui-tempate. When I tried it did not work, although it is clearly working in the video, therefore something must have changed in NR or in DB2.
Gemini was able to fix this saying " In Dashboard 2.0, the Chart object isn't attached to the global window object by default to avoid library version conflicts. To use Chart.js inside a ui-template, you need to explicitly load the script within your template."
Above all, it generated this function in the mounted section:
mounted() {
// 1. Manually load Chart.js
if (!window.Chart) {
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.onload = () => {
this.scriptLoaded = true;
// If data arrived before the script loaded, render it now
if (this.msg?.payload) {
this.renderChart(this.msg.payload);
}
};
document.head.appendChild(script);
} else {
this.scriptLoaded = true;
}
},
Here is a small flow sample for completeness:
[{"id":"e0dc352d2b367475","type":"inject","z":"9d6045f175be2d26","name":"Sample","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"auto_calibration_status\":\"standby\",\"detection_range\":0,\"energy_streaming\":true,\"illuminance\":294,\"led_indicator\":true,\"linkquality\":153,\"occupancy\":false,\"presence_clear_cooldown\":15,\"presence_state\":\"absence\",\"sensitivity_preset\":\"medium\",\"zone_10_active\":true,\"zone_10_motion_energy\":3,\"zone_10_motion_threshold\":5,\"zone_10_presence_energy\":3,\"zone_10_presence_threshold\":4,\"zone_1_active\":true,\"zone_1_motion_energy\":6,\"zone_1_motion_threshold\":14,\"zone_1_presence_energy\":7,\"zone_1_presence_threshold\":15,\"zone_2_active\":true,\"zone_2_motion_energy\":4,\"zone_2_motion_threshold\":11,\"zone_2_presence_energy\":4,\"zone_2_presence_threshold\":12,\"zone_3_active\":true,\"zone_3_motion_energy\":2,\"zone_3_motion_threshold\":9,\"zone_3_presence_energy\":1,\"zone_3_presence_threshold\":10,\"zone_4_active\":true,\"zone_4_motion_energy\":1,\"zone_4_motion_threshold\":6,\"zone_4_presence_energy\":1,\"zone_4_presence_threshold\":6,\"zone_5_active\":true,\"zone_5_motion_energy\":2,\"zone_5_motion_threshold\":6,\"zone_5_presence_energy\":2,\"zone_5_presence_threshold\":6,\"zone_6_active\":true,\"zone_6_motion_energy\":2,\"zone_6_motion_threshold\":5,\"zone_6_presence_energy\":2,\"zone_6_presence_threshold\":4,\"zone_7_active\":true,\"zone_7_motion_energy\":2,\"zone_7_motion_threshold\":5,\"zone_7_presence_energy\":3,\"zone_7_presence_threshold\":4,\"zone_8_active\":true,\"zone_8_motion_energy\":2,\"zone_8_motion_threshold\":5,\"zone_8_presence_energy\":3,\"zone_8_presence_threshold\":4,\"zone_9_active\":true,\"zone_9_motion_energy\":2,\"zone_9_motion_threshold\":5,\"zone_9_presence_energy\":3,\"zone_9_presence_threshold\":4}","payloadType":"json","x":270,"y":2380,"wires":[["a0f53de29453daea"]]},{"id":"5e938ec24a8430c7","type":"function","z":"9d6045f175be2d26","name":"Convert Data for Chart","func":"const motion = [];\nconst presence = [];\n\nfor (let i = 1; i <= 10; i++) {\n\n motion.push({ \n \"range\": (i - 1) * 0.5 + \"m\",\n \"energy\": msg.payload[\"zone_\" + i + \"_motion_energy\"],\n \"threshold\": msg.payload[\"zone_\" + i +\"_motion_threshold\"]\n });\n\n presence.push({\n \"range\": (i - 1) * 0.5 + \"m\",\n \"energy\": msg.payload[\"zone_\" + i + \"_presence_energy\"],\n \"threshold\": msg.payload[\"zone_\" + i + \"_presence_threshold\"]\n });\n\n}\n\nmsg.payload = {\"motion\": motion, \"presence\": presence};\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":2480,"wires":[["0e7766d2ac4e7f2a","3172769fa990f755"]]},{"id":"0e7766d2ac4e7f2a","type":"change","z":"9d6045f175be2d26","name":"Get motion","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.motion","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":2460,"wires":[["f9f30ca98bce76c3"]]},{"id":"3172769fa990f755","type":"change","z":"9d6045f175be2d26","name":"Get presence","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.presence","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":840,"y":2520,"wires":[["956f4cc885a664d6"]]},{"id":"f9f30ca98bce76c3","type":"ui-template","z":"9d6045f175be2d26","group":"a87dc2d2e84fee78","page":"","ui":"","name":"Motion","order":1,"width":0,"height":0,"head":"","format":"<template>\n <div class=\"chart-holder\">\n <canvas id=\"myMotionChart\"></canvas>\n </div>\n</template>\n\n<script>\nexport default {\n data() {\n return {\n chart: null,\n scriptLoaded: false\n }\n },\n mounted() {\n // 1. Manually load Chart.js\n if (!window.Chart) {\n const script = document.createElement('script');\n script.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\n script.onload = () => {\n this.scriptLoaded = true;\n // If data arrived before the script loaded, render it now\n if (this.msg?.payload) {\n this.renderChart(this.msg.payload);\n }\n };\n document.head.appendChild(script);\n } else {\n this.scriptLoaded = true;\n }\n },\n watch: {\n msg: {\n handler(val) {\n if (val?.payload && this.scriptLoaded) {\n this.renderChart(val.payload);\n }\n },\n immediate: true\n }\n },\n methods: {\n renderChart(data) {\n if (!Array.isArray(data)) return;\n\n const labels = data.map(d => d.range);\n const energies = data.map(d => d.energy);\n const thresholds = data.map(d => d.threshold);\n\n // Grab the canvas via ID for reliability in this scope\n const canvas = document.getElementById('myMotionChart');\n if (!canvas) return;\n \n const ctx = canvas.getContext('2d');\n\n if (this.chart) {\n this.chart.destroy();\n }\n\n this.chart = new window.Chart(ctx, {\n type: 'bar',\n data: {\n labels: labels,\n datasets: [\n {\n label: 'Energy',\n data: energies,\n backgroundColor: 'rgba(54, 162, 235, 0.6)',\n borderColor: 'rgba(54, 162, 235, 1)',\n borderWidth: 1,\n order: 2 // Bars in background\n },\n {\n label: 'Threshold',\n type: 'line',\n data: thresholds,\n borderColor: 'rgba(255, 99, 132, 1)',\n showLine: false, // <--- This removes the connecting line\n pointStyle: 'line', // <--- This changes the dot to a horizontal dash\n pointRadius: 10, // <--- Makes the dash wider\n pointBorderWidth: 3, // <--- Makes the dash thicker\n order: 1 // Line in foreground\n }\n ]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n scales: {\n y: { beginAtZero: true }\n }\n }\n });\n }\n },\n unmounted() {\n if (this.chart) {\n this.chart.destroy();\n }\n }\n};\n</script>\n\n<style>\n.chart-container {\n position: relative;\n height: 400px;\n}\n</style>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1030,"y":2460,"wires":[[]]},{"id":"956f4cc885a664d6","type":"ui-template","z":"9d6045f175be2d26","group":"9421109df037ba34","page":"","ui":"","name":"Presence","order":1,"width":0,"height":0,"head":"","format":"<template>\n <div class=\"chart-holder\">\n <canvas id=\"myPresenceChart\"></canvas>\n </div>\n</template>\n\n<script>\nexport default {\n data() {\n return {\n chart: null,\n scriptLoaded: false\n }\n },\n mounted() {\n // 1. Manually load Chart.js\n if (!window.Chart) {\n const script = document.createElement('script');\n script.src = \"https://cdn.jsdelivr.net/npm/chart.js\";\n script.onload = () => {\n this.scriptLoaded = true;\n // If data arrived before the script loaded, render it now\n if (this.msg?.payload) {\n this.renderChart(this.msg.payload);\n }\n };\n document.head.appendChild(script);\n } else {\n this.scriptLoaded = true;\n }\n },\n watch: {\n msg: {\n handler(val) {\n if (val?.payload && this.scriptLoaded) {\n this.renderChart(val.payload);\n }\n },\n immediate: true\n }\n },\n methods: {\n renderChart(data) {\n if (!Array.isArray(data)) return;\n\n const labels = data.map(d => d.range);\n const energies = data.map(d => d.energy);\n const thresholds = data.map(d => d.threshold);\n\n // Grab the canvas via ID for reliability in this scope\n const canvas = document.getElementById('myPresenceChart');\n if (!canvas) return;\n \n const ctx = canvas.getContext('2d');\n\n if (this.chart) {\n this.chart.destroy();\n }\n\n this.chart = new window.Chart(ctx, {\n type: 'bar',\n data: {\n labels: labels,\n datasets: [\n {\n label: 'Energy',\n data: energies,\n backgroundColor: 'rgba(54, 162, 235, 0.6)',\n borderColor: 'rgba(54, 162, 235, 1)',\n borderWidth: 1,\n order: 2 // Bars in background\n },\n {\n label: 'Threshold',\n type: 'line',\n data: thresholds,\n borderColor: 'rgba(255, 99, 132, 1)',\n showLine: false, // <--- This removes the connecting line\n pointStyle: 'line', // <--- This changes the dot to a horizontal dash\n pointRadius: 10, // <--- Makes the dash wider\n pointBorderWidth: 3, // <--- Makes the dash thicker\n order: 1 // Line in foreground\n }\n ]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n scales: {\n y: { beginAtZero: true }\n }\n }\n });\n }\n },\n unmounted() {\n if (this.chart) {\n this.chart.destroy();\n }\n }\n};\n</script>\n\n<style>\n.chart-container {\n position: relative;\n height: 400px;\n}\n</style>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1040,"y":2520,"wires":[[]]},{"id":"a87dc2d2e84fee78","type":"ui-group","name":"Motion Energies","page":"865c8ac82222ced6","width":6,"height":1,"order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"9421109df037ba34","type":"ui-group","name":"Presence Energies","page":"865c8ac82222ced6","width":6,"height":1,"order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"865c8ac82222ced6","type":"ui-page","name":"Zemismart ZPS-Z1","ui":"cb79bc4520925e32","path":"/zps-z1","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},{"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"}}]
This flow processes and visualizes the data from a mmWave radar presence sensor and shows the energy reflections in distance ranges in the bar graph and the lines are the threshold values for reporting occupancy.
Would you say that this approach is safe/good to be used?
