As the current dashboard-2 chart node lacks many formatting options, I'm trying to use a dashboard template node to create a line chart, and thereby use other chart.js options to make my charts display better.
I don't know how far I'll get with this , but I've had great help from hotnipi to establish a basic format.
First task was to try and ensure that the chart data wasn't lost upon a browser refresh, and I'm almost there, however hit a snag...
I'm currently passing data into the template, but also into a function node where the datapoints are added to an array and saved to context. The plan is if a ui-event
node detects a browser connect, then it triggers the function node to feed the array into the template node, restoring the chart data, all good so far...
However I've hit a problem where when the next datapoint arrives, it clears the chart again
I've attached the flow, along with fake data feeds, and the problem can be seen by letting the flow firstly store a number of data points, then F5 refresh the browser (which clears the chart), and inject the 'Simulate browser connect' node, which restores the chart. But when the next datapoint arrives is is all wiped again!
Any suggestions please.
[{"id":"ea7c02fa77fe6efc","type":"ui-template","z":"1326aadbacf36704","group":"2c34b9c7d6324531","page":"","ui":"","name":"Custom Line Chart","order":12,"width":"6","height":"6","head":"","format":"<template>\n <div class=\"base-container\">\n <div class=\"chart-container\" ><canvas ref=\"chart\" /></div>\n </div> \n</template>\n\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n\n<script>\n export default {\n data() { \n return {\n keep:1000000, // keep data for 1000 seconds \n }\n },\n \n mounted() {\n this.$socket.on('msg-input:' + this.id, this.onInput)\n\n // code here when the component is first loaded\n let interval = setInterval(() => {\n if (window.Chart) {\n // the chart.js is loaded, so we can now use it\n clearInterval(interval);\n this.draw()\n }\n }, 100);\n },\n methods: {\n draw () {\n const ctx = this.$refs.chart\n const datasets = []\n \n // Render the chart\n const chart = new Chart(ctx, {\n \n type: 'line',\n data: {\n datasets: [\n {label: \"dataA\", dataA:[]},\n {label: \"dataB\", dataB:[]}\n ]\n },\n options: {\n maintainAspectRatio: false,//let the chart take the shape of the container\n animation: false,// do not animate\n responsive: true,// and please be responisve\n scales: {\n x: {\n type: 'time',//x-axis is configured to be time\n time: {\n unit: 'second',//time resolution second\n displayFormats: {\n second: 'HH:mm:ss' // render ticks in that format\n }\n },\n ticks: {\n stepSize: 1, // by default render gridlaine and tick for defined step (1 here means every second)\n autoSkip: false, // turn off automatic tick count calculations\n maxTicksLimit:10, //limit total ticks amount (overrides setpSize obviously)\n maxRotation:0 //do not allow ticks to be rotated\n }\n }\n },\n elements:{\n line:{\n borderWidth:1,//line thickness\n tension:0.3 //line curvature \n },\n point:{\n pointRadius:0 //remove points (any positive number makes points at defined size)\n }\n },\n parsing: {\n xAxisKey: 'time',\n yAxisKey: 'value'\n },\n plugins: {\n legend: {\n position: 'top',\n },\n title: {\n display: true,\n text: 'Chart.js Line Chart. Basic Options'\n }\n } \n },\n });\n // make this available to all elements of the component\n this.chart = chart\n console.log(this.data)\n },\n clearOldData(){\n this.chart.data.datasets.forEach(dataset => {\n dataset.data = dataset.data.filter(point => point.time > Date.now() - this.keep)\n })\n },\n clearChart(){\n this.chart.data.datasets.forEach(dataset => {\n dataset.data = []\n })\n this.chart.update()\n },\n\n addDataPoint(topic,valueOrFulldata){\n let dataset = this.chart.data.datasets.find(ds => ds.label == topic)\n if(typeof valueOrFulldata === \"number\"){\n dataset.data.push({\n time: (new Date()).getTime(),\n value: valueOrFulldata\n })\n } \n else{\n dataset.data.push({\n time: valueOrFulldata.time,\n value: valueOrFulldata.value\n })\n } \n \n this.chart.update()\n },\n onInput (msg) {\n if(Array.isArray(msg.payload)){\n if(msg.payload.length == 0){\n this.clearChart()\n return\n }\n else{\n // historycal data. for each item in array add the data to proper dataset\n // the data has format [{topic:\"first\",time:123456789,value:120}, and many more of such ...]\n msg.payload.forEach(point => {\n this.addDataPoint(point.topic,point)\n }) \n }\n }\n else{\n this.clearOldData()\n this.addDataPoint(msg.topic,msg.payload)\n }\n }\n }\n }\n</script>\n<style>\n.base-container{\n width:100%;\n height:100%;\n display: flex;\n justify-content: center;\n align-items: center;\n container: chat / size;/*make this container available for container querys*/\n}\n.chart-container{\n position: relative;\n margin: auto;\n height: 100cqb;/*use container query units to give full height of the container (100% of container height) */\n width: 100cqi;/*use container query units to give full width of the container (100% of container height)*/\n display: flex;\n justify-content: center;\n align-items: center;\n}\n \n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":810,"y":1260,"wires":[[]]},{"id":"e12c2fa23f25087e","type":"inject","z":"1326aadbacf36704","name":"clear chart","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[]","payloadType":"json","x":620,"y":1220,"wires":[["ea7c02fa77fe6efc"]]},{"id":"9b20d06596172275","type":"function","z":"1326aadbacf36704","name":"store data points","func":"// Simulate a 'browser connect' message from the UI-event node\nif (msg.payload !== 'redraw') {\n let data = flow.get('chartData')\n \n if (!Array.isArray(data)) {\n data = []\n }\n \n // add latest data points to data array\n const newdata = {\n topic: msg.topic,\n time: msg.time,\n value: msg.payload\n }\n data.push(newdata)\n\n // Limit the size of the data array\n while (data.length > 50) {\n data.shift()\n }\n\n flow.set('chartData', data)\n} else {\n // Clear existing chart and send the complete array\n let data = flow.get('chartData')\n node.send({ payload: [] })\n node.send({ payload: data })\n}\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":610,"y":1320,"wires":[["ea7c02fa77fe6efc"]]},{"id":"c5964592afa50a4c","type":"inject","z":"1326aadbacf36704","name":"Simulate browser connect","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"redraw","payloadType":"str","x":190,"y":1320,"wires":[["9b20d06596172275"]]},{"id":"257ec1778e051d96","type":"inject","z":"1326aadbacf36704","name":"Every 15 seconds","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"15","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":170,"y":1260,"wires":[["c5ab23508895112e"]]},{"id":"c5ab23508895112e","type":"function","z":"1326aadbacf36704","name":"Create dummy data!","func":"const date = new Date()\nconst time = date.toISOString().replace(/\\.\\d{3}Z$/, '.5Z')\n\nconst dataA = {\n payload: Math.floor(Math.random() * 10) + 1,\n topic: 'dataA',\n time: time,\n }\n\nconst dataB = {\n payload: Math.floor(Math.random() * 5) + 2,\n topic: 'dataB',\n time: time\n}\n\nreturn [ [ dataA, dataB ] ]\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":1260,"wires":[["ea7c02fa77fe6efc","9b20d06596172275"]]},{"id":"2c34b9c7d6324531","type":"ui-group","name":"Energy","page":"7294756f31e17b81","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"7294756f31e17b81","type":"ui-page","name":"Home","ui":"ae3d4aeb3f977a90","path":"/home","icon":"home","layout":"tabs","theme":"52ba8a01d6eda628","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":"ae3d4aeb3f977a90","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"temporary","titleBarStyle":"default"},{"id":"52ba8a01d6eda628","type":"ui-theme","name":"Mobile","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"compact","pagePadding":"5px","groupGap":"5px","groupBorderRadius":"10px","widgetGap":"5px"}}]