Taking care of gifness
Rendering your D3 viz outside of your browser
Noah Veltman
@veltman
USE CASES
Portability
Performance
Code once, generate many
Work in JS instead of animation software
I HAVE...
an SVG
a <canvas>
HTML
I WANT...
images (SVG, PNG, etc.)
GIFs
videos
something else
I NEED...
CSS
browser APIs
webfonts
I'M MAKING...
one of them
lots of them
MANUAL CAPTURE
Easy, doesn't scale
HELPER CODE
Relatively easy, still labor-intensive
SERVER SIDE
Scaleable, no browser features
HEADLESS BROWSER
Hybrid approach
OPTION A
Manual Capture
PROS
Easy
Zero code modification
CONS
Doesn't scale
Imprecise crop/timing
VIDEOS
GIFS
IMAGES
SVG
HTML
QuickTime
LICECap
Screenshot
SVG Crowbar
Copy/paste
DIGRESSION
SVG vs. Canvas
SVG
Vector
Manipulate elements
Style with CSS
Have to rasterize
CANVAS
Raster
Draw pixels
Style explicitly
SVG GOTCHA
CSS defined outside of your <svg> won't be included
SOLUTIONS
Use SVG Crowbar
Copy stylesheets into SVG element
COPY ALL THE CSS
var allStyles = Array.from(document.styleSheets)
.map(sheet =>
Array.from(sheet.cssRules)
.map(rule => rule.cssText)
.join(" ")
).join(" ");
d3.select("svg").append("style")
.text(allStyles);
OPTION B
Add helper code
PROS
Repeatable
Precise
CONS
Still manual
No d3.transition()
VIDEOS
GIFS
IMAGES
MediaRecorder API
GIF.js
toDataUrl
DIGRESSION
Animation frames
RENDER LOOPS
// Set stuff up
draw(t) {
// Draw everything based on t
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
d3.transition()
d3.select("circle")
.transition()
.duration(5000)
.attr("r", 5)
.attr("fill", "red");
RENDER LOOP
Specify every detail
Can render any frame
TRANSITION()
Lots of hidden magic
Chainable
Events
Can't frame-seek
TRANSLATION
var circle = d3.select("circle");
color = circle.attr("fill"),
radius = circle.attr("r"),
interpolateColor = d3.interpolate(color, "red"),
interpolateRadius = d3.interpolate(radius, 5);
var timer = d3.timer(function(t){
var easedT = d3.easeCubicInOut(t);
circle.attr("fill", interpolateColor(easedT))
.attr("r", interpolateRadius(easedT);
if (t >= 5000) {
timer.stop();
}
});
SVG GOTCHA
No background color
SOLUTION
Draw a rectangle behind it
GIF.JS
var gif = new GIF();
for (var i = 0; i < numFrames; i++) {
drawFrame(i * duration / frames);
gif.addFrame(myCanvas, {
delay: duration / frames,
});
}
gif.render();
DEMOS
Globe to GIF
SVG to GIF
Globe to WebM
OPTION C
Server side
PROS
Repeatable
CONS
No d3.transition()
No browser APIs
Font nightmares
VIDEOS
GIFS
IMAGES
SVG
HTML
node-canvas -> FFmpeg
node-canvas -> FFmpeg or GIFEncoder
node-canvas or svg2png
jsdom
jsdom
node-canvas
var Canvas = require("canvas");
var myCanvas = new Canvas(600, 600);
var context = myCanvas.getContext("2d");
context.fillStyle = "papayawhip";
context.fillRect(100, 100, 400, 400);
fs.writeFile("whipit.png", canvas.toBuffer(), etc);
d3 + JSDOM
var d3 = require("d3"),
jsdom = require("jsdom");
var body = new jsdom.JSDOM().window.document.body;
var svg = d3
.select(body)
.append("svg")
.attr("width", 100)
.attr("height", 100);
// Add stuff here
fs.writeFile("mychart.svg", body.innerHTML);
DEMOS
Globe to GIF
Globe to video
SVG minimaps
SVG GOTCHA
Coordinate precision increases file size
SOLUTION
Round coordinates
var svgpath = require("svgpath");
function roundedPath(d){
return svgpath(d)
.round(2).toString();
}
SVG GOTCHA
Fonts
SOLUTIONS
Embed fonts in graphics software
Embed font-face in <svg>
OPTION D
Headless browser
PROS
Repeatable
Browser APIs
CONS
Still no d3.transition()
Finicky and complex
Slow
PUPPETEER
NIGHTMAREJS
SELENIUM
PHANTOMJS
DEMO
Globe to GIF
OPTION E
Lie to your computer
SPOT THE CODE SMELL
var fakeTime = 0;
Date.now = function() {
return fakeTime;
};
window.requestAnimationFrame = function(cb) {
saveFrame();
fakeTime += 17;
setTimeout(cb,0);
};
PROS
d3.transition()
Use existing code
CONS
Is completely insane
DEMO
Clock hacking
RESOURCES
github.com/veltman/d3-unconf
github.com/veltman/gifs
github.com/GoogleChrome/puppeteer
github.com/antimatter15/whammy
github.com/fivethirtyeight/d3-pre
d3unconf
By veltman
d3unconf
- 3,516