Skip to content
Documentation menu
docs / examples / data-on-the-globe
Example

Data on the globe

Your own lines and points on the globe projection, from offices to the routes between them. Lines are bent into great circles before they are added, because a straight two-point line is not the shortest path on a sphere.

ts
import { createMap } from "@mapmap/maps";

const map = createMap({
  container: "map",
  apiKey: "snk_...",
  attributionPosition: "bottom-left",
  center: [-30, 40],
  zoom: 1.6,
});
await map.whenReady();

// map.map is the underlying MapLibre map; globe is a MapLibre projection.
map.map.setProjection({ type: "globe" });

const offices: Record<string, [number, number]> = {
  London: [-0.1278, 51.5074],
  "New York": [-74.006, 40.7128],
  Singapore: [103.8198, 1.3521],
  "Sao Paulo": [-46.6333, -23.5505],
};

// Points along the great circle between two places, so the line follows
// the curve of the earth instead of a straight cut across the projection.
function greatCircle(a: [number, number], b: [number, number], steps = 64): [number, number][] {
  const rad = Math.PI / 180;
  const toXyz = ([lng, lat]: [number, number]) => [
    Math.cos(lat * rad) * Math.cos(lng * rad),
    Math.cos(lat * rad) * Math.sin(lng * rad),
    Math.sin(lat * rad),
  ];
  const p = toXyz(a), q = toXyz(b);
  const omega = Math.acos(Math.min(1, p[0] * q[0] + p[1] * q[1] + p[2] * q[2]));
  const out: [number, number][] = [];
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const s1 = Math.sin((1 - t) * omega) / Math.sin(omega);
    const s2 = Math.sin(t * omega) / Math.sin(omega);
    const [x, y, z] = [0, 1, 2].map((k) => s1 * p[k] + s2 * q[k]);
    out.push([Math.atan2(y, x) / rad, Math.atan2(z, Math.hypot(x, y)) / rad]);
  }
  return out;
}

const from = offices.London;
map.map.addSource("links", {
  type: "geojson",
  data: {
    type: "FeatureCollection",
    features: Object.entries(offices)
      .filter(([name]) => name !== "London")
      .map(([name, to]) => ({
        type: "Feature",
        properties: { to: name },
        geometry: { type: "LineString", coordinates: greatCircle(from, to) },
      })),
  },
});
map.map.addSource("offices", {
  type: "geojson",
  data: {
    type: "FeatureCollection",
    features: Object.entries(offices).map(([name, at]) => ({
      type: "Feature",
      properties: { name },
      geometry: { type: "Point", coordinates: at },
    })),
  },
});

// Slots arrive in the SDK 0.17: map.slot("middle") replaces "road-labels".
map.map.addLayer({
  id: "links",
  type: "line",
  source: "links",
  layout: { "line-cap": "round" },
  paint: { "line-color": "#1a6bff", "line-width": 2 },
}, "road-labels");
map.map.addLayer({
  id: "offices",
  type: "circle",
  source: "offices",
  paint: {
    "circle-radius": 5,
    "circle-color": "#1a6bff",
    "circle-stroke-color": "#ffffff",
    "circle-stroke-width": 1.5,
  },
}, "road-labels");

Worth knowing

  • Use lines and points on the globe, not large fills. A large GeoJSON fill at low zoom on the globe is drawn offset from where it belongs once the camera is rotated (still the case in MapLibre 6.9), while lines, circles and the basemap's own tiles stay correct.
  • If you must show a big fill on the globe, keep the view north-up: reset the bearing and call map.map.touchZoomRotate.disableRotation() and map.map.keyboard.disableRotation(). Avoid map.map.dragRotate.disable() there, because it switches off tilt as well as rotation.
  • Up close the globe renders like the flat map, so data drawn for a city looks the same in either projection.