Skip to content

Commit 27de72a

Browse files
feat(plotly): implement sn-curve-basic (#3838)
## Implementation: `sn-curve-basic` - plotly Implements the **plotly** version of `sn-curve-basic`. **File:** `plots/sn-curve-basic/implementations/plotly.py` **Parent Issue:** #3826 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21045723781)* --------- 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 64ed707 commit 27de72a

2 files changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
""" pyplots.ai
2+
sn-curve-basic: S-N Curve (Wöhler Curve)
3+
Library: plotly 6.5.2 | Python 3.13.11
4+
Quality: 94/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data: Simulated fatigue test results for steel
12+
np.random.seed(42)
13+
14+
# Generate realistic S-N curve data with scatter
15+
# Basquin equation: S = A * N^b (where b is negative)
16+
A = 1200 # Material constant (MPa)
17+
b = -0.12 # Fatigue strength exponent
18+
19+
# Generate stress levels and corresponding cycles with scatter
20+
stress_levels = np.array([600, 550, 500, 450, 400, 350, 320, 300, 280, 260, 250, 240])
21+
cycles_base = (stress_levels / A) ** (1 / b)
22+
23+
# Add scatter (multiple specimens at each stress level)
24+
cycles = []
25+
stress = []
26+
for s, n_base in zip(stress_levels, cycles_base, strict=False):
27+
n_samples = np.random.randint(2, 5) # 2-4 specimens per stress level
28+
scatter = 10 ** (np.random.normal(0, 0.15, n_samples)) # Log-normal scatter
29+
for factor in scatter:
30+
cycles.append(n_base * factor)
31+
stress.append(s)
32+
33+
cycles = np.array(cycles)
34+
stress = np.array(stress)
35+
36+
# Material properties for reference lines
37+
ultimate_strength = 650 # MPa
38+
yield_strength = 450 # MPa
39+
endurance_limit = 230 # MPa
40+
41+
# Fit line data (smooth curve)
42+
fit_cycles = np.logspace(2, 8, 100)
43+
fit_stress = A * fit_cycles**b
44+
45+
# Create figure
46+
fig = go.Figure()
47+
48+
# Add S-N curve fit line
49+
fig.add_trace(
50+
go.Scatter(x=fit_cycles, y=fit_stress, mode="lines", name="Basquin Fit", line={"color": "#306998", "width": 4})
51+
)
52+
53+
# Add fatigue test data points
54+
fig.add_trace(
55+
go.Scatter(
56+
x=cycles,
57+
y=stress,
58+
mode="markers",
59+
name="Test Data",
60+
marker={"color": "#FFD43B", "size": 14, "line": {"color": "#306998", "width": 2}},
61+
)
62+
)
63+
64+
# Add horizontal reference lines as traces for legend visibility
65+
x_range = [100, 1e8]
66+
67+
fig.add_trace(
68+
go.Scatter(
69+
x=x_range,
70+
y=[ultimate_strength, ultimate_strength],
71+
mode="lines",
72+
name=f"Ultimate Strength ({ultimate_strength} MPa)",
73+
line={"color": "#D62728", "width": 3, "dash": "dash"},
74+
)
75+
)
76+
77+
fig.add_trace(
78+
go.Scatter(
79+
x=x_range,
80+
y=[yield_strength, yield_strength],
81+
mode="lines",
82+
name=f"Yield Strength ({yield_strength} MPa)",
83+
line={"color": "#2CA02C", "width": 3, "dash": "dash"},
84+
)
85+
)
86+
87+
fig.add_trace(
88+
go.Scatter(
89+
x=x_range,
90+
y=[endurance_limit, endurance_limit],
91+
mode="lines",
92+
name=f"Endurance Limit ({endurance_limit} MPa)",
93+
line={"color": "#9467BD", "width": 3, "dash": "dash"},
94+
)
95+
)
96+
97+
# Update layout
98+
fig.update_layout(
99+
title={"text": "sn-curve-basic · plotly · pyplots.ai", "font": {"size": 32}, "x": 0.5, "xanchor": "center"},
100+
xaxis={
101+
"title": {"text": "Cycles to Failure (N)", "font": {"size": 24}},
102+
"tickfont": {"size": 18},
103+
"type": "log",
104+
"showgrid": True,
105+
"gridwidth": 1,
106+
"gridcolor": "rgba(0,0,0,0.1)",
107+
"showline": True,
108+
"linewidth": 2,
109+
"linecolor": "black",
110+
"range": [2, 8], # 10^2 to 10^8
111+
},
112+
yaxis={
113+
"title": {"text": "Stress Amplitude (MPa)", "font": {"size": 24}},
114+
"tickfont": {"size": 18},
115+
"type": "log",
116+
"showgrid": True,
117+
"gridwidth": 1,
118+
"gridcolor": "rgba(0,0,0,0.1)",
119+
"showline": True,
120+
"linewidth": 2,
121+
"linecolor": "black",
122+
"range": [2.3, 2.9], # ~200 to ~800 MPa
123+
},
124+
legend={
125+
"font": {"size": 18},
126+
"x": 0.95,
127+
"y": 0.95,
128+
"xanchor": "right",
129+
"yanchor": "top",
130+
"bgcolor": "rgba(255,255,255,0.8)",
131+
"bordercolor": "black",
132+
"borderwidth": 1,
133+
},
134+
template="plotly_white",
135+
margin={"l": 100, "r": 150, "t": 100, "b": 100},
136+
)
137+
138+
# Save as PNG (4800 x 2700 px)
139+
fig.write_image("plot.png", width=1600, height=900, scale=3)
140+
141+
# Save as HTML for interactive viewing
142+
fig.write_html("plot.html")
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
library: plotly
2+
specification_id: sn-curve-basic
3+
created: '2026-01-15T20:48:47Z'
4+
updated: '2026-01-15T20:51:48Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21045723781
7+
issue: 3826
8+
python_version: 3.13.11
9+
library_version: 6.5.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/plotly/plot.html
13+
quality_score: 94
14+
review:
15+
strengths:
16+
- Excellent implementation of the S-N curve with all specification requirements
17+
met
18+
- Clear visualization with appropriate log-log scaling on both axes
19+
- Realistic simulated fatigue data using Basquin equation with proper scatter
20+
- All three reference lines (Ultimate, Yield, Endurance) clearly labeled with values
21+
- Clean, readable code following KISS principles
22+
- Proper use of plotly_white template for publication-quality appearance
23+
- Good marker styling with contrasting border colors
24+
weaknesses:
25+
- Legend position overlaps with the fit line at the top of the plot area
26+
- Could leverage Plotly hover capabilities to show stress/cycles values on hover
27+
- Markers could use slight alpha for overlapping points at same stress levels
28+
image_description: 'The plot displays an S-N curve (Wöhler curve) for steel fatigue
29+
testing. The X-axis shows "Cycles to Failure (N)" on a logarithmic scale from
30+
100 to 100M, with minor tick marks at 2 and 5 intervals. The Y-axis shows "Stress
31+
Amplitude (MPa)" on a logarithmic scale from 200 to 750. The main data consists
32+
of yellow circular markers with blue borders representing test data points, showing
33+
natural scatter across multiple stress levels. A solid blue line (Basquin Fit)
34+
runs through the data showing the power-law relationship. Three horizontal dashed
35+
reference lines are displayed: a red dashed line at 650 MPa (Ultimate Strength),
36+
a green dashed line at 450 MPa (Yield Strength), and a purple dashed line at 230
37+
MPa (Endurance Limit). The title "sn-curve-basic · plotly · pyplots.ai" is centered
38+
at the top. A legend in the upper right corner with a white semi-transparent background
39+
clearly identifies all five trace elements. The plot uses the plotly_white template
40+
with a clean white background and subtle gray gridlines.'
41+
criteria_checklist:
42+
visual_quality:
43+
score: 37
44+
max: 40
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 10
49+
max: 10
50+
passed: true
51+
comment: Title at 32pt, axis labels at 24pt, tick labels at 18pt, all perfectly
52+
readable
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 8
56+
max: 8
57+
passed: true
58+
comment: No overlapping text elements, all labels and data clearly separated
59+
- id: VQ-03
60+
name: Element Visibility
61+
score: 7
62+
max: 8
63+
passed: true
64+
comment: Markers size 14 with blue border are well visible; some close data
65+
points at similar stress levels could benefit from slight alpha
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 5
69+
max: 5
70+
passed: true
71+
comment: Colors (blue line, yellow markers, red/green/purple reference lines)
72+
are distinguishable and colorblind-friendly
73+
- id: VQ-05
74+
name: Layout Balance
75+
score: 5
76+
max: 5
77+
passed: true
78+
comment: Good use of canvas space, balanced margins, plot fills ~60% of area
79+
- id: VQ-06
80+
name: Axis Labels
81+
score: 2
82+
max: 2
83+
passed: true
84+
comment: 'Descriptive labels with units: Cycles to Failure (N), Stress Amplitude
85+
(MPa)'
86+
- id: VQ-07
87+
name: Grid & Legend
88+
score: 0
89+
max: 2
90+
passed: false
91+
comment: Legend overlaps with the fit line near the top of the plot area
92+
spec_compliance:
93+
score: 25
94+
max: 25
95+
items:
96+
- id: SC-01
97+
name: Plot Type
98+
score: 8
99+
max: 8
100+
passed: true
101+
comment: Correct S-N curve with log-log axes
102+
- id: SC-02
103+
name: Data Mapping
104+
score: 5
105+
max: 5
106+
passed: true
107+
comment: Cycles on X-axis, Stress on Y-axis as specified
108+
- id: SC-03
109+
name: Required Features
110+
score: 5
111+
max: 5
112+
passed: true
113+
comment: 'All required features present: scatter data, fit line, reference
114+
lines for Ultimate/Yield/Endurance'
115+
- id: SC-04
116+
name: Data Range
117+
score: 3
118+
max: 3
119+
passed: true
120+
comment: Shows full range from low-cycle to high-cycle fatigue regions
121+
- id: SC-05
122+
name: Legend Accuracy
123+
score: 2
124+
max: 2
125+
passed: true
126+
comment: All legend entries correctly describe their corresponding traces
127+
with MPa values
128+
- id: SC-06
129+
name: Title Format
130+
score: 2
131+
max: 2
132+
passed: true
133+
comment: Uses exact format sn-curve-basic · plotly · pyplots.ai
134+
data_quality:
135+
score: 20
136+
max: 20
137+
items:
138+
- id: DQ-01
139+
name: Feature Coverage
140+
score: 8
141+
max: 8
142+
passed: true
143+
comment: Shows realistic scatter (multiple specimens per stress level), covers
144+
low-cycle to high-cycle fatigue regions, includes all key material properties
145+
- id: DQ-02
146+
name: Realistic Context
147+
score: 7
148+
max: 7
149+
passed: true
150+
comment: Steel fatigue testing scenario with realistic Basquin equation parameters
151+
(A=1200 MPa, b=-0.12), realistic stress levels 240-600 MPa
152+
- id: DQ-03
153+
name: Appropriate Scale
154+
score: 5
155+
max: 5
156+
passed: true
157+
comment: Stress values (240-650 MPa) and cycle counts are realistic for steel
158+
materials
159+
code_quality:
160+
score: 9
161+
max: 10
162+
items:
163+
- id: CQ-01
164+
name: KISS Structure
165+
score: 3
166+
max: 3
167+
passed: true
168+
comment: 'Clean linear flow: imports, data, plot, save, no functions/classes'
169+
- id: CQ-02
170+
name: Reproducibility
171+
score: 3
172+
max: 3
173+
passed: true
174+
comment: Uses np.random.seed(42)
175+
- id: CQ-03
176+
name: Clean Imports
177+
score: 2
178+
max: 2
179+
passed: true
180+
comment: Only numpy and plotly.graph_objects imported, both used
181+
- id: CQ-04
182+
name: No Deprecated API
183+
score: 1
184+
max: 1
185+
passed: true
186+
comment: Modern plotly API usage
187+
- id: CQ-05
188+
name: Output Correct
189+
score: 0
190+
max: 1
191+
passed: false
192+
comment: Saves both plot.png and plot.html - correct for plotly
193+
library_features:
194+
score: 3
195+
max: 5
196+
items:
197+
- id: LF-01
198+
name: Distinctive Features
199+
score: 3
200+
max: 5
201+
passed: true
202+
comment: Uses graph_objects with proper trace management, but does not leverage
203+
hover tooltips or other interactive features
204+
verdict: APPROVED
205+
impl_tags:
206+
dependencies: []
207+
techniques:
208+
- html-export
209+
patterns:
210+
- data-generation
211+
- iteration-over-groups
212+
dataprep: []
213+
styling:
214+
- edge-highlighting
215+
- grid-styling

0 commit comments

Comments
 (0)