We set up the zoom behavior, meaning that our Canvas is
- listening on every mouse wheel and drag
- firing the zoomed() handler on each of these events
Let’s now fill our handler to do something to the globe.
What do we want to do? From a bird’s perspective, for each zoom, we want to establish the scale for the projection, apply it to the path, and redraw the globe a little bit bigger or a little smaller. For each drag, we would want to establish the new rotation values, apply them to the projection and path, and redraw the globe a little bit rotated. To get there, the handler should distinguish between a zoom and a drag. A zoom should lead to a projection-scale change and a drag should lead to a rotation change. For each path, you calculate the position change. Once that’s done, you will need to redraw the globe. It’s the game-loop mantra: process user-input, clear the Canvas, then redraw the Canvas with updated data.
Let’s start with the zoom action:
function zoomed() {
var event = d3.event.sourceEvent.type;
if (event === 'wheel') {
var transformScale = d3.event.transform.k;
projectionScaleChange = (transformScale – prevTransformScale) *
origProjectionScale;
projectionScale = projectionScale + projectionScaleChange;
projection.scale(projectionScale);
prevTransformScale = transformScale;
} else if (event === 'mousemove'){
// Here goes the rotation logic as this will be triggered upon dragging
}
requestAnimationFrame(function() {
renderScene(countries);
});
}
At first, we will need to distinguish between a zoom and a drag event. D3 makes this easy for us with the d3.event object holding a sourceEvent property specifying what event type the user triggers. If it’s a wheel event, we change the scale if it's a mousemove event, we change the rotation. Simple.
Changing the scale looks involved but is pretty straightforward. Before we dive into the code, let’s make one important distinction. The projection has a scale, and the transformation upon user zoom also has a scale. However, they are different. Projection scales differ between projections. Our d3.geoOrthographic() projection has an initial scale of around 286 (we set it to height / 2.1 = 286). Our transformation has an initial scale of 1. That’s the default.
So, you retrieve the current transformScale via d3.transform.k. You note down the change of this scale to the previous transform scale, which can be negative for zoom in or positive for zoom out. However, as your projection scale is quite a big number (as in 286 to start with) and the transform scale change per zoom will be small (for a normal mouse wheel turn the change might be around 0.005), you will want to boost this number up to get a noticeable change in your projection. Hence, you will multiply it with a larger number. You can choose any large number you like, but choosing your initial projection scale we called origProjectionScale allows you to port this calculation over to any other projection, and it should work nicely. You then just change the current projectionScale by this projectionScaleChange.
The rest is simple. Just apply it to your globe’s projection with projection.scale(projectionScale), set the previous transform scale to the updated transform scale, and re-render the globe. Note, that you don’t need to update the path generator, as whenever it’s called, it will use the projection as is at the moment of being called, which we’ve changed accordingly.
That was the hard bit. Rotating is even simpler. You just need to track the changes in the user’s mouse movements and apply them to D3’s projection.rotate() parameters. Let’s track the change in mouse coordinates right at the top of the zoomed() handler:
function zoomed(
var dx = d3.event.sourceEvent.movementX;
var dy = d3.event.sourceEvent.movementY;
// all the rest
Note, that the two MouseEvent properties .movementX and .movementY are not available in Safari or Internet Explorer. You can see a cross-browser implementation, which is calculating the two values on the fly in code example 10_02.html at https://github.com/larsvers/Learning-D3.js-4-Mapping.
The rotation logic will trigger when the user drags or rather triggers the mousemove event, which goes into the else if part of our conditional:
if (event === ‘wheel’) {
// here goes the zoom logic described previously
} else if (event === ‘mousemove’) {
var r = projection.rotate();
rotation = [r[0] + dx * 0.4, r[1] - dy * 0.5, r[2]];
projection.rotate(rotation);
} else {
console.warn('unknown mouse event in zoomed()'); // alerting issues
}
In the preceding code, we first retrieve the current rotation values from the projection in the variable r. Then, you change the r[0], the yaw value (responsible for rotating the world around its normal or vertical axis) by the x change of the mouse coordinates. You further change r[1], the roll value (rotating the world around its lateral axis, going horizontally from left to right) by the y change of the mouse coordinates. We leave the third pitch value as is, and yes, the best way is to throttle these values to a reasonable speed of rotation with dx * 0.4 and dy * 0.5, respectively. Note that this is the straightforward but naive way to rotate the globe. It will fully do in our case. If you want to apply utmost precision, you could use versor dragging (check out http://tiny.cc/versor). The key difference is that versor dragging rotates the globe in the right direction even when it's upside down.
That’s it for rotation. Remember, the world gets re-rendered after this conditional as we do this in the following Canvas game loop: get user input – calculate the new position – re-render.
Here’s a static attempt to show dynamic zooming and rotations:

The main benefit of zooming via a projection change is that it allows rotating (that’s a win) and guarantees semantic zooming of the world rather than geometric zooming. When you zoom into a Canvas object with context.scale(), it naively enlarges anything that’s on the Canvas. So, a country border, for example, gets wider and wider, the more you scale it. That’s geometric zooming. We, however, want to keep everything constant, apart from the area of the individual country polygons. This is called semantic zooming. Another benefit of the projection change is that getting the coordinate of a Canvas object by mouse-over is more straightforward. This is our next stop.