Struggling with D3.js chart in UIBUILDER page

Hi I'm using UIBuilder 6.8.2 (along with NR 3.1.3) and think its a fantastic piece of code, really appreciate your efforts. I finally got it to run D3 to build a bar chart and using uibuilder.onChange managed to update dynamically from my MQTT service. But now I have a app that has multiple pages so I change out #more wholesale with a template and a uib-update, ok works beautifully. But now after many days of trial and error and hairpulling, I can't seem to inject the HTML and render the D3 chart,. If I make it permenant before #more then all is good, but injecting does not, of course this means its on every page.

The bar chart is correctly drawn between tags.

Tried using a template node, element node but the svg never gets generated

image

Any help appreciated.

Hi, moved to separate topic for clarity.

If I've understood you correctly. You are generating the D3 HTML in Node-RED and not in the client?

Is there a reason for that?

Where I've used D3, I generate the outputs at the client since D3 is a front-end JavaScript library. I simply send the data to the client using the uibuilder node.

Hi, I generate it at the client using a .js file. But what I found is that if its generated as part of the static text of uibuilder node it gets generated but when I inject it into the html using a uib-update node it doesn't get generated.

This one works:

working.json (8.3 KB)

Then move all code between #more div into uib-update

not_working.json (9.8 KB)

I will check later when I have some more time. But at a guess, you are trying to send a load of js as an HTML injection? If that is the case, it cannot work with UIBUILDER v6. JS injection is, of necessity, restricted by browsers for safety reasons.

As it happens, v7 does have that capability though. Not quite ready for release I'm afraid.

I'll look at your flows and see if I can find a better approach.

index.html is incorrect (in the not_working json):

<!DOCTYPE html>
<html>
<!-- <script src="https://d3js.org/d3.v7.js"></script> -->
<script src="../uibuilder/vendor/d3/dist/d3.js"></script>
<script src="../uibuilder/uibuilder.iife.min.js"></script>
<!-- Your own CSS -->
<link type="text/css" rel="stylesheet" href="./index.css" media="all">
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
  <title>Bar chart with D3.js</title>
  
  <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">

</head>
<body>
  <div id="more">

  </div>
  <script defer src="./index.js"></script>
</body>
</html>

You have things outside the <head>. Also, you HAVE to be consistent with the use of defer. As it is, it will mess up the order of loading.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
  <title>Bar chart with D3.js</title>
  
  <link type="text/css" rel="stylesheet" href="./index.css" media="all">
  <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">

  <!-- <script src="https://d3js.org/d3.v7.js"></script> -->
  <script defer src="../uibuilder/vendor/d3/dist/d3.js"></script>
  <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
  <script defer src="./index.js"></script>

</head>
<body>
  <div id="more"></div>
</body>
</html>

There is also an error in your CSS file. // @import url("../uibuilder/uib-brand.min.css"); must be /* @import url("../uibuilder/uib-brand.min.css"); */, CSS is not JS and does not use quite the same comment style.

Next you have problems in your JS:

image

Same happens again on line 51.

This is because you need to define let sample = [] in index.js and update it when you send the template (though see below for why this is a bad idea anyway).

If you use your browser's dev tools, you will see it highlight errors for you.


Now, lets fine a much better way to approach this which is a LOT simpler and uses UIBUILDER's features properly.

At the top of your index.js, put this:

let sample = []
uibuilder.onChange('msg', (msg) => {
    sample = msg.payload;
})

Get rid of the template and uib-update nodes because they are not needed. Change the inject to inject the data directly as JSON.

[{"id":"7f0b5bc815770786","type":"inject","z":"44ceec9d5e933a9b","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"language\":\"Rust\",\"value\":68.9,\"color\":\"#000000\"},{\"language\":\"Kotlin\",\"value\":75.1,\"color\":\"#00a2ee\"},{\"language\":\"Python\",\"value\":68,\"color\":\"#fbcb39\"},{\"language\":\"TypeScript\",\"value\":67,\"color\":\"#007bc8\"},{\"language\":\"Go\",\"value\":65.6,\"color\":\"#65cedb\"},{\"language\":\"Swift\",\"value\":65.1,\"color\":\"#ff6e52\"},{\"language\":\"JavaScript\",\"value\":61.9,\"color\":\"#f9de3f\"},{\"language\":\"C#\",\"value\":60.4,\"color\":\"#5d2f8e\"},{\"language\":\"F#\",\"value\":59.6,\"color\":\"#008fc9\"},{\"language\":\"Clojure\",\"value\":59.6,\"color\":\"#507dca\"}]","payloadType":"json","x":350,"y":1780,"wires":[["31f722edf26b6019"]]}]

