Retaining chart.js data after browser refresh

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 :wink:, 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 :hot_face:

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"}}]

Try this

[{"id":"ea7c02fa77fe6efc","type":"ui-template","z":"3567e03e18126502","group":"2c34b9c7d6324531","page":"","ui":"","name":"Custom Line Chart","order":1,"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        datasets: [],\n        data() {\n            return {\n                maxAge: 1000000,  // max age of data to keep 1000 seconds, can be set by msg.maxAge\n                keepCount: 50 // can be set by msg.keepCount\n            }\n        },\n        computed: {\n            maxKeepAge: function () {\n                if (typeof this.maxAge === \"number\" && this.maxAge > 0) {\n                    return this.maxAge\n                }\n                return null\n            },\n            maxKeepCount: function () {\n                if (typeof this.keepCount === \"number\" && this.keepCount > 0) {\n                    return this.keepCount\n                }\n                return null\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\", data:[]},\n                            {label: \"dataB\", data:[]}\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                this.updateChart()\n                console.log(this.data)\n            },\n            clearOldData() {\n                console.log('clearing old data')\n                if (this.datasets?.length) {\n                    let countPerSet = this.maxKeepCount ? Math.ceil(this.keepCount / this.datasets.length) : 0\n                    this.datasets.forEach(dataset => {\n                        if (countPerSet && dataset.data.length > countPerSet) {\n                            dataset.data.splice(0, dataset.data.length - countPerSet)\n                        }\n                        if (this.maxKeepAge) {\n                            const minTime = Date.now() - this.maxAge\n                            dataset.data = dataset.data.filter(point => {\n                                const pointTime = (new Date(point.time)).valueOf()\n                                return pointTime > minTime\n                            })\n                        }\n                    })\n                }\n            },\n            clearChart() {\n                this.datasets = []\n                this.updateChart()\n            },\n            addDataPoint(topic, data, doUpdate = true) {\n                if (!topic || !data) { return }\n                this.datasets = this.datasets || []\n                let dataset = this.datasets.find(e => e.label === topic)\n                if (!dataset) {\n                    dataset = { label: topic, data: [] }\n                    this.datasets.push(dataset)\n                }\n                if (typeof data === \"number\") {\n                    dataset.data.push({\n                        time: (new Date()).getTime(),\n                        value: data\n                    })\n                } else {\n                    dataset.data.push({\n                        time: data.time,\n                        value: data.value\n                    })\n                }\n                this.updateChart()\n            },\n            updateChart() {\n                if (this.chart) {\n                    this.chart.data.datasets = this.datasets\n                    this.chart.update()\n                }\n            },\n            onInput (msg) {\n                console.log('got msg:', msg)\n                // sync keepCount - count of points to keep\n                if (typeof msg.keepCount === \"number\") {\n                    this.keepCount = msg.keepCount\n                    // should probably call this.clearOldData() when a change is detected\n                }\n                // sync maxAge - oldest age of points to keep (in ms)\n                if (typeof msg.maxAge === \"number\") {\n                    this.maxAge = msg.maxAge\n                }\n                if (Array.isArray(msg.payload)) {\n                    if (msg.payload.length == 0) {\n                        this.clearChart()\n                        return\n                    }\n                    if (msg.topic === 'reload') {\n                        this.clearChart()\n                    }\n                    // historical 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, false) // add without update\n                    })\n                    this.updateChart() // now they are added, call update\n                } else {\n                    this.clearOldData()\n                    this.addDataPoint(msg.topic, msg.payload, true) // add and update\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;\n    /*make this container available for container querys*/\n}\n\n.chart-container {\n    position: relative;\n    margin: auto;\n    height: 100cqb;\n    /*use container query units to give full height of the container (100% of container height) */\n    width: 100cqi;\n    /*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</style>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1270,"y":640,"wires":[[]]},{"id":"e12c2fa23f25087e","type":"inject","z":"3567e03e18126502","name":"clear chart","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[]","payloadType":"json","x":520,"y":520,"wires":[["fb4b4ec11c331f8b"]]},{"id":"9b20d06596172275","type":"function","z":"3567e03e18126502","name":"data cache","func":"// identify if this is a browser landing on page \"Home\"\n// if not, default to \"a new data point\"\nconst isPageChange = msg._client && msg.payload === \"change\" && msg.name === \"Home\"\nmsg.keepCount = 50\n\nif (isPageChange) {\n    node.status({fill:\"yellow\",shape:\"ring\",text:\"Sending reload + cache\"});\n    const data = flow.get('chartData') || []\n    msg.topic = 'reload'\n    msg.payload = data\n    return msg\n}\nif (msg.topic === 'clear') {\n    node.status({fill:\"yellow\",shape:\"ring\",text:\"Cleared cache\"});\n    flow.set('chartData', [])\n    msg.topic = 'reload'\n    msg.payload = []\n    return msg\n}\n\n// if we get here, then assume its \"a new data point\"\nlet data = flow.get('chartData')\nif (!Array.isArray(data)) {\n    data = []\n}\n\n// add latest data points to data array\nconst newdata = {\n    topic: msg.topic,\n    time: msg.time,\n    value: msg.payload\n}\ndata.push(newdata)\n\n// Limit the size of the data array\nwhile (data.length > msg.keepCount) {\n    data = data.slice(data.length - msg.keepCount)\n}\n\nflow.set('chartData', data)\nnode.status({fill:\"green\",shape:\"ring\",text:\"Sending new data point\"});\nreturn msg\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1030,"y":640,"wires":[["ea7c02fa77fe6efc","e662ce12a8a3b32f"]]},{"id":"257ec1778e051d96","type":"inject","z":"3567e03e18126502","name":"Every 5 seconds","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":550,"y":640,"wires":[["c5ab23508895112e"]]},{"id":"c5ab23508895112e","type":"function","z":"3567e03e18126502","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":780,"y":640,"wires":[["9b20d06596172275"]]},{"id":"04526ca43ba5f66b","type":"ui-control","z":"3567e03e18126502","name":"","ui":"22ea43815413e748","events":"change","x":700,"y":760,"wires":[["bc086024e8bde1a1"]]},{"id":"530a22a0fa932d73","type":"inject","z":"3567e03e18126502","name":"Simulate browser page change to Home","props":[{"p":"payload"},{"p":"tab","v":"1","vt":"num"},{"p":"name","v":"Home","vt":"str"},{"p":"_client","v":"{}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"change","payloadType":"str","x":610,"y":700,"wires":[["bc086024e8bde1a1"]]},{"id":"e662ce12a8a3b32f","type":"debug","z":"3567e03e18126502","name":"debug 172","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1250,"y":700,"wires":[]},{"id":"ac7e15db347084b0","type":"inject","z":"3567e03e18126502","name":"clear cache","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"clear","payload":"[]","payloadType":"json","x":530,"y":580,"wires":[["4eb81b9c157a38da"]]},{"id":"fb4b4ec11c331f8b","type":"junction","z":"3567e03e18126502","x":1100,"y":520,"wires":[["ea7c02fa77fe6efc"]]},{"id":"4eb81b9c157a38da","type":"junction","z":"3567e03e18126502","x":880,"y":580,"wires":[["9b20d06596172275"]]},{"id":"bc086024e8bde1a1","type":"junction","z":"3567e03e18126502","x":880,"y":720,"wires":[["9b20d06596172275"]]},{"id":"2c34b9c7d6324531","type":"ui-group","name":"Energy","page":"7294756f31e17b81","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"22ea43815413e748","type":"ui-base","name":"base","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"fixed","titleBarStyle":"default"},{"id":"7294756f31e17b81","type":"ui-page","name":"Home","ui":"22ea43815413e748","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":2,"className":"","visible":"true","disabled":"false"},{"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"}}]

Part of the issue was attempting to add points to a chart that was not yet initialised.

A handy way of figuring things out like this is to add console.log statements to your code and then inspect/debug/pause in the browser console.

PS: I also added a keepCount (that and maxAge) they can also be set in the msg too so that data is pruned at client side based similar to how your cache in the function prunes by max point count

You do create timestamps like this

Fine.

But the clearing the old data in ui_template tries to compare numbers to figure out the old data.
Numbers because it expects the timestamps to be numbers to do math which is not directly possible with any kind of time related strings.

See the point.time > Date.now() - this.keep

 clearOldData(){
                this.chart.data.datasets.forEach(dataset => {
                    dataset.data = dataset.data.filter(point => point.time > Date.now() - this.keep)
                })
            },

So even this comparing function or your time creation must be changed to make comparing possible.

Aah OK, the reason for the timestamp being in that format is that the data stream was formatted some years ago for a legacy system when I used a TIG stack, but I can change it.
I prefer to work with epoch timestamps anyway.
Thanks

Thanks Steve.

That's exactly the way I want to introduce chart formatting changes, such as line type (dashed, solid, dotted, etc), line colour and others enhancements, by setting them in the message.
It appears that the datasets are created dynamically, so have to get my head around how to specify different styles to different datasets.

custom-period-selector-on-xychart
How about like this way by buttons?
Not chart.js, but Amcharts.

There are lots of different charting packages, each with their own strengths & weaknesses, but for the sake of this topic, let's stay with chart.js.

The chartjs leaves all fancy stuff like zooming to plugins. You can create one if you need or use already built plugins if they do what you wanted. Mostly they are overkill or don't fulfill your desires.
There is zoom plugin but I didn't get any results with it.
And I'm not going to write any.

But for zooming (as long within current data) the one can use the "Poor Man Three Click Zoom" which works on options thus doesn't need to be plugin.

[{"id":"d185e71dbf6af008","type":"ui-template","z":"9ab81d258b54a577","group":"1c6f457dfe15977b","page":"","ui":"","name":"Custom Line Chart","order":1,"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\n<script>\n    export default {\n        data() {           \n            return {\n                keep:1000000,  // keep data for 1000 seconds\n                title:\"Custom chart\"\n                                          \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                const temporaryLimits = []   \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                        onClick: (e) => {\n                            switch (temporaryLimits.length){\n                                case 0:{\n                                    const canvasPosition = getRelativePosition(e, chart);\n                                    const dataX = chart.scales.x.getValueForPixel(canvasPosition.x);                                    \n                                    temporaryLimits.push(dataX)\n                                    break\n                                }\n                                case 1:{\n                                    const canvasPosition = getRelativePosition(e, chart);\n                                    const dataX = chart.scales.x.getValueForPixel(canvasPosition.x);\n                                    temporaryLimits.push(dataX)\n                                    temporaryLimits.sort((a, b) => a - b);                                    \n                                    chart.options.scales.x.min = temporaryLimits[0]\n                                    chart.options.scales.x.max = temporaryLimits[1]\n                                    chart.options.plugins.title.text = this.title +\" * zoomed in\"                                    \n                                    this.chart.update()\n                                    break\n                                }\n                                case 2:{\n                                    temporaryLimits.length = 0\n                                    chart.options.scales.x.min = undefined\n                                    chart.options.scales.x.max = undefined\n                                    chart.options.plugins.title.text = this.title\n                                    this.chart.update()\n                                    break\n                                }\n\n                            }\n                           \n                        },\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: this.title\n                            }\n                        }   \n                    },\n                });\n                // make this available to all elements of the component\n                this.chart = chart                \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":950,"y":1160,"wires":[[]]},{"id":"cb6afc5505802345","type":"inject","z":"9ab81d258b54a577","name":"clear chart","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[]","payloadType":"json","x":700,"y":1120,"wires":[["d185e71dbf6af008"]]},{"id":"add41d2d13967c9c","type":"function","z":"9ab81d258b54a577","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":690,"y":1220,"wires":[["d185e71dbf6af008"]]},{"id":"8c72de4c76b53858","type":"inject","z":"9ab81d258b54a577","name":"Simulate browser connect","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"redraw","payloadType":"str","x":270,"y":1220,"wires":[["add41d2d13967c9c"]]},{"id":"dca7cb685805cc93","type":"inject","z":"9ab81d258b54a577","name":"Every 15 seconds","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"3","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":250,"y":1160,"wires":[["ce575d8959f48057"]]},{"id":"ce575d8959f48057","type":"function","z":"9ab81d258b54a577","name":"Create dummy data!","func":"const date = new Date()\nconst time = Date.now()//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":480,"y":1160,"wires":[["d185e71dbf6af008","add41d2d13967c9c"]]},{"id":"1c6f457dfe15977b","type":"ui-group","name":"Custom Bar Chart","page":"d0621b8f20aee671","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"d0621b8f20aee671","type":"ui-page","name":"Charts","ui":"29792df7d7b05e2e","path":"/charts","icon":"home","layout":"notebook","theme":"5075a7d8e4947586","order":2,"className":"","visible":"true","disabled":"false"},{"id":"29792df7d7b05e2e","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default"},{"id":"5075a7d8e4947586","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]
1 Like

Thanks, I've added that into the flow, and also changed the timestamp format to a epoch.
This is the flow I'm using -

[{"id":"6c017fa405ce6b6e","type":"ui-template","z":"1326aadbacf36704","group":"2c34b9c7d6324531","page":"","ui":"","name":"Custom Line Chart","order":11,"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        datasets: [],\n        data() {\n            return {\n                maxAge: 1000000,  // max age of data to keep 1000 seconds, can be set by msg.maxAge\n                keepCount: 50 // can be set by msg.keepCount\n            }\n        },\n        computed: {\n            maxKeepAge: function () {\n                if (typeof this.maxAge === \"number\" && this.maxAge > 0) {\n                    return this.maxAge\n                }\n                return null\n            },\n            maxKeepCount: function () {\n                if (typeof this.keepCount === \"number\" && this.keepCount > 0) {\n                    return this.keepCount\n                }\n                return null\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                const temporaryLimits = []\n\n                // Render the chart\n                const chart = new Chart(ctx, {\n\n                    type: 'line',\n                    data: {\n                        datasets: [\n                            {label: \"dataA\", data:[]},\n                            {label: \"dataB\", data:[]}\n                        ]\n                    },\n                    options: {\n                            onClick: (e) => {\n                            switch (temporaryLimits.length){\n                                case 0:{\n                                    const canvasPosition = getRelativePosition(e, chart);\n                                    const dataX = chart.scales.x.getValueForPixel(canvasPosition.x);                                    \n                                    temporaryLimits.push(dataX)\n                                    break\n                                }\n                                case 1:{\n                                    const canvasPosition = getRelativePosition(e, chart);\n                                    const dataX = chart.scales.x.getValueForPixel(canvasPosition.x);\n                                    temporaryLimits.push(dataX)\n                                    temporaryLimits.sort((a, b) => a - b);                                    \n                                    chart.options.scales.x.min = temporaryLimits[0]\n                                    chart.options.scales.x.max = temporaryLimits[1]\n                                    chart.options.plugins.title.text = \" * zoomed in *\"                                    \n                                    this.chart.update()\n                                    break\n                                }\n                                case 2:{\n                                    temporaryLimits.length = 0\n                                    chart.options.scales.x.min = undefined\n                                    chart.options.scales.x.max = undefined\n                                    chart.options.plugins.title.text = this.title\n                                    this.chart.update()\n                                    break\n                                }\n\n                            }\n                           \n                        },\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: 'Line chart test'\n                            }\n                        }\n                    },\n                });\n                // make this available to all elements of the component\n                this.chart = chart\n                this.updateChart()\n                console.log(this.data)\n            },\n            clearOldData() {\n                console.log('clearing old data')\n                if (this.datasets?.length) {\n                    let countPerSet = this.maxKeepCount ? Math.ceil(this.keepCount / this.datasets.length) : 0\n                    this.datasets.forEach(dataset => {\n                        if (countPerSet && dataset.data.length > countPerSet) {\n                            dataset.data.splice(0, dataset.data.length - countPerSet)\n                        }\n                        if (this.maxKeepAge) {\n                            const minTime = Date.now() - this.maxAge\n                            dataset.data = dataset.data.filter(point => {\n                                const pointTime = (new Date(point.time)).valueOf()\n                                return pointTime > minTime\n                            })\n                        }\n                    })\n                }\n            },\n            clearChart() {\n                this.datasets = []\n                this.updateChart()\n            },\n            addDataPoint(topic, data, doUpdate = true) {\n                if (!topic || !data) { return }\n                this.datasets = this.datasets || []\n                let dataset = this.datasets.find(e => e.label === topic)\n                if (!dataset) {\n                    dataset = { label: topic, data: [] }\n                    this.datasets.push(dataset)\n                }\n                if (typeof data === \"number\") {\n                    dataset.data.push({\n                        time: (new Date()).getTime(),\n                        value: data\n                    })\n                } else {\n                    dataset.data.push({\n                        time: data.time,\n                        value: data.value\n                    })\n                }\n                this.updateChart()\n            },\n            updateChart() {\n                if (this.chart) {\n                    this.chart.data.datasets = this.datasets\n                    this.chart.update()\n                }\n            },\n            onInput (msg) {\n                console.log('got msg:', msg)\n                // sync keepCount - count of points to keep\n                if (typeof msg.keepCount === \"number\") {\n                    this.keepCount = msg.keepCount\n                    // should probably call this.clearOldData() when a change is detected\n                }\n                // sync maxAge - oldest age of points to keep (in ms)\n                if (typeof msg.maxAge === \"number\") {\n                    this.maxAge = msg.maxAge\n                }\n                if (Array.isArray(msg.payload)) {\n                    if (msg.payload.length == 0) {\n                        this.clearChart()\n                        return\n                    }\n                    if (msg.topic === 'reload') {\n                        this.clearChart()\n                    }\n                    // historical 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, false) // add without update\n                    })\n                    this.updateChart() // now they are added, call update\n                } else {\n                    this.clearOldData()\n                    this.addDataPoint(msg.topic, msg.payload, true) // add and update\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;\n    /*make this container available for container querys*/\n}\n\n.chart-container {\n    position: relative;\n    margin: auto;\n    height: 100cqb;\n    /*use container query units to give full height of the container (100% of container height) */\n    width: 100cqi;\n    /*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</style>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":910,"y":1500,"wires":[[]]},{"id":"3f0db9aa605aad98","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":160,"y":1380,"wires":[["6005ba1dd76acd77"]]},{"id":"87a3e6c101ac7cba","type":"function","z":"1326aadbacf36704","name":"data cache","func":"// identify if this is a browser landing on page \"Home\"\n// if not, default to \"a new data point\"\nconst isPageChange = msg._client && msg.payload === \"change\" && msg.name === \"Home\"\n\nif (isPageChange) {\n    node.status({fill:\"yellow\",shape:\"ring\",text:\"Sending reload + cache\"});\n    const data = flow.get('chartData') || []\n    msg.topic = 'reload'\n    msg.payload = data\n    return msg\n}\nif (msg.topic === 'clear') {\n    node.status({fill:\"yellow\",shape:\"ring\",text:\"Cleared cache\"});\n    flow.set('chartData', [])\n    msg.topic = 'reload'\n    msg.payload = []\n    return msg\n}\n\n// if we get here, then assume its \"a new data point\"\nlet data = flow.get('chartData')\nif (!Array.isArray(data)) {\n    data = []\n}\n\n// add latest data points to data array\nconst newdata = {\n    topic: msg.topic,\n    time: msg.time,\n    value: msg.payload\n}\ndata.push(newdata)\n\n// Limit the size of the data array\nwhile (data.length > msg.keepCount) {\n    data = data.slice(data.length - msg.keepCount)\n}\n\nflow.set('chartData', data)\nnode.status({fill:\"green\",shape:\"ring\",text:\"Sending new data point\"});\nreturn msg\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":670,"y":1500,"wires":[["6c017fa405ce6b6e","c8a34e14a1583959"]]},{"id":"fa2f59aa9d5b5909","type":"inject","z":"1326aadbacf36704","name":"Every 5 seconds","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":190,"y":1500,"wires":[["3a8db11f7faeb9bf"]]},{"id":"3a8db11f7faeb9bf","type":"function","z":"1326aadbacf36704","name":"Create dummy data!","func":"const date = new Date()\nconst time = (new Date()).getTime()\n\nconst dataA = {\n    payload: Math.floor(Math.random() * 10) + 1,\n    topic: 'dataA',\n    time: time,\n    keepCount: 50\n}\n\nconst dataB = {\n    payload: Math.floor(Math.random() * 5) + 2,\n    topic: 'dataB',\n    time: time,\n    keepCount: 50\n}\n\nreturn [ [ dataA, dataB ] ]\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":1500,"wires":[["87a3e6c101ac7cba"]]},{"id":"dbcb4d92e2577637","type":"ui-control","z":"1326aadbacf36704","name":"","ui":"ae3d4aeb3f977a90","events":"change","x":340,"y":1620,"wires":[["beda919d9504c788"]]},{"id":"ff4cb573a004472b","type":"inject","z":"1326aadbacf36704","name":"Simulate browser page change to Home","props":[{"p":"payload"},{"p":"tab","v":"1","vt":"num"},{"p":"name","v":"Home","vt":"str"},{"p":"_client","v":"{}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"change","payloadType":"str","x":250,"y":1560,"wires":[["beda919d9504c788"]]},{"id":"c8a34e14a1583959","type":"debug","z":"1326aadbacf36704","name":"debug 172","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":890,"y":1560,"wires":[]},{"id":"d2cfcde862d92ba3","type":"inject","z":"1326aadbacf36704","name":"clear cache","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"clear","payload":"[]","payloadType":"json","x":170,"y":1440,"wires":[["75ee8d2ec5b27239"]]},{"id":"6005ba1dd76acd77","type":"junction","z":"1326aadbacf36704","x":740,"y":1380,"wires":[["6c017fa405ce6b6e"]]},{"id":"75ee8d2ec5b27239","type":"junction","z":"1326aadbacf36704","x":520,"y":1440,"wires":[["87a3e6c101ac7cba"]]},{"id":"beda919d9504c788","type":"junction","z":"1326aadbacf36704","x":520,"y":1580,"wires":[["87a3e6c101ac7cba"]]},{"id":"2c34b9c7d6324531","type":"ui-group","name":"Energy","page":"7294756f31e17b81","width":"6","height":"1","order":1,"showTitle":true,"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":"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":"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"}}]

I'm currently trying to implement more basic features, such as specifying line color for individual datasets.
I can change both line colors together, for example to red;

 elements:{
         line:{
         borderWidth:1,//line thickness
         tension:0.3, //line curvature
         borderColor: 'red',
               },
               point:{
               pointRadius:0 //remove points
               }
            },

But I can't see how to change line colors individually, for example one dataset red, and the other blue.

Is it something like -

if (this.datasets = "dataA") {
    borderColor: 'red',
    } else if (this.datasets = "dataB") {
    borderColor: 'blue',
    }

(It doesn't work!)

That will set this.datasets (single = is assignment)

What you probably want is a look up or send a color in a control message. Something that is controlled like sending a msg without a payload or a specific topic that you can use to set the specific property in the specified dataset. The chartjs has decent docs showing where the property goes.

Something like (untested, written on my phone)

const ds = this.datasets?.find(e => e.label === msg.label)
if (ds) {
  ds.borderColor = msg.borderColor ?? ds.borderColor
}
1 Like

I think at this point you'll need to make clear which kind of chart widget you are creating.

  1. If the colors always need to be tied with the topic and you know how many different topics (== lines) the chart will show, there's no need to make dynamic datasets.

  2. If you are creating universal thing - the widget level config should have some kind of mapping which you can use to bind color with topic.

And I'd guess you aren't happy with default behaviour where color is figured out by chart (kind of random, color for topic is not guaranteed)

1 Like

I'm using line color as an example, because I will then be able to add/change other characteristics, (although yes, I do wish to control the colors - see the screenshot below, dataB can hardly be seen).

No, not universal (above my paygrade!!), although I want to make the options easy for users to configure.

I'm only using it because that was the example flow that you wrote, and which worked well.

Yes I'm hoping to, and I'll look further at your code tomorrow, it's given me a headache trying to understand it (now where has my 'javascript for dummies' book gone :wink:)