Skip to content

Commit 1534816

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

File tree

2 files changed

+347
-0
lines changed

2 files changed

+347
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
""" pyplots.ai
2+
sn-curve-basic: S-N Curve (Wöhler Curve)
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 78/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data: Fatigue test results for steel specimens
13+
np.random.seed(42)
14+
15+
# Stress levels (MPa) - from high to low
16+
stress_levels = np.array([450, 400, 350, 300, 275, 250, 225, 210, 200, 195])
17+
18+
# Generate cycles to failure with realistic scatter (Basquin relationship: S = A * N^b)
19+
base_cycles = np.array([1e2, 5e2, 2e3, 1e4, 3e4, 1e5, 4e5, 1e6, 5e6, 1e7])
20+
21+
# Add multiple test specimens per stress level (3-5 samples each) with scatter
22+
cycles_data = []
23+
stress_data = []
24+
25+
for stress, base_n in zip(stress_levels, base_cycles, strict=True):
26+
n_samples = np.random.randint(3, 6)
27+
scatter = np.exp(np.random.normal(0, 0.3, n_samples))
28+
cycles = base_n * scatter
29+
cycles_data.extend(cycles)
30+
stress_data.extend([stress] * n_samples)
31+
32+
cycles_data = np.array(cycles_data)
33+
stress_data = np.array(stress_data)
34+
35+
# Fit Basquin equation: S = A * N^b (linear in log-log space)
36+
log_cycles = np.log10(cycles_data)
37+
log_stress = np.log10(stress_data)
38+
coeffs = np.polyfit(log_cycles, log_stress, 1)
39+
b = coeffs[0] # slope (negative for S-N curve)
40+
log_A = coeffs[1] # intercept
41+
A = 10**log_A
42+
43+
# Generate fitted curve points with more points for smooth line
44+
fit_cycles = np.logspace(2, 7, 100)
45+
fit_stress = A * (fit_cycles**b)
46+
47+
# Material reference values (MPa)
48+
ultimate_strength = 520
49+
yield_strength = 350
50+
endurance_limit = 190
51+
52+
# Create XY data points for pygal
53+
xy_points = [(float(c), float(s)) for c, s in zip(cycles_data, stress_data, strict=True)]
54+
55+
# Fitted curve points as list of tuples
56+
fit_points = [(float(c), float(s)) for c, s in zip(fit_cycles, fit_stress, strict=True)]
57+
58+
# Custom style for 4800x2700 canvas with larger fonts
59+
custom_style = Style(
60+
background="white",
61+
plot_background="white",
62+
foreground="#333333",
63+
foreground_strong="#333333",
64+
foreground_subtle="#666666",
65+
colors=("#306998", "#FF6B35", "#E74C3C", "#27AE60", "#8E44AD"),
66+
title_font_size=72,
67+
label_font_size=48,
68+
major_label_font_size=42,
69+
legend_font_size=42,
70+
value_font_size=36,
71+
stroke_width=6,
72+
opacity=0.9,
73+
opacity_hover=1.0,
74+
)
75+
76+
# Create XY chart with logarithmic x-axis
77+
chart = pygal.XY(
78+
width=4800,
79+
height=2700,
80+
style=custom_style,
81+
title="sn-curve-basic · pygal · pyplots.ai",
82+
x_title="Cycles to Failure (N)",
83+
y_title="Stress Amplitude (MPa)",
84+
logarithmic=True,
85+
show_dots=True,
86+
dots_size=16,
87+
stroke=True,
88+
show_x_guides=True,
89+
show_y_guides=True,
90+
x_label_rotation=0,
91+
legend_at_bottom=True,
92+
legend_box_size=32,
93+
margin=80,
94+
truncate_legend=-1,
95+
range=(150, 550),
96+
)
97+
98+
# Add series in logical order for legend: data → fit → references (high to low)
99+
100+
# 1. Test data points (primary data - most important)
101+
chart.add("Test Data", xy_points, dots_size=20, stroke=False, show_dots=True)
102+
103+
# 2. Fitted Basquin curve (derived from data)
104+
chart.add(
105+
"Basquin Fit (S-N Curve)",
106+
fit_points,
107+
stroke=True,
108+
show_dots=False,
109+
stroke_style={"width": 8, "dasharray": "20, 10"},
110+
)
111+
112+
# 3. Reference lines in descending order of stress value
113+
# Ultimate Strength (highest)
114+
ultimate_line = [(100, ultimate_strength), (1e7, ultimate_strength)]
115+
chart.add(
116+
f"Ultimate Strength ({ultimate_strength} MPa)",
117+
ultimate_line,
118+
stroke=True,
119+
show_dots=False,
120+
stroke_style={"width": 4},
121+
)
122+
123+
# Yield Strength (middle)
124+
yield_line = [(100, yield_strength), (1e7, yield_strength)]
125+
chart.add(f"Yield Strength ({yield_strength} MPa)", yield_line, stroke=True, show_dots=False, stroke_style={"width": 4})
126+
127+
# Endurance Limit (lowest)
128+
endurance_line = [(100, endurance_limit), (1e7, endurance_limit)]
129+
chart.add(
130+
f"Endurance Limit ({endurance_limit} MPa)", endurance_line, stroke=True, show_dots=False, stroke_style={"width": 4}
131+
)
132+
133+
# Save outputs
134+
chart.render_to_file("plot.html")
135+
chart.render_to_png("plot.png")
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
library: pygal
2+
specification_id: sn-curve-basic
3+
created: '2026-01-15T21:43:12Z'
4+
updated: '2026-01-15T22:04:17Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21047244255
7+
issue: 3826
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/pygal/plot.html
13+
quality_score: 78
14+
review:
15+
strengths:
16+
- Correctly implements S-N curve with logarithmic X-axis and fitted Basquin equation
17+
- Includes all three key material reference lines (Ultimate Strength, Yield Strength,
18+
Endurance Limit)
19+
- Good use of pygal custom Style with appropriately sized fonts for 4800x2700 canvas
20+
- Data shows realistic scatter with multiple test specimens per stress level
21+
- Color palette is distinct and accessible
22+
weaknesses:
23+
- X-axis tick labels overlap/truncate at high cycle values due to logarithmic scale
24+
rendering in pygal
25+
- Y-axis is linear but spec recommends both axes use logarithmic scales
26+
- Legend order (Basquin Fit first) does not match visual prominence order (Test
27+
Data is primary)
28+
image_description: 'The plot displays an S-N curve (Wöhler curve) on a semi-logarithmic
29+
scale. The X-axis shows "Cycles to Failure (N)" with a logarithmic scale ranging
30+
from approximately 100 to 10^7, and the Y-axis shows "Stress Amplitude (MPa)"
31+
ranging from about 150 to 550 MPa (linear scale). Orange circular markers represent
32+
the test data points showing the characteristic downward trend of fatigue data
33+
- high stress at low cycles transitioning to low stress at high cycles. A dashed
34+
blue line represents the Basquin fit (S-N curve) that follows the data trend.
35+
Three horizontal reference lines are visible: a red line at 520 MPa for Ultimate
36+
Strength, a green line at 350 MPa for Yield Strength, and a purple line at 190
37+
MPa for Endurance Limit. The title "sn-curve-basic · pygal · pyplots.ai" appears
38+
at the top. The legend is positioned at the bottom, showing all five series. The
39+
background is white with subtle grid lines. X-axis tick labels show some crowding/overlap
40+
at higher values.'
41+
criteria_checklist:
42+
visual_quality:
43+
score: 30
44+
max: 40
45+
items:
46+
- id: VQ-01
47+
name: Text Legibility
48+
score: 7
49+
max: 10
50+
passed: true
51+
comment: Title and axis labels readable; tick labels somewhat crowded at high
52+
cycle counts
53+
- id: VQ-02
54+
name: No Overlap
55+
score: 4
56+
max: 8
57+
passed: false
58+
comment: X-axis tick labels overlap at higher cycle values due to logarithmic
59+
scale rendering
60+
- id: VQ-03
61+
name: Element Visibility
62+
score: 8
63+
max: 8
64+
passed: true
65+
comment: Data markers clearly visible at appropriate size, lines distinguishable
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 5
69+
max: 5
70+
passed: true
71+
comment: Orange, blue, red, green, purple are colorblind-friendly and distinct
72+
- id: VQ-05
73+
name: Layout Balance
74+
score: 4
75+
max: 5
76+
passed: true
77+
comment: Good canvas utilization, slight imbalance with legend at bottom
78+
- id: VQ-06
79+
name: Axis Labels
80+
score: 2
81+
max: 2
82+
passed: true
83+
comment: Both axes have descriptive labels with units (MPa, N)
84+
- id: VQ-07
85+
name: Grid & Legend
86+
score: 0
87+
max: 2
88+
passed: false
89+
comment: Legend order differs from visual prominence order
90+
spec_compliance:
91+
score: 23
92+
max: 25
93+
items:
94+
- id: SC-01
95+
name: Plot Type
96+
score: 8
97+
max: 8
98+
passed: true
99+
comment: Correct XY scatter plot with fitted curve for S-N data
100+
- id: SC-02
101+
name: Data Mapping
102+
score: 5
103+
max: 5
104+
passed: true
105+
comment: Cycles on X (log scale), Stress on Y correctly assigned
106+
- id: SC-03
107+
name: Required Features
108+
score: 4
109+
max: 5
110+
passed: true
111+
comment: Has data points, fit curve, and reference lines; Y-axis is linear
112+
not logarithmic
113+
- id: SC-04
114+
name: Data Range
115+
score: 3
116+
max: 3
117+
passed: true
118+
comment: Full range of data visible (10^2 to 10^7 cycles)
119+
- id: SC-05
120+
name: Legend Accuracy
121+
score: 2
122+
max: 2
123+
passed: true
124+
comment: Legend labels correctly identify all series with values
125+
- id: SC-06
126+
name: Title Format
127+
score: 2
128+
max: 2
129+
passed: true
130+
comment: 'Uses correct format: sn-curve-basic · pygal · pyplots.ai'
131+
data_quality:
132+
score: 18
133+
max: 20
134+
items:
135+
- id: DQ-01
136+
name: Feature Coverage
137+
score: 8
138+
max: 8
139+
passed: true
140+
comment: Shows scatter from multiple specimens, low/high cycle regions, and
141+
endurance limit region
142+
- id: DQ-02
143+
name: Realistic Context
144+
score: 7
145+
max: 7
146+
passed: true
147+
comment: Steel fatigue testing is a real engineering application with appropriate
148+
terminology
149+
- id: DQ-03
150+
name: Appropriate Scale
151+
score: 3
152+
max: 5
153+
passed: true
154+
comment: Stress values realistic for steel (190-520 MPa); could extend cycles
155+
further
156+
code_quality:
157+
score: 10
158+
max: 10
159+
items:
160+
- id: CQ-01
161+
name: KISS Structure
162+
score: 3
163+
max: 3
164+
passed: true
165+
comment: 'Simple script: imports → data → plot → save'
166+
- id: CQ-02
167+
name: Reproducibility
168+
score: 3
169+
max: 3
170+
passed: true
171+
comment: np.random.seed(42) ensures reproducibility
172+
- id: CQ-03
173+
name: Clean Imports
174+
score: 2
175+
max: 2
176+
passed: true
177+
comment: Only necessary imports (numpy, pygal, Style)
178+
- id: CQ-04
179+
name: No Deprecated API
180+
score: 1
181+
max: 1
182+
passed: true
183+
comment: Uses current pygal API
184+
- id: CQ-05
185+
name: Output Correct
186+
score: 1
187+
max: 1
188+
passed: true
189+
comment: Saves both plot.png and plot.html
190+
library_features:
191+
score: 3
192+
max: 5
193+
items:
194+
- id: LF-01
195+
name: Distinctive Features
196+
score: 3
197+
max: 5
198+
passed: true
199+
comment: Uses pygal XY chart, custom Style, logarithmic scale; could leverage
200+
tooltips more
201+
verdict: APPROVED
202+
impl_tags:
203+
dependencies: []
204+
techniques:
205+
- html-export
206+
patterns:
207+
- data-generation
208+
- iteration-over-groups
209+
dataprep:
210+
- regression
211+
styling:
212+
- grid-styling

0 commit comments

Comments
 (0)