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,532