Uibuilder with chart.js, Wait on msg from nodered

Dear sir, madam,

I'm kind of new to programming but does anyone know/ care to explain how to wait for msg from nodered before proceeding the function ? // I am using uibuilder
Currently I'm trying to make a chart using chart.js but due to lack of knowledge, I'm unable to figure out how wait for msg, before using the datasets (im getting error because it loads the datasets which is empty because it's not waiting for the msg), my current code is,

function syntaxHighlight(json) {
    json = JSON.stringify(json, undefined, 4);
    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])"(\s:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
        var cls = 'number';
        if (/^"/.test(match)) {
            if (/:$/.test(match)) {
                cls = 'key';
            } else {
                cls = 'string';
            }
        } else if (/true|false/.test(match)) {
            cls = 'boolean';
        } else if (/null/.test(match)) {
            cls = 'null';
        }
        return '<span class="' + cls + '">' + match + '</span>';
    });
    myChart.update();
    return json;
} // --- End of syntaxHighlight --- //
// run this function when the document is loaded
window.onload = function test(){
    // Start up uibuilder - see the docs for the optional parameters
    uibuilder.start();
    // Listen for incoming messages from Node-RED
    uibuilder.onChange('msg', function(msg){
        //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg);
        // dump the msg as text to the "msg" html element
        const eMsg = document.getElementById('msg');
        const actMsg = msg;
        globalvar = msg.payload[0].data[0][1];
       // eMsg.innerHTML = syntaxHighlight(msg);
        console.log(globalvar);
    });
var globalvar;
const test = globalvar;
console.log(test);
};
var myChart = document.getElementById('myChart').getContext('2d');
var myChart2 = new Chart(myChart, {
    type: 'line',
    data: {
        labels: [],
        datasets: [{
            label: '# of Votes',
            data: [12, 19, 3, 5, 2, 3],
            backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
            ],
            borderColor: [
                'rgba(255, 99, 132, 1)',
                'rgba(54, 162, 235, 1)',
                'rgba(255, 206, 86, 1)',
                'rgba(75, 192, 192, 1)',
                'rgba(153, 102, 255, 1)',
                'rgba(255, 159, 64, 1)'
            ],
            borderWidth: 1
        }]
    },
       options: {
            scales: {
                xAxes: [{
                    type: 'time',
                     ticks: {
        autoSkip: true,
        maxTicksLimit: 10
                      },
                    time: {
                        unit: 'minute',
                        unitStepSize: 1,
                        displayFormats: {
                            minute: 'HH:mm'
                        }
                    }
                }],
                yAxes: [{
                    id: "1",
                    ticks: {
                        min: 0,
                        max: 60,
                    }
                }, {
                    id: "2",
                    ticks: {
                        min: -2,
                        max: 2,
                        stepSize: 0.4
                    }
                }]
            },
            animation: {
                duration: 0
            }
        }
});

And im using this data
image

Hi, welcome to the forum.

The uibuilder front-end library is event driven. This means that you create a function that waits for an event to happen. That is done by using the uibuilder.onChange function.

So your example code will update globalvar, and actMsg every time Node-RED sends a message to the uibuilder node.

So you will need to replace the manual set of data in myChart2 with the data coming from Node-RED if you are sending the full data each time. If you are just sending a new value each time, you need to push the new value to the data each time.

Something like:

const myData = []  // Use let instead of const if you are sending all of the data points on each update

