Over the past 6 months, I’ve had the pleasure of working on a project that sought to use d3.js to generate custom graphs on the fly. The learning curve was a bit steep and I still wouldn’t consider myself a d3 expert, but we’ve managed to come up with a tool that allows users to select one or more data sets and graph them over a custom time span with options to tweak line colors and styles. The customer was pleased, but now that they had these impressive graphs, they wanted to be able to export them as images for use in their reports.
The good news is that d3 is an SVG (Scalable Vector Graphics) graphing library, so I knew that this problem had been solved before. There are a great many demos that show how to generate image files from SVG using the <canvas> element. Unfortunately, many of these demos only work in the latest Chrome or Firefox browsers and our client is still standardized on IE9. After much experimentation, I ended up finding a few tweaks to get things working in IE9. Take a look at the following snippet:
Example SVG image
<svg version="1.1" width="300" height="200" xmlns="http://www.w3.org/2000/svg"> <style type="text/css"> <![CDATA[ text { font-size:60px; text-anchor: middle; fill:#FFFFFF; } ]]> </style> <rect width="100%" height="100%" fill="#FF0000" /> <rect width="100%" height="50%" fill="#0000FF" /> <text x="150" y="70">SVG</text> </svg> <button onclick="exportPNG();">Export</button>
Image export script
<script type="text/javascript"> var exportPNG = function() { /* Based off gustavohenke's svg2png.js gist.github.com/gustavohenke/9073132 */ var svg = document.querySelector( "svg" ); var svgData = new XMLSerializer().serializeToString( svg ); var canvas = document.createElement( "canvas" ); var ctx = canvas.getContext( "2d" ); var dataUri = ''; try { dataUri = 'data:image/svg+xml;base64,' + btoa(svgData); } catch (ex) { // For browsers that don't have a btoa() method, send the text off to a webservice for encoding /* Uncomment if needed (requires jQuery) $.ajax({ url: "http://www.mysite.com/webservice/encodeString", data: { svg: svgData }, type: "POST", async: false, success: function(encodedSVG) { dataUri = 'data:image/svg+xml;base64,' + encodedSVG; } }) */ } var img = document.createElement( "img" ); img.onload = function() { ctx.drawImage( img, 0, 0 ); try { // Try to initiate a download of the image var a = document.createElement("a"); a.download = "MDMS_Graph_Export.png"; a.href = canvas.toDataURL("image/png"); document.querySelector("body").appendChild(a); a.click(); document.querySelector("body").removeChild(a); } catch (ex) { // If downloading not possible (as in IE due to canvas.toDataURL() security issue) // then display image for saving via right-click var imgPreview = document.createElement("div"); imgPreview.appendChild(img); document.querySelector("body").appendChild(imgPreview); } }; img.src = dataUri; } </script>
The above script follows the normal routine of extracting the SVG markup, embedding it as a dataUrl for an <img> and then using a <canvas> element to export it as a PNG (or JPG if you swap out the “image/png” for “image/jpeg” on line 45). However, there are a few wrinkles that help IE9+ keep up:
- Line 22 – Some older browsers don’t have the btoa (binary-to-ascii) method baked in, so this block provides an option for you to fire off a request to a server-side resource to handle the conversion for you.
- Line 55 – IE plays its safe with the canvas, marking it as “dirty” or not safe for export. To get around this, you can provide the image somewhere on the page, like a modal, and the instruct those users to right-click on the image and “Save As”. IE allows them to save as PNG, even though the image is technically an SVG still.
One other item of note is that your styling needs to be internal to the SVG markup. This seems obvious in hindsight, but when coding up the graphing tool, all of our styling was external to the SVG. This wasn’t a problem until I tried to tackle this export functionality. I dabbled with having the SVG load a CSS file remotely, but this seemed to trigger some additional security measures and just generally be more trouble than it was worth.
So there it is, generate your SVG graphics any which way you like and then conveniently save them as a PNG or JPG as needed. Me, I would prefer to leave them as SVG, but the client wants what the client wants!