Jonathan Hodgson
Jonathan Hodgson

Reputation: 373

Stop d3 path crossing the map

I have a d3 map that shows shipping lines. The lines are supplied as a list of coordinates that I have plotted using a projection and a d3 path. Pretty normal stuff. The problem is what happend if I want the path to go from, for example, -179°E to +179°E. Unfortunately it crosses the map. Is there any way to make it go off the left of the map and re-appear on the right of the map?

Upvotes: 2

Views: 481

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38171

If plotting points with a projection before drawing a line connecting them (using an array of svg points), the line will have not behave as desired when crossing the anti-meridian. After all, you are just plotting points on 2 dimensional grid and connecting them as though they are points on a 2 dimensional grid.

If, however, you plot lines using a geoPath (using an array of long,lat ponts), the path will follow the shortest distance between points (great circle distance). A geoPath does the 3d math to convert paths connecting points on a globe to paths in a 2d space. This means

  • a line between two points will arc (visibly if far enough apart and depending on the projection)
  • the antimeridian will not matter - great circle distance is independent of where the antimeridian is
  • manipulating the map is a lot easier.

You are probably already using a geoPath for background features. If not, d3.geoPath() needs to be assigned a projection, as you already have a projection, you can use var path = d3.geoPath().projection(projection).

A geoPath requires a geojson feature or geometry object. You can create one for a line fairly easily with something like:

var coords = [[-179,0],[179,0]];

svg.append("path")
  .datum({ "type": "LineString", "coordinates": coords }) 

Here's an example of a path falling off the left of the map, and remerging on the right. (I can't make a pacific rim feature small enough for a snippet, so I've used the US and rotated it so it is cut in half by the anti-meridian, but it will also cut across longitudes of +/-180. See also, for drawing a geoPath between two or more points, this):

var width = 500;
var height = 300;

var svg = d3.select("body")
  .append("svg")
  .attr("width",width)
  .attr("height",height);
  
var projection = d3.geoMercator()
  .rotate([-75,0])
  .scale(50)
  .translate([width/2,height/2]);
  
var path = d3.geoPath().projection(projection);

var usa = {"type":"FeatureCollection", "features": [
{"type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-94.81758,49.38905],[-88.378114,48.302918],[-82.550925,45.347517],[-82.439278,41.675105],[-71.50506,45.0082],[-69.237216,47.447781],[-66.96466,44.8097],[-70.11617,43.68405],[-70.64,41.475],[-73.982,40.628],[-75.72205,37.93705],[-75.72749,35.55074],[-81.49042,30.72999],[-80.056539,26.88],[-81.17213,25.20126],[-83.70959,29.93656],[-89.18049,30.31598],[-94.69,29.48],[-99.02,26.37],[-100.9576,29.38071],[-104.45697,29.57196],[-106.50759,31.75452],[-111.02361,31.33472],[-117.12776,32.53534],[-120.36778,34.44711],[-123.7272,38.95166],[-124.53284,42.76599],[-124.68721,48.184433],[-122.84,49],[-116.04818,49],[-107.05,49],[-100.65,49],[-94.81758,49.38905]]],[[[-155.06779,71.147776],[-140.985988,69.711998],[-140.99777,60.306397],[-148.018066,59.978329],[-157.72277,57.570001],[-166.121379,61.500019],[-164.562508,63.146378],[-168.11056,65.669997],[-161.908897,70.33333],[-155.06779,71.147776]]]]},"properties":{"name":"United States of America"},"id":"USA"}
]};

svg.append("path")
      .attr("d",path(usa));

var coords = [[-150,65],[-80,25],[-121,36]];



svg.append("path")
  .datum({ "type": "LineString", "coordinates": coords })
  .attr("fill","none")
  .attr("stroke","steelblue")
  .attr("stroke-width",4)
  .attr("d",path);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>

Upvotes: 3

Related Questions