diff --git a/plots/waveform-audio/implementations/python/seaborn.py b/plots/waveform-audio/implementations/python/seaborn.py index 4c06653c4a..2e9981cd25 100644 --- a/plots/waveform-audio/implementations/python/seaborn.py +++ b/plots/waveform-audio/implementations/python/seaborn.py @@ -1,9 +1,11 @@ -""" pyplots.ai +""" anyplot.ai waveform-audio: Audio Waveform Plot -Library: seaborn 0.13.2 | Python 3.14.3 -Quality: 90/100 | Created: 2026-03-07 +Library: seaborn 0.13.2 | Python 3.13.13 +Quality: 92/100 | Updated: 2026-06-03 """ +import os + import matplotlib.pyplot as plt import numpy as np import pandas as pd @@ -11,6 +13,33 @@ from matplotlib.patches import Patch +# 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" +BRAND = "#009E73" # Imprint palette position 1 — ALWAYS first series + +sns.set_theme( + style="ticks", + rc={ + "figure.facecolor": PAGE_BG, + "axes.facecolor": PAGE_BG, + "axes.edgecolor": INK_SOFT, + "axes.labelcolor": INK, + "text.color": INK, + "xtick.color": INK_SOFT, + "ytick.color": INK_SOFT, + "grid.color": INK, + "grid.alpha": 0.15, + "grid.linewidth": 0.8, + "legend.facecolor": ELEVATED_BG, + "legend.edgecolor": INK_SOFT, + }, +) + # Data np.random.seed(42) sample_rate = 22050 @@ -44,106 +73,91 @@ signal += np.random.normal(0, 0.01, num_samples) signal = np.clip(signal, -1.0, 1.0) -# Downsample for smooth envelope rendering +# Bin samples for seaborn's percentile-band rendering +# Each chunk covers ~3.6 ms; seaborn computes min-to-max range at each bin natively chunk_size = 80 num_chunks = num_samples // chunk_size -time_chunks = time[: num_chunks * chunk_size].reshape(num_chunks, chunk_size) -signal_chunks = signal[: num_chunks * chunk_size].reshape(num_chunks, chunk_size) +time_chunked = time[: num_chunks * chunk_size].reshape(num_chunks, chunk_size) +signal_chunked = signal[: num_chunks * chunk_size].reshape(num_chunks, chunk_size) -env_time = time_chunks.mean(axis=1) -env_max = signal_chunks.max(axis=1) -env_min = signal_chunks.min(axis=1) +env_time = time_chunked.mean(axis=1) +env_max = signal_chunked.max(axis=1) +env_min = signal_chunked.min(axis=1) -# Smooth the envelope to remove oscillation jaggedness +# Classify bins as Loud vs Quiet via smoothed RMS kernel = np.ones(5) / 5 -env_max = np.convolve(env_max, kernel, mode="same") -env_min = np.convolve(env_min, kernel, mode="same") - -# Classify segments for storytelling: loud vs quiet regions +env_max_smooth = np.convolve(env_max, kernel, mode="same") +env_min_smooth = np.convolve(env_min, kernel, mode="same") smooth_kernel = np.ones(15) / 15 -rms = np.sqrt(np.convolve((env_max - env_min) ** 2, smooth_kernel, mode="same")) +rms = np.sqrt(np.convolve((env_max_smooth - env_min_smooth) ** 2, smooth_kernel, mode="same")) rms_threshold = np.median(rms) * 1.1 segment_label = np.where(rms > rms_threshold, "Loud", "Quiet") -# Build a long-form DataFrame for seaborn envelope edges -df_env = pd.DataFrame( - { - "Time (seconds)": np.concatenate([env_time, env_time]), - "Amplitude": np.concatenate([env_max, env_min]), - "edge": ["upper"] * len(env_time) + ["lower"] * len(env_time), - } -) - -# Color palette for dynamics emphasis -dynamics_palette = {"Loud": "#306998", "Quiet": "#89ABD0"} +# Long-form DataFrame: every sample labeled with its time-bin center +# seaborn lineplot errorbar=('pi', 100) => 0th–100th percentile = envelope min/max +df_long = pd.DataFrame({"Time (s)": np.repeat(env_time, chunk_size), "Amplitude": signal_chunked.flatten()}) # Plot -sns.set_theme( - style="whitegrid", - context="talk", - font_scale=1.2, - rc={ - "grid.alpha": 0.15, - "grid.linewidth": 0.8, - "axes.spines.top": False, - "axes.spines.right": False, - "axes.edgecolor": "#444444", - }, -) - -fig, ax = plt.subplots(figsize=(16, 9)) +fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400) -# Fill quiet regions first (background layer), then loud on top -for label, color, alpha in [("Quiet", "#89ABD0", 0.3), ("Loud", "#306998", 0.55)]: - mask = segment_label == label - sections = np.ma.clump_unmasked(np.ma.masked_where(~mask, mask)) - for sl in sections: - start = max(0, sl.start - 1) - stop = min(len(env_time), sl.stop + 1) - expanded = slice(start, stop) - ax.fill_between(env_time[expanded], env_max[expanded], env_min[expanded], color=color, alpha=alpha, linewidth=0) - -# Draw continuous envelope edges with seaborn lineplot +# Primary waveform: seaborn-native percentile band renders the full amplitude range at each +# time bin — mean line sits near zero (oscillating signal) with the envelope as fill width sns.lineplot( - data=df_env, - x="Time (seconds)", + data=df_long, + x="Time (s)", y="Amplitude", - hue="edge", - palette={"upper": "#306998", "lower": "#306998"}, - linewidth=1.5, - alpha=0.85, - legend=False, + color=BRAND, + errorbar=("pi", 100), + linewidth=0.8, + err_kws={"alpha": 0.4}, ax=ax, ) -# Zero-line -ax.axhline(y=0, color="#888888", linewidth=1.0, alpha=0.4, zorder=1) +# Quiet regions: secondary matplotlib overlay with muted tone to distinguish dynamics +quiet_mask = segment_label == "Quiet" +quiet_sections = np.ma.clump_unmasked(np.ma.masked_where(~quiet_mask, quiet_mask)) +for sl in quiet_sections: + start = max(0, sl.start - 1) + stop = min(len(env_time), sl.stop + 1) + ax.fill_between( + env_time[start:stop], + env_max[start:stop], + env_min[start:stop], + color=INK_MUTED, + alpha=0.35, + linewidth=0, + zorder=3, + ) -# Storytelling annotations for dynamic sections -ax.annotate( - "Attack + Sustain", xy=(0.10, 0.76), fontsize=13, color="#1E4264", fontstyle="italic", ha="center", va="bottom" -) -ax.annotate("Decay", xy=(0.38, 0.22), fontsize=13, color="#5A84A8", fontstyle="italic", ha="center", va="bottom") +# Zero-line reference +ax.axhline(y=0, color=INK_SOFT, linewidth=0.8, alpha=0.5, zorder=2) + +# Annotations for musical dynamics — kept as storytelling (reviewed strength) ax.annotate( - "Second Phrase", xy=(0.72, 0.65), fontsize=13, color="#1E4264", fontstyle="italic", ha="center", va="bottom" + "Attack + Sustain", xy=(0.10, 0.78), fontsize=8, color=INK_SOFT, fontstyle="italic", ha="center", va="bottom" ) +ax.annotate("Decay", xy=(0.38, 0.22), fontsize=8, color=INK_MUTED, fontstyle="italic", ha="center", va="bottom") +ax.annotate("Second Phrase", xy=(0.72, 0.67), fontsize=8, color=INK_SOFT, fontstyle="italic", ha="center", va="bottom") -# Legend for dynamics +# Legend legend_elements = [ - Patch(facecolor="#306998", alpha=0.55, label="Loud"), - Patch(facecolor="#89ABD0", alpha=0.3, label="Quiet"), + Patch(facecolor=BRAND, alpha=0.5, label="Loud"), + Patch(facecolor=INK_MUTED, alpha=0.35, label="Quiet"), ] -ax.legend(handles=legend_elements, loc="upper right", fontsize=14, framealpha=0.8) +ax.legend( + handles=legend_elements, loc="upper right", fontsize=8, framealpha=0.85, facecolor=ELEVATED_BG, edgecolor=INK_SOFT +) # Style -ax.set_xlabel("Time (seconds)", fontsize=20) -ax.set_ylabel("Amplitude", fontsize=20) -ax.set_title("waveform-audio · seaborn · pyplots.ai", fontsize=24, fontweight="medium") -ax.tick_params(axis="both", labelsize=16) +title = "waveform-audio · python · seaborn · anyplot.ai" +ax.set_xlabel("Time (s)", fontsize=10, color=INK) +ax.set_ylabel("Amplitude", fontsize=10, color=INK) +ax.set_title(title, fontsize=12, fontweight="medium", color=INK) +ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT) ax.set_ylim(-1.05, 1.05) ax.set_xlim(0, duration) sns.despine(ax=ax) -# Save +# Save — no bbox_inches='tight': figsize×dpi yields exact 3200×1800 px canvas plt.tight_layout() -plt.savefig("plot.png", dpi=300, bbox_inches="tight") +plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG) diff --git a/plots/waveform-audio/metadata/python/seaborn.yaml b/plots/waveform-audio/metadata/python/seaborn.yaml index 1f18cb234a..871f9e4167 100644 --- a/plots/waveform-audio/metadata/python/seaborn.yaml +++ b/plots/waveform-audio/metadata/python/seaborn.yaml @@ -1,111 +1,148 @@ library: seaborn +language: python specification_id: waveform-audio created: '2026-03-07T14:58:47Z' -updated: '2026-03-07T15:13:55Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 22801238636 +updated: '2026-06-03T01:17:55Z' +generated_by: claude-sonnet +workflow_run: 26857223645 issue: 4563 -python_version: 3.14.3 +language_version: 3.13.13 library_version: 0.13.2 -preview_url: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/seaborn/plot.png -preview_html: null -quality_score: 90 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/seaborn/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/waveform-audio/python/seaborn/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 92 review: strengths: - - Excellent data storytelling with loud/quiet segmentation and musical dynamics - annotations - - Cohesive blue palette with intentional hierarchy through alpha/saturation variation - - Thorough spec compliance with all required features including min/max envelope - rendering - - Clean well-structured code with appropriate envelope smoothing - - Professional visual refinement with removed spines, subtle grid, and balanced - layout + - 'Creative seaborn-native approach: uses sns.lineplot(errorbar=(''pi'', 100)) to + render the full amplitude envelope as a percentile band — an idiomatic seaborn + technique adapted to signal visualization' + - Loud/quiet dynamic classification via smoothed RMS adds informative visual distinction + beyond basic waveform rendering, with semantically correct use of the Imprint + muted anchor for quiet sections + - Musical dynamics annotations (Attack + Sustain, Decay, Second Phrase) in italic + fontstyle tell a clear story about the audio structure and guide the viewer's + eye through the signal + - Correct min/max envelope rendering via chunk binning (chunk_size=80, ~275 bins) + avoids aliasing for dense 22kHz waveform data — exactly as the spec recommends + - 'Perfect theme-adaptive chrome: all text, backgrounds, grid, and legend boxes + correctly flip between light (#FAF8F1) and dark (#1A1A17) themes with no dark-on-dark + or light-on-light failures' + - 'Acoustically accurate synthetic data: 220 Hz fundamental (A3 note) with realistic + harmonic series (0.6 / 0.25 / 0.1 / 0.05 weights) and multi-segment dynamic envelope' weaknesses: - - Library mastery limited since seaborn lacks native waveform/fill_between support; - core visualization relies on matplotlib - - Annotation font size (13pt) could be slightly larger for consistency with other - text elements - image_description: 'The plot displays an audio waveform envelope over 1 second of - time. The waveform is rendered as a filled area symmetric around a zero baseline, - with the upper and lower envelopes traced by dark blue lines. Two fill colors - distinguish dynamics: a darker blue (#306998) at ~55% alpha for "Loud" regions - and a lighter blue (#89ABD0) at ~30% alpha for "Quiet" regions. Three italic annotations - label musical dynamics: "Attack + Sustain" over the initial loud burst (0-0.2s), - "Decay" in the quieter mid-section (~0.35s), and "Second Phrase" over the second - loud region (~0.65-0.8s). The title reads "waveform-audio · seaborn · pyplots.ai" - at the top. X-axis is "Time (seconds)" from 0.0 to 1.0, Y-axis is "Amplitude" - from -1.00 to 1.00. A subtle horizontal zero-line is visible. Top and right spines - are removed. A legend in the upper right shows "Loud" and "Quiet" patches. The - grid is very subtle (low alpha). The overall aesthetic is clean and professional - with a cohesive blue palette.' + - 'Legend labeling creates a slight ambiguity: quiet sections actually show both + the green seaborn envelope AND the muted gray overlay overlaid, so they appear + as a green-gray mix, while the legend implies a clean binary Loud=green / Quiet=gray + distinction; consider adding a note or using opacity more clearly' + - 'DE-01 ceiling: aesthetic sophistication is strong but not publication-tier; additional + touches (e.g., subtle annotation leader lines, a faint horizontal amplitude grid + aligned to 0.5 / -0.5, or a slightly larger alpha contrast between loud and quiet) + could elevate the visual hierarchy further' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) — correct, not pure white. + Chrome: Title "waveform-audio · python · seaborn · anyplot.ai" in dark medium-weight text, clearly readable at ~70% of plot width. Y-axis label "Amplitude" and X-axis label "Time (s)" in dark text, proportional (fontsize 10). Tick labels in dark-soft tone, readable at 8pt. Top and right spines removed via sns.despine(). No grid (appropriate for dense signal). + Data: Large filled waveform envelope in brand green #009E73 (alpha 0.4) representing the full amplitude range at each time bin. A thin oscillating mean line (near zero) is visible within the envelope. Quiet sections (decay region and inter-phrase gap) are overlaid with a semi-transparent muted gray fill (INK_MUTED, alpha 0.35), creating visual distinction. Zero reference line in INK_SOFT at alpha 0.5. Three italic text annotations ("Attack + Sustain", "Decay", "Second Phrase") in INK_SOFT/INK_MUTED tones clearly label the musical phases. Legend in upper right with green and gray patch swatches. + Legibility verdict: PASS — all elements readable against warm off-white background. + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17) — correct, not pure black. + Chrome: Title, axis labels, and tick labels all rendered in light-colored text (#F0EFE8 / #B8B7B0 tokens) — clearly readable against dark background. No dark-on-dark issues anywhere. Legend frame uses elevated dark background (#242420). Annotations visible in lighter tones. + Data: Brand green #009E73 waveform envelope is identical to the light render — only chrome flips. Quiet gray overlay uses the dark-theme INK_MUTED (#A8A79F), which remains visible and distinguishable from both the background and the green fill. Zero reference line still visible. + Legibility verdict: PASS — all text readable against warm near-black background; no dark-on-dark failures. criteria_checklist: visual_quality: - score: 29 + 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 (title=24, labels=20, ticks=16, legend=14); - annotation text at 13pt slightly small but readable + comment: All font sizes explicitly set (title 12pt, axis labels 10pt, ticks + 8pt, annotations 8pt). Balanced proportions, no overflow. Readable in both + themes at desktop and mobile widths. - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: All text elements well separated, no collisions + comment: Annotations placed in open regions of the plot, no collision with + waveform fills or each other. Legend in upper right does not overlap data. - id: VQ-03 name: Element Visibility score: 6 max: 6 passed: true - comment: Envelope fills clearly visible, loud/quiet distinction apparent, - edge lines well-defined + comment: Dense waveform (22,050 samples) handled correctly via chunk-bin envelope + rendering. Green fill at alpha=0.4 and muted overlay at alpha=0.35 create + clear visual distinction between loud and quiet sections. - id: VQ-04 name: Color Accessibility - score: 4 - max: 4 + score: 2 + max: 2 passed: true - comment: Single-hue blue scheme with good luminance contrast; colorblind-safe + comment: Green (#009E73) vs muted gray is CVD-safe. Not using red-green as + sole signal. Good luminance contrast between data and background in both + themes. - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: 16:9 figure, plot fills canvas well with balanced margins + comment: Canvas gate passed. figsize=(8,4.5) dpi=400 → 3200x1800px. Plot fills + canvas well; balanced margins. No content cut off. Legend well-positioned + within plot area. - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: Time (seconds) with units, Amplitude descriptive for normalized signal + comment: 'X-axis: ''Time (s)'' with units. Y-axis: ''Amplitude'' (dimensionless + in normalized audio — no units needed). Title in correct format.' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Primary waveform uses BRAND=#009E73 (Imprint position 1). Quiet + overlay uses INK_MUTED (theme-adaptive muted anchor) for ''quiet/other'' + role — semantically correct. Backgrounds #FAF8F1/#1A1A17. All chrome tokens + theme-adaptive. Data colors identical across both renders.' design_excellence: - score: 16 + score: 13 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 5 max: 8 passed: true - comment: Cohesive two-tone blue palette, loud/quiet color distinction with - intentional hierarchy, italic annotations + comment: 'Above ''well-configured default'' (4): intentional loud/quiet hierarchy + with color distinction, italic annotations for musical phases, semi-transparent + fills, clean L-frame. Not quite FiveThirtyEight-level publication design + (8).' - id: DE-02 name: Visual Refinement - score: 5 + score: 4 max: 6 passed: true - comment: Spines removed, subtle grid (alpha=0.15), generous whitespace, clean - edge colors + comment: sns.despine() removes top/right spines. No grid (appropriate for + dense waveform — would add noise). tight_layout for whitespace. Semi-transparent + fills refined. Not every detail polished (e.g., no annotation leader lines, + legend frame visible). - id: DE-03 name: Data Storytelling - score: 5 + score: 4 max: 6 passed: true - comment: Loud/quiet segmentation with musical dynamics annotations creates - clear narrative + comment: Annotations (Attack + Sustain, Decay, Second Phrase) + loud/quiet + color coding guide the viewer through the audio's dynamic story. Clear structure + is immediately readable. Good visual hierarchy above the default (2) but + not exceptional (6). spec_compliance: score: 15 max: 15 @@ -115,26 +152,31 @@ review: score: 5 max: 5 passed: true - comment: Correct audio waveform with filled envelope symmetric around zero + comment: 'Correct waveform plot: filled area symmetric around zero baseline, + rendered via seaborn percentile band.' - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: 'All spec features present: filled area, semi-transparent fill, zero-line, - min/max envelope, synthetic data' + comment: 'All spec features present: filled area mirrored above/below zero, + semi-transparent fill, horizontal zero reference line, time in seconds on + X, normalized amplitude -1 to +1 on Y, min/max envelope rendering for dense + data, synthetic audio data.' - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: Time on X, amplitude on Y, correct ranges + comment: 'X: Time (s) 0-1.0, Y: Amplitude -1.05 to 1.05 (with 5% padding). + All data visible.' - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Correct title format, legend shows Loud/Quiet dynamics + comment: Title 'waveform-audio · python · seaborn · anyplot.ai' matches required + format exactly. Legend labels 'Loud' and 'Quiet' are appropriate and descriptive. data_quality: score: 15 max: 15 @@ -144,21 +186,25 @@ review: score: 6 max: 6 passed: true - comment: Shows attack, sustain, decay, second phrase with dynamics variation - and quiet regions + comment: 'Shows all waveform aspects: attack/sustain/decay/inter-phrase quiet/second + phrase. Includes harmonics (realistic signal), clipping behavior (np.clip), + amplitude envelope variation, and dynamic range classification.' - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Realistic audio signal with 220Hz base, harmonics, amplitude envelope, - musical dynamics + comment: 'Realistic musical audio signal: 220 Hz fundamental (A3 note), multi-segment + dynamic envelope mimicking a realistic note structure, noise floor added. + Neutral domain (audio/music).' - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: 1s duration, 22050 Hz sample rate, normalized amplitude [-1, +1] + comment: 22050 Hz sample rate (standard), 1.0 s duration (short clip), normalized + amplitude -1 to +1 (industry standard), 220 Hz A3 note, realistic harmonic + weights (0.6/0.25/0.1/0.05). code_quality: score: 10 max: 10 @@ -168,61 +214,69 @@ review: score: 3 max: 3 passed: true - comment: Linear imports-data-plot-save structure, no functions/classes + comment: Imports → Data → Plot → Save. No functions or classes. Linear, readable + flow. - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: np.random.seed(42) + comment: np.random.seed(42) set at start. - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All imports used + comment: 'All imports used: os, matplotlib.pyplot, numpy, pandas, seaborn, + matplotlib.patches.Patch.' - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Clean implementation with appropriate complexity for envelope rendering + comment: Clean and Pythonic. The RMS-based quiet/loud classification is somewhat + complex but justified by the visualization goal. No fake UI elements or + interactive simulation. - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png with dpi=300 + comment: Saves as plot-{THEME}.png. No bbox_inches='tight' on savefig. No + deprecated API calls. library_mastery: - score: 5 + score: 9 max: 10 items: - id: LM-01 name: Idiomatic Usage - score: 3 + score: 5 max: 5 passed: true - comment: Uses sns.set_theme, sns.lineplot with long-form DataFrame and hue, - sns.despine; core fill_between is matplotlib + comment: Expertly uses sns.set_theme() with rc dict for full theme control, + sns.lineplot() with errorbar=(pi, 100) for percentile-band waveform envelope, + sns.despine() for spine cleanup. Long-form DataFrame format as seaborn expects. - id: LM-02 name: Distinctive Features - score: 2 + score: 4 max: 5 - passed: false - comment: Leverages set_theme with context/font_scale and lineplot with hue, - but core visual relies on matplotlib + passed: true + comment: Uses seaborn's statistical errorbar=('pi', 100) (0th-100th percentile + interval) as a waveform envelope renderer — a creative adaptation of seaborn's + statistical estimation to signal processing. However, the quiet-region overlay + uses matplotlib fill_between directly (no seaborn equivalent exists, so + this is acceptable but limits the LM-02 ceiling). verdict: APPROVED impl_tags: dependencies: [] techniques: - annotations - custom-legend - - layer-composition patterns: - data-generation + - explicit-figure - iteration-over-groups dataprep: - binning styling: - alpha-blending - - grid-styling