Here's an example which limits the number of datapoints in the chart, and in this example removes datapoints older than 10 seconds.
It also re-draws the plot when a browser connects or a reboot. (@hotNipi helped me with this )
NB I've left the function nodes separate so it's easier to see what each part of the flow does
[{"id":"9e69a15c.275b7","type":"function","z":"c4396940.2c4cc8","name":"Format incoming data","func":"var msg0 = {};\nvar msg1 = {};\nvar msg2 = {};\n\nmsg0.payload = msg.heater;\nmsg0.topic = 'heater';\n\nmsg1.payload = msg.temp;\nmsg1.topic = 'temp';\n\nmsg2.payload = msg.target;\nmsg2.topic = 'target';\n\nreturn [[msg0,msg1,msg2]];","outputs":1,"noerr":0,"initialize":"","finalize":"","x":400,"y":1000,"wires":[["2ea25389.533f4c"]]},{"id":"e3e7d295.96c1","type":"ui_template","z":"c4396940.2c4cc8","group":"12bd1e56.84a0c2","name":"Line Chart","order":4,"width":"12","height":"6","format":"","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":790,"y":1000,"wires":[[]]},{"id":"e29023d4.a722d","type":"template","z":"c4396940.2c4cc8","name":"Initialisation","field":"template","fieldType":"msg","format":"html","syntax":"mustache","template":"<canvas id=\"myChart\" width=1200 height=500></canvas>\n<script>\nvar textcolor = getComputedStyle(document.documentElement).getPropertyValue('--nr-dashboard-widgetTextColor');\nvar gridcolor = getComputedStyle(document.documentElement).getPropertyValue('--nr-dashboard-groupBorderColor');\nvar linecolors = ['#5d60ff','#24f21d','#f7bcc0']\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: 'temp',\n backgroundColor: linecolors[0],\n borderColor: linecolors[0],\n data: [],\n yAxisID: 'left-y-axis',\n steppedLine: false,\n fill: false,\n radius: 0,\n hitRadius: 8,\n borderWidth: 1\n },\n {\n label: 'target',\n backgroundColor: linecolors[1],\n borderColor: linecolors[1],\n data: [],\n yAxisID: 'left-y-axis',\n lineTension: 0,\n steppedLine: false,\n fill: false,\n radius: 0,\n hitRadius: 8,\n borderWidth: 1\n },\n {\n label: 'heater',\n backgroundColor: linecolors[2],\n borderColor: linecolors[2],\n data: [],\n yAxisID: 'left-y-axis',\n lineTension: 0,\n steppedLine: true,\n fill: true,\n radius: 0,\n hitRadius: 8,\n borderWidth: 1\n }\n ]\n },\n\n // Configuration options go here\n options: {\n scales: {\n yAxes: [\n {\n scaleLabel: {\n //display: true,\n //labelString: 'Temperature °C',\n fontColor: linecolors[0]\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 xAxes: [\n {\n gridLines :{zeroLineColor:gridcolor,color:gridcolor,lineWidth:0.5},\n type: 'time',\n distribution: 'series',\n time:{\n tooltipFormat:'HH:mm:ss',\n displayFormats: {\n quarter: 'MMM YYYY',\n millisecond:'HH:mm:ss.SSS',\n second:\t'HH:mm:ss',\n minute:\t'HH:mm',\n hour:\t'HH' \n }\n },\n \n ticks: {\n fontColor:textcolor\n }\n }\n ]\n },\n elements: {\n point:{\n radius:1.5\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 - 10000 \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":610,"y":920,"wires":[["e3e7d295.96c1"]]},{"id":"2140807.c5df78","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":495,"y":920,"wires":[["e29023d4.a722d"]],"l":false},{"id":"e8f6de10.fc142","type":"ui_ui_control","z":"c4396940.2c4cc8","name":"","events":"connect","x":435,"y":960,"wires":[["d8559393.11195"]],"l":false},{"id":"2ea25389.533f4c","type":"function","z":"c4396940.2c4cc8","name":"store data","func":"var storage = flow.get('ghData') || {temp:[],target:[],heater:[]} // data structure to match the chart datasets\n\nvar now = new Date().getTime() // the moment of datapoint creation\nvar old = now - 10000 // too old data will be removed\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('ghData',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":"","x":600,"y":1000,"wires":[["e3e7d295.96c1"]]},{"id":"3f837ff4.6f9f1","type":"change","z":"c4396940.2c4cc8","name":"Restore","rules":[{"t":"set","p":"payload","pt":"msg","to":"ghData","tot":"flow"},{"t":"set","p":"topic","pt":"msg","to":"restore","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":600,"y":960,"wires":[["e3e7d295.96c1"]]},{"id":"d8559393.11195","type":"switch","z":"c4396940.2c4cc8","name":"","property":"ghData","propertyType":"flow","rules":[{"t":"nnull"}],"checkall":"true","repair":false,"outputs":1,"x":495,"y":960,"wires":[["3f837ff4.6f9f1"]],"l":false},{"id":"8c1eeba5.707ff8","type":"inject","z":"c4396940.2c4cc8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":75,"y":1000,"wires":[["c74a8401.c84028"]],"l":false},{"id":"c74a8401.c84028","type":"change","z":"c4396940.2c4cc8","name":"Example Data","rules":[{"t":"set","p":"temp","pt":"msg","to":"$floor($random()*10)","tot":"jsonata"},{"t":"set","p":"target","pt":"msg","to":"$floor($random()*10)","tot":"jsonata"},{"t":"set","p":"heater","pt":"msg","to":"$floor($random()*10)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":200,"y":1000,"wires":[["9e69a15c.275b7"]]},{"id":"12bd1e56.84a0c2","type":"ui_group","z":"","name":"Default","tab":"ad4af309.f85ed","order":1,"disp":false,"width":"12","collapse":false},{"id":"ad4af309.f85ed","type":"ui_tab","z":"","name":"Home","icon":"dashboard","order":4,"disabled":false,"hidden":false}]
EDIT 2pm 31/7 - spotted typo in flow, now corrected!