nucleon
nucleon

Reputation: 937

Drawing a fixed-length circle in D3.js

This seems like it ought to be straightforward, but I'm struggling to figure it out.

It's easy to draw a circle on a map projection in D3.js, e.g.

p = projection([-73.94,40.7]);
 svg
    .append("svg:circle")
        .attr("cx", function(d, i) { return p[0]; }) //x position translated through projection function
        .attr("cy", function(d, i) { return p[1]; }) //y position           
        .attr("r", function(d, i) { return 20; }) 
        ;

OK, but what if instead of setting the radius as a pixel value (as it is in the above, to 20 pixels), I want to set it to a distance value that would be scaled appropriately to the projection?

In short, this boils down to me asking, "what's the pixel value of 20 km in this projection?" Now I know that this will vary because of the projection — e.g. a 20 km circle in Mercator will look very different (near spherical) at the equator than it does near the poles (highly elongated).

D3.js can do so many other clever projection issues that I was surprised to find that I couldn't easily conjure up some kind of acceptable answer here. Any suggestions? I was thinking that, well, you could calculate a number of lat/lon points for a circle and then plot it as some kind of path... which feels awfully unusually clunky for D3.js. Is there an easier way?

Upvotes: 2

Views: 533

Answers (1)

nucleon
nucleon

Reputation: 937

OK, so I figured out one way. It's a little clunky — and feels like something that ought to be more automatic — but it works. It involves just constructing a path element manually using circle points. Here are the two functions and an example of use:

//example — draws a 15 km circle centered on New York City using my existing projection
var circle = svg
    .append("path")
    .attr("d", "M"+circlePath( 40.7, -73.94, 15, projection).join("L")+"Z")
    .attr("fill","none")
    .attr("stroke","red")
    .attr("stroke-width",2);

//this function generates the points for the path. If a projection is specified, it will
//automatically convert them to it. If not, it returns lat/lon positions.
//from http://stackoverflow.com/questions/20130186/d3-geo-buffer-around-a-feature with modifications
function circlePath(lat, lon, radius, projection) {
    var intervals = 72;
    var intervalAngle = (360 / intervals);
    var pointsData = [];
    for(var i = 0; i < intervals; i++){
        pointsData.push(getDestinationPoint(lat, lon, i * intervalAngle, radius));
    }
    if(projection) {
        pointsData2 = [];
        for(i in pointsData) {
            pointsData2.push([projection([pointsData[i][1],pointsData[i][0]])[0],projection([pointsData[i][1],pointsData[i][0]])[1]]);
        }
        return pointsData2;
    } else {
        return pointsData;
    }
}

//function to get destination points given an initial lat/lon, bearing, distance
//from http://www.movable-type.co.uk/scripts/latlong.html
function getDestinationPoint(lat,lon, brng, d) {
    var R = 6371; //earth's radius in km — change to whatever unit you plan on using (e.g. miles = 3959) 
    var deg2rad = Math.PI/180; var rad2deg = 180/Math.PI; 
    brng*=deg2rad; lat*=deg2rad; lon*=deg2rad; 
    var lat2 = Math.asin( Math.sin(lat)*Math.cos(d/R) +
                        Math.cos(lat)*Math.sin(d/R)*Math.cos(brng) );
    var lon2 = lon + Math.atan2(Math.sin(brng)*Math.sin(d/R)*Math.cos(lat),
                             Math.cos(d/R)-Math.sin(lat)*Math.sin(lat2));
    return  [lat2*rad2deg, lon2*rad2deg];              
}

Upvotes: 1

Related Questions