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