|
| 1 | +""" anyplot.ai |
| 2 | +area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks |
| 3 | +Library: altair 6.1.0 | Python 3.14.4 |
| 4 | +Quality: 93/100 | Created: 2026-04-25 |
| 5 | +""" |
| 6 | + |
| 7 | +import importlib |
| 8 | +import os |
| 9 | +import sys |
| 10 | + |
| 11 | + |
| 12 | +# Drop script directory from sys.path so the `altair` package resolves, not this file |
| 13 | +sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.dirname(os.path.abspath(__file__))] |
| 14 | +alt = importlib.import_module("altair") |
| 15 | +np = importlib.import_module("numpy") |
| 16 | +pd = importlib.import_module("pandas") |
| 17 | + |
| 18 | + |
| 19 | +# Theme tokens (chrome flips with theme; data colors stay constant) |
| 20 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 21 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 22 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 23 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 24 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 25 | +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
| 26 | +BRAND = "#009E73" # Okabe-Ito position 1 — silhouette fill (single data series) |
| 27 | + |
| 28 | +# Theme-adaptive dusk sky gradient (chrome layer above the ridgeline; spec-authorized) |
| 29 | +SKY_HORIZON = "#FFC58A" if THEME == "light" else "#5A3422" # warm dusk glow at ridgeline |
| 30 | +SKY_MID = "#D89AA8" if THEME == "light" else "#2E1F35" # twilight rose / deep plum |
| 31 | +SKY_ZENITH = "#5C5078" if THEME == "light" else "#0C0E1A" # evening blue / night sky |
| 32 | + |
| 33 | +# Data — Wallis (Valais, CH) panorama: 16 4000-m summits along a 180° sweep |
| 34 | +peaks = pd.DataFrame( |
| 35 | + [ |
| 36 | + ("Weisshorn", 4506, 9), |
| 37 | + ("Zinalrothorn", 4221, 20), |
| 38 | + ("Ober Gabelhorn", 4063, 30), |
| 39 | + ("Dent Blanche", 4358, 42), |
| 40 | + ("Matterhorn", 4478, 56), |
| 41 | + ("Breithorn", 4164, 73), |
| 42 | + ("Pollux", 4092, 81), |
| 43 | + ("Castor", 4223, 88), |
| 44 | + ("Liskamm", 4527, 97), |
| 45 | + ("Monte Rosa", 4634, 109), |
| 46 | + ("Strahlhorn", 4190, 122), |
| 47 | + ("Rimpfischhorn", 4199, 132), |
| 48 | + ("Allalinhorn", 4027, 140), |
| 49 | + ("Alphubel", 4206, 148), |
| 50 | + ("Täschhorn", 4491, 158), |
| 51 | + ("Dom", 4545, 168), |
| 52 | + ], |
| 53 | + columns=["name", "elevation_m", "angle_deg"], |
| 54 | +) |
| 55 | + |
| 56 | +# Skyline ridge — gaussians around named peaks plus naturalistic minor ridge texture |
| 57 | +np.random.seed(42) |
| 58 | +angles = np.linspace(-2, 182, 1500) |
| 59 | +ridge_elev = 2950 + 110 * np.sin(angles * 0.11) + 35 * np.sin(angles * 0.43 + 1.1) |
| 60 | + |
| 61 | +for _ in range(55): |
| 62 | + pos = np.random.uniform(-2, 182) |
| 63 | + height = np.random.uniform(150, 480) |
| 64 | + width = np.random.uniform(1.4, 3.0) |
| 65 | + ridge_elev = np.maximum(ridge_elev, 2950 + height * np.exp(-((angles - pos) ** 2) / (2 * width**2))) |
| 66 | + |
| 67 | +for _, row in peaks.iterrows(): |
| 68 | + height = row["elevation_m"] - 2950 |
| 69 | + width = 2.0 + (row["elevation_m"] - 4000) * 0.0007 |
| 70 | + ridge_elev = np.maximum(ridge_elev, 2950 + height * np.exp(-((angles - row["angle_deg"]) ** 2) / (2 * width**2))) |
| 71 | + |
| 72 | +ridge = pd.DataFrame({"angle_deg": angles, "elevation_m": ridge_elev}) |
| 73 | + |
| 74 | +# Stagger label heights so adjacent peaks don't collide; Matterhorn lifted as focal summit |
| 75 | +peaks = peaks.sort_values("angle_deg").reset_index(drop=True) |
| 76 | +LABEL_HIGH = 5800 |
| 77 | +LABEL_LOW = 5400 |
| 78 | +peaks["label_y"] = [LABEL_HIGH if i % 2 == 0 else LABEL_LOW for i in range(len(peaks))] |
| 79 | +peaks.loc[peaks["name"] == "Matterhorn", "label_y"] = 6000 |
| 80 | +peaks["elev_label"] = peaks["elevation_m"].apply(lambda v: f"{v:.0f} m") |
| 81 | + |
| 82 | +matterhorn = peaks[peaks["name"] == "Matterhorn"] |
| 83 | +others = peaks[peaks["name"] != "Matterhorn"] |
| 84 | + |
| 85 | +# Shared scales / axis so all layers register on the same coordinate system |
| 86 | +X_SCALE = alt.Scale(domain=[0, 180]) |
| 87 | +Y_SCALE = alt.Scale(domain=[2900, 6300]) |
| 88 | +Y_AXIS = alt.Axis(values=[3000, 3500, 4000, 4500, 5000]) |
| 89 | + |
| 90 | +# Sky — dusk vertical gradient covering the full plot area; silhouette will mask the lower half |
| 91 | +sky_df = pd.DataFrame({"x_min": [0], "x_max": [180], "y_min": [2900], "y_max": [6300]}) |
| 92 | +sky = ( |
| 93 | + alt.Chart(sky_df) |
| 94 | + .mark_rect( |
| 95 | + color={ |
| 96 | + "x1": 0, |
| 97 | + "y1": 0, |
| 98 | + "x2": 0, |
| 99 | + "y2": 1, |
| 100 | + "gradient": "linear", |
| 101 | + "stops": [ |
| 102 | + {"offset": 0.0, "color": SKY_ZENITH}, |
| 103 | + {"offset": 0.55, "color": SKY_MID}, |
| 104 | + {"offset": 1.0, "color": SKY_HORIZON}, |
| 105 | + ], |
| 106 | + } |
| 107 | + ) |
| 108 | + .encode( |
| 109 | + x=alt.X("x_min:Q", scale=X_SCALE, axis=None), |
| 110 | + x2="x_max:Q", |
| 111 | + y=alt.Y("y_min:Q", scale=Y_SCALE, title="Elevation (m)", axis=Y_AXIS), |
| 112 | + y2="y_max:Q", |
| 113 | + ) |
| 114 | +) |
| 115 | + |
| 116 | +# Silhouette — brand-green photo-like fill; ridge stroke gives the snow-edge alpenglow line |
| 117 | +silhouette = ( |
| 118 | + alt.Chart(ridge) |
| 119 | + .mark_area(color=BRAND, line={"color": BRAND, "strokeWidth": 2.5}, opacity=1.0) |
| 120 | + .encode(x="angle_deg:Q", y="elevation_m:Q") |
| 121 | +) |
| 122 | + |
| 123 | +# Leader lines from summit up to label position (with tooltip for HTML hover) |
| 124 | +leaders = ( |
| 125 | + alt.Chart(others) |
| 126 | + .mark_rule(strokeWidth=1.0, opacity=0.55, color=INK_SOFT) |
| 127 | + .encode( |
| 128 | + x="angle_deg:Q", |
| 129 | + y="elevation_m:Q", |
| 130 | + y2="label_y:Q", |
| 131 | + tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], |
| 132 | + ) |
| 133 | +) |
| 134 | +matterhorn_leader = ( |
| 135 | + alt.Chart(matterhorn) |
| 136 | + .mark_rule(strokeWidth=2.0, opacity=0.9, color=INK) |
| 137 | + .encode( |
| 138 | + x="angle_deg:Q", |
| 139 | + y="elevation_m:Q", |
| 140 | + y2="label_y:Q", |
| 141 | + tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], |
| 142 | + ) |
| 143 | +) |
| 144 | + |
| 145 | +# Two-line peak labels at recommended sizes (name 18, elevation 15 — meets tick-floor) |
| 146 | +name_labels = ( |
| 147 | + alt.Chart(others) |
| 148 | + .mark_text(align="center", baseline="bottom", fontSize=18, fontWeight="bold", color=INK, dy=-26) |
| 149 | + .encode( |
| 150 | + x="angle_deg:Q", |
| 151 | + y="label_y:Q", |
| 152 | + text="name:N", |
| 153 | + tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], |
| 154 | + ) |
| 155 | +) |
| 156 | +elev_labels = ( |
| 157 | + alt.Chart(others) |
| 158 | + .mark_text(align="center", baseline="bottom", fontSize=15, color=INK_SOFT, dy=-8) |
| 159 | + .encode( |
| 160 | + x="angle_deg:Q", |
| 161 | + y="label_y:Q", |
| 162 | + text="elev_label:N", |
| 163 | + tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], |
| 164 | + ) |
| 165 | +) |
| 166 | + |
| 167 | +# Matterhorn focal accent: notably larger label so the anchor summit reads as the composition's focus |
| 168 | +matterhorn_name = ( |
| 169 | + alt.Chart(matterhorn) |
| 170 | + .mark_text(align="center", baseline="bottom", fontSize=26, fontWeight="bold", color=INK, dy=-30) |
| 171 | + .encode( |
| 172 | + x="angle_deg:Q", |
| 173 | + y="label_y:Q", |
| 174 | + text="name:N", |
| 175 | + tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], |
| 176 | + ) |
| 177 | +) |
| 178 | +matterhorn_elev = ( |
| 179 | + alt.Chart(matterhorn) |
| 180 | + .mark_text(align="center", baseline="bottom", fontSize=18, fontWeight="bold", color=INK_SOFT, dy=-8) |
| 181 | + .encode( |
| 182 | + x="angle_deg:Q", |
| 183 | + y="label_y:Q", |
| 184 | + text="elev_label:N", |
| 185 | + tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], |
| 186 | + ) |
| 187 | +) |
| 188 | + |
| 189 | +chart = ( |
| 190 | + (sky + silhouette + leaders + matterhorn_leader + name_labels + elev_labels + matterhorn_name + matterhorn_elev) |
| 191 | + .properties( |
| 192 | + width=1600, |
| 193 | + height=900, |
| 194 | + title=alt.Title( |
| 195 | + "Wallis Panorama · area-mountain-panorama · altair · anyplot.ai", |
| 196 | + subtitle="Sixteen 4000-m summits along a 180° horizontal sweep, Valais Alps", |
| 197 | + subtitleColor=INK_SOFT, |
| 198 | + subtitleFontSize=18, |
| 199 | + fontSize=28, |
| 200 | + anchor="start", |
| 201 | + offset=18, |
| 202 | + color=INK, |
| 203 | + ), |
| 204 | + background=PAGE_BG, |
| 205 | + ) |
| 206 | + .configure_view(fill=PAGE_BG, stroke=None) |
| 207 | + .configure_axis( |
| 208 | + domainColor=INK_SOFT, |
| 209 | + tickColor=INK_SOFT, |
| 210 | + gridColor=INK, |
| 211 | + gridOpacity=0.0, |
| 212 | + labelColor=INK_SOFT, |
| 213 | + titleColor=INK, |
| 214 | + labelFontSize=18, |
| 215 | + titleFontSize=22, |
| 216 | + tickSize=8, |
| 217 | + ) |
| 218 | +) |
| 219 | + |
| 220 | +chart.save(f"plot-{THEME}.png", scale_factor=3.0) |
| 221 | +chart.save(f"plot-{THEME}.html") |
0 commit comments