Wednesday 12 August 2015

Open Flights

WARNING: Large data set. Will require something with a decent processor to render.

Largely because I'm procrastinating, rather than writing up my dissertation, I've spent the afternoon prototyping the client side changes required to support orthographic projections in the d3MapRenderer plugin.

What better to trial it with than the OpenFlights data? It will also push the boundaries of what is possible. Download the data and add a Geom column with a linestring following Alasdair's guidelines, add the country shapefile from Natural Earth and then use the plugin to export the data with a simple projection such as Winkel Tripel.

OpenFlights data in the Winkel Tripel projection with Natural Earth land data. Pan and zoom around the map.

Ok, so lets try and get this into a globe based on Mike Bostock's Interactive Orthographic example. With the OpenFlights example I want to see clearly what happens to the flight paths (just out of interest) so I'll also get rid of the background as well (for now). The first thing to change is to add a new JavaScript library:

<script src="js/d3.geo.zoom.js"></script>

Then we need to change the projection, which now becomes:

//Projection
var projection = d3.geo.orthographic()
.scale(250)
.translate([width / 2, height / 2])
.clipAngle(90);

The scale will need to be chosen carefully, in order to fill the containing div element to achieve the desired effect. To that end, get rid of the re-projecting that occurs further down the JavaScript. The d3MapRenderer plugin uses this to set the scale and translation of the main layer to fit nicely in the container. Not necessary in this prototype, it will just get in the way, so remove the following lines of code:

// Refine projection
var b, s, t;
projection.scale(1).translate([0, 0]);
var b = path.bounds(object2);
var s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height);
var t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection.scale(s).translate(t);

Now, new variables and functions are needed as we're going to use d3.geo.zoom instead of d3.behaviour.zoom. The new variables are as follows:

var λ = d3.scale.linear()
  .domain([0, width])
  .range([-180, 180]);

var φ = d3.scale.linear()
  .domain([0, height])
  .range([90, -90]);

The zoom behaviour is altered to:

svg.call(d3.geo.zoom()
  .projection(projection)
  .on("zoom", onZoom));

Differing slightly from the behaviour in Mike's example, I want the user to grab the globe and rotate it, rather than the mouse simply pass over it. So we need a behaviour adding to the code to achieve this:

var dragging = 0;
svg.on("mousedown", function() { dragging = 1 })
  .on("mouseup", function() { dragging = 0 })
  .on("mousemove", function() {
    if(dragging == 1){
      var p = d3.mouse(this);
      projection.rotate([λ(p[0]), φ(p[1])]);
      svg.selectAll("path").attr("d", path);
      d3.event.preventDefault(); // disable text dragging
    }

The final change required is to simplify the zoom function, and get it to redraw the layer rather than trying to re-scale all the flight paths.

// Zoom/pan 
function onZoom() {
  svg.selectAll("path").attr("d", path);   
}

This then displays the flights draped around a sphere, which can be rotated and zoomed into. However, the result is somewhat laggy, difficult to control and not particularly satisfactory. Certainly this is due to the volume of data in the OpenFlights dataset, as Mike's original example is quite smooth.

OpenFlights data in the Orthographic projection, with the land mass removed. Pan and zoom around the globe.

A second attempt is needed. Jason Davies' Rotate the World example has a slightly different mechanism defining a new drag behaviour and method of setting the rotation angle, which essentially replaces the variables λ and φ along with my attempts rotating the projection. It is replaced by:

var drag = d3.behavior.drag()
  .on("drag", function() {
  for (var i = 0; i < projections_.length; ++i) {
    var projection = projections_[i],
    angle = rotate(projection.rotate());
    projection.rotate(angle.rotate);
  }
d3.select("#rotations")
  .selectAll("svg").each(function(d) {
    d3.select(this).selectAll("path").attr("d", d.path);
  });
});
 
function rotate(rotate) { var angle = update(rotate); return {angle: angle, rotate: rotate}; }

vectors.selectAll(".overlay").call(drag);

This is much easier to control, but still suffers from lag due to the amount of OpenFlights data. This is looking like a good template for adding Orthographic projections to the d3MapRenderer plugin in a later version.

OpenFlights data in the Orthographic projection, with the land mass put back and a better control mechanism. Pan and zoom around the globe.