roadtolarissa
Adam Pearce

D3 to MP4

Generating a high-resolution video from a d3 animation is tricky. LICEcap and QuickTime screen recording work in a pinch, but aren't scriptable and lose FPS without a beefy video card.

Noah Veltman has written about and presented different techniques for exporting d3 graphics. The best way I've found of exporting video come from Veltman and uses a delightful hack: modifying time itself.

Mutate Time

Inside of your clientside code, overwrite performance.now with a function that returns that currentTime variable. This will let us control what time d3-timer and d3-transition think it is.

if (document.URL.includes('d3-video-recording')){
  window.currentTime = 0
  performance.now = () => currentTime
  window.setTime = t => curTime = t

  var graphSel = d3.select('html')
    .st({width: 1920, height: 1080, position: 'absolute'})
}

This code only runs if the url contains d3-video-recording, making it easy toggle between automatic and manual animations with a query string. It also positions the chart at the upper-left corner of the screen and sets its size to 1920x1080, making it easy to crop.

Take Screenshots

puppeteer loads the page, moving time forward slowly and taking a screenshot over and over again. Even though each screenshot takes over half a second to render, controlling time in the browser ensures no frames are dropped.

const puppeteer = require('puppeteer')
const d3 = require('d3')

;(async () => {
  // open new tab and wait for data to load
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('http://localhost:1337?d3-video-recording')
  await sleep(5000)

  // step through each frame:
  // - increment currentTime on the page
  // - save a screenshot
  for (let frame of d3.range(120)){
    await page.evaluate((frame) => currentTime = frame*1000/60, frame)
    await sleep(50)

    let path = __dirname + '/png/' + d3.format('05')(frame) + '.png'
    let clip = {x: 0, y: 0, width: 1920, height: 1080}
    await page.screenshot({path, clip})
  }

  browser.close()
})()


function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

Covert to Video

Finally, convert the directory of screenshots to a video:

ffmpeg -framerate 60 -pattern_type glob -i 'png/*.png' video.mp4

For quick thumbnail previews, check out gistsnap. A similar CLI tool for video could be useful, but I'm not sure passing flags to control the FPS, delay, number of frames, crop area and query string is easier than coding them directly.