{"spec_id":"area-mountain-panorama","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks\n// Library: chartjs 4.4.7 | JavaScript 22.23.0\n// Quality: 89/100 | Created: 2026-06-30\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst theme = window.ANYPLOT_THEME;\n\n// --- Data: Valais Alps panorama from Gornergrat, sweeping W to E ----------\n// Piecewise-linear ridgeline control points [bearing_deg, elevation_m]\n// Sharp triangular flanks with explicit saddle control points between peaks\nconst ctrlPts = [\n  [0, 3100], [5, 3300], [8, 3550],\n  [12, 4506],  // Weisshorn\n  [15, 3800], [17, 3350], [19, 3150],\n  [20, 4221],  // Zinalrothorn\n  [22, 3700], [25, 3050], [27, 3150],\n  [30, 4063],  // Ober Gabelhorn\n  [33, 3600], [36, 3200],\n  [40, 4358],  // Dent Blanche\n  [44, 3500], [47, 3100], [52, 2950], [56, 3200],\n  [58, 3650],\n  [60, 4478],  // Matterhorn (focal summit)\n  [62, 3800], [65, 3100], [68, 2900], [72, 3050],\n  [75, 3600], [78, 3850],\n  [80, 4164],  // Breithorn\n  [84, 3800], [87, 3650],\n  [90, 4092],  // Pollux\n  [93, 3900],\n  [95, 4223],  // Castor\n  [98, 3900], [102, 3700],\n  [105, 4527], // Liskamm\n  [109, 4100], [113, 3800],\n  [120, 4634], // Monte Rosa / Dufourspitze (highest)\n  [126, 4000], [129, 3700],\n  [135, 4190], // Strahlhorn\n  [138, 3700], [140, 3500],\n  [142, 4199], // Rimpfischhorn\n  [145, 3650], [148, 3400],\n  [150, 4027], // Allalinhorn\n  [153, 3700],\n  [158, 4206], // Alphubel\n  [161, 3800], [164, 3700],\n  [168, 4545], // Dom\n  [171, 4350],\n  [175, 4491], // Täschhorn\n  [177, 4000], [180, 3100],\n];\n\nfunction lerpElev(angle) {\n  for (let i = 0; i < ctrlPts.length - 1; i++) {\n    const [a0, e0] = ctrlPts[i];\n    const [a1, e1] = ctrlPts[i + 1];\n    if (angle >= a0 && angle <= a1) {\n      return e0 + ((angle - a0) / (a1 - a0)) * (e1 - e0);\n    }\n  }\n  return ctrlPts[0][1];\n}\n\n// Deterministic sine-hash noise — avoids Math.random() non-reproducibility\nfunction sinNoise(i, salt) {\n  const v = Math.sin(i * 127.1 + salt * 311.7) * 43758.5453;\n  return v - Math.floor(v);\n}\n\nconst N     = 720;\nconst Y_MIN = 2500;\nconst Y_MAX = 5100;\nconst mainPts = [];\nconst bgPts   = [];\n\nfor (let i = 0; i < N; i++) {\n  const angle  = (i / (N - 1)) * 180;\n  const base   = lerpElev(angle);\n  const noise  = (sinNoise(i, 1) - 0.5) * 50;\n  mainPts.push({ x: angle, y: Math.max(Y_MIN, Math.round(base + noise)) });\n\n  // Background (distance) ridge: scaled lower for atmospheric depth\n  const bgNoise = (sinNoise(i, 7) - 0.5) * 80;\n  bgPts.push({ x: angle, y: Math.max(Y_MIN, Math.round(base * 0.70 + 780 + bgNoise)) });\n}\n\n// Annotated summits — staggered yOff (px above summit) to prevent overlap\nconst summits = [\n  { name: \"Weisshorn\",      angle: 12,  elev: 4506, yOff: 72  },\n  { name: \"Zinalrothorn\",   angle: 20,  elev: 4221, yOff: 102 },\n  { name: \"Ober Gabelhorn\", angle: 30,  elev: 4063, yOff: 72  },\n  { name: \"Dent Blanche\",   angle: 40,  elev: 4358, yOff: 108 },\n  { name: \"Matterhorn\",     angle: 60,  elev: 4478, yOff: 55  },\n  { name: \"Breithorn\",      angle: 80,  elev: 4164, yOff: 82  },\n  { name: \"Liskamm\",        angle: 105, elev: 4527, yOff: 68  },\n  { name: \"Monte Rosa\",     angle: 120, elev: 4634, yOff: 55  },\n  { name: \"Rimpfischhorn\",  angle: 142, elev: 4199, yOff: 90  },\n  { name: \"Alphubel\",       angle: 158, elev: 4206, yOff: 72  },\n  { name: \"Dom\",            angle: 168, elev: 4545, yOff: 110 },\n  { name: \"Täschhorn\",      angle: 175, elev: 4491, yOff: 60  },\n];\n\n// --- Mount ------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Theme colors -----------------------------------------------------------\nconst skyHigh  = theme === 'light' ? '#2e6fa8' : '#080d1f';\nconst skyMid   = theme === 'light' ? '#7fb3d8' : '#0f1a36';\nconst skyLow   = theme === 'light' ? '#d6eaf8' : '#1A1A17';\nconst mtFill   = theme === 'light' ? 'rgba(26,24,20,0.94)' : 'rgba(10,9,7,0.96)';\nconst bgFill   = theme === 'light' ? 'rgba(95,115,135,0.28)' : 'rgba(30,45,65,0.36)';\nconst dotColor = t.palette[0];  // #009E73 brand green — summit markers\n\n// --- Plugins ----------------------------------------------------------------\nconst skyPlugin = {\n  id: 'skyGradient',\n  // beforeDraw ensures gradient is under gridlines and dataset fills\n  beforeDraw(chart) {\n    const { ctx, chartArea: { left, top, right, bottom } } = chart;\n    const g = ctx.createLinearGradient(0, top, 0, bottom);\n    g.addColorStop(0,    skyHigh);\n    g.addColorStop(0.42, skyMid);\n    g.addColorStop(1,    skyLow);\n    ctx.save();\n    ctx.fillStyle = g;\n    ctx.fillRect(left, top, right - left, bottom - top);\n    ctx.restore();\n  },\n};\n\nconst annotPlugin = {\n  id: 'peakAnnotations',\n  afterDraw(chart) {\n    const { ctx, scales: { x: xSc, y: ySc } } = chart;\n    ctx.save();\n    ctx.textAlign = 'center';\n\n    // Text shadow for contrast against both light and dark sky\n    ctx.shadowColor = 'rgba(0,0,0,0.45)';\n    ctx.shadowBlur = 3;\n\n    summits.forEach((s) => {\n      const isMatterhorn = s.name === 'Matterhorn';\n      const px = xSc.getPixelForValue(s.angle);\n      const py = ySc.getPixelForValue(s.elev);\n      const ly = py - s.yOff;  // label baseline y (above summit dot)\n\n      // Dashed leader line from just above dot to just below elevation label\n      ctx.shadowBlur = 0;\n      ctx.strokeStyle = 'rgba(250,248,241,0.48)';\n      ctx.lineWidth = 1;\n      ctx.setLineDash([3, 4]);\n      ctx.beginPath();\n      ctx.moveTo(px, py - (isMatterhorn ? 9 : 6));\n      ctx.lineTo(px, ly + 16);\n      ctx.stroke();\n      ctx.setLineDash([]);\n      ctx.shadowBlur = 3;\n\n      // Summit dot — Matterhorn gets a larger highlighted dot\n      if (isMatterhorn) {\n        // Outer highlight ring\n        ctx.fillStyle = 'rgba(250,248,241,0.35)';\n        ctx.beginPath();\n        ctx.arc(px, py, 12, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      ctx.fillStyle = dotColor;\n      ctx.beginPath();\n      ctx.arc(px, py, isMatterhorn ? 7 : 4, 0, Math.PI * 2);\n      ctx.fill();\n\n      // Peak name — Matterhorn uses larger bold font\n      ctx.fillStyle = '#FAF8F1';\n      ctx.font = isMatterhorn ? 'bold 15px system-ui, sans-serif' : 'bold 13px system-ui, sans-serif';\n      ctx.fillText(s.name, px, ly);\n\n      // Elevation in meters below name\n      ctx.font = isMatterhorn ? '12px system-ui, sans-serif' : '11px system-ui, sans-serif';\n      ctx.fillStyle = 'rgba(250,248,241,0.80)';\n      ctx.fillText(`${s.elev.toLocaleString()} m`, px, ly + 16);\n    });\n\n    ctx.shadowBlur = 0;\n\n    ctx.restore();\n  },\n};\n\n// --- Chart ------------------------------------------------------------------\nconst TITLE = \"area-mountain-panorama · javascript · chartjs · anyplot.ai\";\n\nnew Chart(canvas, {\n  type: 'line',\n  data: {\n    datasets: [\n      {\n        label: 'Background Ridge',\n        data: bgPts,\n        parsing: false,\n        fill: 'start',\n        backgroundColor: bgFill,\n        borderColor: 'transparent',\n        borderWidth: 0,\n        pointRadius: 0,\n        tension: 0,\n      },\n      {\n        label: 'Main Ridgeline',\n        data: mainPts,\n        parsing: false,\n        fill: 'start',\n        backgroundColor: mtFill,\n        borderColor: theme === 'light' ? 'rgba(195,208,220,0.40)' : 'rgba(70,85,105,0.40)',\n        borderWidth: 1,\n        pointRadius: 0,\n        tension: 0,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: TITLE,\n        color: t.ink,\n        font: { size: 22 },\n        padding: { top: 10, bottom: 6 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: 'linear',\n        min: 0,\n        max: 180,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          stepSize: 30,\n          callback: (v) => ({ 0: 'W', 30: 'WSW', 60: 'SW', 90: 'S', 120: 'SSE', 150: 'SE', 180: 'E' })[v] || `${v}°`,\n        },\n        grid: { display: false },\n        border: { color: t.inkSoft },\n        title: {\n          display: true,\n          text: 'Bearing — Panorama from Gornergrat',\n          color: t.ink,\n          font: { size: 14 },\n        },\n      },\n      y: {\n        min: Y_MIN,\n        max: Y_MAX,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          callback: (v) => `${v.toLocaleString()} m`,\n        },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft },\n        title: {\n          display: true,\n          text: 'Elevation (m)',\n          color: t.ink,\n          font: { size: 14 },\n        },\n      },\n    },\n  },\n  plugins: [skyPlugin, annotPlugin],\n});\n"}