{"spec_id":"contour-map-geographic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// contour-map-geographic: Contour Lines on Geographic Map\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-01\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: synthetic alpine elevation field over a lon/lat survey grid -----\n// --- (deterministic sum of peak Gaussians — no RNG needed) -----------------\nconst LON_MIN = 6.3;\nconst LON_MAX = 8.7;\nconst LAT_MIN = 45.15;\nconst LAT_MAX = 46.5;\nconst NX = 36;\nconst NY = 20;\n\nconst PEAKS = [\n  { lon: 7.05, lat: 45.55, height: 3100, sigma: 0.32 },\n  { lon: 7.95, lat: 46.05, height: 2550, sigma: 0.28 },\n  { lon: 8.35, lat: 45.35, height: 2050, sigma: 0.26 },\n  { lon: 6.7, lat: 46.05, height: 1750, sigma: 0.24 },\n];\n\nfunction elevationAt(lon, lat) {\n  let z = 420; // valley floor, meters\n  for (const peak of PEAKS) {\n    const dLon = lon - peak.lon;\n    const dLat = lat - peak.lat;\n    const d2 = dLon * dLon + dLat * dLat;\n    z += peak.height * Math.exp(-d2 / (2 * peak.sigma * peak.sigma));\n  }\n  return z;\n}\n\nconst lons = Array.from(\n  { length: NX },\n  (_, i) => LON_MIN + (i * (LON_MAX - LON_MIN)) / (NX - 1),\n);\nconst lats = Array.from(\n  { length: NY },\n  (_, j) => LAT_MIN + (j * (LAT_MAX - LAT_MIN)) / (NY - 1),\n);\nconst grid = lats.map((lat) => lons.map((lon) => elevationAt(lon, lat)));\n\nconst flatValues = grid.flat();\nconst gridMin = Math.min(...flatValues);\nconst gridMax = Math.max(...flatValues);\nconst halfDLon = (lons[1] - lons[0]) / 2;\nconst halfDLat = (lats[1] - lats[0]) / 2;\n\n// --- Locate the single highest grid point for a focal-point callout --------\nlet peakRow = 0;\nlet peakCol = 0;\ngrid.forEach((row, j) => {\n  row.forEach((value, i) => {\n    if (value > grid[peakRow][peakCol]) {\n      peakRow = j;\n      peakCol = i;\n    }\n  });\n});\nconst peakLon = lons[peakCol];\nconst peakLat = lats[peakRow];\n\n// --- Contour levels: round-number intervals spanning the field -------------\nconst LEVEL_STEP = 300; // meters\nconst firstLevel = Math.ceil((gridMin + LEVEL_STEP) / LEVEL_STEP) * LEVEL_STEP;\nconst levels = [];\nfor (let lvl = firstLevel; lvl < gridMax; lvl += LEVEL_STEP) levels.push(lvl);\n\n// --- Imprint sequential color scale (single-polarity: low -> high) ---------\n// Parses both \"#RRGGBB\" and \"rgb(r, g, b)\" so mixHex() results can be re-mixed\n// (e.g. blending an already-mixed isoline color further toward t.ink).\nfunction parseColor(color) {\n  if (color.startsWith(\"#\")) {\n    return {\n      r: parseInt(color.slice(1, 3), 16),\n      g: parseInt(color.slice(3, 5), 16),\n      b: parseInt(color.slice(5, 7), 16),\n    };\n  }\n  const [r, g, b] = color.match(/[\\d.]+/g).map(Number);\n  return { r, g, b };\n}\n\nfunction mixHex(colorLow, colorHigh, frac) {\n  const f = Math.max(0, Math.min(1, frac));\n  const a = parseColor(colorLow);\n  const b = parseColor(colorHigh);\n  const r = Math.round(a.r + (b.r - a.r) * f);\n  const g = Math.round(a.g + (b.g - a.g) * f);\n  const bl = Math.round(a.b + (b.b - a.b) * f);\n  return `rgb(${r}, ${g}, ${bl})`;\n}\n\n// --- Marching squares: trace one level's isolines as lon/lat segments ------\nfunction marchingSquares(level) {\n  const segments = [];\n  const interp = (v0, v1, p0, p1) => {\n    const frac = (level - v0) / (v1 - v0);\n    return [p0[0] + frac * (p1[0] - p0[0]), p0[1] + frac * (p1[1] - p0[1])];\n  };\n\n  for (let j = 0; j < NY - 1; j++) {\n    for (let i = 0; i < NX - 1; i++) {\n      const x0 = lons[i];\n      const x1 = lons[i + 1];\n      const y0 = lats[j];\n      const y1 = lats[j + 1];\n      const bl = grid[j][i];\n      const br = grid[j][i + 1];\n      const tr = grid[j + 1][i + 1];\n      const tl = grid[j + 1][i];\n      const bit =\n        (tl >= level ? 8 : 0) |\n        (tr >= level ? 4 : 0) |\n        (br >= level ? 2 : 0) |\n        (bl >= level ? 1 : 0);\n      if (bit === 0 || bit === 15) continue;\n\n      const top = () => interp(tl, tr, [x0, y1], [x1, y1]);\n      const right = () => interp(tr, br, [x1, y1], [x1, y0]);\n      const bottom = () => interp(bl, br, [x0, y0], [x1, y0]);\n      const left = () => interp(bl, tl, [x0, y0], [x0, y1]);\n\n      // Standard marching-squares case table (TL=8, TR=4, BR=2, BL=1).\n      // Cases 5 and 10 are the ambiguous saddle configurations; this table\n      // resolves them with a fixed diagonal, which is a common convention.\n      const CASES = {\n        1: [[left(), bottom()]],\n        2: [[bottom(), right()]],\n        3: [[left(), right()]],\n        4: [[right(), top()]],\n        5: [\n          [left(), top()],\n          [bottom(), right()],\n        ],\n        6: [[bottom(), top()]],\n        7: [[left(), top()]],\n        8: [[top(), left()]],\n        9: [[top(), bottom()]],\n        10: [\n          [top(), right()],\n          [left(), bottom()],\n        ],\n        11: [[top(), right()]],\n        12: [[right(), left()]],\n        13: [[right(), bottom()]],\n        14: [[bottom(), left()]],\n      };\n      segments.push(...CASES[bit]);\n    }\n  }\n  return segments;\n}\n\n// --- Stitch disjoint segments into continuous polylines ---------------------\nfunction stitchPolylines(segments) {\n  const key = (p) => `${p[0].toFixed(5)},${p[1].toFixed(5)}`;\n  const adjacency = new Map();\n  segments.forEach((segment, segIdx) => {\n    [0, 1].forEach((endIdx) => {\n      const k = key(segment[endIdx]);\n      if (!adjacency.has(k)) adjacency.set(k, []);\n      adjacency.get(k).push({ segIdx, endIdx });\n    });\n  });\n\n  const used = new Array(segments.length).fill(false);\n  const polylines = [];\n  for (let i = 0; i < segments.length; i++) {\n    if (used[i]) continue;\n    used[i] = true;\n    const line = [segments[i][0], segments[i][1]];\n\n    let extended = true;\n    while (extended) {\n      extended = false;\n      const candidates = adjacency.get(key(line[line.length - 1])) || [];\n      for (const cand of candidates) {\n        if (used[cand.segIdx]) continue;\n        const seg = segments[cand.segIdx];\n        line.push(seg[cand.endIdx === 0 ? 1 : 0]);\n        used[cand.segIdx] = true;\n        extended = true;\n        break;\n      }\n    }\n    extended = true;\n    while (extended) {\n      extended = false;\n      const candidates = adjacency.get(key(line[0])) || [];\n      for (const cand of candidates) {\n        if (used[cand.segIdx]) continue;\n        const seg = segments[cand.segIdx];\n        line.unshift(seg[cand.endIdx === 0 ? 1 : 0]);\n        used[cand.segIdx] = true;\n        extended = true;\n        break;\n      }\n    }\n    polylines.push(line);\n  }\n  return polylines;\n}\n\n// --- Build one real Highcharts series per traced polyline -------------------\n// Each isoline is genuine point data (hover works), not a decorative overlay.\n// Lines are blended toward t.ink (more so on dark theme, where both ends of\n// the seq gradient sit close in lightness to the near-black page) so isolines\n// stay legible against the raster without changing the raster's own colors.\nconst ISOLINE_INK_BLEND = t.theme === \"dark\" ? 0.34 : 0.12;\nconst topLevel = levels[levels.length - 1];\nconst contourSeries = [];\nlevels.forEach((level, levelIdx) => {\n  const polylines = stitchPolylines(marchingSquares(level)).filter(\n    (line) => line.length >= 4,\n  );\n  if (polylines.length === 0) return;\n\n  const isFocalLevel = level === topLevel;\n  const baseColor = mixHex(t.seq[0], t.seq[1], (level - gridMin) / (gridMax - gridMin));\n  const lineColor = mixHex(baseColor, t.ink, ISOLINE_INK_BLEND);\n  const longest = polylines.reduce((a, b) => (b.length > a.length ? b : a));\n\n  polylines.forEach((line) => {\n    const midIdx = Math.floor(line.length / 2);\n    const showLabel = line === longest && (levelIdx % 2 === 0 || isFocalLevel);\n    contourSeries.push({\n      name: `${level} m`,\n      type: \"spline\",\n      color: lineColor,\n      lineWidth: isFocalLevel ? 3.2 : 2.2,\n      marker: { enabled: false },\n      showInLegend: false,\n      zIndex: isFocalLevel ? 5 : 4,\n      tooltip: { headerFormat: \"\", pointFormat: `Elevation isoline: <b>${level} m</b>` },\n      data: line.map((p, idx) => {\n        const point = { x: p[0], y: p[1] };\n        if (showLabel && idx === midIdx) {\n          point.dataLabels = {\n            enabled: true,\n            format: `${level} m`,\n            style: { color: t.ink, fontSize: \"12px\", fontWeight: \"600\", textOutline: \"none\" },\n            backgroundColor: t.elevatedBg,\n            borderColor: t.inkSoft,\n            borderWidth: 1,\n            borderRadius: 3,\n            padding: 3,\n          };\n        }\n        return point;\n      }),\n    });\n  });\n});\n\n// --- Survey area boundary (geographic context, inset within the grid) ------\nconst boundaryPoints = [\n  [6.55, 45.3],\n  [6.85, 45.2],\n  [7.35, 45.22],\n  [7.85, 45.28],\n  [8.3, 45.35],\n  [8.5, 45.65],\n  [8.45, 46.0],\n  [8.15, 46.3],\n  [7.7, 46.4],\n  [7.15, 46.38],\n  [6.7, 46.15],\n  [6.5, 45.75],\n  [6.55, 45.3],\n];\nconst boundarySeries = {\n  name: \"Survey area boundary\",\n  type: \"line\",\n  color: t.inkSoft,\n  dashStyle: \"Dash\",\n  lineWidth: 2,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: true,\n  zIndex: 2,\n  data: boundaryPoints.map((p) => ({ x: p[0], y: p[1] })),\n};\n\n// --- Ridge-crest reference line (second geographic context feature, -------\n// --- a stylized topographic divide tracing the massif chain) ---------------\nconst ridgelinePoints = [\n  [6.35, 46.42],\n  [6.72, 46.08],\n  [7.05, 45.58],\n  [7.4, 45.62],\n  [7.95, 46.02],\n  [8.35, 45.42],\n  [8.62, 45.28],\n];\nconst ridgelineSeries = {\n  name: \"Ridge crest (reference)\",\n  type: \"line\",\n  color: t.inkSoft,\n  dashStyle: \"Dot\",\n  lineWidth: 1.6,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n  showInLegend: true,\n  zIndex: 3,\n  data: ridgelinePoints.map((p) => ({ x: p[0], y: p[1] })),\n};\n\n// --- Highest-peak focal callout (sharpens the terrain read beyond an -------\n// --- evenly-weighted four-peak overview) ------------------------------------\nconst peakSeries = {\n  name: \"Highest peak\",\n  type: \"scatter\",\n  color: t.ink,\n  marker: {\n    enabled: true,\n    symbol: \"triangle\",\n    radius: 7,\n    fillColor: t.amber,\n    lineColor: t.ink,\n    lineWidth: 1.5,\n  },\n  enableMouseTracking: false,\n  showInLegend: true,\n  zIndex: 6,\n  dataLabels: {\n    enabled: true,\n    format: `Highest peak · ${Math.round(gridMax)} m`,\n    y: -16,\n    style: { color: t.ink, fontSize: \"13px\", fontWeight: \"700\", textOutline: \"none\" },\n    backgroundColor: t.elevatedBg,\n    borderColor: t.inkSoft,\n    borderWidth: 1,\n    borderRadius: 3,\n    padding: 4,\n  },\n  data: [{ x: peakLon, y: peakLat }],\n};\n\n// --- Filled elevation raster (core Highcharts has no heatmap/colorAxis -----\n// --- module — each grid cell is drawn as a real, data-colored rect) --------\nfunction drawElevationRaster(chart) {\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n  grid.forEach((row, j) => {\n    row.forEach((value, i) => {\n      const frac = (value - gridMin) / (gridMax - gridMin);\n      const x0 = xAxis.toPixels(lons[i] - halfDLon, false);\n      const x1 = xAxis.toPixels(lons[i] + halfDLon, false);\n      const y0 = yAxis.toPixels(lats[j] - halfDLat, false);\n      const y1 = yAxis.toPixels(lats[j] + halfDLat, false);\n      chart.renderer\n        .rect(Math.min(x0, x1), Math.min(y0, y1), Math.abs(x1 - x0), Math.abs(y0 - y1))\n        .attr({ fill: mixHex(t.seq[0], t.seq[1], frac), opacity: 0.85, zIndex: 0 })\n        .add();\n    });\n  });\n}\n\n// --- Elevation color-scale legend (core Highcharts has no colorbar --------\n// --- widget — drawn manually, same swatch idiom as a standard legend) ------\nfunction drawElevationLegend(chart) {\n  const barW = 22;\n  const barH = 260;\n  const barX = chart.plotLeft + chart.plotWidth + 46;\n  const barY = chart.plotTop + 34;\n\n  chart.renderer\n    .rect(barX - 16, barY - 32, 150, barH + 66, 6)\n    .attr({ fill: t.elevatedBg, stroke: t.inkSoft, \"stroke-width\": 1, zIndex: 6, opacity: 0.94 })\n    .add();\n\n  chart.renderer\n    .text(\"Elevation\", barX + 38, barY - 10)\n    .attr({ align: \"center\", zIndex: 7 })\n    .css({ color: t.ink, fontSize: \"14px\", fontWeight: \"600\" })\n    .add();\n\n  chart.renderer\n    .rect(barX, barY, barW, barH)\n    .attr({\n      fill: {\n        linearGradient: { x1: 0, y1: 1, x2: 0, y2: 0 },\n        stops: [\n          [0, t.seq[0]],\n          [1, t.seq[1]],\n        ],\n      },\n      stroke: t.inkSoft,\n      \"stroke-width\": 1,\n      zIndex: 7,\n    })\n    .add();\n\n  [gridMax, (gridMax + gridMin) / 2, gridMin].forEach((value, idx) => {\n    const y = barY + (barH * idx) / 2;\n    chart.renderer\n      .text(`${Math.round(value)} m`, barX + barW + 10, y + 4)\n      .attr({ zIndex: 7 })\n      .css({ color: t.inkSoft, fontSize: \"12px\" })\n      .add();\n  });\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    marginRight: 190,\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load: function () {\n        drawElevationRaster(this);\n        drawElevationLegend(this);\n      },\n    },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"contour-map-geographic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Synthetic alpine survey grid — elevation isolines every 300 m across four peak massifs\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    min: LON_MIN - halfDLon,\n    max: LON_MAX + halfDLon,\n    startOnTick: false,\n    endOnTick: false,\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    title: { text: \"Longitude (°E)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" }, format: \"{value:.1f}°\" },\n  },\n  yAxis: {\n    min: LAT_MIN - halfDLat,\n    max: LAT_MAX + halfDLat,\n    startOnTick: false,\n    endOnTick: false,\n    gridLineWidth: 0,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    title: { text: \"Latitude (°N)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" }, format: \"{value:.1f}°\" },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false, states: { hover: { enabled: false } } },\n  },\n  series: [boundarySeries, ridgelineSeries, peakSeries, ...contourSeries],\n});\n"}