Search code examples
javascriptd3.jsmappingprojectioncomposition

d3 - may I somehow compose two projections, or project a projection?


I would like to create a map which has been tilted back on the Z-axis, as if you were looking at it laid flat on a table. No problem: use Mike Bostock's custom projection approach, define a simple transformation that scales back as it goes towards the top, call it a day.

Except in the process I learned that d3 was defaulting to the Albers USA projection, and that using my custom projection put Alaska and Hawaii back to their correct placements (which I don't want.)

Also, I hadn't realized Puerto Rico wasn't on the Albers USA projection, so I'd actually like to switch to Albers USA + PR.

I would prefer not to remake the projection if I can help it, because someday I'll find an Albers that also has the various other US territories.

Are projections composable somehow?


Solution

  • Without knowing the exact details of your implemenation I have forked Mike Bostock's Block AlbersUSA + PR to show a way this could be done.

    At its core this uses the Albers USA projection which includes Puerto Rico as requested. This is a normal D3 projection:

    var albersUsaPrProj = albersUsaPr()
        .scale(1070)
        .translate([width / 2, height / 2]);
    

    Next, I have implemented a rather naïve projection to the table which might need some refinement, but should be enough to get you started. This one uses d3.geo.transform() to create a stream wrapper for the calculations needed to project the map on the desktop. The wrapped stream listener needs to implement just the point method that will be called with the x, y screen coordinates which are the results from the geo projection.

    // The scale is used for convenient calculations only.
    var yScale = d3.scale.linear()
                   .domain([0,height])
                   .range([0.25,1]);
    
    var desktopProj = d3.geo.transform({
      point: function(x,y) {
        this.stream.point((x - 0.5*width) * yScale(y) + (0.5*width), y);
      }
    });
    

    Both projections are easily combined into one new stream wrapper by creating an object implementing the .stream() method.

    var combinedProj = {
      stream: function(s) {
          return albersUsaPrProj.stream(desktopProj.stream(s));
      }
    };
    

    According to the documentation on projection.stream(listener):

    Returns a projecting stream wrapper for the specified listener. Any geometry streamed to the wrapper is projected before being streamed to the wrapped listener.

    This will first let albersUsaPrProj take care of the map's Albers USA projection and, afterwards, stream the resulting screen coordinates to desktopProj.

    This combined projection may then be passed to path.projection([projection]):

    For more control over the stream transformation, the projection may be specified as an object that implements the stream method. (See example.) The stream method takes an output stream as input, and returns a wrapped stream that projects the input geometry; in other words, it implements projection.stream.

    This give us the final call as

    var path = d3.geo.path()
        .projection(combinedProj);