diff --git a/plots/waveform-audio/implementations/python/letsplot.py b/plots/waveform-audio/implementations/python/letsplot.py index f5724f1077..748ec8cf18 100644 --- a/plots/waveform-audio/implementations/python/letsplot.py +++ b/plots/waveform-audio/implementations/python/letsplot.py @@ -1,25 +1,35 @@ -""" pyplots.ai +""" anyplot.ai waveform-audio: Audio Waveform Plot -Library: letsplot 4.8.2 | Python 3.14.3 -Quality: 90/100 | Created: 2026-03-07 +Library: letsplot 4.10.1 | Python 3.13.13 +Quality: 90/100 | Updated: 2026-06-03 """ +import os + import numpy as np import pandas as pd from lets_plot import * # noqa: F403, F401 -from lets_plot.export import ggsave as export_ggsave +from lets_plot.export import ggsave LetsPlot.setup_html() # noqa: F405 -# Data — synthetic audio waveform: tone with harmonics and amplitude envelope +# Theme tokens — Imprint palette, theme-adaptive chrome +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" +GRID_COLOR = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)" + +# Data — synthetic audio waveform: 220 Hz tone with harmonics and ASR amplitude envelope np.random.seed(42) sample_rate = 22050 duration = 1.5 n_samples = int(sample_rate * duration) time = np.linspace(0, duration, n_samples) -# Primary tone (220 Hz) with harmonics fundamental = 220 signal = ( 0.6 * np.sin(2 * np.pi * fundamental * time) @@ -28,7 +38,7 @@ + 0.05 * np.sin(2 * np.pi * fundamental * 5 * time) ) -# Amplitude envelope: attack-sustain-release shape +# Attack-sustain-release amplitude envelope envelope = np.ones(n_samples) attack_samples = int(0.05 * sample_rate) release_samples = int(0.3 * sample_rate) @@ -39,20 +49,16 @@ signal = signal * envelope signal = signal / np.max(np.abs(signal)) -# Downsample using min/max envelope — vectorized binning +# Downsample via min/max envelope binning to avoid aliasing at display resolution n_bins = 800 bin_edges = np.linspace(0, n_samples, n_bins + 1, dtype=int) time_env = np.array([time[(bin_edges[i] + bin_edges[i + 1]) // 2] for i in range(n_bins)]) amp_max = np.array([signal[bin_edges[i] : bin_edges[i + 1]].max() for i in range(n_bins)]) amp_min = np.array([signal[bin_edges[i] : bin_edges[i + 1]].min() for i in range(n_bins)]) - -# Compute amplitude magnitude per bin for color intensity mapping amp_range = amp_max - amp_min -# Segment dataframe: vertical bars from ymin to ymax at each time point df = pd.DataFrame({"time": time_env, "ymin": amp_min, "ymax": amp_max, "intensity": amp_range}) -# Annotation data for waveform sections ann_data = pd.DataFrame( { "time": [0.025, 0.225, 0.55, 0.95, 1.35], @@ -61,16 +67,32 @@ } ) -# Section boundaries section_df = pd.DataFrame({"x": [0.05, 0.4, 0.7, 1.2]}) -# Subtitle with signal description -subtitle = "220 Hz fundamental + harmonics \u00b7 ASR envelope with amplitude dip at 0.4\u20130.7 s" +subtitle = "220 Hz fundamental + harmonics · ASR envelope with amplitude dip at 0.4–0.7 s" + +# Theme-adaptive chrome applied after theme_minimal() +anyplot_chrome = theme( # noqa: F405 + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), # noqa: F405 + panel_background=element_rect(fill=PAGE_BG), # noqa: F405 + panel_grid_major_y=element_line(color=GRID_COLOR, size=0.3), # noqa: F405 + panel_grid_major_x=element_blank(), # noqa: F405 + panel_grid_minor=element_blank(), # noqa: F405 + axis_title=element_text(color=INK, size=12), # noqa: F405 + axis_text=element_text(color=INK_SOFT, size=10), # noqa: F405 + axis_line=element_line(color=INK_SOFT), # noqa: F405 + plot_title=element_text(color=INK, size=16), # noqa: F405 + plot_subtitle=element_text(color=INK_SOFT, size=10), # noqa: F405 + legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), # noqa: F405 + legend_text=element_text(color=INK_SOFT, size=10), # noqa: F405 + legend_title=element_text(color=INK, size=12), # noqa: F405 + legend_position="right", + plot_margin=[40, 20, 20, 20], +) -# Plot — vertical segments for DAW-style waveform rendering plot = ( ggplot(df) # noqa: F405 - # Waveform bars: vertical segments colored by intensity + # DAW-style vertical bars coloured by local amplitude range (Imprint sequential cmap) + geom_segment( # noqa: F405 mapping=aes(x="time", y="ymin", xend="time", yend="ymax", color="intensity"), # noqa: F405 size=1.5, @@ -83,49 +105,37 @@ .line("Max: @ymax") .line("Min: @ymin"), ) - + scale_color_gradient(low="#7bafd4", high="#1a3a5c", name="Amplitude\nRange") # noqa: F405 + # Imprint sequential: brand green (#009E73) → blue (#4467A3) for single-polarity data + + scale_color_gradient(low="#009E73", high="#4467A3", name="Amplitude\nRange") # noqa: F405 # Zero reference line - + geom_hline(yintercept=0, color="#999999", size=0.5, linetype="dashed") # noqa: F405 - # Section boundary markers + + geom_hline(yintercept=0, color=INK_MUTED, size=0.5, linetype="dashed") # noqa: F405 + # Envelope section boundary markers + geom_vline( # noqa: F405 data=section_df, mapping=aes(xintercept="x"), # noqa: F405 - color="#CCCCCC", + color=INK_MUTED, size=0.4, linetype="dotted", ) - # Section annotations for storytelling + # Section labels — geom_text size is in mm (~2.845 mm = 1 pt) + geom_text( # noqa: F405 data=ann_data, mapping=aes(x="time", y="y", label="label"), # noqa: F405 - size=11, - color="#1a3a5c", + size=4, + color=INK, fontface="italic", ) + scale_x_continuous(name="Time (seconds)", limits=[0, duration]) # noqa: F405 + scale_y_continuous( # noqa: F405 name="Amplitude", limits=[-1.15, 1.18], breaks=[-1.0, -0.5, 0.0, 0.5, 1.0] ) - + labs( # noqa: F405 - title="waveform-audio \u00b7 letsplot \u00b7 pyplots.ai", subtitle=subtitle - ) - + ggsize(1600, 900) # noqa: F405 + + labs(title="waveform-audio · python · letsplot · anyplot.ai", subtitle=subtitle) # noqa: F405 + # Canvas: 800×450 × scale=4 → 3200×1800 px (landscape 16:9) + + ggsize(800, 450) # noqa: F405 + theme_minimal() # noqa: F405 - + theme( # noqa: F405 - axis_text=element_text(size=16), # noqa: F405 - axis_title=element_text(size=20), # noqa: F405 - plot_title=element_text(size=24), # noqa: F405 - plot_subtitle=element_text(size=16, color="#555555"), # noqa: F405 - legend_text=element_text(size=14), # noqa: F405 - legend_title=element_text(size=16), # noqa: F405 - legend_position="right", # noqa: F405 - panel_grid_major_y=element_line(color="#E8E8E8", size=0.3), # noqa: F405 - panel_grid_major_x=element_blank(), # noqa: F405 - panel_grid_minor=element_blank(), # noqa: F405 - plot_margin=[40, 20, 20, 20], # noqa: F405 - ) + + anyplot_chrome ) -# Save -export_ggsave(plot, filename="plot.png", path=".", scale=3) -export_ggsave(plot, filename="plot.html", path=".") +# Save PNG (3200×1800 px) and interactive HTML — path="." keeps files in the current dir +ggsave(plot, filename=f"plot-{THEME}.png", path=".", scale=4) +ggsave(plot, filename=f"plot-{THEME}.html", path=".") diff --git a/plots/waveform-audio/metadata/python/letsplot.yaml b/plots/waveform-audio/metadata/python/letsplot.yaml index c3e09271a7..fce198790a 100644 --- a/plots/waveform-audio/metadata/python/letsplot.yaml +++ b/plots/waveform-audio/metadata/python/letsplot.yaml @@ -1,109 +1,128 @@ library: letsplot +language: python specification_id: waveform-audio created: '2026-03-07T15:01:44Z' -updated: '2026-03-07T15:30:15Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 22801238684 +updated: '2026-06-03T01:24:08Z' +generated_by: claude-sonnet +workflow_run: 26857674811 issue: 4563 -python_version: 3.14.3 -library_version: 4.8.2 -preview_url: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/letsplot/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/letsplot/plot.html +language_version: 3.13.13 +library_version: 4.10.1 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/letsplot/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/letsplot/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/letsplot/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/letsplot/plot-dark.html quality_score: 90 review: strengths: - - Excellent data storytelling with ASR envelope section annotations creating clear - narrative - - Creative use of geom_segment with color gradient for DAW-style waveform rendering - - Cohesive blue color palette from light to dark navy - - Perfect spec compliance with all required features - - Clean, well-structured code with proper min/max envelope downsampling + - DAW-style vertical segment rendering with Imprint sequential colormap (#009E73→#4467A3) + adds meaningful amplitude-range encoding beyond basic waveform display + - ASR phase annotations (Attack/Sustain/Dip/Release) with dotted boundary lines + create clear data storytelling — viewer immediately understands the envelope dynamics + - Correct min/max envelope binning (22050 samples → 800 bins) prevents aliasing + artifacts at display resolution, matching spec guidance + - 'Perfect theme-adaptive chrome in both renders: #FAF8F1 light background and #1A1A17 + dark background with all text tokens properly switched' + - Clean KISS code structure with np.random.seed(42), all sizes explicitly set, and + both PNG + HTML output files saved correctly weaknesses: - - Minor white gaps visible between waveform segments creating streak artifacts - - Annotation text could be slightly larger for better visual balance - - Could leverage letsplot-specific features more deeply (e.g., geom_ribbon for filled - area) - image_description: 'The plot displays a DAW-style audio waveform using vertical - segments colored by a blue gradient (light blue for low amplitude range, dark - navy for high amplitude range). The x-axis shows "Time (seconds)" from 0 to 1.5s, - and the y-axis shows "Amplitude" from -1 to 1 with breaks at 0.5 intervals. A - dashed gray zero-line runs horizontally across the center. Dotted vertical lines - mark section boundaries at 0.05s, 0.4s, 0.7s, and 1.2s. Italic dark-blue labels - at the top identify five sections: "Attack", "Sustain", "Dip", "Sustain", "Release". - The waveform clearly shows the ASR envelope shape — a quick ramp-up, sustained - full amplitude, a visible amplitude dip between 0.4-0.7s, return to full amplitude, - and a gradual release taper. The title reads "waveform-audio · letsplot · pyplots.ai" - with a descriptive subtitle. An "Amplitude Range" color legend appears on the - right. The overall color scheme is cohesive blue tones on a clean white background - with subtle y-axis grid lines.' + - 'Design excellence is good but not publication-ready: section annotation labels + (Attack/Sustain/Dip/Release) all share the same thin italic style with no visual + weight hierarchy — a bolder or larger label for the primary phase transitions + would add polish' + - 'LM-02 could be higher: the implementation uses layer_tooltips() but misses an + opportunity to use geom_ribbon() as the more idiomatic lets-plot ''filled area'' + geom for waveform rendering, which would better match the spec''s ''filled area + mirrored above and below zero'' description' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct Imprint light surface + Chrome: Title "waveform-audio · python · letsplot · anyplot.ai" in dark ink, clearly readable. Subtitle in INK_SOFT gray. X-axis label "Time (seconds)" and Y-axis label "Amplitude" in dark ink at size 12. Tick labels at size 10 in INK_SOFT, all visible. Section annotations (Attack, Sustain, Dip, Sustain, Release) in dark italic text above the waveform. + Data: Dense vertical segment bars rendered with Imprint sequential gradient from #009E73 (green, low amplitude range) to #4467A3 (blue, high amplitude range). Waveform clearly shows ASR envelope: tall sustain sections (±1.0), reduced dip section (±0.5), smooth release taper. Dotted vertical boundary lines in INK_MUTED at section transitions. Dashed zero reference line across full width. Colorbar legend "Amplitude Range" on right with values 0.5, 1, 1.5, 2. + Legibility verdict: PASS — all text readable against warm off-white background + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct Imprint dark surface + Chrome: Title in #F0EFE8 (INK dark token), clearly readable against dark background. Subtitle in #B8B7B0 (INK_SOFT dark). Axis labels and tick labels in light tones, all readable. Section annotations in light INK color — no dark-on-dark failures detected. + Data: Waveform segment colors identical to light render — same #009E73→#4467A3 gradient. Envelope shape identical. Zero dashed line visible in theme-adaptive INK_MUTED. Section dotted lines visible. Colorbar legend readable with light text. + Legibility verdict: PASS — all text readable against near-black background, no dark-on-dark failures criteria_checklist: visual_quality: - score: 28 + score: 30 max: 30 items: - id: VQ-01 name: Text Legibility - score: 7 + score: 8 max: 8 passed: true - comment: All font sizes explicitly set. Annotation text at size=11 slightly - small relative to other elements but readable. + comment: All font sizes explicitly set (title=16, axis=12, tick=10, annotations=4mm). + Readable in both themes. - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No overlapping text. Section labels well-spaced. + comment: Section annotations at y=1.07 clear of waveform. No overlapping text. - id: VQ-03 name: Element Visibility - score: 5 + score: 6 max: 6 passed: true - comment: Waveform segments clearly visible with 800 bins. Minor white gaps - between segments. + comment: 800-bin envelope with alpha=0.85, size=1.5 — ideal density for waveform + rendering. - id: VQ-04 name: Color Accessibility - score: 4 - max: 4 + score: 2 + max: 2 passed: true - comment: Blue gradient is fully colorblind-safe with good contrast. + comment: Imprint sequential gradient is CVD-safe. Reference lines in neutral + INK_MUTED. - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: Plot fills canvas well. Balanced margins with legend on right. + comment: 3200x1800 canvas (gate passed). Legend well placed. Margins generous + with plot_margin=[40,20,20,20]. - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: Time (seconds) includes units. Amplitude is self-descriptive. + comment: 'X: ''Time (seconds)'', Y: ''Amplitude'' — descriptive with units.' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'scale_color_gradient(low=''#009E73'', high=''#4467A3'') = imprint_seq. + Correct backgrounds #FAF8F1/#1A1A17.' design_excellence: - score: 15 + score: 13 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 5 max: 8 passed: true - comment: Custom blue gradient palette, italic section annotations, descriptive - subtitle. Clearly above defaults. + comment: Thoughtful multi-layer design with color encoding of amplitude range. + Above defaults but not FiveThirtyEight-level. - id: DE-02 name: Visual Refinement score: 4 max: 6 passed: true - comment: theme_minimal, x-grid removed, subtle y-grid, custom margins. Good - refinement. + comment: X-grid removed, Y-grid with rgba(26,26,23,0.15) opacity, theme_minimal + + custom chrome, clean legend. - id: DE-03 name: Data Storytelling - score: 5 + score: 4 max: 6 passed: true - comment: Section annotations create clear ASR envelope narrative. Color intensity - adds data dimension. + comment: ASR phase labels with boundary lines create clear narrative. Color + encoding reinforces amplitude story. spec_compliance: score: 15 max: 15 @@ -113,26 +132,27 @@ review: score: 5 max: 5 passed: true - comment: Correct audio waveform with min/max envelope segments. + comment: Correct audio waveform visualization. - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: 'All spec features present: symmetric waveform, semi-transparent, - zero-line, correct axes, envelope rendering.' + comment: Zero reference line, time x-axis, amplitude -1 to +1, min/max envelope + rendering, synthetic data — all present. - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: X=time, Y=amplitude correctly mapped. + comment: X=time seconds, Y=amplitude ±1.0. Full 1.5s duration shown. - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title format correct. Legend label matches mapped aesthetic. + comment: 'Title: ''waveform-audio · python · letsplot · anyplot.ai''. Colorbar + legend ''Amplitude Range'' correct.' data_quality: score: 15 max: 15 @@ -142,22 +162,21 @@ review: score: 6 max: 6 passed: true - comment: Shows attack, sustain, dip, sustain, release. Harmonics create realistic - waveform texture. + comment: Shows attack ramp, full sustain, dip section, second sustain, release + taper — all envelope phases visible. - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: 220 Hz fundamental with harmonics, 22050 Hz sample rate, ASR envelope - — real audio engineering. + comment: 220 Hz musical note with harmonics, ASR envelope — realistic audio + engineering context. - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: 'Standard audio values: 1.5s duration, 22050 sample rate, normalized - amplitude.' + comment: 22050 Hz sample rate, normalized -1 to +1, 1.5s duration — all domain-appropriate. code_quality: score: 10 max: 10 @@ -167,7 +186,7 @@ review: score: 3 max: 3 passed: true - comment: Clean imports-data-plot-save structure. No functions or classes. + comment: 'Flat: imports → tokens → data → plot → save.' - id: CQ-02 name: Reproducibility score: 2 @@ -179,19 +198,19 @@ review: score: 2 max: 2 passed: true - comment: All imports used. + comment: os, numpy, pandas, lets_plot, ggsave — all used. - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Clean vectorized envelope downsampling. Appropriate complexity. + comment: Clean, appropriate complexity, no fake UI. - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png via export_ggsave with scale=3. + comment: Saves plot-{THEME}.png and plot-{THEME}.html correctly. library_mastery: score: 7 max: 10 @@ -201,27 +220,28 @@ review: score: 4 max: 5 passed: true - comment: 'Good ggplot grammar: aes, geom_segment, scale_color_gradient, theme - customization, ggsize.' + comment: Proper ggplot grammar, scale_color_gradient, theme layering, layer_tooltips + — idiomatic letsplot. - id: LM-02 name: Distinctive Features score: 3 max: 5 passed: true - comment: Uses layer_tooltips() with custom formatting (lets-plot distinctive). - HTML export alongside PNG. + comment: layer_tooltips() with format() configuration is distinctive to letsplot. + HTML export native. Could use geom_ribbon for more idiomatic filled area. verdict: APPROVED impl_tags: dependencies: [] techniques: - annotations - - layer-composition - hover-tooltips - html-export + - layer-composition patterns: - data-generation dataprep: - binning + - normalization styling: - custom-colormap - alpha-blending