diff --git a/plots/waveform-audio/implementations/python/plotnine.py b/plots/waveform-audio/implementations/python/plotnine.py index 926bed2fbc..b390479d64 100644 --- a/plots/waveform-audio/implementations/python/plotnine.py +++ b/plots/waveform-audio/implementations/python/plotnine.py @@ -1,9 +1,11 @@ -""" pyplots.ai +""" anyplot.ai waveform-audio: Audio Waveform Plot -Library: plotnine 0.15.3 | Python 3.14.3 -Quality: 92/100 | Created: 2026-03-07 +Library: plotnine 0.15.5 | Python 3.13.13 +Quality: 86/100 | Updated: 2026-06-03 """ +import os + import numpy as np import pandas as pd from plotnine import ( @@ -27,6 +29,16 @@ ) +# 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" + +# Imprint palette — positions 1, 2, 3 for Attack, Sustain, Release phases +IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"] + # Data - synthetic audio waveform: tone with harmonics and amplitude envelope np.random.seed(42) sample_rate = 22050 @@ -34,7 +46,6 @@ num_samples = int(sample_rate * duration) time = np.linspace(0, duration, num_samples) -# Primary tone at 220 Hz with harmonics fundamental = 220 signal = ( 0.6 * np.sin(2 * np.pi * fundamental * time) @@ -43,26 +54,23 @@ + 0.05 * np.sin(2 * np.pi * fundamental * 5 * time) ) -# Amplitude envelope: attack-sustain-release shape envelope = np.ones_like(time) attack_end = int(0.05 * sample_rate) sustain_end = int(1.1 * sample_rate) envelope[:attack_end] = np.linspace(0, 1, attack_end) envelope[sustain_end:] = np.linspace(1, 0, num_samples - sustain_end) -# Add slight vibrato and noise for realism vibrato = 1.0 + 0.03 * np.sin(2 * np.pi * 5 * time) noise = np.random.normal(0, 0.02, num_samples) amplitude = np.clip((signal * envelope * vibrato) + noise, -1.0, 1.0) -# Downsample for plotting using min/max envelope to avoid aliasing +# Downsample using min/max envelope to avoid aliasing chunk_size = 64 num_chunks = num_samples // chunk_size time_chunks = np.array([time[i * chunk_size] for i in range(num_chunks)]) amp_min = np.array([amplitude[i * chunk_size : (i + 1) * chunk_size].min() for i in range(num_chunks)]) amp_max = np.array([amplitude[i * chunk_size : (i + 1) * chunk_size].max() for i in range(num_chunks)]) -# Classify each chunk into attack/sustain/release phase for storytelling attack_time = 0.05 sustain_time = 1.1 phase = [] @@ -83,46 +91,47 @@ } ) -# Phase colors: distinct hues to tell the waveform story -phase_colors = {"Attack": "#E8651A", "Sustain": "#306998", "Release": "#6A4C93"} +# Imprint palette: Attack=green (pos 1), Sustain=lavender (pos 2), Release=blue (pos 3) +phase_colors = {"Attack": IMPRINT_PALETTE[0], "Sustain": IMPRINT_PALETTE[1], "Release": IMPRINT_PALETTE[2]} phase_alphas = {"Attack": 0.85, "Sustain": 0.65, "Release": 0.55} -# Plot - use fill mapped to phase for data storytelling (DE-03) +title = "waveform-audio · python · plotnine · anyplot.ai" + +# Plot plot = ( ggplot(df, aes(x="time")) + geom_ribbon(aes(ymin="amp_min", ymax="amp_max", fill="phase", alpha="phase"), show_legend=False) + scale_fill_manual(values=phase_colors) + scale_alpha_manual(values=phase_alphas) - + geom_hline(yintercept=0, color="#888888", size=0.3, linetype="solid") - # Phase boundary markers - + geom_vline(xintercept=attack_time, color="#E8651A", size=0.4, linetype="dashed", alpha=0.6) - + geom_vline(xintercept=sustain_time, color="#6A4C93", size=0.4, linetype="dashed", alpha=0.6) - # Phase labels via annotate (DE-03 storytelling) - + annotate("text", x=0.025, y=0.92, label="Attack", size=11, color="#E8651A", fontstyle="italic") - + annotate("text", x=0.575, y=0.92, label="Sustain", size=11, color="#306998", fontstyle="italic") - + annotate("text", x=1.30, y=0.92, label="Release", size=11, color="#6A4C93", fontstyle="italic") - + labs(x="Time (seconds)", y="Amplitude", title="waveform-audio · plotnine · pyplots.ai") + + geom_hline(yintercept=0, color=INK_MUTED, size=0.4, linetype="solid") + + geom_vline(xintercept=attack_time, color=INK_SOFT, size=0.3, linetype="dashed", alpha=0.5) + + geom_vline(xintercept=sustain_time, color=INK_SOFT, size=0.3, linetype="dashed", alpha=0.5) + + annotate("text", x=0.025, y=0.90, label="Attack", size=3.5, color=IMPRINT_PALETTE[0], fontstyle="italic") + + annotate("text", x=0.575, y=0.90, label="Sustain", size=3.5, color=IMPRINT_PALETTE[1], fontstyle="italic") + + annotate("text", x=1.30, y=0.90, label="Release", size=3.5, color=IMPRINT_PALETTE[2], fontstyle="italic") + + labs(x="Time (seconds)", y="Amplitude", title=title) + scale_x_continuous( breaks=np.arange(0, duration + 0.1, 0.25), labels=lambda lst: [f"{v:.2f}" for v in lst], expand=(0.01, 0.01) ) + scale_y_continuous(limits=(-1.0, 1.0), breaks=np.arange(-1.0, 1.1, 0.25)) + theme_minimal() + theme( - figure_size=(16, 9), - text=element_text(size=14, color="#2d2d2d"), - axis_title=element_text(size=20, color="#2d2d2d", margin={"t": 12, "r": 12}), - axis_text=element_text(size=16, color="#555555"), - plot_title=element_text(size=24, weight="bold", color="#1a1a1a", margin={"b": 12}), - panel_background=element_rect(fill="#f5f5f0", color="none"), - plot_background=element_rect(fill="#ffffff", color="none"), - panel_grid_major_y=element_line(color="#dcdcdc", size=0.25, linetype="dotted"), + figure_size=(8, 4.5), + text=element_text(size=7, color=INK), + axis_title=element_text(size=10, color=INK), + axis_text=element_text(size=8, color=INK_SOFT), + plot_title=element_text(size=12, color=INK), + legend_text=element_text(size=8, color=INK_SOFT), + panel_background=element_rect(fill=PAGE_BG, color="none"), + plot_background=element_rect(fill=PAGE_BG, color="none"), + panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.15), panel_grid_major_x=element_blank(), panel_grid_minor=element_blank(), - axis_line_x=element_line(color="#888888", size=0.5), - axis_ticks_major_x=element_line(color="#888888", size=0.4), + axis_line_x=element_line(color=INK_SOFT, size=0.5), + axis_line_y=element_line(color=INK_SOFT, size=0.5), + axis_ticks_major_x=element_line(color=INK_SOFT, size=0.4), axis_ticks_major_y=element_blank(), - plot_margin=0.02, ) ) -plot.save("plot.png", dpi=300, verbose=False) +plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False) diff --git a/plots/waveform-audio/metadata/python/plotnine.yaml b/plots/waveform-audio/metadata/python/plotnine.yaml index 40e0bf536a..b836101d31 100644 --- a/plots/waveform-audio/metadata/python/plotnine.yaml +++ b/plots/waveform-audio/metadata/python/plotnine.yaml @@ -1,39 +1,49 @@ library: plotnine +language: python specification_id: waveform-audio created: '2026-03-07T14:58:25Z' -updated: '2026-03-07T15:08:44Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 22801238614 +updated: '2026-06-03T01:27:56Z' +generated_by: claude-sonnet +workflow_run: 26857483373 issue: 4563 -python_version: 3.14.3 -library_version: 0.15.3 -preview_url: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/plotnine/plot.png -preview_html: null -quality_score: 92 +language_version: 3.13.13 +library_version: 0.15.5 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/plotnine/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/plotnine/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 86 review: strengths: - - Excellent data storytelling through phase-colored waveform (Attack/Sustain/Release) - with boundary markers - - Min/max envelope downsampling properly handles dense waveform display - - Warm panel background and custom color palette create polished professional appearance - - Realistic audio data with harmonics vibrato and noise - - Full spec compliance with all required features implemented + - Three-phase color segmentation (Attack/Sustain/Release) with Imprint palette positions + 1-3 in correct order creates an informative, visually purposeful waveform + - Min/max envelope downsampling (chunk_size=64) correctly avoids aliasing on the + 22050 Hz signal — spec-compliant and technically sound + - All theme tokens properly adapted for both light and dark renders with no dark-on-dark + failures + - Realistic synthetic audio data (220 Hz + harmonics + vibrato + noise) at standard + 22050 Hz sample rate weaknesses: - - Phase annotation label size (11) is smaller than other text elements - - Data downsampling uses list comprehensions instead of vectorized numpy reshape - operations - image_description: 'The plot displays an audio waveform as a filled ribbon symmetric - around a zero baseline on a warm off-white panel background (#f5f5f0). The x-axis - shows "Time (seconds)" from 0.00 to 1.50, and the y-axis shows "Amplitude" from - -1.00 to 1.00. The waveform is colored by three distinct phases: orange for the - short Attack phase (0-0.05s) showing rapid amplitude increase, steel blue for - the Sustain phase (0.05-1.10s) showing steady oscillation at ~0.75 amplitude, - and purple for the Release phase (1.10-1.50s) showing gradual decay to zero. Dashed - vertical lines mark the phase boundaries. Italic phase labels appear near the - top of each region. A subtle horizontal zero-line bisects the waveform. The title - reads "waveform-audio · plotnine · pyplots.ai" in bold at the top. Y-axis has - dotted grid lines; x-axis grid is removed. The waveform clearly shows the harmonic - content with visible beat patterns in the sustain region.' + - Y-axis label reads 'Amplitude' without a unit/range qualifier; change to 'Amplitude + (normalized)' or 'Amplitude [-1, 1]' to earn full VQ-06 points + - Phase annotation labels use size=3.5 (~10 pt) which is too small for annotations + at this canvas size and especially cramped for the narrow Attack region at x~0.025; + increase to size=5-6 + - Design Excellence loses points due to the annotation labels being too small to + function as strong narrative anchors — the phase story is told by color but not + reinforced by legible text emphasis + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (~#FAF8F1) — correct, not pure white + Chrome: Title "waveform-audio · python · plotnine · anyplot.ai" in dark ink — readable. Axis labels "Time (seconds)" and "Amplitude" in dark ink — readable. Tick labels (-1.00 to 1.00 on y, 0.00 to 1.50 on x) in secondary ink — readable. Phase annotations "Attack", "Sustain", "Release" in respective palette colors — small but visible. + Data: Attack ribbon in Imprint green (#009E73), Sustain ribbon in Imprint lavender (#C475FD), Release ribbon in Imprint blue (#4467A3). All ribbons symmetric around zero. Horizontal zero reference line visible. Dashed vertical phase boundary lines visible. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black (~#1A1A17) — correct, not pure black + Chrome: Title and axis labels in light ink (#F0EFE8) — readable against dark background. Tick labels in secondary ink (#B8B7B0) — readable. Phase annotations in palette colors — legible. No dark-on-dark failures detected. + Data: Same Imprint green, lavender, and blue data colors as light render — palette is theme-independent and identical. Zero reference line visible in warm muted gray (INK_MUTED dark token). Grid lines subtle. + Legibility verdict: PASS criteria_checklist: visual_quality: score: 29 @@ -41,63 +51,79 @@ review: items: - id: VQ-01 name: Text Legibility - score: 7 + score: 8 max: 8 passed: true - comment: All major font sizes explicitly set; phase annotation labels at size=11 - slightly small + comment: All font sizes explicitly set (title=12pt, axis_title=10pt, axis_text=8pt); + well-proportioned in both themes - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No overlapping text elements + comment: No text or data collisions in either render - id: VQ-03 name: Element Visibility score: 6 max: 6 passed: true - comment: Waveform clearly visible with min/max envelope rendering + comment: Min/max envelope downsampling correctly represents dense waveform; + ribbons clearly visible - id: VQ-04 name: Color Accessibility - score: 4 - max: 4 + score: 2 + max: 2 passed: true - comment: Orange/blue/purple palette is colorblind-distinguishable + comment: CVD-safe Imprint green/lavender/blue triplet with sufficient hue + separation - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: Good 16:9 layout with balanced margins + comment: Canvas gate passed; waveform fills canvas well; balanced margins - id: VQ-06 name: Axis Labels & Title + score: 1 + max: 2 + passed: false + comment: X-axis 'Time (seconds)' complete; Y-axis 'Amplitude' lacks unit qualifier + like '(normalized)' or '[-1, 1]' + - id: VQ-07 + name: Palette Compliance score: 2 max: 2 passed: true - comment: Time (seconds) with units, Amplitude descriptive + comment: 'Attack=#009E73 (pos 1), Sustain=#C475FD (pos 2), Release=#4467A3 + (pos 3); backgrounds #FAF8F1/#1A1A17 correct' design_excellence: - score: 16 + score: 11 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 5 max: 8 passed: true - comment: Custom phase palette, warm background, intentional typography hierarchy + comment: 'Above configured default: phase-segmented design with intentional + Imprint color assignment and per-phase alpha variation (0.85/0.65/0.55) + reflects real audio dynamics' - id: DE-02 name: Visual Refinement - score: 5 + score: 3 max: 6 passed: true - comment: Removed x-grid, subtle dotted y-grid, warm panel background + comment: Y-only grid at alpha=0.15 subtle; x-grid suppressed; theme_minimal + base; axis lines colored. Above minimal but whitespace and typography could + be tighter - id: DE-03 name: Data Storytelling - score: 5 + score: 3 max: 6 passed: true - comment: Phase-colored waveform with boundary markers creates clear narrative + comment: ADSR phase narrative told via color; Sustain dominance is visually + clear. Phase annotation labels at size=3.5 too small to function as strong + narrative anchors spec_compliance: score: 15 max: 15 @@ -107,47 +133,53 @@ review: score: 5 max: 5 passed: true - comment: Correct audio waveform with filled area symmetric around zero + comment: Correct audio waveform as filled ribbon symmetric around zero - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: All spec features present including envelope rendering + comment: Filled area, semi-transparent fill, horizontal zero line, time x-axis, + amplitude y-axis, min/max envelope — all present - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: X=time, Y=amplitude, correct mapping + comment: Time (0-1.5 s) on x, amplitude (-1 to +1) on y, full data range shown - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Correct title format, phase labels annotated directly + comment: Title is 'waveform-audio · python · plotnine · anyplot.ai'; no legend + (replaced by inline annotations) data_quality: - score: 15 + score: 14 max: 15 items: - id: DQ-01 name: Feature Coverage - score: 6 + score: 5 max: 6 passed: true - comment: Shows attack, sustain, release, harmonics, noise + comment: Attack envelope, sustained oscillation with vibrato, harmonic content, + amplitude decay, noise — strong coverage; minor deduction for harmonic detail + obscured by dense aliasing - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: 220Hz A3 note with harmonics, realistic audio scenario + comment: 220 Hz fundamental (A3 note) with 2nd/3rd/5th harmonics, 22050 Hz + sample rate, 1.5 s duration, vibrato at 5 Hz — all real-world plausible - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Standard sample rate, realistic duration and amplitude + comment: Normalized amplitude [-1, 1], realistic duration, proper sample rate; + np.clip prevents artefacts code_quality: score: 10 max: 10 @@ -157,7 +189,7 @@ review: score: 3 max: 3 passed: true - comment: 'Linear flow: imports, data, plot, save' + comment: 'Linear: imports → tokens → data → plot → save; no functions or classes' - id: CQ-02 name: Reproducibility score: 2 @@ -169,19 +201,20 @@ review: score: 2 max: 2 passed: true - comment: All imports used + comment: All imported symbols including scale_alpha_manual are used - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Clean, well-organized code + comment: Appropriate complexity; no fake UI; chunking loop is clear and purposeful - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png with dpi=300 + comment: plot.save(f'plot-{THEME}.png', dpi=400, width=8, height=4.5, units='in') + — correct library_mastery: score: 7 max: 10 @@ -191,23 +224,28 @@ review: score: 4 max: 5 passed: true - comment: Good grammar of graphics usage with aes, geom_ribbon, scales + comment: Fluent ggplot2 grammar with geom_ribbon dual-aes mapping (fill+alpha), + scale_fill_manual + scale_alpha_manual; annotate, geom_hline, geom_vline + layered naturally - id: LM-02 name: Distinctive Features score: 3 max: 5 passed: true - comment: Categorical fill mapping on geom_ribbon with scale_alpha_manual + comment: Combining geom_ribbon with simultaneous fill+alpha aesthetic mappings + from one categorical variable is a plotnine/ggplot2 distinctive pattern + not easily replicated in matplotlib verdict: APPROVED impl_tags: dependencies: [] techniques: - annotations - layer-composition + - manual-ticks patterns: - data-generation + - iteration-over-groups dataprep: - binning styling: - alpha-blending - - grid-styling