const myChart = document.getElementById('myChart').getContext('2d');
const myChart2 = new Chart(myChart, {
    type: 'line',
    data: {
        labels: [],
        datasets: [{
            label: '# of Votes',
            data: myData,
    // ... etc ...
})

window.onload = function go() {
    uibuilder.start()

    uibuilder.onChange('msg', function(msg){
        // If only sending a single data point each time:
        myData.push( msg.payload )
        // If sending the full data each time:
        // myData = msg.payload
    });
}

Not sure if you need to do anything to force the chart to redraw when data is changed. Some chart libraries may have their own update function that you can just send the data to. In that case, replace the manual update in the onChange function with the chart update function.

After updating the data, call myChart.update() See: Updating Charts | Chart.js

2 Likes

Great, yes that was the term i was looking for, "push". When i manually force this kind of data into global, it most likely won't work due to it being constant called by the chart.

Here my solution was instead of using global data, i used push data function.

window.onload = function go(){

    // Start up uibuilder - see the docs for the optional parameters
    uibuilder.start();
    // Listen for incoming messages from Node-RED
    uibuilder.onChange('msg', function(msg){
        fetchData();
        function fetchData(){
        //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg);
        // dump the msg as text to the "msg" html element
        const eMsg = document.getElementById('msg');
        const actMsg = msg;
  
        
        //Data gets pushed here into the graph. Here you can specify which data goes into what set
        
        
    
        
        myChart.config.data.datasets[0].data.push(msg.payload[0].data[0][0]);
        
        
        myChart.update();
 
    }
        

    });
    



};

Complete example - shows a tidier version of the code which will be easier to grok in the future:

html

<!doctype html>
<html lang="en"><head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Chartjs - Node-RED uibuilder</title>
    <meta name="description" content="Node-RED uibuilder - Chartjs">
    <link rel="icon" href="./images/node-blue.ico">

    <link type="text/css" rel="stylesheet" href="./index.css" media="all">

</head><body class="uib">
    
    <h1>uibuilder Chartjs</h1>

    <canvas id="myChart" style="background-color:white;width:50%;height:50%;"></canvas>

    <pre id="msg" class="syntax-highlight">Waiting for a message from Node-RED</pre>

    <script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="./uibuilderfe.min.js"></script>
    <script src="./index.js"></script>

</body></html>

js

/* eslint-disable strict */
/* jshint browser: true, esversion: 6, asi: true */
/* globals uibuilder */
// @ts-nocheck

/** Minimalist code for uibuilder and Node-RED */
'use strict'

const labels = [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
]

const myData = [0, 10, 5, 2, 20, 30, 45]

const data = {
    labels: labels,
    datasets: [{
        label: 'My First dataset',
        backgroundColor: 'rgb(255, 99, 132)',
        borderColor: 'rgb(255, 99, 132)',
        data: myData,
    }]
}

const config = {
    type: 'line',
    data: data,
    options: {}
}

// return formatted HTML version of JSON object
window.syntaxHighlight = function (json) {
    json = JSON.stringify(json, undefined, 4)
    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
        var cls = 'number'
        if (/^"/.test(match)) {
            if (/:$/.test(match)) {
                cls = 'key'
            } else {
                cls = 'string'
            }
        } else if (/true|false/.test(match)) {
            cls = 'boolean'
        } else if (/null/.test(match)) {
            cls = 'null'
        }
        return '<span class="' + cls + '">' + match + '</span>'
    })
    return json
} // --- End of syntaxHighlight --- //

// Send a message back to Node-RED
window.fnSendToNR = function fnSendToNR(payload) {
    uibuilder.send({
        'topic': 'msg-from-uibuilder-front-end',
        'payload': payload,
    })
}

// run this function when the document is loaded
window.onload = function () {
    // Start up uibuilder - see the docs for the optional parameters
    uibuilder.start()

    const myChart = new Chart(
        document.getElementById('myChart'),
        config
    )

    // Listen for incoming messages from Node-RED
    uibuilder.onChange('msg', function (msg) {
        console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)

        // dump the msg as text to the "msg" html element
        const eMsg = document.getElementById('msg')
        eMsg.innerHTML = window.syntaxHighlight(msg)

        labels.push( msg.payload[0] )
        myData.push( msg.payload[1] )
        myChart.update()
    })

}

From node-red, send a msg like:

{
    "payload": [
        "july",
        24
    ]
}
1 Like

thank you! I will for sure make use of this template as an reference to tidy up my messy code :smile: ! And I will make use of this template for future development!

1 Like

and I'll try to find time to turn a more complete example into a uibuilder external template. Thanks for raising because it actually makes for a nice example. Took just a few minutes to expand it so that you can use msg.topic to provide different data/options, add datasets, etc. :beers:

/* eslint-disable strict */
/* jshint browser: true, esversion: 6, asi: true */
/* globals uibuilder */
// @ts-nocheck

/** Minimalist code for uibuilder and Node-RED */
'use strict'

let chartType = 'line'

const chartConfig = {
    type: chartType,
    data: {
        //labels: xLabels, <= better to use the x/y data input than global labels for many things
        datasets: [],
    },
    options: {},
}

// return formatted HTML version of JSON object
window.syntaxHighlight = function (json) {
    json = JSON.stringify(json, undefined, 4)
    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
        var cls = 'number'
        if (/^"/.test(match)) {
            if (/:$/.test(match)) {
                cls = 'key'
            } else {
                cls = 'string'
            }
        } else if (/true|false/.test(match)) {
            cls = 'boolean'
        } else if (/null/.test(match)) {
            cls = 'null'
        }
        return '<span class="' + cls + '">' + match + '</span>'
    })
    return json
} // --- End of syntaxHighlight --- //

// Send a message back to Node-RED
window.fnSendToNR = function fnSendToNR(payload) {
    uibuilder.send({
        'topic': 'msg-from-uibuilder-front-end',
        'payload': payload,
    })
}

// run this function when the document is loaded
window.onload = function () {
    // Start up uibuilder - see the docs for the optional parameters
    uibuilder.start()

    const myChart = new Chart(
        document.getElementById('myChart'),
        chartConfig,
    )

    // Listen for incoming messages from Node-RED
    uibuilder.onChange('msg', function (msg) {
        console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)

        // dump the msg as text to the "msg" html element
        const eMsg = document.getElementById('msg')
        eMsg.innerHTML = window.syntaxHighlight(msg)

        // ! WARNING: This is terribly simplistic! Add better checks on the input data.
        switch (msg.topic) {
            // Send all chart options (everything is replaced)
            case 'options': {
                chartConfig.options = msg.payload
                break;
            }

            // Clear the entire chart
            case 'clear': {
                // TBC
                break;
            }

            // Remove a dataset
            case 'delete-dataset': {
                // TBC
                break;
            }

            // Send a new dataset (e.g. a new line on the chart)
            case 'new-dataset': {
                chartConfig.data.datasets.push( msg.payload )
                break;
            }

            // Send all the values for a specified dataset - can also send other options (the DS is completely replaced)
            case 'dataset': {
                if ( ! msg.dsIndex ) {
                    /** Show a msg if the dataset index not specified
                     * @argument {string} Text to show
                     * @argument {string|null} Location to show the toast (Vue/Bootstrap-vue only)
                     * @argument {object|null} Options that control the toast display
                     */
                    uibuilder.toast("msg.dsIndex must be provided in input message",null,{"variant": "warn", "title": "Replace Dataset"})
                    break
                }
                chartConfig.data.datasets[msg.dsIndex] = msg.payload
                break;
            }
        
            // Send a NEW VALUE for a specific dataset (ds defaults to index=0)
            case 'data':
            default: {
                if ( ! msg.dsIndex ) {
                    uibuilder.showToast("msg.dsIndex must be provided in input message",null,{"variant": "warn", "title": "New/Update Data"})
                    break
                }

                if ( ! chartConfig.data.datasets[msg.dsIndex] ) {
                    uibuilder.showToast("msg.dsIndex # does not exist in chart",null,{"variant": "warn", "title": "New/Update Data"})
                    break
                }

                Object.keys(msg.payload).forEach( key => {
                    chartConfig.data.datasets[msg.dsIndex].data[key] = msg.payload[key]
                })
                break;
            }
        }

        myChart.update()
    })

}
1 Like

Oh, and I missed one off the switch statement which is particularly nice:

            // Set/change the chart type
            case 'type': {
                chartConfig.type = msg.payload
                break;
            }

You can use that to dynamically switch between chart types while keeping the same data :grinning:

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.