Skip to content

Commit 722e4ac

Browse files
feat(altair): implement area-mountain-panorama (#5370)
## Implementation: `area-mountain-panorama` - python/altair Implements the **python/altair** version of `area-mountain-panorama`. **File:** `plots/area-mountain-panorama/implementations/python/altair.py` **Parent Issue:** #5365 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24919026131)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 39b747c commit 722e4ac

2 files changed

Lines changed: 462 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
""" anyplot.ai
2+
area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks
3+
Library: altair 6.1.0 | Python 3.14.4
4+
Quality: 93/100 | Created: 2026-04-25
5+
"""
6+
7+
import importlib
8+
import os
9+
import sys
10+
11+
12+
# Drop script directory from sys.path so the `altair` package resolves, not this file
13+
sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.dirname(os.path.abspath(__file__))]
14+
alt = importlib.import_module("altair")
15+
np = importlib.import_module("numpy")
16+
pd = importlib.import_module("pandas")
17+
18+
19+
# Theme tokens (chrome flips with theme; data colors stay constant)
20+
THEME = os.getenv("ANYPLOT_THEME", "light")
21+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
22+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
23+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
24+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
25+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
26+
BRAND = "#009E73" # Okabe-Ito position 1 — silhouette fill (single data series)
27+
28+
# Theme-adaptive dusk sky gradient (chrome layer above the ridgeline; spec-authorized)
29+
SKY_HORIZON = "#FFC58A" if THEME == "light" else "#5A3422" # warm dusk glow at ridgeline
30+
SKY_MID = "#D89AA8" if THEME == "light" else "#2E1F35" # twilight rose / deep plum
31+
SKY_ZENITH = "#5C5078" if THEME == "light" else "#0C0E1A" # evening blue / night sky
32+
33+
# Data — Wallis (Valais, CH) panorama: 16 4000-m summits along a 180° sweep
34+
peaks = pd.DataFrame(
35+
[
36+
("Weisshorn", 4506, 9),
37+
("Zinalrothorn", 4221, 20),
38+
("Ober Gabelhorn", 4063, 30),
39+
("Dent Blanche", 4358, 42),
40+
("Matterhorn", 4478, 56),
41+
("Breithorn", 4164, 73),
42+
("Pollux", 4092, 81),
43+
("Castor", 4223, 88),
44+
("Liskamm", 4527, 97),
45+
("Monte Rosa", 4634, 109),
46+
("Strahlhorn", 4190, 122),
47+
("Rimpfischhorn", 4199, 132),
48+
("Allalinhorn", 4027, 140),
49+
("Alphubel", 4206, 148),
50+
("Täschhorn", 4491, 158),
51+
("Dom", 4545, 168),
52+
],
53+
columns=["name", "elevation_m", "angle_deg"],
54+
)
55+
56+
# Skyline ridge — gaussians around named peaks plus naturalistic minor ridge texture
57+
np.random.seed(42)
58+
angles = np.linspace(-2, 182, 1500)
59+
ridge_elev = 2950 + 110 * np.sin(angles * 0.11) + 35 * np.sin(angles * 0.43 + 1.1)
60+
61+
for _ in range(55):
62+
pos = np.random.uniform(-2, 182)
63+
height = np.random.uniform(150, 480)
64+
width = np.random.uniform(1.4, 3.0)
65+
ridge_elev = np.maximum(ridge_elev, 2950 + height * np.exp(-((angles - pos) ** 2) / (2 * width**2)))
66+
67+
for _, row in peaks.iterrows():
68+
height = row["elevation_m"] - 2950
69+
width = 2.0 + (row["elevation_m"] - 4000) * 0.0007
70+
ridge_elev = np.maximum(ridge_elev, 2950 + height * np.exp(-((angles - row["angle_deg"]) ** 2) / (2 * width**2)))
71+
72+
ridge = pd.DataFrame({"angle_deg": angles, "elevation_m": ridge_elev})
73+
74+
# Stagger label heights so adjacent peaks don't collide; Matterhorn lifted as focal summit
75+
peaks = peaks.sort_values("angle_deg").reset_index(drop=True)
76+
LABEL_HIGH = 5800
77+
LABEL_LOW = 5400
78+
peaks["label_y"] = [LABEL_HIGH if i % 2 == 0 else LABEL_LOW for i in range(len(peaks))]
79+
peaks.loc[peaks["name"] == "Matterhorn", "label_y"] = 6000
80+
peaks["elev_label"] = peaks["elevation_m"].apply(lambda v: f"{v:.0f} m")
81+
82+
matterhorn = peaks[peaks["name"] == "Matterhorn"]
83+
others = peaks[peaks["name"] != "Matterhorn"]
84+
85+
# Shared scales / axis so all layers register on the same coordinate system
86+
X_SCALE = alt.Scale(domain=[0, 180])
87+
Y_SCALE = alt.Scale(domain=[2900, 6300])
88+
Y_AXIS = alt.Axis(values=[3000, 3500, 4000, 4500, 5000])
89+
90+
# Sky — dusk vertical gradient covering the full plot area; silhouette will mask the lower half
91+
sky_df = pd.DataFrame({"x_min": [0], "x_max": [180], "y_min": [2900], "y_max": [6300]})
92+
sky = (
93+
alt.Chart(sky_df)
94+
.mark_rect(
95+
color={
96+
"x1": 0,
97+
"y1": 0,
98+
"x2": 0,
99+
"y2": 1,
100+
"gradient": "linear",
101+
"stops": [
102+
{"offset": 0.0, "color": SKY_ZENITH},
103+
{"offset": 0.55, "color": SKY_MID},
104+
{"offset": 1.0, "color": SKY_HORIZON},
105+
],
106+
}
107+
)
108+
.encode(
109+
x=alt.X("x_min:Q", scale=X_SCALE, axis=None),
110+
x2="x_max:Q",
111+
y=alt.Y("y_min:Q", scale=Y_SCALE, title="Elevation (m)", axis=Y_AXIS),
112+
y2="y_max:Q",
113+
)
114+
)
115+
116+
# Silhouette — brand-green photo-like fill; ridge stroke gives the snow-edge alpenglow line
117+
silhouette = (
118+
alt.Chart(ridge)
119+
.mark_area(color=BRAND, line={"color": BRAND, "strokeWidth": 2.5}, opacity=1.0)
120+
.encode(x="angle_deg:Q", y="elevation_m:Q")
121+
)
122+
123+
# Leader lines from summit up to label position (with tooltip for HTML hover)
124+
leaders = (
125+
alt.Chart(others)
126+
.mark_rule(strokeWidth=1.0, opacity=0.55, color=INK_SOFT)
127+
.encode(
128+
x="angle_deg:Q",
129+
y="elevation_m:Q",
130+
y2="label_y:Q",
131+
tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")],
132+
)
133+
)
134+
matterhorn_leader = (
135+
alt.Chart(matterhorn)
136+
.mark_rule(strokeWidth=2.0, opacity=0.9, color=INK)
137+
.encode(
138+
x="angle_deg:Q",
139+
y="elevation_m:Q",
140+
y2="label_y:Q",
141+
tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")],
142+
)
143+
)
144+
145+
# Two-line peak labels at recommended sizes (name 18, elevation 15 — meets tick-floor)
146+
name_labels = (
147+
alt.Chart(others)
148+
.mark_text(align="center", baseline="bottom", fontSize=18, fontWeight="bold", color=INK, dy=-26)
149+
.encode(
150+
x="angle_deg:Q",
151+
y="label_y:Q",
152+
text="name:N",
153+
tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")],
154+
)
155+
)
156+
elev_labels = (
157+
alt.Chart(others)
158+
.mark_text(align="center", baseline="bottom", fontSize=15, color=INK_SOFT, dy=-8)
159+
.encode(
160+
x="angle_deg:Q",
161+
y="label_y:Q",
162+
text="elev_label:N",
163+
tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")],
164+
)
165+
)
166+
167+
# Matterhorn focal accent: notably larger label so the anchor summit reads as the composition's focus
168+
matterhorn_name = (
169+
alt.Chart(matterhorn)
170+
.mark_text(align="center", baseline="bottom", fontSize=26, fontWeight="bold", color=INK, dy=-30)
171+
.encode(
172+
x="angle_deg:Q",
173+
y="label_y:Q",
174+
text="name:N",
175+
tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")],
176+
)
177+
)
178+
matterhorn_elev = (
179+
alt.Chart(matterhorn)
180+
.mark_text(align="center", baseline="bottom", fontSize=18, fontWeight="bold", color=INK_SOFT, dy=-8)
181+
.encode(
182+
x="angle_deg:Q",
183+
y="label_y:Q",
184+
text="elev_label:N",
185+
tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")],
186+
)
187+
)
188+
189+
chart = (
190+
(sky + silhouette + leaders + matterhorn_leader + name_labels + elev_labels + matterhorn_name + matterhorn_elev)
191+
.properties(
192+
width=1600,
193+
height=900,
194+
title=alt.Title(
195+
"Wallis Panorama · area-mountain-panorama · altair · anyplot.ai",
196+
subtitle="Sixteen 4000-m summits along a 180° horizontal sweep, Valais Alps",
197+
subtitleColor=INK_SOFT,
198+
subtitleFontSize=18,
199+
fontSize=28,
200+
anchor="start",
201+
offset=18,
202+
color=INK,
203+
),
204+
background=PAGE_BG,
205+
)
206+
.configure_view(fill=PAGE_BG, stroke=None)
207+
.configure_axis(
208+
domainColor=INK_SOFT,
209+
tickColor=INK_SOFT,
210+
gridColor=INK,
211+
gridOpacity=0.0,
212+
labelColor=INK_SOFT,
213+
titleColor=INK,
214+
labelFontSize=18,
215+
titleFontSize=22,
216+
tickSize=8,
217+
)
218+
)
219+
220+
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
221+
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)