{"spec_id":"contour-map-geographic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// contour-map-geographic: Contour Lines on Geographic Map\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-01\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Geographic grid (synthetic elevation, western foothills near Mount Rainier, WA) ---\nconst N = 60;\nconst LON_MIN = -121.85;\nconst LON_MAX = -121.65;\nconst LAT_MIN = 46.75;\nconst LAT_MAX = 46.95;\n\nconst lonArr = [];\nconst latArr = [];\nfor (let k = 0; k < N; k++) {\n  lonArr.push(LON_MIN + ((LON_MAX - LON_MIN) * k) / (N - 1));\n  latArr.push(LAT_MIN + ((LAT_MAX - LAT_MIN) * k) / (N - 1));\n}\n\n// Synthetic elevation surface: foothill base + three Gaussian peaks\nconst BASE_ELEV = 700; // meters\nconst PEAKS = [\n  { lon: -121.75, lat: 46.85, height: 2600, sigmaLon: 0.05, sigmaLat: 0.05 }, // main summit\n  { lon: -121.7, lat: 46.8, height: 1400, sigmaLon: 0.035, sigmaLat: 0.035 },\n  { lon: -121.8, lat: 46.9, height: 1100, sigmaLon: 0.04, sigmaLat: 0.03 },\n];\n\nlet Z_MIN = Infinity;\nlet Z_MAX = -Infinity;\nconst zGrid = [];\nfor (let i = 0; i < N; i++) {\n  zGrid.push([]);\n  for (let j = 0; j < N; j++) {\n    const lon = lonArr[i];\n    const lat = latArr[j];\n    let z = BASE_ELEV;\n    for (const p of PEAKS) {\n      z +=\n        p.height *\n        Math.exp(-(((lon - p.lon) ** 2) / (2 * p.sigmaLon ** 2) + ((lat - p.lat) ** 2) / (2 * p.sigmaLat ** 2)));\n    }\n    zGrid[i].push(z);\n    if (z < Z_MIN) Z_MIN = z;\n    if (z > Z_MAX) Z_MAX = z;\n  }\n}\n\n// --- Imprint sequential colormap (elevation is single-polarity magnitude) ---\nfunction hexToRgb(hex) {\n  return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];\n}\nfunction lerpColor(h1, h2, frac) {\n  const [r1, g1, b1] = hexToRgb(h1);\n  const [r2, g2, b2] = hexToRgb(h2);\n  return `rgb(${Math.round(r1 + (r2 - r1) * frac)},${Math.round(g1 + (g2 - g1) * frac)},${Math.round(b1 + (b2 - b1) * frac)})`;\n}\nfunction seqColor(z) {\n  const frac = Math.max(0, Math.min(1, (z - Z_MIN) / (Z_MAX - Z_MIN)));\n  return lerpColor(t.seq[0], t.seq[1], frac);\n}\n\n// --- Contour levels: 200 m spacing, index (bold+labeled) lines every 1000 m ---\nconst CONTOUR_INTERVAL = 200;\nconst INDEX_INTERVAL = 1000;\n\nconst LEVEL_LO = Math.floor(Z_MIN / CONTOUR_INTERVAL) * CONTOUR_INTERVAL;\nconst LEVEL_HI = Math.ceil(Z_MAX / CONTOUR_INTERVAL) * CONTOUR_INTERVAL;\nconst levels = [];\nfor (let z = LEVEL_LO; z <= LEVEL_HI; z += CONTOUR_INTERVAL) levels.push(z);\n\nconst bandLevels = levels.slice(0, -1); // [low, low + interval) pairs\nconst isoThresholds = levels.filter((z) => z > Z_MIN && z < Z_MAX);\n\n// --- Filled elevation bands ---\nconst datasets = [];\nfor (const zLow of bandLevels) {\n  const zHigh = zLow + CONTOUR_INTERVAL;\n  const color = seqColor((zLow + zHigh) / 2);\n  const points = [];\n  for (let i = 0; i < N; i++) {\n    for (let j = 0; j < N; j++) {\n      const z = zGrid[i][j];\n      if (z >= zLow && z < zHigh) points.push({ x: lonArr[i], y: latArr[j] });\n    }\n  }\n  if (points.length > 0) {\n    datasets.push({\n      label: `${zLow}–${zHigh} m`,\n      data: points,\n      backgroundColor: color,\n      borderWidth: 0,\n      pointRadius: 18,\n      pointHoverRadius: 18,\n      showLine: false,\n    });\n  }\n}\n\n// --- Wilderness boundary (schematic protected-area outline, basemap context) ---\n// Drawn by the overlay plugin (afterDraw) rather than as a dataset, so it\n// always renders above the dense elevation-band fill instead of racing it\n// for z-order.\nconst boundary = [\n  { x: -121.83, y: 46.77 },\n  { x: -121.68, y: 46.78 },\n  { x: -121.66, y: 46.92 },\n  { x: -121.82, y: 46.93 },\n  { x: -121.83, y: 46.77 },\n];\nconst summit = { x: -121.75, y: 46.85 };\n\n// --- Marching squares for contour isolines ---\n// For each 4-bit corner code (BL=bit0, BR=bit1, TR=bit2, TL=bit3, 1=above threshold),\n// which pairs of edge indices to connect as a line segment.\n// Edges: 0=bottom (BL-BR), 1=right (BR-TR), 2=top (TL-TR), 3=left (BL-TL)\nconst SEG = [\n  [], // 0: all below\n  [[0, 3]], // 1: BL\n  [[0, 1]], // 2: BR\n  [[3, 1]], // 3: BL,BR\n  [[1, 2]], // 4: TR\n  [\n    [0, 3],\n    [1, 2],\n  ], // 5: BL,TR (saddle)\n  [[0, 2]], // 6: BR,TR\n  [[3, 2]], // 7: BL,BR,TR\n  [[3, 2]], // 8: TL\n  [[0, 2]], // 9: BL,TL\n  [\n    [0, 1],\n    [2, 3],\n  ], // 10: BR,TL (saddle)\n  [[1, 2]], // 11: BL,BR,TL\n  [[3, 1]], // 12: TR,TL\n  [[0, 1]], // 13: BL,TR,TL\n  [[0, 3]], // 14: BR,TR,TL\n  [], // 15: all above\n];\n\n// --- Isoline + colorbar overlay plugin ---\nconst contourPlugin = {\n  id: 'contourOverlay',\n  afterDraw(chart) {\n    const ctx = chart.ctx;\n    const ca = chart.chartArea;\n    if (!ca) return;\n\n    const xs = chart.scales.x;\n    const ys = chart.scales.y;\n    const xPx = lonArr.map((v) => xs.getPixelForValue(v));\n    const yPx = latArr.map((v) => ys.getPixelForValue(v));\n\n    function edgePx(e, i, j, z00, z10, z11, z01, thresh) {\n      const f = (a, b, za, zb) => a + ((thresh - za) / (zb - za)) * (b - a);\n      switch (e) {\n        case 0:\n          return [f(xPx[i], xPx[i + 1], z00, z10), yPx[j]];\n        case 1:\n          return [xPx[i + 1], f(yPx[j], yPx[j + 1], z10, z11)];\n        case 2:\n          return [f(xPx[i], xPx[i + 1], z01, z11), yPx[j + 1]];\n        default:\n          return [xPx[i], f(yPx[j], yPx[j + 1], z00, z01)];\n      }\n    }\n\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(ca.left, ca.top, ca.right - ca.left, ca.bottom - ca.top);\n    ctx.clip();\n\n    for (const thresh of isoThresholds) {\n      const isIndex = thresh % INDEX_INTERVAL === 0;\n      const segments = [];\n      for (let i = 0; i < N - 1; i++) {\n        for (let j = 0; j < N - 1; j++) {\n          const z00 = zGrid[i][j];\n          const z10 = zGrid[i + 1][j];\n          const z11 = zGrid[i + 1][j + 1];\n          const z01 = zGrid[i][j + 1];\n          const code =\n            (z00 >= thresh ? 1 : 0) | (z10 >= thresh ? 2 : 0) | (z11 >= thresh ? 4 : 0) | (z01 >= thresh ? 8 : 0);\n          for (const [e0, e1] of SEG[code]) {\n            segments.push([edgePx(e0, i, j, z00, z10, z11, z01, thresh), edgePx(e1, i, j, z00, z10, z11, z01, thresh)]);\n          }\n        }\n      }\n      if (segments.length === 0) continue;\n\n      ctx.beginPath();\n      ctx.strokeStyle = t.ink;\n      ctx.globalAlpha = isIndex ? 0.55 : 0.38;\n      ctx.lineWidth = isIndex ? 1.8 : 1.1;\n      for (const [a, b] of segments) {\n        ctx.moveTo(a[0], a[1]);\n        ctx.lineTo(b[0], b[1]);\n      }\n      ctx.stroke();\n\n      // Index contours (multiples of INDEX_INTERVAL) carry an elevation label\n      if (isIndex) {\n        const [lx, ly] = segments[Math.floor(segments.length / 2)][0];\n        const label = `${thresh} m`;\n        ctx.font = 'bold 13px sans-serif';\n        const w = ctx.measureText(label).width;\n        ctx.globalAlpha = 1;\n        ctx.fillStyle = t.pageBg;\n        ctx.fillRect(lx - w / 2 - 4, ly - 9, w + 8, 18);\n        ctx.fillStyle = t.ink;\n        ctx.textAlign = 'center';\n        ctx.textBaseline = 'middle';\n        ctx.fillText(label, lx, ly);\n      }\n    }\n    // --- Wilderness boundary ---\n    ctx.beginPath();\n    boundary.forEach((p, idx) => {\n      const px = xs.getPixelForValue(p.x);\n      const py = ys.getPixelForValue(p.y);\n      if (idx === 0) ctx.moveTo(px, py);\n      else ctx.lineTo(px, py);\n    });\n    ctx.globalAlpha = 1;\n    ctx.setLineDash([8, 5]);\n    ctx.strokeStyle = t.ink;\n    ctx.lineWidth = 2;\n    ctx.stroke();\n    ctx.setLineDash([]);\n\n    // --- Summit marker (triangle) ---\n    const sx = xs.getPixelForValue(summit.x);\n    const sy = ys.getPixelForValue(summit.y);\n    const r = 11;\n    ctx.beginPath();\n    ctx.moveTo(sx, sy - r);\n    ctx.lineTo(sx + r, sy + r * 0.8);\n    ctx.lineTo(sx - r, sy + r * 0.8);\n    ctx.closePath();\n    ctx.fillStyle = t.ink;\n    ctx.fill();\n    ctx.strokeStyle = t.pageBg;\n    ctx.lineWidth = 2;\n    ctx.stroke();\n\n    ctx.restore();\n\n    // --- Colorbar (elevation, meters) ---\n    const barX = ca.right + 24;\n    const barW = 22;\n    const barH = ca.bottom - ca.top;\n\n    const grad = ctx.createLinearGradient(0, ca.bottom, 0, ca.top);\n    grad.addColorStop(0, t.seq[0]);\n    grad.addColorStop(1, t.seq[1]);\n    ctx.fillStyle = grad;\n    ctx.fillRect(barX, ca.top, barW, barH);\n\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(barX, ca.top, barW, barH);\n\n    ctx.fillStyle = t.ink;\n    ctx.font = 'bold 15px sans-serif';\n    ctx.textAlign = 'center';\n    ctx.fillText('Elevation (m)', barX + barW / 2, ca.top - 22);\n\n    // Ticks are inset from the bar's top/bottom edges so their labels never\n    // collide with the \"Elevation (m)\" title or the axis below. Values are\n    // snapped to the nearest 250 m so the colorbar reads round numbers\n    // instead of raw min/mid/max data values.\n    const TICK_INSET = 12;\n    const TICK_ROUND = 250;\n    const ticks = [\n      Math.floor(Z_MAX / TICK_ROUND) * TICK_ROUND,\n      Math.round((Z_MIN + Z_MAX) / 2 / TICK_ROUND) * TICK_ROUND,\n      Math.ceil(Z_MIN / TICK_ROUND) * TICK_ROUND,\n    ];\n    ctx.strokeStyle = t.inkSoft;\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = '15px sans-serif';\n    ctx.textAlign = 'left';\n    ctx.textBaseline = 'middle';\n    for (const zTick of ticks) {\n      const frac = (zTick - Z_MIN) / (Z_MAX - Z_MIN);\n      const ty = ca.bottom - TICK_INSET - frac * (barH - 2 * TICK_INSET);\n      ctx.beginPath();\n      ctx.moveTo(barX + barW, ty);\n      ctx.lineTo(barX + barW + 5, ty);\n      ctx.stroke();\n      ctx.fillText(Math.round(zTick).toString(), barX + barW + 8, ty);\n    }\n  },\n};\n\n// --- Title (scales fontsize down when the descriptive prefix pushes past the 67-char baseline) ---\nconst TITLE = 'Mount Rainier Foothills · contour-map-geographic · javascript · chartjs · anyplot.ai';\nconst TITLE_FONT_DEFAULT = 22;\nconst TITLE_FONT_FLOOR = 14;\nconst titleFontSize = Math.max(TITLE_FONT_FLOOR, Math.round(TITLE_FONT_DEFAULT * Math.min(1, 67 / TITLE.length)));\n\n// --- Mount ---\nconst canvas = document.createElement('canvas');\ndocument.getElementById('container').appendChild(canvas);\n\n// --- Chart ---\nnew Chart(canvas, {\n  type: 'scatter',\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: 110, top: 34, bottom: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: TITLE,\n        color: t.ink,\n        font: { size: titleFontSize, weight: '500' },\n        padding: { top: 12, bottom: 12 },\n      },\n      legend: {\n        onClick: () => {},\n        labels: {\n          color: t.ink,\n          font: { size: 14 },\n          usePointStyle: true,\n          generateLabels: () => [\n            { text: 'Wilderness boundary', pointStyle: 'line', strokeStyle: t.ink, lineWidth: 2, lineDash: [8, 5] },\n            { text: 'Summit', pointStyle: 'triangle', fillStyle: t.ink, strokeStyle: t.pageBg, lineWidth: 2 },\n          ],\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: 'linear',\n        min: LON_MIN,\n        max: LON_MAX,\n        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${Math.abs(v).toFixed(2)}°W` },\n        grid: { color: t.grid },\n        title: { display: true, text: 'Longitude', color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        type: 'linear',\n        min: LAT_MIN,\n        max: LAT_MAX,\n        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${v.toFixed(2)}°N` },\n        grid: { color: t.grid },\n        title: { display: true, text: 'Latitude', color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [contourPlugin],\n});\n"}