First, we set up a few global variables:
var width = 1000,
height = 600,
countries,
airportMap,
requestID;
width and height speak for themselves. Countries will hold the GeoJSON data to draw the globe, which needs to be reached from various function scopes. Hence, it's easier to define it as a global variable in this small app. airportMap will allow us to join the airport with the routes data by the three-letter IATA code. requestID will be filled by our loop function requestAnimationFrom() and used to cancel the current loop. We shall get to this in no time.
We then set up the two contexts: a context for the world and a context for the planes. This little extra work at the beginning makes our life much easier later. If we drew the world and the planes on the same context, we would have to update both the world and the planes every time a plane flies a short distance. Keeping the world on a separate canvas means we only have to draw the world once and can leave that image/context untouched:
var canvasWorld = d3.select('#canvas-map').append('canvas')
.attr('id', 'canvas-world')
.attr('width', width)
.attr('height', height);
var contextWorld = canvasWorld.node().getContext('2d');
var canvasPlane = d3.select('#canvas-map').append('canvas')
.attr('id', 'canvas-plane')
.attr('width', width)
.attr('height', height);
var contextPlane = canvasPlane.node().getContext('2d');
We use absolute CSS positioning for the canvases to stack them perfectly on top of each other:
#canvas-world, #canvas-plane {
position: absolute;
top: 0;
left: 0;
}
Next, we set up the projection:
var projection = d3.geoRobinson()
.scale(180)
.translate([width / 2, height / 2]);
Please note that instead of playing with .scale() and .translate() to center and fit your projection, you can use the D3 convenience methods .fitExtent() or .fitSize(). You pass them your viz dimensions and the GeoJSON object you want to project and it calculates the best scale and translation for you.
Also notice that we don't use the omnipresent Mercator projection but the Robinson projection for our world map. It has the advantage of drawing the world in a slightly more realistic way in terms of country size proportions. The Robinson and many more non-standard projections can be found in the additional d3-geo-projection module.
Now we need a path generator. In fact, you will need to build two path generators:
var pathSVG = d3.geoPath()
.projection(projection);
var pathCanvas = d3.geoPath()
.projection(projection)
.pointRadius(1)
.context(contextWorld);
pathSVG will be used to generate the flight path in memory. We want to do that in SVG as it comes with handy methods to calculate its length and sample points from it. pathCanvas will be used to draw our geo data to the screen. Note that we add d3.geoPath()'s .context() method and pass it our contextWorld. If we pass a Canvas context to this .context() method, the path generator will return a Canvas path for the passed context. If it's not specified it will return an SVG path string. You can think of it as a switch button to tell D3 which renderer to use.