Now, you don't have any errors and you are only sending DATA to the front end, not HTML.

But it still doesn't work, you will always have an empty chart! But now that is because I don't think you are using D3 correctly. You are not telling D3 to update the chart when it receives new data - I think there may be several approaches, here is one: d3.js - D3: How to refresh a chart with new data? - Stack Overflow

I don't do enough with D3 to help you further on that part.

The bottom line here is:

Don't send HTML when DATA is sufficient.

1 Like

So after many many late nights seems like anything that I inject between script tags using
uib-update doesn't execute. If I inject just a simple

<script>
     alert("hi 1");
 </script>

just after #more nothing. Tried many iterations of different variations.

As previously mentioned:

It will work with UIBUILDER v7 because I discovered the magic sauce that allows it to work.

The answer, as outlined in the previous post, the better way to approach this issue is not to send the script, send the data and process the data in the browser.

OK here's a workaround for anyone in my situation needing to get a product out of the door and can't wait for the much anticipated (and very much appreciated!) V7 from @TotallyInformation
My app is an industrial HMI relies heavily on many libraries like D3.js and dynamically generated SVG's using JS to animate them, with 8 or so menu pages. I started down the route of changing out the whole body with the uib-update, but as you can see from the above there's some challenges in V6.
So the workaround which works great is to load all pages into one huge index.html file and add div id's like this:

<button id="btn1" class="menubutton">Overview</button
<button id="btn2" class="menubutton">Monitoring</button>
     .... etc for rest of menu...      

<div id="content1" class="content">
    Content of page 1 goes here.
 </div>

 <div id="content2" class="content">
    Content of page 2 goes here.
 </div>

Then use js in index.js like this:

// Function to handle button click
function handleButtonClick(event) {
    // Select all buttons with the class 'menubutton'
    var buttons = document.querySelectorAll('.menubutton');

    // Remove the 'active' class from all buttons
    buttons.forEach(function(button) {
        button.classList.remove('active');
    });

    // Add the 'active' class to the clicked button
    event.currentTarget.classList.add('active');

    // Get the id of the clicked button
    var buttonId = event.currentTarget.id;

    // Determine which content div to show based on the button id
    var contentId = 'content' + buttonId.slice(-1); // Extract the number and create corresponding content id

    // Hide all content divs
    var contents = document.querySelectorAll('.content');
    contents.forEach(function(content) {
        content.style.display = 'none';
    });

    // Show the corresponding content div
    document.getElementById(contentId).style.display = 'block';
}

// Attach event listeners to each button
var buttons = document.querySelectorAll('.menubutton');
buttons.forEach(function(button) {
    button.addEventListener('click', handleButtonClick);
});

This effectively just "hides" the unwanted blocks other than the clicked menu.

And just to initialise use CSS:

.content {
  display: none; /* Hide all content divs by default */
}

Then as @TotallyInformation advised, inject the updates through msg.payload, just split out with a big if else statement using msg.topic for example to update the values:

uibuilder.onChange('msg', (msg) => {
    if (msg.topic == 'configuration'){
        // Locate the input element with class 'input' and name 'host'
        var inputElement = document.querySelector('.input[name="host"]');
        if (inputElement) {
            inputElement.value = msg.payload.connections.host;
        }
        inputElement = document.querySelector('.input[name="port"]');
        if (inputElement) {
            inputElement.value = msg.payload.connections.port;
        }
    }

One of the beneficial side effects of this approach is that its much easier to make changes in VSCode for example rather than using the uib-update text editor.
There is probably better ways to do this and open for improvements, but time was against me.

Hope this helps someone.

2 Likes

You could also use the included uib-router library if you want to split things up a bit more and to automate the show/hide parts. The basic router is part of v6.

The main limitation here was trying to send JavaScript front-end code from Node-RED. This is possible in v6 but not as convenient as with v7. But in general, you should try to avoid sending code to be executed in the front-end. Keep the front-end code static and send only data. This is best practice for any web app.

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