Contour Lines on Geographic Map — D3.js

A contour map overlays isolines (lines of equal value) onto a geographic basemap, visualizing continuous spatial data such as elevation, temperature, or atmospheric pressure across real-world coordinates. Unlike basic contour plots that use abstract x/y coordinates, this plot anchors contours to latitude/longitude positions with geographic context like coastlines, borders, or terrain. It combines the precision of isoline visualization with spatial awareness, making it ideal for meteorological, topographic, and environmental applications.

Contour Lines on Geographic Map rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// contour-map-geographic: Contour Lines on Geographic Map
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 90/100 | Created: 2026-09-01

//# anyplot-orientation: square
const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
// Hawai'i Island's bbox is nearly square (lon range 1.35 deg x cos(19.6 deg)
// = 1.27 deg vs. lat range 1.3 deg), so a square canvas lets the map fill the
// frame instead of being stranded inside a 16:9 landscape with dead margins.
const margin = { top: 90, right: 150, bottom: 90, left: 90 };

// --- Data: synthetic elevation model of Hawai'i Island (Big Island) --------
// Two shield-volcano summits near the real Mauna Kea / Mauna Loa positions,
// plus deterministic ridge texture. A sea-level floor at base=90 carves a
// closed 0 m coastline out of the smooth terrain — no external DEM needed.
// The bounding box is padded well beyond both summits so every isoline closes
// inside the frame instead of being clipped by the domain edge.
const LON_MIN = -156.3;
const LON_MAX = -154.95;
const LAT_MIN = 19.0;
const LAT_MAX = 20.3;
const NX = 90;
const NY = 90;

const peaks = [
  { lon: -155.4681, lat: 19.8207, elevation: 4207, spread: 0.11 }, // Mauna Kea
  { lon: -155.602, lat: 19.4721, elevation: 4169, spread: 0.13 }, // Mauna Loa
];

const grid = new Float64Array(NX * NY);
for (let j = 0; j < NY; j++) {
  const lat = LAT_MIN + (j / (NY - 1)) * (LAT_MAX - LAT_MIN);
  for (let i = 0; i < NX; i++) {
    const lon = LON_MIN + (i / (NX - 1)) * (LON_MAX - LON_MIN);
    let base = 0;
    for (const p of peaks) {
      const d2 = (lon - p.lon) ** 2 + (lat - p.lat) ** 2;
      base += p.elevation * Math.exp(-d2 / (2 * p.spread * p.spread));
    }
    // Ridge texture fades to zero away from the summits, so it can't push
    // isolated low-lying ocean cells above the coastline threshold.
    const ridgeMask = Math.min(1, base / 600);
    const ridgeTexture = 45 * Math.sin(lon * 90) * Math.cos(lat * 70) * ridgeMask;
    grid[j * NX + i] = Math.max(0, base + ridgeTexture - 90); // sea-level cutoff
  }
}
const maxElevation = d3.max(grid);

// --- Contours (grid-index space -> lon/lat) ---------------------------------
const STEP = 500;
const maxBand = Math.ceil(maxElevation / STEP) * STEP;
const thresholds = [1];
for (let v = STEP; v <= maxBand; v += STEP) thresholds.push(v);

const contoursGeo = d3
  .contours()
  .size([NX, NY])
  .thresholds(thresholds)(grid)
  .map((c) => ({
    type: "MultiPolygon",
    value: c.value,
    coordinates: c.coordinates.map((poly) =>
      poly.map((ring) =>
        ring.map(([x, y]) => [
          LON_MIN + (x / (NX - 1)) * (LON_MAX - LON_MIN),
          LAT_MIN + (y / (NY - 1)) * (LAT_MAX - LAT_MIN),
        ]),
      ),
    ),
  }));

// --- Projection fitted to the region, inside the margin box -----------------
// Ring wound so d3-geo's spherical right-hand rule reads this as the small
// interior bbox (not its complement covering the rest of the globe).
const bboxFeature = {
  type: "Polygon",
  coordinates: [
    [
      [LON_MIN, LAT_MIN],
      [LON_MIN, LAT_MAX],
      [LON_MAX, LAT_MAX],
      [LON_MAX, LAT_MIN],
      [LON_MIN, LAT_MIN],
    ],
  ],
};
const projection = d3
  .geoMercator()
  .fitExtent(
    [
      [margin.left, margin.top],
      [width - margin.right, height - margin.bottom],
    ],
    bboxFeature,
  );
