Using geography as a base

Geography isn't just about drawing maps. A map is usually a base we build to show some data.

Let's turn this into a map of the world's airports. Actually, scratch that - let's do something cooler. Let's make a map of CIA rendition flights out of the US. To do this, we'll still need the world's airports, as the airport values in the Rendition Project's dataset use the airport short codes, not latitude and longitude.

The first step is fetching the airports.dat dataset from http://openflights.org/data.html  and the Rendition Project's U.S. flights data from http://www.therenditionproject.org.uk/pdf/XLS%201%20-%20Flight%20data.%20US%20FOI%20resp.xls. You can also find it in the examples on GitHub at https://github.com/aendrew/learning-d3/blob/master/chapter4/data/airports.dat  and https://github.com/aendrew/learning-d3/blob/master/chapter4/data/renditions.csv  respectively.

For the renditions dataset, you'll need to open it in Excel and save as CSV. I've done that for you if you grab it from GitHub. We also need to install the d3-dsv package for parsing our CSV files:

npm install d3-dsv --save

Now import parseCSV and csvParseRows from that by putting the following line at the top of chapter4/index.js:

import { csvParseRows, parseCsv } from 'd3-dsv';

Make sure the files are named airports.dat and renditions.csv in your data directory, then add two more calls to Promise.all() to load them in. Add a call to addRenditions() after draw().

    addRenditions( 
await (await fetch('data/airports.dat')).text(),
await (await fetch('data/renditions.csv')).text()
);

The function loads the two datasets and then calls (the as yet nonexistent) addRenditions to draw them.

In addRenditions, we first wrangle the data into JavaScript objects, airports into a dictionary by id, and use that to get the latitude and longitude of each destination and arrival airport:

function addRenditions(airportData, renditions) {
const airports = csvParseRows(airportData)
.reduce((obj, airport) => {
obj[airport[4]] = {
lat: airport[6],
lon: airport[7],
};
return obj;
}, {});
const routes = csvParse(renditions).map((v) => {
const dep = v['Departure Airport'];
const arr = v['Arrival Airport'];
return {
from: airports[dep],
to: airports[arr],
};
})
.filter(v => v.to && v.from)
.slice(0, 100);
}

We used d3.csvParseRows to parse CSV files into arrays and manually turned them into dictionaries. The array indices aren't very legible unfortunately, but they make sense when you look at the raw data:

1,"Goroka","Goroka","Papua New Guinea","GKA","AYGA",     -6.081689,145.391881,5282,10,"U"2,"Madang","Madang","Papua New Guinea","MAG","AYMD",
-5.207083,145.7887,20,10,"U"

We then map each rendition flight so that we just have a dictionary of arrival and departure coordinates. We filter out any results where either to or from are missing, which are likely cases where our map function wasn't able to match the airport short codes. Also, because it's a really big dataset and drawing all of it looks a bit messy, we've limited it to the first 100 objects in the array, using Array.prototype.slice.

Next, we'll actually draw the lines, using our projection to translate the latitude and longitude coordinates into something that can fit on our screen. Still inside our drawRenditions function, add the following:

chart.container.selectAll('.route')
.data(routes)
.enter()
.append('line')
.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])
.classed('route', true);

The routes won't show up until we style them. Add the following to index.css:

.route { 
stroke-width: 2px;
stroke: goldenrod;
}

The following screenshot displays the result:

Huh. That doesn't have much, beyond the one route represented by the black line near the middle. We're probably zoomed in too much. Let's tweak it a little. Go back to where we defined projection and set the scale to 200, with -50, 56 set as the center:

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

Hey! There we go! This is suddenly looking like the start of a piece of interactive news content!

We solved what's commonly referred to as the too many markers problem - that is, when zoomed out, data on a map looks cluttered - by simply limiting the amount of data that can be shown. This is admittedly a pretty cheap way out; a better workaround is to either cluster map data (possibly coding departure airports one color and arrival airports another) or provide UI elements to toggle aspects of the dataset. We'll look at interactivity in the coming chapters; hold onto your hats!
..................Content has been hidden....................

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