Created
June 13, 2014 22:26
-
-
Save mhkeller/f41cceac3e7ed969eaeb to your computer and use it in GitHub Desktop.
A basic setup showing how to draw arc paths on a map with D3.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font: 12px sans-serif; | |
} | |
/* For centering */ | |
svg { | |
margin: 0 auto; | |
display: inherit; | |
} | |
.states path { | |
stroke-width: 1px; | |
stroke: white; | |
fill: #DBDBDB; | |
cursor: pointer; | |
} | |
/* .states path:hover, path.highlighted { | |
fill: tomato; | |
} | |
*/ | |
.arcs path { | |
stroke-width: 2px; | |
stroke: tomato; | |
pointer-events: none; | |
fill: none; | |
} | |
</style> | |
<body> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://d3js.org/topojson.v1.min.js"></script> | |
<script> | |
// This is an array of source/target pairs. | |
// Each location array is in the order of longitude and then latitude. | |
// You often see these as lat/lng but since we need this to be in math format we do them in lng/lat, which is x/y. | |
// You could also nest this data and change what object you bind your data to save space. There's no single correct way. | |
// Do what is best for your data and for your deadlines. | |
var arcdata = [ | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-106.503961875, 33.051502817366334] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-97.27544625, 34.29490081496779] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-92.793024375, 34.837711658059135] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-100.3076728125, 41.85852354782116] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-104.6143134375, 43.18636214435451] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-106.152399375, 45.57291634897] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-105.5811103125, 42.3800618087319] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-74.610651328125, 42.160561343227656] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-78.148248984375, 40.20112201100485] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-81.795709921875, 39.89836713516883] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-91.738336875, 42.1320516230261] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-93.902643515625, 39.89836713516886] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-146.68645699218752, 62.84587613514389] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-151.03704292968752, 62.3197734579205] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-150.50969917968752, 68.0575087745829] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-155.58278180000002, 19.896766200000002] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-155.41249371406252, 19.355435189875685] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-156.22204876777346, 20.77817385333129] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-156.08334637519533, 20.781383752662176] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-119.41793240000001, 36.77826099999999] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-111.73848904062501, 34.311442605956636] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-118.62691677500001, 39.80409417718468] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-115.56173122812501, 44.531552843807575] | |
}, | |
{ | |
sourceLocation: [-99.5606025, 41.068178502813595], | |
targetLocation: [-107.13521755625001, 43.90164233696157] | |
} | |
] | |
// Map dimensions (in pixels) | |
var width = 600, | |
height = 349; | |
// Map projection | |
var projection = d3.geo.albersUsa() | |
.scale(730.1630554896399) | |
.translate([width/2,height/2]) //translate to center the map in view | |
// Generate paths based on projection | |
var path = d3.geo.path() | |
.projection(projection); | |
// Create an SVG | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
// Group for the states | |
// SVG drawing order is based strictly on the order in the DOM | |
// So you can't use something like z-index to make an element appear above or below another object | |
// We have to draw the states group first so that it appears below the arcs | |
// Change the order of these two variables if you want to see how it would look incorrect. | |
var states = svg.append("g") | |
.attr("class","states"); | |
// Group for the arcs | |
var arcs = svg.append("g") | |
.attr("class","arcs"); | |
// Keeps track of currently zoomed feature | |
var centered; | |
// Load the basemap data | |
d3.json("us-states.topojson",function(error,geodata) { | |
if (error) return console.log(error); //unknown error, check the console | |
//Create a path for each map feature in the data | |
states.selectAll("path") | |
.data(topojson.feature(geodata,geodata.objects.states).features) //generate features from TopoJSON | |
.enter() | |
.append("path") | |
.attr("d",path) | |
.on("click",clicked); | |
// Create a path for each source/target pair. | |
arcs.selectAll("path") | |
.data(arcdata) | |
.enter() | |
.append("path") | |
.attr('d', function(d) { | |
return lngLatToArc(d, 'sourceLocation', 'targetLocation', 15); // A bend of 5 looks nice and subtle, but this will depend on the length of your arcs and the visual look your visualization requires. Higher number equals less bend. | |
}); | |
}); | |
// This function takes an object, the key names where it will find an array of lng/lat pairs, e.g. `[-74, 40]` | |
// And a bend parameter for how much bend you want in your arcs, the higher the number, the less bend. | |
function lngLatToArc(d, sourceName, targetName, bend){ | |
// If no bend is supplied, then do the plain square root | |
bend = bend || 1; | |
// `d[sourceName]` and `d[targetname]` are arrays of `[lng, lat]` | |
// Note, people often put these in lat then lng, but mathematically we want x then y which is `lng,lat` | |
var sourceLngLat = d[sourceName], | |
targetLngLat = d[targetName]; | |
if (targetLngLat && sourceLngLat) { | |
var sourceXY = projection( sourceLngLat ), | |
targetXY = projection( targetLngLat ); | |
// Uncomment this for testing, useful to see if you have any null lng/lat values | |
// if (!targetXY) console.log(d, targetLngLat, targetXY) | |
var sourceX = sourceXY[0], | |
sourceY = sourceXY[1]; | |
var targetX = targetXY[0], | |
targetY = targetXY[1]; | |
var dx = targetX - sourceX, | |
dy = targetY - sourceY, | |
dr = Math.sqrt(dx * dx + dy * dy)*bend; | |
// To avoid a whirlpool effect, make the bend direction consistent regardless of whether the source is east or west of the target | |
var west_of_source = (targetX - sourceX) < 0; | |
if (west_of_source) return "M" + targetX + "," + targetY + "A" + dr + "," + dr + " 0 0,1 " + sourceX + "," + sourceY; | |
return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY; | |
} else { | |
return "M0,0,l0,0z"; | |
} | |
} | |
// Zoom to feature on click | |
// This is optional but if you use mapstarter.com, you get it for free. | |
function clicked(d,i) { | |
//Add any other onClick events here | |
var x, y, k; | |
if (d && centered !== d) { | |
// Compute the new map center and scale to zoom to | |
var centroid = path.centroid(d); | |
var b = path.bounds(d); | |
x = centroid[0]; | |
y = centroid[1]; | |
k = .8 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height); | |
centered = d | |
} else { | |
x = width / 2; | |
y = height / 2; | |
k = 1; | |
centered = null; | |
} | |
// Highlight the new feature | |
states.selectAll("path") | |
.classed("highlighted",function(d) { | |
return d === centered; | |
}) | |
.style("stroke-width", 1 / k + "px"); // Keep the border width constant | |
//Zoom and re-center the whole map container | |
//Comment `.transition()` and `.duration()` to eliminate gradual zoom | |
svg | |
.transition() | |
.duration(500) | |
.attr("transform","translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")"); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment