How to set the function to wait until libraries loaded

Hello,

I am using vega charts on my dashboard and certain users are on Wi-Fi and sometimes charts can't load in time, so they get a blank widget.

This is the code without timeout:

<html>
    <head>
        <meta charset="utf-8">
        <script src="http://localhost:1880/vega.js"></script>
        <script src="http://localhost:1880/vega-embed.js"></script>
    </head>
    <body>  
        <div id="vis" style="height:500px"></div>
        <script>
            (function(scope) {
                scope.$watch('msg.vega', function(data) {
                    const spec = data;
                    vegaEmbed("#vis", spec).then(result => console.log(result)).catch(console.warn);

                });
            })(scope);
        </script>
    </body>
</html>

I used to have a timeout:

                    setTimeout(()=> {

                    }, 500)

But sometimes it is not enough - and I wouldn't like to increase this time because then the charts would load too slow.

How to make it so the vega charts are sent to the dashboard only when all the libraries are loaded?

Hello Tomislav,

I was wondering about this also when i was testing the code in your thread for the Apexcharts

you can try creating a seperate ui_template and set its Template type : to add to head section

image

and move in this new node just the script tags of your libaries
hopefully this way they should load first before the rest of the code in the main ui_template

Upon further testing - I found out that it doesn't load every time (I've used the CDN online libraries to simulate the lag).

So, we need a javascript solution to the problem :frowning:

OK, this libraries are placed in the head section:

<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@5"></script>

This means they are only loaded once?
So If I don't clear cache in my browser, node-red shouldn't load them again?

Next,

The code for the widget stays the same:

<html>
    <body>  
        <div id="vis" style="height:500px"></div>
        <script>
            (function(scope) {
                scope.$watch('msg.vega', function(data) {
                    const spec = data;
                    vegaEmbed("#vis", spec).then(result => console.log(result)).catch(console.warn);

                });
            })(scope);
        </script>
    </body>
</html>

Next, I have found many topics regarding this, but I'm not sure how to check the scripts are loaded, and pause the execution of the vegaEmbed..

jQuerry:

My scripts do always load, but sometimes I need only the time pause..

i didnt find a simple way of doing this either .. i studied your link but its beyond my programming skills .
i found one example of script loading on the MDN website

[{"id":"f6b104981899da0e","type":"ui_template","z":"5847b7aa62131d37","group":"cecf42695640eca4","name":"","order":0,"width":0,"height":0,"format":"<div id=\"vis\" style=\"height:500px\"></div>\n<script>\n\nlet nr = scope;\n\nfunction loadError(oError) {\n  throw new URIError(\"The script \" + oError.target.src + \" didn't load correctly.\");\n}\n\nfunction affixScriptToHead(url, onloadFunction) {\n  var newScript = document.createElement(\"script\");\n  newScript.onerror = loadError;\n  if (onloadFunction) { newScript.onload = onloadFunction; }\n  document.head.appendChild(newScript);\n  newScript.src = url;\n}\n\n// main code runs with this function\nfunction myJS() {\nnr.$watch('msg.vega', function(data) {\n    const spec = data;\n    console.log(\"Mesg received from NR\", spec)\n    \n    vegaEmbed(\"#vis\", spec).then(result => console.log(result)).catch(console.warn);\n\n});\n}\n\naffixScriptToHead(\"https://cdn.jsdelivr.net/npm/vega@5\");\naffixScriptToHead(\"https://cdn.jsdelivr.net/npm/vega-lite@5\");\naffixScriptToHead(\"https://cdn.jsdelivr.net/npm/vega-embed@6\",  () => { \n  console.log(\"The scripts has been correctly loaded.\");\n  nr.send({topic: \"FROM UI\", payload : \"The scripts has been correctly loaded.\"})\n // console.log(this);\n  //console.log(nr);\n  console.log(vegaEmbed)\n  myJS()  // run main function after scripts\n  })\n\n\n\n</script>\n\n\n\n\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","className":"","x":750,"y":640,"wires":[["b7e50c3d856e85c5"]]},{"id":"b7e50c3d856e85c5","type":"debug","z":"5847b7aa62131d37","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":960,"y":640,"wires":[]},{"id":"66d1189f909dbae5","type":"inject","z":"5847b7aa62131d37","name":"","props":[{"p":"payload"},{"p":"vega","v":"[1,2,3,4]","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":540,"y":640,"wires":[["f6b104981899da0e"]]},{"id":"cecf42695640eca4","type":"ui_group","name":"Default","tab":"39a6d442788cfb84","order":1,"disp":true,"width":"24","collapse":false,"className":""},{"id":"39a6d442788cfb84","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

the above doesnt look very clean ..
maybe a simple setInterval that clears when the Vega library is loaded ?

<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>

<div id="vis" style="height:500px"></div>
<script>

let scriptCheck = setInterval( () => {
console.log("Checking Scripts")
if (vegaEmbed) {
  console.log("Vega Scripts Loaded")
  clearInterval(scriptCheck);
  }

}, 200)

</script>
1 Like

I have no idea if this will help but have you tried the JQuery document ready $(function() {

<script>
    (function(scope) {
      // Important. Use JQuery document ready function so that the Unique id is created & loaded
      $(function() {

@Buckskin i didnt manage to make it work .. do you have a sample code ?

I had another go with getScript .. seems it can work .. but with those callbacks its getting uglier with every call :sweat_smile:


<script>

let theScope = scope;
 
$.getScript("https://cdn.jsdelivr.net/npm/vega@5", function (data, textStatus, jqxhr) {
  console.log("Script1", textStatus, jqxhr.status, "Load was performed."); // Success

  $.getScript("https://cdn.jsdelivr.net/npm/vega-lite@5", function (data, textStatus, jqxhr) {
    console.log("Script2", textStatus, jqxhr.status, "Load was performed."); // Success

    $.getScript("https://cdn.jsdelivr.net/npm/vega-embed@6", function (data, textStatus, jqxhr) {
      console.log("Script3", textStatus, jqxhr.status, "Load was performed."); // Success
      
      main() // run main

    });
  });
});

// Main code goes here //
let main = () => {
    console.log( "ready!" );
    theScope.send( { payload: "ready!" } );   // send msg to NR
    console.log( vegaEmbed );
};

</script>
1 Like

Try something like this:

<head>
        <script defer="true" src="https://cdn.jsdelivr.net/npm/vega@5">></script>
        <script defer="true" src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
        <script defer="true" src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
</head>

<body>
        
        <div id="vis" style="height:500px">{{load}}</div>
    
    <script>
        (function(scope) {
            // Important. Use JQuery document ready function so that the Unique id is created & loaded
                $(function() {

                    scope.load = 'I am not here yet'

                    if (vegaEmbed('#vis')) {
                        console.log("Vega Scripts Loaded")

                        scope.load = 'Here I am'

                    }

                })

        })(scope)

    </script>

</body>

I get 'Here I am' in the Group space on screen but without any data I cannot tell if it is actually working

I can't seem to stop the setInterval function - it runs indefinately..

Sometimes it loads, sometimes it doesn't - Its like before, and I get some errors:

VM1823:1 Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 0
    at JSON.parse (<anonymous>)
    at vega-embed@5:1
    at Generator.next (<anonymous>)
    at a (vega-embed@5:1)

Thank you! It seems it works - I'll test it on the production Pi right now.
There is only 1 yellow error, but I think this is vega related:

TypeError: Cannot read properties of undefined (reading 'usermeta')
    at vega-embed@5?_=1636637716662:1
    at Generator.next (<anonymous>)
    at vega-embed@5?_=1636637716662:1
    at new Promise (<anonymous>)
    at ce (vega-embed@5?_=1636637716662:1)
    at at (vega-embed@5?_=1636637716662:1)
    at lt (vega-embed@5?_=1636637716662:1)
    at <anonymous>:28:21
    at m.$digest (app.min.js:174)
    at m.$apply (app.min.js:177)
let main = () => {
    console.log( "ready!" );
    theScope.send( { payload: "ready!" } );   // send msg to NR
    console.log( vegaEmbed );
    theScope.$watch('msg.vega', function(data) {
        const spec = data;
        vegaEmbed("#vis", spec, {"actions": {export: true, source: false, editor: false}}).then(result => console.log(result)).catch(console.warn);
    }); 
};

On live Pi, this code does run charts load correctly the first time.
First problem is It does not get my refresh message, so it is blank until new message appears.
The original does load with my refresh message:

        <meta charset="utf-8">
        <script src="http://192.168.x.xxx:1880/vega.js"></script>
        <script src="http://192.168.x.xxx:1880/vega-embed.js"></script>
        <div id="vis" style="height:500px"></div>
        <script>
            (function(scope) {
                scope.$watch('msg.vega', function(data) {
                    const spec = data;
                    setTimeout(()=> {
                      	vegaEmbed("#vis", spec, {"actions": {export: true, source: false, editor: false}})
                          .then(result => console.log(result))
                          .catch(console.warn);
                    }, 500) 
           
                });
            })(scope);
        </script>

The bigger problem is that every next load (cached) it throws this error:

VM2987:1 Uncaught SyntaxError: Identifier 'theScope' has already been declared
    at b (app.min.js:20)
    at Ie (app.min.js:20)
    at k.fn.init.append (app.min.js:20)
    at k.fn.init.<anonymous> (app.min.js:20)
    at _ (app.min.js:20)
    at k.fn.init.html (app.min.js:20)
    at app.min.js:596
    at m.$digest (app.min.js:172)
    at m.$apply (app.min.js:176)
    at app.min.js:593

And stops any further chart loads until I clear the cache from this page.

We dont have your chart data or your flow so we cannot reproduce it.

yes, but its just a warning because this line :arrow_down: sends a msg regardless. so i dont think it blocks the rest of the code.

theScope.send( { payload: "ready!" } );   // send msg to NR

I give up .. its something with the inner workings of AngularJS injecting html, scripts etc that need 5 years experience in angular to figure it out .. hint hint ..

Use ui_builder if you dont already have anything else on the Dashboard

I think your code is the solution, but unfortunately I am at a loss with javascript (just realized I was working with plain NodeJS and html most of the time - I would like to learn through projects, though).

What I was about to say is: the code throws an error and stops when tab switching.
Try this tab switching flow: flows (64).json (2.4 KB)

Regarding my dashboard - It's a production monitoring mdashboard created in 06/2020 and used for monitoring the total of 43 signals from 9 machines. Downloaded flow size is 2,9Mb - so switch to uibuilder should be version 2.0 (project of its own).

Thank you in advance

Sorry I was being lazy - the solution was to declare theScope with var (because let is a block scoped), and to remove declaration of the main callback.

Now, only to get the chart to load on my refresh....

1 Like

Ok I've got it. Thanks all.

if (msg.payload === "ready!") {
    msg = {};
    msg.topic = "refresh";
    return msg;
}

Hello Tomislav,

nice find regarding that difference between let and var :+1:

i was reading on Dashboard github and i found a post with a person asking similar question ..
he also went with the setInterval approach to check whether the scripts were loaded.

Here it is if you want to read

Inject js in page

1 Like

Here's another ugly example using an array of Promises that waits for the loading of the scripts to resolve

[{"id":"d52180de48148c7f","type":"ui_template","z":"5847b7aa62131d37","group":"62350f50d4b09ff8","name":"Script Loading - Promises","order":21,"width":"2","height":1,"format":"<script>\n    (function (scope) {\n\n        var scope = scope\n\n        function loadScript(url) {\n            return new Promise(function (resolve, reject) {\n                let script = document.createElement('script');\n                script.src = url;\n                script.async = false;\n                script.onload = function () {\n                    resolve(url);\n                };\n                script.onerror = function () {\n                    reject(url);\n                };\n                document.body.appendChild(script);\n            });\n        }\n\n        let scripts = [\n            'https://cdn.jsdelivr.net/npm/vega@5',\n            'https://cdn.jsdelivr.net/npm/vega-lite@5',\n            'https://cdn.jsdelivr.net/npm/vega-embed@6'\n        ];\n\n        // save all Promises as array\n        let promises = [];\n        scripts.forEach(function (url) {\n            promises.push(loadScript(url));\n        });\n\n        Promise.all(promises)\n            .then(function () {\n                console.log('all scripts loaded');\n                //console.log('scope', scope);\n                scope.$watch('msg', function (msg) {\n                    if (msg) scope.send({ topic: \"Vega loaded and received msg from NR !!!\", payload: msg.payload })\n                });\n\n            }).catch(function (script) { console.log(script + ' failed to load'); });\n\n\n    })(scope)\n\n</script>","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","className":"","x":670,"y":620,"wires":[["d1e9cc937a2a6269"]]},{"id":"d1e9cc937a2a6269","type":"debug","z":"5847b7aa62131d37","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":870,"y":620,"wires":[]},{"id":"ba8d624f24041fd3","type":"inject","z":"5847b7aa62131d37","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":440,"y":620,"wires":[["d52180de48148c7f"]]},{"id":"62350f50d4b09ff8","type":"ui_group","name":"Promises","tab":"554e77d51ad13f2d","order":1,"disp":false,"width":"24","collapse":false,"className":""},{"id":"554e77d51ad13f2d","type":"ui_tab","name":"","icon":"dashboard","order":1,"disabled":false,"hidden":false}]