diff --git a/plots/piano-roll-midi/implementations/python/highcharts.py b/plots/piano-roll-midi/implementations/python/highcharts.py index 11bedc75ff..b85d13736b 100644 --- a/plots/piano-roll-midi/implementations/python/highcharts.py +++ b/plots/piano-roll-midi/implementations/python/highcharts.py @@ -1,9 +1,10 @@ -""" pyplots.ai +""" anyplot.ai piano-roll-midi: MIDI Piano Roll Visualization -Library: highcharts unknown | Python 3.14.3 -Quality: 92/100 | Created: 2026-03-07 +Library: highcharts unknown | Python 3.13.13 +Quality: 94/100 | Updated: 2026-06-03 """ +import os import tempfile import time import urllib.request @@ -12,19 +13,27 @@ import numpy as np from highcharts_core.chart import Chart from highcharts_core.options import HighchartsOptions +from PIL import Image from selenium import webdriver from selenium.webdriver.chrome.options import Options +# Theme tokens +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" +GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)" + np.random.seed(42) # MIDI helpers NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] BLACK_KEY_INDICES = {1, 3, 6, 8, 10} -# Data - A chord progression with melody (C-Am-F-G, 8 measures of 4/4) -# Each note: (start_beat, duration_beats, midi_pitch, velocity, role) -# role: "bass", "harmony", "melody" +# Data: C–Am–F–G chord progression, 8 measures of 4/4 +# (start_beat, duration_beats, midi_pitch, velocity, role) notes = [ # Measure 1-2: C major chord + melody (0.0, 4.0, 48, 90, "bass"), @@ -82,7 +91,7 @@ (21.0, 0.5, 67, 84, "melody"), (21.5, 0.5, 65, 80, "melody"), (22.0, 2.0, 64, 96, "melody"), - # Measure 7-8: G major chord + melody (building to end) + # Measure 7-8: G major chord + melody (24.0, 4.0, 55, 95, "bass"), (24.0, 2.0, 59, 76, "harmony"), (24.0, 2.0, 62, 76, "harmony"), @@ -102,172 +111,177 @@ (30.0, 2.0, 72, 125, "melody"), ] -# Determine pitch range - only include pitches that have notes (no empty rows) +# Pitch range — auto-fit to data used_pitches = sorted({n[2] for n in notes}) min_pitch = min(used_pitches) max_pitch = max(used_pitches) - -# Build category list for only the used range all_midi_range = list(range(min_pitch, max_pitch + 1)) categories = [] for midi in all_midi_range: octave = midi // 12 - 1 name = NOTE_NAMES[midi % 12] categories.append(f"{name}{octave}") - pitch_to_index = {midi: i for i, midi in enumerate(all_midi_range)} -# Velocity color mapping: teal (soft) -> amber (medium) -> crimson (loud) -# Using brighter amber midpoint for crisper transitions +# Velocity → color: imprint_div reversed — blue (soft/piano) → theme midpoint → red (loud/forte) vel_min, vel_max = 60, 127 -color_stops_rgb = [ - (20, 130, 150), # deep teal (soft/piano) - (255, 165, 0), # bright amber (medium/mezzo) - (185, 25, 40), # crimson (loud/forte) -] - +_s0 = (68, 103, 163) # #4467A3 blue at vel=60 +_s1 = (250, 248, 241) if THEME == "light" else (26, 26, 23) # #FAF8F1 / #1A1A17 midpoint +_s2 = (174, 48, 48) # #AE3030 red at vel=127 -def vel_to_rgb(t, stops=color_stops_rgb): - """Interpolate velocity 0..1 through three color stops.""" - if t < 0.5: - s = t / 0.5 - c0, c1 = stops[0], stops[1] - else: - s = (t - 0.5) / 0.5 - c0, c1 = stops[1], stops[2] - return tuple(int(c0[i] * (1 - s) + c1[i] * s) for i in range(3)) - - -# Pre-compute colorbar gradient segments (shared by Python→JS) -N_COLORBAR_SEGS = 80 -colorbar_colors = [] -for i in range(N_COLORBAR_SEGS): - t = 1 - i / (N_COLORBAR_SEGS - 1) - r, g, b = vel_to_rgb(t) - colorbar_colors.append(f"rgb({r},{g},{b})") - -# Role-specific styling +# Role config: distinct pointWidth + opacity + border for colorblind safety role_config = { - "melody": {"borderWidth": 2, "borderColor": "rgba(255, 255, 255, 0.7)", "pointWidth": 56, "opacity": 1.0}, - "harmony": {"borderWidth": 1, "borderColor": "rgba(0, 0, 0, 0.08)", "pointWidth": 48, "opacity": 0.72}, - "bass": {"borderWidth": 1, "borderColor": "rgba(0, 0, 0, 0.12)", "pointWidth": 64, "opacity": 0.85}, + "melody": {"borderWidth": 2, "borderColor": "rgba(255,255,255,0.85)", "pointWidth": 44, "opacity": 1.0}, + "harmony": {"borderWidth": 1, "borderColor": "rgba(0,0,0,0.06)", "pointWidth": 30, "opacity": 0.60}, + "bass": {"borderWidth": 2, "borderColor": "rgba(0,0,0,0.22)", "pointWidth": 52, "opacity": 0.90}, } -# Build xrange data points grouped by role +# Build series data — inline velocity interpolation series_data = {"melody": [], "harmony": [], "bass": []} for start, dur, pitch, vel, role in notes: t = float(np.clip((vel - vel_min) / (vel_max - vel_min), 0.0, 1.0)) - r, g, b = vel_to_rgb(t) + if t < 0.5: + s = t / 0.5 + rv = int(_s0[0] * (1 - s) + _s1[0] * s) + gv = int(_s0[1] * (1 - s) + _s1[1] * s) + bv = int(_s0[2] * (1 - s) + _s1[2] * s) + else: + s = (t - 0.5) / 0.5 + rv = int(_s1[0] * (1 - s) + _s2[0] * s) + gv = int(_s1[1] * (1 - s) + _s2[1] * s) + bv = int(_s1[2] * (1 - s) + _s2[2] * s) alpha = role_config[role]["opacity"] - color = f"rgba({r},{g},{b},{alpha})" series_data[role].append( { "x": start, "x2": start + dur, "y": pitch_to_index[pitch], - "color": color, + "color": f"rgba({rv},{gv},{bv},{alpha})", "custom": {"pitch": pitch, "velocity": vel, "noteName": categories[pitch_to_index[pitch]], "role": role}, } ) -# Plot bands for black keys (subtle darker background rows) +# Colorbar gradient (80 segments, top=loud/crimson, bottom=soft/teal) +colorbar_colors = [] +for i in range(80): + t = 1 - i / 79 + if t < 0.5: + s = t / 0.5 + rv = int(_s0[0] * (1 - s) + _s1[0] * s) + gv = int(_s0[1] * (1 - s) + _s1[1] * s) + bv = int(_s0[2] * (1 - s) + _s1[2] * s) + else: + s = (t - 0.5) / 0.5 + rv = int(_s1[0] * (1 - s) + _s2[0] * s) + gv = int(_s1[1] * (1 - s) + _s2[1] * s) + bv = int(_s1[2] * (1 - s) + _s2[2] * s) + colorbar_colors.append(f"rgb({rv},{gv},{bv})") + +# Black key row shading — theme-adaptive +band_color = "rgba(0,0,0,0.07)" if THEME == "light" else "rgba(255,255,255,0.07)" plot_bands = [] for midi in all_midi_range: if (midi % 12) in BLACK_KEY_INDICES: idx = pitch_to_index[midi] - plot_bands.append({"from": idx - 0.5, "to": idx + 0.5, "color": "rgba(0, 0, 0, 0.06)"}) + plot_bands.append({"from": idx - 0.5, "to": idx + 0.5, "color": band_color}) -# Beat grid lines for x-axis (stronger at measure boundaries) -total_beats = 32 +# Beat grid lines — stronger at measure boundaries beat_lines = [] -for beat in range(total_beats + 1): +for beat in range(33): is_measure = beat % 4 == 0 beat_lines.append( - {"value": beat, "color": "#999999" if is_measure else "#dddddd", "width": 2 if is_measure else 1, "zIndex": 3} + {"value": beat, "color": INK_SOFT if is_measure else GRID, "width": 2 if is_measure else 1, "zIndex": 3} ) # Chart chart = Chart(container="container") chart.options = HighchartsOptions() +title_text = "piano-roll-midi · python · highcharts · anyplot.ai" +n_title = len(title_text) +title_fs = round(66 * 67 / n_title) if n_title > 67 else 66 + chart.options.chart = { "type": "xrange", - "width": 4800, - "height": 2700, - "backgroundColor": "#fafafa", - "marginLeft": 200, - "marginTop": 180, - "marginBottom": 200, - "marginRight": 360, - "style": {"fontFamily": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif"}, + "width": 3200, + "height": 1800, + "backgroundColor": PAGE_BG, + "marginLeft": 175, + "marginTop": 160, + "marginBottom": 155, + "marginRight": 245, + "style": {"fontFamily": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", "color": INK}, } chart.options.title = { - "text": "piano-roll-midi \u00b7 highcharts \u00b7 pyplots.ai", - "style": {"fontSize": "44px", "fontWeight": "600", "color": "#2c2c2c"}, - "y": 55, + "text": title_text, + "style": {"fontSize": f"{title_fs}px", "fontWeight": "600", "color": INK}, + "y": 78, } chart.options.subtitle = { - "text": "C \u2013 Am \u2013 F \u2013 G chord progression \u00b7 8 measures \u00b7 velocity-colored dynamics", - "style": {"fontSize": "26px", "color": "#666666", "fontWeight": "400"}, - "y": 100, + "text": "C – Am – F – G · 8 measures of 4/4 · velocity-colored dynamics", + "style": {"fontSize": "38px", "color": INK_SOFT, "fontWeight": "400"}, + "y": 126, } chart.options.x_axis = { - "title": {"text": "Beats (quarter notes)", "style": {"fontSize": "28px", "color": "#444444"}}, - "labels": {"style": {"fontSize": "22px", "color": "#555555"}, "step": 1}, + "title": {"text": "Beats (quarter notes)", "style": {"fontSize": "56px", "color": INK}}, + "labels": {"style": {"fontSize": "44px", "color": INK_SOFT}, "step": 1}, "min": 0, "max": 32, "tickInterval": 4, "gridLineWidth": 0, "plotLines": beat_lines, "lineWidth": 0, + "tickLength": 0, } chart.options.y_axis = { "type": "category", "categories": categories, - "title": {"text": "Pitch (note name)", "style": {"fontSize": "28px", "color": "#444444"}}, - "labels": {"style": {"fontSize": "22px", "color": "#555555"}}, + "title": {"text": "Pitch", "style": {"fontSize": "56px", "color": INK}}, + "labels": {"style": {"fontSize": "44px", "color": INK_SOFT}}, "gridLineWidth": 1, - "gridLineColor": "rgba(0, 0, 0, 0.04)", + "gridLineColor": GRID, "plotBands": plot_bands, "reversed": False, "lineWidth": 0, + "tickLength": 0, } chart.options.legend = { "enabled": True, - "align": "left", - "verticalAlign": "top", - "x": 220, - "y": 110, - "floating": True, - "itemStyle": {"fontSize": "22px", "fontWeight": "500", "color": "#444"}, - "itemDistance": 40, - "symbolWidth": 28, - "symbolHeight": 16, + "align": "center", + "verticalAlign": "bottom", + "itemStyle": {"fontSize": "44px", "fontWeight": "500", "color": INK_SOFT}, + "itemDistance": 56, + "symbolWidth": 40, + "symbolHeight": 22, "symbolRadius": 4, - "backgroundColor": "rgba(255,255,255,0.8)", - "borderWidth": 0, + "backgroundColor": ELEVATED_BG, + "borderColor": INK_SOFT, + "borderWidth": 1, + "borderRadius": 4, + "padding": 14, + "margin": 18, } chart.options.tooltip = { "headerFormat": "", - "pointFormat": '{point.custom.noteName} (MIDI {point.custom.pitch})
' - "Beat {point.x} \u2013 {point.x2}
" - "Velocity: {point.custom.velocity}
" - "Role: {point.custom.role}
", - "backgroundColor": "rgba(255,255,255,0.95)", - "borderColor": "#cccccc", + "pointFormat": ( + '{point.custom.noteName} (MIDI {point.custom.pitch})
' + "Beat {point.x} – {point.x2}
" + "Velocity: {point.custom.velocity}
" + "Role: {point.custom.role}
" + ), + "backgroundColor": ELEVATED_BG, + "borderColor": INK_SOFT, "borderRadius": 8, - "shadow": {"color": "rgba(0,0,0,0.15)", "offsetX": 2, "offsetY": 2, "width": 4}, + "style": {"color": INK}, } -# Three series for visual hierarchy: melody (prominent), bass (solid), harmony (subtle) -series_configs = [("Melody", "melody", "#d94040"), ("Bass", "bass", "#2a7a8a"), ("Harmony", "harmony", "#c0a030")] - +series_configs = [("Melody", "melody", "#009E73"), ("Bass", "bass", "#C475FD"), ("Harmony", "harmony", "#4467A3")] series_list = [] for label, role, legend_color in series_configs: cfg = role_config[role] @@ -278,25 +292,20 @@ def vel_to_rgb(t, stops=color_stops_rgb): "data": series_data[role], "color": legend_color, "pointWidth": cfg["pointWidth"], - "borderRadius": 5, + "borderRadius": 4, "borderWidth": cfg["borderWidth"], "borderColor": cfg["borderColor"], "dataLabels": {"enabled": False}, } ) chart.options.series = series_list - chart.options.credits = {"enabled": False} -# Download Highcharts JS modules +# Download Highcharts JS modules (with /tmp cache) cache_dir = Path("/tmp") cdn_urls = { "highcharts": ("https://cdn.jsdelivr.net/npm/highcharts@11.4.8/highcharts.js", cache_dir / "highcharts.js"), "xrange": ("https://cdn.jsdelivr.net/npm/highcharts@11.4.8/modules/xrange.js", cache_dir / "hc_xrange.js"), - "annotations": ( - "https://cdn.jsdelivr.net/npm/highcharts@11.4.8/modules/annotations.js", - cache_dir / "hc_annotations.js", - ), } js_scripts = {} for name, (url, cache_path) in cdn_urls.items(): @@ -309,32 +318,19 @@ def vel_to_rgb(t, stops=color_stops_rgb): js_scripts[name] = content highcharts_js = js_scripts["highcharts"] xrange_js = js_scripts["xrange"] -annotations_js = js_scripts["annotations"] -# Generate HTML with inline scripts and velocity colorbar via Highcharts renderer callback html_str = chart.to_js_literal() -# Chord labels positioned above the chart area -chord_labels = [ - {"text": "C", "beat": 2}, - {"text": "C", "beat": 6}, - {"text": "Am", "beat": 10}, - {"text": "Am", "beat": 14}, - {"text": "F", "beat": 18}, - {"text": "F", "beat": 22}, - {"text": "G", "beat": 26}, - {"text": "G", "beat": 30}, -] - -# JavaScript for chord labels and velocity colorbar +# Chord labels rendered above the plot area via Highcharts renderer +chord_labels = [("C", 2), ("C", 6), ("Am", 10), ("Am", 14), ("F", 18), ("F", 22), ("G", 26), ("G", 30)] chord_labels_js = "" -for cl in chord_labels: +for text, beat in chord_labels: chord_labels_js += f""" - r.text('{cl["text"]}', chart.xAxis[0].toPixels({cl["beat"]}), 155) - .attr({{'text-anchor':'middle'}}) - .css({{fontSize:'26px', color:'#888', fontWeight:'600', fontStyle:'italic'}}).add();""" + r.text('{text}', chart.xAxis[0].toPixels({beat}), 168) + .attr({{'text-anchor': 'middle'}}) + .css({{fontSize: '32px', color: '{INK_SOFT}', fontWeight: '600', fontStyle: 'italic'}}).add();""" -# Build JS array of pre-computed colors (eliminates duplicated interpolation in JS) +# Velocity colorbar via Highcharts renderer colors_js_array = "[" + ",".join(f"'{c}'" for c in colorbar_colors) + "]" colorbar_js = f""" @@ -345,18 +341,23 @@ def vel_to_rgb(t, stops=color_stops_rgb): if (!chart) return; clearInterval(checkChart); var r = chart.renderer; - var x = 4470, y = 220, w = 34, totalH = 2200; + var cx = 2958, cy = 162, cw = 26, totalH = 1410; var colors = {colors_js_array}; var nSegs = colors.length; var segH = totalH / nSegs; for (var i = 0; i < nSegs; i++) {{ - r.rect(x, y + i*segH, w, segH+1, 0).attr({{fill:colors[i], 'stroke-width':0}}).add(); + r.rect(cx, cy + i * segH, cw, segH + 1, 0).attr({{fill: colors[i], 'stroke-width': 0}}).add(); }} - r.rect(x, y, w, totalH, 6).attr({{fill:'none', stroke:'#bbb', 'stroke-width':1}}).add(); - r.text('Velocity', x + w/2, y - 15).attr({{'text-anchor':'middle'}}).css({{fontSize:'24px', color:'#444', fontWeight:'600'}}).add(); - r.text('127 (forte)', x + w + 14, y + 18).css({{fontSize:'20px', color:'#666'}}).add(); - r.text('93 (mezzo)', x + w + 14, y + totalH/2 + 6).css({{fontSize:'20px', color:'#666'}}).add(); - r.text('60 (piano)', x + w + 14, y + totalH - 4).css({{fontSize:'20px', color:'#666'}}).add(); + r.rect(cx, cy, cw, totalH, 5).attr({{fill: 'none', stroke: '{INK_SOFT}', 'stroke-width': 1}}).add(); + r.text('Velocity', cx + cw / 2, cy - 18) + .attr({{'text-anchor': 'middle'}}) + .css({{fontSize: '38px', color: '{INK}', fontWeight: '600'}}).add(); + r.text('forte', cx + cw + 14, cy + 20).css({{fontSize: '30px', color: '{INK_SOFT}'}}).add(); + r.text('127', cx + cw + 14, cy + 50).css({{fontSize: '28px', color: '{INK_SOFT}'}}).add(); + r.text('mezzo', cx + cw + 14, cy + totalH / 2 + 6).css({{fontSize: '30px', color: '{INK_SOFT}'}}).add(); + r.text('93', cx + cw + 14, cy + totalH / 2 + 36).css({{fontSize: '28px', color: '{INK_SOFT}'}}).add(); + r.text('piano', cx + cw + 14, cy + totalH - 14).css({{fontSize: '30px', color: '{INK_SOFT}'}}).add(); + r.text('60', cx + cw + 14, cy + totalH + 16).css({{fontSize: '28px', color: '{INK_SOFT}'}}).add(); {chord_labels_js} }}, 100); }})(); @@ -369,37 +370,45 @@ def vel_to_rgb(t, stops=color_stops_rgb): - - -
+ +
{colorbar_js} """ -# Save HTML -with open("plot.html", "w", encoding="utf-8") as f: +# Save HTML artifact +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: f.write(html_content) -# Screenshot with Selenium +# Screenshot via Selenium + CDP viewport override with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: f.write(html_content) temp_path = f.name chrome_options = Options() -chrome_options.add_argument("--headless") +chrome_options.add_argument("--headless=new") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") chrome_options.add_argument("--disable-gpu") -chrome_options.add_argument("--window-size=5000,3000") +chrome_options.add_argument("--hide-scrollbars") +chrome_options.add_argument("--window-size=3200,1800") driver = webdriver.Chrome(options=chrome_options) +driver.execute_cdp_cmd( + "Emulation.setDeviceMetricsOverride", {"width": 3200, "height": 1800, "deviceScaleFactor": 1, "mobile": False} +) driver.get(f"file://{temp_path}") time.sleep(5) - -container = driver.find_element("id", "container") -container.screenshot("plot.png") - +driver.save_screenshot(f"plot-{THEME}.png") driver.quit() + Path(temp_path).unlink() + +# Pin to exact canvas dimensions +_img = Image.open(f"plot-{THEME}.png").convert("RGB") +if _img.size != (3200, 1800): + _norm = Image.new("RGB", (3200, 1800), PAGE_BG) + _norm.paste(_img, ((3200 - _img.size[0]) // 2, (1800 - _img.size[1]) // 2)) + _norm.save(f"plot-{THEME}.png") diff --git a/plots/piano-roll-midi/metadata/python/highcharts.yaml b/plots/piano-roll-midi/metadata/python/highcharts.yaml index f0c05f9932..9d321f9e3c 100644 --- a/plots/piano-roll-midi/metadata/python/highcharts.yaml +++ b/plots/piano-roll-midi/metadata/python/highcharts.yaml @@ -1,90 +1,121 @@ library: highcharts +language: python specification_id: piano-roll-midi created: '2026-03-07T19:49:41Z' -updated: '2026-03-07T20:22:03Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 22805912217 +updated: '2026-06-03T04:03:18Z' +generated_by: claude-sonnet +workflow_run: 26861938850 issue: 4565 -python_version: 3.14.3 +language_version: 3.13.13 library_version: unknown -preview_url: https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/highcharts/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/highcharts/plot.html -quality_score: 92 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/python/highcharts/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/python/highcharts/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/python/highcharts/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/piano-roll-midi/python/highcharts/plot-dark.html +quality_score: 94 review: strengths: - - Excellent musical data with realistic C-Am-F-G chord progression, three distinct - roles, and varied dynamics - - Professional velocity colorbar with teal-amber-crimson gradient and labeled intensity - levels - - Strong visual hierarchy through role-based opacity, border styling, and bar width - differentiation - - Chord labels above the chart provide musical context and narrative - - Black key row shading faithfully mirrors piano keyboard layout + - 'Perfect spec compliance — all piano roll requirements implemented: pitch-labeled + Y-axis, alternating black/white key shading, beat grid lines with stronger measure + boundaries, velocity-based diverging colormap' + - 'Sophisticated per-role visual differentiation: melody bars wider and fully opaque, + harmony bars narrower at 60% opacity, bass bars widest with dark border — provides + musical hierarchy without a separate encoding channel' + - 'Custom velocity colorbar built via Highcharts renderer API with correct imprint_div + colors (#4467A3 blue → #FAF8F1/#1A1A17 midpoint → #AE3030 red)' + - Chord annotations (C, Am, F, G) rendered above plot via Highcharts renderer, providing + musical storytelling context + - 'Correct canvas size (3200×1800), CDP override + PIL pin in place, both renders + properly themed (#FAF8F1 light / #1A1A17 dark)' + - 'Excellent code quality: flat KISS structure, seed set, all imports used, correct + output files saved' weaknesses: - - Unused annotations module downloaded and embedded in HTML (minor code bloat) - - vel_to_rgb helper function breaks strict KISS structure (minor) - - Harmony and bass colors may blend at certain velocity ranges for colorblind viewers - image_description: 'The plot displays a MIDI piano roll visualization as horizontal - rectangles on a grid. The y-axis shows pitch names from C3 to C5, and the x-axis - shows beats from 0 to 32 (quarter notes). Notes are colored by velocity using - a teal (soft/piano) to amber (mezzo) to crimson (forte) gradient, with a vertical - colorbar on the right labeled "Velocity" with annotations for 127 (forte), 93 - (mezzo), and 60 (piano). Three series are distinguished: Melody (orange/red tones, - white borders, prominent), Bass (teal/green tones, wider bars), and Harmony (olive/amber - tones, more transparent). Chord labels (C, C, Am, Am, F, F, G, G) appear above - the plot area in italic gray text. Black key rows (C#, D#, F#, G#, A#) have subtle - gray shading to distinguish from white keys. Beat grid lines are visible with - stronger lines at measure boundaries (every 4 beats). The title reads "piano-roll-midi - · highcharts · pyplots.ai" with a subtitle "C – Am – F – G chord progression · - 8 measures · velocity-colored dynamics". The background is a light off-white. - A legend in the top-left shows Melody, Bass, and Harmony series.' + - Supplementary annotation text below the 44px floor — chord labels at 32px and + colorbar labels (forte/mezzo/piano at 30px, values at 28px) would be hard to read + when the plot is scaled down to mobile width (~400px); raise to at least 36–40px + - DE-03 storytelling could be stronger — no single visual focal point or climax + moment highlighted (e.g. the high-velocity G-measure peak at beat 29–30 with velocity + 120–125 could be annotated or emphasized) + - Harmony bars at opacity=0.6 on the dark background become noticeably dim; consider + bumping opacity to 0.72–0.78 for dark theme to maintain readability without losing + the hierarchy contrast + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct. Plot area has alternating slightly-darker horizontal bands marking black keys on the piano keyboard, visible against the cream background. + Chrome: Title "piano-roll-midi · python · highcharts · anyplot.ai" in dark ink at 66px, clearly readable. Subtitle "C – Am – F – G · 8 measures of 4/4 · velocity-colored dynamics" at 38px, readable. Y-axis label "Pitch" and X-axis label "Beats (quarter notes)" both at 56px, readable. Pitch tick labels (C3–C5 range) at 44px clearly readable. Beat tick labels (0, 4, 8 … 32) at 44px readable. Chord annotations (C, C, Am, Am, F, F, G, G) at 32px visible but small. Colorbar labels (forte/mezzo/piano at 30px, 127/93/60 at 28px) are readable at full resolution but small. Legend (Melody, Bass, Harmony) at 44px clearly readable at bottom. + Data: Note bars are velocity-colored using a diverging blue (#4467A3) → cream midpoint → red (#AE3030) scale. Low-velocity notes appear cool blue, high-velocity notes (G-measure climax at beats 28–31) are deep red. Melody bars are widest and fully opaque with white border. Bass bars are widest (52px) with dark border at 90% opacity. Harmony bars are narrowest at 60% opacity. Velocity colorbar on right correctly runs from red (forte/127) at top to blue (piano/60) at bottom. Beat grid lines visible with stronger lines at measures (beats 0, 4, 8, 12, 16, 20, 24, 28, 32). First series legend swatch is #009E73 green (Melody). + Legibility verdict: PASS — all primary text is readable; supplementary colorbar text (28–30px) and chord labels (32px) are slightly small but legible at full resolution. + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct. Same alternating black-key row shading at 7% white opacity, subtly visible against the dark background. + Chrome: Title, axis labels, tick labels all appear in light ink (#F0EFE8/INK) against the dark background. Readable. Chord annotations and colorbar labels similarly readable in their INK_SOFT color (#B8B7B0). Legend text readable with elevated-dark background (#242420). No dark-on-dark failures observed. + Data: Data colors are identical to light render — same velocity-based blue-to-red diverging scale. The note bars maintain the same hue and saturation regardless of theme (Imprint palette identity preserved). Harmony bars at 60% opacity appear slightly more muted on dark background than on light but remain distinguishable. Colorbar gradient identical. Melody white-border note bars pop cleanly against the dark background. + Legibility verdict: PASS — no dark-on-dark failures; all text readable. Harmony bars at opacity=0.6 are somewhat less prominent in the dark render but still visible. criteria_checklist: visual_quality: - score: 29 + score: 28 max: 30 items: - id: VQ-01 name: Text Legibility - score: 8 + score: 6 max: 8 passed: true - comment: 'All font sizes explicitly set: title 44px, axis titles 28px, tick - labels 22px, legend 22px, subtitle 26px' + comment: Primary labels (title 66px, axis titles 56px, tick labels 44px, legend + 44px) all correctly sized and readable in both themes. Chord annotations + at 32px and colorbar labels at 28-30px are below the 44px floor — readable + at full resolution but would struggle at mobile 400px scale. - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No text overlap anywhere. Chord labels, axis labels, legend, and - title all well-spaced + comment: No overlapping text or data elements in either render. Per-role pointWidth + differentiation prevents bar overlap confusion. - id: VQ-03 name: Element Visibility score: 6 max: 6 passed: true - comment: Note rectangles well-sized with role-based widths (melody 56px, harmony - 48px, bass 64px) + comment: All note bars clearly visible. Velocity colorbar gradient renders + correctly. Alternating key shading visible in both themes. Chord annotations + above plot area visible. - id: VQ-04 name: Color Accessibility - score: 3 - max: 4 + score: 2 + max: 2 passed: true - comment: Teal-amber-crimson avoids red-green conflicts but harmony and bass - may blend at certain velocities for colorblind viewers + comment: Imprint diverging colormap (blue-to-red) is used for a continuous + velocity scale with labeled anchors (forte/mezzo/piano), providing non-color + cues. Role differentiation uses pointWidth and opacity as redundant encodings + beyond color. - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: Plot fills ~70% of canvas with balanced margins and well-placed colorbar + comment: 'Canvas 3200x1800 confirmed. Margins: left=175, top=160, bottom=155, + right=245 — sufficient for all labels. No clipping observed in either render. + Title at expected width. Canvas gate passed.' - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: 'Descriptive labels: Beats (quarter notes) and Pitch (note name)' + comment: Title matches required format 'piano-roll-midi · python · highcharts + · anyplot.ai'. Y-axis 'Pitch' is concise and appropriate. X-axis 'Beats + (quarter notes)' includes units. + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Velocity uses imprint_div (blue #4467A3 → PAGE_BG midpoint → red + #AE3030) — correct diverging cmap. Backgrounds #FAF8F1 (light) / #1A1A17 + (dark). First legend series (Melody) uses #009E73 brand green. All chrome + tokens theme-adaptive.' design_excellence: - score: 17 + score: 16 max: 20 items: - id: DE-01 @@ -92,22 +123,27 @@ review: score: 7 max: 8 passed: true - comment: Custom velocity palette, role-based opacity/sizing, white melody - borders, chord labels, hand-drawn colorbar. Professional design. + comment: 'Genuinely sophisticated: per-note velocity color interpolation, + per-role visual differentiation (pointWidth + opacity + border), piano keyboard + alternating row shading, custom velocity colorbar via Highcharts renderer, + chord annotations above plot, meaningful subtitle. Well above default.' - id: DE-02 name: Visual Refinement score: 5 max: 6 passed: true - comment: Off-white background, subtle grid, axis lines removed, plot bands - for black keys, rounded corners. Very polished. + comment: Axes cleaned (lineWidth=0, tickLength=0). Credits disabled. Subtle + grid. Legend with rounded corners and proper padding. Black-key shading + subtle (7% opacity). Harmony opacity differentiation adds musical refinement. - id: DE-03 name: Data Storytelling - score: 5 + score: 4 max: 6 passed: true - comment: Three roles create visual hierarchy; chord labels add musical narrative; - velocity gradient tells dynamics story + comment: 'Chord progression annotations (C-Am-F-G) narrate the harmonic journey. + Velocity colorbar explains the dynamics arc. Subtitle contextualizes the + data. Missing: no explicit focal point or highlighted climax (the high-velocity + G-measure peak at beats 28-31 with velocity 115-125 could be emphasized).' spec_compliance: score: 15 max: 15 @@ -117,26 +153,30 @@ review: score: 5 max: 5 passed: true - comment: Correct xrange chart type for horizontal note rectangles + comment: Correct piano roll using xrange chart type. Horizontal rectangles + positioned by pitch (Y) and time (X) with bar length = duration. - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: 'All spec features present: note names, black/white key shading, - beat grid, velocity colors, auto-fit pitch range' + comment: 'All spec features present: note names on Y-axis, black/white key + row shading, beat division grid lines, stronger lines at measure boundaries, + velocity-to-color mapping.' - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: X=time in beats, Y=pitch. Correct mapping with full data range + comment: Start on X, pitch on Y, duration as bar width (x to x2), velocity + as color. Pitch range auto-fit to data (C3-C5) with no excess. - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Correct title format and clear legend labels for Melody, Bass, Harmony + comment: Title exactly 'piano-roll-midi · python · highcharts · anyplot.ai'. + Legend shows Melody/Bass/Harmony with correct first-series green swatch. data_quality: score: 15 max: 15 @@ -146,90 +186,99 @@ review: score: 6 max: 6 passed: true - comment: Varying durations, velocities, three roles, chord progression, rhythmic - variation + comment: 'Full coverage: multiple pitch octaves (C3-C5), varied note durations + (0.5 to 4.0 beats), velocity range 60-125, three musical roles (melody/bass/harmony), + 8 measures of 4/4, C-Am-F-G chord progression with 68 notes.' - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Real C-Am-F-G chord progression with melody, harmony, and bass. Authentic - to DAW workflows + comment: C-Am-F-G is an iconic, neutral pop chord progression. Note velocities, + durations, and pitch ranges are authentic MIDI values. No controversial + or biased content. - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: MIDI pitches 48-72 (C3-C5), velocities 60-125, 8 measures of 4/4 - time. All sensible + comment: 32 beats = 8 measures of 4/4. MIDI pitches 48-72 (C3-C5, appropriate + range around middle C). 68 notes total (within spec's 20-200 range). code_quality: - score: 8 + score: 10 max: 10 items: - id: CQ-01 name: KISS Structure - score: 2 + score: 3 max: 3 passed: true - comment: Mostly linear but contains vel_to_rgb function definition + comment: Flat, linear structure. No unnecessary functions or classes. - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: np.random.seed(42) set and all data is hardcoded/deterministic + comment: np.random.seed(42) set. Data is fully hardcoded so deterministic + regardless. - id: CQ-03 name: Clean Imports - score: 1 + score: 2 max: 2 - passed: false - comment: Annotations Highcharts module downloaded and embedded but never used + passed: true + comment: All imports (os, tempfile, time, urllib.request, pathlib, numpy, + highcharts_core, PIL, selenium) are used. - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Clean, well-organized code with pre-computed colorbar gradient and - role-based config + comment: Clean velocity interpolation logic. Inline JS for custom renderer + features is complex but unavoidable for this library's annotation pattern. + No fake interactivity. - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png via Selenium screenshot. Current Highcharts API + comment: Saves plot-{THEME}.png and plot-{THEME}.html correctly. Uses highcharts-core + Python API idiomatically. library_mastery: - score: 8 + score: 10 max: 10 items: - id: LM-01 name: Idiomatic Usage - score: 4 + score: 5 max: 5 passed: true - comment: Good use of xrange series, HighchartsOptions, plotBands, plotLines, - tooltip formatting, to_js_literal() + comment: xrange chart type used correctly. chart.options object API used throughout. + Container='container' correctly set. Selenium CDP viewport override applied. + PIL pin for exact canvas dims. All Highcharts best practices followed. - id: LM-02 name: Distinctive Features - score: 4 + score: 5 max: 5 passed: true - comment: xrange chart type, renderer API for colorbar and chord labels, plotBands - for black keys, rich tooltips + comment: Highcharts renderer API used for custom colorbar and chord annotations + (not possible in static libs). plotBands for alternating key shading. plotLines + for beat/measure grid. Per-point color assignment in xrange data. JS cache + with /tmp for faster re-runs. verdict: APPROVED impl_tags: dependencies: - selenium + - pillow techniques: - - hover-tooltips - html-export - - custom-legend + - annotations - colorbar + - manual-ticks patterns: - data-generation - iteration-over-groups - dataprep: [] + dataprep: + - normalization styling: - custom-colormap - alpha-blending - - grid-styling - - edge-highlighting