Use the dials on the left to adjust the rotation of a geographic projection. D3 supports three-axis rotation, allowing you to adjust the lambda (λ), phi (φ) and gamma (γ) axes.
This map uses an orthographic projection with D3.js and Topojson to draw a map of the world's countries onto a globe, rendered here with a graticule that is 10° by 10°.
The world countries polygons were downloaded from ArcGIS.
xxxxxxxxxx
<html>
<head>
<style>
body {
font-family: "Helvetica Neue", sans-serif;
margin: 0;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script>
var width = window.innerWidth, height = window.innerHeight;
var projection = d3.geoOrthographic()
.scale(width / 4.1)
.translate([width / 2, height / 2])
.clipAngle(90 + 1e-6)
.precision(1)
.rotate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
var graticule = d3.geoGraticule()
.step([10, 10]);
g.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill", "#fff")
.style("stroke", "#ccc");
var dials = [{
name: "λ",
scale: d3.scaleLinear().domain([40, height / 2 - 40]).range([-180, 180]),
rscale: d3.scaleLinear().domain([-180, 180]).range([40, height / 2 - 40])
},{
name: "φ",
scale: d3.scaleLinear().domain([40, height / 2 - 40]).range([90, -90]),
rscale: d3.scaleLinear().domain([90, -90]).range([40, height / 2 - 40])
},{
name: "γ",
scale: d3.scaleLinear().domain([40, height / 2 - 40]).range([180, -180]),
rscale: d3.scaleLinear().domain([180, -180]).range([40, height / 2 - 40])
}];
svg.selectAll(".dial-rect")
.data(dials)
.enter().append("rect")
.attr("class", "dial-rect")
.attr("x", function(d,i){ return 45 * (i + 1); })
.attr("y", 40)
.attr("width", 20)
.attr("height", height / 2 - 80)
.attr("rx", 10)
.attr("ry", 10)
.style("stroke", "#ccc")
.style("fill", "#3a403d");
svg.selectAll(".dial-text")
.data(dials)
.enter().append("text")
.attr("class", "dial-text")
.attr("x", function(d,i) { return 45 * (i + 1); })
.attr("y", 15)
.attr("dx", 10)
.attr("text-anchor", "middle")
.style("fill", "#3a403d")
.text(function(d){ return d.name; });
svg.selectAll(".dial-circle")
.data(dials)
.enter().append("circle")
.attr("class", function(d){ return "dial-circle dial-" + d.name; })
.attr("cx", function(d,i){ return 45 * (i + 1) + 10; })
.attr("cy", function(d){ return d.rscale(0); })
.attr("r", 20)
.style("stroke", "#aaa")
.style("fill", "#ccc")
.style("cursor", "ns-resize")
.call(d3.drag().on("drag", dragged));
svg.selectAll(".dial-circle-text")
.data(dials)
.enter().append("text")
.attr("class", function(d) { return "dial-circle-text dial-" + d.name; })
.attr("x", function(d,i){ return 45 * (i + 1) + 10; })
.attr("y", function(d){ return d.rscale(0) + 5; })
.attr("text-anchor", "middle")
.style("font-size", ".7em")
.style("cursor", "ns-resize")
.text("0")
.call(d3.drag().on("drag", dragged));
function dragged(d){
var y = d3.mouse(this)[1];
y < 40 ? y = 40 : y = y;
y > height / 2 - 40 ? y = height / 2 - 40 : y = y;
d3.select(".dial-circle.dial-" + d.name)
.attr("cy", y);
d3.select(".dial-circle-text.dial-" + d.name)
.attr("y", y + 5)
.text(Math.round(d.scale(y)));
projection.rotate([dials[0].scale(d3.select(".dial-λ").attr("cy")), dials[1].scale(d3.select(".dial-φ").attr("cy")), dials[2].scale(d3.select(".dial-γ").attr("cy"))])
g.selectAll("path").attr("d", path);
}
var c = d3.scaleOrdinal(d3.schemeCategory20);
d3.json("countries.json", function(error, data){
g.selectAll(".subunit")
.data(topojson.feature(data, data.objects.polygons).features)
.enter().append("path")
.attr("class", "subunit")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-width", "1px")
.style("fill", function(d,i){ return c(i); })
.style("opacity", ".6");
});
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://d3js.org/topojson.v1.min.js