Geospatial Data in D3

An introduction d3-geo and geoPath

Code, Recording

  1. These notes summarize methods for static visualization of geospatial data in D3. Before we can make any plots, we need to be able to read in data. To read in vector data, it’s most convenient to store the data as geojson and read it in using d3.json(). The javascript object created by this function includes a features array. Element i in this array gives properties of feature i in the vector dataset.

  2. For example, we can use this to read in and visualize the glaciers data.

    d3.json("https://raw.githubusercontent.com/krisrs1128/stat679_code/main/examples/week7/week7-3/glaciers.geojson")
      .then(visualize) // we define the visualize function
    

    which shows an object like this in the console,

  3. Even though these look like basic javascript objects, d3.json is actually keeping track geographic metadata behind the scenes. This allows us to do some basic geographic queries directly from javascript. For example, if we want to query the geographic centroid, bounds, or areas of each feature, we can use the calls below,

    let centroids = data.features.map(d3.geoCentroid),
      bounds = data.features.map(d3.geoBounds),
      areas = data.features.map(d3.geoArea);
    
    console.log(centroids, bounds, areas)
    

    You can view the console output at this link. A similar query for the Brasilia roads dataset prints output like this. A variety of related processing functions can be found in the d3-geo library.

  4. How can we display these data? We need a way of translating the abstract data into SVGs on the screen. For vector data, we can use D3’s geoPath generators. These work like line generators — they are initialized with visual encoding functions and can then be applied to any new collection of vector features.

  5. For example, we can use a path generator to draw the glacier boundaries,

    To draw the glacier boundaries, we defined the geographic path generator. The proj object serves a role similar to the x and y scales used in d3.line() generators – it is mapping abstract geographical coordinates to the pixel extent of the screen. Specifically, .fitSize takes the spatial coordinates in data and relates it to the size of the screen, [width, height]. For geographic data, this process is formally called a projection, because we have to associate the surface of the 3D globe to a 2D plane.

    let proj = d3.geoMercator()
      .fitSize([width, height], data)
    let path = d3.geoPath()
      .projection(proj);
    
  6. Given this d3.geoPath() generator, we can append one polygon per glacier in the dataset using the usual D3 data bind. Note that we bind data.features rather than just data. In geojson objects, the .features attribute contains an array with the spatial features coordinates.

    d3.select("#map")
      .selectAll("path")
      .data(data.features).enter()
      .append("path")
      .attrs({
        d: path,
        fill: d => scales.fill(d.properties.Thickness)
      })
    

    In the block above, we’ve also passed a color scale to encode glacier depth with color. In general, these geospatial data are treated just like any other SVG path element, and we can set set their attributes using the usual .attrs command. This is also how we achieved the mouseover effect – we simply added a .on("mouseover", ...) listener for whenever we hover over one of these paths.

  7. We haven’t considered raster data in this lecture. This is because javascript doesn’t have a simple built-in way to handle raster data. If we want to visualize a raster dataset, we need to first convert them to simple PNG images. This loses the geographic metadata, and so any geographic processing has to be done before this step. In practice, it’s common to either manually convert to a PNG or to use a tiling library, which automatically converts raster data into a collection of PNGs. Both of these techniques are beyond the scope of our class, but if you are curious, you can check out these resources [1, 2, 3].