Skip to content

Commit 221a02b

Browse files
feat(highcharts): implement andrews-curves (#2906)
## Implementation: `andrews-curves` - highcharts Implements the **highcharts** version of `andrews-curves`. **File:** `plots/andrews-curves/implementations/highcharts.py` **Parent Issue:** #2859 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20608527516)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 3228979 commit 221a02b

2 files changed

Lines changed: 206 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
""" pyplots.ai
2+
andrews-curves: Andrews Curves for Multivariate Data
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-31
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 selenium import webdriver
17+
from selenium.webdriver.chrome.options import Options
18+
from sklearn.datasets import load_iris
19+
from sklearn.preprocessing import StandardScaler
20+
21+
22+
# Data - Iris dataset normalized
23+
iris = load_iris()
24+
X = StandardScaler().fit_transform(iris.data)
25+
y = iris.target
26+
species_names = ["Setosa", "Versicolor", "Virginica"]
27+
28+
# Andrews curve transformation: f(t) = x1/sqrt(2) + x2*sin(t) + x3*cos(t) + x4*sin(2t) + ...
29+
t = np.linspace(-np.pi, np.pi, 200)
30+
31+
32+
def andrews_curve(x, t):
33+
"""Transform multivariate observation to Andrews curve"""
34+
n = len(x)
35+
result = x[0] / np.sqrt(2)
36+
for i in range(1, n):
37+
if i % 2 == 1:
38+
result += x[i] * np.sin((i // 2 + 1) * t)
39+
else:
40+
result += x[i] * np.cos((i // 2) * t)
41+
return result
42+
43+
44+
# Create chart
45+
chart = Chart(container="container")
46+
chart.options = HighchartsOptions()
47+
48+
# Chart settings
49+
chart.options.chart = {
50+
"type": "line",
51+
"width": 4800,
52+
"height": 2700,
53+
"backgroundColor": "#ffffff",
54+
"marginBottom": 180,
55+
"marginTop": 120,
56+
"marginLeft": 150,
57+
"marginRight": 100,
58+
}
59+
60+
# Title
61+
chart.options.title = {
62+
"text": "Iris Species · andrews-curves · highcharts · pyplots.ai",
63+
"style": {"fontSize": "56px", "fontWeight": "bold"},
64+
"y": 60,
65+
}
66+
67+
# Axes
68+
chart.options.x_axis = {
69+
"title": {"text": "t (radians)", "style": {"fontSize": "40px"}, "margin": 30},
70+
"labels": {"style": {"fontSize": "32px"}, "y": 40},
71+
"gridLineWidth": 1,
72+
"gridLineColor": "rgba(0,0,0,0.15)",
73+
"tickInterval": 1,
74+
"min": -3.15,
75+
"max": 3.15,
76+
}
77+
78+
chart.options.y_axis = {
79+
"title": {"text": "f(t)", "style": {"fontSize": "40px"}, "margin": 30},
80+
"labels": {"style": {"fontSize": "32px"}, "x": -15},
81+
"gridLineWidth": 1,
82+
"gridLineColor": "rgba(0,0,0,0.15)",
83+
"tickInterval": 1,
84+
}
85+
86+
# Colors for species (colorblind-safe)
87+
colors = ["#306998", "#FFD43B", "#9467BD"]
88+
89+
# Legend styling - positioned inside chart area to avoid clipping
90+
chart.options.legend = {
91+
"enabled": True,
92+
"itemStyle": {"fontSize": "36px", "fontWeight": "bold"},
93+
"symbolWidth": 60,
94+
"symbolHeight": 4,
95+
"align": "right",
96+
"verticalAlign": "top",
97+
"layout": "vertical",
98+
"x": -50,
99+
"y": 80,
100+
"itemDistance": 20,
101+
"itemMarginTop": 10,
102+
"backgroundColor": "rgba(255,255,255,0.9)",
103+
"borderWidth": 1,
104+
"borderColor": "#cccccc",
105+
"padding": 15,
106+
}
107+
108+
# Plot options for transparency
109+
chart.options.plot_options = {
110+
"line": {"lineWidth": 3, "marker": {"enabled": False}, "animation": False},
111+
"series": {"animation": False},
112+
}
113+
114+
# Generate curves for each observation (sample 20 per species for clarity)
115+
np.random.seed(42)
116+
samples_per_species = 20
117+
118+
for species_idx in range(3):
119+
species_mask = y == species_idx
120+
species_X = X[species_mask]
121+
sample_indices = np.random.choice(len(species_X), min(samples_per_species, len(species_X)), replace=False)
122+
123+
for i, idx in enumerate(sample_indices):
124+
curve_values = andrews_curve(species_X[idx], t)
125+
data_points = [[float(t[j]), float(curve_values[j])] for j in range(len(t))]
126+
127+
series = LineSeries()
128+
series.data = data_points
129+
series.name = species_names[species_idx]
130+
series.color = colors[species_idx]
131+
series.opacity = 0.5
132+
series.show_in_legend = i == 0 # Only show first curve in legend
133+
series.line_width = 2
134+
135+
chart.add_series(series)
136+
137+
# Download Highcharts JS
138+
highcharts_url = "https://code.highcharts.com/highcharts.js"
139+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
140+
highcharts_js = response.read().decode("utf-8")
141+
142+
# Generate HTML with inline script
143+
html_str = chart.to_js_literal()
144+
html_content = f"""<!DOCTYPE html>
145+
<html>
146+
<head>
147+
<meta charset="utf-8">
148+
<script>{highcharts_js}</script>
149+
</head>
150+
<body style="margin:0;">
151+
<div id="container" style="width: 4800px; height: 2700px;"></div>
152+
<script>{html_str}</script>
153+
</body>
154+
</html>"""
155+
156+
# Write temp HTML and capture screenshot
157+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
158+
f.write(html_content)
159+
temp_path = f.name
160+
161+
chrome_options = Options()
162+
chrome_options.add_argument("--headless")
163+
chrome_options.add_argument("--no-sandbox")
164+
chrome_options.add_argument("--disable-dev-shm-usage")
165+
chrome_options.add_argument("--disable-gpu")
166+
chrome_options.add_argument("--window-size=4900,2800")
167+
168+
driver = webdriver.Chrome(options=chrome_options)
169+
driver.get(f"file://{temp_path}")
170+
time.sleep(5)
171+
172+
# Screenshot the chart element for exact dimensions
173+
container = driver.find_element("id", "container")
174+
container.screenshot("plot.png")
175+
driver.quit()
176+
177+
Path(temp_path).unlink()
178+
179+
# Also save HTML for interactive version
180+
with open("plot.html", "w", encoding="utf-8") as f:
181+
f.write(html_content)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: highcharts
2+
specification_id: andrews-curves
3+
created: '2025-12-31T00:08:21Z'
4+
updated: '2025-12-31T00:13:07Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20608527516
7+
issue: 2859
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/andrews-curves/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/andrews-curves/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/andrews-curves/highcharts/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent colorblind-safe palette choice (blue/yellow/purple)
17+
- Clear cluster separation visible - Setosa distinctly separated from Versicolor/Virginica
18+
- Proper use of transparency (0.5 opacity) to show overlapping curve density
19+
- Well-implemented Andrews curve transformation following Fourier series formula
20+
- Good canvas utilization with appropriate margins
21+
- Interactive HTML version also generated
22+
- Normalized data using StandardScaler as recommended in spec
23+
weaknesses:
24+
- Helper function andrews_curve() deviates from pure KISS structure (minor)
25+
- Y-axis label f(t) could be more descriptive for general audience

0 commit comments

Comments
 (0)