Created
June 17, 2014 16:01
-
-
Save mhkeller/3921b109bd2b097e8412 to your computer and use it in GitHub Desktop.
A more complex setup showing how to draw arc paths on a map with D3, loading and transforming data from a csv.
This file contains hidden or 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
source_lng | source_lat | target_lng | target_lat | |
---|---|---|---|---|
-99.5606025 | 41.068178502813595 | -106.503961875 | 33.051502817366334 | |
-99.5606025 | 41.068178502813595 | -97.27544625 | 34.29490081496779 | |
-99.5606025 | 41.068178502813595 | -92.793024375 | 34.837711658059135 | |
-99.5606025 | 41.068178502813595 | -100.3076728125 | 41.85852354782116 | |
-99.5606025 | 41.068178502813595 | -104.6143134375 | 43.18636214435451 | |
-99.5606025 | 41.068178502813595 | -106.152399375 | 45.57291634897 | |
-99.5606025 | 41.068178502813595 | -105.5811103125 | 42.3800618087319 | |
-99.5606025 | 41.068178502813595 | -74.610651328125 | 42.160561343227656 | |
-99.5606025 | 41.068178502813595 | -78.148248984375 | 40.20112201100485 | |
-99.5606025 | 41.068178502813595 | -81.795709921875 | 39.89836713516883 | |
-99.5606025 | 41.068178502813595 | -91.738336875 | 42.1320516230261 | |
-99.5606025 | 41.068178502813595 | -93.902643515625 | 39.89836713516886 | |
-99.5606025 | 41.068178502813595 | -146.68645699218752 | 62.84587613514389 | |
-99.5606025 | 41.068178502813595 | -151.03704292968752 | 62.3197734579205 | |
-99.5606025 | 41.068178502813595 | -150.50969917968752 | 68.0575087745829 | |
-99.5606025 | 41.068178502813595 | -155.58278180000002 | 19.896766200000002 | |
-99.5606025 | 41.068178502813595 | -155.41249371406252 | 19.355435189875685 | |
-99.5606025 | 41.068178502813595 | -156.22204876777346 | 20.77817385333129 | |
-99.5606025 | 41.068178502813595 | -156.08334637519533 | 20.781383752662176 | |
-99.5606025 | 41.068178502813595 | -119.41793240000001 | 36.77826099999999 | |
-99.5606025 | 41.068178502813595 | -111.73848904062501 | 34.311442605956636 | |
-99.5606025 | 41.068178502813595 | -118.62691677500001 | 39.80409417718468 | |
-99.5606025 | 41.068178502813595 | -115.56173122812501 | 44.531552843807575 | |
-99.5606025 | 41.068178502813595 | -107.13521755625001 | 43.90164233696157 |
This file contains hidden or 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: 1px; | |
opacity: .5; | |
stroke: tomato; | |
pointer-events: none; | |
fill: none; | |
} | |
.arcs .great-arc-end{ | |
fill: tomato; | |
} | |
</style> | |
<body> | |
<div class="map-container" data-contains="main"></div> | |
<div class="map-container" data-contains="second"></div> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://d3js.org/topojson.v1.min.js"></script> | |
<script> | |
var gfx = { | |
viz: { | |
draw: function(layer){ | |
gfx.baseMap.bake(layer); | |
gfx.arcs.bake(layer); | |
} | |
}, | |
baseMap: { | |
setValues: function(){ | |
// These values are shared among all instances of our basemap | |
// Map dimensions (in pixels) | |
this.width = 600; | |
this.height = 349; | |
// Map projection | |
this.projection = d3.geo.albersUsa() | |
.scale(730.1630554896399) | |
.translate([this.width/2, this.height/2]); //translate to center the map in view | |
// Generate paths based on projection | |
this.path = d3.geo.path() | |
.projection(this.projection); | |
}, | |
bake: function(layer){ | |
this[layer] = {}; | |
// Create an SVG | |
this[layer].svg = d3.select('.map-container[data-contains="'+layer+'"]').append('svg') | |
.attr('width', this.width) | |
.attr('height', this.height); | |
// Keeps track of currently zoomed feature | |
this[layer].centered; | |
this[layer].states = this[layer].svg.append('g') | |
.attr('class','states'); | |
//Create a path for each map feature in the data | |
this[layer].states.selectAll('path') | |
.data(topojson.feature(data.baseMapGeometry, data.baseMapGeometry.objects.states).features) //generate features from TopoJSON | |
.enter() | |
.append('path') | |
.attr('d', this.path) | |
.on('click', function(d,i) { gfx.baseMap.zoom(d,i,layer) }); | |
}, | |
zoom: function(d,i,layer){ | |
//Add any other onClick events here | |
var x, y, k; | |
if (d && gfx.baseMap[layer].centered !== d) { | |
// Compute the new map center and scale to zoom to | |
var centroid = gfx.baseMap.path.centroid(d); | |
var b = gfx.baseMap.path.bounds(d); | |
x = centroid[0]; | |
y = centroid[1]; | |
k = .8 / Math.max((b[1][0] - b[0][0]) / gfx.baseMap.width, (b[1][1] - b[0][1]) / gfx.baseMap.height); | |
gfx.baseMap[layer].centered = d | |
} else { | |
x = gfx.baseMap.width / 2; | |
y = gfx.baseMap.height / 2; | |
k = 1; | |
gfx.baseMap[layer].centered = null; | |
} | |
// Highlight the new feature | |
gfx.baseMap[layer].states.selectAll("path") | |
.classed("highlighted",function(d) { | |
return d === gfx.baseMap[layer].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 | |
gfx.baseMap[layer].svg | |
.transition() | |
.duration(500) | |
.attr("transform","translate(" + gfx.baseMap.width / 2 + "," + gfx.baseMap.height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")"); | |
} | |
}, | |
arcs: { | |
bake: function(layer){ | |
// Group for the arcs | |
gfx.baseMap[layer].arcs = gfx.baseMap[layer].svg.append('g') | |
.attr('class','arcs'); | |
// We're going to have an arc and a circle point, so let's make a separate group for those items to keep things organized | |
var arc_group = gfx.baseMap[layer].arcs.selectAll('.great-arc-group') | |
.data(data.arcs).enter() | |
.append('g') | |
.classed('great-arc-group', true); | |
// In each group, create a path for each source/target pair. | |
arc_group.append('path') | |
.attr('d', function(d) { | |
console.log(d) | |
return gfx.arcs.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. | |
}); | |
// And a circle for each end point | |
arc_group.append('circle') | |
.attr('r', 2) | |
.classed('great-arc-end', true) | |
.attr("transform", function(d) { | |
return "translate(" + gfx.arcs.lngLatToPoint(d.targetLocation) + ")"; | |
}); | |
}, | |
lngLatToArc: function(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 = gfx.baseMap.projection( sourceLngLat ), | |
targetXY = gfx.baseMap.projection( targetLngLat ); | |
// Comment this out for production, 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"; | |
} | |
}, | |
lngLatToPoint: function(location_array){ | |
// Our projection function handles the conversion between lng/lat pairs and svg space | |
// But we put this wrapper around it to handle the even of any empty rows | |
if (location_array) { | |
return gfx.baseMap.projection(location_array); | |
} else { | |
return '0,0'; | |
} | |
} | |
} | |
} | |
var onDone = { | |
initViz: function(){ | |
gfx.baseMap.setValues(); | |
gfx.viz.draw('main'); | |
} | |
} | |
var data = { | |
load: { | |
baseMap: function(callback){ | |
d3.json('us-states.topojson', function(error, baseMapGeometry){ | |
if (error) return console.log(error); // Unknown error, check the console | |
// Store the geodata on the data object for reference later | |
data.baseMapGeometry = baseMapGeometry; | |
callback(); | |
}); | |
}, | |
arcs: function(callback){ | |
d3.csv('arcs.csv', function(error, arcs){ | |
if (error) return console.log(error); // Unknown error, check the console | |
data.arcs = data.transform.locationifyArcCsv(arcs); | |
callback(); | |
}) | |
} | |
}, | |
transform: { | |
locationifyArcCsv: function(arcs){ | |
// Our csv has location stored as separate columns | |
// We need to turn those columns into arrays | |
// And, importantly, we need to convert the values from strings, which the csv probably sees them as into numbers | |
// We can do this conversion (referred to as "casting") by putting a `+` before the value. | |
arcs.forEach(function(arc){ | |
arc.sourceLocation = [+arc.source_lng, +arc.source_lat]; | |
arc.targetLocation = [+arc.target_lng, +arc.target_lat]; | |
}); | |
return arcs; | |
} | |
} | |
} | |
var init = { | |
go: function(){ | |
// Instead of loading the data through this callback situation | |
// You could use queue.js and wait for all of them to be done. | |
// But there's enough going on here for one tutorial. | |
data.load.baseMap(function(){ | |
data.load.arcs(onDone.initViz); | |
}) | |
} | |
} | |
init.go(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment