Search code examples
node-red

How to use D3 in Node-Red


I am using the Dashboard nodes but also want to show an animation. D3 would be perfect for the animation, therefore I an wondering if it is possible to fill a group of a dashboard tab in Node-Red with D3?

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js"></script>
<script src="https://rawgit.com/gka/d3-jetpack/master/d3-jetpack.js"></script>
<script>
var svg = d3.select("body")
    .append("svg")
    .attr("width", '800px')
    .attr("height", '800px');


var data = [{x: 100, y: 100, h: 10, w: 50, c: "red"}, {x: 200, y: 200, h: 20, w: 15, c: "yellow"}, {x: 300, y: 300, h: 60, w: 80, c: "green"}]

svg.selectAll("rect")
    .data(data)
    .enter().append("rect")
    .attr("x", function(d) { return d.x; })
    .attr("y", function(d) { return d.y; }) 
    .attr("width", function(d) { return d.w; })
    .attr("height", function(d) { return d.h; })
    .style("fill", function(d) { return d.c; })


//# sourceURL=userscript.js
</script>

Solution

  • In order to use external js libs from inside your ui_template code, it's best to put the src links and the html/css/svg code into a separate templates. In the first one, choose the "Add to site <head> section" option, and include your library references:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js"></script>
    <script src="https://rawgit.com/gka/d3-jetpack/master/d3-jetpack.js"></script>
    

    Then, add the other ui_template node to your dashboard tab/group, and include this code:

    <style>
        #myBoxes {
            height: 800px;
            width: 800px;
        }
    </style>
    
    <svg id="myBoxes">
        <!-- svg content goes here -->
    </svg>
    
    <script>
    (function(scope) { 
        // watch for msgs from node-red
        scope.$watch('msg.payload', function(payload) {
            drawBoxes("myBoxes", payload);
        })
    })(scope);
    
    function drawBoxes(id, data) {
        var svg = d3.select("#" + id);
        var rect = svg.selectAll("rect")
            .data(data);
    
        rect.enter()
            .append("rect");
    
        rect.attr("x", function(d) { return d.x; })
            .attr("y", function(d) { return d.y; }) 
            .attr("width", function(d) { return d.w; })
            .attr("height", function(d) { return d.h; })
            .style("fill", function(d) { return d.c; });
    
        rect.exit()
            .remove();
    }
    </script>
    

    I made several revisions to your sample code --

    • Use CSS styling for the fixed DOM elements
    • Create an empty <svg> element, with a unique id
    • Include the angular scope.$watch(...) to receive new msgs from node-red
    • Place the D3 logic inside a new function, called from the scope watcher
    • Use the D3 enter/update/exit selections to support object constancy

    Here is a sample flow with a few different inputs:

    [{"id":"7b4017ce.0802d8","type":"inject","z":"76a6ae8d.8a35","name":"red/white/blue","topic":"","payload":"[{\"x\":100,\"y\":100,\"h\":40,\"w\":50,\"c\":\"red\"},{\"x\":200,\"y\":200,\"h\":20,\"w\":75,\"c\":\"white\"},{\"x\":300,\"y\":300,\"h\":20,\"w\":75,\"c\":\"blue\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":210,"y":540,"wires":[["3adb8b4a.dbf3c4"]]},{"id":"3adb8b4a.dbf3c4","type":"ui_template","z":"76a6ae8d.8a35","group":"f2c8d56c.4d75b8","name":"D3 template","order":1,"width":0,"height":0,"format":"<style>\n    #myBoxes {\n        height: 800px;\n        width: 800px;\n    }\n</style>\n\n<svg id=\"myBoxes\">\n    <!-- svg content goes here -->\n</svg>\n\n<script>\n(function(scope) { \n    // watch for msgs from node-red\n    scope.$watch('msg.payload', function(payload) {\n        drawBoxes(\"myBoxes\", payload);\n    })\n})(scope);\n\nfunction drawBoxes(id, data) {\n    var svg = d3.select(\"#\" + id);\n    var rect = svg.selectAll(\"rect\")\n        .data(data);\n\n    rect.enter()\n        .append(\"rect\");\n    \n    rect.attr(\"x\", function(d) { return d.x; })\n        .attr(\"y\", function(d) { return d.y; }) \n        .attr(\"width\", function(d) { return d.w; })\n        .attr(\"height\", function(d) { return d.h; })\n        .style(\"fill\", function(d) { return d.c; });\n    \n    rect.exit()\n        .remove();\n}\n</script>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":510,"y":540,"wires":[[]]},{"id":"e1ab2277.a8f19","type":"ui_template","z":"76a6ae8d.8a35","group":"d215108d.8b51b","name":"D3 libraries","order":0,"width":0,"height":0,"format":"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js\"></script>\n<script src=\"https://rawgit.com/gka/d3-jetpack/master/d3-jetpack.js\"></script>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"global","x":510,"y":500,"wires":[[]]},{"id":"a4d0e22c.4689","type":"inject","z":"76a6ae8d.8a35","name":"black/white","topic":"","payload":"[{\"x\":100,\"y\":100,\"h\":40,\"w\":40,\"c\":\"black\"},{\"x\":200,\"y\":200,\"h\":40,\"w\":40,\"c\":\"white\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":200,"y":580,"wires":[["3adb8b4a.dbf3c4"]]},{"id":"e7af594f.9862d8","type":"inject","z":"76a6ae8d.8a35","name":"red/yellow/green","topic":"","payload":"[{\"x\":100,\"y\":100,\"h\":10,\"w\":50,\"c\":\"red\"},{\"x\":200,\"y\":200,\"h\":20,\"w\":15,\"c\":\"yellow\"},{\"x\":300,\"y\":300,\"h\":60,\"w\":80,\"c\":\"green\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":220,"y":500,"wires":[["3adb8b4a.dbf3c4"]]},{"id":"f2c8d56c.4d75b8","type":"ui_group","z":"","name":"D3","tab":"e3e3a35c.7a77f","order":2,"disp":true,"width":"12","collapse":false},{"id":"d215108d.8b51b","type":"ui_group","z":"","name":"Recalls","tab":"3c000bae.098b94","order":2,"disp":true,"width":"18"},{"id":"e3e3a35c.7a77f","type":"ui_tab","z":"","name":"Libraries","icon":"fa-bank","order":6},{"id":"3c000bae.098b94","type":"ui_tab","z":"","name":"Vehicles","icon":"fa-car"}]