{"spec_id":"scatter-map-geographic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-map-geographic: Scatter Map with Geographic Points\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (fixed-seed LCG + Box-Muller) -----------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function lcg() {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\nfunction gaussian(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\nfunction clip(v, lo, hi) {\n  return Math.min(hi, Math.max(lo, v));\n}\n// Rejection sampling: redraw out-of-range Gaussian tails instead of clamping,\n// so the boundary doesn't accumulate an artificial pile-up of points.\nfunction gaussianInRange(mean, std, lo, hi) {\n  for (let tries = 0; tries < 50; tries++) {\n    const v = gaussian(mean, std);\n    if (v >= lo && v <= hi) return v;\n  }\n  return clip(gaussian(mean, std), lo, hi);\n}\n\n// --- Geography: simplified Pacific coastline of Tohoku/Kanto, Japan --------\n// [lat, lon] north -> south, deliberately simplified for map context (not\n// navigational). Equirectangular (lat/lon as-is) is an adequate approximation\n// of Mercator at this ~8 degree regional latitude band.\nconst LAT_MIN = 33.0;\nconst LAT_MAX = 41.0;\nconst LON_MIN = 134.0;\nconst LON_MAX = 144.5;\n\nconst COASTLINE = [\n  [40.55, 141.95],\n  [39.95, 141.95],\n  [39.35, 141.85],\n  [38.65, 141.55],\n  [37.95, 141.05],\n  [37.3, 141.02],\n  [36.55, 140.75],\n  [35.85, 140.75],\n  [35.15, 140.85],\n  [34.65, 139.85],\n  [34.15, 138.9],\n  [33.55, 136.05],\n];\nconst TRENCH_OFFSET_DEG = 1.8; // Japan Trench sits ~150-200km offshore\n\nfunction coastLonAtLat(lat) {\n  for (let i = 0; i < COASTLINE.length - 1; i++) {\n    const [latA, lonA] = COASTLINE[i];\n    const [latB, lonB] = COASTLINE[i + 1];\n    if (lat <= latA && lat >= latB) {\n      const f = (latA - lat) / (latA - latB);\n      return lonA + (lonB - lonA) * f;\n    }\n  }\n  return COASTLINE[COASTLINE.length - 1][1];\n}\nconst TRENCH = COASTLINE.map(([lat, lon]) => [lat, lon + TRENCH_OFFSET_DEG]);\n\n// --- Earthquake epicenters: shallow thrust events near the trench, deeper\n// Wadati-Benioff events further inland as the Pacific plate subducts westward\nconst EVENT_COUNT = 160;\nconst DEPTH_DOMAIN_MAX = 600; // km\nconst MAG_MIN = 3.8;\nconst MAG_MAX = 7.9;\n\nconst earthquakes = [];\nfor (let i = 0; i < EVENT_COUNT; i++) {\n  const lat = gaussianInRange((LAT_MIN + LAT_MAX) / 2, 2.1, LAT_MIN + 0.2, LAT_MAX - 0.2);\n  const trenchLon = coastLonAtLat(lat) + TRENCH_OFFSET_DEG;\n  const westOfTrench = gaussianInRange(0.55, 1.05, -1.2, 3.6); // + = inland/deeper, - = outer-rise\n  // Geometric safety net (compound of trenchLon lookup + westOfTrench), not a raw\n  // Gaussian tail, so a hard clip here does not create a boundary pile-up.\n  const lon = clip(trenchLon - westOfTrench, LON_MIN, LON_MAX);\n  const depthBase = 15 + Math.max(0, westOfTrench) * 145;\n  const depth = gaussianInRange(depthBase, 10, 8, DEPTH_DOMAIN_MAX);\n  const isSignificant = rand() < 0.16;\n  const magnitude = gaussianInRange(isSignificant ? 6.5 : 4.9, isSignificant ? 0.55 : 0.5, MAG_MIN, MAG_MAX);\n  earthquakes.push({ x: lon, y: lat, depth, magnitude });\n}\n\n// --- Color: imprint_seq (sequential, single-polarity depth) ----------------\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction seqColor(ratio, alpha) {\n  const [r0, g0, b0] = hexToRgb(t.seq[0]);\n  const [r1, g1, b1] = hexToRgb(t.seq[1]);\n  const r = Math.round(r0 + (r1 - r0) * ratio);\n  const g = Math.round(g0 + (g1 - g0) * ratio);\n  const b = Math.round(b0 + (b1 - b0) * ratio);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\nfunction magToRadius(mag) {\n  return 4 + ((mag - MAG_MIN) / (MAG_MAX - MAG_MIN)) * 19;\n}\n\nconst pointColors = earthquakes.map((e) => seqColor(e.depth / DEPTH_DOMAIN_MAX, 0.78));\nconst pointRadii = earthquakes.map((e) => magToRadius(e.magnitude));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: sequential depth colorbar --------------------------------\n// Drawn in the reserved right-side layout.padding band (outside chartArea), so\n// it can never overlap a data marker regardless of where events happen to fall.\nconst COLORBAR_MARGIN = 22;\nconst depthColorbarPlugin = {\n  id: \"depthColorbar\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const barW = 24;\n    const barH = 190;\n    const x = chartArea.right + COLORBAR_MARGIN;\n    const y = chartArea.top + 24;\n\n    ctx.save();\n    const gradient = ctx.createLinearGradient(0, y, 0, y + barH);\n    gradient.addColorStop(0, seqColor(1, 0.9));\n    gradient.addColorStop(1, seqColor(0, 0.9));\n    ctx.fillStyle = gradient;\n    ctx.fillRect(x, y, barW, barH);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(x, y, barW, barH);\n\n    ctx.fillStyle = t.ink;\n    ctx.font = \"15px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(`${DEPTH_DOMAIN_MAX} km`, x + barW + 8, y);\n    ctx.fillText(\"0 km\", x + barW + 8, y + barH);\n\n    ctx.save();\n    ctx.translate(x - 10, y + barH / 2);\n    ctx.rotate(-Math.PI / 2);\n    ctx.textAlign = \"center\";\n    ctx.fillText(\"Depth\", 0, 0);\n    ctx.restore();\n    ctx.restore();\n  },\n};\n\n// --- Custom plugin: magnitude size legend ------------------------------------\nconst magnitudeLegendPlugin = {\n  id: \"magnitudeLegend\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const refs = [4.5, 6.0, 7.5];\n    const x = chartArea.right - 60;\n    let y = chartArea.bottom - 20;\n\n    ctx.save();\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"15px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(\"Magnitude\", x - 34, y - (magToRadius(refs[2]) + 26));\n    for (const m of refs) {\n      const r = magToRadius(m);\n      const cy = y - r;\n      ctx.beginPath();\n      ctx.arc(x, cy, r, 0, Math.PI * 2);\n      ctx.strokeStyle = t.inkSoft;\n      ctx.lineWidth = 1.25;\n      ctx.stroke();\n      ctx.fillText(`M${m.toFixed(1)}`, x + r + 10, cy);\n      y = cy - r - 6;\n    }\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Coastline\",\n        type: \"line\",\n        data: COASTLINE.map(([lat, lon]) => ({ x: lon, y: lat })),\n        showLine: true,\n        borderColor: t.inkSoft,\n        borderWidth: 2.5,\n        pointRadius: 0,\n        tension: 0.25,\n        order: 1,\n      },\n      {\n        label: \"Japan Trench (approx.)\",\n        type: \"line\",\n        data: TRENCH.map(([lat, lon]) => ({ x: lon, y: lat })),\n        showLine: true,\n        borderColor: t.inkSoft,\n        borderDash: [6, 6],\n        borderWidth: 1.5,\n        pointRadius: 0,\n        tension: 0.25,\n        order: 1,\n      },\n      {\n        label: \"Epicenters\",\n        data: earthquakes,\n        pointBackgroundColor: pointColors,\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 1,\n        pointRadius: pointRadii,\n        pointHoverRadius: pointRadii.map((r) => r + 3),\n        order: 2,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    // right padding reserves space for the depthColorbarPlugin so it never\n    // overlaps a data marker: COLORBAR_MARGIN + barW + label-text width + margin\n    layout: { padding: { top: 8, right: 130, bottom: 8, left: 8 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"Tohoku-Oki Earthquakes · scatter-map-geographic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        // 83-char title vs. the 67-char baseline: 22 × 67/83 ≈ 18px (see plot-generator.md \"Title fontsize\")\n        font: { size: 18 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          generateLabels: (chart) => {\n            const [coastline, trench] = chart.data.datasets;\n            return [\n              {\n                text: coastline.label,\n                strokeStyle: coastline.borderColor,\n                lineWidth: coastline.borderWidth,\n                fillStyle: \"transparent\",\n                pointStyle: \"line\",\n                datasetIndex: 0,\n              },\n              {\n                text: trench.label,\n                strokeStyle: trench.borderColor,\n                lineDash: trench.borderDash,\n                lineWidth: trench.borderWidth,\n                fillStyle: \"transparent\",\n                pointStyle: \"line\",\n                datasetIndex: 1,\n              },\n            ];\n          },\n        },\n      },\n      tooltip: {\n        callbacks: {\n          title: () => \"Epicenter\",\n          label: (ctx) => {\n            const d = ctx.raw;\n            if (d.magnitude === undefined) {\n              return `${d.y.toFixed(2)}°N, ${d.x.toFixed(2)}°E`;\n            }\n            return [\n              `Magnitude: M${d.magnitude.toFixed(1)}`,\n              `Depth: ${Math.round(d.depth)} km`,\n              `Location: ${d.y.toFixed(2)}°N, ${d.x.toFixed(2)}°E`,\n            ];\n          },\n        },\n      },\n    },\n    scales: {\n      x: {\n        min: LON_MIN,\n        max: LON_MAX,\n        title: { display: true, text: \"Longitude (°E)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n      y: {\n        min: LAT_MIN,\n        max: LAT_MAX,\n        title: { display: true, text: \"Latitude (°N)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n    },\n  },\n  plugins: [depthColorbarPlugin, magnitudeLegendPlugin],\n});\n"}