Search code examples
d3.jsgisgeojsonmap-projections

How to make geoAlbersUSA projection straight(not curved) like geoMercator?


I followed a guide to create a basic map of USA in d3js

Guide link - http://bl.ocks.org/michellechandra/0b2ce4923dc9b5809922.


I have understood the basics of GeoJSON and Map Projections.

Currently my map looks like this - (Map 1) - this uses d3.geoAlbersUsa() projection enter image description here

I want my map to look like this - (Map 2) - i.e. Not curved on top but straight line boundary

link https://www.worldatlas.com/webimage/countrys/namerica/usstates/usa50mer.htm

enter image description here

Requirements -

  1. I want to keep alaska and hawaii visible in map like in both images

What have I tried already -

  1. I have tried to use d3.geoMercator() projection but alaska is too big to fit in viewport and is located on top-left which I don't want.

Solution

  • You can't straighten the Albers. An Albers projection is a conical projection - the parallels are different sizes because they are projected on to a cone, which means that there will be a curve since the projected lengths are different (the parallels are different lengths on the globe as well).

    The cylindrical Mecator projection stretches the shorter parallels to occupy the same width as the equator, which is why parallels are straight and horizontal and equal length in a Mercator projection.

    We could abuse an Albers projection to iron out some of the curve, but d3's geoAlbersUsa projection is a composite projection that doesn't allow such modification. It is really just a collection of projections.

    Instead, we can create our own composite projection using d3.geoTransform and d3.geoMercator. The result should look like this:

    enter image description here

    Here's a simple d3 geoTransform:

      let projection = d3.geoTransform({
          point: function(x, y) {
              this.stream.point(x*2,y*2);
          }
        });
    

    This just multiplies x and y values by two, it can be passed to a path generator like so: d3.geoPath(projection);

    To make a composite projection here we need to have three projections set properly and then translated properly onto the map. I built these projections assuming a 960x500 map (similar to d3.geoAlbersUsa):

      let scale = 800;
      let width = 960;
      let height = 500;
    
      let continental = d3.geoMercator()
        .center([-98.58,39.83])
        .translate([width*0.5,height*0.42])  // placed near center
        .scale(scale);
    
      let hawaii = d3.geoMercator()
        .center([-157.25,20.8])
        .scale(scale)
        .translate([width*0.35,height*0.87])  // placed near bottom, slightly left of center
    
      let alaska = d3.geoMercator()
        .center([-152.5,65])
        .translate([width*0.15,height*0.8])    // placed in lower left
        .scale(scale*0.3)
    

    A Mercator really exaggerates size as one moves to extreme latitudes, so Alaska, which is already geographically large (over twice the size of Texas) becomes much too big when projected. I've scaled it down so it fits (this way it is about 1/10th the projected size it would be otherwise).

    We also need to choose what projection to use when, so based on a few rules we can determine if a point is in Alaska, Hawaii, or the lower 48:

      let projection = d3.geoTransform({
          point: function(x, y) {
            if(y < 50 && x < -140) {  // south and west : hawaii
              this.stream.point(...hawaii([x,y]));
            }
            else if (y > 50) {       // north : alaska
              this.stream.point(...alaska([x,y]));
            }
            else {                    // otherwise, it's in the lower 48.
              this.stream.point(...continental([x,y]));
            }
          }
        });
    

    That's it, we have three projections and a method to determine which one should be used for a given point.

    I'll just bundle it all up as d3.geoMercatorUsa() to keep it more self contained and we get:

    // Composite Mercator projection for the US
    d3.geoMercatorUsa = function() {
      let scale = 800;
      let width = 960;
      let height = 500;
    
      let continental = d3.geoMercator()
        .center([-98.58,39.83])
        .translate([width*0.5,height*0.42])
        .scale(scale);
    
      let hawaii = d3.geoMercator()
        .center([-157.25,20.8])
        .scale(scale)
        .translate([width*0.35,height*0.87])
    
      let alaska = d3.geoMercator()
        .center([-152.5,65])
        .translate([width*0.15,height*0.8])
        .scale(scale*0.3)
    
      let projection = d3.geoTransform({
          point: function(x, y) {
            if(y < 50 && x < -140) {
              this.stream.point(...hawaii([x,y]));
            }
            else if (y > 50) {
              this.stream.point(...alaska([x,y]));
            }
            else {
              this.stream.point(...continental([x,y]));
            }
          }
        });
        return projection;
    }
    
    // Demonstration:
    let width = 960;
    let height = 500;
    
    let svg = d3.select("body")
      .append("svg")
      .attr("width",width)
      .attr("height",height);
    
    let path = d3.geoPath(d3.geoMercatorUsa());  
      
    d3.json("https://raw.githubusercontent.com/johan/world.geo.json/master/countries/USA.geo.json").then(function(data) {
    
       svg.append("path")
         .datum(data)
         .attr("d",path);
    })
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>