Skip to content

Commit 0407fb3

Browse files
feat(highcharts): implement candlestick-volume (#3159)
## Implementation: `candlestick-volume` - highcharts Implements the **highcharts** version of `candlestick-volume`. **File:** `plots/candlestick-volume/implementations/highcharts.py` **Parent Issue:** #3068 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20645827229)* --------- 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 faf5fe5 commit 0407fb3

2 files changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
""" pyplots.ai
2+
candlestick-volume: Stock Candlestick Chart with Volume
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 90/100 | Created: 2026-01-01
5+
"""
6+
7+
import json
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from datetime import datetime, timedelta
12+
from pathlib import Path
13+
14+
import numpy as np
15+
from selenium import webdriver
16+
from selenium.webdriver.chrome.options import Options
17+
18+
19+
# Data - Generate 60 trading days of OHLC data
20+
np.random.seed(42)
21+
22+
# Start date and generate trading days (skip weekends)
23+
start_date = datetime(2024, 9, 2) # A Monday
24+
dates = []
25+
current_date = start_date
26+
while len(dates) < 60:
27+
if current_date.weekday() < 5: # Monday to Friday
28+
dates.append(current_date)
29+
current_date += timedelta(days=1)
30+
31+
# Generate realistic stock price movements
32+
n_days = 60
33+
initial_price = 150.0
34+
returns = np.random.normal(0.001, 0.02, n_days) # Daily returns with slight upward bias
35+
close_prices = initial_price * np.cumprod(1 + returns)
36+
37+
# Generate OHLC from close prices
38+
open_prices = np.zeros(n_days)
39+
high_prices = np.zeros(n_days)
40+
low_prices = np.zeros(n_days)
41+
volumes = np.zeros(n_days)
42+
43+
open_prices[0] = initial_price
44+
for i in range(n_days):
45+
if i > 0:
46+
# Open is close of previous day with small gap
47+
gap = np.random.normal(0, close_prices[i - 1] * 0.005)
48+
open_prices[i] = close_prices[i - 1] + gap
49+
50+
# High and low based on volatility
51+
volatility = abs(close_prices[i] - open_prices[i]) + np.random.uniform(0.5, 2.0)
52+
if close_prices[i] >= open_prices[i]: # Bullish candle
53+
high_prices[i] = max(open_prices[i], close_prices[i]) + np.random.uniform(0.3, volatility)
54+
low_prices[i] = min(open_prices[i], close_prices[i]) - np.random.uniform(0.2, volatility * 0.7)
55+
else: # Bearish candle
56+
high_prices[i] = max(open_prices[i], close_prices[i]) + np.random.uniform(0.2, volatility * 0.7)
57+
low_prices[i] = min(open_prices[i], close_prices[i]) - np.random.uniform(0.3, volatility)
58+
59+
# Ensure high >= max(open, close) and low <= min(open, close)
60+
high_prices[i] = max(high_prices[i], open_prices[i], close_prices[i])
61+
low_prices[i] = min(low_prices[i], open_prices[i], close_prices[i])
62+
63+
# Volume: higher on days with larger price moves
64+
base_volume = 5_000_000
65+
move_factor = 1 + abs(close_prices[i] - open_prices[i]) / open_prices[i] * 20
66+
volumes[i] = int(base_volume * move_factor * np.random.uniform(0.6, 1.4))
67+
68+
# Convert dates to JavaScript timestamps (milliseconds since epoch)
69+
timestamps = [int(d.timestamp() * 1000) for d in dates]
70+
71+
# Prepare data for Highcharts
72+
ohlc_data = []
73+
volume_data = []
74+
75+
for i in range(n_days):
76+
ohlc_data.append(
77+
[
78+
timestamps[i],
79+
round(open_prices[i], 2),
80+
round(high_prices[i], 2),
81+
round(low_prices[i], 2),
82+
round(close_prices[i], 2),
83+
]
84+
)
85+
# Volume color matches candle direction
86+
color = "#306998" if close_prices[i] >= open_prices[i] else "#E74C3C"
87+
volume_data.append({"x": timestamps[i], "y": int(volumes[i]), "color": color})
88+
89+
# Convert to JSON for JavaScript
90+
ohlc_json = json.dumps(ohlc_data)
91+
volume_json = json.dumps(volume_data)
92+
93+
# Chart configuration using Highstock (for synchronized charts)
94+
chart_js = """
95+
Highcharts.stockChart('container', {
96+
chart: {
97+
width: 4800,
98+
height: 2700,
99+
backgroundColor: '#ffffff',
100+
spacingBottom: 100,
101+
style: {
102+
fontFamily: 'Arial, sans-serif'
103+
}
104+
},
105+
106+
title: {
107+
text: 'candlestick-volume \\u00b7 highcharts \\u00b7 pyplots.ai',
108+
style: {
109+
fontSize: '48px',
110+
fontWeight: 'bold'
111+
}
112+
},
113+
114+
rangeSelector: {
115+
enabled: false
116+
},
117+
118+
navigator: {
119+
enabled: false
120+
},
121+
122+
scrollbar: {
123+
enabled: false
124+
},
125+
126+
credits: {
127+
enabled: false
128+
},
129+
130+
yAxis: [{
131+
labels: {
132+
align: 'right',
133+
x: -10,
134+
style: {
135+
fontSize: '24px'
136+
},
137+
formatter: function() {
138+
return '$' + this.value.toFixed(0);
139+
}
140+
},
141+
title: {
142+
text: 'Price (USD)',
143+
style: {
144+
fontSize: '28px'
145+
}
146+
},
147+
height: '70%',
148+
lineWidth: 2,
149+
resize: {
150+
enabled: false
151+
},
152+
gridLineWidth: 1,
153+
gridLineColor: '#E0E0E0'
154+
}, {
155+
labels: {
156+
align: 'right',
157+
x: -10,
158+
style: {
159+
fontSize: '24px'
160+
},
161+
formatter: function() {
162+
return (this.value / 1000000).toFixed(1) + 'M';
163+
}
164+
},
165+
title: {
166+
text: 'Volume',
167+
style: {
168+
fontSize: '28px'
169+
}
170+
},
171+
top: '72%',
172+
height: '22%',
173+
offset: 0,
174+
lineWidth: 2,
175+
gridLineWidth: 1,
176+
gridLineColor: '#E0E0E0'
177+
}],
178+
179+
xAxis: {
180+
type: 'datetime',
181+
labels: {
182+
style: {
183+
fontSize: '28px'
184+
},
185+
format: '{value:%b %d}',
186+
y: 40
187+
},
188+
crosshair: {
189+
width: 2,
190+
color: '#888888',
191+
snap: false
192+
},
193+
gridLineWidth: 1,
194+
gridLineColor: '#E0E0E0',
195+
lineWidth: 2
196+
},
197+
198+
tooltip: {
199+
split: true,
200+
style: {
201+
fontSize: '20px'
202+
}
203+
},
204+
205+
plotOptions: {
206+
candlestick: {
207+
color: '#E74C3C',
208+
upColor: '#306998',
209+
lineColor: '#E74C3C',
210+
upLineColor: '#306998',
211+
lineWidth: 2
212+
},
213+
column: {
214+
borderWidth: 0
215+
}
216+
},
217+
218+
series: [{
219+
type: 'candlestick',
220+
name: 'Stock Price',
221+
data: OHLC_DATA_PLACEHOLDER,
222+
yAxis: 0
223+
}, {
224+
type: 'column',
225+
name: 'Volume',
226+
data: VOLUME_DATA_PLACEHOLDER,
227+
yAxis: 1
228+
}]
229+
});
230+
"""
231+
232+
# Replace data placeholders
233+
chart_js = chart_js.replace("OHLC_DATA_PLACEHOLDER", ohlc_json)
234+
chart_js = chart_js.replace("VOLUME_DATA_PLACEHOLDER", volume_json)
235+
236+
# Download Highcharts and Highstock JS
237+
highcharts_url = "https://code.highcharts.com/stock/highstock.js"
238+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
239+
highcharts_js = response.read().decode("utf-8")
240+
241+
# Generate HTML with inline scripts
242+
html_content = f"""<!DOCTYPE html>
243+
<html>
244+
<head>
245+
<meta charset="utf-8">
246+
<script>{highcharts_js}</script>
247+
</head>
248+
<body style="margin:0; padding:0;">
249+
<div id="container" style="width: 4800px; height: 2700px;"></div>
250+
<script>
251+
{chart_js}
252+
</script>
253+
</body>
254+
</html>"""
255+
256+
# Write temp HTML file
257+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
258+
f.write(html_content)
259+
temp_path = f.name
260+
261+
# Also save HTML for interactive version
262+
with open("plot.html", "w", encoding="utf-8") as f:
263+
f.write(html_content)
264+
265+
# Take screenshot using Selenium
266+
chrome_options = Options()
267+
chrome_options.add_argument("--headless")
268+
chrome_options.add_argument("--no-sandbox")
269+
chrome_options.add_argument("--disable-dev-shm-usage")
270+
chrome_options.add_argument("--disable-gpu")
271+
chrome_options.add_argument("--window-size=4800,2700")
272+
273+
driver = webdriver.Chrome(options=chrome_options)
274+
driver.get(f"file://{temp_path}")
275+
time.sleep(8) # Wait for chart to render
276+
driver.save_screenshot("plot.png")
277+
driver.quit()
278+
279+
# Clean up temp file
280+
Path(temp_path).unlink()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
library: highcharts
2+
specification_id: candlestick-volume
3+
created: '2026-01-01T21:31:03Z'
4+
updated: '2026-01-01T21:33:14Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20645827229
7+
issue: 3068
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/candlestick-volume/highcharts/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Excellent use of Highstock for synchronized dual-pane layout
17+
- Proper 70%/25% vertical split between price and volume panes as specified
18+
- Volume bars correctly color-matched to candlestick direction
19+
- Crosshair configured for precise price/volume reading across both panes
20+
- Realistic stock data generation with proper gap handling and volatility modeling
21+
- Clean title format following pyplots.ai convention
22+
- Well-configured axis formatting ($ prefix for prices, M suffix for volumes)
23+
weaknesses:
24+
- Blue/red color scheme could be improved for colorblind accessibility (consider
25+
blue/orange instead)
26+
- X-axis date labels appear clipped/truncated at bottom of image

0 commit comments

Comments
 (0)