Skip to content

Instantly share code, notes, and snippets.

@mhkeller
Created June 13, 2014 22:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhkeller/f41cceac3e7ed969eaeb to your computer and use it in GitHub Desktop.
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.
<!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>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment