block by mhkeller 3921b109bd2b097e8412

A more complex setup showing how to draw arc paths on a map with D3, loading and transforming data from a csv.

Full Screen

index.html

<!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="//d3js.org/d3.v3.min.js"></script>
	<script src="//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>

arcs.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"