diff --git a/plots/spectrogram-basic/implementations/python/highcharts.py b/plots/spectrogram-basic/implementations/python/highcharts.py
index 80fc049a82..e248e04c08 100644
--- a/plots/spectrogram-basic/implementations/python/highcharts.py
+++ b/plots/spectrogram-basic/implementations/python/highcharts.py
@@ -1,12 +1,13 @@
-""" pyplots.ai
+""" anyplot.ai
spectrogram-basic: Spectrogram Time-Frequency Heatmap
-Library: highcharts unknown | Python 3.13.11
-Quality: 91/100 | Created: 2025-12-31
+Library: highcharts unknown | Python 3.13.13
+Quality: 85/100 | Updated: 2026-05-15
"""
-import tempfile
+import os
+import threading
import time
-import urllib.request
+from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
import numpy as np
@@ -18,7 +19,15 @@
from selenium.webdriver.chrome.options import Options
-# Generate chirp signal (frequency increases over time)
+# 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.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
+
+# Data
np.random.seed(42)
sample_rate = 1000 # 1000 Hz sampling rate
duration = 2.0 # 2 seconds
@@ -28,7 +37,7 @@
f0, f1 = 10, 200
chirp_signal = signal.chirp(t, f0=f0, f1=f1, t1=duration, method="linear")
-# Add some noise for realism
+# Add noise for realism
noise = 0.1 * np.random.randn(len(t))
combined_signal = chirp_signal + noise
@@ -40,7 +49,7 @@
# Convert to dB scale for better visualization
Sxx_db = 10 * np.log10(Sxx + 1e-10)
-# Get dB range for colorbar (use actual dB values, not normalized)
+# Get dB range for colorbar
Sxx_min = float(Sxx_db.min())
Sxx_max = float(Sxx_db.max())
@@ -70,40 +79,46 @@
"type": "heatmap",
"width": 4800,
"height": 2700,
- "backgroundColor": "#ffffff",
- "marginTop": 160, # Increased for subtitle
+ "backgroundColor": PAGE_BG,
+ "marginTop": 160,
"marginBottom": 250,
"marginLeft": 200,
- "marginRight": 320, # Increased for legend title spacing
+ "marginRight": 320,
}
-# Title and subtitle
+# Title
chart.options.title = {
- "text": "spectrogram-basic · highcharts · pyplots.ai",
- "style": {"fontSize": "48px", "fontWeight": "bold"},
+ "text": "spectrogram-basic · highcharts · anyplot.ai",
+ "style": {"fontSize": "28px", "fontWeight": "bold", "color": INK},
}
+# Subtitle
chart.options.subtitle = {
"text": "Linear chirp signal (10-200 Hz) with linear frequency axis",
- "style": {"fontSize": "32px", "color": "#666666"},
+ "style": {"fontSize": "22px", "color": INK_SOFT},
}
# X-axis (time)
time_labels = [f"{t:.2f}" for t in times_ds]
chart.options.x_axis = {
"categories": time_labels,
- "title": {"text": "Time (seconds)", "style": {"fontSize": "36px"}},
- "labels": {"style": {"fontSize": "24px"}, "step": max(1, len(time_labels) // 10)},
- "tickLength": 10,
+ "title": {"text": "Time (seconds)", "style": {"fontSize": "22px", "color": INK}},
+ "labels": {"style": {"fontSize": "18px", "color": INK_SOFT}, "step": max(1, len(time_labels) // 10)},
+ "lineColor": INK_SOFT,
+ "tickColor": INK_SOFT,
+ "gridLineColor": GRID,
}
# Y-axis (frequency)
freq_labels = [f"{f:.0f}" for f in frequencies_ds]
chart.options.y_axis = {
"categories": freq_labels,
- "title": {"text": "Frequency (Hz)", "style": {"fontSize": "36px"}},
- "labels": {"style": {"fontSize": "24px"}, "step": max(1, len(freq_labels) // 10)},
+ "title": {"text": "Frequency (Hz)", "style": {"fontSize": "22px", "color": INK}},
+ "labels": {"style": {"fontSize": "18px", "color": INK_SOFT}, "step": max(1, len(freq_labels) // 10)},
"reversed": False,
+ "lineColor": INK_SOFT,
+ "tickColor": INK_SOFT,
+ "gridLineColor": GRID,
}
# Color axis (legend for heatmap intensity) - use actual dB values
@@ -117,7 +132,7 @@
[0.75, "#5ec962"], # green
[1, "#fde725"], # yellow
],
- "labels": {"style": {"fontSize": "24px"}, "format": "{value:.0f} dB"},
+ "labels": {"style": {"fontSize": "18px", "color": INK_SOFT}, "format": "{value:.0f} dB"},
}
# Legend
@@ -127,15 +142,19 @@
"verticalAlign": "middle",
"symbolHeight": 800,
"symbolWidth": 40,
- "x": -20, # Shift legend left to avoid edge cramping
- "title": {"text": "Power (dB)", "style": {"fontSize": "28px"}},
+ "x": -20,
+ "title": {"text": "Power (dB)", "style": {"fontSize": "22px", "color": INK}},
+ "itemStyle": {"color": INK_SOFT},
+ "backgroundColor": ELEVATED_BG,
+ "borderColor": INK_SOFT,
+ "borderWidth": 1,
}
-# Tooltip - show actual dB values
+# Tooltip
chart.options.tooltip = {
"headerFormat": "",
"pointFormat": "Time: {point.x_label} s
Frequency: {point.y_label} Hz
Power: {point.value:.1f} dB",
- "style": {"fontSize": "20px"},
+ "style": {"fontSize": "16px"},
}
# Series
@@ -146,35 +165,41 @@
chart.add_series(series)
-# Download Highcharts JS and heatmap module
-highcharts_url = "https://code.highcharts.com/highcharts.js"
-with urllib.request.urlopen(highcharts_url, timeout=30) as response:
- highcharts_js = response.read().decode("utf-8")
-
-heatmap_url = "https://code.highcharts.com/modules/heatmap.js"
-with urllib.request.urlopen(heatmap_url, timeout=30) as response:
- heatmap_js = response.read().decode("utf-8")
-
-# Generate HTML with inline scripts
+# Generate chart JavaScript
html_str = chart.to_js_literal()
+
+# HTML with unpkg CDN (Cloudflare-friendly alternative)
html_content = f"""
-
-
+
+
-
+
"""
-# Write temp HTML and take screenshot
-with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
+# Save interactive HTML
+with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f:
f.write(html_content)
- temp_path = f.name
+
+# Start simple HTTP server in background thread to serve HTML
+class QuietHTTPRequestHandler(SimpleHTTPRequestHandler):
+ def log_message(self, format, *args):
+ pass
+
+
+html_dir = Path.cwd()
+server = HTTPServer(("127.0.0.1", 0), QuietHTTPRequestHandler)
+port = server.server_port
+server_thread = threading.Thread(target=server.serve_forever, daemon=True)
+server_thread.start()
+
+# Take screenshot with Selenium via HTTP
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
@@ -182,27 +207,22 @@
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--window-size=4800,2700")
-driver = webdriver.Chrome(options=chrome_options)
-driver.get(f"file://{temp_path}")
-time.sleep(5) # Wait for chart to render
-driver.save_screenshot("plot.png")
-driver.quit()
-
-Path(temp_path).unlink() # Clean up temp file
-
-# Also save HTML for interactive version
-with open("plot.html", "w", encoding="utf-8") as f:
- interactive_html = f"""
-
-
-
- spectrogram-basic · highcharts · pyplots.ai
-
-
-
-
-
-
-
-"""
- f.write(interactive_html)
+try:
+ driver = webdriver.Chrome(options=chrome_options)
+ driver.set_page_load_timeout(30)
+ driver.get(f"http://127.0.0.1:{port}/plot-{THEME}.html")
+
+ # Wait for container to be visible and chart to render
+ time.sleep(20)
+
+ # Additional wait to ensure rendering is complete
+ try:
+ driver.execute_script("return document.readyState === 'complete' && window.Highcharts !== undefined")
+ except Exception:
+ pass
+
+ time.sleep(2)
+ driver.save_screenshot(f"plot-{THEME}.png")
+ driver.quit()
+finally:
+ server.shutdown()
diff --git a/plots/spectrogram-basic/metadata/python/highcharts.yaml b/plots/spectrogram-basic/metadata/python/highcharts.yaml
index cacee0527b..53a94f4d18 100644
--- a/plots/spectrogram-basic/metadata/python/highcharts.yaml
+++ b/plots/spectrogram-basic/metadata/python/highcharts.yaml
@@ -1,207 +1,259 @@
library: highcharts
+language: python
specification_id: spectrogram-basic
created: '2025-12-31T05:36:53Z'
-updated: '2025-12-31T06:07:15Z'
-generated_by: claude-opus-4-5-20251101
-workflow_run: 20612809126
+updated: '2026-05-15T22:12:25Z'
+generated_by: claude-haiku
+workflow_run: 25906243314
issue: 2927
-python_version: 3.13.11
+python_version: 3.13.13
library_version: unknown
-preview_url: https://storage.googleapis.com/anyplot-images/plots/spectrogram-basic/highcharts/plot.png
-preview_html: https://storage.googleapis.com/anyplot-images/plots/spectrogram-basic/highcharts/plot.html
-quality_score: 91
-impl_tags:
- dependencies:
- - scipy
- - selenium
- techniques:
- - colorbar
- - html-export
- patterns:
- - data-generation
- dataprep: []
- styling: []
+preview_url_light: https://storage.googleapis.com/anyplot-images/plots/spectrogram-basic/python/highcharts/plot-light.png
+preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/spectrogram-basic/python/highcharts/plot-dark.png
+preview_html_light: https://storage.googleapis.com/anyplot-images/plots/spectrogram-basic/python/highcharts/plot-light.html
+preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/spectrogram-basic/python/highcharts/plot-dark.html
+quality_score: 85
review:
strengths:
- - Clear visualization of the chirp signal with frequency increasing over time (diagonal
- yellow band is unmistakable)
- - Excellent use of viridis colormap for colorblind accessibility
- - Proper dB scale labeling on colorbar with actual dB values (-100 to -10 dB range)
- - Clean code structure with scipy spectrogram computation
- - Both PNG and interactive HTML outputs generated
- - Appropriate downsampling for Highcharts heatmap performance
+ - 'Perfect spec compliance: correct spectrogram type with time-frequency heatmap,
+ dB power scale, viridis colormap, and labeled colorbar'
+ - 'Excellent theme adaptation: both renders use correct background tokens (#FAF8F1/#1A1A17)
+ with fully theme-adaptive chrome (INK, INK_SOFT tokens throughout); no legibility
+ failures in either theme'
+ - Informative subtitle contextualizes the signal; the natural chirp sweep focal
+ point (bright yellow diagonal band) provides immediate visual storytelling without
+ annotations
+ - Realistic signal processing pipeline (scipy chirp + STFT spectrogram) with appropriate
+ dB scaling and noise for a scientifically credible example
weaknesses:
- - No grid lines on the heatmap area (would improve readability at specific time/frequency
- points)
- - Y-axis shows frequencies up to ~469 Hz but the chirp only goes to 200 Hz (wasted
- vertical space showing noise)
- image_description: The spectrogram displays a heatmap of a linear chirp signal.
- The plot shows time (0.06 to 1.91 seconds) on the x-axis and frequency (0 to ~469
- Hz) on the y-axis. A clear diagonal yellow band sweeps from low frequencies at
- early times to high frequencies at later times, representing the increasing frequency
- of the chirp signal. The background is predominantly teal/green (mid-power levels)
- with scattered darker blue/purple spots (low power). A vertical colorbar on the
- right shows "Power (dB)" ranging from approximately -100 dB (purple) to -10 dB
- (yellow), using a viridis colormap. The title reads "spectrogram-basic · highcharts
- · pyplots.ai" with a subtitle "Linear chirp signal (10-200 Hz) with linear frequency
- axis".
+ - CDN + HTTP server approach deviates from the highcharts.md guideline to embed
+ JS inline; introduces network dependency and extra complexity (threading, class
+ definition)
+ - Design sophistication is at the well-configured defaults tier (DE-01=4); lacks
+ distinctive typographic or layout choices that would push to publication-ready
+ quality
+ - Downsampling to 80x50 grid creates blocky cell appearance; higher resolution (e.g.,
+ 120x70) would better represent the smooth frequency sweep and improve perceived
+ quality
+ image_description: |-
+ Light render (plot-light.png):
+ Background: Warm off-white (#FAF8F1) - correct light theme surface, not pure white
+ Chrome: Title "spectrogram-basic · highcharts · anyplot.ai" in dark ink (#1A1A17) clearly readable at top. Subtitle "Linear chirp signal (10-200 Hz) with linear frequency axis" in INK_SOFT (#4A4A44) readable below title. X-axis label "Time (seconds)" and Y-axis label "Frequency (Hz)" in dark ink, clearly visible. Tick labels (0-469 Hz on Y; 0.06-1.91s on X) in INK_SOFT, readable. Colorbar "Power (dB)" title and dB labels (-70 to -10 dB) in INK_SOFT on right side, readable.
+ Data: Viridis colormap (dark purple low power → teal mid → bright yellow high power). Clear diagonal yellow band tracing chirp sweep from ~10 Hz at t=0 to ~188 Hz at t=1.91s. Background shows green/teal noise floor. First and only series uses viridis continuous scale (not categorical Okabe-Ito, correct for continuous data).
+ Legibility verdict: PASS
+
+ Dark render (plot-dark.png):
+ Background: Warm near-black (#1A1A17) - correct dark theme surface, not pure black
+ Chrome: Title in off-white (#F0EFE8) clearly readable against dark background. Subtitle in INK_SOFT dark token (#B8B7B0) clearly visible. X-axis and Y-axis labels in light off-white, clearly readable. Tick labels in #B8B7B0, visible against dark surface. Colorbar title and labels in #B8B7B0, readable. No dark-on-dark failures detected - all chrome text is appropriately light-colored.
+ Data: Viridis color stops identical to light render - same diagonal yellow chirp sweep, same teal/green noise floor. Data colors (positions in viridis) unchanged between themes, confirming only chrome flipped.
+ Legibility verdict: PASS
criteria_checklist:
visual_quality:
- score: 36
- max: 40
+ score: 28
+ max: 30
items:
- id: VQ-01
name: Text Legibility
- score: 9
- max: 10
+ score: 7
+ max: 8
passed: true
- comment: Title and axis labels are clearly readable; tick labels slightly
- small but legible
+ comment: All font sizes explicitly set (title 28px, axis labels 22px, tick
+ labels 18px); readable in both themes; minor deduction for colorbar labels
+ being on smaller end within the margin column
- id: VQ-02
name: No Overlap
- score: 8
- max: 8
+ score: 6
+ max: 6
passed: true
- comment: No overlapping text elements
+ comment: No overlapping text; axis label stepping configured to avoid crowding
- id: VQ-03
name: Element Visibility
- score: 8
- max: 8
+ score: 6
+ max: 6
passed: true
- comment: Heatmap cells are appropriately sized for the data density
+ comment: Chirp sweep clearly visible as bright diagonal band; heatmap cells
+ well-sized at 80x50 grid; colorbar distinct
- id: VQ-04
name: Color Accessibility
- score: 5
- max: 5
+ score: 2
+ max: 2
passed: true
- comment: Viridis colormap is colorblind-safe
+ comment: Viridis is perceptually uniform and fully CVD-safe; excellent contrast
+ across power range
- id: VQ-05
- name: Layout Balance
- score: 4
- max: 5
+ name: Layout & Canvas
+ score: 3
+ max: 4
passed: true
- comment: Good canvas utilization; slight imbalance with right margin for legend
+ comment: Plot fills approximately 70-75% of canvas; margins balanced and accommodate
+ colorbar; slight bottom whitespace prevents perfect 4
- id: VQ-06
- name: Axis Labels
+ name: Axis Labels & Title
score: 2
max: 2
passed: true
- comment: '"Time (seconds)" and "Frequency (Hz)" with proper units'
+ comment: 'X: Time (seconds), Y: Frequency (Hz), colorbar: Power (dB) - all
+ descriptive with units'
- id: VQ-07
- name: Grid & Legend
- score: 0
+ name: Palette Compliance
+ score: 2
max: 2
passed: true
- comment: No visible grid lines; legend/colorbar is present but lacks subtle
- gridlines on plot
+ comment: 'Viridis stops correctly applied to continuous data; light bg #FAF8F1,
+ dark bg #1A1A17; all chrome tokens theme-adaptive in both renders'
+ design_excellence:
+ score: 12
+ max: 20
+ items:
+ - id: DE-01
+ name: Aesthetic Sophistication
+ score: 4
+ max: 8
+ passed: false
+ comment: Well-configured professional appearance with proper theme tokens
+ and viridis colormap, but falls in configured library default tier; no custom
+ typography or distinctive visual flourish
+ - id: DE-02
+ name: Visual Refinement
+ score: 4
+ max: 6
+ passed: true
+ comment: 'Custom margins, borderWidth: 0 for cleaner cells, subtle GRID tokens;
+ noticeably above bare defaults'
+ - id: DE-03
+ name: Data Storytelling
+ score: 4
+ max: 6
+ passed: true
+ comment: Subtitle contextualizes signal; viridis creates natural focal point
+ with bright yellow sweep; viewer reads chirp trajectory immediately
spec_compliance:
- score: 25
- max: 25
+ score: 15
+ max: 15
items:
- id: SC-01
name: Plot Type
- score: 8
- max: 8
- passed: true
- comment: Correct heatmap spectrogram visualization
- - id: SC-02
- name: Data Mapping
score: 5
max: 5
passed: true
- comment: Time on x-axis, frequency on y-axis, power as color intensity
- - id: SC-03
+ comment: Correct spectrogram as time-frequency heatmap using HeatmapSeries
+ - id: SC-02
name: Required Features
- score: 5
- max: 5
+ score: 4
+ max: 4
passed: true
- comment: Colorbar with dB units, proper axis labels, perceptually uniform
- colormap
- - id: SC-04
- name: Data Range
+ comment: Color intensity encodes power; dB scale; time on X, frequency on
+ Y; colorbar with dB labels; chirp demonstrates non-stationary frequency
+ content
+ - id: SC-03
+ name: Data Mapping
score: 3
max: 3
passed: true
- comment: Full time and frequency range displayed
- - id: SC-05
- name: Legend Accuracy
- score: 2
- max: 2
- passed: true
- comment: Colorbar correctly labeled "Power (dB)"
- - id: SC-06
- name: Title Format
- score: 2
- max: 2
+ comment: Time (seconds) on X, Frequency (Hz) on Y, Power (dB) in color - all
+ correct
+ - id: SC-04
+ name: Title & Legend
+ score: 3
+ max: 3
passed: true
- comment: Uses correct format "{spec-id} · {library} · pyplots.ai"
+ comment: Title 'spectrogram-basic · highcharts · anyplot.ai' matches required
+ format; colorbar labeled 'Power (dB)'
data_quality:
- score: 18
- max: 20
+ score: 14
+ max: 15
items:
- id: DQ-01
name: Feature Coverage
- score: 7
- max: 8
+ score: 5
+ max: 6
passed: true
- comment: Chirp signal clearly shows frequency increasing over time; could
- show additional signal features
+ comment: Demonstrates chirp sweep, background noise, power dynamic range,
+ full spectrogram structure; minor deduction for 80x50 downsampling losing
+ fine frequency resolution
- id: DQ-02
name: Realistic Context
- score: 7
- max: 7
+ score: 5
+ max: 5
passed: true
- comment: Signal processing context is realistic and neutral
+ comment: Chirp signal with gaussian noise is a standard neutral signal processing
+ test case; scientifically realistic and non-controversial
- id: DQ-03
name: Appropriate Scale
score: 4
- max: 5
+ max: 4
passed: true
- comment: Frequencies 10-200 Hz appropriate; y-axis extends to ~500 Hz which
- is fine but shows noise above signal range
+ comment: 1000 Hz sample rate, 2-second duration, 10-200 Hz sweep, dB scaling
+ - all physically realistic and domain-appropriate
code_quality:
score: 9
max: 10
items:
- id: CQ-01
name: KISS Structure
- score: 3
+ score: 2
max: 3
passed: true
- comment: 'Linear flow: imports → data → plot → save'
+ comment: Sequential flow clear but QuietHTTPRequestHandler class definition
+ deviates from pure imports-data-plot-save pattern
- id: CQ-02
name: Reproducibility
- score: 3
- max: 3
+ score: 2
+ max: 2
passed: true
- comment: Uses np.random.seed(42)
+ comment: np.random.seed(42) set before noise generation
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
- comment: All imports are used
+ comment: All imported modules are used
- id: CQ-04
- name: No Deprecated API
- score: 1
- max: 1
+ name: Code Elegance
+ score: 2
+ max: 2
passed: true
- comment: Uses current APIs
+ comment: HTTP server approach justified; spectrogram pipeline clean; try/finally
+ ensures server cleanup
- id: CQ-05
- name: Output Correct
- score: 0
+ name: Output & API
+ score: 1
max: 1
- passed: false
- comment: Saves as plot.png (correct)
- library_features:
- score: 3
- max: 5
+ passed: true
+ comment: Saves plot-{THEME}.png and plot-{THEME}.html correctly for interactive
+ library
+ library_mastery:
+ score: 7
+ max: 10
items:
- - id: LF-01
- name: Uses distinctive library features
+ - id: LM-01
+ name: Idiomatic Usage
+ score: 4
+ max: 5
+ passed: true
+ comment: Correctly uses HeatmapSeries with [x, y, value] format, color_axis
+ for colorbar, chart.to_js_literal(); deduction for CDN via HTTP server instead
+ of recommended inline embedding
+ - id: LM-02
+ name: Distinctive Features
score: 3
max: 5
passed: true
- comment: Uses Highcharts heatmap with proper color axis and tooltip; could
- leverage more interactive features
+ comment: Highcharts-specific color_axis with custom viridis stops, symbolHeight/symbolWidth
+ for colorbar legend, HeatmapSeries - not trivially portable to other libraries
verdict: APPROVED
+impl_tags:
+ dependencies:
+ - scipy
+ - selenium
+ techniques:
+ - colorbar
+ - html-export
+ patterns:
+ - data-generation
+ - matrix-construction
+ - iteration-over-groups
+ dataprep:
+ - stft
+ styling:
+ - custom-colormap