Flight Animation with d3.js

APRIL 02, 2014

This is a step by step tutorial of how to create flight animation on the map, using d3.js Geo and Transition. Everything is rendered in SVG, and you can learn from creating a world map, plotting airports to animating flying planes.


A few months ago, I wrote a blog titled "Interactive Map with d3.js". This post is an extension or another variation of it. Let's go through how it's created.


1 Create TopoJson World Map Data

First, draw the world map with TopoJson data. Details were explained in this post, but quickly re-list key steps.

In this example, once again, I used Natural Earth Shapefile data. Since the focus is animating airplanes in this example, I used the least detailed (smallest file size) data, 1:110m. Once you download the "Admin 0 - Countries" zip file, convert a Shapefile into GeoJSON using GDAL. As usual, I excluded Antarctica from the map by adding this condition -where "SU_A3 <> 'ATA'".

> ogr2ogr -f GeoJSON -where "SU_A3 <> 'ATA'" countries.json ne_110m_admin_0_countries_lakes.shp


Now, let's convert GeoJSON to TopoJson in order to further reduce the file size.

> topojson --id-property SU_A3 -p name=NAME -p name -o countries.topo.json countries.json


That's it for the map data preparation.


2 Create TopoJson Airport Data

Airport data are also available in Natural Earth, and we do the exact same process: Shapefile > GeoJSON > TopoJson. Download the airport file from here. Since the data contain hundreds of airports around the globe, let's just take the major airports (scalerank < 6). I also keep the three-letter airport codes as ID by issuing the following commands:

> ogr2ogr -f GeoJSON -where "scalerank < 6" airports.json ne_10m_airports.shp
> topojson --id-property abbrev -o airports.topo.json airports.json


This still generated more than 300 airports. In my example, I refined airports to around 30 by handpicking major airports.


3 Draw Map and Airports

Now that we got both map and airport data, let's draw them using d3.js. d3.geo package is a comprehensive library for building amazing SVG map applications. Here is CSS and Javascript code:

