Skip to content

Commit f8ebcf3

Browse files
feat(highcharts): implement sn-curve-basic (#3865)
## Implementation: `sn-curve-basic` - highcharts Implements the **highcharts** version of `sn-curve-basic`. **File:** `plots/sn-curve-basic/implementations/highcharts.py` **Parent Issue:** #3826 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047248220)* --------- 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 1f8f00f commit f8ebcf3

2 files changed

Lines changed: 497 additions & 0 deletions

File tree

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
""" pyplots.ai
2+
sn-curve-basic: S-N Curve (Wöhler Curve)
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
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 highcharts_core.options.series.scatter import ScatterSeries
17+
from selenium import webdriver
18+
from selenium.webdriver.chrome.options import Options
19+
20+
21+
# Data: Simulated fatigue test results for steel specimens
22+
np.random.seed(42)
23+
24+
# Generate realistic S-N curve data points with scatter
25+
stress_levels = np.array([450, 400, 350, 320, 300, 280, 260, 250, 240, 230, 220, 210])
26+
27+
# Generate multiple test points per stress level with realistic scatter
28+
cycles_data = []
29+
stress_data = []
30+
31+
for stress in stress_levels:
32+
# Basquin equation: N = (S/A)^(-1/b)
33+
A = 1200 # Material constant
34+
b = 0.12 # Fatigue strength exponent
35+
N_mean = (stress / A) ** (-1 / b)
36+
37+
# Add 2-4 test specimens per stress level with log-normal scatter
38+
n_samples = np.random.randint(2, 5)
39+
for _ in range(n_samples):
40+
scatter = np.exp(np.random.normal(0, 0.3))
41+
cycles_data.append(N_mean * scatter)
42+
stress_data.append(stress + np.random.normal(0, 5))
43+
44+
cycles = np.array(cycles_data)
45+
stress = np.array(stress_data)
46+
47+
# Fit line using Basquin equation (log-linear fit)
48+
log_cycles = np.log10(cycles)
49+
log_stress = np.log10(stress)
50+
coeffs = np.polyfit(log_cycles, log_stress, 1)
51+
fit_cycles = np.logspace(2, 8, 100)
52+
fit_stress = 10 ** (coeffs[0] * np.log10(fit_cycles) + coeffs[1])
53+
54+
# Material property reference values (typical for structural steel)
55+
ultimate_strength = 500 # MPa
56+
yield_strength = 350 # MPa
57+
endurance_limit = 200 # MPa
58+
59+
# Create chart
60+
chart = Chart(container="container")
61+
chart.options = HighchartsOptions()
62+
63+
# Chart configuration
64+
chart.options.chart = {
65+
"type": "scatter",
66+
"width": 4800,
67+
"height": 2700,
68+
"backgroundColor": "#ffffff",
69+
"style": {"fontFamily": "Arial, sans-serif"},
70+
"marginBottom": 150,
71+
"marginLeft": 150,
72+
}
73+
74+
# Title
75+
chart.options.title = {
76+
"text": "sn-curve-basic · highcharts · pyplots.ai",
77+
"style": {"fontSize": "48px", "fontWeight": "bold"},
78+
}
79+
80+
# X-axis (logarithmic for cycles)
81+
chart.options.x_axis = {
82+
"type": "logarithmic",
83+
"title": {"text": "Number of Cycles to Failure (N)", "style": {"fontSize": "36px"}},
84+
"labels": {"style": {"fontSize": "28px"}, "rotation": 0},
85+
"min": 100,
86+
"max": 100000000,
87+
"gridLineWidth": 1,
88+
"gridLineColor": "#cccccc",
89+
"gridLineDashStyle": "Dash",
90+
"tickInterval": 1, # Show only major ticks (powers of 10)
91+
}
92+
93+
# Y-axis (logarithmic for stress)
94+
chart.options.y_axis = {
95+
"type": "logarithmic",
96+
"title": {"text": "Stress Amplitude (MPa)", "style": {"fontSize": "36px"}},
97+
"labels": {"style": {"fontSize": "28px"}},
98+
"min": 150,
99+
"max": 600,
100+
"gridLineWidth": 1,
101+
"gridLineColor": "#cccccc",
102+
"gridLineDashStyle": "Dash",
103+
"plotLines": [
104+
{
105+
"value": ultimate_strength,
106+
"color": "#E53935",
107+
"width": 4,
108+
"dashStyle": "Dash",
109+
"label": {
110+
"text": f"Ultimate Strength ({ultimate_strength} MPa)",
111+
"align": "right",
112+
"style": {"fontSize": "24px", "color": "#E53935", "fontWeight": "bold"},
113+
"x": -10,
114+
},
115+
"zIndex": 3,
116+
},
117+
{
118+
"value": yield_strength,
119+
"color": "#FB8C00",
120+
"width": 4,
121+
"dashStyle": "Dash",
122+
"label": {
123+
"text": f"Yield Strength ({yield_strength} MPa)",
124+
"align": "right",
125+
"style": {"fontSize": "24px", "color": "#FB8C00", "fontWeight": "bold"},
126+
"x": -10,
127+
},
128+
"zIndex": 3,
129+
},
130+
{
131+
"value": endurance_limit,
132+
"color": "#43A047",
133+
"width": 4,
134+
"dashStyle": "Dash",
135+
"label": {
136+
"text": f"Endurance Limit ({endurance_limit} MPa)",
137+
"align": "right",
138+
"style": {"fontSize": "24px", "color": "#43A047", "fontWeight": "bold"},
139+
"x": -10,
140+
},
141+
"zIndex": 3,
142+
},
143+
],
144+
}
145+
146+
# Legend
147+
chart.options.legend = {
148+
"enabled": True,
149+
"itemStyle": {"fontSize": "28px"},
150+
"align": "right",
151+
"verticalAlign": "top",
152+
"layout": "vertical",
153+
"x": -50,
154+
"y": 80,
155+
}
156+
157+
# Plot options
158+
chart.options.plot_options = {
159+
"scatter": {
160+
"marker": {"radius": 14, "symbol": "circle", "lineWidth": 2, "lineColor": "#ffffff"},
161+
"states": {"hover": {"enabled": False}},
162+
},
163+
"line": {"lineWidth": 5, "marker": {"enabled": False}, "states": {"hover": {"enabled": False}}},
164+
}
165+
166+
# Add test data scatter series
167+
scatter_series = ScatterSeries()
168+
scatter_series.data = [[float(c), float(s)] for c, s in zip(cycles, stress, strict=True)]
169+
scatter_series.name = "Test Data"
170+
scatter_series.color = "#306998"
171+
scatter_series.marker = {"fillColor": "#306998", "fillOpacity": 0.7}
172+
chart.add_series(scatter_series)
173+
174+
# Add fitted S-N curve line series
175+
fit_series = LineSeries()
176+
fit_series.data = [[float(c), float(s)] for c, s in zip(fit_cycles, fit_stress, strict=True)]
177+
fit_series.name = "Basquin Fit"
178+
fit_series.color = "#FFD43B"
179+
chart.add_series(fit_series)
180+
181+
# Add annotation series for fatigue regions (using invisible points with data labels)
182+
chart.options.annotations = [
183+
{
184+
"labels": [
185+
{
186+
"point": {"x": 500, "y": 420, "xAxis": 0, "yAxis": 0},
187+
"text": "Low-Cycle<br>Fatigue",
188+
"style": {"fontSize": "28px", "color": "#555555", "fontStyle": "italic"},
189+
"backgroundColor": "transparent",
190+
"borderWidth": 0,
191+
"shadow": False,
192+
},
193+
{
194+
"point": {"x": 100000, "y": 280, "xAxis": 0, "yAxis": 0},
195+
"text": "High-Cycle<br>Fatigue",
196+
"style": {"fontSize": "28px", "color": "#555555", "fontStyle": "italic"},
197+
"backgroundColor": "transparent",
198+
"borderWidth": 0,
199+
"shadow": False,
200+
},
201+
{
202+
"point": {"x": 50000000, "y": 180, "xAxis": 0, "yAxis": 0},
203+
"text": "Infinite Life",
204+
"style": {"fontSize": "28px", "color": "#555555", "fontStyle": "italic"},
205+
"backgroundColor": "transparent",
206+
"borderWidth": 0,
207+
"shadow": False,
208+
},
209+
],
210+
"labelOptions": {"shape": "rect"},
211+
}
212+
]
213+
214+
# Credits
215+
chart.options.credits = {"enabled": False}
216+
217+
# Download Highcharts JS and annotations module
218+
highcharts_url = "https://code.highcharts.com/highcharts.js"
219+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
220+
highcharts_js = response.read().decode("utf-8")
221+
222+
annotations_url = "https://code.highcharts.com/modules/annotations.js"
223+
with urllib.request.urlopen(annotations_url, timeout=30) as response:
224+
annotations_js = response.read().decode("utf-8")
225+
226+
# Generate HTML with inline scripts
227+
html_str = chart.to_js_literal()
228+
html_content = f"""<!DOCTYPE html>
229+
<html>
230+
<head>
231+
<meta charset="utf-8">
232+
<script>{highcharts_js}</script>
233+
<script>{annotations_js}</script>
234+
</head>
235+
<body style="margin:0;">
236+
<div id="container" style="width: 4800px; height: 2700px;"></div>
237+
<script>{html_str}</script>
238+
</body>
239+
</html>"""
240+
241+
# Write temp HTML and take screenshot
242+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
243+
f.write(html_content)
244+
temp_path = f.name
245+
246+
chrome_options = Options()
247+
chrome_options.add_argument("--headless")
248+
chrome_options.add_argument("--no-sandbox")
249+
chrome_options.add_argument("--disable-dev-shm-usage")
250+
chrome_options.add_argument("--disable-gpu")
251+
chrome_options.add_argument("--window-size=4800,2800") # Slightly taller to capture full chart
252+
253+
driver = webdriver.Chrome(options=chrome_options)
254+
driver.get(f"file://{temp_path}")
255+
time.sleep(5)
256+
257+
# Find the container element and take screenshot of just that element
258+
container = driver.find_element("id", "container")
259+
container.screenshot("plot.png")
260+
driver.quit()
261+
262+
Path(temp_path).unlink()
263+
264+
# Also save HTML for interactive version
265+
with open("plot.html", "w", encoding="utf-8") as f:
266+
interactive_html = f"""<!DOCTYPE html>
267+
<html>
268+
<head>
269+
<meta charset="utf-8">
270+
<title>sn-curve-basic · highcharts · pyplots.ai</title>
271+
<script src="https://code.highcharts.com/highcharts.js"></script>
272+
<script src="https://code.highcharts.com/modules/annotations.js"></script>
273+
</head>
274+
<body style="margin:0;">
275+
<div id="container" style="width: 100%; height: 100vh;"></div>
276+
<script>{html_str}</script>
277+
</body>
278+
</html>"""
279+
f.write(interactive_html)

0 commit comments

Comments
 (0)