diff --git a/plots/area-mountain-panorama/implementations/python/altair.py b/plots/area-mountain-panorama/implementations/python/altair.py index 761d78467c..2af1effafe 100644 --- a/plots/area-mountain-panorama/implementations/python/altair.py +++ b/plots/area-mountain-panorama/implementations/python/altair.py @@ -1,7 +1,7 @@ """ anyplot.ai area-mountain-panorama: Mountain Panorama Profile with Labeled Peaks -Library: altair 6.1.0 | Python 3.14.4 -Quality: 93/100 | Created: 2026-04-25 +Library: altair 6.2.2 | Python 3.13.14 +Quality: 84/100 | Updated: 2026-06-30 """ import importlib @@ -9,86 +9,120 @@ import sys -# Drop script directory from sys.path so the `altair` package resolves, not this file +# Drop script directory from sys.path so `altair` resolves the package, not this file sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.dirname(os.path.abspath(__file__))] alt = importlib.import_module("altair") np = importlib.import_module("numpy") pd = importlib.import_module("pandas") +from PIL import Image -# Theme tokens (chrome flips with theme; data colors stay constant) +# Theme tokens — chrome flips with theme; Imprint palette data colors stay constant THEME = os.getenv("ANYPLOT_THEME", "light") PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" -ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" INK = "#1A1A17" if THEME == "light" else "#F0EFE8" INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" -INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" -BRAND = "#009E73" # Okabe-Ito position 1 — silhouette fill (single data series) +BRAND = "#009E73" # Imprint palette position 1 — silhouette fill (single data series) -# Theme-adaptive dusk sky gradient (chrome layer above the ridgeline; spec-authorized) -SKY_HORIZON = "#FFC58A" if THEME == "light" else "#5A3422" # warm dusk glow at ridgeline -SKY_MID = "#D89AA8" if THEME == "light" else "#2E1F35" # twilight rose / deep plum -SKY_ZENITH = "#5C5078" if THEME == "light" else "#0C0E1A" # evening blue / night sky +# Theme-adaptive dusk sky gradient (spec-authorized chrome above the ridgeline) +SKY_HORIZON = "#FFC58A" if THEME == "light" else "#5A3422" +SKY_MID = "#D89AA8" if THEME == "light" else "#2E1F35" +SKY_ZENITH = "#5C5078" if THEME == "light" else "#0C0E1A" +# Alpenglow rim — warm gold / rose-copper at the sky-to-silhouette boundary +ALPENGLOW = "#FFBA6A" if THEME == "light" else "#C88060" -# Data — Wallis (Valais, CH) panorama: 16 4000-m summits along a 180° sweep +BASE_ELEV = 2950 + +# All 16 major Wallis (Valais, CH) summits across a 180° horizontal sweep. +# left_slope / right_slope: m/degree for piecewise-linear tent flanks. peaks = pd.DataFrame( [ - ("Weisshorn", 4506, 9), - ("Zinalrothorn", 4221, 20), - ("Ober Gabelhorn", 4063, 30), - ("Dent Blanche", 4358, 42), - ("Matterhorn", 4478, 56), - ("Breithorn", 4164, 73), - ("Pollux", 4092, 81), - ("Castor", 4223, 88), - ("Liskamm", 4527, 97), - ("Monte Rosa", 4634, 109), - ("Strahlhorn", 4190, 122), - ("Rimpfischhorn", 4199, 132), - ("Allalinhorn", 4027, 140), - ("Alphubel", 4206, 148), - ("Täschhorn", 4491, 158), - ("Dom", 4545, 168), + ("Weisshorn", 4506, 9, 280, 200), + ("Zinalrothorn", 4221, 22, 180, 250), + ("Ober Gabelhorn", 4063, 30, 220, 180), + ("Dent Blanche", 4358, 42, 200, 300), + ("Matterhorn", 4478, 56, 350, 280), # focal point — steepest flanks + ("Breithorn", 4164, 72, 150, 160), + ("Pollux", 4092, 83, 200, 170), + ("Castor", 4223, 88, 180, 210), + ("Liskamm", 4527, 97, 200, 180), + ("Monte Rosa", 4634, 109, 180, 250), + ("Strahlhorn", 4190, 122, 200, 180), + ("Rimpfischhorn", 4199, 130, 220, 190), + ("Allalinhorn", 4027, 137, 180, 200), + ("Alphubel", 4206, 148, 160, 200), + ("Täschhorn", 4491, 155, 250, 180), + ("Dom", 4545, 168, 200, 280), ], - columns=["name", "elevation_m", "angle_deg"], + columns=["name", "elevation_m", "angle_deg", "left_slope", "right_slope"], ) -# Skyline ridge — gaussians around named peaks plus naturalistic minor ridge texture +# Ridgeline — piecewise-linear tent/triangle functions per spec. +# Spec explicitly forbids Gaussian/bell-curve bumps; each summit uses two linear +# flanks meeting at a sharp apex, with asymmetric slope steepness. np.random.seed(42) angles = np.linspace(-2, 182, 1500) -ridge_elev = 2950 + 110 * np.sin(angles * 0.11) + 35 * np.sin(angles * 0.43 + 1.1) -for _ in range(55): +# Base ridge always at or above BASE_ELEV — positive-only sinusoidal undulation +ridge_elev = BASE_ELEV + np.maximum(0, 70 * np.sin(angles * 0.12) + 22 * np.sin(angles * 0.47 + 1.1)) + +# Rocky inter-peak jaggedness: 65 random tent functions (NOT Gaussian) +for _ in range(65): pos = np.random.uniform(-2, 182) - height = np.random.uniform(150, 480) - width = np.random.uniform(1.4, 3.0) - ridge_elev = np.maximum(ridge_elev, 2950 + height * np.exp(-((angles - pos) ** 2) / (2 * width**2))) + height = np.random.uniform(60, 320) + lslope = np.random.uniform(60, 220) + rslope = np.random.uniform(60, 220) + tent = BASE_ELEV + height - np.where(angles <= pos, lslope * (pos - angles), rslope * (angles - pos)) + ridge_elev = np.maximum(ridge_elev, np.maximum(BASE_ELEV, tent)) +# Named peaks: steep asymmetric tent functions — sharp apex + linear flanks for _, row in peaks.iterrows(): - height = row["elevation_m"] - 2950 - width = 2.0 + (row["elevation_m"] - 4000) * 0.0007 - ridge_elev = np.maximum(ridge_elev, 2950 + height * np.exp(-((angles - row["angle_deg"]) ** 2) / (2 * width**2))) + pos, elev = row["angle_deg"], row["elevation_m"] + tent = elev - np.where(angles <= pos, row["left_slope"] * (pos - angles), row["right_slope"] * (angles - pos)) + ridge_elev = np.maximum(ridge_elev, np.maximum(BASE_ELEV, tent)) ridge = pd.DataFrame({"angle_deg": angles, "elevation_m": ridge_elev}) -# Stagger label heights so adjacent peaks don't collide; Matterhorn lifted as focal summit -peaks = peaks.sort_values("angle_deg").reset_index(drop=True) -LABEL_HIGH = 5800 -LABEL_LOW = 5400 -peaks["label_y"] = [LABEL_HIGH if i % 2 == 0 else LABEL_LOW for i in range(len(peaks))] -peaks.loc[peaks["name"] == "Matterhorn", "label_y"] = 6000 -peaks["elev_label"] = peaks["elevation_m"].apply(lambda v: f"{v:.0f} m") +# Four label tiers assigned by round-robin for maximum same-tier angular separation +# (minimum ~33° within each tier, preventing label collision). +# TIER_A (5300): Weisshorn(9°), Liskamm(97°), Allalinhorn(137°) +# TIER_B (5100): Zinalrothorn(22°), Breithorn(72°), Monte Rosa(109°), Alphubel(148°) +# TIER_C (4900): Ober Gabelhorn(30°), Pollux(83°), Strahlhorn(122°), Täschhorn(155°) +# TIER_D (4700): Dent Blanche(42°), Castor(88°), Rimpfischhorn(130°), Dom(168°) +# MATTERHORN SPECIAL (5500): strongest focal accent +TIER_A, TIER_B, TIER_C, TIER_D, TIER_MAT = 5300, 5100, 4900, 4700, 5500 +label_y_map = { + "Weisshorn": TIER_A, + "Zinalrothorn": TIER_B, + "Ober Gabelhorn": TIER_C, + "Dent Blanche": TIER_D, + "Matterhorn": TIER_MAT, + "Breithorn": TIER_B, + "Pollux": TIER_C, + "Castor": TIER_D, + "Liskamm": TIER_A, + "Monte Rosa": TIER_B, + "Strahlhorn": TIER_C, + "Rimpfischhorn": TIER_D, + "Allalinhorn": TIER_A, + "Alphubel": TIER_B, + "Täschhorn": TIER_C, + "Dom": TIER_D, +} +peaks["label_y"] = peaks["name"].map(label_y_map) +peaks["elev_label"] = peaks["elevation_m"].apply(lambda v: f"{v} m") matterhorn = peaks[peaks["name"] == "Matterhorn"] others = peaks[peaks["name"] != "Matterhorn"] -# Shared scales / axis so all layers register on the same coordinate system +# Coordinate system — only the sky layer carries the explicit scale + axis; +# other layers share it implicitly via Vega-Lite layer scale resolution. X_SCALE = alt.Scale(domain=[0, 180]) -Y_SCALE = alt.Scale(domain=[2900, 6300]) +Y_SCALE = alt.Scale(domain=[2900, 5800]) Y_AXIS = alt.Axis(values=[3000, 3500, 4000, 4500, 5000]) -# Sky — dusk vertical gradient covering the full plot area; silhouette will mask the lower half -sky_df = pd.DataFrame({"x_min": [0], "x_max": [180], "y_min": [2900], "y_max": [6300]}) +# Layer 1: dusk sky gradient (vertical linear, zenith → ridge horizon) +sky_df = pd.DataFrame({"x_min": [0], "x_max": [180], "y_min": [2900], "y_max": [5800]}) sky = ( alt.Chart(sky_df) .mark_rect( @@ -113,92 +147,85 @@ ) ) -# Silhouette — brand-green photo-like fill; ridge stroke gives the snow-edge alpenglow line -silhouette = ( +# Layer 2: mountain silhouette — brand-green filled area below the ridgeline +silhouette = alt.Chart(ridge).mark_area(color=BRAND, opacity=1.0).encode(x="angle_deg:Q", y="elevation_m:Q") + +# Layer 3: alpenglow rim — warm glowing stroke at the sky-to-silhouette boundary +alpenglow = ( alt.Chart(ridge) - .mark_area(color=BRAND, line={"color": BRAND, "strokeWidth": 2.5}, opacity=1.0) + .mark_line(color=ALPENGLOW, strokeWidth=3.5, opacity=0.88) .encode(x="angle_deg:Q", y="elevation_m:Q") ) -# Leader lines from summit up to label position (with tooltip for HTML hover) +_tooltip = [alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")] + +# Layers 4–5: leader lines from summit apex to label anchor leaders = ( alt.Chart(others) .mark_rule(strokeWidth=1.0, opacity=0.55, color=INK_SOFT) - .encode( - x="angle_deg:Q", - y="elevation_m:Q", - y2="label_y:Q", - tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], - ) + .encode(x="angle_deg:Q", y="elevation_m:Q", y2="label_y:Q", tooltip=_tooltip) ) matterhorn_leader = ( alt.Chart(matterhorn) - .mark_rule(strokeWidth=2.0, opacity=0.9, color=INK) - .encode( - x="angle_deg:Q", - y="elevation_m:Q", - y2="label_y:Q", - tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], - ) + .mark_rule(strokeWidth=2.5, opacity=0.9, color=INK) + .encode(x="angle_deg:Q", y="elevation_m:Q", y2="label_y:Q", tooltip=_tooltip) ) -# Two-line peak labels at recommended sizes (name 18, elevation 15 — meets tick-floor) +# Layers 6–7: center-aligned name/elevation labels for all non-Matterhorn peaks name_labels = ( alt.Chart(others) - .mark_text(align="center", baseline="bottom", fontSize=18, fontWeight="bold", color=INK, dy=-26) - .encode( - x="angle_deg:Q", - y="label_y:Q", - text="name:N", - tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], - ) + .mark_text(align="center", baseline="bottom", fontSize=10, fontWeight="bold", color=INK, dy=-22) + .encode(x="angle_deg:Q", y="label_y:Q", text="name:N", tooltip=_tooltip) ) elev_labels = ( alt.Chart(others) - .mark_text(align="center", baseline="bottom", fontSize=15, color=INK_SOFT, dy=-8) - .encode( - x="angle_deg:Q", - y="label_y:Q", - text="elev_label:N", - tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], - ) + .mark_text(align="center", baseline="bottom", fontSize=10, color=INK_SOFT, dy=-6) + .encode(x="angle_deg:Q", y="label_y:Q", text="elev_label:N", tooltip=_tooltip) ) -# Matterhorn focal accent: notably larger label so the anchor summit reads as the composition's focus +# Layers 8–9: Matterhorn focal accent — larger font, heavier weight, composition anchor matterhorn_name = ( alt.Chart(matterhorn) - .mark_text(align="center", baseline="bottom", fontSize=26, fontWeight="bold", color=INK, dy=-30) - .encode( - x="angle_deg:Q", - y="label_y:Q", - text="name:N", - tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], - ) + .mark_text(align="center", baseline="bottom", fontSize=15, fontWeight="bold", color=INK, dy=-28) + .encode(x="angle_deg:Q", y="label_y:Q", text="name:N", tooltip=_tooltip) ) matterhorn_elev = ( alt.Chart(matterhorn) - .mark_text(align="center", baseline="bottom", fontSize=18, fontWeight="bold", color=INK_SOFT, dy=-8) - .encode( - x="angle_deg:Q", - y="label_y:Q", - text="elev_label:N", - tooltip=[alt.Tooltip("name:N", title="Peak"), alt.Tooltip("elevation_m:Q", title="Elevation (m)", format=",d")], - ) + .mark_text(align="center", baseline="bottom", fontSize=12, fontWeight="bold", color=INK_SOFT, dy=-8) + .encode(x="angle_deg:Q", y="label_y:Q", text="elev_label:N", tooltip=_tooltip) ) +title_str = "Wallis Panorama · area-mountain-panorama · python · altair · anyplot.ai" +n = len(title_str) +ratio = 67 / n if n > 67 else 1.0 +title_fs = max(11, round(16 * ratio)) + +# height=190: vl-convert with explicit Y_SCALE on the sky anchor layer adds ~47 CSS px +# of Y overhead per 190 CSS px (source height ≈ 1500px pre-pad, ≤1800px with title). +# DO NOT increase to ≥210 — overhead scales with height and tips over 1800 source px. chart = ( - (sky + silhouette + leaders + matterhorn_leader + name_labels + elev_labels + matterhorn_name + matterhorn_elev) + ( + sky + + silhouette + + alpenglow + + leaders + + matterhorn_leader + + name_labels + + elev_labels + + matterhorn_name + + matterhorn_elev + ) .properties( - width=1600, - height=900, + width=620, + height=190, title=alt.Title( - "Wallis Panorama · area-mountain-panorama · altair · anyplot.ai", + title_str, subtitle="Sixteen 4000-m summits along a 180° horizontal sweep, Valais Alps", subtitleColor=INK_SOFT, - subtitleFontSize=18, - fontSize=28, + subtitleFontSize=13, + fontSize=title_fs, anchor="start", - offset=18, + offset=12, color=INK, ), background=PAGE_BG, @@ -211,11 +238,25 @@ gridOpacity=0.0, labelColor=INK_SOFT, titleColor=INK, - labelFontSize=18, - titleFontSize=22, - tickSize=8, + labelFontSize=10, + titleFontSize=12, + tickSize=5, ) ) -chart.save(f"plot-{THEME}.png", scale_factor=3.0) +chart.save(f"plot-{THEME}.png", scale_factor=4.0) chart.save(f"plot-{THEME}.html") + +# Pad-only to exact 3200×1800 target — altair.md Canvas rule (never crop) +TW, TH = 3200, 1800 +_img = Image.open(f"plot-{THEME}.png").convert("RGB") +_w, _h = _img.size +if _w > TW or _h > TH: + raise SystemExit( + f"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. " + f"Shrink chart .properties(width=, height=) values and re-render." + ) +if _w < TW or _h < TH: + _canvas = Image.new("RGB", (TW, TH), PAGE_BG) + _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2)) + _canvas.save(f"plot-{THEME}.png") diff --git a/plots/area-mountain-panorama/metadata/python/altair.yaml b/plots/area-mountain-panorama/metadata/python/altair.yaml index 3ec23d4e65..8a4ab4f499 100644 --- a/plots/area-mountain-panorama/metadata/python/altair.yaml +++ b/plots/area-mountain-panorama/metadata/python/altair.yaml @@ -2,96 +2,115 @@ library: altair language: python specification_id: area-mountain-panorama created: '2026-04-25T01:22:56Z' -updated: '2026-04-25T21:51:41Z' -generated_by: claude-opus -workflow_run: 24919026131 +updated: '2026-06-30T23:14:04Z' +generated_by: claude-sonnet +workflow_run: 28478344725 issue: 5365 -python_version: 3.14.4 -library_version: 6.1.0 +language_version: 3.13.14 +library_version: 6.2.2 preview_url_light: https://storage.googleapis.com/anyplot-images/plots/area-mountain-panorama/python/altair/plot-light.png preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/area-mountain-panorama/python/altair/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/area-mountain-panorama/python/altair/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/area-mountain-panorama/python/altair/plot-dark.html -quality_score: 93 +quality_score: 84 review: strengths: - - Custom dusk sky gradient (theme-adaptive to both light/dark) transforms the chart - into a striking landscape infographic - - 'Proper Altair layer composition: sky rect + silhouette area + leaders + labels - in clean declarative layers' - - 'Focal summit hierarchy: Matterhorn at 26px bold vs 18px for others, elevated - label position — composition has a clear center of gravity' - - Complete theme adaptation with all chrome tokens (INK, INK_SOFT, PAGE_BG) correctly - applied - - Real Valais Alps data with authentic elevations for all 16 peaks + - Dusk sky gradient (theme-adaptive, zenith→horizon) using Vega-Lite native gradient + spec creates authentic alpine atmosphere in both light and dark themes + - Brand green (#009E73) silhouette with alpenglow warm-gold rim is visually striking + and spec-appropriate + - 'Matterhorn treated as an unambiguous compositional focal point: highest label + tier (5500m), larger font (15pt vs 10pt), bolder leader line (2.5px vs 1.0px)' + - Piecewise-linear ridgeline using tent functions (65 random jagged peaks + 16 named + summits) strictly follows the spec's no-Gaussian-bumps requirement + - All 16 Wallis summits with accurate names and real-world elevations; Y-axis range + and scale realistic + - 'Full palette compliance: brand green (#009E73) for single data series; backgrounds + #FAF8F1/#1A1A17; only chrome adapts to theme' weaknesses: - - Elevation sub-labels (elev_labels) set to 15px are marginally below the 16px minimum - — raise fontSize=15 to fontSize=16 - - DE-01 could reach 7-8 with a slightly more polished sky-to-silhouette boundary - or a subtle alpenglow rim highlight at the ridgeline + - 'Inter-tier label overlap in left cluster (Weisshorn 9°, Zinalrothorn 22°, Ober + Gabelhorn 30°, Dent Blanche 42°): adjacent Y-tier spacing of ~200m (≈52px at scale + 4) is less than two stacked name+elevation text lines (~80px), causing elevation + numbers to overlap with adjacent-tier peak names. Increase label Y-tier separation + from 200m to ~280–300m, or raise the Y_SCALE domain ceiling to spread tiers further + apart.' + - 'Same inter-tier overlap affects right cluster (Allalinhorn 137°, Alphubel 148°, + Täschhorn 155°, Dom 168°): four peaks within 31° at adjacent tiers.' + - Bottom third of the chart (~2900–3200m Y zone) is entirely empty PAGE_BG fill; + raising the Y_SCALE lower bound to ~3100m or adding a subtle baseline annotation + would use the canvas more efficiently. image_description: |- Light render (plot-light.png): - Background: Warm off-white #FAF8F1 — correct, not pure white. - Chrome: Title "Wallis Panorama · area-mountain-panorama · altair · anyplot.ai" in dark #1A1A17 — clearly readable. Y-axis "Elevation (m)" label and tick labels (3000–5000 m) in INK_SOFT #4A4A44 — readable. Peak name labels (18–26px bold dark) and elevation sub-labels (15px #4A4A44) visible against the gradient sky above the silhouette. No light-on-light issues. - Data: Brand green #009E73 mountain silhouette fills the lower portion. Dusk sky gradient: warm peach/orange at ridgeline through rose/mauve to deep violet-blue at zenith. All 16 peaks annotated with leader lines and staggered labels. - Legibility verdict: PASS + Background: Warm off-white #FAF8F1 — correct + Chrome: Title "Wallis Panorama · area-mountain-panorama · python · altair · anyplot.ai" top-left in dark ink — readable. Subtitle in INK_SOFT — readable. Y-axis label "Elevation (m)" in dark ink — readable. Y-axis tick labels 3,000–5,000 in INK_SOFT — readable. Peak name/elevation labels in INK/INK_SOFT — readable individually, but crowded in left (9°–42°) and right (137°–168°) clusters due to inter-tier overlap. + Data: Brand green #009E73 silhouette fills area below ridgeline. Alpenglow warm-gold/orange stroke traces ridgeline boundary. Dusk sky gradient (zenith violet → mid rose-purple → horizon warm orange/peach) fills upper chart zone. Leader lines thin but visible. Matterhorn gets heavier 2.5px leader and larger 15pt bold font. + Legibility verdict: PASS (all elements readable; crowding in label clusters is a layout issue, not a legibility failure) Dark render (plot-dark.png): - Background: Near-black #1A1A17 — correct, not pure black. - Chrome: Title in light #F0EFE8 — readable. Y-axis label and ticks in INK_SOFT #B8B7B0 — readable against dark background. Peak name labels in #F0EFE8 (light) and elevation sub-labels in #B8B7B0 — white/light-gray text on dark sky — readable. No dark-on-dark failures observed; all chrome tokens properly flipped. - Data: Mountain silhouette remains identical brand green #009E73 — same as light render. Sky gradient flips to dark night palette: deep navy at zenith through deep indigo to dark maroon/rust at ridgeline. - Legibility verdict: PASS + Background: Warm near-black #1A1A17 — correct + Chrome: Title and all text in light-colored ink (INK #F0EFE8 and INK_SOFT #B8B7B0) — clearly readable against dark background. No dark-on-dark failure observed anywhere. + Data: Brand green #009E73 silhouette — IDENTICAL to light render (only chrome flips, data color stays constant). Alpenglow rim becomes warm copper-terra-cotta stroke. Sky gradient adapts: deep navy/near-black at zenith, dark aubergine/brown mid, warm copper at horizon. + Legibility verdict: PASS (all text readable, no dark-on-dark failure) criteria_checklist: visual_quality: - score: 29 + score: 22 max: 30 items: - id: VQ-01 name: Text Legibility - score: 7 + score: 5 max: 8 passed: true - comment: All sizes explicitly set; elevation sub-labels at 15px slightly below - 16px floor; both themes readable + comment: Title/subtitle/axis labels readable in both themes. Peak labels readable + individually but inter-tier crowding in left cluster (9°-42°) and right + cluster (137°-168°) causes partial overlap of elevation numbers with adjacent-tier + peak names. - id: VQ-02 name: No Overlap - score: 6 + score: 3 max: 6 - passed: true - comment: Alternating LABEL_HIGH/LABEL_LOW stagger prevents all overlaps + passed: false + comment: 'Round-robin tier assignment prevents within-tier collision (min + ~33° same-tier separation) but adjacent-tier labels for angularly close + peaks overlap: Zinalrothorn 4221 overlaps with Ober Gabelhorn text, 4063 + overlaps with Dent Blanche in left cluster.' - id: VQ-03 name: Element Visibility - score: 6 + score: 5 max: 6 passed: true - comment: Green silhouette, leader lines, and labels all clearly visible in - both themes + comment: Mountain silhouette prominent and clearly visible. Ridgeline convincingly + alpine. Alpenglow rim adds definition. Leader lines thin (strokeWidth=1.0, + opacity=0.55) — may be hard at mobile scale. - id: VQ-04 name: Color Accessibility score: 2 max: 2 passed: true - comment: CVD-safe brand green; adequate contrast on gradient sky + comment: Single data series uses brand green; sky gradient and alpenglow are + chrome. No red-green confusion risk. - id: VQ-05 name: Layout & Canvas - score: 4 + score: 3 max: 4 passed: true - comment: Wide landscape format fills canvas appropriately for panoramic subject + comment: Canvas gate passed. Landscape format ideal for panoramic chart. Bottom + Y region (2900-3200m) is empty PAGE_BG fill — slightly wasted canvas. - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: 'Y-axis: Elevation (m) with units; x-axis intentionally hidden per - spec' + comment: Y-axis labeled Elevation (m) with units. X-axis hidden per spec (bearing + labels optional). Title format correct. - id: VQ-07 name: Palette Compliance score: 2 max: 2 passed: true - comment: 'Single series #009E73; light #FAF8F1, dark #1A1A17 backgrounds; - all chrome tokens theme-adaptive' + comment: 'Brand green #009E73 for single silhouette series. Backgrounds #FAF8F1 + (light) / #1A1A17 (dark) correct. Sky gradient and alpenglow are spec-authorized + artistic chrome.' design_excellence: score: 15 max: 20 @@ -101,22 +120,24 @@ review: score: 6 max: 8 passed: true - comment: 'Strong design: custom dusk gradient sky, focal summit hierarchy, - intentional panoramic composition — above defaults but not quite publication-ready' + comment: Dusk sky gradient with theme-adaptive color stops. Alpenglow warm-gold + rim adds photographic realism. Matterhorn receives compositional accent. + Five-tier staggering system shows deliberate design thought. - id: DE-02 name: Visual Refinement - score: 5 + score: 4 max: 6 passed: true - comment: No grid, no view stroke, generous whitespace, thin elegant leader - lines + comment: No grid (gridOpacity=0.0, appropriate). No top/right spines. INK/INK_SOFT + tokens throughout. Label zone somewhat busy in crowded clusters. - id: DE-03 name: Data Storytelling - score: 4 + score: 5 max: 6 passed: true - comment: Matterhorn elevated as focal summit via size and height; staggered - annotations guide viewer across panorama + comment: 'Matterhorn unambiguously focal: special tier height, bold font, + heavier leader. Panoramic left-to-right composition guides viewer naturally. + Minor: bottom third empty padding.' spec_compliance: score: 15 max: 15 @@ -126,27 +147,30 @@ review: score: 5 max: 5 passed: true - comment: Correct filled-area panorama silhouette chart + comment: 'Correct: filled-area mountain panorama with labeled peaks, angular + ridgeline (triangular tent functions), sky background.' - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: Sky gradient, peak annotations with leader lines, staggered labels, - focal summit — all present; brand palette overrides dark-color guidance + comment: Piecewise-linear ridgeline (no Gaussians), filled silhouette, sky + gradient (dusk), staggered labels, name+elevation format, Matterhorn focal + point, Y-axis with sensible lower bound, X-axis hidden as optional. - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: angle_deg on X, elevation_m on Y; all 16 Valais summits plotted + comment: X=angle_deg, Y=elevation_m; all 16 Wallis summits from spec included; + full 180° sweep covered. - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title format correct with area-mountain-panorama · altair · anyplot.ai; - no legend needed + comment: 'Title: Wallis Panorama · area-mountain-panorama · python · altair + · anyplot.ai. No legend needed (single series).' data_quality: score: 15 max: 15 @@ -156,22 +180,22 @@ review: score: 6 max: 6 passed: true - comment: 'All panorama features: silhouette, sky gradient, 16 annotated peaks, - staggered labels, focal summit' + comment: 'All aspects: ridgeline profile, named peaks, elevation labels, leader + lines, staggered layout, focal summit, sky gradient, alpenglow rim.' - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Real Valais Alps peaks with correct elevations; canonical Zermatt - panorama; neutral geographic context + comment: Real Wallis/Valais Alps peaks with accurate names and elevations. + Classic alpine tourism panorama scenario. No controversial content. - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Elevations 4027-4634m accurate for Swiss 4000-m peaks; lower bound - 2900m realistic + comment: Y range 2900-5800m appropriate; all peaks are 4000m+ summits. 180° + sweep is realistic observer panorama width. code_quality: score: 10 max: 10 @@ -181,61 +205,68 @@ review: score: 3 max: 3 passed: true - comment: 'Flat linear: theme tokens -> data -> chart layers -> save' + comment: No functions or classes. Flat procedural script. Loops over peaks + DataFrame minimal and clear. - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: np.random.seed(42) set for ridge generation + comment: np.random.seed(42) for jagged ridgeline noise. - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only altair, numpy, pandas (via importlib), os, sys used + comment: Dynamic import for altair package shadowing; PIL used for canvas + padding; all imports used. - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Layer composition via + is clean and idiomatic; gradient sky via - rect mark with color dict is appropriate + comment: Multi-layer composition correctly assembled. Dynamic title fontsize + scaling formula appropriate. Tier assignment map clear. - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves plot-{THEME}.png with scale_factor=3.0 and plot-{THEME}.html + comment: Saves plot-{THEME}.png and plot-{THEME}.html. PIL padding to exact + 3200x1800 target. library_mastery: - score: 9 + score: 7 max: 10 items: - id: LM-01 name: Idiomatic Usage - score: 5 + score: 4 max: 5 passed: true - comment: Expert use of layer composition, all mark types, encode() shorthand, - configure_* global styling + comment: 'Altair layer composition (+) correct. Proper encoding type suffixes. + alt.Scale, alt.Axis, alt.Title idiomatic. All mark types applied correctly. + Minor: tooltip on non-interactive area/line layers is unused overhead.' - id: LM-02 name: Distinctive Features - score: 4 + score: 3 max: 5 passed: true - comment: Altair gradient fill via color dict with gradient/stops is distinctively - Altair; layer composition with shared scales; HTML export + comment: Vega-Lite linear gradient spec in mark_rect color property (distinctive). + 9-layer composition. HTML export with interactive tooltips. alt.Title with + subtitleColor, anchor, offset. verdict: APPROVED impl_tags: - dependencies: [] + dependencies: + - pillow techniques: - layer-composition - hover-tooltips + - annotations - html-export patterns: - data-generation - iteration-over-groups dataprep: [] styling: - - alpha-blending - gradient-fill + - alpha-blending