clrnd's

A Temperature Map

Yesterday I finally decided to port our D3 v3 viz code to the latest D3 v4, and it was a really pleasant experience actually! This came as a nice surprise, since most JavaScript libraries don’t offer a nice upgrading experience, in my opinion (react-router I’m looking at you).

Also, this semester me and some class mates did a lot of viz work for our Numerical Methods class which resulted in some really nice graphics. Yet there was a specific one we didn’t have enough time to make: a map showing the average yearly temperature of the planet in a polyhedral projection.

So today, finally with some free time available, and the D3 knowledge refreshened, I made it. The thing is, the data I have is per city, so what can be done? I have been wanting to use d3-contour for some time and thought I finally had found an use case, but no. You need an even grid of samples for that, we only scattered cities. So what can be done? Well, another voronoi map!

Let me be clear: this is the least statistically significant map that can be drawn. Don’t show it to your friends.

But also, it looks amazing! Show it to your boss!

(For example, some Antartida zones fall inside some California city’s polygon, so yeah, nope)

How?

The temperature data is from Kaggle and the planet from our beloved Natural Earth.

First of all, the data is grouped by year so we have the average temperature for each city for a given year. For this map I chose 1992. Secondly, we take each city and project it’s latitude and longitude using d3.geoPolyhedralWaterman().

Finally, we separate the space using a voronoi diagram so each city is given a polygon that contains all the points it is closest to.

Wanna hear awesome? Initially I used d3.geoEquirectangular() for tying everything up, only at the end I switched to the butterfly and it worked at once!

Also I’m using Susie Lu’s excellent d3-legend. The legend’s entire code is:

let legend = d3.legendColor()
    .shapeWidth(30)
    .shapePadding(0)
    .labelFormat(d => d3.format('.0f')(d) + ' ºC')
    .labelAlign('start')
    .cells(8)
    .orient('vertical')
    .ascending(true)
    .scale(scale);

let legend_y = height / 2;
svg.append('g')
    .attr('class', 'legend')
    .attr('transform', 'translate(10, ' + legend_y + ')')
    .call(legend);

Another interesting bit is the clip path. Since the Waterman projection is not continuous, we have to cut some pieces of the map away. For that we use two things: that d3.geoPath() and the projection know how to render a {type: 'Sphere'} GeoJSON datum, and an svg clipPath in a <defs> element (which we also use to render the limit of the world). The full code for the clipping and limit drawing is:

var defs = svg.append('defs');

defs.append('path')
    .datum({type: 'Sphere'})
    .attr('id', 'sphere')
    .attr('d', path);

defs.append('clipPath')
    .attr('id', 'clip')
  .append('use')
    .attr('xlink:href', '#sphere');

svg.append('use')
    .attr('class', 'limit')
    .attr('xlink:href', '#sphere');

Followed by .attr('clip-path', 'url(#clip)') on the <path> elements we want to clip. So simple!

Anyway

Another horribly bad thing going on in this map: d3.voronoi() is two dimensional, it’s not made for spherical coordinates. This is greatly discussed here. I have over 3500 cities here so using the O(n^2) algorithm mentioned there is just not possible (my tab crashed instantly).

I hope you liked it or found it useful. The code is over at GitHub, like always.

Cheers!

July 14, 2017