<style>
.countries {
  fill: #b0d0ab;
  stroke: #6cb0e0;
  stroke-width: 0.5px;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.airports {
  fill: #036;
  stroke: #6cb0e0;
  stroke-width: 0.5px;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
</style>
...
<script>
var width = 938;
var height = 620;

var projection = d3.geo
                   .mercator()
                   .scale(150)
                   .translate([width / 2, height / 2]);

var path = d3.geo
             .path()
             .pointRadius(2)
             .projection(projection);
  
var svg = d3.select("#map")
            .append("svg")
            .attr("width", width)
            .attr("height", height);
...

function loaded(error, countries, airports) {
  svg.append("g")
     .attr("class", "countries")
     .selectAll("path")
     .data(topojson.feature(countries, countries.objects.countries).features)
     .enter()
     .append("path")
     .attr("d", path);

  svg.append("g")
     .attr("class", "airports")
     .selectAll("path")
     .data(topojson.feature(airports, airports.objects.airports).features)
     .enter()
     .append("path")
     .attr("id", function(d) {return d.id;})
     .attr("d", path);
  ...
}

queue().defer(d3.json, "countries.topo.json")
       .defer(d3.json, "airports.topo.json")
       .await(loaded);
</script>


First of all, it loads two TopoJson files (map and airports), so rendering process is synchronized by using d3.js Queue. Secondly, you may wonder why both map and aiports are rendered as "path". It may make sense for country borders, but airports should rather be a SVG circle. In this case, instead of defining shapes in SVG, d3.geo relies on GeoJSON's geometry object types. For country borders, the geometry is "Polygon" or "MultiPolygon" while airports are "Point". If you look at TopoJson files, you can see "type" attribute. That determines the shape of objects. In SVG elements, however, both are rendered as "path".



4 Draw a plane in SVG

Before developing animation/transition, we need to have an airplane. You can use either a pixel image such as GIF or PNG, or render it in SVG. Since my example is designed responsive, it's probably better to use SVG so that I can scale it smoothly. There are many free SVG shapes available online, or create your own vector image and export it as SVG.

Here is the actual SVG code:

<svg width="50" height="50">
  <path d="m25.21488,3.93375c-0.44355,0 -0.84275,0.18332 -1.17933,0.51592c-0.33397,0.33267 -0.61055,0.80884 -0.84275,1.40377c-0.45922,1.18911 -0.74362,2.85964 -0.89755,4.86085c-0.15655,1.99729 -0.18263,4.32223 -0.11741,6.81118c-5.51835,2.26427 -16.7116,6.93857 -17.60916,7.98223c-1.19759,1.38937 -0.81143,2.98095 -0.32874,4.03902l18.39971,-3.74549c0.38616,4.88048 0.94192,9.7138 1.42461,13.50099c-1.80032,0.52703 -5.1609,1.56679 -5.85232,2.21255c-0.95496,0.88711 -0.95496,3.75718 -0.95496,3.75718l7.53,-0.61316c0.17743,1.23545 0.28701,1.95767 0.28701,1.95767l0.01304,0.06557l0.06002,0l0.13829,0l0.0574,0l0.01043,-0.06557c0,0 0.11218,-0.72222 0.28961,-1.95767l7.53164,0.61316c0,0 0,-2.87006 -0.95496,-3.75718c-0.69044,-0.64577 -4.05363,-1.68813 -5.85133,-2.21516c0.48009,-3.77545 1.03061,-8.58921 1.42198,-13.45404l18.18207,3.70115c0.48009,-1.05806 0.86881,-2.64965 -0.32617,-4.03902c-0.88969,-1.03062 -11.81147,-5.60054 -17.39409,-7.89352c0.06524,-2.52287 0.04175,-4.88024 -0.1148,-6.89989l0,-0.00476c-0.15655,-1.99844 -0.44094,-3.6683 -0.90277,-4.8561c-0.22699,-0.59493 -0.50356,-1.07111 -0.83754,-1.40377c-0.33658,-0.3326 -0.73578,-0.51592 -1.18194,-0.51592l0,0l-0.00001,0l0,0l0.00002,0.00001z" stroke-width="0" fill="#73b6e6"/>
</svg>


Or create it through d3.js:

var plane = svg.append("path")
                   .attr("class", "plane")
                   .attr("d", "m25.21488,3.93375c-0.44355,0 -0.84275,0.18332 -1.17933,0.51592c-0.33397,0.33267 -0.61055,0.80884 -0.84275,1.40377c-0.45922,1.18911 -0.74362,2.85964 -0.89755,4.86085c-0.15655,1.99729 -0.18263,4.32223 -0.11741,6.81118c-5.51835,2.26427 -16.7116,6.93857 -17.60916,7.98223c-1.19759,1.38937 -0.81143,2.98095 -0.32874,4.03902l18.39971,-3.74549c0.38616,4.88048 0.94192,9.7138 1.42461,13.50099c-1.80032,0.52703 -5.1609,1.56679 -5.85232,2.21255c-0.95496,0.88711 -0.95496,3.75718 -0.95496,3.75718l7.53,-0.61316c0.17743,1.23545 0.28701,1.95767 0.28701,1.95767l0.01304,0.06557l0.06002,0l0.13829,0l0.0574,0l0.01043,-0.06557c0,0 0.11218,-0.72222 0.28961,-1.95767l7.53164,0.61316c0,0 0,-2.87006 -0.95496,-3.75718c-0.69044,-0.64577 -4.05363,-1.68813 -5.85133,-2.21516c0.48009,-3.77545 1.03061,-8.58921 1.42198,-13.45404l18.18207,3.70115c0.48009,-1.05806 0.86881,-2.64965 -0.32617,-4.03902c-0.88969,-1.03062 -11.81147,-5.60054 -17.39409,-7.89352c0.06524,-2.52287 0.04175,-4.88024 -0.1148,-6.89989l0,-0.00476c-0.15655,-1.99844 -0.44094,-3.6683 -0.90277,-4.8561c-0.22699,-0.59493 -0.50356,-1.07111 -0.83754,-1.40377c-0.33658,-0.3326 -0.73578,-0.51592 -1.18194,-0.51592l0,0l-0.00001,0l0,0z");


5 Draw Flight Route/Path

When you look at flight animations carefully, you realized that they fly over an arch path, not a straight line. This is because the map is projected on Mercator and it's actually a straight line on Earth (cube). If I use a different projection, a flying path will change. Of course, you don't want to draw arcs for all possible airport combinations. Fortunately, d3.js geo does a heavy lifting for us.


You can draw an arc (flight path) between two airports like this:

var route = svg.append("path")
               .datum({type: "LineString", coordinates: [origin, destination]})
               .attr("class", "route")
               .attr("d", path);


Arc is drawn as SVG Path, but it's LineString geometry type in GeoJSON. As coordinates, I passed two points, origin and destination, and a route is determined by a "path" object passed in "d" value. If you look at an example code in section 3, the path and projection objects are created there. If we make a path visible, it looks like this:

Flight route arcs


6 Animate Plane

Once we figure out flight routes, all we have to do is to move airplanes along a path. Using d3.js Transition, the animation should be straightforward. Here is the transition code snippet, and the key is attrTween() and path.getPointAtLength().

function transition(plane, route) {
  var l = route.node().getTotalLength();
  plane.transition()
       .duration(5000)
       .attrTween("transform", delta(route.node()));
}
  
function delta(path) {
  var l = path.getTotalLength();
  return function(i) {
    return function(t) {
      var p = path.getPointAtLength(t * l);
      return "translate(" + p.x + "," + p.y + ")";
    }
  }
}

...

transition(plane, route);


The passed function to attrTween is invoked repeatedly during the duration. Invocation interval is a tween function (easing in/out), so it's ideal for flight animation (flight is slow at taking off and landing). Inner function in the delta function has an argument, t. This value is between 0 and 1, in other words, percentage. By multiplying the total arc length, getPointAtLength(length) will return a coordinate. That's where we move an airplane. transform = translate(x, y) does the move.


7 Rotate and Up/Down Plane

The code above enables flight animation, but there is a few problems. First of all, airplane's head is pointing at North all the time. It's moving but does look like flying. The head should point to a moving direction. Secondly, it might be better if we can express takeoff and landing in animation by changing airplane scale. Scaling shows 3D animation in two dimension.


Plane rotation can be achieved by finding two points on the path and calculate an angle from North. Here is an example code:

var p = path.getPointAtLength(t * l);
var t2 = Math.min(t + 0.05, 1);
var p2 = path.getPointAtLength(t2 * l);

var x = p2.x - p.x;
var y = p2.y - p.y;
var r = 90 - Math.atan2(-y, x) * 180 / Math.PI;

return "translate(" + p.x + "," + p.y + ") rotate(" + r + ")";


By adding rotate to transform, we make a plane point to a flying direction. For takeoff and landing, you can use any kinds of concave functions that express plane altitude (zoom in/out). I used Sin function with maximum y limit.

...
var s = Math.min(Math.sin(Math.PI * t) * 0.7, 0.3);
return "translate(" + p.x + "," + p.y + ") scale(" + s + ") rotate(" + r + ")";


Now, you can see a plane rotation as well as takeoff/landing. There are more improvements you can make, such as shadowing, fixing rotation axis..., but I'll leave them to you.


It's all in CSS, JSON and Javascript, so feel free to take my code from this page! Happy coding!


Related Posts

Interactive Map with d3.js

This is a step by step tutorial of how to make responsive, interactive and zoomable map with d3.js. Converting Shapefile into GeoJSON then Topojson, you can build web/mobile-ready map application.

SVG-based Sparkline with d3.js

This is a tutorial of how to create various SVG based sparklines with d3.js. Sparkline, a tiny line chart, is often very effective and visually appealing, especially for today's small screen devices.

Responsive d3.js

It's increasingly important to make d3.js charts responsive for touch-based devices such as mobile and tablet. While this approach doesn't solve everything, it's one way of making SVG based charts dynamic and responsive.