const geoPath = d3.geoPath(projection);

const corners = bboxFeature.coordinates[0].map(projection);
const mapX0 = d3.min(corners, (d) => d[0]);
const mapX1 = d3.max(corners, (d) => d[0]);
const mapY0 = d3.min(corners, (d) => d[1]);
const mapY1 = d3.max(corners, (d) => d[1]);

// --- Color: single-polarity elevation -> imprint_seq -------------------------
const colorScale = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, maxBand]);

// --- SVG mount ----------------------------------------------------------------
const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);

// Lon/lat graticule (drawn first, visible over the ocean background)
const graticule = d3.geoGraticule().extent([
  [LON_MIN, LAT_MIN],
  [LON_MAX, LAT_MAX],
]).step([0.2, 0.2]);
svg
  .append("path")
  .datum(graticule())
  .attr("d", geoPath)
  .attr("fill", "none")
  .attr("stroke", t.grid)
  .attr("stroke-width", 1);

// Filled elevation bands, painted ascending so each nested level overwrites
// the wider band beneath it — turns the "value >= threshold" isoband stack
// from d3-contour into a proper stepped hypsometric fill.
const bandGroup = svg.append("g");
for (const c of contoursGeo) {
  bandGroup
    .append("path")
    .datum(c)
    .attr("d", geoPath)
    .attr("fill", colorScale(c.value))
    .attr("stroke", t.pageBg)
    .attr("stroke-width", 0.5);
}

// Isoline strokes on top of the fills (line-only contour detail)
for (const c of contoursGeo.slice(1)) {
  svg
    .append("path")
    .datum(c)
    .attr("d", geoPath)
    .attr("fill", "none")
    .attr("stroke", t.inkSoft)
    .attr("stroke-width", 1)
    .attr("stroke-opacity", 0.5);
}

// Coastline (0 m isoline) emphasized as the map's geographic anchor
svg
  .append("path")
  .datum(contoursGeo[0])
  .attr("d", geoPath)
  .attr("fill", "none")
  .attr("stroke", t.ink)
  .attr("stroke-width", 2);

// Contour value labels at meaningful elevation intervals. Some bands merge
// into one ring around both summits, others split into two disjoint rings —
// so each label reads off the ring's extreme point in a distinct compass
// direction (E/W/S/N) rather than an arc fraction, which keeps callouts
// spread around the island's open flanks instead of stacking near a peak.
const LABEL_VALUES = [1000, 2000, 3000, 4000];
const LABEL_DIRECTIONS = [
  [0, 1], // 1000 m -> easternmost point (max lon)
  [0, -1], // 2000 m -> westernmost point (min lon)
  [1, -1], // 3000 m -> southernmost point (min lat)
  [1, 1], // 4000 m -> northernmost point (max lat)
];
LABEL_VALUES.forEach((val, i) => {
  const c = contoursGeo.find((d) => d.value === val);
  if (!c) return;
  const [axis, dir] = LABEL_DIRECTIONS[i];
  let best = null;
  for (const poly of c.coordinates) {
    for (const ring of poly) {
      for (const pt of ring) {
        if (!best || pt[axis] * dir > best[axis] * dir) best = pt;
      }
    }
  }
  const [lon, lat] = best;
  const [px, py] = projection([lon, lat]);
  svg
    .append("text")
    .attr("x", px)
    .attr("y", py)
    .attr("text-anchor", "middle")
    .attr("dy", "0.35em")
    .style("font-size", "15px")
    .style("font-weight", "600")
    .style("paint-order", "stroke")
    .style("stroke", t.pageBg)
    .style("stroke-width", "4px")
    .attr("fill", t.ink)
    .text(`${val} m`);
});

// Map frame
svg
  .append("rect")
  .attr("x", mapX0)
  .attr("y", mapY0)
  .attr("width", mapX1 - mapX0)
  .attr("height", mapY1 - mapY0)
  .attr("fill", "none")
  .attr("stroke", t.inkSoft)
  .attr("stroke-width", 1.5);

// Longitude ticks (bottom) and latitude ticks (left)
const lonTicks = d3.range(Math.ceil(LON_MIN / 0.2) * 0.2, LON_MAX, 0.2);
const latTicks = d3.range(Math.ceil(LAT_MIN / 0.2) * 0.2, LAT_MAX, 0.2);

