Skip to content

Commit 0cd1f88

Browse files
feat(highcharts): implement line-parametric (#5077)
## Implementation: `line-parametric` - highcharts Implements the **highcharts** version of `line-parametric`. **File:** `plots/line-parametric/implementations/highcharts.py` **Parent Issue:** #4424 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23339035358)* --------- 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 bc23fc3 commit 0cd1f88

2 files changed

Lines changed: 477 additions & 0 deletions

File tree

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
""" pyplots.ai
2+
line-parametric: Parametric Curve Plot
3+
Library: highcharts unknown | Python 3.14.3
4+
Quality: 88/100 | Created: 2026-03-20
5+
"""
6+
7+
import colorsys
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from pathlib import Path
12+
13+
import numpy as np
14+
from highcharts_core.chart import Chart
15+
from highcharts_core.options import HighchartsOptions
16+
from highcharts_core.options.series.area import LineSeries
17+
from highcharts_core.options.series.scatter import ScatterSeries
18+
from selenium import webdriver
19+
from selenium.webdriver.chrome.options import Options
20+
21+
22+
# Data - parametric curves
23+
t_lissajous = np.linspace(0, 2 * np.pi, 500)
24+
x_lissajous = np.sin(3 * t_lissajous)
25+
y_lissajous = np.sin(2 * t_lissajous)
26+
27+
t_spiral = np.linspace(0, 4 * np.pi, 600)
28+
x_spiral = t_spiral * np.cos(t_spiral) / (4 * np.pi)
29+
y_spiral = t_spiral * np.sin(t_spiral) / (4 * np.pi)
30+
31+
# Generate gradient palettes inline (blue-to-teal for Lissajous, purple-to-rose for spiral)
32+
n_segments = 18
33+
lissajous_colors = []
34+
for i in range(n_segments):
35+
frac = i / max(n_segments - 1, 1)
36+
h = 0.58 + (0.48 - 0.58) * frac # blue (0.58) to teal (0.48)
37+
r, g, b = colorsys.hls_to_rgb(h % 1.0, 0.35 + 0.10 * frac, 0.80)
38+
lissajous_colors.append(f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}")
39+
40+
spiral_colors = []
41+
for i in range(n_segments):
42+
frac = i / max(n_segments - 1, 1)
43+
h = 0.82 + (0.95 - 0.82) * frac # purple (0.82) to rose (0.95)
44+
r, g, b = colorsys.hls_to_rgb(h % 1.0, 0.40 + 0.08 * frac, 0.70)
45+
spiral_colors.append(f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}")
46+
47+
# Build segments inline for Lissajous
48+
lissajous_segments = []
49+
pts_per_seg = len(x_lissajous) // n_segments
50+
for i in range(n_segments):
51+
start = i * pts_per_seg
52+
end = start + pts_per_seg + 1 if i < n_segments - 1 else len(x_lissajous)
53+
data = [[float(x_lissajous[j]), float(y_lissajous[j])] for j in range(start, end)]
54+
lissajous_segments.append({"data": data, "color": lissajous_colors[i]})
55+
56+
# Build segments inline for spiral
57+
spiral_segments = []
58+
pts_per_seg_sp = len(x_spiral) // n_segments
59+
for i in range(n_segments):
60+
start = i * pts_per_seg_sp
61+
end = start + pts_per_seg_sp + 1 if i < n_segments - 1 else len(x_spiral)
62+
data = [[float(x_spiral[j]), float(y_spiral[j])] for j in range(start, end)]
63+
spiral_segments.append({"data": data, "color": spiral_colors[i]})
64+
65+
# Create chart - square canvas for equal aspect ratio
66+
chart = Chart(container="container")
67+
chart.options = HighchartsOptions()
68+
69+
chart.options.chart = {
70+
"width": 3600,
71+
"height": 3600,
72+
"backgroundColor": "#fafafa",
73+
"spacingTop": 80,
74+
"spacingBottom": 160,
75+
"spacingLeft": 80,
76+
"spacingRight": 80,
77+
"style": {"fontFamily": "'Segoe UI', Helvetica, Arial, sans-serif"},
78+
}
79+
80+
chart.options.title = {
81+
"text": "line-parametric \u00b7 highcharts \u00b7 pyplots.ai",
82+
"style": {"fontSize": "64px", "fontWeight": "bold", "color": "#222222"},
83+
}
84+
85+
chart.options.subtitle = {
86+
"text": "Lissajous figure x=sin(3t), y=sin(2t) | Archimedean spiral x=t\u00b7cos(t), y=t\u00b7sin(t)",
87+
"style": {"fontSize": "36px", "color": "#555555"},
88+
}
89+
90+
chart.options.x_axis = {
91+
"title": {
92+
"text": "x(t) \u2014 horizontal position",
93+
"style": {"fontSize": "40px", "color": "#333333"},
94+
"margin": 24,
95+
},
96+
"labels": {"style": {"fontSize": "30px", "color": "#444444"}},
97+
"gridLineWidth": 1,
98+
"gridLineColor": "rgba(0,0,0,0.06)",
99+
"gridLineDashStyle": "Dot",
100+
"lineWidth": 2,
101+
"lineColor": "#999999",
102+
"tickWidth": 0,
103+
"tickInterval": 0.5,
104+
"min": -1.3,
105+
"max": 1.3,
106+
}
107+
108+
chart.options.y_axis = {
109+
"title": {"text": "y(t) \u2014 vertical position", "style": {"fontSize": "40px", "color": "#333333"}, "margin": 24},
110+
"labels": {"style": {"fontSize": "30px", "color": "#444444"}},
111+
"gridLineWidth": 1,
112+
"gridLineColor": "rgba(0,0,0,0.06)",
113+
"gridLineDashStyle": "Dot",
114+
"lineWidth": 2,
115+
"lineColor": "#999999",
116+
"tickInterval": 0.5,
117+
"min": -1.3,
118+
"max": 1.3,
119+
}
120+
121+
chart.options.legend = {
122+
"enabled": True,
123+
"layout": "horizontal",
124+
"align": "center",
125+
"verticalAlign": "bottom",
126+
"floating": False,
127+
"itemStyle": {"fontSize": "34px", "fontWeight": "normal", "color": "#333333"},
128+
"symbolWidth": 50,
129+
"symbolHeight": 18,
130+
"itemDistance": 60,
131+
"margin": 30,
132+
"padding": 20,
133+
"backgroundColor": "rgba(255,255,255,0.85)",
134+
"borderRadius": 8,
135+
"borderWidth": 1,
136+
"borderColor": "#dddddd",
137+
}
138+
139+
chart.options.tooltip = {
140+
"headerFormat": "",
141+
"pointFormat": "<b>{series.name}</b><br/>x: {point.x:.3f}, y: {point.y:.3f}",
142+
"style": {"fontSize": "28px"},
143+
}
144+
145+
chart.options.credits = {"enabled": False}
146+
147+
chart.options.plot_options = {
148+
"line": {"lineWidth": 5, "states": {"hover": {"lineWidthPlus": 0}}, "marker": {"enabled": False}},
149+
"series": {"animation": False, "turboThreshold": 0},
150+
}
151+
152+
# Add Lissajous segments using LineSeries
153+
for i, seg in enumerate(lissajous_segments):
154+
s = LineSeries()
155+
s.data = seg["data"]
156+
s.color = seg["color"]
157+
s.name = "Lissajous: sin(3t), sin(2t)"
158+
s.line_width = 5
159+
s.marker = {"enabled": False}
160+
s.show_in_legend = i == 0
161+
s.linked_to = ":previous" if i > 0 else None
162+
s.enable_mouse_tracking = i == 0
163+
chart.add_series(s)
164+
165+
# Add spiral segments using LineSeries
166+
for i, seg in enumerate(spiral_segments):
167+
s = LineSeries()
168+
s.data = seg["data"]
169+
s.color = seg["color"]
170+
s.name = "Spiral: t\u00b7cos(t), t\u00b7sin(t)"
171+
s.line_width = 5
172+
s.marker = {"enabled": False}
173+
s.show_in_legend = i == 0
174+
s.linked_to = ":previous" if i > 0 else None
175+
s.enable_mouse_tracking = i == 0
176+
chart.add_series(s)
177+
178+
# Start/end markers - offset labels to avoid overlap near origin
179+
marker_data = [
180+
(x_lissajous[0], y_lissajous[0], "Lissajous t=0,2\u03c0", lissajous_colors[0], "circle", True, -50, "right"),
181+
(x_spiral[0], y_spiral[0], "Spiral start", spiral_colors[0], "circle", False, 45, "left"),
182+
(x_spiral[-1], y_spiral[-1], "Spiral end", spiral_colors[-1], "diamond", True, -40, "center"),
183+
]
184+
185+
for x, y, name, color, symbol, show_legend, y_offset, align in marker_data:
186+
s = ScatterSeries()
187+
s.data = [{"x": float(x), "y": float(y), "name": name}]
188+
s.name = name
189+
s.color = color
190+
s.marker = {"enabled": True, "radius": 20, "symbol": symbol, "lineWidth": 4, "lineColor": "#ffffff"}
191+
s.data_labels = {
192+
"enabled": True,
193+
"format": "{point.name}",
194+
"style": {"fontSize": "28px", "fontWeight": "bold", "color": "#333333", "textOutline": "3px #ffffff"},
195+
"y": y_offset,
196+
"align": align,
197+
}
198+
s.show_in_legend = show_legend
199+
chart.add_series(s)
200+
201+
# Download Highcharts JS for inline embedding
202+
highcharts_js = None
203+
for url in ["https://code.highcharts.com/highcharts.js", "https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js"]:
204+
try:
205+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
206+
with urllib.request.urlopen(req, timeout=30) as response:
207+
highcharts_js = response.read().decode("utf-8")
208+
break
209+
except Exception:
210+
continue
211+
if not highcharts_js:
212+
raise RuntimeError("Failed to download Highcharts JS")
213+
214+
# Generate HTML with inline scripts
215+
html_str = chart.to_js_literal()
216+
html_content = f"""<!DOCTYPE html>
217+
<html>
218+
<head>
219+
<meta charset="utf-8">
220+
<script>{highcharts_js}</script>
221+
</head>
222+
<body style="margin:0; background:#fafafa;">
223+
<div id="container" style="width: 3600px; height: 3600px;"></div>
224+
<script>{html_str}</script>
225+
</body>
226+
</html>"""
227+
228+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
229+
f.write(html_content)
230+
temp_path = f.name
231+
232+
with open("plot.html", "w", encoding="utf-8") as f:
233+
f.write(html_content)
234+
235+
chrome_options = Options()
236+
chrome_options.add_argument("--headless")
237+
chrome_options.add_argument("--no-sandbox")
238+
chrome_options.add_argument("--disable-dev-shm-usage")
239+
chrome_options.add_argument("--disable-gpu")
240+
chrome_options.add_argument("--window-size=3600,3600")
241+
242+
driver = webdriver.Chrome(options=chrome_options)
243+
driver.get(f"file://{temp_path}")
244+
time.sleep(5)
245+
driver.save_screenshot("plot.png")
246+
driver.quit()
247+
248+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)