Exporting a D3 chart as an image

I recently worked on an interesting feature for WaterWorth. It gives the user the ability to download a D3 chart and save it as a JPEG or PNG, so that it can be used for reporting purposes. In this post, I will try to explain how we can convert a D3 tool to an image.

For simplicity, I will assume that one has already done the hard work of using D3 to create a beautiful chart, and now you have an SVG element ready to be downloaded in your DOM. For example,

    <svg id='mySVG'>
        <rect x="10" y="10" height="100" width="100"/>
    </svg>

The first step is to select the SVG element, for which we can use either D3 or JQuery.

    var imgSVG = d3.select('#mySVG');

Then, if you have external stylesheets(which most of the D3 charts have), we need to get those external styles in a string format, so that we can apply those styles. Without them, our downloaded image will not look as impressive as D3. This is how we can get the external stylesheets:

    var style = "\n";
    var requiredSheets = ['chart.css']; // list of required CSS
    for (var i = 0; i < document.styleSheets.length; i++) {
        var sheet = document.styleSheets[i];
        if (sheet.href) {
            var sheetName = sheet.href.split('/').pop();
            if (requiredSheets.indexOf(sheetName) != -1) {
                var rules = (<any>sheet).rules;
                if (rules) {
                    for (var j = 0; j < rules.length; j++) {
                        style += (rules[j].cssText + '\n');
                    }
                }
            }
        }
    }

Once we have the styles in style variable, we can inject those styles in our SVG for later use using deferred SVG aka <defs> element.

    // prepend style to svg
    imgSVG.insert('defs', ":first-child")
    d3.select("svg defs")
        .append('style')
        .attr('type', 'text/css')
        .html(style);

Next step is to create the image element and generate the src attribute value for this image. I am using an XMLSerializer object which is used to convert a DOM subtree into text. XMLSerializer has a serializeToString(dom) method, which returns a string equivalent of dom node.

Once we have the string version of the imgSVG, I am using btoa() method of window to creates a base-64 encoded ASCII string from a String object in which each character in the string is treated as a byte of binary data.

    var serializer = new XMLSerializer();

    var svgStr = serializer.serializeToString(imgSVG.node());
    var src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);

    var img = '<img src="' + src + '">';
    d3.select("#svgImg").html(img);

Now, to actually convert the generated image to a downloadable image format, we will use canvas in HTML5. In my html, I have already added an empty <canvas></canvas> element.

    var canvas = document.querySelector("canvas"),
    context = (<any>canvas).getContext("2d");

The canvas.getContext() method returns a drawing context on the canvas, or null if the context identifier is not supported. Next few lines of code generate an image element so that we can attach onload event handler, in which we can add a link and click it programmatically to download the given image.

    var image = new Image;
    image.src = imgsrc;

    image.onload = function () {
        context.drawImage(image, 0, 0);

        var canvasdata = (<any>canvas).toDataURL("image/jpeg");

        var a = document.createElement("a");
        (<any>a).download = "sample.jpeg";
        a.href = canvasdata;
        a.click();
    };