Drawing geographically

d3.geoPath() is going to be the workhorse of our geographic drawings.

It's similar to the SVG path generators we learned about earlier, except it draws geographic data and is smart enough to decide whether to draw a line or an area.

To flatten spherical objects such as planets into a 2D image, d3.geoPath() uses projections. Different kinds of projections are designed to showcase different things about the data, but the end result is you can completely change what the map looks like just by changing the projection or moving its focal point.

Let's draw a map of the world, centered on and zoomed into Europe. We'll make our map navigable in Chapter 5, Defining the User Experience – Animation and Interaction.

First though, we need to install the TopoJSON files into your project.

    $ npm install topojson --save

Now require topojson at the top of lib/chapter4/index.js.

import * as topojson from 'topojson';

Let's create a new enclosure for all of this in chapter4/index.js:

const geoDemo = ((enabled) => {
if (!enabled) return;
const chart = chartFactory();
})(true);

Next, after const chart, we define a geographic projection in the constructor:

const projection = d3.geoEquirectangular()
.center([8, 56])
.scale(500);

The equirectangular projection is one of the hundred-odd projections in the d3-geo-projection package, and is perhaps the most common projection we're used to seeing ever since high school.

The problem with equirectangular is that it doesn't preserve areas or represent the earth's surface all that well. The debate of how to best project a sphere onto a two dimensional surface has a fascinating history, and D3's enormous collection of geographic projections shows the many ways people have attempted to do so over the years: https://github.com/d3/d3-geo-projection.

The next two lines define where our map is centered and how zoomed in it is. By fiddling, I got all three values, latitude of 8, longitude of 56, and a scaling factor of 500. Play around to get a different look.

Now we load our data, using Fetch:

const world = await Promise.all([
(await fetch('data/water.json')).json(),
(await fetch('data/land.json')).json(),
(await fetch('data/cultural.json')).json(),
]);

We're using ES2015 promises to run the three loading operations simultaneously. Each will use d3.json() to load and parse the data, either rejecting (if there's an error) or resolving the promise (if the error function argument is undefined or null). The promises are then collected in Promise.all(), which returns its resolved value to await once all the promises are accounted for.

We need one more thing before we start drawing; a function that adds a feature to the map, which will help us to reduce code repetition:

const addToMap = (collection, key) => chart.container.append('g')
.selectAll('path')
.data(topojson.feature(collection, collection.objects[key]).features)
.enter()
.append('path')
.attr('d', d3.geoPath().projection(projection));

This function takes a collection of objects and a key to choose which object to display. topojson.object() translates a TopoJSON topology into a GeoJSON one for d3.geoPath().

Whether it's more efficient to transform to GeoJSON than transferring data in the target representation depends on your use case. Transforming data takes some computational time, but transferring megabytes instead of kilobytes can make a big difference in responsiveness.

Finally, we create a new d3.geoPath() and tell it to use our projection. Other than generating the SVG path string, d3.geoPath() can also calculate different properties of our feature, such as the area (.area()) and the bounding box (.bounds()).

Now we can start drawing:

const draw = (worldData) => {
const [sea, land, cultural] = worldData;
addToMap(sea, 'water').classed('water', true);
addToMap(land, 'land').classed('land', true);
addToMap(cultural, 'ne_50m_admin_0_boundary_lines_land').classed('boundary', true);
addToMap(cultural, 'ne_50m_urban_areas').classed('urban', true);
chart.svg.node().classList.add('map');
};

Our draw function takes the error returned from loading data and the three datasets then lets addtoMap do the heavy lifting. We use a new ES2015 feature here - argument destructuring - to assign each element of the array to a new variable.

What's all this now about destructuring? To quote the Mozilla Developer's Network, destructuring assignment syntax allows the "extract[ion of] data from arrays or objects using a syntax that mirrors the construction of array and object literals". The equivalent code in ES5 would be: var land = values[0]; var sea = values[1]; var cultural = values[2]. For more on destructuring and how it can make your code awesome, check out: https://mdn.io/Destructuring_assignment.

Add some styling to styles/index.css:

.river { 
fill: none;
stroke: #759dd1;
stroke-width: 1;
}

.land {
fill: #ede9c9;
stroke: #7b5228;
stroke-width: 1;
}

.boundary {
stroke: #7b5228;
stroke-width: 1;
fill: none;
}

.urban {
fill: #e1c0a3;
}

.map {
background: #79bcd3;
}

And then make sure it's still required at the top of lib/main.js:

import '../styles/index.css';

Make sure webpack-dev-server is running and look at your page. It should resemble this:

We now have a slowly-rendering world map zoomed into Europe, displaying the world's urban areas as blots.

There are many reasons why it's so slow. We transform between TopoJSON and GeoJSON on every call to addToMap. Even when using the same dataset, we're using data that's too detailed for such a zoomed-out map, and we render the whole world to look at a tiny part. We've traded flexibility for rendering speed in this instance. That said, we could dramatically speed up our rendering by reducing the size of our TopoJSON files by passing them through toposimplify from the topojson-simplify package.

I've written a more detailed guide to the TopoJSON command-line tools in this post: https://medium.com/@aendrew/creating-topojson-using-d3-v4-10838d1a9538. For a very thorough walk-through of all the tools, direct from Mike Bostock, read his series, starting at https://medium.com/@mbostock/command-line-cartography-part-1-897aa8f8ca2c.

..................Content has been hidden....................

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