Has anyone created a scatter/bubble plot?

I'm looking to plot some data - a bunch of points both positive and negative, and want to visualize them. I'm looking to create a scatter plot but haven't been able to find an example.

Before I go off and try to build a flow or node to create one, I just thought I'd ask. Thanks.

https://flows.nodered.org/flow/9d407ebf23b1f9214720dd9360445b6e

I don't know exactly what @zenofmud is looking for but that is not what I would call a scatter chart. A scatter chart should accept a set of x,y pairs and plot the points. I don't think the flow will allow you to have multiple points with the same x value (ie vertically above each other) for example.

That's true, that flow won't let you have two exact same x values, but they only have to be not exactly the same to be ok. :slight_smile:

1 Like

Correct @Colin, but I don't think there is any other way in node-red. I think Grafana has the possibility to do a real scatter plot.

I guess I’ll take a deeper look at chart.js which I think can do it, but not till tomorrow, the grandkids have invaded and moved in three houses up the street

1 Like

I decided I could use a bubble chart and it turns out that all you need to do is use a ui_template with the correct info. Here is what it looks like with hard coded data pioints and the 'one node' flow is attached. Note the size of the points could be changed vwey easily.

[{"id":"46a8dffe.2dda18","type":"ui_template","z":"6445adf7.f2fb24","group":"6169c1d9.e98618","name":"Bubble","order":0,"width":0,"height":0,"format":"<div ng-bind-html=\"msg.payload\"></div>\n\t<div style=\"width:600px;\">\n\t\t<canvas id=\"bubble-chart\" width=\"300\" height=\"300\"></canvas>\n\t</div>\n\n<script>\nnew Chart(document.getElementById(\"bubble-chart\"), {\n    type: 'bubble',\n    data: {\n      labels: \"label\",\n      datasets: [\n        {\n          label: \"Bases\",\n          backgroundColor: \"rgba(255, 216, 0, 1.0)\",\n          borderColor: \"#000\",\n          data: [\n          \t{ x: -85, y: 79, r: 10 },\n          \t{ x: 385, y: -279, r: 10 },\n          \t{x: 207, y: -461, r: 10 },\n          \t{x: 262, y: -548, r: 10 },\n          \t{x: 388, y: -1320, r: 10 },\n          \t{x: 415, y: 185, r: 10 },\n          \t{x: 584, y: -1100, r: 10 }\n          ]\n        }, {\n          label: [\"POI\"], \n          backgroundColor: \"rgba(72, 255, 70,1.0)\",\n          borderColor: \"#000\",\n          data: [\n          \t{x: 106, y: -381, r: 10 },\n          \t{x: 174, y: -119, r: 10 },\n          \t{x: 185, y: -780, r: 10 },\n          \t{x: 244, y: -1113, r: 10 },\n          \t{x: 261, y: -1319, r: 10 },\n          \t{x: 296, y: -100, r: 10 }\n\t\t  ]\n        }\n      ]\n    },\n    options: {\n        legend: {\n            display: true,\n            labels: {\n                fontColor: 'rgb(255, 99, 132)'\n            }\n        },\n      title: {\n        display: true,\n        text: 'Locations of Bases'\n      }, scales: {\n        yAxes: [{ \n          scaleLabel: {\n            display: true,\n            labelString: \"yAxes\"\n          }\n        }],\n        xAxes: [{ \n          scaleLabel: {\n            display: true,\n            labelString: \"xAxes\"\n          }\n        }]\n      }\n    }\n});\n\n</script>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":340,"y":320,"wires":[[]]},{"id":"6169c1d9.e98618","type":"ui_group","z":"","name":"Chart","tab":"421ff38.a99248c","disp":true,"width":"12","collapse":false},{"id":"421ff38.a99248c","type":"ui_tab","z":"","name":"Home","icon":"dashboard"}]
4 Likes

Very nice ! Def worth adding to flows.nodered.org

@dceejay - I can do that, but I was wondering if I should explore adding it to ui_charts as a possible PR...

well you could do - but some things to think about... most of the widgets expect just a number in the payload - so that angular filters can be applied to them - so how would you handle the (in this case) x co-ordinate ? (assuming y is in payload)... and then also maybe the radius... To me it would seem odd not to pass in a single object, so that needs some thought.

Hmmm, you make some very good points there...I think this calls for a nice little writeup for flows.nodered.org :grin:

just added an example you can find at https://flows.nodered.org/flow/d1b9f2c606e536ce4a3960abcd887754

@dceejay - I figured out how to add an image to a entry on flows.nodered.org!!! (google is my friend :grin:)

2 Likes

I tried to use this dynamic scatter/bubble plot (adding the data and reset at some point) and got it working. In my solution I update the plot every minute and other plot is updated every hour. I'm doing this because I don't want to exhaust the node-red with large amount of data.

I have one problem. When I go to the dashboard page (or reload it) the plot is updated only after the one minute or one hour update has passed and before that it shows only the default screen "this will be ignored". How this can be prevented so that it shows the plot straight away when the page is reloaded.

