score:2

your assumption is right: d3 uses great circle distances to draw lines: this means that any path between two points using a d3 geoprojection and geopath follows the shortest real world path between those two points. this means:

  • that the same path between two points aligns with other geographic points and features no matter the projection
  • that the anti-meridian can be accounted for
  • and the resulting map more accurately depicts lines.

to draw straight lines and/or lines that follow parallels (meridians are the shortest path between two points that fall on them - so paths follow this already, assuming an unrotated graticule) there are a few possibilities.

the easiest solution is to use a cylindrical projection like a mercator to create a custom geotransform. d3.geotransforms do not use spherical geometry, unlike d3.geoprojections, instead they use cartesian data. consequently they don't sample along lines to create curved lines: this is unecessary when working with cartesian data. this allows us to use spherical geometry for the geojson vertices within the geotransform while still keeping straight lines on the map:

var transform = d3.geotransform({
    point: function(x, y) {
      var projection = d3.geomercator();
      this.stream.point(...projection([x,y]));
    }
});

as seen below:

var projection = d3.geomercator();

var transform = d3.geotransform({
    point: function(x, y) {
      var projection = d3.geomercator();
      this.stream.point(...projection([x,y]));
    }
});

var color = ["steelblue","orange"]


var geojson = {type:"linestring",coordinates:[[-160,60],[30,45]]};
var geojson2 = {type:"polygon",coordinates:[[[-160,60,],[-80,60],[-100,30],[-160,60]]]}

var svg = d3.select("body")
  .append("svg")
  .attr("width",960)
  .attr("height",500);
  
svg.selectall(null)
  .data([projection,transform])
  .enter()
  .append("path")
  .attr("d", function(d) {
    return d3.geopath().projection(d)(geojson)
  })
  .attr("fill","none")
  .attr("stroke",function(d,i) { return color[i]; } )
  .attr("stroke-width",1);

svg.selectall(null)
  .data([projection,transform])
  .enter()
  .append("path")
  .attr("d", function(d) {
    return d3.geopath().projection(d)(geojson2)
  })
  .attr("fill","none")
  .attr("stroke",function(d,i) { return color[i]; } )
  .attr("stroke-width",2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

the orange lines use the transform, the blue lines use a plain mercator

in some cases you could set the precision of the projection (which regulates the adaptive sampling) to some absurdly high number, this will work for some lines, but not others due to things like anti-meridian cutting:

var projection = d3.geomercator().precision(1000000);

var transform = d3.geotransform({
    point: function(x, y) {
      var projection = d3.geomercator();
      this.stream.point(...projection([x,y]));
    }
});

var color = ["steelblue","orange"]


var geojson = {type:"linestring",coordinates:[[-160,60],[30,45]]};
var geojson2 = {type:"polygon",coordinates:[[[-160,60,],[-80,60],[-100,30],[-160,60]]]}

var svg = d3.select("body")
  .append("svg")
  .attr("width",960)
  .attr("height",500);
  
svg.selectall(null)
  .data([projection,transform])
  .enter()
  .append("path")
  .attr("d", function(d) {
    return d3.geopath().projection(d)(geojson)
  })
  .attr("fill","none")
  .attr("stroke",function(d,i) { return color[i]; } )
  .attr("stroke-width",1);

svg.selectall(null)
  .data([projection,transform])
  .enter()
  .append("path")
  .attr("d", function(d) {
    return d3.geopath().projection(d)(geojson2)
  })
  .attr("fill","none")
  .attr("stroke",function(d,i) { return color[i]; } )
  .attr("stroke-width",2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

neither approach works if you want to draw lines that are aligned with parallels on a non-cylindrical projection. for a cylindrical projection parallels are straight. the above approaches will only create straight lines. if the parallels aren't projected straight, such as the aitoff, the lines will not align with the graticule.

to have a line follow a parallel you will need to sample points along your paths because the projected parallels will not all be straight and parallels don't follow great circle distance. therefore neither the default projection nor the method above will work in these instances.

when sampling you will need to treat the data as cartesian - essentially using a cylindrical projection (plate carree) to have lines follow parallels.


Related Query