After playing with interactive data visualization using D3.js and React, I wanted to have a "Download" function so that I can easily save the visualization as an image rather than taking a screenshot of a browser window:
- Practicing D3 Interactive Data Visualization with Fitbit Activity/Sleep Log
- How Much CO2 Emissions Have Your Flights Made?
In short, it can be achieved by writing a JavaScript snippet that does the following job:
<svg>
toBlob
Blob
toImage
Image
to<canvas>
Eventually, HTML Canvas enables you to save the content in the form of image file.
For the future reference, let me walk-through the process line-by-line since coming up with a complete snippet wasn't straightforward; there were some "tricky" parts that aren't fully covered by a piece of online resources e.g.:
Assume we have an SVG drawing index.html
and its stylesheet e.g., style.css
:
<svg xmlns="http://www.w3.org/2000/svg">
<rect width=64 height=64 />
</svg>
svg rect {
fill: blue;
}
SVG to Blob
The first and most confusing step is to convert SVG into a Blob
object represented by XML:
const svg = document.querySelector('svg');
// CSS must be explicitly embedded
const style = createStyleElementFromCSS();
svg.insertBefore(style, svg.firstChild);
const data = (new XMLSerializer()).serializeToString(svg);
const svgBlob = new Blob([data], {
type: 'image/svg+xml;charset=utf-8'
});
// remove the temporarily injected CSS
style.remove();
To unlock the conversion, we have to keep the following points in our mind:
- Define
<svg>
withxmlns="http://www.w3.org/2000/svg"
- Embed
style
inside of the<svg>
tag
Here, the inserted style
element can be dynamically constructed as follows:
const createStyleElementFromCSS = () => {
// assume index.html loads only one CSS file in <header></header>
const sheet = document.styleSheets[0];
const styleRules = [];
for (let i = 0; i < sheet.cssRules.length; i++)
styleRules.push(sheet.cssRules.item(i).cssText);
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(styleRules.join(' ')))
return style;
};
Although we defined the style in the separate .css
file, it's important for <svg>
to explicitly contain the information. Otherwise, a saved image loses all the information about color, font, shape, etc.
Consequently, the svgBlob
object has the XML representation of an SVG element below:
<svg xmlns="http://www.w3.org/2000/svg">
<style type="text/css">
svg rect { fill: blue; }
</style>
<rect width=64 height=64 />
</svg>
Blob to Image
Next, loading the Blob
object to Image
allows the application to treat the original SVG in a more handy way:
// convert the blob object to a dedicated URL
const url = URL.createObjectURL(svgBlob);
// load the SVG blob to a flesh image object
const img = new Image();
img.addEventListener('load', () => {
// (Next step: Image to Canvas)
});
img.src = url;
For instance, url
denotes a reference to the object as: blob:http://localhost:3000/a1c7704c-09a5-46f5-a102-7bc84d8ecbce
Image to Canvas
Once Image
recognizes the Blob URL, we finally draw the image on an HTML Canvas and trigger a download operation.
img.addEventListener('load', () => {
// draw the image on an ad-hoc canvas
const bbox = svg.getBBox();
const canvas = document.createElement('canvas');
canvas.width = bbox.width;
canvas.height = bbox.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, bbox.width, bbox.height);
URL.revokeObjectURL(url);
// trigger a synthetic download operation with a temporary link
const a = document.createElement('a');
a.download = 'image.png';
document.body.appendChild(a);
a.href = canvas.toDataURL();
a.click();
a.remove();
});
Notice that the event handler cleanses the environment by removing url
and a
, an ad-hoc download link. Meanwhile, in case you want to obtain an image in a particular type, canvas.toDataURL()
takes an optional parameter like canvas.toDataURL('image/jpeg')
.
That's it. Now we're capable of extracting an image file from an HTML-coded SVG illustration. My flight emission calculator app already has a Download button for the desired use case.
A set of code snippets below is a complete, compact example of things I explained in this article.
Share
Categories
See also
- 2021-03-13
- How Much CO2 Emissions Have Your Flights Made?
- 2021-02-03
- Practicing D3 Interactive Data Visualization with Fitbit Activity/Sleep Log
- 2021-01-23
- Datavis 2020: A Free Online Course About D3.js & React
Last updated: 2022-09-02
Author: Takuya Kitazawa
Takuya Kitazawa is a freelance software developer based in British Columbia, Canada. As a technologist specializing in AI and data-driven solutions, he has worked globally at Big Tech and start-up companies for a decade. At the intersection of tech and society, he is passionate about promoting the ethical use of information technologies through his mentoring, business consultation, and public engagement activities. See CV for more information, or contact at [email protected].
Now Gift a cup of coffeeDisclaimer
- Opinions are my own and do not represent the views of organizations I am/was belonging to.
- I am doing my best to ensure the accuracy and fair use of the information. However, there might be some errors, outdated information, or biased subjective statements because the main purpose of this blog is to jot down my personal thoughts as soon as possible before conducting an extensive investigation. Visitors understand the limitations and rely on any information at their own risk.
- That said, if there is any issue with the content, please contact me so I can take the necessary action.