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