Skip to content

Commit 6393d95

Browse files
feat(highcharts): implement indicator-sma (#3691)
## Implementation: `indicator-sma` - highcharts Implements the **highcharts** version of `indicator-sma`. **File:** `plots/indicator-sma/implementations/highcharts.py` **Parent Issue:** #3651 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20886980688)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent d907c19 commit 6393d95

File tree

2 files changed

+419
-0
lines changed

2 files changed

+419
-0
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
""" pyplots.ai
2+
indicator-sma: Simple Moving Average (SMA) Indicator Chart
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-11
5+
"""
6+
7+
import json
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from pathlib import Path
12+
13+
import numpy as np
14+
import pandas as pd
15+
from selenium import webdriver
16+
from selenium.webdriver.chrome.options import Options
17+
18+
19+
# Generate sample stock data (365 trading days)
20+
np.random.seed(42)
21+
n_days = 365
22+
dates = pd.date_range(start="2024-01-02", periods=n_days, freq="B") # Business days
23+
24+
# Generate realistic stock price movement using random walk
25+
returns = np.random.normal(0.0005, 0.015, n_days) # Daily returns
26+
price_start = 150.0
27+
close_prices = price_start * np.cumprod(1 + returns)
28+
29+
# Add some trend and volatility patterns
30+
trend = np.linspace(0, 20, n_days)
31+
close_prices = close_prices + trend
32+
33+
# Create DataFrame
34+
df = pd.DataFrame({"date": dates, "close": close_prices})
35+
36+
# Calculate SMAs
37+
df["sma_20"] = df["close"].rolling(window=20).mean()
38+
df["sma_50"] = df["close"].rolling(window=50).mean()
39+
df["sma_200"] = df["close"].rolling(window=200).mean()
40+
41+
# Convert dates to timestamps for Highcharts (milliseconds since epoch)
42+
timestamps = [int(d.timestamp() * 1000) for d in df["date"]]
43+
44+
# Prepare data series (as [timestamp, value] pairs, handling NaN for initial periods)
45+
close_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["close"], strict=True)]
46+
sma20_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["sma_20"], strict=True) if not np.isnan(v)]
47+
sma50_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["sma_50"], strict=True) if not np.isnan(v)]
48+
sma200_data = [[t, round(v, 2)] for t, v in zip(timestamps, df["sma_200"], strict=True) if not np.isnan(v)]
49+
50+
# Colors (colorblind-safe palette)
51+
colors = {
52+
"close": "#306998", # Python Blue - price line
53+
"sma20": "#FFD43B", # Python Yellow - short-term
54+
"sma50": "#17BECF", # Cyan - medium-term
55+
"sma200": "#9467BD", # Purple - long-term
56+
}
57+
58+
# Chart options for Highcharts
59+
chart_options = {
60+
"chart": {
61+
"type": "line",
62+
"width": 4800,
63+
"height": 2700,
64+
"backgroundColor": "#ffffff",
65+
"marginBottom": 200,
66+
"marginLeft": 220,
67+
"marginRight": 100,
68+
"marginTop": 180,
69+
"style": {"fontFamily": "Arial, sans-serif"},
70+
},
71+
"title": {
72+
"text": "indicator-sma · highcharts · pyplots.ai",
73+
"style": {"fontSize": "64px", "fontWeight": "bold", "color": "#333333"},
74+
"y": 70,
75+
},
76+
"subtitle": {
77+
"text": "Stock Price with 20, 50, and 200-day Simple Moving Averages",
78+
"style": {"fontSize": "36px", "color": "#666666"},
79+
"y": 130,
80+
},
81+
"xAxis": {
82+
"type": "datetime",
83+
"title": {"text": "Date", "style": {"fontSize": "40px", "color": "#333333"}, "margin": 25},
84+
"labels": {"style": {"fontSize": "28px", "color": "#333333"}, "format": "{value:%b %Y}", "y": 35},
85+
"gridLineWidth": 1,
86+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
87+
"lineWidth": 2,
88+
"lineColor": "#333333",
89+
"tickWidth": 2,
90+
"tickColor": "#333333",
91+
"tickLength": 12,
92+
},
93+
"yAxis": {
94+
"title": {"text": "Price (USD)", "style": {"fontSize": "40px", "color": "#333333"}, "margin": 25},
95+
"labels": {"style": {"fontSize": "28px", "color": "#333333"}, "format": "${value:.0f}", "x": -15},
96+
"gridLineWidth": 1,
97+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
98+
"lineWidth": 2,
99+
"lineColor": "#333333",
100+
},
101+
"legend": {
102+
"enabled": True,
103+
"align": "right",
104+
"verticalAlign": "top",
105+
"layout": "vertical",
106+
"x": -60,
107+
"y": 120,
108+
"itemStyle": {"fontSize": "28px", "color": "#333333"},
109+
"itemMarginBottom": 15,
110+
"symbolWidth": 40,
111+
"symbolHeight": 18,
112+
},
113+
"tooltip": {
114+
"shared": True,
115+
"valueDecimals": 2,
116+
"valuePrefix": "$",
117+
"headerFormat": '<span style="font-size: 22px">{point.key:%b %d, %Y}</span><br/>',
118+
"style": {"fontSize": "22px"},
119+
},
120+
"plotOptions": {"line": {"lineWidth": 4, "marker": {"enabled": False}}, "series": {"animation": False}},
121+
"credits": {"enabled": False},
122+
"series": [
123+
{"name": "Close Price", "data": close_data, "color": colors["close"], "lineWidth": 5, "zIndex": 4},
124+
{
125+
"name": "SMA 20",
126+
"data": sma20_data,
127+
"color": colors["sma20"],
128+
"lineWidth": 3,
129+
"dashStyle": "Solid",
130+
"zIndex": 3,
131+
},
132+
{
133+
"name": "SMA 50",
134+
"data": sma50_data,
135+
"color": colors["sma50"],
136+
"lineWidth": 3,
137+
"dashStyle": "ShortDash",
138+
"zIndex": 2,
139+
},
140+
{
141+
"name": "SMA 200",
142+
"data": sma200_data,
143+
"color": colors["sma200"],
144+
"lineWidth": 3,
145+
"dashStyle": "LongDash",
146+
"zIndex": 1,
147+
},
148+
],
149+
}
150+
151+
# Download Highcharts JS
152+
highcharts_url = "https://code.highcharts.com/highcharts.js"
153+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
154+
highcharts_js = response.read().decode("utf-8")
155+
156+
# Generate chart options JSON
157+
chart_options_json = json.dumps(chart_options)
158+
159+
# Generate HTML with inline scripts
160+
html_content = f"""<!DOCTYPE html>
161+
<html>
162+
<head>
163+
<meta charset="utf-8">
164+
<script>{highcharts_js}</script>
165+
</head>
166+
<body style="margin:0; background-color: #ffffff;">
167+
<div id="container" style="width: 4800px; height: 2700px;"></div>
168+
<script>
169+
document.addEventListener('DOMContentLoaded', function() {{
170+
Highcharts.chart('container', {chart_options_json});
171+
}});
172+
</script>
173+
</body>
174+
</html>"""
175+
176+
# Write temp HTML file
177+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
178+
f.write(html_content)
179+
temp_path = f.name
180+
181+
# Also save the HTML for interactive viewing
182+
with open("plot.html", "w", encoding="utf-8") as f:
183+
f.write(html_content)
184+
185+
# Take screenshot with headless Chrome
186+
chrome_options = Options()
187+
chrome_options.add_argument("--headless")
188+
chrome_options.add_argument("--no-sandbox")
189+
chrome_options.add_argument("--disable-dev-shm-usage")
190+
chrome_options.add_argument("--disable-gpu")
191+
chrome_options.add_argument("--window-size=4800,2700")
192+
193+
driver = webdriver.Chrome(options=chrome_options)
194+
driver.get(f"file://{temp_path}")
195+
time.sleep(5) # Wait for chart to render
196+
driver.save_screenshot("plot.png")
197+
driver.quit()
198+
199+
# Clean up temp file
200+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)