Zoom

Despite the name, the zoom behavior lets you do more than just zoom--you can also pan! Like the drag behavior, zoom automatically handles both mouse and touch events and then triggers the higher-level zoom event. Yes, that includes pinch-to-zoom!

Remember that map from Chapter 4, Making Data Useful--the one with the rendition flights? Let's add the zoom and pan behavior to it!

You attach the zoom behavior to an element, which then fires an event when the user interacts with that element in a zoom-y way. This results in a transform value, which you can use to either apply a transform to the container element, or use for reprojecting geometry. We'll start with the latter.

Comment out everything in chapter5/index.js and add the following:

import { addZoomBehavior } from './zoomMap'; 

(async (enabled) => {
if (!enabled) return;
addZoomBehavior();
})(true);

Next, create lib/chapter5/zoomMap.js and add the following:

import * as d3 from 'd3'; 
import '../chapter4';

const zoomMap = {};

const NE_SCALE = 200;
const projection = d3.geoEquirectangular()
.center([-50, 56])
.scale(NE_SCALE);

We import the entirety of chapter4/index.js here, which only had the geoDemo IIFE remaining. In this instance, we designed things pretty badly, because it means that we don't have access to any of our chart object's internal state anymore. We can either go back and have chapter4/index.js export geoDemo, or we can throw caution to the wind and just work with what we have, applying the zoom behavior to an existing chart after it's rendered. Even though the first option is far better in terms of general maintenance, let's instead do the latter - it's pretty straightforward and demonstrates that you can use D3 to manipulate SVG elements that have been created elsewhere.

That said, we need to replicate our projection from earlier so that we can accurately reproject our updated paths. The preceding projection is the same as we have in chapter4/index.

Let's add the zoom behavior now. Insert the following in zoomMap.js:

zoomMap.addZoomBehavior = () => { 
const chart = d3.select('#chart');

const zoom = d3.zoom()
.scaleExtent([0.5, 2])
.on('zoom', zoomMap.onZoom);

const center = projection(projection.center());

chart.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(center[0], center[1])
);
};

We instantiate the zoom behavior and define the minimum and maximum scale values using scaleExtent() before attaching a zoom event callback. We then calculate the center of the projection by supplying the original center values to our projection, which we'll use in the next line when we call() the zoom behavior on our chart. Note how we actually use .call() twice--the second time, we use our zoom behavior's transform method to set the initial zoom transform, to which we supply the center of our projection. If we don't do this, the initial interaction will be jumpy as the initial reprojection will not be calibrated to the chart's initial projection.

Note that in D3 v3, you would set the initial zoom vector using zoom.scale() and zoom.translate(); these have been removed in favor of the transform syntax used previously.

Next, let's flesh out the event callback:

zoomMap.onZoom = () => { 
const { x, y, k } = d3.event.transform;
projection
.scale(k * NE_SCALE)
.translate([x, y]);

d3.selectAll('path')
.attr('d', d3.geoPath().projection(projection));

d3.selectAll('line.route')
.attr('x1', d => projection([d.from.lon, d.from.lat])[0])
.attr('y1', d => projection([d.from.lon, d.from.lat])[1])
.attr('x2', d => projection([d.to.lon, d.to.lat])[0])
.attr('y2', d => projection([d.to.lon, d.to.lat])[1]);
};

The zoom transform properties are supplied via d3.event.transform; we destructure them into constants x (x position), y (y position), and k (scale). We update our projection to reflect the values provided by the behavior, and then use our updated projection to update the d value for all the paths and the start and end values of all of our flights.

Hit save and you should have a ludicrously-slow zooming and panning map! Wow, that's really unperformant and awful!

Doing what we did with complex Natural Earth geometry and reprojecting on every tick is awful for performance. You will, quite infrequently, need to do everything you did in the last example to get this behavior working on a chart, but it's good to know because it exposes the ability of d3-zoom to programmatically zoom via transforms. Let's change our approach and just transform the parent container.

In chapter5/index, comment out the following:

zoomMap.addZoomBehavior();

Now, add the following:

zoomMap.addZoomBehaviorTransform();

Save and head back to zoomMap.js:

zoomMap.addZoomBehaviorTransform = () => { 
const chart = d3.select('#chart');

const zoom = d3.zoom()
.scaleExtent([0.5, 2])
.on('zoom', zoomMap.onZoomTransform);

chart.call(zoom);
};

Wow, that's quite a lot easier, isn't it? Same as before, just not having to set up projections or initial transforms. Let's move on to our event handler:

zoomMap.onZoomTransform = () => { 
const container = d3.select('#container');
container.attr('transform', d3.event.transform);
container.selectAll('path')
.style('stroke-width', 1 / d3.event.transform.k);
};

This is even simpler! We select the container and set its transform property to the contents of d3.event.transform (which outputs as a value you can pass to the transform property when output as a string). We even get a bit fancy and scale the stroke width of our geometry to reflect the scale value; in reality, it can be a one-liner.

Hit save and try your map again. That's miles better, isn't it? D3 gives you a lot of freedom in implementing things; understanding which is the best route for any particular project comes from practice and knowing your audience.

However, generally, it's better not to reproject if you don't need to, it's awful for performance.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset