SteppedLine Chart together with Line Chart

I can't see any other options. If there is no browser session, the data must be still be available for next session. But after the dashboard connected and chunk of data feed to the chart, it is reasonable to update again as live chart, adding every single data-point. Heavy chunks of data over socket just don't do any good so you can keep it low that way.
According to this example flow, it misses the option of updating with chunk of data, but it is not too complicated thing to add.
Also the limitation by count should be changed to filter out data older than you'll need.

1 Like

I've built the chunk of data in a function node and saved it to context (keeping the arrays to a preset length).
On a browser connect, the context 'chunk data' is injected into the template node using the msg.topic - "restore".
In the template node, I am distinguishing the 'live data' from the 'chunk data' .

(function(scope) {
  scope.$watch('msg', function(msg) {
    //check if msg is 'live' or 'restore'
    if (msg.topic=="restore") {
      //do something to restore data
    } else if (msg) {
      // update live data
      addData(chart,{x:new Date(),y:msg.payload.first},"first")
      addData(chart,{x:new Date(),y:msg.payload.second},"second")
    }
  });

...and there I have come to a halt, because although the 'context chunk data' is formatted as per your earlier example, I don't know how to use it in the template.
Any ideas please?

linechart

[{"id":"8496abf.bdd4b58","type":"ui_template","z":"c4396940.2c4cc8","group":"6d56c1dd.99aa2","name":"Line Chart","order":4,"width":"15","height":"10","format":"","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":570,"y":1000,"wires":[[]]},{"id":"e91eab68.64fd68","type":"template","z":"c4396940.2c4cc8","name":"","field":"template","fieldType":"msg","format":"html","syntax":"mustache","template":"<canvas id=\"myChart\" width=600 height =300></canvas>\n<script>\nvar textcolor = getComputedStyle(document.documentElement).getPropertyValue('--nr-dashboard-widgetTextColor');\nvar gridcolor = getComputedStyle(document.documentElement).getPropertyValue('--nr-dashboard-groupBorderColor');\nvar linecolors = ['#5d60ff','#ff5d5d']\nvar maxDataPoints = 20\n\nvar ctx = document.getElementById('myChart').getContext('2d');\nvar chart = new Chart(ctx, {\n    // The type of chart we want to create\n    type: 'line',\n\n    // The data for our dataset\n    data: {\n        labels: [],\n        datasets: [\n            {\n                label: 'first',\n                backgroundColor: linecolors[0],\n                borderColor: linecolors[0],\n                data: [],\n                yAxisID: 'left-y-axis',\n                steppedLine: false,\n                fill: false,\n                borderWidth: 1\n            },\n            {\n                label: 'second',\n                backgroundColor: linecolors[1],\n                borderColor: linecolors[1],\n                data: [],\n                yAxisID: 'right-y-axis',\n                steppedLine: false,\n                fill: true,\n                borderWidth: 1\n            }\n        ]\n    },\n\n    // Configuration options go here\n    options: {\n        scales: {\n            yAxes: [\n                {\n                    gridLines :{display:false},\n                    id: 'left-y-axis',\n                    type: 'linear',\n                    position: 'left',\n                    ticks: {\n                        fontColor: linecolors[0]\n                    }\n                },\n                {\n                    gridLines :{zeroLineColor:gridcolor,color:gridcolor,lineWidth:0.5},\n                    id: 'right-y-axis',\n                    type: 'linear',\n                    position: 'right',\n                    ticks: {\n                        fontColor:linecolors[1]\n                    }\n                }\n            ],\n            xAxes: [\n                {\n                    gridLines :{zeroLineColor:gridcolor,color:gridcolor,lineWidth:0.5},\n                    type: 'time',\n                    distribution: 'series',\n                    time:{\n                        displayFormats: {\n                            quarter: 'MMM YYYY',\n                            millisecond:'h:mm:ss',\n                            second:\t'h:mm:ss',\n                            minute:\t'h:mm',\n                            hour:\t'h'                        \n                        }\n                    },\n                    \n                    ticks: {\n                        fontColor:textcolor\n                    }\n                }\n            ]\n        }\n    }\n});\nfunction addData(chart, data, label) {\n    chart.data.datasets.forEach((dataset) => {\n        if(dataset.label == label){\n            dataset.data.push(data);\n        }\n        if(dataset.data.length > maxDataPoints){\n            dataset.data.shift()\n        }\n    });\n    chart.update(0);//0 means no animation\n}\n(function(scope) {\n  scope.$watch('msg', function(msg) {\n    //check if msg is 'live' or 'restore'\n    if (msg.topic==\"restore\") {\n      //do something to restore data\n    } else if (msg) {\n      // update live data\n      addData(chart,{x:new Date(),y:msg.payload.first},\"first\")\n      addData(chart,{x:new Date(),y:msg.payload.second},\"second\")\n    }\n  });\n})(scope);\n</script>\n","output":"str","x":380,"y":960,"wires":[["8496abf.bdd4b58"]]},{"id":"6d48838f.c861ac","type":"inject","z":"c4396940.2c4cc8","name":"Initialize","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":960,"wires":[["e91eab68.64fd68"]]},{"id":"15644e1e.7c2ab2","type":"inject","z":"c4396940.2c4cc8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":false,"onceDelay":"0.62","topic":"","payload":"","payloadType":"date","x":115,"y":1040,"wires":[["c3575dfd.f6964"]],"l":false},{"id":"c3575dfd.f6964","type":"function","z":"c4396940.2c4cc8","name":"fake live data","func":"msg.payload = {}\nmsg.payload.first = Math.round(Math.random() * 100)\nmsg.payload.second = Math.round(Math.random() * 100)\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":230,"y":1040,"wires":[["2ce6466a.38212a"]]},{"id":"2ce6466a.38212a","type":"function","z":"c4396940.2c4cc8","name":"Config","func":"var data1 = msg.payload.first\nvar data2 = msg.payload.second\nvar maxData = 20\n\nvar m_first = flow.get(\"chartdata.first\")||[]\nvar m_second = flow.get(\"chartdata.second\")||[]\n\nvar a\nvar timeStamp = new Date()\n\n    a = {x: timeStamp, y: data1}\n    m_first.push(a)\n    if (m_first.length > maxData){m_first.shift()}\n\n    a = {x: timeStamp, y: data2}\n    m_second.push(a)\n    if (m_second.length > maxData){m_second.shift()}\n\nlet savedD = {}\nsavedD.first = (m_first)\nsavedD.second = (m_second)\nflow.set(\"chartdata\", savedD);\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":390,"y":1040,"wires":[["8496abf.bdd4b58"]]},{"id":"68f0956f.f388ec","type":"change","z":"c4396940.2c4cc8","name":"Restore data","rules":[{"t":"set","p":"chartdata","pt":"msg","to":"chartdata","tot":"flow"},{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"payload.first","pt":"msg","to":"$string(chartdata.first)","tot":"jsonata"},{"t":"set","p":"payload.second","pt":"msg","to":"$string(chartdata.second)","tot":"jsonata"},{"t":"set","p":"topic","pt":"msg","to":"restore","tot":"str"},{"t":"delete","p":"chartdata","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":1000,"wires":[["8496abf.bdd4b58"]]},{"id":"63e9f633.a79e08","type":"ui_ui_control","z":"c4396940.2c4cc8","name":"","events":"connect","x":140,"y":1000,"wires":[["68f0956f.f388ec"]]},{"id":"6d56c1dd.99aa2","type":"ui_group","z":"","name":"Chart","tab":"b7b17e22.6df42","order":3,"disp":true,"width":"15","collapse":false},{"id":"b7b17e22.6df42","type":"ui_tab","z":"","name":"Ovens","icon":"dashboard","order":1,"disabled":false,"hidden":false}]

Made a little changes (with comments inside)

[{"id":"8496abf.bdd4b58","type":"ui_template","z":"63163740.2d4508","group":"6d56c1dd.99aa2","name":"Line Chart","order":4,"width":"15","height":"10","format":"","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":710,"y":280,"wires":[[]]},{"id":"e91eab68.64fd68","type":"template","z":"63163740.2d4508","name":"","field":"template","fieldType":"msg","format":"html","syntax":"mustache","template":"<canvas id=\"myChart\" width=600 height =300></canvas>\n<script>\nvar textcolor = getComputedStyle(document.documentElement).getPropertyValue('--nr-dashboard-widgetTextColor');\nvar gridcolor = getComputedStyle(document.documentElement).getPropertyValue('--nr-dashboard-groupBorderColor');\nvar linecolors = ['#5d60ff','#ff5d5d']\n\nvar ctx = document.getElementById('myChart').getContext('2d');\nvar chart = new Chart(ctx, {\n    // The type of chart we want to create\n    type: 'line',\n\n    // The data for our dataset\n    data: {\n        labels: [],\n        datasets: [\n            {\n                label: 'first',\n                backgroundColor: linecolors[0],\n                borderColor: linecolors[0],\n                data: [],\n                yAxisID: 'left-y-axis',\n                steppedLine: false,\n                fill: false,\n                borderWidth: 1\n            },\n            {\n                label: 'second',\n                backgroundColor: linecolors[1],\n                borderColor: linecolors[1],\n                data: [],\n                yAxisID: 'right-y-axis',\n                steppedLine: false,\n                fill: true,\n                borderWidth: 1\n            }\n        ]\n    },\n\n    // Configuration options go here\n    options: {\n        scales: {\n            yAxes: [\n                {\n                    gridLines :{display:false},\n                    id: 'left-y-axis',\n                    type: 'linear',\n                    position: 'left',\n                    ticks: {\n                        fontColor: linecolors[0]\n                    }\n                },\n                {\n                    gridLines :{zeroLineColor:gridcolor,color:gridcolor,lineWidth:0.5},\n                    id: 'right-y-axis',\n                    type: 'linear',\n                    position: 'right',\n                    ticks: {\n                        fontColor:linecolors[1]\n                    }\n                }\n            ],\n            xAxes: [\n                {\n                    gridLines :{zeroLineColor:gridcolor,color:gridcolor,lineWidth:0.5},\n                    type: 'time',\n                    distribution: 'series',\n                    time:{\n                        displayFormats: {\n                            quarter: 'MMM YYYY',\n                            millisecond:'h:mm:ss',\n                            second:\t'h:mm:ss',\n                            minute:\t'h:mm',\n                            hour:\t'h'                        \n                        }\n                    },\n                    \n                    ticks: {\n                        fontColor:textcolor\n                    }\n                }\n            ]\n        }\n    }\n});\nfunction addData(chart, data, label) {\n    // same calculation as for data storage at server side\n    // data.x is newest data so treated as now.\n    var old = data.x - 1000 * 60 * 2 \n    \n    chart.data.datasets.forEach((dataset) => {\n        if(dataset.label == label){\n            dataset.data.push(data);\n        }\n        dataset.data = dataset.data.filter(entry => entry.x > old)// filter out old data.\n        \n    });\n    chart.update(0);//0 means no animation\n}\nfunction restoreChart(chart, data) {\n    // overwrite datasets (but not clear all)\n    // based on incoming data\n    // if chart has datasets which then not found from icoming data, those remain\n    var dataset\n    Object.keys(data).forEach(label => {\n        dataset = chart.data.datasets.find(ds => {return ds.label === label })// find corresponding dataset\n        if(dataset){\n            dataset.data = data[label]// if found, replace with incoming data for that dataset\n        }\n    })\n    chart.update(0);//0 means no animation\n}\n(function(scope) {\n    scope.$watch('msg', function(msg) {\n        if(msg) {\n            if (msg.topic == \"restore\") {\n                // restore chart\n                restoreChart(chart, msg.payload)\n            }\n            else{\n                //add datapoints one by one\n                addData(chart, msg.payload, msg.topic)\n            }\n        }\n   \n  });\n})(scope);\n</script>\n","output":"str","x":520,"y":240,"wires":[["8496abf.bdd4b58"]]},{"id":"6d48838f.c861ac","type":"inject","z":"63163740.2d4508","name":"Initialize","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":300,"y":240,"wires":[["e91eab68.64fd68"]]},{"id":"15644e1e.7c2ab2","type":"inject","z":"63163740.2d4508","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":false,"onceDelay":"0.62","topic":"","payload":"","payloadType":"date","x":215,"y":340,"wires":[["c3575dfd.f6964","6dd9fa64.0ebb74"]],"l":false},{"id":"c3575dfd.f6964","type":"function","z":"63163740.2d4508","name":"fake first data","func":"msg.payload = Math.round(Math.random() * 100)\nmsg.topic = 'first'\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":320,"wires":[["1735652c.72d0ab"]]},{"id":"63e9f633.a79e08","type":"ui_ui_control","z":"63163740.2d4508","name":"","events":"connect","x":200,"y":280,"wires":[["d4de9d72.8ce92"]]},{"id":"6dd9fa64.0ebb74","type":"function","z":"63163740.2d4508","name":"fake second data","func":"msg.payload = Math.round(Math.random() * 100)\nmsg.topic = 'second'\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":360,"wires":[["1735652c.72d0ab"]]},{"id":"1735652c.72d0ab","type":"function","z":"63163740.2d4508","name":"store","func":"var storage = flow.get('chartData') || {first:[],second:[]} // data structure to match the chart datasets\n\nvar now = new Date().getTime() // the moment of datapoint creation\nvar old = now - 1000 * 60 * 2  // too old data will be 2 minutes\nvar datapoint = {x:now, y:msg.payload} // create the datapoint\n\nstorage[msg.topic].push(datapoint) // push datapoint into correct object's array in storage\n\nstorage[msg.topic] = storage[msg.topic].filter(entry => entry.x > old)// filter out any too old data\n\nflow.set('chartData',storage)// write storage to flow context\n\nmsg.payload = datapoint // send out the created datapoint (msg.topic remains same)\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":340,"wires":[["8496abf.bdd4b58"]]},{"id":"99d40b3b.6ca708","type":"change","z":"63163740.2d4508","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"chartData","tot":"flow"},{"t":"set","p":"topic","pt":"msg","to":"restore","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":280,"wires":[["8496abf.bdd4b58"]]},{"id":"d4de9d72.8ce92","type":"switch","z":"63163740.2d4508","name":"","property":"chartData","propertyType":"flow","rules":[{"t":"nnull"}],"checkall":"true","repair":false,"outputs":1,"x":350,"y":280,"wires":[["99d40b3b.6ca708"]]},{"id":"6d56c1dd.99aa2","type":"ui_group","name":"Chart","tab":"b7b17e22.6df42","order":3,"disp":true,"width":"15","collapse":false},{"id":"b7b17e22.6df42","type":"ui_tab","name":"Ovens","icon":"dashboard","order":1,"disabled":false,"hidden":false}]
1 Like

Thanks @hotNipi, your version works well, and data is now restored as soon as a browser connects.

It's amazing how many different options can be used in chartjs - different line styles, gradients, fills... the list is exhaustive!!

Beautiful world behind the curtains ... :slight_smile:

2 Likes