block by fil 2e885160bf2b4d690e976db0f9de1557

visualizing map distortion d3v4

Full Screen

Ported to d3v4 by Philippe Rivière from Ian Johnson‘s block: visualizing map distortion


@enjalot: Whenever we try to represent our 3D earth on a 2D map we necessarily introduce distortion. This tool attempts to visualize the phenomenon.

Inspiration

Original prompt by @curran
Bounding box solution by @tyrasd

References

projection comparison
map zoom

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<style>
  svg {
    margin: 22px;
  }
  select {
    margin-left: 20px;
  }
  path.foreground {
    fill: none;
    stroke: #333;
    stroke-width: 1.5px;
  }
  path.graticule {
    fill: none;
    stroke: #aaa;
    stroke-width: .5px;
  }
  #left {
    cursor: move;
  }
  #left .land {
    fill: #d7c7ad;
    stroke: #a5967e;
  }
  #right .land {
    fill: #cfcece;
    stroke: #a5967e;
  }
  
  #left circle {
    fill: #d8355e;
  }
  #right circle {
    stroke: #d8355e;
    fill: none;
  }
</style>
</head>

<body>
  <svg id="left"></svg>
  <svg id="right"></svg>
  <select></select>
  
  <script>

    var map_width = 400;
    var map_height = 400;
    var center = [-90, 37];
    var scale0 = (map_width - 1) / 2 / Math.PI * 6
    var scale1 = (map_width - 1) / 2 / Math.PI * 3
    
    var zoom = d3.zoom()
    .on("zoom", zoomed);
    
    
    var projectionLeft = d3.geoAitoff()
    .center(center)
    
    var projectionRight = d3.geoOrthographic()
    .center(center)
    .translate([map_width/5, map_height / 5])
    .scale(scale1)
    .clipAngle(90)

    var pathLeft = d3.geoPath()
        .projection(projectionLeft);
    var pathRight = d3.geoPath()
        .projection(projectionRight);
    
    function zoomed() {
      projectionLeft
          .translate([d3.event.transform.x, d3.event.transform.y])
          .scale(d3.event.transform.k)
      
      var newCenter = projectionLeft.invert([map_width/2,map_height/2]);
      projectionRight .rotate([-newCenter[0], -newCenter[1]])

      update();
    }
    
    function update() {
      d3.selectAll("#left path")
          .attr("d", pathLeft);
      d3.selectAll("#right path")
          .attr("d", pathRight);
      
      d3.selectAll("#left circle")
      .attr('transform', function(d,i) {
        return 'translate(' + [ d.x, d.y ] + ')';
      })
      
      d3.selectAll("#right circle")
      .attr('transform', function(d,i) {
        try {
          var latlon = projectionLeft.invert([d.x, d.y])
          return 'translate(' + projectionRight(latlon) + ')';
        } catch(e) {
          return 'translate(-100,-100)';
        }
      })
    }
    
    

    var graticule = d3.geoGraticule();
    var svgLeft = d3.select("#left")
        .attr("width", map_width)
        .attr("height", map_height);
    var svgRight = d3.select("#right")
        .attr("width", map_width + 40)
        .attr("height", map_height);

    svgLeft.append("path")
        .datum(graticule)
        .attr("class", "graticule")
        .attr("d", pathLeft);
    
    svgRight.append("path")
        .datum(graticule)
        .attr("class", "graticule")
        .attr("d", pathRight);

    d3.json("world-110m.json", function(error,world) {
      if (error) throw error;

      svgLeft.insert("path", ".graticule")
          .datum(topojson.feature(world, world.objects.land))
          .attr("class", "land")
          .attr("d", pathLeft);
      
      svgRight.insert("path", ".graticule")
          .datum(topojson.feature(world, world.objects.land))
          .attr("class", "land")
          .attr("d", pathRight);
      
      var points = generateRect(100, 25, 25, map_width - 50, map_height - 50);
      
      svgLeft.selectAll("circle")
        .data(points)
        .enter().append("circle")
        .attr('r', 3)
      
      svgRight.selectAll("circle")
        .data(points)
        .enter().append("circle")
        .attr('r', 2)
      
      svgLeft
        .call(zoom)
        .call(
          zoom.transform,
           d3.zoomIdentity.translate(map_width/2,map_height/2)
           .scale(scale0)
        )
      ;
    });
    
    var projections = {
      "Aitoff": d3.geoAitoff().scale(90),
      "Boggs Eumorphic": d3.geoBoggs().scale(90),
      "Craster Parabolic (Putnins P4)": d3.geoCraster().scale(90),
      "Cylindrical Equal-Area": d3.geoCylindricalEqualArea().scale(120),
      "Eckert I": d3.geoEckert1().scale(95),
      "Eckert III": d3.geoEckert3().scale(105),
      "Eckert IV": d3.geoEckert4().scale(105),
      "Eckert V": d3.geoEckert5().scale(100),
      "Equidistant Cylindrical (Plate Carrée)": d3.geoEquirectangular().scale(90),
      "Fahey": d3.geoFahey().scale(75),
      "Foucaut Sinusoidal": d3.geoFoucaut().scale(80),
      "Gall (Gall Stereographic)": d3.geoCylindricalStereographic().scale(70),
      "Ginzburg VIII (TsNIIGAiK 1944)": d3.geoGinzburg8().scale(75),
      "Kavraisky VII": d3.geoKavrayskiy7().scale(90),
      "Larrivée": d3.geoLarrivee().scale(55),
      "McBryde-Thomas Flat-Pole Sine (No. 2)": d3.geoMtFlatPolarSinusoidal().scale(95),
      "Mercator": d3.geoMercator().scale(50),
      "Miller Cylindrical I": d3.geoMiller().scale(60),
      "Mollweide": d3.geoMollweide().scale(100),
      "Natural Earth": d3.geoNaturalEarth().scale(100),
      "Nell-Hammer": d3.geoNellHammer().scale(120),
      "Quartic Authalic": d3.geoHammer().coefficient(Infinity).scale(95),
      "Robinson": d3.geoRobinson().scale(90),
      "Sinusoidal": d3.geoSinusoidal().scale(90),
      "van der Grinten (I)": d3.geoVanDerGrinten().scale(50),
      "Wagner VI": d3.geoWagner6().scale(90),
      "Wagner VII": d3.geoWagner7().scale(90),
      "Winkel Tripel": d3.geoWinkel3().scale(90),
      "Wiechel": d3.geoWiechel().scale(90)    };
    var selector = d3.select("select")
    selector.selectAll("option")
      .data(Object.keys(projections))
      .enter().append("option")
      .attr('value', function(d) { return d }).text(function(d) { return d })
    
    selector.on("change", function(d) {
      console.log("sup", d3.event)
      var proj = d3.event.target.selectedOptions[0].value;
      projectionLeft = projections[proj].center(center);
      pathLeft = d3.geoPath()
        .projection(projectionLeft);
      svgLeft
        .call(zoom)
        .call(
          zoom.transform,
           d3.zoomIdentity.translate(map_width/2,map_height/2)
           .scale(scale0)
        )
      ;
    })
    
    
    function generateRect(num, x, y, width, height) {
      var points = []
      var sideNum = Math.floor(num/4) + 1;
      // top
      d3.range(sideNum).forEach(function(i) {
        points.push({ x: x + i * width/sideNum, y: y })
      })
      // right
      d3.range(sideNum).forEach(function(i) {
        points.push({ x: x + width, y: y + i * height/sideNum })
      })
      // bottom
      d3.range(sideNum).forEach(function(i) {
        points.push({ x: x + width - i * width/sideNum, y: y + height })
      })
      // left
      d3.range(sideNum).forEach(function(i) {
        points.push({ x: x, y: y + height - i * height/sideNum })
      })
      return points;
    }
    
  </script>
</body>


<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  ga('create', 'UA-67666917-1', 'auto');
  ga('send', 'pageview');
</script>