{"spec_id":"contour-map-geographic","library":"d3","language":"javascript","code":"// anyplot.ai\n// contour-map-geographic: Contour Lines on Geographic Map\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-01\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n// Hawai'i Island's bbox is nearly square (lon range 1.35 deg x cos(19.6 deg)\n// = 1.27 deg vs. lat range 1.3 deg), so a square canvas lets the map fill the\n// frame instead of being stranded inside a 16:9 landscape with dead margins.\nconst margin = { top: 90, right: 150, bottom: 90, left: 90 };\n\n// --- Data: synthetic elevation model of Hawai'i Island (Big Island) --------\n// Two shield-volcano summits near the real Mauna Kea / Mauna Loa positions,\n// plus deterministic ridge texture. A sea-level floor at base=90 carves a\n// closed 0 m coastline out of the smooth terrain — no external DEM needed.\n// The bounding box is padded well beyond both summits so every isoline closes\n// inside the frame instead of being clipped by the domain edge.\nconst LON_MIN = -156.3;\nconst LON_MAX = -154.95;\nconst LAT_MIN = 19.0;\nconst LAT_MAX = 20.3;\nconst NX = 90;\nconst NY = 90;\n\nconst peaks = [\n  { lon: -155.4681, lat: 19.8207, elevation: 4207, spread: 0.11 }, // Mauna Kea\n  { lon: -155.602, lat: 19.4721, elevation: 4169, spread: 0.13 }, // Mauna Loa\n];\n\nconst grid = new Float64Array(NX * NY);\nfor (let j = 0; j < NY; j++) {\n  const lat = LAT_MIN + (j / (NY - 1)) * (LAT_MAX - LAT_MIN);\n  for (let i = 0; i < NX; i++) {\n    const lon = LON_MIN + (i / (NX - 1)) * (LON_MAX - LON_MIN);\n    let base = 0;\n    for (const p of peaks) {\n      const d2 = (lon - p.lon) ** 2 + (lat - p.lat) ** 2;\n      base += p.elevation * Math.exp(-d2 / (2 * p.spread * p.spread));\n    }\n    // Ridge texture fades to zero away from the summits, so it can't push\n    // isolated low-lying ocean cells above the coastline threshold.\n    const ridgeMask = Math.min(1, base / 600);\n    const ridgeTexture = 45 * Math.sin(lon * 90) * Math.cos(lat * 70) * ridgeMask;\n    grid[j * NX + i] = Math.max(0, base + ridgeTexture - 90); // sea-level cutoff\n  }\n}\nconst maxElevation = d3.max(grid);\n\n// --- Contours (grid-index space -> lon/lat) ---------------------------------\nconst STEP = 500;\nconst maxBand = Math.ceil(maxElevation / STEP) * STEP;\nconst thresholds = [1];\nfor (let v = STEP; v <= maxBand; v += STEP) thresholds.push(v);\n\nconst contoursGeo = d3\n  .contours()\n  .size([NX, NY])\n  .thresholds(thresholds)(grid)\n  .map((c) => ({\n    type: \"MultiPolygon\",\n    value: c.value,\n    coordinates: c.coordinates.map((poly) =>\n      poly.map((ring) =>\n        ring.map(([x, y]) => [\n          LON_MIN + (x / (NX - 1)) * (LON_MAX - LON_MIN),\n          LAT_MIN + (y / (NY - 1)) * (LAT_MAX - LAT_MIN),\n        ]),\n      ),\n    ),\n  }));\n\n// --- Projection fitted to the region, inside the margin box -----------------\n// Ring wound so d3-geo's spherical right-hand rule reads this as the small\n// interior bbox (not its complement covering the rest of the globe).\nconst bboxFeature = {\n  type: \"Polygon\",\n  coordinates: [\n    [\n      [LON_MIN, LAT_MIN],\n      [LON_MIN, LAT_MAX],\n      [LON_MAX, LAT_MAX],\n      [LON_MAX, LAT_MIN],\n      [LON_MIN, LAT_MIN],\n    ],\n  ],\n};\nconst projection = d3\n  .geoMercator()\n  .fitExtent(\n    [\n      [margin.left, margin.top],\n      [width - margin.right, height - margin.bottom],\n    ],\n    bboxFeature,\n  );\nconst geoPath = d3.geoPath(projection);\n\nconst corners = bboxFeature.coordinates[0].map(projection);\nconst mapX0 = d3.min(corners, (d) => d[0]);\nconst mapX1 = d3.max(corners, (d) => d[0]);\nconst mapY0 = d3.min(corners, (d) => d[1]);\nconst mapY1 = d3.max(corners, (d) => d[1]);\n\n// --- Color: single-polarity elevation -> imprint_seq -------------------------\nconst colorScale = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, maxBand]);\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// Lon/lat graticule (drawn first, visible over the ocean background)\nconst graticule = d3.geoGraticule().extent([\n  [LON_MIN, LAT_MIN],\n  [LON_MAX, LAT_MAX],\n]).step([0.2, 0.2]);\nsvg\n  .append(\"path\")\n  .datum(graticule())\n  .attr(\"d\", geoPath)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// Filled elevation bands, painted ascending so each nested level overwrites\n// the wider band beneath it — turns the \"value >= threshold\" isoband stack\n// from d3-contour into a proper stepped hypsometric fill.\nconst bandGroup = svg.append(\"g\");\nfor (const c of contoursGeo) {\n  bandGroup\n    .append(\"path\")\n    .datum(c)\n    .attr(\"d\", geoPath)\n    .attr(\"fill\", colorScale(c.value))\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 0.5);\n}\n\n// Isoline strokes on top of the fills (line-only contour detail)\nfor (const c of contoursGeo.slice(1)) {\n  svg\n    .append(\"path\")\n    .datum(c)\n    .attr(\"d\", geoPath)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1)\n    .attr(\"stroke-opacity\", 0.5);\n}\n\n// Coastline (0 m isoline) emphasized as the map's geographic anchor\nsvg\n  .append(\"path\")\n  .datum(contoursGeo[0])\n  .attr(\"d\", geoPath)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2);\n\n// Contour value labels at meaningful elevation intervals. Some bands merge\n// into one ring around both summits, others split into two disjoint rings —\n// so each label reads off the ring's extreme point in a distinct compass\n// direction (E/W/S/N) rather than an arc fraction, which keeps callouts\n// spread around the island's open flanks instead of stacking near a peak.\nconst LABEL_VALUES = [1000, 2000, 3000, 4000];\nconst LABEL_DIRECTIONS = [\n  [0, 1], // 1000 m -> easternmost point (max lon)\n  [0, -1], // 2000 m -> westernmost point (min lon)\n  [1, -1], // 3000 m -> southernmost point (min lat)\n  [1, 1], // 4000 m -> northernmost point (max lat)\n];\nLABEL_VALUES.forEach((val, i) => {\n  const c = contoursGeo.find((d) => d.value === val);\n  if (!c) return;\n  const [axis, dir] = LABEL_DIRECTIONS[i];\n  let best = null;\n  for (const poly of c.coordinates) {\n    for (const ring of poly) {\n      for (const pt of ring) {\n        if (!best || pt[axis] * dir > best[axis] * dir) best = pt;\n      }\n    }\n  }\n  const [lon, lat] = best;\n  const [px, py] = projection([lon, lat]);\n  svg\n    .append(\"text\")\n    .attr(\"x\", px)\n    .attr(\"y\", py)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"dy\", \"0.35em\")\n    .style(\"font-size\", \"15px\")\n    .style(\"font-weight\", \"600\")\n    .style(\"paint-order\", \"stroke\")\n    .style(\"stroke\", t.pageBg)\n    .style(\"stroke-width\", \"4px\")\n    .attr(\"fill\", t.ink)\n    .text(`${val} m`);\n});\n\n// Map frame\nsvg\n  .append(\"rect\")\n  .attr(\"x\", mapX0)\n  .attr(\"y\", mapY0)\n  .attr(\"width\", mapX1 - mapX0)\n  .attr(\"height\", mapY1 - mapY0)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5);\n\n// Longitude ticks (bottom) and latitude ticks (left)\nconst lonTicks = d3.range(Math.ceil(LON_MIN / 0.2) * 0.2, LON_MAX, 0.2);\nconst latTicks = d3.range(Math.ceil(LAT_MIN / 0.2) * 0.2, LAT_MAX, 0.2);\n\nfor (const lon of lonTicks) {\n  const [px] = projection([lon, LAT_MIN]);\n  svg\n    .append(\"line\")\n    .attr(\"x1\", px)\n    .attr(\"x2\", px)\n    .attr(\"y1\", mapY1)\n    .attr(\"y2\", mapY1 + 8)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1);\n  svg\n    .append(\"text\")\n    .attr(\"x\", px)\n    .attr(\"y\", mapY1 + 28)\n    .attr(\"text-anchor\", \"middle\")\n    .style(\"font-size\", \"14px\")\n    .attr(\"fill\", t.inkSoft)\n    .text(`${Math.abs(lon).toFixed(1)}°W`);\n}\n\nfor (const lat of latTicks) {\n  const [, py] = projection([LON_MIN, lat]);\n  svg\n    .append(\"line\")\n    .attr(\"x1\", mapX0 - 8)\n    .attr(\"x2\", mapX0)\n    .attr(\"y1\", py)\n    .attr(\"y2\", py)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1);\n  svg\n    .append(\"text\")\n    .attr(\"x\", mapX0 - 14)\n    .attr(\"y\", py)\n    .attr(\"text-anchor\", \"end\")\n    .attr(\"dy\", \"0.32em\")\n    .style(\"font-size\", \"14px\")\n    .attr(\"fill\", t.inkSoft)\n    .text(`${lat.toFixed(1)}°N`);\n}\n\n// Colorbar legend — hugs the map's actual right edge instead of the margin\n// box, so there's no dead gap on near-square geographic bboxes.\nconst barWidth = 26;\nconst barX = mapX1 + 34;\nconst barY0 = mapY0;\nconst barY1 = mapY1;\n\nconst gradientId = \"elevationGradient\";\nconst defs = svg.append(\"defs\");\nconst gradient = defs\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0%\")\n  .attr(\"y1\", \"100%\")\n  .attr(\"x2\", \"0%\")\n  .attr(\"y2\", \"0%\");\nconst stopCount = 6;\nfor (let i = 0; i <= stopCount; i++) {\n  const f = i / stopCount;\n  gradient\n    .append(\"stop\")\n    .attr(\"offset\", `${f * 100}%`)\n    .attr(\"stop-color\", colorScale(f * maxBand));\n}\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", barX + barWidth / 2)\n  .attr(\"y\", barY0 - 16)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"14px\")\n  .attr(\"fill\", t.ink)\n  .text(\"Elevation (m)\");\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", barX)\n  .attr(\"y\", barY0)\n  .attr(\"width\", barWidth)\n  .attr(\"height\", barY1 - barY0)\n  .attr(\"fill\", `url(#${gradientId})`)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\n// Includes maxBand itself so the top of the gradient always carries a label,\n// even when it falls between the round 1000 m steps (e.g. 4500).\nconst colorbarTicks = d3.range(0, maxBand, 1000).concat(maxBand);\nfor (const val of colorbarTicks) {\n  const y = barY1 - (val / maxBand) * (barY1 - barY0);\n  svg\n    .append(\"line\")\n    .attr(\"x1\", barX + barWidth)\n    .attr(\"x2\", barX + barWidth + 6)\n    .attr(\"y1\", y)\n    .attr(\"y2\", y)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1);\n  svg\n    .append(\"text\")\n    .attr(\"x\", barX + barWidth + 12)\n    .attr(\"y\", y)\n    .attr(\"dy\", \"0.32em\")\n    .style(\"font-size\", \"13px\")\n    .attr(\"fill\", t.inkSoft)\n    .text(val);\n}\n\n// Title — scaled down from the 67-char baseline for this longer title\nconst titleText =\n  \"Hawai'i Island Elevation · contour-map-geographic · javascript · d3 · anyplot.ai\";\nconst titleRatio = titleText.length > 67 ? 67 / titleText.length : 1;\nconst titleFontSize = Math.max(14, Math.round(22 * titleRatio));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(titleText);\n"}