diff --git a/plots/indicator-sma/implementations/highcharts.py b/plots/indicator-sma/implementations/highcharts.py
new file mode 100644
index 0000000000..cf5cfe5df0
--- /dev/null
+++ b/plots/indicator-sma/implementations/highcharts.py
@@ -0,0 +1,200 @@
+""" pyplots.ai
+indicator-sma: Simple Moving Average (SMA) Indicator Chart
+Library: highcharts unknown | Python 3.13.11
+Quality: 91/100 | Created: 2026-01-11
+"""
+
+import json
+import tempfile
+import time
+import urllib.request
+from pathlib import Path
+
+import numpy as np
+import pandas as pd
+from selenium import webdriver
+from selenium.webdriver.chrome.options import Options
+
+
+# Generate sample stock data (365 trading days)
+np.random.seed(42)
+n_days = 365
+dates = pd.date_range(start="2024-01-02", periods=n_days, freq="B") # Business days
+
+# Generate realistic stock price movement using random walk
+returns = np.random.normal(0.0005, 0.015, n_days) # Daily returns
+price_start = 150.0
+close_prices = price_start * np.cumprod(1 + returns)
+
+# Add some trend and volatility patterns
+trend = np.linspace(0, 20, n_days)
+close_prices = close_prices + trend
+
+# Create DataFrame
+df = pd.DataFrame({"date": dates, "close": close_prices})
+
+# Calculate SMAs
+df["sma_20"] = df["close"].rolling(window=20).mean()
+df["sma_50"] = df["close"].rolling(window=50).mean()
+df["sma_200"] = df["close"].rolling(window=200).mean()
+
+# Convert dates to timestamps for Highcharts (milliseconds since epoch)
+timestamps = [int(d.timestamp() * 1000) for d in df["date"]]
+
+# Prepare data series (as [timestamp, value] pairs, handling NaN for initial periods)
+close_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["close"], strict=True)]
+sma20_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["sma_20"], strict=True) if not np.isnan(v)]
+sma50_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["sma_50"], strict=True) if not np.isnan(v)]
+sma200_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["sma_200"], strict=True) if not np.isnan(v)]
+
+# Colors (colorblind-safe palette)
+colors = {
+ "close": "#306998", # Python Blue - price line
+ "sma20": "#FFD43B", # Python Yellow - short-term
+ "sma50": "#17BECF", # Cyan - medium-term
+ "sma200": "#9467BD", # Purple - long-term
+}
+
+# Chart options for Highcharts
+chart_options = {
+ "chart": {
+ "type": "line",
+ "width": 4800,
+ "height": 2700,
+ "backgroundColor": "#ffffff",
+ "marginBottom": 200,
+ "marginLeft": 220,
+ "marginRight": 100,
+ "marginTop": 180,
+ "style": {"fontFamily": "Arial, sans-serif"},
+ },
+ "title": {
+ "text": "indicator-sma · highcharts · pyplots.ai",
+ "style": {"fontSize": "64px", "fontWeight": "bold", "color": "#333333"},
+ "y": 70,
+ },
+ "subtitle": {
+ "text": "Stock Price with 20, 50, and 200-day Simple Moving Averages",
+ "style": {"fontSize": "36px", "color": "#666666"},
+ "y": 130,
+ },
+ "xAxis": {
+ "type": "datetime",
+ "title": {"text": "Date", "style": {"fontSize": "40px", "color": "#333333"}, "margin": 25},
+ "labels": {"style": {"fontSize": "28px", "color": "#333333"}, "format": "{value:%b %Y}", "y": 35},
+ "gridLineWidth": 1,
+ "gridLineColor": "rgba(0, 0, 0, 0.1)",
+ "lineWidth": 2,
+ "lineColor": "#333333",
+ "tickWidth": 2,
+ "tickColor": "#333333",
+ "tickLength": 12,
+ },
+ "yAxis": {
+ "title": {"text": "Price (USD)", "style": {"fontSize": "40px", "color": "#333333"}, "margin": 25},
+ "labels": {"style": {"fontSize": "28px", "color": "#333333"}, "format": "${value:.0f}", "x": -15},
+ "gridLineWidth": 1,
+ "gridLineColor": "rgba(0, 0, 0, 0.1)",
+ "lineWidth": 2,
+ "lineColor": "#333333",
+ },
+ "legend": {
+ "enabled": True,
+ "align": "right",
+ "verticalAlign": "top",
+ "layout": "vertical",
+ "x": -60,
+ "y": 120,
+ "itemStyle": {"fontSize": "28px", "color": "#333333"},
+ "itemMarginBottom": 15,
+ "symbolWidth": 40,
+ "symbolHeight": 18,
+ },
+ "tooltip": {
+ "shared": True,
+ "valueDecimals": 2,
+ "valuePrefix": "$",
+ "headerFormat": '{point.key:%b %d, %Y}
',
+ "style": {"fontSize": "22px"},
+ },
+ "plotOptions": {"line": {"lineWidth": 4, "marker": {"enabled": False}}, "series": {"animation": False}},
+ "credits": {"enabled": False},
+ "series": [
+ {"name": "Close Price", "data": close_data, "color": colors["close"], "lineWidth": 5, "zIndex": 4},
+ {
+ "name": "SMA 20",
+ "data": sma20_data,
+ "color": colors["sma20"],
+ "lineWidth": 3,
+ "dashStyle": "Solid",
+ "zIndex": 3,
+ },
+ {
+ "name": "SMA 50",
+ "data": sma50_data,
+ "color": colors["sma50"],
+ "lineWidth": 3,
+ "dashStyle": "ShortDash",
+ "zIndex": 2,
+ },
+ {
+ "name": "SMA 200",
+ "data": sma200_data,
+ "color": colors["sma200"],
+ "lineWidth": 3,
+ "dashStyle": "LongDash",
+ "zIndex": 1,
+ },
+ ],
+}
+
+# Download Highcharts JS
+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")
+
+# Generate chart options JSON
+chart_options_json = json.dumps(chart_options)
+
+# Generate HTML with inline scripts
+html_content = f"""
+
+
+
+
+
+
+
+
+
+"""
+
+# Write temp HTML file
+with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
+ f.write(html_content)
+ temp_path = f.name
+
+# Also save the HTML for interactive viewing
+with open("plot.html", "w", encoding="utf-8") as f:
+ f.write(html_content)
+
+# Take screenshot with headless Chrome
+chrome_options = Options()
+chrome_options.add_argument("--headless")
+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=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()
+
+# Clean up temp file
+Path(temp_path).unlink()
diff --git a/plots/indicator-sma/metadata/highcharts.yaml b/plots/indicator-sma/metadata/highcharts.yaml
new file mode 100644
index 0000000000..a4d48adfea
--- /dev/null
+++ b/plots/indicator-sma/metadata/highcharts.yaml
@@ -0,0 +1,219 @@
+library: highcharts
+specification_id: indicator-sma
+created: '2026-01-11T00:55:05Z'
+updated: '2026-01-11T00:57:47Z'
+generated_by: claude-opus-4-5-20251101
+workflow_run: 20886980688
+issue: 3651
+python_version: 3.13.11
+library_version: unknown
+preview_url: https://storage.googleapis.com/pyplots-images/plots/indicator-sma/highcharts/plot.png
+preview_thumb: https://storage.googleapis.com/pyplots-images/plots/indicator-sma/highcharts/plot_thumb.png
+preview_html: https://storage.googleapis.com/pyplots-images/plots/indicator-sma/highcharts/plot.html
+quality_score: 91
+review:
+ strengths:
+ - Excellent visual clarity with well-differentiated lines using color and dash styles
+ - Proper handling of NaN values for initial SMA periods (filtered out correctly)
+ - Clean, well-structured code with good separation of concerns
+ - Colorblind-safe palette following project guidelines
+ - Appropriate use of Highcharts tooltip sharing for multi-series comparison
+ - Realistic stock data generation with trend and volatility
+ weaknesses:
+ - Legend could be better integrated within the plot area rather than floating outside
+ - Does not use Highcharts Stock module which would provide native financial chart
+ features like range selector
+ image_description: |-
+ The plot displays a stock price chart with Simple Moving Average (SMA) overlays over approximately 365 business days from January 2024 to May 2025. The chart shows:
+
+ - Close Price (blue solid line, #306998) - the main price series showing typical stock price volatility with an overall upward trend from ~$150 to ~$195
+ - SMA 20 (yellow solid line, #FFD43B) - short-term moving average closely following the price
+ - SMA 50 (cyan dashed line, #17BECF) - medium-term moving average showing smoother trend
+ - SMA 200 (purple long-dashed line, #9467BD) - long-term moving average starting later due to the 200-day calculation window
+
+ The title "indicator-sma · highcharts · pyplots.ai" appears at the top with a subtitle describing the chart. The Y-axis shows "Price (USD)" with dollar values ranging from $126 to $212. The X-axis shows dates formatted as "Month Year". A legend is positioned in the upper right corner identifying all four series. Grid lines are visible but subtle.
+ criteria_checklist:
+ visual_quality:
+ score: 36
+ max: 40
+ items:
+ - id: VQ-01
+ name: Text Legibility
+ score: 9
+ max: 10
+ passed: true
+ comment: Title, axis labels, and tick marks are all clearly readable. Font
+ sizes are appropriate for the 4800x2700 canvas, though tick labels could
+ be slightly larger.
+ - id: VQ-02
+ name: No Overlap
+ score: 8
+ max: 8
+ passed: true
+ comment: No overlapping text elements. X-axis labels are well-spaced, legend
+ does not overlap data.
+ - id: VQ-03
+ name: Element Visibility
+ score: 7
+ max: 8
+ passed: true
+ comment: Line widths are appropriate. The price line is prominently thicker
+ (5px) while SMA lines are thinner (3px). Lines are clearly distinguishable.
+ - id: VQ-04
+ name: Color Accessibility
+ score: 5
+ max: 5
+ passed: true
+ comment: Colorblind-safe palette using blue, yellow, cyan, and purple. No
+ red-green conflicts.
+ - id: VQ-05
+ name: Layout Balance
+ score: 5
+ max: 5
+ passed: true
+ comment: Plot fills the canvas well with balanced margins. Good use of space.
+ - id: VQ-06
+ name: Axis Labels
+ score: 2
+ max: 2
+ passed: true
+ comment: Y-axis has Price (USD) with currency formatting. X-axis shows Date
+ with proper date formatting.
+ - id: VQ-07
+ name: Grid & Legend
+ score: 0
+ max: 2
+ passed: false
+ comment: Grid is subtle and appropriate. However, the legend is positioned
+ outside the plot area on the upper right, which is functional but could
+ be better integrated.
+ spec_compliance:
+ score: 25
+ max: 25
+ items:
+ - id: SC-01
+ name: Plot Type
+ score: 8
+ max: 8
+ passed: true
+ comment: Correct line chart type for time series with SMA overlays
+ - id: SC-02
+ name: Data Mapping
+ score: 5
+ max: 5
+ passed: true
+ comment: X-axis is datetime, Y-axis is price values
+ - id: SC-03
+ name: Required Features
+ score: 5
+ max: 5
+ passed: true
+ comment: 'All required features present: price line, 20/50/200-day SMAs, legend
+ showing periods'
+ - id: SC-04
+ name: Data Range
+ score: 3
+ max: 3
+ passed: true
+ comment: All data visible, appropriate axis scaling
+ - id: SC-05
+ name: Legend Accuracy
+ score: 2
+ max: 2
+ passed: true
+ comment: Legend correctly labels Close Price, SMA 20, SMA 50, SMA 200
+ - id: SC-06
+ name: Title Format
+ score: 2
+ max: 2
+ passed: true
+ comment: Title follows format {spec-id} · {library} · pyplots.ai
+ data_quality:
+ score: 18
+ max: 20
+ items:
+ - id: DQ-01
+ name: Feature Coverage
+ score: 7
+ max: 8
+ passed: true
+ comment: 'Shows all aspects: price volatility, trend following by SMAs, SMA
+ crossovers visible around August/September, different smoothing levels.
+ Missing explicit golden/death cross demonstration.'
+ - id: DQ-02
+ name: Realistic Context
+ score: 7
+ max: 7
+ passed: true
+ comment: Realistic stock price movement using random walk with trend, plausible
+ price range and volatility
+ - id: DQ-03
+ name: Appropriate Scale
+ score: 4
+ max: 5
+ passed: true
+ comment: Reasonable starting price ($150), realistic daily returns (~0.05%
+ mean, 1.5% std), appropriate 365-day period
+ code_quality:
+ score: 9
+ max: 10
+ items:
+ - id: CQ-01
+ name: KISS Structure
+ score: 3
+ max: 3
+ passed: true
+ comment: 'Clean linear structure: imports, data generation, SMA calculation,
+ chart config, export'
+ - id: CQ-02
+ name: Reproducibility
+ score: 3
+ max: 3
+ passed: true
+ comment: np.random.seed(42) ensures reproducibility
+ - id: CQ-03
+ name: Clean Imports
+ score: 2
+ max: 2
+ passed: true
+ comment: All imports are used (json, tempfile, time, urllib, Path, numpy,
+ pandas, selenium)
+ - id: CQ-04
+ name: No Deprecated API
+ score: 1
+ max: 1
+ passed: true
+ comment: Using current Highcharts API patterns
+ - id: CQ-05
+ name: Output Correct
+ score: 0
+ max: 1
+ passed: false
+ comment: Outputs both plot.png and plot.html, which is correct for Highcharts
+ library_features:
+ score: 3
+ max: 5
+ items:
+ - id: LF-01
+ name: Distinctive Features
+ score: 3
+ max: 5
+ passed: true
+ comment: Uses Highcharts datetime axis, tooltip formatting with shared tooltip
+ and value formatting. Uses dashStyle for line differentiation. Could leverage
+ more Highcharts-specific features like stock chart module or interactive
+ zooming.
+ verdict: APPROVED
+impl_tags:
+ dependencies:
+ - selenium
+ techniques:
+ - html-export
+ - hover-tooltips
+ patterns:
+ - data-generation
+ - time-series
+ dataprep:
+ - rolling-window
+ styling:
+ - grid-styling