diff --git a/plots/rose-basic/implementations/python/altair.py b/plots/rose-basic/implementations/python/altair.py index dc57813d28..253b790017 100644 --- a/plots/rose-basic/implementations/python/altair.py +++ b/plots/rose-basic/implementations/python/altair.py @@ -1,139 +1,89 @@ -""" pyplots.ai +""" anyplot.ai rose-basic: Basic Rose Chart -Library: altair 6.0.0 | Python 3.13.11 -Quality: 92/100 | Created: 2025-12-23 +Library: altair 6.1.0 | Python 3.13.13 +Quality: 87/100 | Updated: 2026-04-30 """ +import os + import altair as alt import numpy as np import pandas as pd -# Data - Monthly rainfall in mm (cyclical 12-month pattern) +# Theme +THEME = os.getenv("ANYPLOT_THEME", "light") +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" + +# Data - Monthly rainfall in mm (12-month cyclical pattern) months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] rainfall = [78, 52, 68, 45, 35, 28, 22, 30, 55, 85, 92, 88] n = len(months) - -# Calculate angles starting at 12 o'clock (top) and going clockwise -# -90 degrees offset to start at top, then proceeding clockwise angle_step = 360 / n start_angles = [-90 + i * angle_step for i in range(n)] end_angles = [-90 + (i + 1) * angle_step for i in range(n)] -# Create DataFrame with explicit angles df = pd.DataFrame( - { - "month": months, - "value": rainfall, - "order": range(n), - "startAngle": np.radians(start_angles), - "endAngle": np.radians(end_angles), - } + {"month": months, "value": rainfall, "startAngle": np.radians(start_angles), "endAngle": np.radians(end_angles)} ) -# Max value for radius scaling - use 100 for nicer gridline values max_val = 100 +chart_radius = 460 -# Color palette - colorblind-friendly distinct colors -colors = [ - "#306998", # Python Blue (Jan) - "#FFD43B", # Python Yellow (Feb) - "#4ECDC4", # Teal (Mar) - "#FF6B6B", # Coral (Apr) - "#95E1D3", # Mint (May) - "#F38181", # Salmon (Jun) - "#A8D5BA", # Sage (Jul) - "#FFC93C", # Gold (Aug) - "#5D9CEC", # Sky Blue (Sep) - "#AC92EB", # Lavender (Oct) - "#EC87C0", # Pink (Nov) - "#48CFAD", # Seafoam (Dec) -] - -# Create radial gridlines data (concentric circles at 25, 50, 75, 100 mm) +# Radial gridlines at 25, 50, 75, 100 mm grid_values = [25, 50, 75, 100] -grid_data = pd.DataFrame({"value": grid_values, "label": [f"{v}" for v in grid_values]}) +grid_data = pd.DataFrame({"value": grid_values}) -# Chart radius for the radial visualization -chart_radius = 450 - -# Radial gridlines - concentric circles using mark_arc gridlines = ( alt.Chart(grid_data) - .mark_arc(filled=False, stroke="#cccccc", strokeWidth=1.5, strokeDash=[6, 4]) + .mark_arc(filled=False, stroke=INK_SOFT, strokeWidth=1.0, strokeOpacity=0.35, strokeDash=[6, 4]) .encode( - theta=alt.value(2 * np.pi), # Full circle + theta=alt.value(2 * np.pi), radius=alt.Radius("value:Q", scale=alt.Scale(type="linear", domain=[0, max_val], range=[0, chart_radius])), ) ) -# Grid labels positioned at 3 o'clock position (right side) to avoid overlap with data +# Grid labels at 3 o'clock position grid_label_data = pd.DataFrame( - { - "value": grid_values, - "label": [f"{v} mm" for v in grid_values], - # Position labels at 3 o'clock (right) - angle = 0 degrees - "theta": [0.0] * len(grid_values), - } + {"value": grid_values, "label": [f"{v} mm" for v in grid_values], "theta": [0.0] * len(grid_values)} ) grid_labels = ( alt.Chart(grid_label_data) - .mark_text(fontSize=18, fontWeight="bold", dx=12, color="#666666", align="left", baseline="middle") + .mark_text(fontSize=18, dx=10, align="left", baseline="middle") .encode( theta=alt.Theta("theta:Q"), radius=alt.Radius("value:Q", scale=alt.Scale(type="linear", domain=[0, max_val], range=[0, chart_radius])), text="label:N", + color=alt.value(INK_SOFT), ) ) -# Rose chart using mark_arc with explicit angles to start at 12 o'clock +# Rose chart segments — viridis colormap for value-based color encoding (12 categories) rose = ( alt.Chart(df) - .mark_arc(stroke="#ffffff", strokeWidth=2, innerRadius=0) + .mark_arc(stroke=PAGE_BG, strokeWidth=2, innerRadius=0) .encode( theta=alt.Theta("startAngle:Q", stack=None), theta2=alt.Theta2("endAngle:Q"), radius=alt.Radius("value:Q", scale=alt.Scale(type="linear", domain=[0, max_val], range=[0, chart_radius])), - color=alt.Color( - "month:N", - scale=alt.Scale(domain=months, range=colors), - legend=None, # Legend disabled to control canvas size; colors are self-explanatory with labels - ), + color=alt.Color("value:Q", scale=alt.Scale(scheme="viridis"), legend=None), tooltip=[alt.Tooltip("month:N", title="Month"), alt.Tooltip("value:Q", title="Rainfall (mm)")], ) ) -# Calculate label positions - midpoint angle for each segment -mid_angles = [(-90 + (i + 0.5) * angle_step) for i in range(n)] +# Value labels near segment tips +mid_angles = [-90 + (i + 0.5) * angle_step for i in range(n)] mid_angles_rad = np.radians(mid_angles) -# For small values that cluster together (Jun-Aug: 28, 22, 30), push labels further out -# to prevent crowding; larger values can have labels closer to segment edge -label_radii = [] -for v in rainfall: - if v < 35: - # Small segments - push label further outside segment - label_radii.append(max(v * 1.35, 45)) - else: - # Normal segments - position just outside - label_radii.append(v * 1.15) - -# Create label data with theta and radius for polar positioning -label_data = pd.DataFrame({"month": months, "value": rainfall, "theta": mid_angles_rad, "labelRadius": label_radii}) +label_radii = [max(v * 1.35, 45) if v < 35 else v * 1.15 for v in rainfall] -# Create month labels positioned at outer edge of chart -month_label_data = pd.DataFrame( - { - "month": months, - "theta": mid_angles_rad, - "labelRadius": [115] * n, # Just outside the 100mm gridline - } -) +label_data = pd.DataFrame({"month": months, "value": rainfall, "theta": mid_angles_rad, "labelRadius": label_radii}) -# Text labels showing values on each segment using polar coordinates -text = ( +value_labels = ( alt.Chart(label_data) .mark_text(fontSize=20, fontWeight="bold") .encode( @@ -142,11 +92,13 @@ "labelRadius:Q", scale=alt.Scale(type="linear", domain=[0, max_val], range=[0, chart_radius]) ), text=alt.Text("value:Q"), - color=alt.value("#333333"), + color=alt.value(INK), ) ) -# Month labels at outer edge +# Month labels at outer edge — just beyond the 100 mm gridline +month_label_data = pd.DataFrame({"month": months, "theta": mid_angles_rad, "labelRadius": [115.0] * n}) + month_labels = ( alt.Chart(month_label_data) .mark_text(fontSize=22, fontWeight="bold") @@ -156,66 +108,23 @@ "labelRadius:Q", scale=alt.Scale(type="linear", domain=[0, max_val], range=[0, chart_radius]) ), text=alt.Text("month:N"), - color=alt.value("#333333"), + color=alt.value(INK), ) ) # Combine all layers chart = ( - alt.layer(gridlines, grid_labels, rose, text, month_labels) - .properties(title=alt.Title(text="rose-basic · altair · pyplots.ai", fontSize=32, anchor="middle", offset=15)) - .configure_view(strokeWidth=0) + alt.layer(gridlines, grid_labels, rose, value_labels, month_labels) + .properties( + width=1200, + height=1200, + background=PAGE_BG, + title=alt.Title(text="rose-basic · altair · anyplot.ai", fontSize=32, anchor="middle", offset=20, color=INK), + ) + .configure_view(strokeWidth=0, fill=PAGE_BG) .configure_axis(grid=False, domain=False, ticks=False, labels=False, title=None) ) -# Save chart - Altair radial charts position content in upper portion -# Use high scale factor to ensure quality, then crop to center the content -chart.save("plot_raw.png", scale_factor=3.0) -chart.save("plot.html") - -# Post-process: crop to center the radial chart and resize to 3600x3600 (square format) -from PIL import Image - - -img = Image.open("plot_raw.png") -width, height = img.size - -# Altair radial charts center content horizontally but place it in the upper portion vertically -# The radial center is approximately at 36% from the top of the rendered image -content_center_y = int(height * 0.36) -content_center_x = width // 2 - -# Use a crop size that captures all content including outer month labels with some padding -crop_size = min(width, int(height * 0.80)) - -# Center the crop on the content -left = content_center_x - crop_size // 2 -top = content_center_y - crop_size // 2 -right = left + crop_size -bottom = top + crop_size - -# Adjust if crop extends beyond image boundaries -if left < 0: - left = 0 - right = crop_size -if top < 0: - top = 0 - bottom = crop_size -if right > width: - right = width - left = width - crop_size -if bottom > height: - bottom = height - top = height - crop_size - -cropped = img.crop((left, top, right, bottom)) - -# Resize to target 3600x3600 -final = cropped.resize((3600, 3600), Image.Resampling.LANCZOS) -final.save("plot.png") - -# Clean up temp file -import os - - -os.remove("plot_raw.png") +# Save +chart.save(f"plot-{THEME}.png", scale_factor=3.0) +chart.save(f"plot-{THEME}.html") diff --git a/plots/rose-basic/metadata/python/altair.yaml b/plots/rose-basic/metadata/python/altair.yaml index d829462235..2d84f41058 100644 --- a/plots/rose-basic/metadata/python/altair.yaml +++ b/plots/rose-basic/metadata/python/altair.yaml @@ -1,167 +1,185 @@ library: altair +language: python specification_id: rose-basic created: '2025-12-23T19:45:47Z' -updated: '2025-12-23T20:20:47Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20469994919 -issue: 0 -python_version: 3.13.11 -library_version: 6.0.0 -preview_url: https://storage.googleapis.com/anyplot-images/plots/rose-basic/altair/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/rose-basic/altair/plot.html -quality_score: 92 -impl_tags: - dependencies: - - pillow - techniques: - - html-export - - layer-composition - patterns: - - data-generation - - iteration-over-groups - dataprep: [] - styling: [] +updated: '2026-04-30T07:33:39Z' +generated_by: claude-sonnet +workflow_run: 25151891021 +issue: 1003 +python_version: 3.13.13 +library_version: 6.1.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/rose-basic/python/altair/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/rose-basic/python/altair/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/rose-basic/python/altair/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/rose-basic/python/altair/plot-dark.html +quality_score: 87 review: strengths: - - Proper rose chart implementation with radial segments where radius represents - values - - Colorblind-friendly distinct color palette for all 12 months - - Radial gridlines at 25/50/75/100 mm with labels for scale reference - - Value labels positioned intelligently to avoid crowding on smaller segments - - Correct title format following pyplots.ai convention + - Viridis colormap is semantically correct for continuous value-based color encoding, + providing intuitive low-to-high visual hierarchy + - Segment stroke using PAGE_BG creates clean visual separation that adapts correctly + to both light and dark themes + - Five-layer Altair composition is clean and idiomatic — gridlines, labels, data + each isolated + - 'Full theme-adaptive chrome: all INK/INK_SOFT/PAGE_BG tokens properly threaded + throughout' + - 'Output format correct: both PNG and HTML exported for both themes' weaknesses: - - Chart occupies only upper-left quadrant of canvas leaving excessive whitespace - in lower portion - - Month labels missing from outer edge - only values shown without category context - on chart itself - - Legend placed far to the right isolated from the chart rather than integrated - image_description: The plot displays a rose chart (Nightingale/coxcomb diagram) - showing monthly rainfall data in a radial format. The chart features 12 colorful - wedge segments representing each month, with radius proportional to rainfall values - (ranging from 22mm for July to 92mm for November). Colors used include Python - blue (Jan), yellow (Feb), teal (Mar), coral (Apr), mint (May), salmon (Jun), sage - (Jul), gold (Aug), sky blue (Sep), lavender (Oct), pink (Nov), and seafoam (Dec). - Two dashed concentric gridlines appear at approximately 75mm and 100mm levels - with labels on the right side. Value labels (22-92) are positioned near each segment. - A legend in the upper right lists all months with their corresponding colors. - The title "rose-basic · altair · pyplots.ai" appears at the top. The chart is - positioned in the upper portion of the canvas with significant empty space below. + - Rose chart is positioned in the upper half of the 1200x1200 canvas, leaving excessive + empty space below; center of chart area should be better utilized + - No data storytelling element (e.g. highlighting peak month, focal annotation) + — the chart shows data but does not guide the viewer to the key insight + - Value labels for low-rainfall months (Jun=28, Jul=22, Aug=30) are crowded near + the center; minimum offset or different placement needed for very small segments + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct theme surface + Chrome: Title "rose-basic · altair · anyplot.ai" in dark #1A1A17 ink, bold and readable. Month labels (Jan–Dec) in dark INK around the outer edge. Value labels (22–92) in dark INK near segment tips. Grid labels "75 mm" and "100 mm" in INK_SOFT at 3 o'clock position — visible. "25 mm" and "50 mm" labels at very small radii are harder to distinguish but present. + Data: 12 rose segments using viridis colormap — deep purple/indigo for low summer months (Jul=22mm minimum), transitioning through teal and green to bright yellow-green for peak autumn months (Nov=92mm maximum). Segment strokes in PAGE_BG create clean separation. Dashed radial gridlines at four intervals. + Legibility verdict: PASS — all major text elements readable against light background + + Dark render (plot-dark.png): + Background: Near-black #1A1A17 — correct theme surface + Chrome: Title text in light cream #F0EFE8, clearly readable. Month labels in light text. Value labels in light INK. Grid labels in INK_SOFT (#B8B7B0) — readable. No dark-on-dark failures detected. + Data: Viridis colors are identical to light render — purple for low months, yellow-green for high months. Segment strokes use #1A1A17 (dark PAGE_BG) providing appropriate separation on dark surface. + Legibility verdict: PASS — all text readable against dark background, no theme-adaptation failures criteria_checklist: visual_quality: - score: 35 - max: 40 + score: 27 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 7 + max: 8 passed: true - comment: Title, value labels, and legend text are all clearly readable at - full size + comment: 'Font sizes explicitly set (title=32, month=22, values=20, grid=18). + All text readable in both themes. Minor: 25mm grid label at very small radius + barely distinguishable.' - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 5 + max: 6 passed: true - comment: No overlapping text elements; value labels well positioned + comment: Value labels for small summer segments (Jun=28, Jul=22, Aug=30) are + clustered near center; slight crowding but no hard collision. - id: VQ-03 name: Element Visibility - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: All 12 wedge segments clearly visible with appropriate sizing + comment: All 12 segments clearly visible with distinct stroke separation. + Dashed radial gridlines readable at all rings. - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 2 + max: 2 passed: true - comment: Colorblind-friendly palette with distinct hues for each month + comment: Viridis is perceptually uniform and CVD-safe. No red-green sole signal. - id: VQ-05 - name: Layout Balance - score: 2 - max: 5 - passed: false - comment: Chart occupies upper-left area with excessive whitespace below; not - centered + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: 3600x3600px correct square format. Rose positioned correctly but + sits in upper half leaving disproportionate empty space below. - id: VQ-06 - name: Axis Labels - score: 0 + name: Axis Labels & Title + score: 2 max: 2 passed: true - comment: N/A for radial chart without traditional axes (gridlines provide - scale) + comment: Title format correct. Grid labels provide scale context in mm units. - id: VQ-07 - name: Grid & Legend + name: Palette Compliance score: 2 max: 2 passed: true - comment: Subtle dashed gridlines at 75mm and 100mm; legend well formatted + comment: 'Viridis correctly applied to continuous value-based color encoding. + Backgrounds #FAF8F1 (light) and #1A1A17 (dark) correct.' + design_excellence: + score: 11 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Viridis creates meaningful visual hierarchy from low to peak months. + Segment stroke with PAGE_BG creates clean separation. Professional but conventional + composition. + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: true + comment: Dashed radial gridlines deliberate and appropriate. Axes/ticks suppressed + correctly for circular chart. Moderate refinement beyond defaults. + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: true + comment: Cyclical seasonal pattern visible through radius + viridis. No explicit + focal-point emphasis or annotations to guide viewer beyond raw data. spec_compliance: - score: 25 - max: 25 + score: 15 + max: 15 items: - id: SC-01 name: Plot Type - score: 8 - max: 8 - passed: true - comment: Correct rose/coxcomb chart type with radial segments - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Values correctly mapped to segment radius - - id: SC-03 + comment: Correct Nightingale/rose chart with radius proportional to value. + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: 'All spec requirements met: cyclical data, radial gridlines, 12-month - pattern' - - id: SC-04 - name: Data Range + comment: Radial gridlines at 25/50/75/100mm, radius proportional to value, + 12-month circular ordering, Jan at 12 o'clock. + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All values visible within gridline scale (0-100mm) - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Legend correctly maps colors to months - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Monthly rainfall correctly encoded as segment radius. All 12 data + points shown. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: Correct format "rose-basic · altair · pyplots.ai" + comment: Title matches required format. No categorical legend needed for continuous + viridis encoding. data_quality: - score: 20 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: 'Shows full variation: low summer values (22-35mm), high autumn/winter - (78-92mm)' + comment: 'All rose chart aspects demonstrated: radius encoding, angular categories, + radial gridlines, month labels, value labels.' - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Monthly rainfall pattern is realistic and relatable + comment: Monthly rainfall in mm is plausible, neutral, domain-appropriate. + Values show realistic seasonal pattern. - id: DQ-03 name: Appropriate Scale - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Values in plausible rainfall range (22-92mm) + comment: 22-92mm monthly rainfall realistic for temperate climate. 12 months + is optimal category count per spec. code_quality: - score: 9 + score: 10 max: 10 items: - id: CQ-01 @@ -169,41 +187,61 @@ review: score: 3 max: 3 passed: true - comment: 'Linear script: imports → data → plot → save' + comment: Flat top-level code, no functions or classes. - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: Deterministic data (no random generation) + comment: Fully deterministic; hardcoded data with no randomness. - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All imports used (altair, numpy, pandas, PIL for post-processing) + comment: os, altair, numpy, pandas — all actively used. - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current Altair API + comment: Clean Altair layer composition. Radius domain/range explicitly controlled. + No fake UI. - id: CQ-05 - name: Output Correct - score: 0 + name: Output & API + score: 1 max: 1 - passed: false - comment: Creates intermediate file plot_raw.png before final plot.png - library_features: - score: 3 - max: 5 + passed: true + comment: Saves plot-{THEME}.png and plot-{THEME}.html with scale_factor=3.0. + library_mastery: + score: 9 + max: 10 items: - - id: LF-01 + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Correct use of alt.Theta/Theta2/Radius encoding for arc marks. alt.layer() + composition. Explicit alt.Scale for polar radius control. + - id: LM-02 name: Distinctive Features - score: 3 + score: 4 max: 5 passed: true - comment: Good use of mark_arc, layered composition, and polar coordinates; - however post-processing with PIL for cropping detracts from pure Altair - solution + comment: mark_arc with theta/theta2/radius (Altair polar encoding), 5-layer + composition, interactive hover tooltips, HTML export. Could push further + with dynamic elements. verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - hover-tooltips + - html-export + - layer-composition + - polar-projection + patterns: [] + dataprep: [] + styling: + - custom-colormap + - edge-highlighting