Using d3.js both on the front end and server for generating images with node.js

D3.js is a great javascript lib for creating visualisations based on given data.

Here's a really really simple example creating different sized circles based on the data given:

& here's that same code in an index.html file:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">
    <title>Demo visualisation</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>  
<body>

<div id="my-visualisation"></div>

<script>

var data = [3, 5, 8, 4, 7];

// create the outer svg
var svg = d3.select('#my-visualisation')  
            .append('svg')
              .attr('height', 100)
              .attr('width', 500);

// append circles for each data point sized relative to the value
svg.selectAll('circle')  
    .data(data)
    .enter()
        .append('circle')
        .attr('cx', function (d, i) {
            return (i + 1) * 100 - 50;
        })
        .attr('cy', svg.attr('height') / 2)
        .attr('r', function (d) {
            return d * 5;
        });

</script>

</body>  
</html>  

But, what if we need it as an image?

Node.js to the rescue, along with how awesome d3 is, it currently (at least at the point of me writing this) packages with jsdom, this allows us to use it in node without much hassel.

So first, lets take our previous html and abstract the js to its own file we can share:

index.html:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">
    <title>Demo visualisation</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>  
<body>

<div id="my-visualisation"></div>

<script src="my-visualisation.js"></script>

</body>  
</html>  

my-visualisation.js:

var data = [3, 5, 8, 4, 7];

// create the outer svg
var svg = d3.select('#my-visualisation')  
            .append('svg')
              .attr('height', 100)
              .attr('width', 500);

// append circles for each data point sized relative to the value
svg.selectAll('circle')  
    .data(data)
    .enter()
        .append('circle')
        .attr('cx', function (d, i) {
            return (i + 1) * 100 - 50;
        })
        .attr('cy', svg.attr('height') / 2)
        .attr('r', function (d) {
            return d * 5;
        });

Easy, and quick check in the browser confirms it still works.

Next we need to setup a simple node.js file to read this above file and output it's <svg> content.

So first we setup npm init, it'll ask a few questions but for now i've just hit enter on each for the defaults.

Next we install d3 with npm install --save d3.

Once installed we can begin our script, for this i'll just setup an index.js file for now which will output the svg. We could make this into a server/service we could make requests to to generate but for this demo this should be enough.

So, in our index.js we want to bring in the my-visualisation.js file, and output to the terminal.

With d3.js, we can grab the dom svg node with: svg.node() and then call .outerHTML to get the entire <svg... html string.

index.js:

var svg = require('./my-visualisation.js');

// output the entire <svg> element
console.log(svg.node().outerHTML);  

We could improve this by bringing in the fs module and calling fs.writeFile('filename.svg', svg.node().outerHTML); to output the file, but for now we'll keep it simple and just have it output to the terminal, we can always redirect this stdout in bash later anyway.

Next we need to make a couple minor changes to our my-visualisation.js file in order for it to use the node d3 package, and for it to return the svg object.

To do this we can add the following to the top of the file: (to require d3 when it's not found)

// bring in d3 if its not globaly available (e.g. browser side usage)
var d3 = d3 || require('d3');  

and the following the bottom of the file to export svg when required from another file in node.js:

// Export the svg
if (typeof module !== 'undefined' && module.exports) {  
    module.exports = svg;
}

Now we are almost done, although we have jsdom taking care of the document, we don't have that <div id="my-visualisation"></div> element we had before. So we could go back and install jsdom and use it to use out html as a stub and build a document off that, but let's do this a little simpler for now, and change our svg to be generated on #my-visualisation for the browser, but on the body for our node.js script.

To do this we can add:

var element =  typeof module !== 'undefined' && module.exports ? 'body' : '#my-visualisation';  

and change our svg to be built off the element var

var svg = d3.select(element)  

So our end my-visualisation.js file should look like this:

// bring in d3 if its not globaly available (e.g. browser side usage)
var d3 = d3 || require('d3');

var element =  typeof module !== 'undefined' && module.exports ? 'body' : '#my-visualisation';

var data = [3, 5, 8, 4, 7];

// create the outer svg
var svg = d3.select(element)  
            .append('svg')
              .attr('height', 100)
              .attr('width', 500);

// append circles for each data point sized relative to the value
svg.selectAll('circle')  
    .data(data)
    .enter()
        .append('circle')
        .attr('cx', function (d, i) {
            return (i + 1) * 100 - 50;
        })
        .attr('cy', svg.attr('height') / 2)
        .attr('r', function (d) {
            return d * 5;
        });

// Export it
if (typeof module !== 'undefined' && module.exports) {  
    module.exports = svg;
}

Now we can simply run node index.js > out.svg and we'll have our image.

And a view back in the browser of our index.html confirms it still works!

As i meantioned previously, this probably isn't enough for you're project, you probably need it to take dynamic data and to be a server that can generate the image files for you rather than being manually called in the terminal.
But this is the start point, and please leave feedback if you'd like me to write up about how we'd take this further, thanks :)

comments powered by Disqus
Want to setup your own server? Digital Ocean offer $10 free for new accounts.