D3 geoPath can be used to create paths that follow greater circle distance: they aren't curved for style, they are curved as needed, depending on projection, to represent the shortest path on earth to connect two points. D3 geoPaths are dynamically re sampled to allow this.
This behavior is unusual in web geographic mapping libraries, most of which treat latitude and longitude as Cartesian data rather than three dimensional data: where latitude and longitude are points on a sphere. In treating data as Cartesian, lines are straight when connecting two points. In d3 this can be accomplished with methods such as these.
If you want a consistent curve for all line segments, we will treat the data as Cartesian and interpolate a curve. As we won't be using d3.geoPath
for this, there is no need to convert your destinations and sources into geojson LineStrings, we can just use the points directly.
We can use a curve interpolator for this, but the default interpolators won't work without adding control points between the end and start destinations. Instead, let's try a custom curve - see these answers (a,b) for more on custom curves.
Our custom curve could take any point after the first to find the mid point between it and the point before it and offset a point to create a control point forming a triangle between the prior point and the current point, then we just draw a quadratic curve between them:
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
With this in hand we can simply draw lines with something like:
d3.line()
.curve(curve)
.x(function(d) { return d.lon; })
.y(function(d) { return d.lat; })
As seen below:
let data = [{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": 21.05,
"lon": 105.55
}
},
{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": -35.15,
"lon": 149.08
}
}]
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
var projection = d3.geoEquirectangular().translate([250,150]).scale(500/Math.PI/2);
var path = d3.geoPath(projection);
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(world) {
var worldOutline = svg.append("path")
.datum(topojson.mesh(world))
.attr("d", path );
var line = d3.line()
.x(function(d) {
return projection([d.lon,d.lat])[0];
})
.y(function(d) {
return projection([d.lon,d.lat])[1];
})
.curve(curve);
svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return [d.source,d.destination]; // d3.line expects an array where each item represnts a vertex.
})
.attr("d",line)
.style("stroke","black")
.style("stroke-width",1.5);
});
path {
fill: none;
stroke: #ccc;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
Below, just for fun, I compare straight lines using d3.line
, curved lines using d3.line
with a custom curve interpolator, and plain old d3.geoPath
with some animation:
let data = [{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": 21.05,
"lon": 105.55
}
},
{
"source": {
"lat": 40.712776,
"lon": -74.005974
},
"destination": {
"lat": -35.15,
"lon": 149.08
}
}]
var curve = function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
this.x0 = x; this.y0 = y;
break;
}
}
return custom;
}
var projection = d3.geoEquirectangular().translate([250,150]).scale(500/Math.PI/2);
var path = d3.geoPath(projection);
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(world) {
var worldOutline = svg.append("path")
.datum(topojson.mesh(world))
.attr("d", path );
var line = d3.line()
.x(function(d) {
return projection([d.lon,d.lat])[0];
})
.y(function(d) {
return projection([d.lon,d.lat])[1];
})
.curve(curve);
var fauxArcPaths = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return [d.source,d.destination];
})
.attr("d",line)
.style("stroke","black")
.style("stroke-width",1.5);
var greatCirclePaths = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return {type:"LineString",coordinates:
[[d.source.lon,d.source.lat],[d.destination.lon,d.destination.lat]] }
})
.attr("d",path)
.style("stroke","steelblue")
.style("stroke-width",1.5);
var straightline = d3.line()
.x(function(d) {
return projection([d.lon,d.lat])[0];
})
.y(function(d) {
return projection([d.lon,d.lat])[1];
});
var straightPaths = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.datum(function(d) {
return [d.source,d.destination];
})
.attr("d",straightline)
.style("stroke-width",1.5)
.style("stroke","orange");
// animate:
d3.interval(function(elapsed) {
projection.rotate([ -elapsed / 150, elapsed/300 ]);
straightPaths.attr("d",straightline);
greatCirclePaths.attr("d",path);
fauxArcPaths.attr("d",line);
worldOutline.attr("d",path);
}, 50);
});
path {
fill: none;
stroke: #aaa;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>