|
| 1 | +// anyplot.ai |
| 2 | +// area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks |
| 3 | +// Library: chartjs 4.4.7 | JavaScript 22.23.0 |
| 4 | +// Quality: 89/100 | Created: 2026-06-30 |
| 5 | +//# anyplot-orientation: landscape |
| 6 | + |
| 7 | +const t = window.ANYPLOT_TOKENS; |
| 8 | +const theme = window.ANYPLOT_THEME; |
| 9 | + |
| 10 | +// --- Data: Valais Alps panorama from Gornergrat, sweeping W to E ---------- |
| 11 | +// Piecewise-linear ridgeline control points [bearing_deg, elevation_m] |
| 12 | +// Sharp triangular flanks with explicit saddle control points between peaks |
| 13 | +const ctrlPts = [ |
| 14 | + [0, 3100], [5, 3300], [8, 3550], |
| 15 | + [12, 4506], // Weisshorn |
| 16 | + [15, 3800], [17, 3350], [19, 3150], |
| 17 | + [20, 4221], // Zinalrothorn |
| 18 | + [22, 3700], [25, 3050], [27, 3150], |
| 19 | + [30, 4063], // Ober Gabelhorn |
| 20 | + [33, 3600], [36, 3200], |
| 21 | + [40, 4358], // Dent Blanche |
| 22 | + [44, 3500], [47, 3100], [52, 2950], [56, 3200], |
| 23 | + [58, 3650], |
| 24 | + [60, 4478], // Matterhorn (focal summit) |
| 25 | + [62, 3800], [65, 3100], [68, 2900], [72, 3050], |
| 26 | + [75, 3600], [78, 3850], |
| 27 | + [80, 4164], // Breithorn |
| 28 | + [84, 3800], [87, 3650], |
| 29 | + [90, 4092], // Pollux |
| 30 | + [93, 3900], |
| 31 | + [95, 4223], // Castor |
| 32 | + [98, 3900], [102, 3700], |
| 33 | + [105, 4527], // Liskamm |
| 34 | + [109, 4100], [113, 3800], |
| 35 | + [120, 4634], // Monte Rosa / Dufourspitze (highest) |
| 36 | + [126, 4000], [129, 3700], |
| 37 | + [135, 4190], // Strahlhorn |
| 38 | + [138, 3700], [140, 3500], |
| 39 | + [142, 4199], // Rimpfischhorn |
| 40 | + [145, 3650], [148, 3400], |
| 41 | + [150, 4027], // Allalinhorn |
| 42 | + [153, 3700], |
| 43 | + [158, 4206], // Alphubel |
| 44 | + [161, 3800], [164, 3700], |
| 45 | + [168, 4545], // Dom |
| 46 | + [171, 4350], |
| 47 | + [175, 4491], // Täschhorn |
| 48 | + [177, 4000], [180, 3100], |
| 49 | +]; |
| 50 | + |
| 51 | +function lerpElev(angle) { |
| 52 | + for (let i = 0; i < ctrlPts.length - 1; i++) { |
| 53 | + const [a0, e0] = ctrlPts[i]; |
| 54 | + const [a1, e1] = ctrlPts[i + 1]; |
| 55 | + if (angle >= a0 && angle <= a1) { |
| 56 | + return e0 + ((angle - a0) / (a1 - a0)) * (e1 - e0); |
| 57 | + } |
| 58 | + } |
| 59 | + return ctrlPts[0][1]; |
| 60 | +} |
| 61 | + |
| 62 | +// Deterministic sine-hash noise — avoids Math.random() non-reproducibility |
| 63 | +function sinNoise(i, salt) { |
| 64 | + const v = Math.sin(i * 127.1 + salt * 311.7) * 43758.5453; |
| 65 | + return v - Math.floor(v); |
| 66 | +} |
| 67 | + |
| 68 | +const N = 720; |
| 69 | +const Y_MIN = 2500; |
| 70 | +const Y_MAX = 5100; |
| 71 | +const mainPts = []; |
| 72 | +const bgPts = []; |
| 73 | + |
| 74 | +for (let i = 0; i < N; i++) { |
| 75 | + const angle = (i / (N - 1)) * 180; |
| 76 | + const base = lerpElev(angle); |
| 77 | + const noise = (sinNoise(i, 1) - 0.5) * 50; |
| 78 | + mainPts.push({ x: angle, y: Math.max(Y_MIN, Math.round(base + noise)) }); |
| 79 | + |
| 80 | + // Background (distance) ridge: scaled lower for atmospheric depth |
| 81 | + const bgNoise = (sinNoise(i, 7) - 0.5) * 80; |
| 82 | + bgPts.push({ x: angle, y: Math.max(Y_MIN, Math.round(base * 0.70 + 780 + bgNoise)) }); |
| 83 | +} |
| 84 | + |
| 85 | +// Annotated summits — staggered yOff (px above summit) to prevent overlap |
| 86 | +const summits = [ |
| 87 | + { name: "Weisshorn", angle: 12, elev: 4506, yOff: 72 }, |
| 88 | + { name: "Zinalrothorn", angle: 20, elev: 4221, yOff: 102 }, |
| 89 | + { name: "Ober Gabelhorn", angle: 30, elev: 4063, yOff: 72 }, |
| 90 | + { name: "Dent Blanche", angle: 40, elev: 4358, yOff: 108 }, |
| 91 | + { name: "Matterhorn", angle: 60, elev: 4478, yOff: 55 }, |
| 92 | + { name: "Breithorn", angle: 80, elev: 4164, yOff: 82 }, |
| 93 | + { name: "Liskamm", angle: 105, elev: 4527, yOff: 68 }, |
| 94 | + { name: "Monte Rosa", angle: 120, elev: 4634, yOff: 55 }, |
| 95 | + { name: "Rimpfischhorn", angle: 142, elev: 4199, yOff: 90 }, |
| 96 | + { name: "Alphubel", angle: 158, elev: 4206, yOff: 72 }, |
| 97 | + { name: "Dom", angle: 168, elev: 4545, yOff: 110 }, |
| 98 | + { name: "Täschhorn", angle: 175, elev: 4491, yOff: 60 }, |
| 99 | +]; |
| 100 | + |
| 101 | +// --- Mount ------------------------------------------------------------------ |
| 102 | +const canvas = document.createElement("canvas"); |
| 103 | +document.getElementById("container").appendChild(canvas); |
| 104 | + |
| 105 | +// --- Theme colors ----------------------------------------------------------- |
| 106 | +const skyHigh = theme === 'light' ? '#2e6fa8' : '#080d1f'; |
| 107 | +const skyMid = theme === 'light' ? '#7fb3d8' : '#0f1a36'; |
| 108 | +const skyLow = theme === 'light' ? '#d6eaf8' : '#1A1A17'; |
| 109 | +const mtFill = theme === 'light' ? 'rgba(26,24,20,0.94)' : 'rgba(10,9,7,0.96)'; |
| 110 | +const bgFill = theme === 'light' ? 'rgba(95,115,135,0.28)' : 'rgba(30,45,65,0.36)'; |
| 111 | +const dotColor = t.palette[0]; // #009E73 brand green — summit markers |
| 112 | + |
| 113 | +// --- Plugins ---------------------------------------------------------------- |
| 114 | +const skyPlugin = { |
| 115 | + id: 'skyGradient', |
| 116 | + // beforeDraw ensures gradient is under gridlines and dataset fills |
| 117 | + beforeDraw(chart) { |
| 118 | + const { ctx, chartArea: { left, top, right, bottom } } = chart; |
| 119 | + const g = ctx.createLinearGradient(0, top, 0, bottom); |
| 120 | + g.addColorStop(0, skyHigh); |
| 121 | + g.addColorStop(0.42, skyMid); |
| 122 | + g.addColorStop(1, skyLow); |
| 123 | + ctx.save(); |
| 124 | + ctx.fillStyle = g; |
| 125 | + ctx.fillRect(left, top, right - left, bottom - top); |
| 126 | + ctx.restore(); |
| 127 | + }, |
| 128 | +}; |
| 129 | + |
| 130 | +const annotPlugin = { |
| 131 | + id: 'peakAnnotations', |
| 132 | + afterDraw(chart) { |
| 133 | + const { ctx, scales: { x: xSc, y: ySc } } = chart; |
| 134 | + ctx.save(); |
| 135 | + ctx.textAlign = 'center'; |
| 136 | + |
| 137 | + // Text shadow for contrast against both light and dark sky |
| 138 | + ctx.shadowColor = 'rgba(0,0,0,0.45)'; |
| 139 | + ctx.shadowBlur = 3; |
| 140 | + |
| 141 | + summits.forEach((s) => { |
| 142 | + const isMatterhorn = s.name === 'Matterhorn'; |
| 143 | + const px = xSc.getPixelForValue(s.angle); |
| 144 | + const py = ySc.getPixelForValue(s.elev); |
| 145 | + const ly = py - s.yOff; // label baseline y (above summit dot) |
| 146 | + |
| 147 | + // Dashed leader line from just above dot to just below elevation label |
| 148 | + ctx.shadowBlur = 0; |
| 149 | + ctx.strokeStyle = 'rgba(250,248,241,0.48)'; |
| 150 | + ctx.lineWidth = 1; |
| 151 | + ctx.setLineDash([3, 4]); |
| 152 | + ctx.beginPath(); |
| 153 | + ctx.moveTo(px, py - (isMatterhorn ? 9 : 6)); |
| 154 | + ctx.lineTo(px, ly + 16); |
| 155 | + ctx.stroke(); |
| 156 | + ctx.setLineDash([]); |
| 157 | + ctx.shadowBlur = 3; |
| 158 | + |
| 159 | + // Summit dot — Matterhorn gets a larger highlighted dot |
| 160 | + if (isMatterhorn) { |
| 161 | + // Outer highlight ring |
| 162 | + ctx.fillStyle = 'rgba(250,248,241,0.35)'; |
| 163 | + ctx.beginPath(); |
| 164 | + ctx.arc(px, py, 12, 0, Math.PI * 2); |
| 165 | + ctx.fill(); |
| 166 | + } |
| 167 | + ctx.fillStyle = dotColor; |
| 168 | + ctx.beginPath(); |
| 169 | + ctx.arc(px, py, isMatterhorn ? 7 : 4, 0, Math.PI * 2); |
| 170 | + ctx.fill(); |
| 171 | + |
| 172 | + // Peak name — Matterhorn uses larger bold font |
| 173 | + ctx.fillStyle = '#FAF8F1'; |
| 174 | + ctx.font = isMatterhorn ? 'bold 15px system-ui, sans-serif' : 'bold 13px system-ui, sans-serif'; |
| 175 | + ctx.fillText(s.name, px, ly); |
| 176 | + |
| 177 | + // Elevation in meters below name |
| 178 | + ctx.font = isMatterhorn ? '12px system-ui, sans-serif' : '11px system-ui, sans-serif'; |
| 179 | + ctx.fillStyle = 'rgba(250,248,241,0.80)'; |
| 180 | + ctx.fillText(`${s.elev.toLocaleString()} m`, px, ly + 16); |
| 181 | + }); |
| 182 | + |
| 183 | + ctx.shadowBlur = 0; |
| 184 | + |
| 185 | + ctx.restore(); |
| 186 | + }, |
| 187 | +}; |
| 188 | + |
| 189 | +// --- Chart ------------------------------------------------------------------ |
| 190 | +const TITLE = "area-mountain-panorama · javascript · chartjs · anyplot.ai"; |
| 191 | + |
| 192 | +new Chart(canvas, { |
| 193 | + type: 'line', |
| 194 | + data: { |
| 195 | + datasets: [ |
| 196 | + { |
| 197 | + label: 'Background Ridge', |
| 198 | + data: bgPts, |
| 199 | + parsing: false, |
| 200 | + fill: 'start', |
| 201 | + backgroundColor: bgFill, |
| 202 | + borderColor: 'transparent', |
| 203 | + borderWidth: 0, |
| 204 | + pointRadius: 0, |
| 205 | + tension: 0, |
| 206 | + }, |
| 207 | + { |
| 208 | + label: 'Main Ridgeline', |
| 209 | + data: mainPts, |
| 210 | + parsing: false, |
| 211 | + fill: 'start', |
| 212 | + backgroundColor: mtFill, |
| 213 | + borderColor: theme === 'light' ? 'rgba(195,208,220,0.40)' : 'rgba(70,85,105,0.40)', |
| 214 | + borderWidth: 1, |
| 215 | + pointRadius: 0, |
| 216 | + tension: 0, |
| 217 | + }, |
| 218 | + ], |
| 219 | + }, |
| 220 | + options: { |
| 221 | + responsive: true, |
| 222 | + maintainAspectRatio: false, |
| 223 | + animation: false, |
| 224 | + plugins: { |
| 225 | + title: { |
| 226 | + display: true, |
| 227 | + text: TITLE, |
| 228 | + color: t.ink, |
| 229 | + font: { size: 22 }, |
| 230 | + padding: { top: 10, bottom: 6 }, |
| 231 | + }, |
| 232 | + legend: { display: false }, |
| 233 | + tooltip: { enabled: false }, |
| 234 | + }, |
| 235 | + scales: { |
| 236 | + x: { |
| 237 | + type: 'linear', |
| 238 | + min: 0, |
| 239 | + max: 180, |
| 240 | + ticks: { |
| 241 | + color: t.inkSoft, |
| 242 | + font: { size: 13 }, |
| 243 | + stepSize: 30, |
| 244 | + callback: (v) => ({ 0: 'W', 30: 'WSW', 60: 'SW', 90: 'S', 120: 'SSE', 150: 'SE', 180: 'E' })[v] || `${v}°`, |
| 245 | + }, |
| 246 | + grid: { display: false }, |
| 247 | + border: { color: t.inkSoft }, |
| 248 | + title: { |
| 249 | + display: true, |
| 250 | + text: 'Bearing — Panorama from Gornergrat', |
| 251 | + color: t.ink, |
| 252 | + font: { size: 14 }, |
| 253 | + }, |
| 254 | + }, |
| 255 | + y: { |
| 256 | + min: Y_MIN, |
| 257 | + max: Y_MAX, |
| 258 | + ticks: { |
| 259 | + color: t.inkSoft, |
| 260 | + font: { size: 13 }, |
| 261 | + callback: (v) => `${v.toLocaleString()} m`, |
| 262 | + }, |
| 263 | + grid: { color: t.grid }, |
| 264 | + border: { color: t.inkSoft }, |
| 265 | + title: { |
| 266 | + display: true, |
| 267 | + text: 'Elevation (m)', |
| 268 | + color: t.ink, |
| 269 | + font: { size: 14 }, |
| 270 | + }, |
| 271 | + }, |
| 272 | + }, |
| 273 | + }, |
| 274 | + plugins: [skyPlugin, annotPlugin], |
| 275 | +}); |
0 commit comments