One solution is to update the plot for example every 5s, but there would be 17 280x2 data points in one day and with the other plot I was thinking to reset it every year so 6 307 200x2 data point :grimacing:
Would this exhaust the node-red?

Other solution is to inject the node whenever the node-red dashboard is reloaded. Can this be made somehow?

One minute update plot:

[{"id":"c42b1a5f.948b78","type":"debug","z":"86c9171.40beee8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":1130,"y":180,"wires":[]},{"id":"dca3aba9.d2b7e8","type":"debug","z":"86c9171.40beee8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1450,"y":100,"wires":[]},{"id":"9125df13.c99f4","type":"function","z":"86c9171.40beee8","name":"Build two cumulating random datasets","func":"// this is where you define the legend for \n// the datasets\nvar title = \"ID3\";\nvar legend1 = \"Latauskäyrä\";\n\n\n// variables to be used to \nvar i, x, y, r, data1;\n// initialise the counter to 0 if it doesn't exist already\nvar data1 = flow.get('data1')||'';\n\n\nvar count = flow.get('count')||0;\ncount += 1;\nx = global.get('ID3soc');\ny = global.get('ID3latausTeho');\nr = 1;\nif (count > 1440) {\n    count = 1;\n    data1 = '{x: \"x\",  y: \"y\",  r: \"r\"},'; // Force the size of the\n} \n// store the value back\nflow.set('count',count);\n// make it part of the outgoing msg object\nmsg.count = count;\n\n// for dataset #1\n// this code generates one x/y point \n// between -100 and +100 and a random \n//size between 1 and 10\n//node.warn(\"db1=\"+data1);\nx = global.get('ID3soc');\ny = global.get('ID3latausTeho');\nr = 1;\ndata1 += \"{x: \"+x+\", y: \"+y+\", r: \"+r+\"},\";\n//node.warn(\"db2=\"+data1);\n// this line removes the last comma to end the dataset\ndata1 = data1.replace(/^(.*),(.*?)$/, '$1')\n\n// Now we build msg.payload\nmsg.payload = { \"title\"   : title,\n                \"legend1\" : legend1,\n                \"data1\"   : data1}\n                \ndata1 += \",\";\n\n\nflow.set([\"data1\"], [data1]);\nreturn msg;\n\n","outputs":1,"noerr":0,"x":510,"y":100,"wires":[["8e767188.51d14","3846208e.4e493"]]},{"id":"8e767188.51d14","type":"debug","z":"86c9171.40beee8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":790,"y":180,"wires":[]},{"id":"3846208e.4e493","type":"template","z":"86c9171.40beee8","name":"Create the msg.template to use","field":"template","fieldType":"msg","format":"html","syntax":"mustache","template":"<canvas id=\"bubble-chart\" width=\"1\" height=\"1\" style=\"border:1px solid #ffffff;\"></canvas>\n\n<script>\nnew Chart(document.getElementById(\"bubble-chart\"), {\n    type: 'bubble',\n    data: {\n      labels: \"label\",\n      datasets: [\n        {\n          label: \"{{{payload.legend1}}}\",\n          backgroundColor: \"rgba(255, 255, 255, 0.3000)\",\n          borderColor: \"rgba(255,255,255,0.3000)\",\n          data: [\n            {{{payload.data1}}}\n          ]\n        }\n      ]\n    },\n    options: {\n        animation: false,\n        legend: {\n            display: true,\n            labels: {\n                fontColor: 'rgb(255, 255, 255)'\n            }\n        },\n    title: {\n        display: true,\n        text: '{{{payload.title}}}',\n        fontColor: 'rgb(255, 255, 255)'\n      }, scales: {\n        yAxes: [{ \n          scaleLabel: {\n            display: true,\n            labelString: \"Latausteho [kW]\",\n            fontColor: 'rgb(255, 255, 255)'\n          }\n        }],\n        xAxes: [{ \n          scaleLabel: {\n            display: true,\n            labelString: \"SOC [%]\",\n            fontColor: 'rgb(255, 255, 255)'\n          }\n        }]\n      }\n    }\n});\n\n</script>\n","output":"str","x":850,"y":100,"wires":[["c42b1a5f.948b78","11b0ec5.6409c14"]]},{"id":"11b0ec5.6409c14","type":"ui_template","z":"86c9171.40beee8","group":"bfbd69a3.7f8268","name":"ChargingPlot","order":1,"width":12,"height":11,"format":"this will be ignored","storeOutMessages":false,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":1150,"y":100,"wires":[["dca3aba9.d2b7e8"]]},{"id":"7a065a8d.e1db44","type":"inject","z":"86c9171.40beee8","name":"","topic":"","payload":"","payloadType":"date","repeat":"60","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":100,"wires":[["9125df13.c99f4"]]},{"id":"bfbd69a3.7f8268","type":"ui_group","z":"","name":"ID3 latausgraafi","tab":"41d22295.86ebec","order":1,"disp":false,"width":"12","collapse":false},{"id":"41d22295.86ebec","type":"ui_tab","z":"","name":"ID3 latausgraafi","icon":"dashboard","order":3,"disabled":false,"hidden":false}]

One hour update plot

[{"id":"a596ec6f.f76ff","type":"function","z":"64ff8ec1.57aa1","name":"Build two cumulating random datasets","func":"// this is where you define the legend for \n// the datasets\nvar title = \"ID3 kokonaisrange vs. lämpötila\";\nvar legend1 = \"Kokonaisrange\";\n\n\n// variables to be used to \nvar i, x, y, r, data1;\n// initialise the counter to 0 if it doesn't exist already\nvar data1 = flow.get('data1')||'';\n\n\nvar count = flow.get('count')||0;\ncount += 1;\nx = global.get('ulkolampotila');\ny = global.get('ID3range')/global.get('ID3soc')*100;\nr = 1;\nif (count > 2000) {\n    count = 1;\n    data1 = '{x: \"x\",  y: \"y\",  r: \"r\"},';\n} \n// store the value back\nflow.set('count',count);\n// make it part of the outgoing msg object\nmsg.count = count;\n\n// for dataset #1\n// this code generates one x/y point \n// between -100 and +100 and a random \n//size between 1 and 10\n//node.warn(\"db1=\"+data1);\n\ndata1 += \"{x: \"+x+\", y: \"+y+\", r: \"+r+\"},\";\n//node.warn(\"db2=\"+data1);\n// this line removes the last comma to end the dataset\ndata1 = data1.replace(/^(.*),(.*?)$/, '$1')\n\n// Now we build msg.payload\nmsg.payload = { \"title\"   : title,\n                \"legend1\" : legend1,\n                \"data1\"   : data1}\n                \ndata1 += \",\";\n\n\nflow.set([\"data1\"], [data1]);\nreturn msg;\n\n","outputs":1,"noerr":0,"x":390,"y":100,"wires":[["968244b0.400ae8","64c23a17.8a2964"]]},{"id":"968244b0.400ae8","type":"template","z":"64ff8ec1.57aa1","name":"Create the msg.template to use","field":"template","fieldType":"msg","format":"html","syntax":"mustache","template":"<canvas id=\"bubble-chart\" width=\"1\" height=\"1\" style=\"border:1px solid #ffffff;\"></canvas>\n\n<script>\nnew Chart(document.getElementById(\"bubble-chart\"), {\n    type: 'bubble',\n    data: {\n      labels: \"label\",\n      datasets: [\n        {\n          label: \"{{{payload.legend1}}}\",\n          backgroundColor: \"rgba(255, 255, 255, 0.3000)\",\n          borderColor: \"rgba(255,255,255,0.3000)\",\n          data: [\n            {{{payload.data1}}}\n          ]\n        }\n      ]\n    },\n    options: {\n        animation: false,\n        legend: {\n            display: true,\n            labels: {\n                fontColor: 'rgb(255, 255, 255)'\n            }\n        },\n    title: {\n        display: true,\n        text: '{{{payload.title}}}',\n        fontColor: 'rgb(255, 255, 255)'\n      }, scales: {\n        yAxes: [{ \n          scaleLabel: {\n            display: true,\n            labelString: \"Kokonaisrange [km]\",\n            fontColor: 'rgb(255, 255, 255)'\n          }\n        }],\n        xAxes: [{ \n          scaleLabel: {\n            display: true,\n            labelString: \"Ulkolämpötila [C]\",\n            fontColor: 'rgb(255, 255, 255)'\n          }\n        }]\n      }\n    }\n});\n\n</script>\n","output":"str","x":810,"y":100,"wires":[["273d40fc.ac60a"]]},{"id":"cc56e7fe.b85638","type":"inject","z":"64ff8ec1.57aa1","name":"","topic":"","payload":"","payloadType":"date","repeat":"3600","crontab":"","once":false,"onceDelay":0.1,"x":70,"y":100,"wires":[["a596ec6f.f76ff"]]},{"id":"273d40fc.ac60a","type":"ui_template","z":"64ff8ec1.57aa1","group":"1551020.ad467fe","name":"TotalRange","order":1,"width":"0","height":"0","format":"this will be ignored","storeOutMessages":false,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","x":1140,"y":100,"wires":[[]]},{"id":"64c23a17.8a2964","type":"debug","z":"64ff8ec1.57aa1","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":700,"y":180,"wires":[]},{"id":"1551020.ad467fe","type":"ui_group","z":"","name":"ID3 kokonaisrange","tab":"90f09cb7.cba8c","order":1,"disp":true,"width":12,"collapse":false},{"id":"90f09cb7.cba8c","type":"ui_tab","z":"","name":"ID3 kokonaisrange","icon":"dashboard","order":11,"disabled":false,"hidden":false}]

Welcome to the forum @pelti51.

Have a look at the ui_control node. That will send a message every time a new tab is selected, including if the browser is refreshed. You could use that to trigger your refresh.

2 Likes

Excelent.That did the trick. Thanks for your fast responce and solution!