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