block by mhkeller f41cceac3e7ed969eaeb

A basic setup showing how to draw arc paths on a map with D3.

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: 2px;
	  stroke: tomato;
	  pointer-events: none;
	  fill: none;
	}

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