Skip to content

Commit 30ef877

Browse files
feat(highcharts): implement streamline-basic (#2917)
## Implementation: `streamline-basic` - highcharts Implements the **highcharts** version of `streamline-basic`. **File:** `plots/streamline-basic/implementations/highcharts.py` **Parent Issue:** #2861 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20608714881)* --------- 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 be29bda commit 30ef877

2 files changed

Lines changed: 277 additions & 0 deletions

File tree

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
""" pyplots.ai
2+
streamline-basic: Basic Streamline Plot
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 90/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+
19+
20+
# Data: Create a vortex flow field
21+
np.random.seed(42)
22+
grid_size = 30
23+
x_grid = np.linspace(-3, 3, grid_size)
24+
y_grid = np.linspace(-3, 3, grid_size)
25+
X, Y = np.meshgrid(x_grid, y_grid)
26+
27+
# Vortex flow: u = -y, v = x (circular flow around origin)
28+
U = -Y
29+
V = X
30+
31+
# Generate streamlines from distributed starting points at varying radii
32+
streamlines = []
33+
streamline_speeds = [] # Track average speed for color encoding
34+
35+
# Use different radii for varied circular patterns
36+
# Reduce inner streamlines to prevent dense packing that makes paths hard to distinguish
37+
radii = [0.8, 1.3, 1.8, 2.3, 2.8]
38+
angles_per_radius = [3, 4, 5, 6, 7]
39+
40+
x_min, x_max = x_grid.min(), x_grid.max()
41+
y_min, y_max = y_grid.min(), y_grid.max()
42+
43+
for radius, n_angles in zip(radii, angles_per_radius, strict=False):
44+
for angle in np.linspace(0, 2 * np.pi, n_angles, endpoint=False):
45+
# Starting point at given radius
46+
x0 = radius * np.cos(angle)
47+
y0 = radius * np.sin(angle)
48+
49+
# Trace streamline using inline Euler integration
50+
points = [(x0, y0)]
51+
speeds = []
52+
x_curr, y_curr = x0, y0
53+
max_steps = 150
54+
dt = 0.08
55+
56+
for _ in range(max_steps):
57+
# Find grid indices
58+
xi = int((x_curr - x_min) / (x_max - x_min) * (grid_size - 1))
59+
yi = int((y_curr - y_min) / (y_max - y_min) * (grid_size - 1))
60+
61+
# Check bounds
62+
if xi < 0 or xi >= grid_size - 1 or yi < 0 or yi >= grid_size - 1:
63+
break
64+
65+
# Get velocity
66+
u = U[yi, xi]
67+
v = V[yi, xi]
68+
69+
# Calculate speed for color encoding
70+
speed = np.sqrt(u**2 + v**2)
71+
if speed < 1e-6:
72+
break
73+
speeds.append(speed)
74+
75+
# Normalize velocity for consistent step size
76+
u_norm = u / speed
77+
v_norm = v / speed
78+
79+
# Step forward
80+
x_new = x_curr + u_norm * dt
81+
y_new = y_curr + v_norm * dt
82+
83+
# Check if out of bounds
84+
if x_new < x_min or x_new > x_max or y_new < y_min or y_new > y_max:
85+
break
86+
87+
points.append((x_new, y_new))
88+
x_curr, y_curr = x_new, y_new
89+
90+
# Only keep meaningful streamlines
91+
if len(points) > 5:
92+
streamlines.append(points)
93+
streamline_speeds.append(np.mean(speeds) if speeds else 0)
94+
95+
# Create Highcharts chart
96+
chart = Chart(container="container")
97+
chart.options = HighchartsOptions()
98+
99+
# Chart configuration
100+
chart.options.chart = {
101+
"type": "line",
102+
"width": 4800,
103+
"height": 2700,
104+
"backgroundColor": "#ffffff",
105+
"marginBottom": 250,
106+
"marginLeft": 220,
107+
"marginRight": 450,
108+
"marginTop": 180,
109+
"spacingBottom": 50,
110+
}
111+
112+
# Title
113+
chart.options.title = {
114+
"text": "streamline-basic · highcharts · pyplots.ai",
115+
"style": {"fontSize": "56px", "fontWeight": "bold"},
116+
}
117+
118+
# Subtitle describing the vortex
119+
chart.options.subtitle = {
120+
"text": "Vortex Flow Field: u = -y, v = x (color indicates velocity magnitude)",
121+
"style": {"fontSize": "40px", "color": "#666666"},
122+
}
123+
124+
# Axes with extended range to prevent clipping
125+
chart.options.x_axis = {
126+
"title": {"text": "X Position (arbitrary units)", "style": {"fontSize": "42px"}},
127+
"labels": {"style": {"fontSize": "32px"}},
128+
"min": -4.0,
129+
"max": 4.0,
130+
"tickInterval": 1,
131+
"gridLineWidth": 1,
132+
"gridLineColor": "#e0e0e0",
133+
"lineWidth": 2,
134+
"lineColor": "#333333",
135+
}
136+
137+
chart.options.y_axis = {
138+
"title": {"text": "Y Position (arbitrary units)", "style": {"fontSize": "42px"}},
139+
"labels": {"style": {"fontSize": "32px"}},
140+
"min": -4.0,
141+
"max": 4.0,
142+
"tickInterval": 1,
143+
"gridLineWidth": 1,
144+
"gridLineColor": "#e0e0e0",
145+
"lineWidth": 2,
146+
"lineColor": "#333333",
147+
}
148+
149+
# Color palette based on velocity magnitude (viridis-like colorblind-safe)
150+
speed_min = min(streamline_speeds) if streamline_speeds else 0
151+
speed_max = max(streamline_speeds) if streamline_speeds else 1
152+
speed_range = speed_max - speed_min if speed_max > speed_min else 1
153+
154+
# Viridis-inspired colors: dark purple -> blue -> teal -> green
155+
viridis_colors = ["#440154", "#3B528B", "#21918C", "#5DC863"]
156+
157+
# Plot options
158+
chart.options.plot_options = {"line": {"lineWidth": 5, "marker": {"enabled": False}, "enableMouseTracking": False}}
159+
160+
# Legend - disabled for streamlines, we'll use a custom HTML color scale
161+
chart.options.legend = {"enabled": False}
162+
163+
# Build color scale legend HTML for the right side
164+
color_scale_html = f"""
165+
<div style="position:absolute; right:50px; top:300px; font-family:Arial,sans-serif;">
166+
<div style="font-size:32px; font-weight:bold; margin-bottom:20px; color:#333;">Velocity Magnitude</div>
167+
<div style="display:flex; align-items:center; margin-bottom:15px;">
168+
<div style="width:40px; height:30px; background:{viridis_colors[3]}; margin-right:15px;"></div>
169+
<span style="font-size:28px;">High ({speed_max:.1f})</span>
170+
</div>
171+
<div style="display:flex; align-items:center; margin-bottom:15px;">
172+
<div style="width:40px; height:30px; background:{viridis_colors[2]}; margin-right:15px;"></div>
173+
<span style="font-size:28px;">Med-High</span>
174+
</div>
175+
<div style="display:flex; align-items:center; margin-bottom:15px;">
176+
<div style="width:40px; height:30px; background:{viridis_colors[1]}; margin-right:15px;"></div>
177+
<span style="font-size:28px;">Med-Low</span>
178+
</div>
179+
<div style="display:flex; align-items:center; margin-bottom:15px;">
180+
<div style="width:40px; height:30px; background:{viridis_colors[0]}; margin-right:15px;"></div>
181+
<span style="font-size:28px;">Low ({speed_min:.1f})</span>
182+
</div>
183+
</div>
184+
"""
185+
186+
# Add streamlines as series with velocity-based colors
187+
for i, (streamline, avg_speed) in enumerate(zip(streamlines, streamline_speeds, strict=False)):
188+
series = LineSeries()
189+
series.data = [[round(pt[0], 4), round(pt[1], 4)] for pt in streamline]
190+
series.name = f"Streamline {i + 1} (v={avg_speed:.2f})"
191+
192+
# Inline color selection based on normalized speed
193+
t = (avg_speed - speed_min) / speed_range
194+
if t < 0.25:
195+
series.color = viridis_colors[0] # Dark purple (low speed)
196+
elif t < 0.5:
197+
series.color = viridis_colors[1] # Blue
198+
elif t < 0.75:
199+
series.color = viridis_colors[2] # Teal
200+
else:
201+
series.color = viridis_colors[3] # Green (high speed)
202+
203+
series.line_width = 5
204+
chart.add_series(series)
205+
206+
# Download Highcharts JS
207+
highcharts_url = "https://code.highcharts.com/highcharts.js"
208+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
209+
highcharts_js = response.read().decode("utf-8")
210+
211+
# Generate HTML with inline scripts and color scale legend
212+
html_str = chart.to_js_literal()
213+
html_content = f"""<!DOCTYPE html>
214+
<html>
215+
<head>
216+
<meta charset="utf-8">
217+
<script>{highcharts_js}</script>
218+
</head>
219+
<body style="margin:0; position:relative;">
220+
<div id="container" style="width: 4800px; height: 2700px;"></div>
221+
{color_scale_html}
222+
<script>{html_str}</script>
223+
</body>
224+
</html>"""
225+
226+
# Write temp HTML
227+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
228+
f.write(html_content)
229+
temp_path = f.name
230+
231+
# Also save as plot.html for interactive version
232+
with open("plot.html", "w", encoding="utf-8") as f:
233+
f.write(html_content)
234+
235+
# Take screenshot with Selenium
236+
chrome_options = Options()
237+
chrome_options.add_argument("--headless")
238+
chrome_options.add_argument("--no-sandbox")
239+
chrome_options.add_argument("--disable-dev-shm-usage")
240+
chrome_options.add_argument("--disable-gpu")
241+
chrome_options.add_argument("--window-size=4800,2700")
242+
243+
driver = webdriver.Chrome(options=chrome_options)
244+
driver.get(f"file://{temp_path}")
245+
time.sleep(5) # Wait for chart to render
246+
driver.save_screenshot("plot.png")
247+
driver.quit()
248+
249+
# Clean up temp file
250+
Path(temp_path).unlink()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: highcharts
2+
specification_id: streamline-basic
3+
created: '2025-12-31T00:17:48Z'
4+
updated: '2025-12-31T00:37:00Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20608714881
7+
issue: 2861
8+
python_version: 3.13.11
9+
library_version: unknown
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/streamline-basic/highcharts/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/streamline-basic/highcharts/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/streamline-basic/highcharts/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Excellent viridis-inspired colorblind-safe color palette effectively encoding
17+
velocity magnitude
18+
- Clean implementation of Euler integration for streamline tracing
19+
- Well-formatted title following the pyplots.ai convention
20+
- Good axis labeling with units and clear subtitle explaining the vector field equations
21+
- Properly distributed starting points across multiple radii creating visually balanced
22+
concentric streamlines
23+
weaknesses:
24+
- HTML overlay legend is not integrated into Highcharts native charting; using colorAxis
25+
would be more elegant
26+
- Grid/Legend scoring affected by custom HTML approach rather than native Highcharts
27+
legend

0 commit comments

Comments
 (0)