Skip to content

Commit d9345d9

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

2 files changed

Lines changed: 399 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
""" anyplot.ai
2+
area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks
3+
Library: letsplot 4.9.0 | Python 3.14.4
4+
Quality: 86/100 | Created: 2026-04-25
5+
"""
6+
7+
import os
8+
9+
import numpy as np
10+
import pandas as pd
11+
from lets_plot import (
12+
LetsPlot,
13+
aes,
14+
element_blank,
15+
element_line,
16+
element_rect,
17+
element_text,
18+
geom_area,
19+
geom_segment,
20+
geom_text,
21+
ggplot,
22+
ggsize,
23+
labs,
24+
scale_x_continuous,
25+
scale_y_continuous,
26+
theme,
27+
theme_minimal,
28+
)
29+
from lets_plot.export import ggsave
30+
31+
32+
LetsPlot.setup_html()
33+
34+
# Theme tokens
35+
THEME = os.getenv("ANYPLOT_THEME", "light")
36+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
37+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
38+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
39+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
40+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
41+
RULE = "#D6D3C7" if THEME == "light" else "#3A3A34"
42+
BRAND = "#009E73"
43+
44+
# Data — Wallis (Valais, Switzerland) panorama anchored on the Matterhorn
45+
peak_records = [
46+
("Matterhorn", 22, 4478),
47+
("Dent Blanche", 46, 4358),
48+
("Ober Gabelhorn", 64, 4063),
49+
("Zinalrothorn", 80, 4221),
50+
("Weisshorn", 96, 4506),
51+
("Dom", 122, 4545),
52+
("Täschhorn", 132, 4491),
53+
("Alphubel", 144, 4206),
54+
("Allalinhorn", 156, 4027),
55+
("Rimpfischhorn", 170, 4199),
56+
("Strahlhorn", 184, 4190),
57+
("Monte Rosa", 212, 4634),
58+
("Liskamm", 230, 4527),
59+
("Castor", 244, 4223),
60+
("Pollux", 252, 4092),
61+
("Breithorn", 268, 4164),
62+
]
63+
peaks_df = pd.DataFrame(peak_records, columns=["name", "angle", "elev"])
64+
65+
# Skyline — sum-of-Gaussians around peaks plus minor inter-peak ridges + organic noise
66+
np.random.seed(42)
67+
n_samples = 1600
68+
angle = np.linspace(0, 290, n_samples)
69+
base_elev = 3000.0
70+
71+
skyline = np.full_like(angle, base_elev)
72+
for _, p in peaks_df.iterrows():
73+
bell = base_elev + (p["elev"] - base_elev) * np.exp(-((angle - p["angle"]) ** 2) / (2 * 7.0**2))
74+
skyline = np.maximum(skyline, bell)
75+
76+
minor_offsets = [9, 33, 56, 73, 88, 108, 138, 162, 198, 224, 258, 280]
77+
for off in minor_offsets:
78+
h = 3450 + np.random.uniform(-180, 200)
79+
bell = base_elev + (h - base_elev) * np.exp(-((angle - off) ** 2) / (2 * 4.0**2))
80+
skyline = np.maximum(skyline, bell)
81+
82+
skyline = skyline + np.cumsum(np.random.randn(n_samples)) * 0.6
83+
skyline_df = pd.DataFrame({"angle": angle, "elev": skyline})
84+
85+
# Stagger labels into three rows — name labels are wide, so use a generous gap
86+
label_rows = [5650, 5350, 5050]
87+
min_dx = 26
88+
placed = []
89+
label_y_values = []
90+
for _, p in peaks_df.iterrows():
91+
chosen = label_rows[-1]
92+
for ry in label_rows:
93+
conflict = any(abs(p["angle"] - pa) < min_dx and pr == ry for pa, pr in placed)
94+
if not conflict:
95+
chosen = ry
96+
break
97+
label_y_values.append(chosen)
98+
placed.append((p["angle"], chosen))
99+
peaks_df["label_y"] = label_y_values
100+
peaks_df["elev_y"] = peaks_df["label_y"] - 140
101+
peaks_df["elev_text"] = peaks_df["elev"].astype(str) + " m"
102+
103+
# Highlight the anchor summit (Matterhorn) typographically
104+
anchor_mask = peaks_df["name"] == "Matterhorn"
105+
anchor_df = peaks_df[anchor_mask]
106+
other_df = peaks_df[~anchor_mask]
107+
108+
# Compass bearing ticks — vantage point ~Gornergrat sweeping WSW → ENE
109+
compass_breaks = [22, 90, 160, 230, 280]
110+
compass_labels = ["WSW", "W", "NW", "N", "NE"]
111+
112+
plot = (
113+
ggplot()
114+
# Mountain silhouette
115+
+ geom_area(data=skyline_df, mapping=aes(x="angle", y="elev"), fill=BRAND, color=BRAND, size=0.6, alpha=1.0)
116+
# Leader lines from each summit up to its label
117+
+ geom_segment(
118+
data=peaks_df, mapping=aes(x="angle", y="elev", xend="angle", yend="label_y"), color=INK_SOFT, size=0.4
119+
)
120+
# Peak names — non-anchor
121+
+ geom_text(
122+
data=other_df, mapping=aes(x="angle", y="label_y", label="name"), size=7, color=INK, fontface="bold", vjust=0.0
123+
)
124+
# Peak names — Matterhorn anchor (slightly larger for visual emphasis)
125+
+ geom_text(
126+
data=anchor_df, mapping=aes(x="angle", y="label_y", label="name"), size=9, color=INK, fontface="bold", vjust=0.0
127+
)
128+
# Elevation under each name
129+
+ geom_text(data=peaks_df, mapping=aes(x="angle", y="elev_y", label="elev_text"), size=6, color=INK_SOFT, vjust=0.0)
130+
+ scale_x_continuous(name="Bearing", breaks=compass_breaks, labels=compass_labels, limits=[0, 290], expand=[0, 0])
131+
+ scale_y_continuous(name="Elevation (m)", breaks=[3000, 3500, 4000, 4500], limits=[2800, 6000], expand=[0, 0])
132+
+ labs(
133+
title="Wallis Panorama from Gornergrat · area-mountain-panorama · letsplot · anyplot.ai",
134+
subtitle="Skyline of the Pennine Alps with 16 labeled 4000-m summits",
135+
)
136+
+ ggsize(1600, 900)
137+
+ theme_minimal()
138+
+ theme(
139+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
140+
panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
141+
panel_grid_major_y=element_line(color=RULE, size=0.3),
142+
panel_grid_major_x=element_blank(),
143+
panel_grid_minor=element_blank(),
144+
axis_line_x=element_line(color=INK_SOFT, size=0.5),
145+
axis_line_y=element_blank(),
146+
axis_ticks_x=element_line(color=INK_SOFT),
147+
axis_ticks_y=element_blank(),
148+
axis_title=element_text(size=20, color=INK),
149+
axis_text=element_text(size=16, color=INK_SOFT),
150+
plot_title=element_text(size=24, color=INK),
151+
plot_subtitle=element_text(size=16, color=INK_MUTED),
152+
plot_margin=[40, 40, 20, 20],
153+
)
154+
)
155+
156+
ggsave(plot, f"plot-{THEME}.png", path=".", scale=3)
157+
ggsave(plot, f"plot-{THEME}.html", path=".")

0 commit comments

Comments
 (0)