for (const lon of lonTicks) {
  const [px] = projection([lon, LAT_MIN]);
  svg
    .append("line")
    .attr("x1", px)
    .attr("x2", px)
    .attr("y1", mapY1)
    .attr("y2", mapY1 + 8)
    .attr("stroke", t.inkSoft)
    .attr("stroke-width", 1);
  svg
    .append("text")
    .attr("x", px)
    .attr("y", mapY1 + 28)
    .attr("text-anchor", "middle")
    .style("font-size", "14px")
    .attr("fill", t.inkSoft)
    .text(`${Math.abs(lon).toFixed(1)}°W`);
}

for (const lat of latTicks) {
  const [, py] = projection([LON_MIN, lat]);
  svg
    .append("line")
    .attr("x1", mapX0 - 8)
    .attr("x2", mapX0)
    .attr("y1", py)
    .attr("y2", py)
    .attr("stroke", t.inkSoft)
    .attr("stroke-width", 1);
  svg
    .append("text")
    .attr("x", mapX0 - 14)
    .attr("y", py)
    .attr("text-anchor", "end")
    .attr("dy", "0.32em")
    .style("font-size", "14px")
    .attr("fill", t.inkSoft)
    .text(`${lat.toFixed(1)}°N`);
}

// Colorbar legend — hugs the map's actual right edge instead of the margin
// box, so there's no dead gap on near-square geographic bboxes.
const barWidth = 26;
const barX = mapX1 + 34;
const barY0 = mapY0;
const barY1 = mapY1;

const gradientId = "elevationGradient";
const defs = svg.append("defs");
const gradient = defs
  .append("linearGradient")
  .attr("id", gradientId)
  .attr("x1", "0%")
  .attr("y1", "100%")
  .attr("x2", "0%")
  .attr("y2", "0%");
const stopCount = 6;
for (let i = 0; i <= stopCount; i++) {
  const f = i / stopCount;
  gradient
    .append("stop")
    .attr("offset", `${f * 100}%`)
    .attr("stop-color", colorScale(f * maxBand));
}

svg
  .append("text")
  .attr("x", barX + barWidth / 2)
  .attr("y", barY0 - 16)
  .attr("text-anchor", "middle")
  .style("font-size", "14px")
  .attr("fill", t.ink)
  .text("Elevation (m)");

svg
  .append("rect")
  .attr("x", barX)
  .attr("y", barY0)
  .attr("width", barWidth)
  .attr("height", barY1 - barY0)
  .attr("fill", `url(#${gradientId})`)
  .attr("stroke", t.inkSoft)
  .attr("stroke-width", 1);

// Includes maxBand itself so the top of the gradient always carries a label,
// even when it falls between the round 1000 m steps (e.g. 4500).
const colorbarTicks = d3.range(0, maxBand, 1000).concat(maxBand);
for (const val of colorbarTicks) {
  const y = barY1 - (val / maxBand) * (barY1 - barY0);
  svg
    .append("line")
    .attr("x1", barX + barWidth)
    .attr("x2", barX + barWidth + 6)
    .attr("y1", y)
    .attr("y2", y)
    .attr("stroke", t.inkSoft)
    .attr("stroke-width", 1);
  svg
    .append("text")
    .attr("x", barX + barWidth + 12)
    .attr("y", y)
    .attr("dy", "0.32em")
    .style("font-size", "13px")
    .attr("fill", t.inkSoft)
    .text(val);
}

// Title — scaled down from the 67-char baseline for this longer title
const titleText =
  "Hawai'i Island Elevation · contour-map-geographic · javascript · d3 · anyplot.ai";
const titleRatio = titleText.length > 67 ? 67 / titleText.length : 1;
const titleFontSize = Math.max(14, Math.round(22 * titleRatio));
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 44)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", `${titleFontSize}px`)
  .style("font-weight", "600")
  .text(titleText);

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/contour-map-geographic/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "contour-map-geographic",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/contour-map-geographic/javascript/d3",
  "hub": "https://anyplot.ai/contour-map-geographic",
  "code_json": "https://api.anyplot.ai/specs/contour-map-geographic/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/contour-map-geographic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/contour-map-geographic/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/contour-map-geographic/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/contour-map-geographic/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/contour-map-geographic/javascript/d3/plot-dark.html",
  "quality_score": 90.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Contour Lines on Geographic Map on anyplot.ai.

Other implementations