Animating points on maps with deck.gl

You'd think I'd be tired of animating points by now, but it hasn't gotten old for me yet. I recently had the opportunity at work to build some map-based animations for a video we were putting together and the first thing that came to mind was to use the excellent deck.gl library from the Uber data vis team.

I wanted to have a glitzy reveal of all 17,000+ public libraries in the US and ended up with the following:

This post will cover how to make something like that yourself.

Basic point animation using transitions with ScatterplotLayer

If you haven't used deck.gl before, check out the examples in their documentation or view the code for this demo for details on how to get started.

The first approach we'll try for bringing animation to our map vis is to leverage the built-in support for transitions. Consider a basic ScatterplotLayer:

const librariesLayer = new ScatterplotLayer({
  id: 'points-layer',
  data: librariesData,
  getPosition: d => d.position, // [longitude, latitude] tuple
  getFillColor: [60, 220, 255],
  getRadius: 5000,
});

This will render our libraries as a bunch of points colored rgb(60, 220, 255), as shown below.

ScatterplotLayer Libraries

Now if we want to animate the colors of the points, we can modify our layer props to include transitions with getFillColor specified:

let showLibraries = false;
const librariesLayer = new ScatterplotLayer({
  id: 'points-layer',
  data: librariesData,
  getPosition: d => d.position,
  getFillColor: showLibraries ? [60, 220, 255] : [255, 255, 255],
  getRadius: 5000,
  transitions: {
    // transition with a duration of 3000ms
    getFillColor: 3000,
  },
});

When we set showLibraries = true and update deck.gl's layers, we'll get the following animation:

We're using the transitions shorthand here, which says when the getFillColor value changes we should transition to the new value over 3000ms.

Note: If you're using a function to specify getFillColor, you may also need to provide a value for updateTriggers so deck.gl knows it needs to recompute the colors of each point:

const librariesLayer = new ScatterplotLayer({
  ...
  updateTriggers: {
    // if showLibraries changes, recompute getFillColor for each point
    getFillColor: [showLibraries]
  }
});

If we want the points to grow into place in addition to the color tween, we can animate the radius by specifying getRadius in the transitions object.

const librariesLayer = new ScatterplotLayer({
    id: 'points-layer',
    data: librariesData,
    getPosition: d => d.position,
    getFillColor: showLibraries ? [60, 220, 255] : [255, 255, 255],
    getRadius: showLibraries ? 5000 : 0,
    transitions: {
      getFillColor: 3000,
      getRadius: {
        duration: 3000,
        easing: d3.easeBackInOut,
      },
    },
  });

This time we used the expanded transitions definition which allows us to set an easing for the animation to follow. The effect is shown below:

That was pretty easy! Thanks deck.gl, you're the best. The effect is more noticeable with a bigger radius or higher resolution, but you get the idea.

That was cool and all, however often times things look even cooler if we can add a little delay to each point instead of having them all animate in at once. To solve that, we have to do something a bit more complex and use a custom layer.

Add in point delay with a custom layer

I've created a layer I call the DelayPointLayer that allows you to render in points with delay specified by a function, similar to how d3 transitions work. I won't cover the internals of how it works here, but please feel free to view the source code and tune it to your needs.

The DelayPointLayer requires specifying three new properties on your point layer:

  • animationProgress How far through the animation you currently are (a value between 0 and 1). At 0, the animation has not started. At 1, all points are done animating.
  • pointDuration How long a single point takes to animate as a proportion of the animation progress (a value between 0 and 1). A value of 0.25 means any given point will take 25% of the animation duration to completely animate (works best with linear easing on animationProgress). The final point begins animating when animationProgress is at 1 - pointDuration.
  • getDelayFactor A function mapping from a data point to a value between 0 and 1 representing when this point should begin animating. A value of 0 means it will begin animating immediately and 1 means it will be the last point to animate.

Let's look at our library example:

// value between 0 and 1 representing animation progress
const librariesAnimation = { enterProgress: 0 };

// create a scale that maps longitudinal values to values between 0 and 1
// based on the min and max longitudes in the dataset.
const longitudeDelayScale = d3.scaleLinear()
  .domain(d3.extent(librariesData, d => d.position[0]))
  .range([1, 0]);

const librariesLayer = new DelayedPointLayer({
  id: 'points-layer',
  data: librariesData,
  getPosition: d => d.position,
  getFillColor: [60, 220, 255],
  getRadius: 50,
  radiusMinPixels: 3,

  // specify how far we are through the animation (value between 0 and 1)
  animationProgress: librariesAnimation.enterProgress,

  // specify how long a point takes to completely animate as a
  // proportion of the overall animation progress (value between 0 and 1)
  pointDuration: 0.25,

  // specify the delay factor for each point (value between 0 and 1)
  getDelayFactor: d => {
    // the longitude scale means the points will animate in
    // from right to left.
    longitudeDelayScale(d.position[0])
  },
});

Now as we update enterProgress to new values between 0 and 1 and tell deck.gl to update its layers, we will see our points animate in. A simple way to get enterProgress to animate with regular updates is to use anime.js:

const animation = anime({
  duration: 3000,
  // animate values in this object
  targets: librariesAnimation,
  // update this property to have value 1
  enterProgress: 1,
  update() {
    // each tick, update the deck.gl layers with new values
    // see full source code for details
    updateDeckLayers();
  },
});

There's plenty of other modifications you can do with the DelayPointLayer and I tend to just edit the shader for whatever I am currently looking to do on an ad-hoc basis. I encourage you to do the same! Or even better, make it more robust and release it as a reusable component for everyone to enjoy :)

Bonus: make things look fancy with additive blending

Now to really make our points pop, we can dip into the wonderful world of blending. Additive blending is when overlapping colors have their RGB values added together to determine the color that is shown on screen. This results in brighter regions when points are close together and can sometimes feel like a nice glow effect (although typically glows use blurring in addition to this).

Luckily, it doesn't take much to turn on additive blending in deck.gl, we just provide some parameters to our layer:

import GL from '@luma.gl/constants';
const librariesLayer = new DelayedPointLayer({
  ...
  parameters: {
    // prevent flicker from z-fighting
    [GL.DEPTH_TEST]: false,

    // turn on additive blending to make them look more glowy
    [GL.BLEND]: true,
    [GL.BLEND_SRC_RGB]: GL.ONE,
    [GL.BLEND_DST_RGB]: GL.ONE,
    [GL.BLEND_EQUATION]: GL.FUNC_ADD,
  },
});

Boom, roasted.

What's next?

After you get your fix animating points, maybe you'll move on to animating the ArcLayer. There's a great starting point here. I ended up using something similar to produce the following video:

Wrapping up

Thanks for reading! Hopefully you now have an idea of how to begin animating with deck.gl. To finish up the videos I made, I saved my animations using CCapture.js and added some glow to them in After Effects. If you make something after reading this, I'd love to see it— find me as @pbesh on Twitter.

Have a great day!