Drag Interaction

D3 provides data visualizers powerful abilities for designing complex and sophisticated interactions.

Building interaction into a static Orthographic Projection map, like was built in the last tutorial, is straightforward. Let's start with the base code.

//general variables
var width = 800;
var height = 600;

//setup the projection
var projection = d3.geo.orthographic()
    .center([0, 0])
    .scale(200)
    .translate([width/2,height/2])
    .rotate([0,0,-23.5])
    .clipAngle(90)
    ;

//make svg container
var svg = d3.select("body")
    .append("svg")
    .attr("width", width)
    .attr("height", height)
    ;

//load d3 path generator
var path = d3.geo.path()
    .projection(projection)
    ;

//create sphere to serve as ocean and map outline
svg.append("path")
  .datum({type: "Sphere"})
  .attr("class", "outline")
  .attr("d", path)
  ;

//create svg group to hold objects
var g = svg.append("g");

// load world map!
d3.json("world-110m.json", function(error, topology) {
    g.selectAll("path")
      .data(topojson.object(topology, topology.objects.countries).geometries)
      .enter()
      .append("path")
      .attr("d", path)
      ;

    //setup Graticules
    var graticule = d3.geo.graticule()
                    .step([18, 18])
                    ;

    //draw Graticules above map
    g.append("path")
        .datum(graticule)
        .attr("class", "graticule")
        .attr("d", path)
        ;
});

With some styling, this renders like so. Note that the third value of the rotation parameter of the D3 projection has been set to 23.5° to simulate the tilt of the Earth during the Northern Hemisphere's winter.

Adding interaction is trivial once all of this code is in place. With D3, and most computer programming, interaction is simluated by redrawing all of the content with an adjusted set of inputs.

The values in the rotation parameter of the projection can be increased and decreased to reposition the landmasses, exactly like what is desired in our mouse interaction.

//setup the projection
var projection = d3.geo.orthographic()
    .center([0, 0])
    .scale(200)
    .translate([width/2,height/2])
    .rotate([180,180,-23.5])
    .clipAngle(90)
    ;

D3 has some easy-to-use, built-in handlers for mouse and touch interaction. These are called behaviors and there exists documentation on their advantages and limitations.

The code for enabling drag is fairly straightforward. We capture the rotation at the origin of the drag click, and adjust the rotation based on D3's ability to track the mouse around the screen with d3.event.x and d3.event.y.

This code gets added to the definition

//basic map drawing from above...
d3.json("http://idspaceviz.com/user/pages/01.blog/circles-great-and-small/world-110m.json", function(error, topology) {
    g.selectAll("path")
      .data(topojson.object(topology, topology.objects.countries).geometries)
      .enter()
      .append("path")
      .attr("d", path)

//enable drag behavior on the drawn map
.call(d3.behavior.drag()
    .origin(function() { 

        //get current rotation values on the beginning of the drag
        var currentRotation = projection.rotate(); 
        return {x: currentRotation[0], y: currentRotation[1]}; 
    })

    .on("drag", function() {
        //set new projection rotation values.
        //note that the final rotation axis is left alone

        projection.rotate([d3.event.x, d3.event.y, -23.5]); 

        //redraw all paths with newly updated rotation values
        svg.selectAll("path").attr("d", path);
    })
);

Click and drag the continents, it works! But, there are some obvious problems

  • The y-axis interaction seems inverted, since increasing the angle of rotation rotates the map upwards
  • The globe spins very quickly, as 1 pixel of motion is rotating the map 1 degree
  • The ocean isn't interactive

These can be easily solved with some arithmetic manipulation and a few code additions.

  • Adding a negative sign at the two references to y-axis rotation solves rotation inversion.
  • A coefficient variable can adjust how many degrees the projection rotates as the mouse moves.
  • Copying and pasting the drag behavior code to the Sphere path code enables dragging on the ocean.
//new variable to serve as dragging speed coefficient
var sensitivity = .25;

//basic map drawing from above...
d3.json("world-110m.json", function(error, topology) {
    g.selectAll("path")
      .data(topojson.object(topology, topology.objects.countries).geometries)
      .enter()
      .append("path")
      .attr("d", path)

//enable drag behavior on the drawn map
.call(d3.behavior.drag()
    .origin(function() { 

        //get current rotation values on the beginning of the drag
        var currentRotation = projection.rotate(); 
        return {x: currentRotation[0], y: -currentRotation[1]}; 
    })

    .on("drag", function() {
        //set new projection rotation values.
        //note that the final rotation axis is left alone
        //sensitivity has been added as a coefficient to slow down the motion

        projection.rotate([d3.event.x * sensitivity, -d3.event.y * sensitivity, -23.5]);    

        //redraw all paths with newly updated rotation values
        svg.selectAll("path").attr("d", path);
    })
);

Drag the map!