Skip to content

Commit 05f945d

Browse files
Merge branch 'main' into implementation/dumbbell-basic/altair
2 parents c1704d9 + 69940be commit 05f945d

24 files changed

Lines changed: 3717 additions & 750 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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

Comments
 (0)