Skip to content

Commit aabd133

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

2 files changed

Lines changed: 447 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
""" anyplot.ai
2+
area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks
3+
Library: plotly 6.7.0 | Python 3.14.4
4+
Quality: 89/100 | Created: 2026-04-25
5+
"""
6+
7+
import os
8+
9+
import numpy as np
10+
import plotly.graph_objects as go
11+
12+
13+
# Theme tokens
14+
THEME = os.getenv("ANYPLOT_THEME", "light")
15+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
16+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
17+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
18+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
19+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
20+
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
21+
BRAND = "#009E73"
22+
23+
# Data — Wallis (Valais) panorama: peaks ordered along a 0–180° angular sweep
24+
peaks = [
25+
("Weisshorn", 8, 4506),
26+
("Zinalrothorn", 20, 4221),
27+
("Ober Gabelhorn", 31, 4063),
28+
("Dent Blanche", 42, 4358),
29+
("Matterhorn", 58, 4478),
30+
("Breithorn", 72, 4164),
31+
("Pollux", 81, 4092),
32+
("Castor", 89, 4223),
33+
("Liskamm", 97, 4527),
34+
("Dufourspitze", 109, 4634),
35+
("Strahlhorn", 121, 4190),
36+
("Rimpfischhorn", 132, 4199),
37+
("Allalinhorn", 142, 4027),
38+
("Alphubel", 152, 4206),
39+
("Täschhorn", 162, 4491),
40+
("Dom", 174, 4545),
41+
]
42+
43+
# Build ridgeline control points: peaks alternating with saddles (cols)
44+
np.random.seed(42)
45+
ctrl_x = [-3.0]
46+
ctrl_y = [3250.0]
47+
for i, (_, ang, el) in enumerate(peaks):
48+
ctrl_x.append(float(ang))
49+
ctrl_y.append(float(el))
50+
if i < len(peaks) - 1:
51+
next_ang = peaks[i + 1][1]
52+
next_el = peaks[i + 1][2]
53+
col_ang = (ang + next_ang) / 2 + np.random.uniform(-1.2, 1.2)
54+
col_drop = np.random.uniform(420, 820)
55+
col_el = min(el, next_el) - col_drop
56+
ctrl_x.append(float(col_ang))
57+
ctrl_y.append(float(col_el))
58+
ctrl_x.append(184.0)
59+
ctrl_y.append(3350.0)
60+
61+
ctrl_x = np.array(ctrl_x)
62+
ctrl_y = np.array(ctrl_y)
63+
64+
# Smooth ridgeline via cosine smoothstep between adjacent control points
65+
ridge_x = []
66+
ridge_y = []
67+
for i in range(len(ctrl_x) - 1):
68+
n = 80
69+
last = i == len(ctrl_x) - 2
70+
t = np.linspace(0.0, 1.0, n, endpoint=last)
71+
s = 0.5 - 0.5 * np.cos(np.pi * t)
72+
ridge_x.append(ctrl_x[i] + (ctrl_x[i + 1] - ctrl_x[i]) * t)
73+
ridge_y.append(ctrl_y[i] + (ctrl_y[i + 1] - ctrl_y[i]) * s)
74+
ridge_x = np.concatenate(ridge_x)
75+
ridge_y = np.concatenate(ridge_y)
76+
77+
# Anchor the silhouette polygon at the lower edge of the visible y-range
78+
Y_FLOOR = 2500
79+
poly_x = np.concatenate([[ridge_x[0]], ridge_x, [ridge_x[-1]]])
80+
poly_y = np.concatenate([[Y_FLOOR], ridge_y, [Y_FLOOR]])
81+
82+
# Plot
83+
fig = go.Figure()
84+
85+
# Mountain silhouette (first categorical series — brand green)
86+
fig.add_trace(
87+
go.Scatter(
88+
x=poly_x,
89+
y=poly_y,
90+
mode="lines",
91+
line={"color": BRAND, "width": 2},
92+
fill="toself",
93+
fillcolor=BRAND,
94+
hoverinfo="skip",
95+
showlegend=False,
96+
)
97+
)
98+
99+
# Leader lines + peak markers + labels
100+
LEVEL_TIERS = [4880, 5040, 5200]
101+
annotations = []
102+
for i, (name, ang, el) in enumerate(peaks):
103+
label_y = LEVEL_TIERS[i % 3]
104+
is_focal = name == "Matterhorn"
105+
106+
# Leader line (thin, theme-adaptive)
107+
fig.add_trace(
108+
go.Scatter(
109+
x=[ang, ang],
110+
y=[el + 25, label_y - 80],
111+
mode="lines",
112+
line={"color": INK_SOFT, "width": 1},
113+
hoverinfo="skip",
114+
showlegend=False,
115+
)
116+
)
117+
118+
# Summit dot (slightly larger for the focal peak)
119+
fig.add_trace(
120+
go.Scatter(
121+
x=[ang],
122+
y=[el],
123+
mode="markers",
124+
marker={"size": 10 if is_focal else 6, "color": INK if is_focal else INK_SOFT, "line": {"width": 0}},
125+
hoverinfo="skip",
126+
showlegend=False,
127+
)
128+
)
129+
130+
# Label: name on top, elevation below
131+
name_size = 17 if is_focal else 14
132+
weight = "700" if is_focal else "600"
133+
annotations.append(
134+
{
135+
"x": ang,
136+
"y": label_y,
137+
"text": (
138+
f"<span style='font-size:{name_size}px;font-weight:{weight};color:{INK}'>{name}</span><br>"
139+
f"<span style='font-size:13px;color:{INK_MUTED}'>{el:,} m</span>"
140+
),
141+
"showarrow": False,
142+
"align": "center",
143+
"xanchor": "center",
144+
"yanchor": "middle",
145+
}
146+
)
147+
148+
# Subtitle / footnote
149+
annotations.append(
150+
{
151+
"x": 0.5,
152+
"y": 1.08,
153+
"xref": "paper",
154+
"yref": "paper",
155+
"text": f"<span style='color:{INK_SOFT}'>Wallis panorama — sixteen 4 000 m peaks of the Pennine Alps</span>",
156+
"showarrow": False,
157+
"font": {"size": 18},
158+
"xanchor": "center",
159+
}
160+
)
161+
162+
fig.update_layout(
163+
title={
164+
"text": "area-mountain-panorama · plotly · anyplot.ai",
165+
"font": {"size": 28, "color": INK},
166+
"x": 0.5,
167+
"xanchor": "center",
168+
"y": 0.96,
169+
},
170+
annotations=annotations,
171+
paper_bgcolor=PAGE_BG,
172+
plot_bgcolor=PAGE_BG,
173+
font={"color": INK},
174+
xaxis={
175+
"range": [-3, 184],
176+
"showgrid": False,
177+
"showticklabels": False,
178+
"ticks": "",
179+
"zeroline": False,
180+
"showline": False,
181+
"fixedrange": True,
182+
},
183+
yaxis={
184+
"title": {"text": "Elevation (m)", "font": {"size": 22, "color": INK}},
185+
"tickfont": {"size": 18, "color": INK_SOFT},
186+
"tickvals": [2500, 3000, 3500, 4000, 4500, 5000],
187+
"ticksuffix": " ",
188+
"gridcolor": GRID,
189+
"linecolor": INK_SOFT,
190+
"showgrid": True,
191+
"zeroline": False,
192+
"showline": False,
193+
"ticks": "",
194+
"range": [Y_FLOOR, 5400],
195+
},
196+
margin={"l": 120, "r": 80, "t": 160, "b": 60},
197+
)
198+
199+
fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3)
200+
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

0 commit comments

Comments
 (0)