block by harrystevens a9aa1ccd35c904d98bc4809e0bd2ac06

Space Bars

Full Screen

Not all maps are of Earth! To measure distances on other planets, a map’s scale bar must be configured so that it knows the radius of the planet upon which it will rest. When using d3-geo-scale-bar, you can use use scaleBar.radius.

This example displays a map showing the locations of spacecraft landings on the near side of the Moon. To place a scale bar on the Moon, simply pass 1737.4, the radius of the Moon in kilometers, to scaleBar.radius. If you wish to indicate miles, set scaleBar.radius to 1079.4, the radius of the Moon in miles.

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      margin: 0;
    }

    #titles {
      margin-bottom: 10px;
    }

    .headline {
      display: inline-block;
      font-weight: 600;
      margin-right: 20px;
    }

    .legend {
      display: inline-block;
    }

    .legend .item {
      margin-right: 10px;
      display: inline-block;
    }

    .legend .swatch {
      border-radius: 50%;
      display: inline-block;
      height: 8px;
      margin-right: 5px;
      width: 8px;
    }

    .legend .category {
      display: inline-block;
    }

    .scale-bar {
      paint-order: stroke fill;
      stroke: white;
      stroke-linecap: round;
      stroke-linejoin: round;
      stroke-opacity: .5;
      stroke-width: 3px;
    }

    .position text {
      paint-order: stroke fill;
      stroke: white;
      stroke-linecap: round;
      stroke-linejoin: round;
      stroke-opacity: .5;
      stroke-width: 3px;
      font-family: sans-serif;
      font-size: 14px;      
    }
  </style>
</head>
<body>
  <div id="titles">
    <div class="headline">Landings on the near side of the moon, <span class="start"></span><span class="end"></span></div>
    <div class="legend"></div>
  </div>
  <div id="map"></div>
  <script src="https://d3js.org/d3.v6.min.js"></script>
  <script src="https://unpkg.com/d3-geo-scale-bar@1.1.1"></script>
  <script src="https://unpkg.com/geometric@2.2.3"></script>
  <script src="geo-raster-reproject.js"></script>
  <script>
    // Titles
    const headline = d3.select("#titles .headline");
    const legend = d3.select("#titles .legend");
    const item = legend.selectAll(".item")
        .data(["USA", "USSR", "China"])
      .enter().append("div")
        .attr("class", "item");

    item.append("div")
        .attr("class", "swatch")
        .style("background", d => d === "USA" ? "steelblue" : d === "USSR" ? "tomato" : "yellow")
        .style("border", d => `1px solid ${d === "USA" ? "blue" : d === "USSR" ? "red" : "gold"}`)
        
    // Map
    const projection = d3.geoConicEquidistant();

    const scaleBarMoon = d3.geoScaleBar()
        .projection(projection)
        .radius(1737.4) // The radius of the moon is 1,737.4 kilometers
        .top(.9)
        .left(.05);

    const el = d3.select("#map").append("div");

    const canvas = el.append("canvas")
        .style("opacity", .8)
        .style("position", "absolute");

    const reproject = geoRasterReproject()
        .projection(projection)
        .context(canvas.node().getContext("2d"));

    const svg = el.append("svg")
        .style("position", "absolute");

    const bar = svg.append("g")
        .attr("class", "scale-bar");

    // Fetch the moon landings JSON
    d3.json("moon-landings.json")
      .then(landings => {
        // Fill in the headline
        headline.select(".start").text(d3.min(landings, d => d.year));
        headline.select(".end").text(d3.max(landings, d => d.year));

        // Fill in the legend
        item.append("div")
            .attr("class", "category")
            .text(d => `${d} (${landings.filter(f => f.country === d).length})`);

        // Make the convex hull of the moon landings
        // to use to fit the projection
        const hull = (_ => {
          const h = geometric.polygonScale(geometric.polygonHull(landings.map(d => [d.lon, d.lat])).reverse(), 1.1);
          
          return {
            type: "Polygon",
            coordinates: [[...h, h[0]]]
          };
        
        })();

        // Load the raster image
        const moon = new Image;
        moon.src = "2k_moon.jpg";
        moon.onload = draw;

        // Place the moon landings
        const position = svg.selectAll(".position")
            .data(landings)
          .enter().append("g")
            .attr("class", "position");

        position.append("circle")
            .attr("r", 4)
            .attr("fill", d => d.country === "USA" ? "steelblue" : d.country === "USSR" ? "tomato" : "yellow")
            .attr("stroke", d => d.country === "USA" ? "blue" : d.country === "USSR" ? "red" : "gold");

        const label = position.append("text")
            .attr("text-anchor", d => d.label.includes("e") ? "start" : "end")
            .attr("transform", d => `translate(${d.label.includes("e") ? 7 : -7}, ${d.label.includes("n") ? 0 : 10})`)
            .text(d => d.mission);

        addEventListener("resize", draw);

        function draw(){
          const width = innerWidth,
                height = fitWidth(projection, width);

          scaleBarMoon.size([width, height]);

          projection.fitSize([width, height], hull);

          canvas
              .attr("width", width)
              .attr("height", height);
          
          reproject
              .size([width, height])
              (moon);

          svg
              .attr("width", width)
              .attr("height", height);

          bar.call(scaleBarMoon);
          
          position.attr("transform", d => `translate(${projection([d.lon, d.lat])})`);

          label.style("display", d => width < 600 && d.mission !== "Apollo 11" ? "none" : "block");
        }

      });

    function fitWidth(projection, width) {
      const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, {type: "Sphere"})).bounds({type: "Sphere"});
      const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
      projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
      return dy;
    }
  </script>
