Uber has built a cool suite of data visualization tools for WebGL. Let's explore with a real-time dataset of global airplane positions.
How it works āļø
Giving up on luma.gl as too low level, we tried something else: Deck.gl. Same suite of WebGL React tools from Uber but higher level and therefore more fun.
Of course Deck.gl is built for maps so we had to make a map. What better way to have fun with a map than drawing live positions of all airplanes in the sky?
All six thousand of them. Sixty times per second.
Yes we can! šŖ
This is the plan:
- Fetch data from OpenSky
- Render map with react-map-gl
- Overlay a Deck.gl IconLayer
- Predict each airplane's position on the next Fetch
- Interpolate positions 60 times per second
- Update and redraw
Out goal is to create a faux live map of airplane positions. We can fetch real positions every 10 seconds per OpenSky usage policy.
You can see the full code on GitHub. No Codesandbox today because it makes my computer struggle when WebGL is involved.
See the airplanes in your browser š click me š©
Fetch data from OpenSky
OpenSky is a receiver network which continuously collects air traffic surveillance data. They keep it for forever and make it available via an API.
As an anon user you can get real-time data of all the world's airplanes current positions every 10 seconds. With some finnagling you can get historic data, super real-time stuff, and so on. We don't need any of that.
We fetchData
in componentDidMount
. Parse each entry into an object, update local state, and start the animation. Also schedule the next fetch.
componentDidMount() {this.fetchData();}fetchData = () => {d3.json("https://opensky-network.org/api/states/all").then(({ states }) =>this.setState({// from https://opensky-network.org/apidoc/rest.html#responseairplanes: states.map(d => ({callsign: d[1],longitude: d[5],latitude: d[6],velocity: d[9],altitude: d[13],origin_country: d[2],true_track: -d[10],interpolatePos: d3.geoInterpolate([d[5], d[6]],destinationPoint(d[5],d[6],d[9] * this.fetchEverySeconds,d[10]))}))},() => {this.startAnimation();setTimeout(this.fetchData,this.fetchEverySeconds * 1000);}));};
d3.json
fetches JSON data from a URL, returns a promise. We map through the data and assign indexes to representative object keys. Makes the other code easier to read.
In the setState
callback, we start the animation and use a setTimeout
to call fetchData
again in 10 seconds. More about teh animation in a bit.
Render map with react-map-gl
Turns out rendering a map with Uber's react-map-gl is really easy. The library does everything for you.
import { StaticMap } from 'react-map-gl'import DeckGL, { IconLayer } from "deck.gl";// Set your mapbox access token hereconst MAPBOX_ACCESS_TOKEN = '<your token>'// Initial viewport settingsconst initialViewState = {longitude: -122.41669,latitude: 37.7853,zoom: 5,pitch: 0,bearing: 0,}// ...<DeckGLinitialViewState={initialViewState}controller={true}layers={layers}><StaticMap mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} /></DeckGL>
That is all.
You need to create a Mapbox account and get your token, the initialViewState
I copied from Uber's docs. It points to San Francisco.
In the render method you then return <DeckGL
which sets up the layering stuff, and plop a <StaticMap>
inside. This gives you pan and zoom behavior out of the box. I'm sure with some twiddling you could get cool views and rotations and all sorts of 3D stuff.
I say that because I've seen pics in Uber docs :P
Overlay a Deck.gl IconLayer
That layers
prop needs a list of layers. You're meant to create a new copy on every render, but internally Deck.gl promises to keep things memoized and figure out a minimal set of changes necessary. How they do that I don't know and as long as it works it doesn't really matter how.
We configure the icon layer like this:
import Airplane from './airplane-icon.jpg'const layers = [new IconLayer({id: 'airplanes',data: this.state.airplanes,pickable: false,iconAtlas: Airplane,iconMapping: {airplane: {x: 0,y: 0,width: 512,height: 512,},},sizeScale: 20,getPosition: d => [d.longitude, d.latitude],getIcon: d => 'airplane',getAngle: d => 45 + (d.true_track * 180) / Math.PI,}),]
We name it airplanes
because it's showing airplanes, pass in our data, and define the airplane icon. iconAtlas
is a sprite and the mapping specifies which parts of the image map to which name. With just one icon in the image that's pretty quick.
We use getPosition
to fetch longitude and latitude from each airplane and pass it to the drawing layer. getIcon
specifies that we're rendering the airplane
icon and getAngle
rotates everything first by 45 degrees because our icon is weird, and then by the direction of the airplane from our data.
true_track
is the airplane's bearing in radians so we transform it to degrees with some math.
Predict airplanes' next position
Predicting each airplane's position 10 seconds from now is ... mathsy. Positions are in latitudes and longitudes, velocities are in meters per second.
I'm not so great with spherical euclidean maths so I borrowed the solution from StackOverflow and made some adjustments to fit our arguments.
We use that to create a d3.geoInterpolate
interpolator between the start and end point. That enables us to feed in numbers between 0 and 1 and get airplane positions at specific moments in time.
interpolatePos: d3.geoInterpolate([d[5], d[6]],destinationPoint(d[5], d[6], d[9] * this.fetchEverySeconds, d[10]))
Gobbledygook. Almost as bad as the destinationPoint function code
Interpolate and redraw
With that interpolator in hand, we can start our animation.
currentFrame = nulltimer = nullstartAnimation = () => {if (this.timer) {this.timer.stop()}this.currentFrame = 0this.timer = d3.timer(this.animationFrame)}animationFrame = () => {let { airplanes } = this.stateairplanes = airplanes.map(d => {const [longitude, latitude] = d.interpolatePos(this.currentFrame / this.framesPerFetch)return {...d,longitude,latitude,}})this.currentFrame += 1this.setState({ airplanes })}
We use a d3.timer
to run our animationFrame
function 60 times per second. Or every requestAnimationFrame
. That's all internal and D3 figures out the best option.
Also gotta make sure to stop any existing timers when running a new one :)
The animationFrame
method itself maps through the airplanes and creates a new list. On each iteration we copy over the whole datapoint and use the interpolator we defined earlier to calculate the new position.
To get numbers from 0 to 1 we try to predict how many frames we're gonna render and keep track of which frame we're at. So 0/60 gives 0, 10/60 gives 0.16, 60/60 gives 1 etc. The interpolator takes this and returns geospatial positions along that path.
Of course this can't take into account any changes in direction the airplane might make.
Updating component state triggers a re-render.
And that's cool
What I find really cool about all this is that even though we're copying and recreating and recalculating and ultimately redrawing some 6000 airplanes it works smoothly. Because WebGL is more performant than I ever dreamed possible.
We could improve performance further by moving this animation out of React state and redraw into vertex shaders but that's hard and turns out we don't have to.
About the Author
Hi, Iām Swizec Teller. I help coders become software engineers.
Story time š
React+D3 started as a bet in April 2015. A friend wanted to learn React and challenged me to publish a book. A month later React+D3 launched with 79 pages of hard earned knowledge.
In April 2016 it became React+D3 ES6. 117 pages and growing beyond a single big project it was a huge success. I kept going, started live streaming, and publishing videos on YouTube.
In 2017, after 10 months of work, React + D3v4 became the best book I'd ever written. At 249 pages, many examples, and code to play with it was designed like a step-by-step course. But I felt something was missing.
So in late 2018 I rebuilt the entire thing as React for Data Visualization ā a proper video course. Designed for busy people with real lives like you. Over 8 hours of video material, split into chunks no longer than 5 minutes, a bunch of new chapters, and techniques I discovered along the way.
React for Data Visualization is the best way to learn how to build scalable dataviz components your whole team can understand.
Some of my work has been featured in š