diff --git a/plots/streamgraph-basic/implementations/python/pygal.py b/plots/streamgraph-basic/implementations/python/pygal.py index 5eb401088a..a6d0ed33cb 100644 --- a/plots/streamgraph-basic/implementations/python/pygal.py +++ b/plots/streamgraph-basic/implementations/python/pygal.py @@ -1,14 +1,34 @@ -""" pyplots.ai +""" anyplot.ai streamgraph-basic: Basic Stream Graph -Library: pygal 3.1.0 | Python 3.13.11 -Quality: 75/100 | Created: 2025-12-23 +Library: pygal 3.1.0 | Python 3.13.13 +Quality: 82/100 | Updated: 2026-05-06 """ +import os +import sys + + +# Remove script directory from sys.path so 'import pygal' finds the installed package +_script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir] + import numpy as np import pygal from pygal.style import Style +# Theme tokens +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" +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" + +OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00") + +# Prepend background color for the invisible baseline series, then Okabe-Ito for genres +COLORS = (PAGE_BG,) + OKABE_ITO + # Data: monthly streaming hours by music genre over two years np.random.seed(42) @@ -40,107 +60,82 @@ "Dec 24", ] genres = ["Pop", "Rock", "Hip-Hop", "Electronic", "Jazz"] - -# Generate smooth, realistic streaming data with trends base_values = {"Pop": 45, "Rock": 35, "Hip-Hop": 40, "Electronic": 30, "Jazz": 15} data = {} for genre in genres: base = base_values[genre] - # Add trend over time trend = np.linspace(0, np.random.uniform(-10, 15), months) - # Seasonal variation seasonal = 8 * np.sin(np.linspace(0, 4 * np.pi, months) + np.random.uniform(0, 2 * np.pi)) - # Random noise noise = np.random.randn(months) * 3 values = base + trend + seasonal + noise - values = np.maximum(values, 5) # Ensure positive values + values = np.maximum(values, 5) data[genre] = values.tolist() -# Convert data to array for streamgraph calculation +# True streamgraph baseline: center the entire stack symmetrically around y=0. +# baseline[t] = -total[t]/2 so the stack spans from -total/2 to +total/2. data_array = np.array([data[genre] for genre in genres]) - -# Calculate centered baseline for true streamgraph effect -# Symmetric baseline: streams expand outward from center (x-axis at y=0) total_at_each_time = data_array.sum(axis=0) -baseline_offset = total_at_each_time / 2 +baseline = (-total_at_each_time / 2).tolist() + +# Symmetric y-axis range with a small margin +half_total_max = total_at_each_time.max() / 2 +y_range = half_total_max * 1.12 -# Custom style with colorblind-safe palette -# Colors ordered for maximum contrast between adjacent layers +# Style custom_style = Style( - background="white", - plot_background="white", - foreground="#2c3e50", - foreground_strong="#2c3e50", - foreground_subtle="#7f8c8d", - # High contrast palette: dark blue, orange, teal, crimson, gold - colors=("#1a5276", "#e67e22", "#138d75", "#c0392b", "#d4ac0d"), - title_font_size=80, - label_font_size=48, - major_label_font_size=40, - legend_font_size=48, - value_font_size=36, - opacity=0.90, + background=PAGE_BG, + plot_background=PAGE_BG, + foreground=INK, + foreground_strong=INK, + foreground_subtle=INK_MUTED, + colors=COLORS, + title_font_size=28, + label_font_size=22, + major_label_font_size=18, + legend_font_size=16, + value_font_size=14, + stroke_width=3, + opacity=0.88, opacity_hover=0.95, - guide_stroke_color="#e0e0e0", - major_guide_stroke_color="#cccccc", ) -# Calculate y-axis range based on actual data -# After centering, min is -baseline_offset, max is baseline_offset (total stack height) -y_min = -baseline_offset.max() * 1.1 # Add 10% padding -y_max = baseline_offset.max() * 1.1 - -# Create StackedLine chart - pygal's native stacked area chart -# We'll shift all data to center around zero by subtracting half the total from each layer +# Chart chart = pygal.StackedLine( width=4800, height=2700, - title="streamgraph-basic · pygal · pyplots.ai", + title="streamgraph-basic · pygal · anyplot.ai", x_title="Month", - y_title="Streaming Hours (centered)", + y_title="Streaming Hours", style=custom_style, fill=True, show_dots=False, show_y_guides=True, show_x_guides=False, legend_at_bottom=True, - legend_box_size=36, + legend_box_size=30, margin=100, spacing=40, truncate_legend=-1, - truncate_label=-1, # Prevent x-axis label truncation - interpolate="cubic", # Smooth flowing curves + truncate_label=-1, + interpolate="cubic", show_minor_x_labels=False, - x_label_rotation=45, # Rotate labels to prevent overlap - range=(y_min, y_max), # Dynamic y-axis range based on data + x_label_rotation=45, + range=(-y_range, y_range), ) -# Set x-axis labels showing months chart.x_labels = month_labels chart.x_labels_major = ["Jan 23", "Jul 23", "Jan 24", "Jul 24"] -# To create a centered streamgraph with StackedLine: -# We need to shift each layer's values so the visual center is at y=0 -# First layer starts at -baseline_offset, subsequent layers stack on top - -# Calculate shifted values for each layer -# The first (bottom) layer is shifted down by baseline_offset -# This makes the visual center of the total stack sit at y=0 -shifted_data = [] -for i, genre in enumerate(genres): - if i == 0: - # First layer: shift down by baseline to center the stream - shifted_values = (np.array(data[genre]) - baseline_offset).tolist() - else: - # Subsequent layers: use original values (they stack on top) - shifted_values = data[genre] - shifted_data.append((genre, shifted_values)) - -# Add all layers -for genre, values in shifted_data: - chart.add(genre, values) - -# Save as PNG and HTML -chart.render_to_png("plot.png") -chart.render_to_file("plot.html") +# Invisible baseline series shifts the stack so genres span -total/2 to +total/2. +# Color matches background (PAGE_BG) so it renders as transparent. +chart.add("", baseline) + +# Genre series added with actual positive values — pygal stacks them on top of baseline +for genre in genres: + chart.add(genre, data[genre]) + +# Save +chart.render_to_png(f"plot-{THEME}.png") +with open(f"plot-{THEME}.html", "wb") as f: + f.write(chart.render()) diff --git a/plots/streamgraph-basic/metadata/python/pygal.yaml b/plots/streamgraph-basic/metadata/python/pygal.yaml index 8d13abdb51..aad8421a45 100644 --- a/plots/streamgraph-basic/metadata/python/pygal.yaml +++ b/plots/streamgraph-basic/metadata/python/pygal.yaml @@ -1,37 +1,249 @@ library: pygal +language: python specification_id: streamgraph-basic created: '2025-12-23T21:57:05Z' -updated: '2025-12-23T22:25:41Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20472385283 -issue: 0 -python_version: 3.13.11 +updated: '2026-05-06T21:56:36Z' +generated_by: claude-sonnet +workflow_run: 25356184730 +issue: 856 +python_version: 3.13.13 library_version: 3.1.0 -preview_url: https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/pygal/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/pygal/plot.html -quality_score: 75 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/pygal/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/pygal/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/pygal/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/streamgraph-basic/python/pygal/plot-dark.html +quality_score: 82 +review: + strengths: + - 'Perfect spec compliance: symmetric streamgraph baseline correctly computed via + invisible series trick' + - Canonical Okabe-Ito palette in correct order; full theme-adaptive chrome in both + light and dark renders + - Cubic interpolation delivers authentic smooth flowing curves as specified + - Clean flat code structure with seed for reproducibility; saves both PNG and HTML + as required + - No dark-on-dark failures; both renders fully legible + weaknesses: + - 'Design Excellence is modest (9/20): technically correct but visually generic + — no visual hierarchy, focal points, or emphasis guiding the viewer to an insight' + - 'No data storytelling: all 5 genres given equal visual weight; viewer cannot easily + discern which genre is trending up or down' + - Layout leaves minor wasted space in corners; refined padding/margin pass could + improve canvas utilization + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) — correct, not pure white + Chrome: Title "streamgraph-basic · pygal · anyplot.ai" in dark text — readable; Y-axis label "Streaming Hours" and X-axis label "Month" clearly visible; major x-axis tick labels (Jan 23, Jul 23, Jan 24, Jul 24) rotated 45° — readable; y-axis numeric ticks visible; legend at bottom with all 5 genre labels + Data: Five stream bands in Okabe-Ito order from bottom: green (#009E73, Pop), orange-red (#D55E00, Rock), blue (#0072B2, Hip-Hop), pink (#CC79A7, Electronic), yellow (#E69F00, Jazz); smooth cubic curves; symmetric centering around y=0 + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Near-black (#1A1A17) — correct, not pure black + Chrome: Title, axis labels, tick labels, and legend text all appear in light off-white tones — fully readable; invisible baseline series (#1A1A17) merges with background — no visible artifact; no dark-on-dark failures detected + Data: Colors identical to light render — all five Okabe-Ito positions preserved; brand green #009E73 clearly visible on dark surface + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 28 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All font sizes explicitly set (title=28, label=22, major_label=18, + legend=16); readable in both renders + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: Major-only x-axis labels with 45deg rotation prevent crowding; legend + at bottom has adequate spacing + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Stream bands clearly distinct with strong fill colors and 0.88 opacity + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Okabe-Ito palette is CVD-safe; all 5 bands distinguishable by hue + and brightness + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Chart fills ~65% of 4800x2700 canvas with balanced margins; minor + wasted space in upper right + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Streaming Hours and Month are descriptive labels + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First real series (Pop) = #009E73; all 5 genres follow canonical + Okabe-Ito order; backgrounds are #FAF8F1 / #1A1A17; chrome is theme-correct + in both renders' + design_excellence: + score: 9 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 4 + max: 8 + passed: false + comment: Well-configured style with Okabe-Ito palette and correct chrome tokens; + clean and professional but no outstanding visual design choices above a + library default + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: false + comment: y-guides only (no x-guides), explicit font hierarchy, opacity softening; + adequate refinement but no spine treatment or advanced polish + - id: DE-03 + name: Data Storytelling + score: 2 + max: 6 + passed: false + comment: Five streaming genres over two years with seasonal variation; data + is displayed but no visual hierarchy, emphasis, or focal point guides the + viewer + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: StackedLine with fill + invisible symmetric baseline correctly implements + streamgraph centered around y=0 + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Cubic interpolation for smooth curves, symmetric baseline centering, + distinct Okabe-Ito colors per category, legend present + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Time (months) on x-axis, streaming magnitude on y-axis, all 5 categories + visible + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title is exactly streamgraph-basic · pygal · anyplot.ai; legend labels + match the 5 genre series + data_quality: + score: 13 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Shows 5 genres with distinct trends, seasonal oscillations, and varying + magnitudes; demonstrates relative flow clearly + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: 'Monthly music genre streaming over two years: neutral, comprehensible, + real-world scenario' + - id: DQ-03 + name: Appropriate Scale + score: 3 + max: 4 + passed: true + comment: Base values (Pop 45, Hip-Hop 40, Rock 35, Electronic 30, Jazz 15) + are plausible relative proportions; y-axis shows negative values as expected + streamgraph artifact from symmetric centering + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Flat linear structure: imports -> theme tokens -> data generation + -> style -> chart -> save' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: np.random.seed(42) + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: numpy, pygal, pygal.style, os, sys — all used; sys needed for path + workaround + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean, Pythonic; invisible baseline trick for stream centering is + a clever workaround + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves plot-{THEME}.png and plot-{THEME}.html; current API + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Uses StackedLine with fill, Style object for full theming, cubic + interpolation, legend_at_bottom, major vs minor label distinction + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Leverages pygal's cubic interpolation for smooth stream curves and + interactive HTML export; invisible baseline exploits pygal's stacking model + to achieve streamgraph centering + verdict: APPROVED impl_tags: dependencies: [] techniques: - html-export patterns: - data-generation - - matrix-construction + - iteration-over-groups dataprep: [] styling: - alpha-blending -review: - strengths: - - Successfully implements a centered streamgraph using pygal StackedLine chart with - mathematical baseline shifting - - Excellent colorblind-safe palette with high contrast between adjacent layers (dark - blue, orange, teal, crimson, gold) - - Smooth flowing curves achieved through cubic interpolation - - Realistic data scenario with seasonal patterns and genre trends - - Clean code structure following KISS principles with proper random seed - weaknesses: - - X-axis labels are truncated showing Jan... instead of full Jan 23 labels despite - truncate_label=-1 setting - - Y-axis label includes (centered) which is a technical detail not a meaningful - unit - - Grid lines are not visible in the output despite configuration