Skip to content

Commit 16cf671

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

2 files changed

Lines changed: 385 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
""" pyplots.ai
2+
sn-curve-basic: S-N Curve (Wöhler Curve)
3+
Library: bokeh 3.8.2 | Python 3.13.11
4+
Quality: 93/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
from bokeh.io import export_png
9+
from bokeh.models import ColumnDataSource, Label, Span
10+
from bokeh.plotting import figure
11+
12+
13+
# Data - Typical S-N fatigue test results for steel specimens
14+
np.random.seed(42)
15+
16+
# Generate realistic S-N curve data points
17+
# Basquin equation: S = A * N^b (power law relationship)
18+
# Using typical steel fatigue parameters
19+
A = 1200 # MPa (coefficient)
20+
b = -0.12 # Basquin exponent
21+
22+
# Create stress levels with multiple specimens at each level
23+
stress_levels = np.array([450, 400, 350, 320, 300, 280, 260, 250, 240, 230, 220, 210])
24+
25+
# Generate cycles for each stress level with realistic scatter
26+
cycles_list = []
27+
stress_list = []
28+
29+
for stress in stress_levels:
30+
# Calculate theoretical cycles from Basquin equation
31+
N_theoretical = (stress / A) ** (1 / b)
32+
# Add scatter (typical for fatigue data)
33+
n_specimens = np.random.randint(2, 5)
34+
scatter = np.random.lognormal(0, 0.3, n_specimens)
35+
cycles_actual = N_theoretical * scatter
36+
cycles_list.extend(cycles_actual)
37+
stress_list.extend([stress] * n_specimens)
38+
39+
cycles = np.array(cycles_list)
40+
stress = np.array(stress_list)
41+
42+
# Material properties reference values
43+
ultimate_strength = 500 # MPa
44+
yield_strength = 350 # MPa
45+
endurance_limit = 200 # MPa (below which infinite life expected)
46+
47+
# Fit line data (using Basquin equation)
48+
cycles_fit = np.logspace(2, 7, 100)
49+
stress_fit = A * (cycles_fit**b)
50+
51+
# Create ColumnDataSource for data points
52+
source = ColumnDataSource(data={"cycles": cycles, "stress": stress})
53+
54+
# Create ColumnDataSource for fit line
55+
source_fit = ColumnDataSource(data={"cycles_fit": cycles_fit, "stress_fit": stress_fit})
56+
57+
# Create figure with log scales
58+
p = figure(
59+
width=4800,
60+
height=2700,
61+
title="sn-curve-basic · bokeh · pyplots.ai",
62+
x_axis_label="Number of Cycles to Failure (N)",
63+
y_axis_label="Stress Amplitude (MPa)",
64+
x_axis_type="log",
65+
y_axis_type="log",
66+
y_range=(150, 600),
67+
x_range=(100, 2e7),
68+
)
69+
70+
# Plot S-N curve fit line
71+
p.line(
72+
x="cycles_fit",
73+
y="stress_fit",
74+
source=source_fit,
75+
line_width=5,
76+
line_color="#306998",
77+
line_alpha=0.9,
78+
legend_label="Basquin Fit (S = A·N^b)",
79+
)
80+
81+
# Plot data points
82+
p.scatter(
83+
x="cycles",
84+
y="stress",
85+
source=source,
86+
size=22,
87+
fill_color="#306998",
88+
fill_alpha=0.7,
89+
line_color="#1a3d5c",
90+
line_width=2,
91+
legend_label="Fatigue Test Data",
92+
)
93+
94+
# Add horizontal reference lines for material properties
95+
ultimate_line = Span(
96+
location=ultimate_strength, dimension="width", line_color="#c0392b", line_width=4, line_dash="dashed"
97+
)
98+
p.add_layout(ultimate_line)
99+
100+
yield_line = Span(location=yield_strength, dimension="width", line_color="#e67e22", line_width=4, line_dash="dashed")
101+
p.add_layout(yield_line)
102+
103+
endurance_line = Span(
104+
location=endurance_limit, dimension="width", line_color="#27ae60", line_width=4, line_dash="dashed"
105+
)
106+
p.add_layout(endurance_line)
107+
108+
# Add labels for reference lines
109+
ultimate_label = Label(
110+
x=150,
111+
y=520,
112+
text=f"Ultimate Strength ({ultimate_strength} MPa)",
113+
text_font_size="22pt",
114+
text_color="#c0392b",
115+
text_font_style="bold",
116+
)
117+
p.add_layout(ultimate_label)
118+
119+
yield_label = Label(
120+
x=150,
121+
y=365,
122+
text=f"Yield Strength ({yield_strength} MPa)",
123+
text_font_size="22pt",
124+
text_color="#e67e22",
125+
text_font_style="bold",
126+
)
127+
p.add_layout(yield_label)
128+
129+
endurance_label = Label(
130+
x=150,
131+
y=208,
132+
text=f"Endurance Limit ({endurance_limit} MPa)",
133+
text_font_size="22pt",
134+
text_color="#27ae60",
135+
text_font_style="bold",
136+
)
137+
p.add_layout(endurance_label)
138+
139+
# Style the plot
140+
p.title.text_font_size = "36pt"
141+
p.title.text_color = "#2c3e50"
142+
p.xaxis.axis_label_text_font_size = "26pt"
143+
p.yaxis.axis_label_text_font_size = "26pt"
144+
p.xaxis.major_label_text_font_size = "22pt"
145+
p.yaxis.major_label_text_font_size = "22pt"
146+
147+
# Legend styling
148+
p.legend.location = "bottom_left"
149+
p.legend.label_text_font_size = "22pt"
150+
p.legend.background_fill_alpha = 0.9
151+
p.legend.border_line_width = 2
152+
p.legend.border_line_color = "#cccccc"
153+
p.legend.glyph_width = 50
154+
p.legend.glyph_height = 35
155+
p.legend.spacing = 12
156+
p.legend.padding = 20
157+
158+
# Grid styling
159+
p.grid.grid_line_alpha = 0.3
160+
p.grid.grid_line_dash = [6, 4]
161+
162+
# Background
163+
p.background_fill_color = "#fafafa"
164+
p.border_fill_color = "#ffffff"
165+
166+
# Axis styling
167+
p.xaxis.axis_line_width = 2
168+
p.yaxis.axis_line_width = 2
169+
p.xaxis.major_tick_line_width = 2
170+
p.yaxis.major_tick_line_width = 2
171+
172+
# Save as PNG
173+
export_png(p, filename="plot.png")
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
library: bokeh
2+
specification_id: sn-curve-basic
3+
created: '2026-01-15T20:47:36Z'
4+
updated: '2026-01-15T20:50:12Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 21045723821
7+
issue: 3826
8+
python_version: 3.13.11
9+
library_version: 3.8.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/bokeh/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/bokeh/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/sn-curve-basic/bokeh/plot.html
13+
quality_score: 93
14+
review:
15+
strengths:
16+
- Excellent implementation of the Basquin equation for realistic S-N curve generation
17+
- All three material property reference lines (Ultimate Strength, Yield Strength,
18+
Endurance Limit) are clearly displayed with appropriate colors and labels
19+
- Clean code structure with proper use of ColumnDataSource for both data points
20+
and fit line
21+
- Appropriate use of log-log scales as required by the specification
22+
- Professional styling with good font sizes and color choices
23+
weaknesses:
24+
- Legend text appears relatively small compared to other text elements in the plot
25+
- Could benefit from HoverTool to show exact values when hovering over data points
26+
(better use of Bokeh interactive capabilities)
27+
image_description: 'The plot displays an S-N curve (Wöhler curve) on a log-log scale.
28+
The x-axis shows "Number of Cycles to Failure (N)" ranging from 10² to 10⁷, and
29+
the y-axis shows "Stress Amplitude (MPa)" ranging from approximately 200 to 600
30+
MPa. Blue circular markers represent fatigue test data points (approximately 40
31+
points) with realistic scatter at each stress level. A solid blue line shows the
32+
Basquin fit curve (S = A·N^b) decreasing from upper left to lower right. Three
33+
horizontal dashed reference lines are displayed: red for Ultimate Strength (500
34+
MPa), orange for Yield Strength (350 MPa), and green for Endurance Limit (200
35+
MPa). Each reference line has a corresponding colored label on the left side.
36+
The legend is positioned in the bottom-left corner showing "Basquin Fit (S = A·N^b)"
37+
and "Fatigue Test Data". The title "sn-curve-basic · bokeh · pyplots.ai" appears
38+
at the top. The background is a light gray (#fafafa) with a subtle dashed grid.'
39+
criteria_checklist:
40+
visual_quality:
41+
score: 37
42+
max: 40
43+
items:
44+
- id: VQ-01
45+
name: Text Legibility
46+
score: 10
47+
max: 10
48+
passed: true
49+
comment: Title at 36pt, axis labels at 26pt, tick labels at 22pt - all perfectly
50+
readable
51+
- id: VQ-02
52+
name: No Overlap
53+
score: 8
54+
max: 8
55+
passed: true
56+
comment: No overlapping text elements, all labels clearly positioned
57+
- id: VQ-03
58+
name: Element Visibility
59+
score: 7
60+
max: 8
61+
passed: true
62+
comment: Markers size 22 with good alpha, visible but some data points overlap
63+
slightly at similar stress levels
64+
- id: VQ-04
65+
name: Color Accessibility
66+
score: 5
67+
max: 5
68+
passed: true
69+
comment: Blue for data, red/orange/green reference lines are distinguishable
70+
and colorblind-safe
71+
- id: VQ-05
72+
name: Layout Balance
73+
score: 5
74+
max: 5
75+
passed: true
76+
comment: Plot fills canvas well with balanced margins
77+
- id: VQ-06
78+
name: Axis Labels
79+
score: 2
80+
max: 2
81+
passed: true
82+
comment: 'Descriptive with units: Stress Amplitude (MPa), Number of Cycles
83+
to Failure (N)'
84+
- id: VQ-07
85+
name: Grid & Legend
86+
score: 0
87+
max: 2
88+
passed: false
89+
comment: Grid alpha 0.3 is good, but legend text appears small relative to
90+
other elements
91+
spec_compliance:
92+
score: 25
93+
max: 25
94+
items:
95+
- id: SC-01
96+
name: Plot Type
97+
score: 8
98+
max: 8
99+
passed: true
100+
comment: Correct S-N curve with log-log scales
101+
- id: SC-02
102+
name: Data Mapping
103+
score: 5
104+
max: 5
105+
passed: true
106+
comment: Cycles on X, Stress on Y as specified
107+
- id: SC-03
108+
name: Required Features
109+
score: 5
110+
max: 5
111+
passed: true
112+
comment: Includes data points, fit line, and all three reference lines
113+
- id: SC-04
114+
name: Data Range
115+
score: 3
116+
max: 3
117+
passed: true
118+
comment: Axes show full range from 10² to 10⁷ cycles
119+
- id: SC-05
120+
name: Legend Accuracy
121+
score: 2
122+
max: 2
123+
passed: true
124+
comment: Legend correctly identifies Basquin Fit and Fatigue Test Data
125+
- id: SC-06
126+
name: Title Format
127+
score: 2
128+
max: 2
129+
passed: true
130+
comment: 'Correct format: sn-curve-basic · bokeh · pyplots.ai'
131+
data_quality:
132+
score: 18
133+
max: 20
134+
items:
135+
- id: DQ-01
136+
name: Feature Coverage
137+
score: 7
138+
max: 8
139+
passed: true
140+
comment: Shows scatter at multiple stress levels, fit curve, but could show
141+
more distinct fatigue regions
142+
- id: DQ-02
143+
name: Realistic Context
144+
score: 7
145+
max: 7
146+
passed: true
147+
comment: Steel fatigue testing is a perfect engineering scenario with realistic
148+
Basquin parameters
149+
- id: DQ-03
150+
name: Appropriate Scale
151+
score: 4
152+
max: 5
153+
passed: true
154+
comment: Values are realistic (200-500 MPa for steel), though some generated
155+
points slightly exceed expected scatter
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: 'Clean linear structure: imports → data → plot → save'
166+
- id: CQ-02
167+
name: Reproducibility
168+
score: 3
169+
max: 3
170+
passed: true
171+
comment: Uses np.random.seed(42)
172+
- id: CQ-03
173+
name: Clean Imports
174+
score: 2
175+
max: 2
176+
passed: true
177+
comment: All imports are used
178+
- id: CQ-04
179+
name: No Deprecated API
180+
score: 1
181+
max: 1
182+
passed: true
183+
comment: Uses current Bokeh API
184+
- id: CQ-05
185+
name: Output Correct
186+
score: 1
187+
max: 1
188+
passed: true
189+
comment: Saves as plot.png
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 ColumnDataSource, Span for reference lines, Label for annotations
200+
- good but could leverage HoverTool
201+
verdict: APPROVED
202+
impl_tags:
203+
dependencies: []
204+
techniques:
205+
- annotations
206+
patterns:
207+
- data-generation
208+
- columndatasource
209+
dataprep: []
210+
styling:
211+
- alpha-blending
212+
- grid-styling

0 commit comments

Comments
 (0)