Skip to content

Instantly share code, notes, and snippets.

@rokotyan
Last active December 12, 2023 13:07
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save rokotyan/0556f8facbaf344507cdc45dc3622177 to your computer and use it in GitHub Desktop.
Save rokotyan/0556f8facbaf344507cdc45dc3622177 to your computer and use it in GitHub Desktop.
Export SVG D3 visualization to PNG or JPEG
license: mit

This example shows how to properly export a D3 SVG visualization to an image (png/jpeg) taking into account external css styles and embedded images. The code uses small FileSaver.js library to save generated images and Canvas-to-Blob.js library to ensure browser compatibility.

<!DOCTYPE html>
<html lang="en">
<head>
<title>How to properly export SVG D3 visualization to PNG or JPEG</title>
<meta charset="utf-8">
<script src="https://cdn.rawgit.com/eligrey/canvas-toBlob.js/f1a01896135ab378aa5c0118eadd81da55e698d8/canvas-toBlob.js"></script>
<script src="https://cdn.rawgit.com/eligrey/FileSaver.js/e9d941381475b5df8b7d7691013401e171014e89/FileSaver.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
.blendCircle {
mix-blend-mode: multiply;
}
</style>
</head>
<body>
<div>
<button id='saveButton'>Export my D3 visualization to PNG</button>
</div>
</body>
<script type="text/javascript">
// Let's create a mock visualization
var width = 300, height = 300;
var circleSizeMax = 15;
var rMax = Math.min(width,height)/2 - circleSizeMax;
var radius = d3.scale.linear().range([0,rMax]);
var angle = d3.scale.linear().range([0,2*Math.PI]);
var size = d3.scale.linear().range([0,circleSizeMax]);
var color = d3.scale.ordinal().range(['#fcfb3c','#fcf900','#ff825a','#ffd2cb','#71d362','#ffd16f','#ff3d5d','#ff7218','#04b3f3','#bce5ac','#6e0215','#69D2E7','#A7DBDB','#E0E4CC','#F38630','#E94C6F','#542733','#5A6A62','#C6D5CD','#DB3340','#E8B71A','#F7EAC8','#1FDA9A','#588C73','#F2E394','#F2AE72','#D96459','#D0C91F','#85C4B9','#008BBA','#DF514C','#00C8F8','#59C4C5','#FFC33C','#FBE2B4','#5E412F','#FCEBB6','#78C0A8','#F07818','#DE4D4E','#DA4624','#DE593A','#FFD041','#B1EB00','#53BBF4','#FF85CB','#FF432E','#354458','#3A9AD9','#29ABA4','#E9E0D6','#4298B5','#ADC4CC','#92B06A','#E19D29','#BCCF02','#5BB12F','#73C5E1','#9B539C','#FFA200','#00A03E','#24A8AC','#0087CB','#260126','#59323C','#F2EEB3','#BFAF80','#BFF073','#0DC9F7','#7F7F7F','#F05B47','#3B3A35','#20457C','#5E3448','#FB6648','#E45F56','#A3D39C','#7ACCC8','#4AAAA5','#DC2742','#AFA577','#ABA918','#8BAD39','#F2671F','#C91B26','#9C0F5F','#60047A','#0F5959','#17A697','#638CA6','#8FD4D9','#83AA30','#1499D3','#4D6684','#3D3D3D','#333333','#424242','#00CCD6','#EFEFEF','#CCC51C','#FFE600','#F05A28','#B9006E','#F17D80','#737495','#68A8AD','#C4D4AF']);
var x = function(d) { return radius( d.r ) * Math.cos( angle( d.angle ) ); };
var y = function(d) { return radius( d.r ) * Math.sin( angle( d.angle ) ); };
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height',height);
var chart = svg.append('g').attr('transform','translate('+[width/2,height/2]+')');
var data = d3.range(150).map( function(d) { return {r: Math.random(), angle: Math.random(), size: Math.random() }; });
chart.selectAll('cirle')
.data(data).enter()
.append('circle')
.attr('class','blendCircle')
.attr({
cx: x,
cy: y,
r: function(d) { return size(d.size); },
fill: function(d,i) { return color(i); }
});
// Set-up the export button
d3.select('#saveButton').on('click', function(){
var svgString = getSVGString(svg.node());
svgString2Image( svgString, 2*width, 2*height, 'png', save ); // passes Blob and filesize String to the callback
function save( dataBlob, filesize ){
saveAs( dataBlob, 'D3 vis exported to PNG.png' ); // FileSaver.js function
}
});
// Below are the functions that handle actual exporting:
// getSVGString ( svgNode ) and svgString2Image( svgString, width, height, format, callback )
function getSVGString( svgNode ) {
svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink');
var cssStyleText = getCSSStyles( svgNode );
appendCSS( cssStyleText, svgNode );
var serializer = new XMLSerializer();
var svgString = serializer.serializeToString(svgNode);
svgString = svgString.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink='); // Fix root xlink without namespace
svgString = svgString.replace(/NS\d+:href/g, 'xlink:href'); // Safari NS namespace fix
return svgString;
function getCSSStyles( parentElement ) {
var selectorTextArr = [];
// Add Parent element Id and Classes to the list
selectorTextArr.push( '#'+parentElement.id );
for (var c = 0; c < parentElement.classList.length; c++)
if ( !contains('.'+parentElement.classList[c], selectorTextArr) )
selectorTextArr.push( '.'+parentElement.classList[c] );
// Add Children element Ids and Classes to the list
var nodes = parentElement.getElementsByTagName("*");
for (var i = 0; i < nodes.length; i++) {
var id = nodes[i].id;
if ( !contains('#'+id, selectorTextArr) )
selectorTextArr.push( '#'+id );
var classes = nodes[i].classList;
for (var c = 0; c < classes.length; c++)
if ( !contains('.'+classes[c], selectorTextArr) )
selectorTextArr.push( '.'+classes[c] );
}
// Extract CSS Rules
var extractedCSSText = "";
for (var i = 0; i < document.styleSheets.length; i++) {
var s = document.styleSheets[i];
try {
if(!s.cssRules) continue;
} catch( e ) {
if(e.name !== 'SecurityError') throw e; // for Firefox
continue;
}
var cssRules = s.cssRules;
for (var r = 0; r < cssRules.length; r++) {
if ( contains( cssRules[r].selectorText, selectorTextArr ) )
extractedCSSText += cssRules[r].cssText;
}
}
return extractedCSSText;
function contains(str,arr) {
return arr.indexOf( str ) === -1 ? false : true;
}
}
function appendCSS( cssText, element ) {
var styleElement = document.createElement("style");
styleElement.setAttribute("type","text/css");
styleElement.innerHTML = cssText;
var refNode = element.hasChildNodes() ? element.children[0] : null;
element.insertBefore( styleElement, refNode );
}
}
function svgString2Image( svgString, width, height, format, callback ) {
var format = format ? format : 'png';
var imgsrc = 'data:image/svg+xml;base64,'+ btoa( unescape( encodeURIComponent( svgString ) ) ); // Convert SVG string to data URL
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
var image = new Image();
image.onload = function() {
context.clearRect ( 0, 0, width, height );
context.drawImage(image, 0, 0, width, height);
canvas.toBlob( function(blob) {
var filesize = Math.round( blob.length/1024 ) + ' KB';
if ( callback ) callback( blob, filesize );
});
};
image.src = imgsrc;
}
</script>
</html>
@brunojennrich
Copy link

great start! thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment