Skip to content

Commit f0de48c

Browse files
feat(highcharts): implement scatter-lag (#5276)
## Implementation: `scatter-lag` - highcharts Implements the **highcharts** version of `scatter-lag`. **File:** `plots/scatter-lag/implementations/highcharts.py` **Parent Issue:** #5251 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/24313009902)* --------- 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 95301d1 commit f0de48c

File tree

2 files changed

+476
-0
lines changed

2 files changed

+476
-0
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
""" pyplots.ai
2+
scatter-lag: Lag Plot for Time Series Autocorrelation Diagnosis
3+
Library: highcharts unknown | Python 3.14.3
4+
Quality: 88/100 | Created: 2026-04-12
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 highcharts_core.options.series.spline import SplineSeries
17+
from selenium import webdriver
18+
from selenium.webdriver.chrome.options import Options
19+
20+
21+
# Data — synthetic AR(1) process with strong positive autocorrelation
22+
np.random.seed(42)
23+
n = 300
24+
phi = 0.85
25+
noise = np.random.normal(0, 1, n)
26+
temperature = np.zeros(n)
27+
temperature[0] = 20.0
28+
for i in range(1, n):
29+
temperature[i] = 20 + phi * (temperature[i - 1] - 20) + noise[i]
30+
31+
# Lag plot data: y(t) vs y(t+1)
32+
lag = 1
33+
yt = temperature[:-lag]
34+
yt_lag = temperature[lag:]
35+
time_index = np.arange(len(yt))
36+
37+
# Correlation coefficient
38+
r_value = np.corrcoef(yt, yt_lag)[0, 1]
39+
40+
# Axis bounds
41+
pad = 0.5
42+
all_vals = np.concatenate([yt, yt_lag])
43+
axis_min = float(np.floor(all_vals.min() - pad))
44+
axis_max = float(np.ceil(all_vals.max() + pad))
45+
46+
# Color points by time index — early=light, late=dark using Python Blue palette
47+
n_points = len(yt)
48+
colors = []
49+
for i in range(n_points):
50+
t = i / (n_points - 1)
51+
r = int(48 + (180 - 48) * (1 - t))
52+
g = int(105 + (210 - 105) * (1 - t))
53+
b = int(152 + (230 - 152) * (1 - t))
54+
alpha = 0.4 + 0.45 * t
55+
colors.append(f"rgba({r}, {g}, {b}, {alpha:.2f})")
56+
57+
# Build scatter data with per-point color
58+
scatter_data = []
59+
for i in range(n_points):
60+
scatter_data.append({"x": float(yt[i]), "y": float(yt_lag[i]), "color": colors[i]})
61+
62+
# Diagonal reference line (y = x)
63+
diag_data = [[axis_min, axis_min], [axis_max, axis_max]]
64+
65+
# Chart
66+
chart = Chart(container="container")
67+
chart.options = HighchartsOptions()
68+
69+
chart.options.chart = {
70+
"type": "scatter",
71+
"width": 4800,
72+
"height": 2700,
73+
"backgroundColor": "#fafbfc",
74+
"style": {"fontFamily": "'Segoe UI', Helvetica, Arial, sans-serif"},
75+
"marginTop": 160,
76+
"marginBottom": 300,
77+
"marginLeft": 260,
78+
"marginRight": 200,
79+
}
80+
81+
chart.options.title = {
82+
"text": "scatter-lag \u00b7 highcharts \u00b7 pyplots.ai",
83+
"style": {"fontSize": "64px", "fontWeight": "600", "color": "#2c3e50", "letterSpacing": "1px"},
84+
"margin": 50,
85+
}
86+
87+
chart.options.subtitle = {
88+
"text": f"AR(1) Temperature Series \u2014 Lag {lag} Autocorrelation (r = {r_value:.3f})",
89+
"style": {"fontSize": "38px", "color": "#7f8c8d", "fontWeight": "400"},
90+
}
91+
92+
chart.options.x_axis = {
93+
"title": {"text": "y(t)", "style": {"fontSize": "44px", "color": "#34495e", "fontWeight": "500"}, "margin": 30},
94+
"labels": {"style": {"fontSize": "34px", "color": "#7f8c8d"}},
95+
"min": axis_min,
96+
"max": axis_max,
97+
"tickInterval": 2,
98+
"startOnTick": False,
99+
"endOnTick": False,
100+
"gridLineWidth": 1,
101+
"gridLineColor": "rgba(0, 0, 0, 0.06)",
102+
"gridLineDashStyle": "Dot",
103+
"lineColor": "#bdc3c7",
104+
"lineWidth": 2,
105+
"tickColor": "#bdc3c7",
106+
"tickLength": 10,
107+
}
108+
109+
chart.options.y_axis = {
110+
"title": {
111+
"text": f"y(t+{lag})",
112+
"style": {"fontSize": "44px", "color": "#34495e", "fontWeight": "500"},
113+
"margin": 30,
114+
},
115+
"labels": {"style": {"fontSize": "34px", "color": "#7f8c8d"}},
116+
"min": axis_min,
117+
"max": axis_max,
118+
"tickInterval": 2,
119+
"startOnTick": False,
120+
"endOnTick": False,
121+
"gridLineWidth": 1,
122+
"gridLineColor": "rgba(0, 0, 0, 0.06)",
123+
"gridLineDashStyle": "Dot",
124+
"lineColor": "#bdc3c7",
125+
"lineWidth": 2,
126+
"tickColor": "#bdc3c7",
127+
"tickLength": 10,
128+
}
129+
130+
chart.options.legend = {
131+
"enabled": True,
132+
"align": "right",
133+
"verticalAlign": "top",
134+
"layout": "vertical",
135+
"x": -40,
136+
"y": 80,
137+
"floating": True,
138+
"backgroundColor": "rgba(255, 255, 255, 0.85)",
139+
"borderWidth": 1,
140+
"borderColor": "#e0e0e0",
141+
"borderRadius": 8,
142+
"itemStyle": {"fontSize": "30px", "fontWeight": "400", "color": "#34495e"},
143+
"padding": 16,
144+
"symbolRadius": 6,
145+
}
146+
147+
chart.options.credits = {"enabled": False}
148+
149+
chart.options.tooltip = {
150+
"headerFormat": "",
151+
"pointFormat": (
152+
'<span style="font-size:24px;color:{point.color}">\u25cf</span> '
153+
'<span style="font-size:26px">'
154+
"y(t): <b>{point.x:.2f}</b><br/>"
155+
f"y(t+{lag}): <b>{{point.y:.2f}}</b></span>"
156+
),
157+
"backgroundColor": "rgba(255, 255, 255, 0.95)",
158+
"borderColor": "#306998",
159+
"borderRadius": 10,
160+
"borderWidth": 2,
161+
"shadow": {"color": "rgba(0,0,0,0.1)", "offsetX": 2, "offsetY": 2, "width": 4},
162+
"style": {"fontSize": "26px"},
163+
}
164+
165+
# Scatter series — colored by time index
166+
scatter = ScatterSeries()
167+
scatter.data = scatter_data
168+
scatter.name = f"Lag {lag} pairs"
169+
scatter.marker = {
170+
"radius": 10,
171+
"symbol": "circle",
172+
"lineWidth": 1.5,
173+
"lineColor": "#ffffff",
174+
"states": {"hover": {"radiusPlus": 4, "lineWidthPlus": 1, "lineColor": "#306998"}},
175+
}
176+
scatter.z_index = 2
177+
178+
# Diagonal reference line (y = x)
179+
diag_line = SplineSeries()
180+
diag_line.data = diag_data
181+
diag_line.name = "y = x reference"
182+
diag_line.color = "rgba(149, 165, 166, 0.6)"
183+
diag_line.line_width = 3
184+
diag_line.dash_style = "LongDash"
185+
diag_line.marker = {"enabled": False}
186+
diag_line.enable_mouse_tracking = False
187+
diag_line.z_index = 1
188+
189+
chart.add_series(scatter)
190+
chart.add_series(diag_line)
191+
192+
# Export
193+
highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js"
194+
req = urllib.request.Request(highcharts_url, headers={"User-Agent": "Mozilla/5.0"})
195+
with urllib.request.urlopen(req, timeout=30) as response:
196+
highcharts_js = response.read().decode("utf-8")
197+
198+
html_str = chart.to_js_literal()
199+
html_content = f"""<!DOCTYPE html>
200+
<html>
201+
<head>
202+
<meta charset="utf-8">
203+
<script>{highcharts_js}</script>
204+
</head>
205+
<body style="margin:0; background:#fafbfc;">
206+
<div id="container" style="width: 4800px; height: 2700px;"></div>
207+
<script>{html_str}</script>
208+
</body>
209+
</html>"""
210+
211+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
212+
f.write(html_content)
213+
temp_path = f.name
214+
215+
chrome_options = Options()
216+
chrome_options.add_argument("--headless")
217+
chrome_options.add_argument("--no-sandbox")
218+
chrome_options.add_argument("--disable-dev-shm-usage")
219+
chrome_options.add_argument("--disable-gpu")
220+
chrome_options.add_argument("--window-size=4800,2700")
221+
222+
driver = webdriver.Chrome(options=chrome_options)
223+
driver.get(f"file://{temp_path}")
224+
time.sleep(5)
225+
226+
container = driver.find_element("id", "container")
227+
container.screenshot("plot.png")
228+
driver.quit()
229+
230+
Path(temp_path).unlink()
231+
232+
# Save HTML for interactive version
233+
with open("plot.html", "w", encoding="utf-8") as f:
234+
interactive_html = f"""<!DOCTYPE html>
235+
<html>
236+
<head>
237+
<meta charset="utf-8">
238+
<script src="https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js"></script>
239+
</head>
240+
<body style="margin:0; background:#fafbfc;">
241+
<div id="container" style="width: 100%; height: 100vh;"></div>
242+
<script>{html_str}</script>
243+
</body>
244+
</html>"""
245+
f.write(interactive_html)

0 commit comments

Comments
 (0)