Deck GL Time Frame Animations

Introduction

In my previous article, I covered a few different examples of using DeckGL with static data. In this article, I will demonstrate creating an animated visualization of NYC Taxi trip data.

The Data

We will use a pre-formatted version of the NYC Taxi Trip data from the VisGL team. A single record in the dataset contains three fields: vendor, path, and timestamps.

{
  "vendor": 0,
  "path": [
    [-74.20986, 40.81773],
    [-74.20987, 40.81765],
    ...
  ],
  "timestamps": [1191, 1193.803, ...]
}

Analysis of the Data

  • The vendor field will be used for color differentiation.

  • The path and timestamp fields are crucial for the TripsLayer to know which point to draw based on the map's current time.

Trips Layer

We'll use the TripsLayer from DeckGL, defined as follows:

new TripsLayer({
  id: 'trips',
  data: '/data/detailed-trips.json',
  getPath: d => d.path,
  getTimestamps: d => d.timestamps,
  getColor: d => (d.vendor === 0 ? [253, 128, 93] : [23, 184, 190]),
  opacity: 0.5,
  widthMinPixels: 3,
  rounded: true,
  trailLength: 150,
  currentTime: 0,
})

Animate Trips

To automate the stepping through time, we can use a setInterval or window.requestAnimationFrame.

Using setInterval

const [time, setTime] = useState(0);
useEffect(() => {
  const interval = setInterval(() => {
    setTime(t => (t + step) % loopLength);
  }, intervalMS);
  return () => clearInterval(interval);
}, []);

Using window.requestAnimationFrame

This method provides smoother animations and is more efficient for complex visualizations.

const [animation] = useState({});
const animate = () => {
  setTime(t => (t + step) % loopLength);
  animation.id = window.requestAnimationFrame(animate);
};
useEffect(() => {
  if (!running) {
    window.cancelAnimationFrame(animation.id);
    return;
  }
  animation.id = window.requestAnimationFrame(animate);
  return () => window.cancelAnimationFrame(animation.id);
}, [running]);

React Component Implementation

import React, { useState, useEffect } from 'react';
import DeckGL from 'deck.gl';
import { TripsLayer } from '@deck.gl/geo-layers';
import { StaticMap } from 'react-map-gl';
const { assign } = Object;
import { isMobile } from '../util/mobile';
import config from '../config';

const mapStyle = 'mapbox://styles/mapbox/light-v9';
const mapboxApiAccessToken = config('mapboxApiAccessToken');

const initialStyle = {
    position: "relative",
    width: "100%",
    height: "550px",
    border: "1px solid black",
};

const mobileStyle = {
    height: "300px",
}

// Viewport settings
const initialViewState = {
    latitude: 40.71460213064598,
    longitude: -73.97744746237277,
    zoom: 11.5,
    minZoom: 2,
    maxZoom: 15,
    pitch: 0,
    bearing: 0,
};

const mobileViewState = {
    latitude: 40.72491632450353,
    longitude: -73.98669446445103,
    zoom: 11.38228886864759,
};

const BLUE = [23, 184, 190];
const RED = [253, 128, 93];

// variables
const step = 1;
const loopLength = 2500;
let loopRunning = false;

const DeckGLTripsAnimateMap = ({ running }) => {
    const [style, setStyle] = useState(assign({}, initialStyle));
    const [viewState, setViewState] = useState(initialViewState);
    const [time, setTime] = useState(0);
    const [animation] = useState({});

    useEffect(() => {
        if (isMobile()) {
            setStyle(assign({}, initialStyle, mobileStyle))
            setViewState(assign({}, initialViewState, mobileViewState))
        }
    }, []);

    const animate = () => {
        if (loopRunning) {
            // use variable outside of closure to allow toggle
            setTime(t => (t + step) % loopLength);
            animation.id = window.requestAnimationFrame(animate); // draw next frame
        }
    };

    useEffect(() => {
        if (!running) {
            loopRunning = false;
            window.cancelAnimationFrame(animation.id);
            return;
        }
        loopRunning = true;
        animation.id = window.requestAnimationFrame(animate); // start animation
        return () => {
            loopRunning = false;
            window.cancelAnimationFrame(animation.id);
        };
    }, [running]);

    const layers = [
        new TripsLayer({
            id: 'trips',
            data: '/data/detailed-trips.json',
            getPath: d => d.path,
            getTimestamps: d => d.timestamps,
            getColor: d => (d.vendor === 0 ? RED : BLUE),
            opacity: 0.5,
            widthMinPixels: 3,
            rounded: true,
            trailLength: 180,
            currentTime: time,
        }),
    ];

    return (
        <DeckGL
            controller
            viewState={viewState}
            layers={layers}
            style={style}
            onViewStateChange={
                (nextViewState) => {
                    setViewState(nextViewState.viewState);
                }
            }
        >
            <StaticMap mapboxApiAccessToken={mapboxApiAccessToken} mapStyle={mapStyle}>
                <div style={{ margin: "0.5rem", fontFamily: "monospace", fontSize: "18px" }}>
                    Current Time: {time}
                </div>
            </StaticMap>
        </DeckGL>
    );
}

export default DeckGLTripsAnimateMap;

Last updated