Skip to content

Instantly share code, notes, and snippets.

@mhkeller
Created June 17, 2014 16:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mhkeller/3921b109bd2b097e8412 to your computer and use it in GitHub Desktop.
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.
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
<!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>
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