Skip to content

Commit 64ed707

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

2 files changed

Lines changed: 327 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
""" pyplots.ai
2+
sn-curve-basic: S-N Curve (Wöhler Curve)
3+
Library: letsplot 4.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from lets_plot import * # noqa: F403
10+
from lets_plot.export import ggsave as export_ggsave
11+
12+
13+
LetsPlot.setup_html() # noqa: F405
14+
15+
# Generate realistic S-N curve data for steel specimen
16+
np.random.seed(42)
17+
18+
# Material properties (typical structural steel in MPa)
19+
ultimate_strength = 500
20+
yield_strength = 350
21+
endurance_limit = 200
22+
23+
# Generate data points at different stress levels
24+
stress_levels = np.array([450, 400, 350, 320, 300, 280, 260, 240, 220, 210])
25+
26+
# Base cycles following Basquin equation: S = A * N^b (b typically -0.05 to -0.15)
27+
# Rearranged: N = (S/A)^(1/b)
28+
A = 800
29+
b = -0.10
30+
base_cycles = (stress_levels / A) ** (1 / b)
31+
32+
# Generate multiple test specimens per stress level with scatter
33+
all_stress = []
34+
all_cycles = []
35+
for stress, base_N in zip(stress_levels, base_cycles, strict=True):
36+
n_specimens = np.random.randint(3, 6)
37+
scatter = np.random.lognormal(0, 0.15, n_specimens)
38+
cycles = base_N * scatter
39+
all_stress.extend([stress] * n_specimens)
40+
all_cycles.extend(cycles)
41+
42+
df = pd.DataFrame({"stress": all_stress, "cycles": all_cycles})
43+
44+
# Create fit line data
45+
fit_cycles = np.logspace(2, 7, 100)
46+
fit_stress = A * fit_cycles**b
47+
48+
df_fit = pd.DataFrame({"cycles": fit_cycles, "stress": fit_stress})
49+
50+
# Create the S-N curve plot
51+
plot = (
52+
ggplot() # noqa: F405
53+
# Basquin fit line
54+
+ geom_line( # noqa: F405
55+
data=df_fit,
56+
mapping=aes(x="cycles", y="stress"), # noqa: F405
57+
color="#306998",
58+
size=2,
59+
alpha=0.8,
60+
)
61+
# Data points with tooltips
62+
+ geom_point( # noqa: F405
63+
data=df,
64+
mapping=aes(x="cycles", y="stress"), # noqa: F405
65+
color="#306998",
66+
size=5,
67+
alpha=0.7,
68+
tooltips=layer_tooltips() # noqa: F405
69+
.line("Cycles|@cycles")
70+
.line("Stress|@stress MPa"),
71+
)
72+
# Reference lines for material properties
73+
+ geom_hline(yintercept=ultimate_strength, color="#DC2626", size=1.5, linetype="dashed") # noqa: F405
74+
+ geom_hline(yintercept=yield_strength, color="#B8860B", size=1.5, linetype="dashed") # noqa: F405
75+
+ geom_hline(yintercept=endurance_limit, color="#22C55E", size=1.5, linetype="dashed") # noqa: F405
76+
# Labels for reference lines (positioned on left side)
77+
+ geom_text( # noqa: F405
78+
data=pd.DataFrame({"cycles": [200], "stress": [ultimate_strength * 1.04], "label": ["Ultimate Strength"]}),
79+
mapping=aes(x="cycles", y="stress", label="label"), # noqa: F405
80+
color="#DC2626",
81+
size=14,
82+
hjust=0,
83+
)
84+
+ geom_text( # noqa: F405
85+
data=pd.DataFrame({"cycles": [200], "stress": [yield_strength * 1.04], "label": ["Yield Strength"]}),
86+
mapping=aes(x="cycles", y="stress", label="label"), # noqa: F405
87+
color="#B8860B",
88+
size=14,
89+
hjust=0,
90+
)
91+
+ geom_text( # noqa: F405
92+
data=pd.DataFrame({"cycles": [200], "stress": [endurance_limit * 1.04], "label": ["Endurance Limit"]}),
93+
mapping=aes(x="cycles", y="stress", label="label"), # noqa: F405
94+
color="#22C55E",
95+
size=14,
96+
hjust=0,
97+
)
98+
# Scales - log on both axes
99+
+ scale_x_log10() # noqa: F405
100+
+ scale_y_log10() # noqa: F405
101+
# Labels
102+
+ labs( # noqa: F405
103+
title="sn-curve-basic · letsplot · pyplots.ai", x="Number of Cycles to Failure (N)", y="Stress Amplitude (MPa)"
104+
)
105+
# Theme
106+
+ theme_minimal() # noqa: F405
107+
+ theme( # noqa: F405
108+
plot_title=element_text(size=24), # noqa: F405
109+
axis_title=element_text(size=20), # noqa: F405
110+
axis_text=element_text(size=16), # noqa: F405
111+
panel_grid=element_line(color="#CCCCCC", size=0.5, linetype="dashed"), # noqa: F405
112+
)
113+
+ ggsize(1600, 900) # noqa: F405
114+
)
115+
116+
# Save PNG (scale 3x to get 4800 x 2700 px)
117+
export_ggsave(plot, filename="plot.png", path=".", scale=3)
118+
119+
# Save HTML for interactive version
120+
export_ggsave(plot, filename="plot.html", path=".")
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
library: letsplot
2+
specification_id: sn-curve-basic
3+
created: '2026-01-15T20:48:18Z'
4+
updated: '2026-01-15T20:51:05Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21045722559
7+
issue: 3826
8+
python_version: 3.13.11
9+
library_version: 4.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/letsplot/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/letsplot/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/letsplot/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent implementation of S-N curve with proper log-log scaling
17+
- Clear horizontal reference lines with color-coded labels for Ultimate Strength,
18+
Yield Strength, and Endurance Limit
19+
- Realistic steel fatigue data using Basquin equation
20+
- Interactive tooltips for data points showing cycles and stress values
21+
- Clean code structure following KISS principles
22+
- Well-balanced canvas utilization with appropriate 16:9 aspect ratio
23+
weaknesses:
24+
- Missing legend identifying the fit line and data points
25+
image_description: |-
26+
The plot displays an S-N curve (Wöhler Curve) for steel fatigue data on a log-log scale. The X-axis shows "Number of Cycles to Failure (N)" ranging from 100 to 10^7, and the Y-axis shows "Stress Amplitude (MPa)" ranging from approximately 158 to 501 MPa. Blue data points represent individual fatigue test specimens with visible scatter at each stress level. A solid blue Basquin fit line passes through the data showing the characteristic inverse relationship between stress and cycles.
27+
28+
Three horizontal dashed reference lines are prominently displayed:
29+
- Red dashed line at ~501 MPa labeled "Ultimate Strength"
30+
- Dark yellow/olive dashed line at ~355 MPa labeled "Yield Strength"
31+
- Green dashed line at 200 MPa labeled "Endurance Limit"
32+
33+
The title "sn-curve-basic · letsplot · pyplots.ai" appears at the top. The plot uses a minimal theme with light gray dashed grid lines. Text labels for the reference lines are positioned on the left side of the plot in matching colors. Overall the layout is clean and uses the canvas space effectively.
34+
criteria_checklist:
35+
visual_quality:
36+
score: 36
37+
max: 40
38+
items:
39+
- id: VQ-01
40+
name: Text Legibility
41+
score: 10
42+
max: 10
43+
passed: true
44+
comment: Title, axis labels, and tick labels are all clearly readable at appropriate
45+
sizes
46+
- id: VQ-02
47+
name: No Overlap
48+
score: 8
49+
max: 8
50+
passed: true
51+
comment: No overlapping text elements; reference line labels are well positioned
52+
- id: VQ-03
53+
name: Element Visibility
54+
score: 6
55+
max: 8
56+
passed: true
57+
comment: Data points are visible with appropriate alpha; size could be slightly
58+
larger
59+
- id: VQ-04
60+
name: Color Accessibility
61+
score: 5
62+
max: 5
63+
passed: true
64+
comment: Red, yellow, green reference lines are distinguishable; blue data/line
65+
has good contrast
66+
- id: VQ-05
67+
name: Layout Balance
68+
score: 5
69+
max: 5
70+
passed: true
71+
comment: Plot fills canvas well with balanced margins; good use of 16:9 aspect
72+
ratio
73+
- id: VQ-06
74+
name: Axis Labels
75+
score: 2
76+
max: 2
77+
passed: true
78+
comment: Both axes have descriptive labels with units
79+
- id: VQ-07
80+
name: Grid & Legend
81+
score: 0
82+
max: 2
83+
passed: false
84+
comment: Grid is subtle and appropriate, but no legend for the data points/fit
85+
line
86+
spec_compliance:
87+
score: 24
88+
max: 25
89+
items:
90+
- id: SC-01
91+
name: Plot Type
92+
score: 8
93+
max: 8
94+
passed: true
95+
comment: Correct S-N curve with log-log scales
96+
- id: SC-02
97+
name: Data Mapping
98+
score: 5
99+
max: 5
100+
passed: true
101+
comment: Cycles on X-axis, Stress on Y-axis as specified
102+
- id: SC-03
103+
name: Required Features
104+
score: 4
105+
max: 5
106+
passed: true
107+
comment: Has reference lines and fit line; missing explicit legend
108+
- id: SC-04
109+
name: Data Range
110+
score: 3
111+
max: 3
112+
passed: true
113+
comment: Axes show full data range appropriately
114+
- id: SC-05
115+
name: Legend Accuracy
116+
score: 2
117+
max: 2
118+
passed: true
119+
comment: Reference line labels are accurate and color-matched
120+
- id: SC-06
121+
name: Title Format
122+
score: 2
123+
max: 2
124+
passed: true
125+
comment: 'Correct format: sn-curve-basic · letsplot · pyplots.ai'
126+
data_quality:
127+
score: 19
128+
max: 20
129+
items:
130+
- id: DQ-01
131+
name: Feature Coverage
132+
score: 7
133+
max: 8
134+
passed: true
135+
comment: Shows scatter from multiple specimens, Basquin fit, reference lines
136+
- id: DQ-02
137+
name: Realistic Context
138+
score: 7
139+
max: 7
140+
passed: true
141+
comment: Realistic steel fatigue test scenario with appropriate material properties
142+
- id: DQ-03
143+
name: Appropriate Scale
144+
score: 5
145+
max: 5
146+
passed: true
147+
comment: Stress values 200-500 MPa appropriate for structural steel
148+
code_quality:
149+
score: 9
150+
max: 10
151+
items:
152+
- id: CQ-01
153+
name: KISS Structure
154+
score: 3
155+
max: 3
156+
passed: true
157+
comment: Clean imports → data → plot → save structure, no functions/classes
158+
- id: CQ-02
159+
name: Reproducibility
160+
score: 3
161+
max: 3
162+
passed: true
163+
comment: Uses np.random.seed(42)
164+
- id: CQ-03
165+
name: Clean Imports
166+
score: 2
167+
max: 2
168+
passed: true
169+
comment: Only necessary imports used
170+
- id: CQ-04
171+
name: No Deprecated API
172+
score: 0
173+
max: 1
174+
passed: false
175+
comment: Uses non-standard ggsave import pattern
176+
- id: CQ-05
177+
name: Output Correct
178+
score: 1
179+
max: 1
180+
passed: true
181+
comment: Saves as plot.png
182+
library_features:
183+
score: 3
184+
max: 5
185+
items:
186+
- id: LF-01
187+
name: Distinctive Features
188+
score: 3
189+
max: 5
190+
passed: true
191+
comment: Uses layer_tooltips for interactivity, ggplot grammar, but could
192+
leverage more lets-plot specific features
193+
verdict: APPROVED
194+
impl_tags:
195+
dependencies: []
196+
techniques:
197+
- annotations
198+
- layer-composition
199+
- hover-tooltips
200+
- html-export
201+
patterns:
202+
- data-generation
203+
- iteration-over-groups
204+
dataprep: []
205+
styling:
206+
- alpha-blending
207+
- grid-styling

0 commit comments

Comments
 (0)