diff --git a/plots/waveform-audio/implementations/python/altair.py b/plots/waveform-audio/implementations/python/altair.py index 76d102e4ab..1408903db9 100644 --- a/plots/waveform-audio/implementations/python/altair.py +++ b/plots/waveform-audio/implementations/python/altair.py @@ -1,22 +1,33 @@ -""" pyplots.ai +""" anyplot.ai waveform-audio: Audio Waveform Plot -Library: altair 6.0.0 | Python 3.14.3 -Quality: 92/100 | Created: 2026-03-07 +Library: altair 6.1.0 | Python 3.13.13 +Quality: 92/100 | Updated: 2026-06-03 """ +import os + import altair as alt import numpy as np import pandas as pd +from PIL import Image + + +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" +# Imprint palette — semantic anchor for clipping regions +CLIP_COLOR = "#AE3030" # matte red: error/clipping role -# Data - synthetic audio waveform: sine tone with harmonics and amplitude envelope -np.random.seed(42) +# Data - synthetic audio: 440 Hz tone with harmonics and amplitude envelope sample_rate = 22050 duration = 1.5 num_samples = int(sample_rate * duration) time = np.linspace(0, duration, num_samples) -# Primary tone (440 Hz) with harmonics fundamental = 440 signal = ( 0.6 * np.sin(2 * np.pi * fundamental * time) @@ -24,21 +35,31 @@ + 0.15 * np.sin(2 * np.pi * 3 * fundamental * time) ) -# Amplitude envelope: attack-sustain(with dip)-release with brief clipping section +# Attack-sustain-release envelope with amplitude dip and brief clipping boost envelope = np.ones_like(time) attack = int(0.05 * sample_rate) release = int(0.3 * sample_rate) envelope[:attack] = np.linspace(0, 1, attack) envelope[-release:] = np.linspace(1, 0, release) -envelope[int(0.4 * sample_rate) : int(0.7 * sample_rate)] *= 0.5 -# Boost a short section to demonstrate clipping behavior + +# Smooth amplitude dip (0.4-0.7 s) via cosine taper — avoids abrupt rectangular step +dip_start = int(0.4 * sample_rate) +dip_end = int(0.7 * sample_rate) +transition_len = int(0.035 * sample_rate) +t_fade = np.linspace(0, 1, transition_len) +dip_mult = np.ones_like(time) +dip_mult[dip_start : dip_start + transition_len] = 1.0 - 0.25 * (1 - np.cos(np.pi * t_fade)) +dip_mult[dip_start + transition_len : dip_end - transition_len] = 0.5 +dip_mult[dip_end - transition_len : dip_end] = 0.5 + 0.25 * (1 - np.cos(np.pi * t_fade)) +envelope *= dip_mult + envelope[int(0.15 * sample_rate) : int(0.25 * sample_rate)] *= 1.35 signal = signal * envelope signal = np.clip(signal, -1.0, 1.0) -# Vectorized min/max envelope binning for efficient rendering -num_bins = 2000 +# Min/max envelope binning — 600 bins avoids sub-pixel vertical striping artifacts +num_bins = 600 bin_size = num_samples // num_bins usable = num_bins * bin_size signal_trimmed = signal[:usable].reshape(num_bins, bin_size) @@ -53,25 +74,25 @@ ) df["clipped"] = (df["amp_max"] >= 0.99) | (df["amp_min"] <= -0.99) -# Shared x/y encodings +# Shared encodings x_enc = alt.X("time:Q", title="Time (seconds)", axis=alt.Axis(format=".2f", tickCount=8)) y_enc = alt.Y("amp_min:Q", title="Amplitude", scale=alt.Scale(domain=[-1.0, 1.0])) # Nearest-point selection for interactive crosshair nearest = alt.selection_point(nearest=True, on="pointerover", fields=["time"], empty=False) -# Main waveform fill with vertical gradient +# Main waveform: Imprint brand green (#009E73) vertical gradient waveform_gradient = ( alt.Chart(df) .mark_area( - interpolate="monotone", + interpolate="linear", color=alt.Gradient( gradient="linear", stops=[ - alt.GradientStop(color="rgba(48, 105, 152, 0.10)", offset=0), - alt.GradientStop(color="rgba(48, 105, 152, 0.60)", offset=0.45), - alt.GradientStop(color="rgba(48, 105, 152, 0.60)", offset=0.55), - alt.GradientStop(color="rgba(48, 105, 152, 0.10)", offset=1), + alt.GradientStop(color="rgba(0, 158, 115, 0.28)", offset=0), + alt.GradientStop(color="rgba(0, 158, 115, 0.60)", offset=0.45), + alt.GradientStop(color="rgba(0, 158, 115, 0.60)", offset=0.55), + alt.GradientStop(color="rgba(0, 158, 115, 0.28)", offset=1), ], x1=0, x2=0, @@ -83,34 +104,37 @@ .encode(x=x_enc, y=y_enc, y2="amp_max:Q") ) -# Clipped regions overlay (filtered layer with red tint) +# Clipped regions overlay — Imprint matte red (#AE3030) semantic anchor clipped_overlay = ( alt.Chart(df) - .mark_area(interpolate="monotone", color="rgba(180, 50, 50, 0.50)", line=False) + .mark_area(interpolate="linear", color="rgba(174, 48, 48, 0.50)", line=False) .encode(x="time:Q", y=y_enc, y2="amp_max:Q") .transform_filter(alt.datum.clipped == True) # noqa: E712 ) -# Zero baseline reference line +# Zero baseline reference line (theme-adaptive) zero_line = ( alt.Chart(pd.DataFrame({"y": [0]})) - .mark_rule(color="#306998", strokeWidth=1.5, opacity=0.35, strokeDash=[6, 4]) - .encode(y="y:Q") + .mark_rule(strokeWidth=1.5, opacity=0.35, strokeDash=[6, 4]) + .encode(y="y:Q", color=alt.value(INK_SOFT)) ) -# Clipping threshold lines at ±1.0 +# Clipping threshold lines at ±1.0 (semantic matte red) clip_lines = ( alt.Chart(pd.DataFrame({"y": [-1.0, 1.0]})) - .mark_rule(color="#b43232", strokeWidth=0.8, opacity=0.3, strokeDash=[3, 5]) - .encode(y="y:Q") + .mark_rule(strokeWidth=0.8, opacity=0.3, strokeDash=[3, 5]) + .encode(y="y:Q", color=alt.value(CLIP_COLOR)) ) -# Interactive vertical crosshair following the pointer +# Interactive crosshair following pointer crosshair_rule = ( - alt.Chart(df).mark_rule(color="#306998", strokeWidth=1, opacity=0.5).encode(x="time:Q").transform_filter(nearest) + alt.Chart(df) + .mark_rule(strokeWidth=1, opacity=0.5) + .encode(x="time:Q", color=alt.value(INK_SOFT)) + .transform_filter(nearest) ) -# Invisible selection trigger layer with tooltips +# Invisible selection trigger with tooltips selection_layer = ( alt.Chart(df) .mark_point(opacity=0) @@ -126,24 +150,49 @@ .add_params(nearest) ) -# Compose layers and style +# Compose layers and apply theme-adaptive configuration chart = ( alt.layer(waveform_gradient, clipped_overlay, zero_line, clip_lines, crosshair_rule, selection_layer) .properties( - width=1600, - height=900, + width=620, + height=320, + background=PAGE_BG, title=alt.Title( - "waveform-audio · altair · pyplots.ai", - fontSize=28, + "waveform-audio · python · altair · anyplot.ai", + fontSize=16, subtitle="440 Hz tone with harmonics · attack–sustain–release envelope · clipped region highlighted", - subtitleFontSize=16, - subtitleColor="#666666", + subtitleFontSize=10, ), ) - .configure_axis(labelFontSize=18, titleFontSize=22, gridOpacity=0.12, domainColor="#aaaaaa", tickColor="#aaaaaa") - .configure_view(strokeWidth=0) + .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=620, continuousHeight=320) + .configure_axis( + labelFontSize=10, + titleFontSize=12, + gridColor=INK, + gridOpacity=0.15, + domainColor=INK_SOFT, + tickColor=INK_SOFT, + labelColor=INK_SOFT, + titleColor=INK, + ) + .configure_title(color=INK, subtitleColor=INK_MUTED) ) -# Save -chart.save("plot.png", scale_factor=3.0) -chart.interactive().save("plot.html") +# Save PNG and pad to exact 3200×1800 (landscape target) +chart.save(f"plot-{THEME}.png", scale_factor=4.0) + +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}. " + "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") + +# Save interactive HTML +chart.interactive().save(f"plot-{THEME}.html") diff --git a/plots/waveform-audio/metadata/python/altair.yaml b/plots/waveform-audio/metadata/python/altair.yaml index 240fdffc2e..9b198acf88 100644 --- a/plots/waveform-audio/metadata/python/altair.yaml +++ b/plots/waveform-audio/metadata/python/altair.yaml @@ -1,42 +1,60 @@ library: altair +language: python specification_id: waveform-audio created: '2026-03-07T14:58:17Z' -updated: '2026-03-07T15:11:56Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 22801238637 +updated: '2026-06-03T01:34:44Z' +generated_by: claude-sonnet +workflow_run: 26857420845 issue: 4563 -python_version: 3.14.3 -library_version: 6.0.0 -preview_url: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/altair/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/altair/plot.html +language_version: 3.13.13 +library_version: 6.1.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/altair/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/altair/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/altair/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/altair/plot-dark.html quality_score: 92 review: strengths: - - Excellent vertical gradient fill creating a professional DAW-like aesthetic - - Clipping region highlighted in red with transform_filter for effective visual - storytelling - - Min/max envelope binning (2000 bins from 33075 samples) for efficient rendering - - 'Strong use of Altair-specific features: gradients, selections, layer composition' - - Complete spec compliance with all required features present - - Clean well-structured code with vectorized operations + - Vertical gradient fill on mark_area is a sophisticated aesthetic choice — the + opacity taper from center (0.60) to edges (0.28) gives the waveform visual depth + - Semantic use of Imprint matte red (#AE3030) for clipping overlay is intentional + and correctly represents the audio engineering concept + - Min/max envelope binning (600 bins) is the correct approach for this dense signal + and avoids sub-pixel aliasing artifacts + - Theme-adaptive chrome is fully wired (PAGE_BG, INK, INK_SOFT, INK_MUTED) — both + renders pass legibility check + - 'Interactive features are genuinely altair-native: alt.selection_point crosshair + + transform_filter + tooltip layer composition' + - Zero baseline and ±1.0 clipping threshold lines are clean design refinements + - 'Data is highly realistic: A440 fundamental with harmonics, proper attack-sustain-release + envelope, realistic clipping segment' + - All font sizes explicitly set (title=16, subtitle=10, axis label=12, tick label=10) + - Canvas correctly sized at 3200×1800 with PIL pad-to-target block + - Both PNG and HTML outputs saved correctly weaknesses: - - Minor vertical striping visible in the area chart from individual bin rendering - - np.random.seed(42) is set but no random functions are actually called - image_description: The plot displays an audio waveform as a filled area chart with - time (0–1.6 seconds) on the x-axis and normalized amplitude (-1.0 to +1.0) on - the y-axis. The main waveform body uses a steel-blue vertical gradient that is - more opaque near the zero baseline and fades toward the amplitude extremes, creating - a polished DAW-like appearance. A clipped region around 0.15–0.25 seconds is highlighted - with a semi-transparent red overlay where amplitude hits ±1.0. The envelope clearly - shows an attack phase (0–0.05s), a loud section with clipping, a sustained region, - a dip around 0.4–0.7s (half amplitude), then gradual release to silence. A dashed - blue zero-line and faint dashed red clip-threshold lines at ±1.0 are visible. - The title reads "waveform-audio · altair · pyplots.ai" with a descriptive subtitle. - Axis labels are large and readable. Grid lines are extremely subtle. The view - frame is removed for a clean look. + - Both X and Y grid lines are present (11 horizontal + 9 vertical lines) — for a + waveform time series, Y-axis-only grid would be cleaner and less visually heavy + - Horizontal grid lines at exactly y=1.0 and y=-1.0 (the y-axis domain boundaries) + create a 'boxed top/bottom' visual effect; slight y-domain padding (e.g. domain=[-1.05, + 1.05]) would give the waveform breathing room + - No visual annotation distinguishing the amplitude dip region (0.4–0.7 s) — a subtle + annotation or legend callout could help viewers understand the dynamics phases + beyond what the subtitle states + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) — correct, clearly distinct from pure white + Chrome: Title "waveform-audio · python · altair · anyplot.ai" in bold dark ink, subtitle in lighter INK_MUTED, axis label "Time (seconds)" on x-axis and "Amplitude" on y-axis — all clearly readable against light background + Data: Main waveform as a teal/green filled area (#009E73 with gradient opacity 0.28–0.60) symmetric around zero; clipped region (~0.15–0.25 s) overlaid in brownish-red (#AE3030, 50% opacity); dashed zero baseline at y=0; subtle dashed threshold lines at y=±1.0; green fills from amplitude -0.8 to +0.8 at peak, narrowing to ±0.4 during the amplitude dip (0.40–0.70 s) and tapering to zero in the release + Legibility verdict: PASS — all text clearly readable, no light-on-light issues + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17) — correct, clearly distinct from pure black + Chrome: Title in near-white (#F0EFE8), subtitle in lighter gray, axis labels and tick labels all in light INK_SOFT tones — all clearly readable against dark background; no dark-on-dark failures detected + Data: Identical brand green (#009E73) gradient fill waveform and identical #AE3030 clipping overlay compared to light render — data colors unchanged as required; zero line and threshold lines visible with light-colored dashes + Legibility verdict: PASS — all chrome correctly flips to light-on-dark, data colors identical to light render criteria_checklist: visual_quality: - score: 29 + score: 30 max: 30 items: - id: VQ-01 @@ -44,65 +62,77 @@ review: score: 8 max: 8 passed: true - comment: 'All font sizes explicitly set: title 28pt, axis titles 22pt, tick - labels 18pt, subtitle 16pt' + comment: All font sizes explicitly set (title=16, subtitle=10, axis title=12, + tick=10); proportional, readable in both themes - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No overlapping text or elements + comment: No overlapping text elements in either render - id: VQ-03 name: Element Visibility - score: 5 + score: 6 max: 6 passed: true - comment: Waveform envelope clearly visible with gradient; slight vertical - striping from bin rendering + comment: Waveform clearly visible; gradient fill effective; clipping overlay + distinct; reference lines visible - id: VQ-04 name: Color Accessibility - score: 4 - max: 4 + score: 2 + max: 2 passed: true - comment: Blue/red scheme distinguishable for colorblind users + comment: Green and matte-red are perceptually distinct under CVD; not sole + distinguishing signal - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: 4800x2700 with balanced margins, good canvas utilization + comment: 3200x1800 canvas correctly produced; plot fills canvas proportionally; + balanced margins - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: Time (seconds) with units, Amplitude is appropriate for normalized - scale + comment: Time (seconds) on x-axis with unit; Amplitude on y-axis is descriptive + for a normalized signal + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Main series uses #009E73 (brand green); clipping uses #AE3030 (semantic + anchor); backgrounds #FAF8F1/#1A1A17 correct; theme chrome flips correctly' design_excellence: - score: 14 + score: 13 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 5 max: 8 passed: true - comment: Strong design with vertical gradient, Python Blue palette, red clipping - overlay, descriptive subtitle + comment: 'Above defaults: vertical gradient fill with center-dense opacity + is intentional and effective; semantic red for clipping; subtitle adds context + — but overall still ''well-configured altair'' rather than publication-ready' - id: DE-02 name: Visual Refinement score: 4 max: 6 passed: true - comment: View stroke removed, grid opacity 0.12, custom axis colors, dashed - reference lines + comment: 'Clear refinements: dashed zero baseline, clipping threshold lines, + gridOpacity=0.15, strokeWidth=0 on view — but both X and Y grid lines present + (heavier than minimalist ideal)' - id: DE-03 name: Data Storytelling score: 4 max: 6 passed: true - comment: Red clipping region as focal point, amplitude dip creates narrative, - subtitle narrates the story + comment: 'Clear focal point: red clipping region immediately draws attention; + attack-sustain-release dynamics visible; subtitle labels all features — + good visual hierarchy' spec_compliance: score: 15 max: 15 @@ -112,26 +142,29 @@ review: score: 5 max: 5 passed: true - comment: Correct audio waveform as filled area chart symmetric around zero + comment: Correct audio waveform as filled area with min/max envelope rendering - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: 'All spec features present: filled area, semi-transparent gradient, - zero-line, envelope rendering, synthetic data' + comment: 'All spec features: filled area symmetric around zero, semi-transparent + fill, zero baseline, time on x, amplitude on y, min/max envelope, synthetic + data' - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: X=time, Y=amplitude correctly assigned with explicit scale domain + comment: Time (seconds) on x-axis, normalized amplitude -1.0 to 1.0 on y-axis - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title format correct, no legend needed for single-series waveform + comment: Title is exactly 'waveform-audio · python · altair · anyplot.ai'; + informative subtitle; no legend needed (appropriate for this single-waveform + chart) data_quality: score: 15 max: 15 @@ -141,21 +174,22 @@ review: score: 6 max: 6 passed: true - comment: Shows dynamics, clipping, amplitude dip, harmonics, attack-sustain-release - envelope + comment: Shows attack, sustain, release, clipping segment, amplitude dip, + fundamental with 2nd and 3rd harmonics - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: 440 Hz concert A with harmonics at 22050 Hz sample rate - real audio - engineering scenario + comment: A440 musical note with harmonics = real audio engineering scenario; + clipping = realistic issue in recordings - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Realistic sample rate, duration, and normalized amplitude values + comment: Normalized amplitude -1.0 to 1.0; 22050 Hz sample rate; 1.5 s duration + — all realistic and plausible code_quality: score: 10 max: 10 @@ -165,32 +199,33 @@ review: score: 3 max: 3 passed: true - comment: 'Clean linear flow: imports, data, envelope, binning, chart layers, - compose, save' + comment: Clean imports → data → plot → save structure; no functions or classes - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Seed set (data is deterministic via sine waves) + comment: Fully deterministic — no random values used - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All three imports used + comment: All 5 imports (os, altair, numpy, pandas, PIL.Image) are used - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Well-organized with vectorized binning, appropriate complexity + comment: Clean, Pythonic altair layer composition; appropriate complexity + for the visualization - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png and plot.html + comment: Saves plot-{THEME}.png and plot-{THEME}.html; uses current altair + 6.1.0 API library_mastery: score: 9 max: 10 @@ -200,26 +235,29 @@ review: score: 5 max: 5 passed: true - comment: 'Expert use of declarative grammar: layered composition, encodings, - selections, transforms, configure' + comment: Expert use of layer composition, encoding types, configure_* methods, + transform_filter, alt.Title with subtitle - id: LM-02 name: Distinctive Features score: 4 max: 5 passed: true - comment: alt.Gradient, selection_point with nearest, transform_filter, layer - composition, interactive tooltips + comment: alt.Gradient on mark_area, alt.selection_point with nearest+pointerover, + transform_filter on datum condition — genuinely altair/vega-lite distinctive; + HTML export verdict: APPROVED impl_tags: - dependencies: [] + dependencies: + - pillow techniques: - - layer-composition - hover-tooltips - html-export + - layer-composition patterns: - data-generation + - matrix-construction dataprep: - binning styling: - gradient-fill - - grid-styling + - alpha-blending