Skip to content

Commit 07690bf

Browse files
feat(highcharts): implement point-and-figure-basic (#3862)
## Implementation: `point-and-figure-basic` - highcharts Implements the **highcharts** version of `point-and-figure-basic`. **File:** `plots/point-and-figure-basic/implementations/highcharts.py` **Parent Issue:** #3755 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047207473)* --------- 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 e4fafdf commit 07690bf

2 files changed

Lines changed: 488 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
""" pyplots.ai
2+
point-and-figure-basic: Point and Figure Chart
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
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.scatter import ScatterSeries
16+
from selenium import webdriver
17+
from selenium.webdriver.chrome.options import Options
18+
19+
20+
# Data - Generate realistic stock price data with trends and reversals
21+
np.random.seed(42)
22+
n_days = 300
23+
base_price = 100
24+
25+
# Create price series with multiple trends for interesting P&F patterns
26+
returns = np.zeros(n_days)
27+
# Uptrend phase
28+
returns[:80] = np.random.normal(0.003, 0.015, 80)
29+
# Downtrend phase
30+
returns[80:140] = np.random.normal(-0.004, 0.018, 60)
31+
# Consolidation
32+
returns[140:200] = np.random.normal(0.0, 0.012, 60)
33+
# Strong uptrend
34+
returns[200:260] = np.random.normal(0.005, 0.015, 60)
35+
# Correction
36+
returns[260:] = np.random.normal(-0.003, 0.016, 40)
37+
38+
prices = base_price * np.cumprod(1 + returns)
39+
40+
# P&F parameters
41+
box_size = 2.0 # Each box represents $2
42+
reversal = 3 # 3-box reversal
43+
44+
45+
# Round to box size
46+
def to_box(price):
47+
return int(np.floor(price / box_size))
48+
49+
50+
# Build P&F chart - track boxes in each column
51+
columns = [] # List of dicts: {'direction': 'X'/'O', 'boxes': set of box indices}
52+
current_col = None
53+
last_box = to_box(prices[0])
54+
55+
for price in prices:
56+
current_box = to_box(price)
57+
58+
if current_col is None:
59+
# Start first column
60+
if current_box > last_box:
61+
current_col = {"direction": "X", "boxes": set(range(last_box, current_box + 1))}
62+
columns.append(current_col)
63+
elif current_box < last_box:
64+
current_col = {"direction": "O", "boxes": set(range(current_box, last_box + 1))}
65+
columns.append(current_col)
66+
last_box = current_box
67+
continue
68+
69+
if current_col["direction"] == "X":
70+
if current_box > last_box:
71+
# Continue upward - add all boxes from last to current
72+
for b in range(last_box + 1, current_box + 1):
73+
current_col["boxes"].add(b)
74+
last_box = current_box
75+
elif current_box <= last_box - reversal:
76+
# Reversal down - start new O column
77+
new_col = {"direction": "O", "boxes": set(range(current_box, last_box))}
78+
columns.append(new_col)
79+
current_col = new_col
80+
last_box = current_box
81+
else: # direction == 'O'
82+
if current_box < last_box:
83+
# Continue downward - add all boxes from current to last
84+
for b in range(current_box, last_box):
85+
current_col["boxes"].add(b)
86+
last_box = current_box
87+
elif current_box >= last_box + reversal:
88+
# Reversal up - start new X column
89+
new_col = {"direction": "X", "boxes": set(range(last_box + 1, current_box + 1))}
90+
columns.append(new_col)
91+
current_col = new_col
92+
last_box = current_box
93+
94+
# Convert to plot points
95+
x_points = []
96+
o_points = []
97+
98+
for col_idx, col in enumerate(columns):
99+
for box in col["boxes"]:
100+
price_val = box * box_size
101+
point = {"x": col_idx, "y": price_val}
102+
if col["direction"] == "X":
103+
x_points.append(point)
104+
else:
105+
o_points.append(point)
106+
107+
# Create chart
108+
chart = Chart(container="container")
109+
chart.options = HighchartsOptions()
110+
111+
# Chart configuration
112+
chart.options.chart = {
113+
"type": "scatter",
114+
"width": 4800,
115+
"height": 2700,
116+
"backgroundColor": "#ffffff",
117+
"marginBottom": 200,
118+
"marginLeft": 200,
119+
"marginRight": 200,
120+
"marginTop": 180,
121+
}
122+
123+
# Title
124+
chart.options.title = {
125+
"text": "point-and-figure-basic · highcharts · pyplots.ai",
126+
"style": {"fontSize": "56px", "fontWeight": "bold"},
127+
}
128+
129+
chart.options.subtitle = {
130+
"text": f"Box Size: ${box_size:.0f} | {reversal}-Box Reversal | Simulated Stock Data",
131+
"style": {"fontSize": "36px", "color": "#666666"},
132+
}
133+
134+
# Get ranges
135+
max_col = len(columns) - 1 if columns else 0
136+
all_boxes = set()
137+
for col in columns:
138+
all_boxes.update(col["boxes"])
139+
min_box = min(all_boxes) if all_boxes else 40
140+
max_box = max(all_boxes) if all_boxes else 70
141+
142+
# X-axis (columns)
143+
chart.options.x_axis = {
144+
"title": {"text": "Column (Reversal Number)", "style": {"fontSize": "40px", "fontWeight": "bold"}},
145+
"labels": {"style": {"fontSize": "32px"}},
146+
"tickInterval": 1,
147+
"min": -0.5,
148+
"max": max_col + 0.5,
149+
"gridLineWidth": 1,
150+
"gridLineColor": "#e0e0e0",
151+
}
152+
153+
# Y-axis (price)
154+
chart.options.y_axis = {
155+
"title": {"text": "Price ($)", "style": {"fontSize": "40px", "fontWeight": "bold"}},
156+
"labels": {"style": {"fontSize": "32px"}, "format": "${value}"},
157+
"tickInterval": box_size * 2,
158+
"min": (min_box - 2) * box_size,
159+
"max": (max_box + 2) * box_size,
160+
"gridLineWidth": 1,
161+
"gridLineColor": "#e0e0e0",
162+
}
163+
164+
# Legend
165+
chart.options.legend = {
166+
"enabled": True,
167+
"align": "right",
168+
"verticalAlign": "top",
169+
"layout": "vertical",
170+
"x": -30,
171+
"y": 80,
172+
"itemStyle": {"fontSize": "36px", "fontWeight": "normal"},
173+
"symbolRadius": 0,
174+
"symbolHeight": 32,
175+
"symbolWidth": 32,
176+
}
177+
178+
# X series (bullish/rising) - display X character
179+
x_series = ScatterSeries()
180+
x_series.name = "X (Rising)"
181+
x_series.data = x_points
182+
x_series.color = "#306998" # Python Blue for bullish
183+
x_series.marker = {"enabled": False}
184+
x_series.data_labels = {
185+
"enabled": True,
186+
"format": "X",
187+
"style": {"fontSize": "36px", "fontWeight": "bold", "color": "#306998", "textOutline": "none"},
188+
"align": "center",
189+
"verticalAlign": "middle",
190+
"y": 0,
191+
}
192+
chart.add_series(x_series)
193+
194+
# O series (bearish/falling) - display O character
195+
o_series = ScatterSeries()
196+
o_series.name = "O (Falling)"
197+
o_series.data = o_points
198+
o_series.color = "#E74C3C" # Red for bearish
199+
o_series.marker = {"enabled": False}
200+
o_series.data_labels = {
201+
"enabled": True,
202+
"format": "O",
203+
"style": {"fontSize": "36px", "fontWeight": "bold", "color": "#E74C3C", "textOutline": "none"},
204+
"align": "center",
205+
"verticalAlign": "middle",
206+
"y": 0,
207+
}
208+
chart.add_series(o_series)
209+
210+
# Tooltip
211+
chart.options.tooltip = {
212+
"headerFormat": "<b>Column {point.x}</b><br/>",
213+
"pointFormat": "Price: ${point.y}",
214+
"style": {"fontSize": "28px"},
215+
}
216+
217+
# Download Highcharts JS
218+
highcharts_url = "https://code.highcharts.com/highcharts.js"
219+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
220+
highcharts_js = response.read().decode("utf-8")
221+
222+
# Generate HTML
223+
html_str = chart.to_js_literal()
224+
html_content = f"""<!DOCTYPE html>
225+
<html>
226+
<head>
227+
<meta charset="utf-8">
228+
<script>{highcharts_js}</script>
229+
</head>
230+
<body style="margin:0;">
231+
<div id="container" style="width: 4800px; height: 2700px;"></div>
232+
<script>{html_str}</script>
233+
</body>
234+
</html>"""
235+
236+
# Write HTML file
237+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
238+
f.write(html_content)
239+
temp_path = f.name
240+
241+
# Also save as plot.html for interactive version
242+
with open("plot.html", "w", encoding="utf-8") as f:
243+
# Use CDN for the standalone HTML file
244+
standalone_html = f"""<!DOCTYPE html>
245+
<html>
246+
<head>
247+
<meta charset="utf-8">
248+
<script src="https://code.highcharts.com/highcharts.js"></script>
249+
</head>
250+
<body style="margin:0;">
251+
<div id="container" style="width: 100%; height: 100vh;"></div>
252+
<script>{html_str}</script>
253+
</body>
254+
</html>"""
255+
f.write(standalone_html)
256+
257+
# Screenshot with Selenium
258+
chrome_options = Options()
259+
chrome_options.add_argument("--headless")
260+
chrome_options.add_argument("--no-sandbox")
261+
chrome_options.add_argument("--disable-dev-shm-usage")
262+
chrome_options.add_argument("--disable-gpu")
263+
chrome_options.add_argument("--window-size=4800,2700")
264+
265+
driver = webdriver.Chrome(options=chrome_options)
266+
driver.get(f"file://{temp_path}")
267+
time.sleep(5)
268+
driver.save_screenshot("plot.png")
269+
driver.quit()
270+
271+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)