Skip to content

Instantly share code, notes, and snippets.

@gka
Last active April 29, 2021 21:44
Show Gist options
  • Save gka/b2c2a92c07bba6d0d0ee579503abadcb to your computer and use it in GitHub Desktop.
Save gka/b2c2a92c07bba6d0d0ee579503abadcb to your computer and use it in GitHub Desktop.
like svg-crowbar, but for multiple svg elements!
  1. Paste multi-crowbar.js into the browser developer console or use this bookmarklet
  2. Call multiCrowbar function and pass a selector for the container div
multiCrowbar(".my-chart")

if you want to include html labels or captions you can pass another selector as second argument

multiCrowbar(".my-chart", "h2.my-caption")
var multiCrowbar = (function() {
/*
* SVG Export
* converts html labels to svg text nodes
* will produce incorrect results when used with multi-line html texts
*
* Author: Gregor Aisch
* based on https://github.com/NYTimes/svg-crowbar/blob/gh-pages/svg-crowbar-2.js
*/
window.d3 = null;
var s = document.createElement('script');
s.src = 'https://d3js.org/d3.v3.min.js';
document.getElementsByTagName('head')[0].appendChild(s);
function check() {
if (!window.d3) return setTimeout(check, 200);
console.log('ready...');
// run('body');
}
check();
var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
var fontFamilyMapping = {
'nyt-franklin': 'NYTFranklin'
};
return function(cont, label_selector) {
var parent = d3.select(cont),
parent_n = parent.node();
var out_w = parent_n.nodeName == 'body' ? parent_n.scrollWidth : parent_n.clientWidth,
out_h = parent_n.nodeName == 'body' ? parent_n.scrollHeight: parent_n.clientHeight;
var labels = label_selector ? parent.selectAll(label_selector) : null,
nodes = parent.selectAll('path, line, rect, circle, text'),
copydefs = parent.selectAll('linearGradient,radialGradient,filter,pattern,clipPath');
// divs = parent.selectAll('.export-rect,.rect'),
// circles = parent.selectAll('.circle');
var svgNodes = parent.selectAll('svg');
// 1. create a new svg container of the size of the page
var out = parent.append('svg');
// empty css declaration
var emptyCSS = window.getComputedStyle(out.node());
out.attr({ width: out_w, height: out_h })
.style({ position: 'absolute', left: 0, top: 0 });
var out_defs = out.append('defs');
copydefs.each(function() {
var el = this;
var cloned = el.cloneNode(true);
out_defs.node().appendChild(cloned);
});
// top offset to parent element
var offsetTop = parent_n.getBoundingClientRect().top,
offsetLeft = parent_n.getBoundingClientRect().left;// - parent_n.parentNode.getBoundingClientRect().top;
var out_g = out.append('g').attr('id', 'svg');
nodes.each(function() {
var el = this,
cur = el,
curCSS,
bbox,
transforms = [];
while (cur) {
curCSS = getComputedStyle(cur);
if (cur.nodeName == 'defs') return;
if (cur.nodeName != 'svg') {
// check node visibility
transforms.push(attr(cur, 'transform'));
cur = cur.parentNode;
} else {
bbox = cur.getBoundingClientRect();
transforms.push('translate('+[bbox.left - offsetLeft, bbox.top - offsetTop]+')');
cur = null;
}
if (isHidden(curCSS)) return;
}
transforms = transforms.filter(function(d) { return d; }).reverse();
var cloned = el.cloneNode(true);
cloned.setAttribute('transform', transforms.join(' '));
// copy all computed style attributes
explicitlySetStyle(el, cloned);
out_g.node().appendChild(cloned);
});
if (labels) {
out_g = out.append('g').attr('id', 'text');
labels.each(function() {
// create a text node for each label
var el = this,
cur = el,
bbox = el.getBoundingClientRect(),
align = 'left',
content = el.innerText,
transforms = [];
var lblPos = { x: bbox.left - offsetLeft, y: bbox.top - offsetTop };
var txt = out_g.append('text')
.text(content)
.attr({ x: lblPos.x });
copyTextStyles(el, txt.node());
txt.attr('y', lblPos.y)
.style('dominant-baseline', 'text-before-edge');
bbox = txt.node().getBoundingClientRect();
txt.attr('y', lblPos.y+bbox.height).style('dominant-baseline', 'text-after-edge');
});
}
download(out.node(), cont.replace(/\.#/g, ''));
out.remove();
// labels.remove();
// svgNodes.remove();
function isHidden(css) {
return css.display == 'none' ||
css.visibility == 'hidden' ||
+css.opacity === 0 ||
(+css.fillOpacity === 0 || css.fill == 'none') &&
(css.stroke == 'none' || !css.stroke || +css.strokeOpacity === 0);
}
function explicitlySetStyle(element, target) {
var elCSS = getComputedStyle(element),
i, len, key, value,
computedStyleStr = "";
for (i=0, len=elCSS.length; i<len; i++) {
key=elCSS[i];
value=elCSS.getPropertyValue(key);
if (value!==emptyCSS.getPropertyValue(key)) {
if (key == 'font-family' && fontFamilyMapping[value]) value = fontFamilyMapping[value];
computedStyleStr+=key+":"+value+";";
}
}
target.setAttribute('style', computedStyleStr);
}
function copyTextStyles(element, target) {
var elCSS = getComputedStyle(element),
i, len, key, value,
computedStyleStr = "";
for (i=0, len=elCSS.length; i<len; i++) {
key=elCSS[i];
if (key.substr(0,4) == 'font' || key.substr(0,4) == 'text' || key == 'color') {
value=elCSS.getPropertyValue(key);
if (key == 'color') key = 'fill';
if (value!==emptyCSS.getPropertyValue(key)) {
if (key == 'font-family' && fontFamilyMapping[value]) value = fontFamilyMapping[value];
computedStyleStr+=key+":"+value+";";
}
}
}
target.setAttribute('style', computedStyleStr);
}
function download(svg, filename) {
var source = (new XMLSerializer()).serializeToString(svg);
var url = window.URL.createObjectURL(new Blob([doctype + source], { "type" : "text\/xml" }));
var a = document.createElement("a");
document.body.appendChild(a);
a.setAttribute("class", "svg-crowbar");
a.setAttribute("download", filename + ".svg");
a.setAttribute("href", url);
a.style.display = "none";
a.click();
setTimeout(function() {
window.URL.revokeObjectURL(url);
}, 10);
}
function attr(n, v) { return n.getAttribute(v); }
};
})();
<html>
<head><style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
</style></head>
<body>
drag this to your bookmarks:
<a style="background:#4078c0;color:#fff;display:inline-block;padding:6px 10px; border-radius:20px;" href="javascript:(function()%7Bfunction%20callback()%7Balert(%22test!%22)%7Dvar%20s%3Ddocument.createElement(%22script%22)%3Bs.src%3D%22https%3A%2F%2Fcdn.rawgit.com%2Fgka%2Fb2c2a92c07bba6d0d0ee579503abadcb%2Fraw%2Fe4b91f8d4125ec177a8a3645de76716e80d01632%2Fmulti-crowbar.js%22%3Bif(s.addEventListener)%7Bs.addEventListener(%22load%22%2Ccallback%2Cfalse)%7Delse%20if(s.readyState)%7Bs.onreadystatechange%3Dcallback%7Ddocument.body.appendChild(s)%3B%7D)()">multi-crowbar</a>
</body>
</html>
@TennisVisuals
Copy link

TennisVisuals commented Aug 14, 2016

Every time I try it I get:

VM340:71 Uncaught ReferenceError: _ is not defined(…)

The reason being that it makes the assumption the target website includes underscore.js

When I include underscore.js in my own webpages I get partial (clipped) .svg files, and it seems to have something to do with the scroll position of the webpage at the moment that I call multiCrowbar()

@gka
Copy link
Author

gka commented Sep 1, 2016

Update Sept 1:

  • removed unnecessary underscore dependency
  • fixed scroll offset bug, svg + labels are now exactly where they're supposed to be
  • added support for gradients and other defs

cc @TennisVisuals

@kklai
Copy link

kklai commented Apr 6, 2020

The bookmarklet linked still has the _.filter error ☹️

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