|
| 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