Skip to content

Instantly share code, notes, and snippets.

@cambecc
Last active January 17, 2021 04:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cambecc/eb1380f8cb0da3d4142e870a3a6b623b to your computer and use it in GitHub Desktop.
Save cambecc/eb1380f8cb0da3d4142e870a3a6b623b to your computer and use it in GitHub Desktop.
interrupted projections sometimes have partially undefined inverse (for d3-geo-projection 3.0.0)

An interrupted projection's inverse is sometimes partially undefined, depending on the shape of the interruption. This is using d3-geo-projection 3.0.0.

For example, the first projection is "Interrupted Mollweide Oceans" from here. The blue pixels represent the screen coordinates where the projection's inverse is defined. Note that only part of the globe is colored blue.

The second projection is a slightly modified "Interrupted Mollweide Oceans", where the lobes are adjusted to land on the equator rather than ±5° lat. In this case, the globe is fully blue because the projection's inverse is defined for all screen coordinates that are inside the bounds of the globe.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>proj tester</title>
<meta name="viewport" content="width=device-width"/>
</head>
<body style="margin: 0">
<canvas id="a" style="position: absolute; left:0; top: 0; width: 100%; height: 50%;"></canvas>
<canvas id="b" style="position: absolute; left:0; top: 50%; width: 100%; height: 50%;"></canvas>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
<script>
// Source: https://observablehq.com/@d3/interrupted-mollweide-oceans
const interruptedMollweideOceans = d3.geoInterrupt(
d3.geoMollweideRaw,
[[ // northern hemisphere
[[-180, 0], [-130, 90], [-90, 5]],
[[-90, 5], [-30, 90], [60, 5]],
[[60, 5], [120, 90], [180, 0]]
], [ // southern hemisphere
[[-180, 0], [-120, -90], [-60, -5]],
[[-60, -5], [20, -90], [90, -5]],
[[90, -5], [140, -90], [180, 0]]
]]
);
// Same as above, but with interruption points placed at the equator
const interruptedMollweideOceans_adjusted = d3.geoInterrupt(
d3.geoMollweideRaw,
[[ // northern hemisphere
[[-180, 0], [-130, 90], [-90, 0]],
[[-90, 0], [-30, 90], [60, 0]],
[[60, 0], [120, 90], [180, 0]]
], [ // southern hemisphere
[[-180, 0], [-120, -90], [-60, 0]],
[[-60, 0], [20, -90], [90, 0]],
[[90, 0], [140, -90], [180, 0]]
]]
);
function fillGlobeWithProjectionInvert(canvas, proj) {
const width = canvas.clientWidth * devicePixelRatio;
const height = canvas.clientHeight * devicePixelRatio;
canvas.width = width;
canvas.height = height;
proj.fitExtent([[20, 20], [width - 20, height - 20]], {type: "Sphere"});
const ctx = canvas.getContext("2d");
const path = d3.geoPath().projection(proj).context(ctx);
// first fill the whole globe with red as a basis for comparison
ctx.fillStyle = "red";
ctx.beginPath();
path({type: "Sphere"});
ctx.clip();
ctx.fill();
// now scan all pixels in the canvas and switch red to blue wherever the
// projection's invert function is defined
const imageData = ctx.getImageData(0, 0, width, height);
const {data} = imageData;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const [lon, lat] = proj.invert([x, y]) ?? [];
if (!isNaN(lon) && !isNaN(lat)) {
const i = (y * width + x) * 4;
data[i ] = 100; // r
data[i+1] = 100; // g
data[i+2] = 255; // b
}
}
}
ctx.putImageData(imageData, 0, 0);
// add a graticule to help visualize the projection
const graticule = d3.geoGraticule()();
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
path(graticule);
ctx.stroke();
}
fillGlobeWithProjectionInvert(d3.select("#a").node(), interruptedMollweideOceans);
fillGlobeWithProjectionInvert(d3.select("#b").node(), interruptedMollweideOceans_adjusted);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment