block by HarryStevens 1c953744582a7b50d77f79d206dbfbc6

Turn the Earth

Full Screen

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.

index.html

<!DOCTYPE html>
<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>