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.
<!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>
// 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;
}
[
{"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"}
]