Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

line on d3 map not forming a curve

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.

like image 505
Lone Ranger Avatar asked Jun 12 '19 12:06

Lone Ranger


People also ask

How to give a curve to a line in D3?

The d3.line.curve () method is used to give a curve to our line. D3.js provides several curve factories that can be used to give different curves. curve_factory: type of curve to be given to the line.

What is the difference between area and line in D3?

Define the chart’s area and line. area () and line () are D3 helper functions. The area function transforms each data point into information that describes the shape, and the line function draws a line according to data values. curveCardinal is the type of line/area curve (check D3 curve explorer for more).

What is the use of D3 line () method?

The d3.line () method is used to constructs a new line generator with the default settings. The line generator is then used to make a line. Parameters: This method takes no parameters. Return Value: This method returns a line Generator. Example 1: Making a simple line using this method. Example 2: Making a Multiconnected line.

What are point interpolators in D3?

In this section we discuss the point interpolators provided by D3.js. These are used with various generators (lines and areas) to determine the points of the lines. Depending on the type of curve, the points specified in the dataset passed to the generator will be used as either end points or control points.


1 Answers

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>
like image 155
Andrew Reid Avatar answered Oct 04 '22 13:10

Andrew Reid