Skip to content

Commit f89167e

Browse files
feat(highcharts): implement subplot-grid (#2810)
## Implementation: `subplot-grid` - highcharts Implements the **highcharts** version of `subplot-grid`. **File:** `plots/subplot-grid/implementations/highcharts.py` --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20602452357)* --------- 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 eb1442a commit f89167e

2 files changed

Lines changed: 316 additions & 0 deletions

File tree

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
""" pyplots.ai
2+
subplot-grid: Subplot Grid Layout
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-30
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
import numpy as np
13+
from highcharts_core.chart import Chart
14+
from highcharts_core.options import HighchartsOptions
15+
from highcharts_core.options.series.area import LineSeries
16+
from highcharts_core.options.series.bar import ColumnSeries
17+
from highcharts_core.options.series.scatter import ScatterSeries
18+
from selenium import webdriver
19+
from selenium.webdriver.chrome.options import Options
20+
21+
22+
# Data - Financial dashboard with price, volume, returns
23+
np.random.seed(42)
24+
days = 60
25+
dates = [f"Day {i + 1}" for i in range(days)]
26+
27+
# Stock price (random walk)
28+
price_changes = np.random.randn(days) * 2
29+
prices = 100 + np.cumsum(price_changes)
30+
31+
# Volume (random with some correlation to price change magnitude)
32+
volumes = 50000 + np.abs(price_changes) * 15000 + np.random.randn(days) * 10000
33+
volumes = np.maximum(volumes, 10000)
34+
35+
# Daily returns (percentage)
36+
returns = (price_changes / prices) * 100
37+
38+
# Moving average for price
39+
ma_window = 10
40+
ma = np.convolve(prices, np.ones(ma_window) / ma_window, mode="valid")
41+
42+
43+
# Create 4 separate charts for the grid
44+
# Chart 1: Price Line Chart (top-left)
45+
chart1 = Chart(container="container1")
46+
chart1.options = HighchartsOptions()
47+
chart1.options.chart = {
48+
"type": "line",
49+
"width": 2300,
50+
"height": 1250,
51+
"backgroundColor": "#ffffff",
52+
"marginBottom": 100,
53+
}
54+
chart1.options.title = {"text": "Stock Price Trend", "style": {"fontSize": "32px", "fontWeight": "bold"}}
55+
chart1.options.x_axis = {
56+
"title": {"text": "Trading Day", "style": {"fontSize": "24px"}},
57+
"labels": {"style": {"fontSize": "20px"}, "format": "Day {value}"},
58+
"tickInterval": 10,
59+
"min": 0,
60+
"max": days - 1,
61+
}
62+
chart1.options.y_axis = {
63+
"title": {"text": "Price ($)", "style": {"fontSize": "24px"}},
64+
"labels": {"style": {"fontSize": "18px"}},
65+
"gridLineWidth": 1,
66+
"gridLineColor": "#e0e0e0",
67+
}
68+
chart1.options.legend = {
69+
"enabled": True,
70+
"itemStyle": {"fontSize": "20px"},
71+
"align": "right",
72+
"verticalAlign": "top",
73+
"layout": "vertical",
74+
"x": -20,
75+
"y": 60,
76+
"floating": True,
77+
"backgroundColor": "rgba(255,255,255,0.9)",
78+
"borderWidth": 1,
79+
"borderColor": "#e0e0e0",
80+
}
81+
chart1.options.plot_options = {"line": {"lineWidth": 4, "marker": {"radius": 0}}}
82+
83+
price_series = LineSeries()
84+
price_series.data = [{"x": i, "y": float(p)} for i, p in enumerate(prices)]
85+
price_series.name = "Price"
86+
price_series.color = "#306998"
87+
chart1.add_series(price_series)
88+
89+
ma_series = LineSeries()
90+
ma_series.data = [{"x": i + ma_window - 1, "y": float(m)} for i, m in enumerate(ma)]
91+
ma_series.name = f"{ma_window}-Day MA"
92+
ma_series.color = "#FFD43B"
93+
ma_series.dash_style = "Dash"
94+
chart1.add_series(ma_series)
95+
96+
97+
# Chart 2: Volume Bar Chart (top-right)
98+
chart2 = Chart(container="container2")
99+
chart2.options = HighchartsOptions()
100+
chart2.options.chart = {
101+
"type": "column",
102+
"width": 2300,
103+
"height": 1250,
104+
"backgroundColor": "#ffffff",
105+
"marginBottom": 100,
106+
}
107+
chart2.options.title = {"text": "Trading Volume", "style": {"fontSize": "32px", "fontWeight": "bold"}}
108+
chart2.options.x_axis = {
109+
"title": {"text": "Trading Day", "style": {"fontSize": "24px"}},
110+
"labels": {"style": {"fontSize": "20px"}, "format": "Day {value}"},
111+
"tickInterval": 10,
112+
"min": 0,
113+
"max": days - 1,
114+
}
115+
chart2.options.y_axis = {
116+
"title": {"text": "Volume (shares)", "style": {"fontSize": "24px"}},
117+
"labels": {"style": {"fontSize": "18px"}},
118+
"gridLineWidth": 1,
119+
"gridLineColor": "#e0e0e0",
120+
}
121+
chart2.options.legend = {"enabled": False}
122+
chart2.options.plot_options = {"column": {"borderWidth": 0, "pointPadding": 0.1}}
123+
124+
volume_series = ColumnSeries()
125+
volume_series.data = [{"x": i, "y": float(v)} for i, v in enumerate(volumes)]
126+
volume_series.name = "Volume"
127+
volume_series.color = "#306998"
128+
chart2.add_series(volume_series)
129+
130+
131+
# Chart 3: Returns Histogram (bottom-left)
132+
# Create histogram bins manually
133+
hist_counts, bin_edges = np.histogram(returns, bins=15)
134+
bin_centers = [(bin_edges[i] + bin_edges[i + 1]) / 2 for i in range(len(bin_edges) - 1)]
135+
bin_labels = [f"{b:.1f}%" for b in bin_centers]
136+
137+
chart3 = Chart(container="container3")
138+
chart3.options = HighchartsOptions()
139+
chart3.options.chart = {
140+
"type": "column",
141+
"width": 2300,
142+
"height": 1250,
143+
"backgroundColor": "#ffffff",
144+
"marginBottom": 120,
145+
}
146+
chart3.options.title = {"text": "Daily Returns Distribution", "style": {"fontSize": "32px", "fontWeight": "bold"}}
147+
chart3.options.x_axis = {
148+
"categories": bin_labels,
149+
"title": {"text": "Return (%)", "style": {"fontSize": "24px"}},
150+
"labels": {"style": {"fontSize": "18px"}, "rotation": 0},
151+
}
152+
chart3.options.y_axis = {
153+
"title": {"text": "Frequency", "style": {"fontSize": "24px"}},
154+
"labels": {"style": {"fontSize": "18px"}},
155+
"gridLineWidth": 1,
156+
"gridLineColor": "#e0e0e0",
157+
}
158+
chart3.options.legend = {"enabled": False}
159+
chart3.options.plot_options = {"column": {"borderWidth": 0, "pointPadding": 0}}
160+
161+
hist_series = ColumnSeries()
162+
hist_series.data = [int(c) for c in hist_counts]
163+
hist_series.name = "Frequency"
164+
hist_series.color = "#FFD43B"
165+
chart3.add_series(hist_series)
166+
167+
168+
# Chart 4: Price vs Volume Scatter (bottom-right)
169+
chart4 = Chart(container="container4")
170+
chart4.options = HighchartsOptions()
171+
chart4.options.chart = {
172+
"type": "scatter",
173+
"width": 2300,
174+
"height": 1250,
175+
"backgroundColor": "#ffffff",
176+
"marginBottom": 100,
177+
}
178+
chart4.options.title = {"text": "Price vs Volume Relationship", "style": {"fontSize": "32px", "fontWeight": "bold"}}
179+
chart4.options.x_axis = {
180+
"title": {"text": "Stock Price ($)", "style": {"fontSize": "24px"}},
181+
"labels": {"style": {"fontSize": "18px"}},
182+
"gridLineWidth": 1,
183+
"gridLineColor": "#e0e0e0",
184+
}
185+
chart4.options.y_axis = {
186+
"title": {"text": "Volume (shares)", "style": {"fontSize": "24px"}},
187+
"labels": {"style": {"fontSize": "18px"}},
188+
"gridLineWidth": 1,
189+
"gridLineColor": "#e0e0e0",
190+
}
191+
chart4.options.legend = {"enabled": False}
192+
chart4.options.plot_options = {"scatter": {"marker": {"radius": 10, "symbol": "circle"}}}
193+
194+
scatter_series = ScatterSeries()
195+
scatter_series.data = [[float(p), float(v)] for p, v in zip(prices, volumes, strict=True)]
196+
scatter_series.name = "Price vs Volume"
197+
scatter_series.color = "#9467BD"
198+
chart4.add_series(scatter_series)
199+
200+
201+
# Download Highcharts JS
202+
highcharts_url = "https://code.highcharts.com/highcharts.js"
203+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
204+
highcharts_js = response.read().decode("utf-8")
205+
206+
# Generate JS for all charts
207+
js1 = chart1.to_js_literal()
208+
js2 = chart2.to_js_literal()
209+
js3 = chart3.to_js_literal()
210+
js4 = chart4.to_js_literal()
211+
212+
# Create HTML with 2x2 grid layout
213+
html_content = f"""<!DOCTYPE html>
214+
<html>
215+
<head>
216+
<meta charset="utf-8">
217+
<script>{highcharts_js}</script>
218+
<style>
219+
body {{
220+
margin: 0;
221+
padding: 50px;
222+
background: #ffffff;
223+
font-family: Arial, sans-serif;
224+
}}
225+
.main-title {{
226+
text-align: center;
227+
font-size: 48px;
228+
font-weight: bold;
229+
color: #333;
230+
margin-bottom: 30px;
231+
}}
232+
.grid-container {{
233+
display: grid;
234+
grid-template-columns: 1fr 1fr;
235+
grid-template-rows: 1fr 1fr;
236+
gap: 50px;
237+
width: 4700px;
238+
height: 2550px;
239+
}}
240+
.chart-cell {{
241+
background: #ffffff;
242+
border: 2px solid #e0e0e0;
243+
border-radius: 10px;
244+
overflow: hidden;
245+
}}
246+
</style>
247+
</head>
248+
<body>
249+
<div class="main-title">subplot-grid · highcharts · pyplots.ai</div>
250+
<div class="grid-container">
251+
<div class="chart-cell" id="container1"></div>
252+
<div class="chart-cell" id="container2"></div>
253+
<div class="chart-cell" id="container3"></div>
254+
<div class="chart-cell" id="container4"></div>
255+
</div>
256+
<script>
257+
{js1}
258+
{js2}
259+
{js3}
260+
{js4}
261+
</script>
262+
</body>
263+
</html>"""
264+
265+
# Save HTML file
266+
with open("plot.html", "w", encoding="utf-8") as f:
267+
f.write(html_content)
268+
269+
# Take screenshot with Selenium
270+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
271+
f.write(html_content)
272+
temp_path = f.name
273+
274+
chrome_options = Options()
275+
chrome_options.add_argument("--headless")
276+
chrome_options.add_argument("--no-sandbox")
277+
chrome_options.add_argument("--disable-dev-shm-usage")
278+
chrome_options.add_argument("--disable-gpu")
279+
chrome_options.add_argument("--window-size=4800,2700")
280+
281+
driver = webdriver.Chrome(options=chrome_options)
282+
driver.get(f"file://{temp_path}")
283+
time.sleep(5)
284+
driver.save_screenshot("plot.png")
285+
driver.quit()
286+
287+
Path(temp_path).unlink()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
library: highcharts
2+
specification_id: subplot-grid
3+
created: '2025-12-30T17:48:43Z'
4+
updated: '2025-12-30T18:06:59Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20602452357
7+
issue: 0
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/subplot-grid/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/subplot-grid/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/subplot-grid/highcharts/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent implementation of a financial dashboard subplot grid with 4 distinct
17+
chart types
18+
- Perfect title format following the spec-id · library · pyplots.ai convention
19+
- Colorblind-safe color palette throughout (blue, yellow, purple)
20+
- Clean CSS grid layout for positioning the 4 charts
21+
- Realistic financial data scenario with meaningful relationships between price,
22+
volume, and returns
23+
- Moving average overlay on price chart demonstrates multi-series capability
24+
- Good use of Highcharts container system for multi-chart layout
25+
weaknesses:
26+
- Histogram x-axis labels (return percentages) are not visible in the rendered output
27+
- LineSeries imported from .area module instead of .line module (minor code issue)
28+
- Could benefit from synchronized tooltips across charts for better interactivity
29+
- Legend disabled on most charts where it could provide useful context

0 commit comments

Comments
 (0)