Skip to content

Commit 59b274e

Browse files
feat(highcharts): implement scatter-regression-lowess (#2886)
## Implementation: `scatter-regression-lowess` - highcharts Implements the **highcharts** version of `scatter-regression-lowess`. **File:** `plots/scatter-regression-lowess/implementations/highcharts.py` **Parent Issue:** #2855 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20608465113)* --------- 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 7624656 commit 59b274e

2 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
""" pyplots.ai
2+
scatter-regression-lowess: Scatter Plot with LOWESS Regression
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.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+
def lowess(x, y, frac=0.3):
22+
"""Simple LOWESS implementation using tricube weighting."""
23+
n = len(x)
24+
k = int(np.ceil(frac * n)) # Number of neighbors to use
25+
y_smooth = np.zeros(n)
26+
sorted_idx = np.argsort(x)
27+
x_sorted = x[sorted_idx]
28+
y_sorted = y[sorted_idx]
29+
30+
for i in range(n):
31+
# Calculate distances to all points
32+
distances = np.abs(x_sorted - x_sorted[i])
33+
# Find k nearest neighbors
34+
nearest_idx = np.argsort(distances)[:k]
35+
# Maximum distance among neighbors
36+
max_dist = distances[nearest_idx[-1]]
37+
if max_dist == 0:
38+
max_dist = 1.0
39+
# Tricube weights
40+
u = distances[nearest_idx] / max_dist
41+
weights = (1 - u**3) ** 3
42+
# Weighted linear regression
43+
x_local = x_sorted[nearest_idx]
44+
y_local = y_sorted[nearest_idx]
45+
# Weighted least squares
46+
w_sum = np.sum(weights)
47+
wx_sum = np.sum(weights * x_local)
48+
wy_sum = np.sum(weights * y_local)
49+
wxx_sum = np.sum(weights * x_local * x_local)
50+
wxy_sum = np.sum(weights * x_local * y_local)
51+
# Solve for slope and intercept
52+
denom = w_sum * wxx_sum - wx_sum**2
53+
if abs(denom) < 1e-10:
54+
y_smooth[i] = wy_sum / w_sum if w_sum > 0 else y_sorted[i]
55+
else:
56+
slope = (w_sum * wxy_sum - wx_sum * wy_sum) / denom
57+
intercept = (wy_sum - slope * wx_sum) / w_sum
58+
y_smooth[i] = slope * x_sorted[i] + intercept
59+
60+
# Return in original order
61+
result = np.zeros(n)
62+
result[sorted_idx] = y_smooth
63+
return x, result
64+
65+
66+
# Data - Generate complex non-linear relationship
67+
np.random.seed(42)
68+
n_points = 200
69+
x = np.linspace(0, 10, n_points)
70+
# Create a complex non-linear pattern: sine wave + quadratic + noise
71+
y = 3 * np.sin(x * 1.2) + 0.3 * x**2 - 0.5 * x + np.random.normal(0, 1.5, n_points)
72+
73+
# Compute LOWESS regression curve
74+
x_lowess, y_lowess = lowess(x, y, frac=0.3)
75+
76+
# Create chart with container
77+
chart = Chart(container="container")
78+
chart.options = HighchartsOptions()
79+
80+
# Chart configuration
81+
chart.options.chart = {
82+
"type": "scatter",
83+
"width": 4800,
84+
"height": 2700,
85+
"backgroundColor": "#ffffff",
86+
"marginBottom": 250,
87+
"marginLeft": 150,
88+
"spacingTop": 60,
89+
"spacingRight": 100,
90+
}
91+
92+
# Title
93+
chart.options.title = {
94+
"text": "scatter-regression-lowess · highcharts · pyplots.ai",
95+
"style": {"fontSize": "48px", "fontWeight": "bold"},
96+
}
97+
98+
# Subtitle with context
99+
chart.options.subtitle = {"text": "Non-linear Trend with LOWESS Smoothing (frac=0.3)", "style": {"fontSize": "32px"}}
100+
101+
# X-axis configuration
102+
chart.options.x_axis = {
103+
"title": {"text": "X Value", "style": {"fontSize": "36px"}},
104+
"labels": {"style": {"fontSize": "28px"}},
105+
"gridLineWidth": 1,
106+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
107+
}
108+
109+
# Y-axis configuration
110+
chart.options.y_axis = {
111+
"title": {"text": "Y Value", "style": {"fontSize": "36px"}},
112+
"labels": {"style": {"fontSize": "28px"}},
113+
"gridLineWidth": 1,
114+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
115+
}
116+
117+
# Legend
118+
chart.options.legend = {
119+
"enabled": True,
120+
"itemStyle": {"fontSize": "28px"},
121+
"align": "right",
122+
"verticalAlign": "top",
123+
"layout": "vertical",
124+
"x": -50,
125+
"y": 100,
126+
}
127+
128+
# Plot options
129+
chart.options.plot_options = {
130+
"scatter": {
131+
"marker": {"radius": 10, "fillColor": "rgba(48, 105, 152, 0.6)", "lineWidth": 1, "lineColor": "#306998"}
132+
},
133+
"spline": {"marker": {"enabled": False}, "lineWidth": 5},
134+
}
135+
136+
# Add scatter series (data points)
137+
scatter_series = ScatterSeries()
138+
scatter_series.name = "Data Points"
139+
scatter_series.data = [[float(xi), float(yi)] for xi, yi in zip(x, y, strict=True)]
140+
scatter_series.color = "#306998"
141+
chart.add_series(scatter_series)
142+
143+
# Add LOWESS curve as spline series
144+
lowess_series = SplineSeries()
145+
lowess_series.name = "LOWESS Curve"
146+
lowess_series.data = [[float(xi), float(yi)] for xi, yi in zip(x_lowess, y_lowess, strict=True)]
147+
lowess_series.color = "#FFD43B"
148+
lowess_series.marker = {"enabled": False}
149+
lowess_series.line_width = 6
150+
chart.add_series(lowess_series)
151+
152+
# Download Highcharts JS for inline embedding
153+
highcharts_url = "https://code.highcharts.com/highcharts.js"
154+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
155+
highcharts_js = response.read().decode("utf-8")
156+
157+
# Generate HTML with inline scripts
158+
html_str = chart.to_js_literal()
159+
html_content = f"""<!DOCTYPE html>
160+
<html>
161+
<head>
162+
<meta charset="utf-8">
163+
<script>{highcharts_js}</script>
164+
</head>
165+
<body style="margin:0;">
166+
<div id="container" style="width: 4800px; height: 2700px;"></div>
167+
<script>{html_str}</script>
168+
</body>
169+
</html>"""
170+
171+
# Write temp HTML and take screenshot
172+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
173+
f.write(html_content)
174+
temp_path = f.name
175+
176+
# Also save HTML for interactive version
177+
with open("plot.html", "w", encoding="utf-8") as f:
178+
f.write(html_content)
179+
180+
# Configure Chrome for headless screenshot
181+
chrome_options = Options()
182+
chrome_options.add_argument("--headless")
183+
chrome_options.add_argument("--no-sandbox")
184+
chrome_options.add_argument("--disable-dev-shm-usage")
185+
chrome_options.add_argument("--disable-gpu")
186+
chrome_options.add_argument("--window-size=4800,2700")
187+
188+
driver = webdriver.Chrome(options=chrome_options)
189+
driver.get(f"file://{temp_path}")
190+
time.sleep(5) # Wait for chart to render
191+
driver.save_screenshot("plot.png")
192+
driver.quit()
193+
194+
# Clean up temp file
195+
Path(temp_path).unlink()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: highcharts
2+
specification_id: scatter-regression-lowess
3+
created: '2025-12-30T23:55:52Z'
4+
updated: '2025-12-31T00:02:06Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20608465113
7+
issue: 2855
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/scatter-regression-lowess/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/scatter-regression-lowess/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/scatter-regression-lowess/highcharts/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent color contrast with colorblind-safe blue/yellow combination
17+
- LOWESS curve is smooth and clearly distinguishable from scatter points
18+
- Proper title format and informative subtitle mentioning the smoothing parameter
19+
- Good marker sizing and transparency for data density
20+
- Generates both PNG and HTML outputs for static and interactive viewing
21+
- Custom LOWESS implementation works correctly with tricube weighting
22+
weaknesses:
23+
- Axis labels are generic (X Value, Y Value) rather than context-specific
24+
- Custom function definition in code deviates from KISS principle (though necessary)
25+
- Data could be tied to a more realistic scenario with meaningful labels

0 commit comments

Comments
 (0)