Client-Side Solution For Downloading Highcharts Charts as Images
It even works in IE
Highcharts is an excellent web-based charting package. But, because it’s a client-side solution, downloading charts as images is tricky. The default solution is to use Highcarts’ export module, which posts the chart’s SVG code to Highcharts’ export server. The export server converts the chart to an image and then sends it back to the browser.
The export server is a well-executed solution and should work for most situations. However for my latest project, the charts contain sensitive data. Sending them to a third-party server is not possible. I considered setting up my own export server, or adding an endpoint to my web application. But both these solutions add complexity to the server for a task that theoretically can be handled in the client.
In this blog post, I explore various client-side solutions to downloading Highcharts as images.
Strategy
Highcharts are rendered as SVG (scalable vector graphics). The basic steps to download a chart as an image are:
- Get the chart’s SVG code
- Render the SVG onto a
<cavas>
element - Use toDataURL() to extract the canvas contents as an image
- Save this image to the local filesystem
The Example Code
Let’s start with a basic chart and download button.
1. Get the chart’s SVG code
Highcharts has an exporting module that adds functions for downloading and printing charts. This module must be included separately.
The exporting module adds the function getSVG which extracts the chart’s SVG code and sanitizes it for export.
NOTE: Because we don’t specify a hard-coded chart width in our css, we need to specify sourceWidth and
sourceHeight in the call to getSVG
. This seems like a bug to me. getSVG
should be able to calculate
the chart dimensions itself. (If we can get the dimensions from chart.chartWidth / chart.chartHeight,
getSVG
could too)
NOTE: Alternatively, you could grab the chart’s SVG code directly.
While this eliminates the need for the exporting module, it also skips the sanitizing code. The most obvious problem I ran into is that the resulting image may contain tooltips, which you probably don’t want in your downloaded chart image.
2. Render the chart’s SVG onto a <canvas>
element
Rendering SVG onto a <canvas>
element is easy. You can put SVG into the src
attribute of an <img>
tag. The browser will render the SVG into the image. Once the image has been rendered, you render this
image onto the canvas element. You don’t even need to insert the image into the DOM for this to work.
To show how this works, we’ll create a preliminary version of save_chart
that renders the chart’s SVG
onto a <canvas>
element.
NOTE: Since we typically want to download a chart in a higher resolution than it’s displayed on the page, we use EXPORT_WIDTH to control the size of the exported image.
See JSFiddle demo
3. Use toDataURL
to extract the canvas contents as an image
The canvas element has a handy function toDataURL
that extracts its content as an image.
toDataURL
returns a data URI with
the image encoded in base64. This URI can then be downloaded as an image to the user’s computer.
NOTE: We don’t need to to add the canvas element to the DOM in order to render and extract the
image data. So we can remove document.body.appendChild(canvas)
.
4. Save the image to the local filesystem
This is where things get a little hacky. There is no standard way to save data to the user’s
local filesystem. The most widely used technique is to create an <a>
tag and then send
it a click event.
The download
attribute of the <a>
tag tells the browser to present the target (our chart
image) as a downloadable file with a given filename. Pretty nice, but it’s currently only
supported in Chrome and Firefox. Other browsers will just open the image in the current window
instead of presenting it as a download. Not ideal, but it’s the best we can do for now.
And that’s everything. See this JSFiddle demo for the complete code.
Not quite so fast
So everything looks good… except it doesn’t work in IE. The first problem is that IE throws a
“SecurityError” on the call to toDataURL
. This is because IE considers the canvas to be
“tainted” once an SVG image has been rendered. SVG code can reference content from the user’s
local file system. Once that’s in the canvas element it would be security vulnerability
to allow the canvas contents to be extracted. Otherwise a malicious site could read data from
the user’s file system and send it on to an external server.
This forum message is the only reference I can find discussing the issue in IE. Either Chrome, Firefox, and Safari have ways of sanitizing SVG content before rendering onto a canvas, or they don’t allow SVG code to reference local content, or they are not concerned about this vulnerability.
IE work-around #1: use canvg
We can work-around the canvas tainting issue with the canvg library. canvg is a full SVG parser and renderer. It can render SVG onto a canvas element with all of the features of the browser’s built-in SVG render. It’s an impressive piece of code, and works flawlessly. But it’s also 2800 lines of javascript that are entirely unnecessary in any modern browser, because the browser itself can render SVG onto a canvas element. However, because canvg parses the SVG code by hand, manually rendering each path, box, quadratic curve, gradient, pattern etc. it sidesteps the IE “SecurityError”.
Replace the var image = new Image...
code with this:
Still not working… URL limit
With the “SecurityError” fix in place, we hit the next barrier. IE stops with the error
“The data area passed to a system call is too small” on the call to a.click()
. I think
this is because IE has a 2083 character URL limit.
The data URIs generated by toDataURL
are much longer than 2083 characters.
IE work-around #2: use msSaveOrOpenBlob
I’m calling this a work-around. But it’s actually an improvement over the <a>
tag hack.
IE has a function msSaveOrOpenBlob
that presents client-side data to the user as if it were downloaded from the internet. It’s
exactly what we need. Let’s add logic to use msSaveOrOpenBlob
in IE and use the <a>
tag hack everywhere else.
Here’s the JSFiddle demo putting it all together.
Wrapping up
It took a while to get here, but the final solution isn’t bad. My main reservation is pulling in canvg. But it’s a stable library and seems like a necessary tradeoff for IE support.
A few other libraries that I tested along the way:
FileSaver.js - If you want a fully-supported,
cross-browser “just save it” solution, this is a good library. At its core, FileSaver.js uses
the same <a>
tag hack I used above. But FileSaver.js has a lot of additional complexity
to handle edge cases that I didn’t feel was needed for this situation. I ultimately decided
not to use it. But it’s a solid library and Eli Grey has put a lot of work into making it work
flawlessly. Definitely work a look.
canvas-toBlob.js - If you use FileSaver.js, you will need this library to pull the blob data out of the canvas element. It implements the standard canvas.toBlob() function for browsers that don’t support it yet. It’s another solid piece of code by Eli Grey.