{"spec_id":"area-elevation-profile","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// area-elevation-profile: Terrain Elevation Profile Along Transect\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-10\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// Alpine trail — control points (distance km, elevation m)\nconst ctrlPoints = [\n  { d: 0,  e: 940  },\n  { d: 6,  e: 1120 },\n  { d: 14, e: 1620 },\n  { d: 20, e: 2100 },\n  { d: 27, e: 1820 },\n  { d: 33, e: 2450 },\n  { d: 40, e: 2200 },\n  { d: 47, e: 1700 },\n  { d: 56, e: 2300 },\n  { d: 63, e: 1880 },\n  { d: 70, e: 1280 },\n  { d: 76, e: 1050 },\n  { d: 80, e: 880  },\n];\n\n// Landmarks to annotate (trailhead, passes, summit, hut, end)\nconst landmarks = [\n  { d: 0,  e: 940,  name: \"Trailhead\",    elev: \"940 m\"  },\n  { d: 20, e: 2100, name: \"First Pass\",   elev: \"2100 m\" },\n  { d: 33, e: 2450, name: \"Summit Peak\",  elev: \"2450 m\" },\n  { d: 47, e: 1700, name: \"Mountain Hut\", elev: \"1700 m\" },\n  { d: 56, e: 2300, name: \"Second Pass\",  elev: \"2300 m\" },\n  { d: 80, e: 880,  name: \"Trail End\",    elev: \"880 m\"  },\n];\n\n// Cubic Hermite spline interpolation for smooth elevation profile\nfunction hermite(x, pts) {\n  const n = pts.length;\n  if (x <= pts[0].d) return pts[0].e;\n  if (x >= pts[n - 1].d) return pts[n - 1].e;\n  let i = 1;\n  while (i < n - 1 && pts[i].d < x) i++;\n  i--;\n  const p0 = pts[Math.max(0, i - 1)];\n  const p1 = pts[i];\n  const p2 = pts[i + 1];\n  const p3 = pts[Math.min(n - 1, i + 2)];\n  const tl = (x - p1.d) / (p2.d - p1.d);\n  const dt = p2.d - p1.d;\n  const m1 = (p2.e - p0.e) / (p2.d - p0.d);\n  const m2 = (p3.e - p1.e) / (p3.d - p1.d);\n  const h00 = 2*tl*tl*tl - 3*tl*tl + 1;\n  const h10 = tl*tl*tl - 2*tl*tl + tl;\n  const h01 = -2*tl*tl*tl + 3*tl*tl;\n  const h11 = tl*tl*tl - tl*tl;\n  return h00*p1.e + h10*dt*m1 + h01*p2.e + h11*dt*m2;\n}\n\n// Sample 200 points along the trail\nconst N = 200;\nconst distances = [];\nconst elevations = [];\nfor (let i = 0; i < N; i++) {\n  const d = (i / (N - 1)) * 80;\n  distances.push(parseFloat(d.toFixed(3)));\n  elevations.push(Math.round(hermite(d, ctrlPoints)));\n}\n\n// Mount canvas\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Custom plugin: draws vertical dashed guide lines + label boxes at each landmark\nconst landmarkPlugin = {\n  id: \"landmarks\",\n  afterDraw(chart) {\n    const { ctx: c, chartArea, scales } = chart;\n    const xSc = scales.x;\n    const ySc = scales.y;\n    c.save();\n\n    landmarks.forEach(lm => {\n      const xPx = xSc.getPixelForValue(lm.d);\n      const yPx = ySc.getPixelForValue(lm.e);\n      const lineH = 17;\n      const pad = 5;\n      const gap = 9;\n\n      // Dashed vertical guide from terrain dot down to x-axis baseline\n      c.beginPath();\n      c.setLineDash([5, 4]);\n      c.strokeStyle = t.inkSoft + \"90\";\n      c.lineWidth = 1.5;\n      c.moveTo(xPx, yPx + 6);\n      c.lineTo(xPx, chartArea.bottom);\n      c.stroke();\n      c.setLineDash([]);\n\n      // Hollow dot at terrain elevation\n      c.beginPath();\n      c.arc(xPx, yPx, 5, 0, 2 * Math.PI);\n      c.fillStyle = t.pageBg;\n      c.fill();\n      c.beginPath();\n      c.arc(xPx, yPx, 5, 0, 2 * Math.PI);\n      c.strokeStyle = t.ink;\n      c.lineWidth = 2;\n      c.stroke();\n\n      // Measure label box dimensions\n      c.font = \"bold 15px sans-serif\";\n      const nameW = c.measureText(lm.name).width;\n      c.font = \"14px sans-serif\";\n      const elevW = c.measureText(lm.elev).width;\n      const boxW = Math.max(nameW, elevW) + pad * 2;\n      const boxH = lineH * 2 + pad;\n\n      // Place label above dot; flip below if too close to chart top\n      const flipBelow = (yPx - gap - boxH) < chartArea.top + 5;\n      const boxTop = flipBelow ? yPx + gap : yPx - gap - boxH;\n\n      // Clamp box horizontally to stay within chart area (handles edge landmarks)\n      let boxLeft = xPx - boxW / 2;\n      boxLeft = Math.max(chartArea.left, Math.min(chartArea.right - boxW, boxLeft));\n      const boxCenterX = boxLeft + boxW / 2;\n\n      // Background box for readability\n      c.fillStyle = t.elevatedBg;\n      c.beginPath();\n      c.roundRect(boxLeft, boxTop, boxW, boxH, 3);\n      c.fill();\n\n      // Landmark name (bold)\n      c.font = \"bold 15px sans-serif\";\n      c.fillStyle = t.ink;\n      c.textAlign = \"center\";\n      c.textBaseline = \"top\";\n      c.fillText(lm.name, boxCenterX, boxTop + pad);\n\n      // Elevation value (regular, secondary)\n      c.font = \"14px sans-serif\";\n      c.fillStyle = t.inkSoft;\n      c.fillText(lm.elev, boxCenterX, boxTop + pad + lineH);\n    });\n\n    c.restore();\n  },\n};\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    labels: distances,\n    datasets: [{\n      label: \"Elevation\",\n      data: elevations,\n      borderColor: t.palette[0],\n      borderWidth: 2.5,\n      fill: \"start\",\n      backgroundColor(ctx) {\n        const chart = ctx.chart;\n        const { ctx: c, chartArea } = chart;\n        if (!chartArea) return t.palette[0] + \"60\";\n        const grad = c.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);\n        grad.addColorStop(0, t.palette[0] + \"BB\");\n        grad.addColorStop(1, t.palette[0] + \"18\");\n        return grad;\n      },\n      pointRadius: 0,\n      tension: 0.3,\n    }],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"area-elevation-profile · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { top: 16, bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Vertical exaggeration ≈ 10×\",\n        color: t.inkSoft,\n        font: { size: 13 },\n        padding: { bottom: 10 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: 80,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          stepSize: 10,\n          callback: (v) => v + \" km\",\n        },\n        grid: { display: false },\n        title: {\n          display: true,\n          text: \"Distance (km)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        border: { color: t.inkSoft },\n      },\n      y: {\n        min: 400,\n        max: 2800,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          stepSize: 400,\n          callback: (v) => v + \" m\",\n        },\n        grid: { color: t.grid },\n        title: {\n          display: true,\n          text: \"Elevation (m)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n        border: { color: t.inkSoft },\n      },\n    },\n  },\n  plugins: [landmarkPlugin],\n});\n"}