line on d3 map not forming a curve

2024/2/27 8:54:46

I have created a map using d3.js. I want to show a curved line between two locations. I am able to show a line, but sometimes it does not form a perfect curve. For some lines, the lines curve behind the map (across the anti-meridian) to their destination.

Here's a code pen demonstrating the problem: https://codepen.io/peeyush-pant/pen/WqbPax

And an image:

enter image description here

Here's my projection data:

var projection = d3.geoEquirectangular();var path = d3.geoPath().projection(projection);

And here's how I draw the lines:

  arcGroup.selectAll("myPath").data(links).enter().append("path").attr("class", "line").attr("id", function (d, i) {return "line" + i;}).attr("d", function (d) {return path(d)}).style("fill", "none").style("stroke", '#fff787').style("stroke-width", 1.5);

Thank you.

Answer

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>

http://en.ppmy.cn/q/41437.html

Related Q&A

Function declaration - Function Expression - Scope

In javascript, What is the difference between function declaration and function expression in terms of scope? function declaration means we are polluting the global space. Is it the same case with fun…

Get geolocation on submit in Google Forms using Google Script

I have a Google Form where I want to get the user geolocation along with the inputs. Currently, Im able to get it by making the user click on a url after he submits the answers. This is the code in Go…

How to catch an SVG parsing error?

Im trying to write a unit test (using qunit) for my code that generates an SVG path as a string. One of the test should be whether that thing is actually valid SVG at all.In the Chrome browser console,…

Javascript script to find gibberish words in form inputs

I need a script or regex (which I will be using with Javascript / jQuery to check form input on a website) to check if someone has entered words which are mostly gibberish.Normal words or sentences sho…

Dynamic import base on props name in react

Dynamic import base on props name in reactimport { a, b, c } from some-package/theme // should not import everything hereconst MyComp = ({ theme, ...other }) => { console.log(theme) //can be a, b, c…

ng-pattern with regex having double quotes does not escape correctly

I have a ng-pattern validation for a regex of ^[^\./:*\?\"<>\|]{1}[^\/:*\?\"<>\|]{0,254}$ which basically tests the invalid chars in filepath and teh limit. but when i have the …

How is division before multiplication in Javascript?

I run some code and I get same results with or without parenthesis, even if I know that multiplication have higher precedence then division. Here is example:let calculate = 16 / 30 * 100I gott same res…

Need to Default select an Angular JS Radio Button

I am new to Angular JS and I am trying to create a set of radio buttons. Creating the buttons was the easy part, but I am having problems figuring out how to default select one of them without breakin…

In Javascript, is it possible to pass a variable into script src parameter?

Is it possible in Javascript to pass a variable through the src parameter? ie.<script type="text/javascript" src="http://domain.com/twitter.js?handle=aplusk" />`Id like twit…

Getting the height of a div

<span style="width:100%" id="learning_menu"><div id="aLM" style="width:100%;height:100%">test | test | test</div></span>The code above is…