</body>
</html>

geo-raster-reproject.js

// Inspired by: https://bl.ocks.org/mbostock/4329423
function geoRasterReproject(){
  let context,
      projection,
      size;

  function reproject(image){
    let dx = image.width,
        dy = image.height,
        w = size[0],
        h = size[1],
        divisor = 1;

    while (dx / w > divisor) divisor *= 2;
    dx /= divisor;
    dy /= divisor;
    
    context.drawImage(image, 0, 0, dx, dy);
    
    const sourceData = context.getImageData(0, 0, dx, dy).data,
        target = context.createImageData(w, h),
        targetData = target.data;
    
    for (let y = 0, i = -1; y < h; ++y) {
      for (let x = 0; x < w; ++x) {
        const p = projection.invert([x, y]), lambda = p[0], phi = p[1];
        if (lambda > 180 || lambda < -180 || phi > 90 || phi < -90) { i += 4; continue; }
        let q = ((90 - phi) / 180 * dy | 0) * dx + ((180 + lambda) / 360 * dx | 0) << 2;
        targetData[++i] = sourceData[q];
        targetData[++i] = sourceData[++q];
        targetData[++i] = sourceData[++q];
        targetData[++i] = 255;
      }
    }
    
    context.clearRect(0, 0, w, h);
    context.putImageData(target, 0, 0);
  }
  
  reproject.context = function(_){
    return arguments.length ? (context = _, reproject) : context;
  }

  reproject.projection = function(_){
    return arguments.length ? (projection = _, reproject) : projection;
  }
  
  reproject.size = function(_){
    return arguments.length ? (size = _, reproject) : size;
  }
  
  return reproject;
}

moon-landings.json

[
  {"mission":"Chang'e 3","year":2013,"country":"China","lat":44.12,"lon":-19.51,"label":"ne"},
  {"mission":"Luna 24","year":1976,"country":"USSR","lat":12.7145,"lon":62.2129,"label":"ne"},
  {"mission":"Luna 21","year":1973,"country":"USSR","lat":25.85,"lon":30.45,"label":"ne"},
  {"mission":"Apollo 17","year":1972,"country":"USA","lat":20.18809,"lon":30.77475,"label":"se"},
  {"mission":"Apollo 16","year":1972,"country":"USA","lat":-8.97341,"lon":15.49859,"label":"sw"},
  {"mission":"Luna 20","year":1972,"country":"USSR","lat":3.533333,"lon":56.55,"label":"ne"},
  {"mission":"Apollo 15","year":1971,"country":"USA","lat":26.13224,"lon":3.634,"label":"sw"},
  {"mission":"Apollo 14","year":1971,"country":"USA","lat":-3.64544,"lon":-17.47139,"label":"se"},
  {"mission":"Luna 17","year":1970,"country":"USSR","lat":38.28,"lon":-35,"label":"nw"},
  {"mission":"Luna 16","year":1970,"country":"USSR","lat":-0.683333,"lon":56.3,"label":"se"},
  {"mission":"Apollo 12","year":1969,"country":"USA","lat":-3.01381,"lon":-23.4193,"label":"ne"},
  {"mission":"Surveyor 5","year":1967,"country":"USA","lat":1.41,"lon":23.18,"label":"nw"},
  {"mission":"Apollo 11","year":1969,"country":"USA","lat":0.67409,"lon":23.47298,"label":"se"},
  {"mission":"Surveyor 7","year":1968,"country":"USA","lat":-41.01,"lon":-11.41,"label":"ne"},
  {"mission":"Surveyor 6","year":1967,"country":"USA","lat":0.49,"lon":-1.4,"label":"se"},
  {"mission":"Surveyor 3","year":1967,"country":"USA","lat":-2.94,"lon":-23.34,"label":"sw"},
  {"mission":"Luna 13","year":1966,"country":"USSR","lat":18.87,"lon":-62.05,"label":"nw"},
  {"mission":"Surveyor 1","year":1966,"country":"USA","lat":-2.474,"lon":-43.339,"label":"sw"},
  {"mission":"Luna 9","year":1966,"country":"USSR","lat":7.08,"lon":-64.37,"label":"nw"}
]