This is the code for Chapter 7, Figure 12 from D3.js in Action showing how to create a globe using the d3.geo.orthographic() projection as well as a modified zoom that changes the rotate of the projection based on mouse behavior.
<html>
<head>
<title>D3 in Action Chapter 7 - Example 5</title>
<meta charset="utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://d3js.org/d3.geo.projection.v0.min.js" type="text/JavaScript"></script>
<script src="https://d3js.org/queue.v1.min.js" type="text/JavaScript"></script>
<script src="https://d3js.org/colorbrewer.v1.min.js" type="text/JavaScript"></script>
</head>
<style>
svg {
height: 500px;
width: 500px;
border: 1px solid gray;
}
.countries {
fill: red;
fill-opacity: .5;
stroke: black;
stroke-width: 1px;
}
</style>
<body>
<div id="viz">
<svg>
</svg>
</div>
<div id="controls" />
</body>
<footer>
<script>
queue()
.defer(d3.json, "world.geojson")
.defer(d3.csv, "cities.csv")
.await(function(error, file1, file2) { createMap(file1, file2); });
function createMap(countries, cities) {
expData = countries;
width = 500;
height = 500;
projection = d3.geo.orthographic()
.scale(200)
.translate([width / 2, height / 2])
.clipAngle(90);
geoPath = d3.geo.path().projection(projection);
var mapZoom = d3.behavior.zoom().translate(projection.translate()).scale(projection.scale()).on("zoom", zoomed);
d3.select("svg").call(mapZoom);
var rotateScale = d3.scale.linear()
.domain([0, width])
.range([-180, 180]);
d3.select("svg").on("mousedown", startRotating).on("mouseup", stopRotating);
function startRotating() {
d3.select("svg").on("mousemove", function() {
var p = d3.mouse(this);
projection.rotate([rotateScale(p[0]), 0]);
zoomed();
});
}
function stopRotating() {
d3.select("svg").on("mousemove", null);
}
function zoomed() {
var currentRotate = projection.rotate()[0];
projection.scale(mapZoom.scale());
d3.selectAll("path.graticule").attr("d", geoPath);
d3.selectAll("path.countries").attr("d", geoPath);
d3.selectAll("circle.cities")
.attr("cx", function(d) {return projection([d.y,d.x])[0]})
.attr("cy", function(d) {return projection([d.y,d.x])[1]})
.style("display", function(d) {return parseInt(d.y) + currentRotate < 90 && parseInt(d.y) + currentRotate > -90 ? "block" : "none"})
}
function zoomButton(zoomDirection) {
if (zoomDirection == "in") {
var newZoom = mapZoom.scale() * 1.5;
var newX = ((mapZoom.translate()[0] - (width / 2)) * 1.5) + width / 2;
var newY = ((mapZoom.translate()[1] - (height / 2)) * 1.5) + height / 2;
}
else if (zoomDirection == "out") {
var newZoom = mapZoom.scale() * .75;
var newX = ((mapZoom.translate()[0] - (width / 2)) * .75) + width / 2;
var newY = ((mapZoom.translate()[1] - (height / 2)) * .75) + height / 2;
}
mapZoom.scale(newZoom).translate([newX,newY])
zoomed();
}
d3.select("#controls").append("button").on("click", function () {zoomButton("in")}).html("Zoom In");
d3.select("#controls").append("button").on("click", function () {zoomButton("out")}).html("Zoom Out");
featureSize = d3.extent(countries.features, function(d) {return geoPath.area(d)});
countryColor = d3.scale.quantize().domain(featureSize).range(colorbrewer.Reds[7]);
var graticule = d3.geo.graticule();
d3.select("svg").append("path")
.datum(graticule)
.attr("class", "graticule line")
.attr("d", geoPath)
.style("fill", "none")
.style("stroke", "lightgray")
.style("stroke-width", "1px");
d3.select("svg").selectAll("path.countries").data(countries.features)
.enter()
.append("path")
.attr("d", geoPath)
.attr("class", "countries")
.style("fill", function(d) {return countryColor(geoPath.area(d))})
.style("stroke-width", 1)
.style("stroke", "black")
.style("opacity", .5)
.on("mouseover", centerBounds)
.on("mouseout", clearCenterBounds)
d3.select("svg").selectAll("circle").data(cities)
.enter()
.append("circle")
.attr("class", "cities")
.style("fill", "black")
.style("stroke", "white")
.style("stroke-width", 1)
.attr("r", 3)
.attr("cx", function(d) {return projection([d.y,d.x])[0]})
.attr("cy", function(d) {return projection([d.y,d.x])[1]})
function centerBounds(d,i) {
thisBounds = geoPath.bounds(d);
thisCenter = geoPath.centroid(d);
d3.select("svg")
.append("rect")
.attr("class", "bbox")
.attr("x", thisBounds[0][0])
.attr("y", thisBounds[0][1])
.attr("width", thisBounds[1][0] - thisBounds[0][0])
.attr("height", thisBounds[1][1] - thisBounds[0][1])
.style("fill", "none")
.style("stroke-dasharray", "5 5")
.style("stroke", "black")
.style("stroke-width", 2)
.style("pointer-events", "none")
d3.select("svg")
.append("circle")
.attr("class", "centroid")
.attr("r", 5)
.attr("cx", thisCenter[0]).attr("cy", thisCenter[1])
.style("pointer-events", "none")
}
function clearCenterBounds() {
d3.selectAll("circle.centroid").remove();
d3.selectAll("rect.bbox").remove();
}
}
</script>
</footer>
</html>
"label","population","country","x","y"
"San Francisco", 750000,"USA",37,-122
"Fresno", 500000,"USA",36,-119
"Lahore",12500000,"Pakistan",31,74
"Karachi",13000000,"Pakistan",24,67
"Rome",2500000,"Italy",41,12
"Naples",1000000,"Italy",40,14
"Rio",12300000,"Brazil",-22,-43
"Sao Paolo",12300000,"Brazil",-23,-46