A sphere does not fit on a rectangle, and every way of forcing it lies about
something: shape, area, or distance. mappo now lets you choose the lie. Four projections
ship, any central meridian, and a seam for your own — and every one of them gets the whole
toolkit for free: the dot field, contours, vector coastlines, borders, places, overlays,
locate() and the graticule are one code path with a projection plugged in.
A prototype · mappo · more demos · other worlds
Left, the equirectangular map every version of mappo has drawn: latitude and longitude as straight lines, areas stretched toward the poles (Greenland looks the size of Africa; it is a fourteenth). Right, Equal Earth (Šavrič, Patterson & Jenny, 2019): areas are true everywhere, the meridians curve, and the corners of the frame are simply not part of the world. Same body, same data, same attributes but one.
The default. The frame is 360° by the latitude band; the graticule is a grid of straight lines.
Equal-area. The frame is 2.05:1 for the whole sphere and the edge of the world is the curve the clip follows.
<mappo-world projection="equal-earth" lat-min="-90" lat-max="90"
figure="solid outline" figure-source="vector" borders graticule></mappo-world>
A map centred on 0° cuts the Pacific in half and puts Auckland and Los Angeles
at opposite ends of the page. center-lon moves the central meridian; the seam moves
with it, and the coastlines are re-cut at the new seam rather than at the old one — the
Chukchi coast and Fiji are whole, the Atlantic is split, and nothing is stroked along the edge.
Dots, places and the equator, all through the same central meridian.
<mappo-world center-lon="150" places="Tokyo, Sydney, Auckland, Los Angeles"></mappo-world>
On an equirectangular map the whole Artemis landing region — a few hundred
kilometres around the lunar south pole — is smeared across the bottom row. Polar stereographic
puts the pole at the centre and keeps shapes true (it is conformal; the scale grows toward the
rim, 2× at the equator of a hemisphere). On a polar map lat-min/lat-max
name the band you see: the far bound is the rim of the disc. Longitude 90°E is to the right in
both aspects, so 0° points down on a north map and up on a south one, the way NASA and USGS
print them. Sites arrive as ordinary data-lat/data-lon children.
The last ten degrees to the lunar south pole: highland almost entirely, so the disc is regolith, and what this map is for is where things are — Shackleton at the centre, the Artemis candidate regions around it, each an ordinary overlay child. On an equirectangular map all of this is one row of dots.
The Arctic as one ocean, the way it is: Greenland, Svalbard and the Siberian coast around it, borders included.
Places come from the gazetteer or as { name, lat, lon } records.
Mars from above the north pole: the lowlands of Vastitas Borealis as one basin, whole across the 180° meridian.
<mappo-moon projection="stereographic-south" lat-max="-80" figure="solid" figure-source="vector" graticule>
<a class="pin" data-lat="-89.7" data-lon="129.2">Shackleton</a>
</mappo-moon>
// or in JavaScript — the same options, the same answers
const map = new Mappo(el, { body: "moon", projection: "stereographic-south", latMax: -80 });
map.projection.forward(-89.7, 129.2); // → { x: 0.5..., y: 0.5... } the pole is the centre
map.locate(45, 0); // → null: the far hemisphere has no place on this map
A projection is a small object: forward(lat, lon) to a point in
the unit frame (or null), inverse(x, y) back to a coordinate (or
null off the world), an aspect, and optionally an
outline() for the clip. The dot field, the contours and the highlights come from
the inverse alone. A custom projection is antimeridian-cut by default; set
seam: false only when it has no cylindrical seam. Below, a sinusoidal projection
written in eight lines. A d3-geo projection works unchanged: mappo uses its stream, including
rotation, clipping and adaptive resampling.
Equal-area, straight parallels, meridians as sine curves. Nothing in mappo knows its name.
const RAD = Math.PI / 180;
const sinusoidal = {
id: "sinusoidal", aspect: 2,
forward: (lat, lon) => ({ x: 0.5 + lon * Math.cos(lat * RAD) / 360, y: (90 - lat) / 180 }),
inverse: (x, y) => {
const lat = 90 - y * 180, lon = (x - 0.5) * 360 / Math.cos(lat * RAD);
return Math.abs(lon) <= 180 ? { lat, lon } : null; // null: off the world
},
outline: () => [ [ ...meridian(-180), ...meridian(180).reverse() ] ]
};
new Mappo(el, { projection: sinusoidal, latRange: [-90, 90] });
// a d3-geo projection is accepted as it is (.invert + .stream)
import { geoMollweide } from "d3-geo-projection";
new Mappo(el, { projection: geoMollweide(), latRange: [-90, 90] });