roadtolarissa
Adam Pearce github twitter email rss

D3 to MP4

Generating a high-resolution video from a d3 animation is tricky. LICEcap and QuickTime screen recording work in a pinch, but they 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 him and uses a delightful hack: modifying time itself.

Mutate Time

Inside of your clientside code, overwrite performance.now with a function that returns the 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
}

This code only runs if the url contains d3-video-recording, making it easy to toggle between automatic and manual animations with a query string.

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 the browser’s perception of time 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'

    await page.setViewport({width: 1920, height: 1080, deviceScaleFactor: 2})
    const chartEl = await page.$('.chart')
    await chartEl.screenshot({path})
  }

  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.