Skip to content

Commit b0ea9b0

Browse files
feat(seaborn): implement area-mountain-panorama (#5369)
## Implementation: `area-mountain-panorama` - python/seaborn Implements the **python/seaborn** version of `area-mountain-panorama`. **File:** `plots/area-mountain-panorama/implementations/python/seaborn.py` **Parent Issue:** #5365 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/24918698887)* --------- 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 c58fdc1 commit b0ea9b0

2 files changed

Lines changed: 420 additions & 0 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
""" anyplot.ai
2+
area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks
3+
Library: seaborn 0.13.2 | Python 3.14.4
4+
Quality: 88/100 | Created: 2026-04-25
5+
"""
6+
7+
import os
8+
9+
import matplotlib.pyplot as plt
10+
import numpy as np
11+
import pandas as pd
12+
import seaborn as sns
13+
14+
15+
# Theme tokens
16+
THEME = os.getenv("ANYPLOT_THEME", "light")
17+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
18+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
19+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
20+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
21+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
22+
BRAND = "#009E73"
23+
24+
sns.set_theme(
25+
style="ticks",
26+
rc={
27+
"figure.facecolor": PAGE_BG,
28+
"axes.facecolor": PAGE_BG,
29+
"axes.edgecolor": INK_SOFT,
30+
"axes.labelcolor": INK,
31+
"text.color": INK,
32+
"xtick.color": INK_SOFT,
33+
"ytick.color": INK_SOFT,
34+
"grid.color": INK,
35+
"grid.alpha": 0.10,
36+
},
37+
)
38+
39+
# Data — Wallis (Valais, Switzerland) panorama from Gornergrat, west → east sweep.
40+
# `sigma` is the angular half-width that shapes how broad each summit reads on
41+
# the silhouette: smaller = sharper, more iconic profile.
42+
peaks = pd.DataFrame(
43+
[
44+
{"name": "Weisshorn", "angle_deg": 10.0, "elevation_m": 4506, "sigma": 4.6},
45+
{"name": "Zinalrothorn", "angle_deg": 22.0, "elevation_m": 4221, "sigma": 4.2},
46+
{"name": "Ober Gabelhorn", "angle_deg": 32.0, "elevation_m": 4063, "sigma": 5.4},
47+
{"name": "Dent Blanche", "angle_deg": 44.0, "elevation_m": 4358, "sigma": 4.6},
48+
{"name": "Matterhorn", "angle_deg": 62.0, "elevation_m": 4478, "sigma": 3.0},
49+
{"name": "Breithorn", "angle_deg": 82.0, "elevation_m": 4164, "sigma": 7.0},
50+
{"name": "Pollux", "angle_deg": 92.0, "elevation_m": 4092, "sigma": 3.6},
51+
{"name": "Castor", "angle_deg": 99.0, "elevation_m": 4223, "sigma": 3.6},
52+
{"name": "Liskamm", "angle_deg": 110.0, "elevation_m": 4527, "sigma": 6.5},
53+
{"name": "Dufourspitze", "angle_deg": 124.0, "elevation_m": 4634, "sigma": 5.2},
54+
{"name": "Strahlhorn", "angle_deg": 142.0, "elevation_m": 4190, "sigma": 5.0},
55+
{"name": "Rimpfischhorn", "angle_deg": 152.0, "elevation_m": 4199, "sigma": 4.4},
56+
{"name": "Allalinhorn", "angle_deg": 161.0, "elevation_m": 4027, "sigma": 5.2},
57+
{"name": "Alphubel", "angle_deg": 171.0, "elevation_m": 4206, "sigma": 4.6},
58+
{"name": "Täschhorn", "angle_deg": 181.0, "elevation_m": 4491, "sigma": 3.6},
59+
{"name": "Dom", "angle_deg": 191.0, "elevation_m": 4545, "sigma": 3.4},
60+
]
61+
)
62+
63+
# Build the skyline as the upper envelope of per-peak Gaussian bumps over a
64+
# slowly undulating valley floor. This produces distinct peak shapes
65+
# (sharper for iconic summits, broader for massif shoulders) instead of the
66+
# uniform scallop pattern a global spline would give.
67+
np.random.seed(42)
68+
sample_angles = np.linspace(-5.0, 205.0, 1800)
69+
70+
valley_floor = 2950 + 90 * np.sin(sample_angles * np.pi / 95.0 + 0.4) + 55 * np.cos(sample_angles * np.pi / 47.0 + 1.1)
71+
72+
ridge = np.copy(valley_floor)
73+
for _, row in peaks.iterrows():
74+
floor_at_peak = valley_floor[np.argmin(np.abs(sample_angles - row["angle_deg"]))]
75+
bump_height = row["elevation_m"] - floor_at_peak
76+
bump = bump_height * np.exp(-0.5 * ((sample_angles - row["angle_deg"]) / row["sigma"]) ** 2)
77+
ridge = np.maximum(ridge, valley_floor + bump)
78+
79+
# High-frequency rocky texture; tapered so edges blend into the valley floor
80+
texture = (
81+
35 * np.sin(sample_angles * 1.7 + 0.3)
82+
+ 22 * np.sin(sample_angles * 3.1 + 1.7)
83+
+ np.random.normal(0, 14, size=sample_angles.shape)
84+
)
85+
edge_taper = np.clip((sample_angles - 0) / 6, 0, 1) * np.clip((200 - sample_angles) / 6, 0, 1)
86+
ridge = ridge + texture * edge_taper
87+
88+
skyline = pd.DataFrame({"angle_deg": sample_angles, "elevation_m": ridge})
89+
90+
# Plot
91+
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
92+
ax.set_facecolor(PAGE_BG)
93+
94+
Y_FLOOR = 2500
95+
LABEL_BASE_Y = 5150
96+
LABEL_STAGGER = 360
97+
98+
# Filled silhouette
99+
ax.fill_between(skyline["angle_deg"], skyline["elevation_m"], Y_FLOOR, color=BRAND, alpha=1.0, linewidth=0, zorder=2)
100+
sns.lineplot(data=skyline, x="angle_deg", y="elevation_m", color=BRAND, linewidth=1.6, ax=ax, legend=False)
101+
102+
# Peak labels with leader lines, alternating heights to avoid overlap
103+
for i, row in peaks.iterrows():
104+
is_anchor = row["name"] == "Matterhorn"
105+
label_y = LABEL_BASE_Y + (LABEL_STAGGER if i % 2 == 0 else 0)
106+
leader_top = label_y - 80
107+
108+
ax.plot(
109+
[row["angle_deg"], row["angle_deg"]],
110+
[row["elevation_m"], leader_top],
111+
color=INK_SOFT,
112+
linewidth=1.0,
113+
alpha=0.65,
114+
zorder=3,
115+
)
116+
117+
ax.text(
118+
row["angle_deg"],
119+
label_y,
120+
row["name"],
121+
fontsize=15 if is_anchor else 13,
122+
fontweight="semibold" if is_anchor else "regular",
123+
color=INK,
124+
ha="center",
125+
va="bottom",
126+
zorder=4,
127+
)
128+
ax.text(
129+
row["angle_deg"],
130+
label_y - 195,
131+
f"{int(row['elevation_m'])} m",
132+
fontsize=11,
133+
color=INK_MUTED,
134+
ha="center",
135+
va="bottom",
136+
zorder=4,
137+
)
138+
139+
# Highlight Matterhorn summit as the focal anchor
140+
matterhorn = peaks.loc[peaks["name"] == "Matterhorn"].iloc[0]
141+
ax.scatter(
142+
[matterhorn["angle_deg"]],
143+
[matterhorn["elevation_m"]],
144+
s=110,
145+
color=PAGE_BG,
146+
edgecolor=BRAND,
147+
linewidth=2.8,
148+
zorder=6,
149+
)
150+
151+
# Style
152+
ax.set_xlim(0, 200)
153+
ax.set_ylim(Y_FLOOR, LABEL_BASE_Y + LABEL_STAGGER + 600)
154+
ax.set_xlabel("Compass bearing", fontsize=20, color=INK)
155+
ax.set_ylabel("Elevation (m)", fontsize=20, color=INK)
156+
ax.set_title(
157+
"Wallis 4000ers from Gornergrat · area-mountain-panorama · seaborn · anyplot.ai",
158+
fontsize=24,
159+
fontweight="medium",
160+
color=INK,
161+
pad=18,
162+
)
163+
164+
ax.set_xticks([0, 50, 100, 150, 200])
165+
ax.set_xticklabels(["W", "SW", "S", "SE", "E"])
166+
ax.tick_params(axis="x", labelsize=16, colors=INK_SOFT, length=0)
167+
ax.tick_params(axis="y", labelsize=16, colors=INK_SOFT, length=0)
168+
169+
ax.spines["top"].set_visible(False)
170+
ax.spines["right"].set_visible(False)
171+
ax.spines["left"].set_color(INK_SOFT)
172+
ax.spines["bottom"].set_color(INK_SOFT)
173+
ax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)
174+
ax.set_axisbelow(True)
175+
176+
plt.tight_layout()
177+
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)

0 commit comments

Comments
 (0)