Unconstant Conjunction A personal blog

Don't Forget to Reproject Spatial Data when Exporting to GeoJSON

In the process of working on a recent choropeth piece for work, I discovered that it’s easy to stumble when moving spatial data out of R and onto the web. It’s a poorly-documented reality that many web-based mapping libraries (including both D3 and Leaflet) expect GeoJSON data to be in EPSG:4326, and it is by no means a given that that your spatial data will start off in this projection.

If you’re like me and do your spatial data pre-processing in R before exporting to GeoJSON, you may have to re-project your data before these libraries will handle them properly. Thankfully, this is fairly easy to do with the modern spatial packages.

The next-gen spatial package sf provides an st_transform() function for reprojecting spatial objects. My current workflow for taking shapefiles, adding/removing features and data, and writing out a GeoJSON file that can be read in D3 or Leaflet looks something like the following:

library(dplyr)
library(sf)

sf::st_read("path/to/shapefile", layer = "layer") %>%
  # Filter, mutate, join, select on this spatial data.
  ... %>%
  # Re-project the map into EPSG:4326 before writing to GeoJSON.
  sf::st_transform(crs = 4326) %>%
  sf::st_write("map.json", driver = "GeoJSON")

This GeoJSON data can then be re-projected using the Javascript library into something more appropriate than EPSG:4326. In D3 this looks something like the following:

// This example projection works well for Canadian maps. I
// cribbed it from Statistics Canada:
// http://www.statcan.gc.ca/pub/82-402-x/2015001/gui-eng.htm#a6
var proj = d3.geoConicConformal()
    .parallels([49, 77])
    .rotate([91.86, -63.390675]);

var path = d3.geoPath()
    .projection(proj);

d3.json("map.json", function(error, map) {
    if (error) throw error;

    // Protip: use `fitSize()` set the projection scale, center,
    // and translation automatically to fit in the SVG element.
    proj.fitSize([780, 500], map.features);

    d3.select("svg")
        .attr("width", 780)
        .attr("height", 500)
        .selectAll("path")
        .data(map.features)
        .enter()
        .append("path")
        .attr("d", path)
        .style("stroke", "white");
});

I did report this issue to sf itself, but the maintainer indicated that the library ought to be sticking to the standard, regardless of existing implementation quirks. In the meantime, if you plan to export GeoJSON data for use with Javascript libraries, don’t forget to reproject as necessary.

comments powered by Disqus