Skip to content

Commit fff4e9d

Browse files
feat(plotly): implement sn-curve-basic (#7520)
## Implementation: `sn-curve-basic` - python/plotly Implements the **python/plotly** version of `sn-curve-basic`. **File:** `plots/sn-curve-basic/implementations/python/plotly.py` **Parent Issue:** #3826 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26168194705)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent ebce5da commit fff4e9d

2 files changed

Lines changed: 170 additions & 137 deletions

File tree

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,96 @@
11
""" anyplot.ai
22
sn-curve-basic: S-N Curve (Wöhler Curve)
33
Library: plotly 6.7.0 | Python 3.13.13
4-
Quality: 80/100 | Updated: 2026-05-20
4+
Quality: 89/100 | Updated: 2026-05-20
55
"""
66

7+
import os
8+
79
import numpy as np
810
import plotly.graph_objects as go
911

1012

11-
# Data: Simulated fatigue test results for steel
13+
# Theme
14+
THEME = os.getenv("ANYPLOT_THEME", "light")
15+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
16+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
17+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
18+
GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
19+
20+
# Okabe-Ito palette
21+
BRAND = "#009E73" # position 1 — Basquin fit line
22+
C2 = "#D55E00" # position 2 — test data markers
23+
C3 = "#0072B2" # position 3 — ultimate strength
24+
C4 = "#CC79A7" # position 4 — yield strength
25+
C5 = "#E69F00" # position 5 — endurance limit
26+
27+
# Data: Steel fatigue test data (Basquin model)
1228
np.random.seed(42)
1329

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)
30+
A = 1200 # Fatigue coefficient (MPa)
1731
b = -0.12 # Fatigue strength exponent
1832

19-
# Generate stress levels and corresponding cycles with scatter
2033
stress_levels = np.array([600, 550, 500, 450, 400, 350, 320, 300, 280, 260, 250, 240])
2134
cycles_base = (stress_levels / A) ** (1 / b)
2235

23-
# Add scatter (multiple specimens at each stress level)
2436
cycles = []
2537
stress = []
2638
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
39+
n_samples = np.random.randint(2, 5)
40+
scatter = 10 ** (np.random.normal(0, 0.15, n_samples))
2941
for factor in scatter:
3042
cycles.append(n_base * factor)
3143
stress.append(s)
3244

3345
cycles = np.array(cycles)
3446
stress = np.array(stress)
3547

36-
# Material properties for reference lines
48+
# Material properties
3749
ultimate_strength = 650 # MPa
3850
yield_strength = 450 # MPa
3951
endurance_limit = 230 # MPa
4052

41-
# Fit line data (smooth curve)
42-
fit_cycles = np.logspace(2, 8, 100)
53+
# Basquin fit line
54+
fit_cycles = np.logspace(2, 7, 100)
4355
fit_stress = A * fit_cycles**b
4456

45-
# Create figure
57+
# Plot
4658
fig = go.Figure()
4759

48-
# Add S-N curve fit line
4960
fig.add_trace(
50-
go.Scatter(x=fit_cycles, y=fit_stress, mode="lines", name="Basquin Fit", line={"color": "#306998", "width": 4})
61+
go.Scatter(
62+
x=fit_cycles,
63+
y=fit_stress,
64+
mode="lines",
65+
name="Basquin Fit",
66+
line={"color": BRAND, "width": 3},
67+
hovertemplate="Cycles: %{x:.2e}<br>Stress: %{y:.0f} MPa<extra>Basquin Fit</extra>",
68+
)
5169
)
5270

53-
# Add fatigue test data points
5471
fig.add_trace(
5572
go.Scatter(
5673
x=cycles,
5774
y=stress,
5875
mode="markers",
5976
name="Test Data",
60-
marker={"color": "#FFD43B", "size": 14, "line": {"color": "#306998", "width": 2}},
77+
marker={"color": C2, "size": 12, "line": {"color": PAGE_BG, "width": 1.5}},
78+
hovertemplate="Cycles: %{x:.2e}<br>Stress: %{y:.0f} MPa<extra>Test Data</extra>",
6179
)
6280
)
6381

64-
# Add horizontal reference lines as traces for legend visibility
65-
x_range = [100, 1e8]
82+
x_range = [100, 1e7]
83+
84+
fig.add_hrect(y0=200, y1=endurance_limit, opacity=0.05, fillcolor=BRAND, layer="below")
6685

6786
fig.add_trace(
6887
go.Scatter(
6988
x=x_range,
7089
y=[ultimate_strength, ultimate_strength],
7190
mode="lines",
7291
name=f"Ultimate Strength ({ultimate_strength} MPa)",
73-
line={"color": "#D62728", "width": 3, "dash": "dash"},
92+
line={"color": C3, "width": 2, "dash": "dash"},
93+
hovertemplate=f"Ultimate Strength: {ultimate_strength} MPa<extra></extra>",
7494
)
7595
)
7696

@@ -80,7 +100,8 @@
80100
y=[yield_strength, yield_strength],
81101
mode="lines",
82102
name=f"Yield Strength ({yield_strength} MPa)",
83-
line={"color": "#2CA02C", "width": 3, "dash": "dash"},
103+
line={"color": C4, "width": 2, "dash": "dash"},
104+
hovertemplate=f"Yield Strength: {yield_strength} MPa<extra></extra>",
84105
)
85106
)
86107

@@ -90,53 +111,61 @@
90111
y=[endurance_limit, endurance_limit],
91112
mode="lines",
92113
name=f"Endurance Limit ({endurance_limit} MPa)",
93-
line={"color": "#9467BD", "width": 3, "dash": "dash"},
114+
line={"color": C5, "width": 2, "dash": "dash"},
115+
hovertemplate=f"Endurance Limit: {endurance_limit} MPa<extra></extra>",
94116
)
95117
)
96118

97-
# Update layout
119+
# Style
98120
fig.update_layout(
99-
title={"text": "sn-curve-basic · plotly · pyplots.ai", "font": {"size": 32}, "x": 0.5, "xanchor": "center"},
121+
autosize=False,
122+
paper_bgcolor=PAGE_BG,
123+
plot_bgcolor=PAGE_BG,
124+
hovermode="closest",
125+
title={
126+
"text": "sn-curve-basic · python · plotly · anyplot.ai",
127+
"font": {"size": 16, "color": INK},
128+
"x": 0.5,
129+
"xanchor": "center",
130+
},
100131
xaxis={
101-
"title": {"text": "Cycles to Failure (N)", "font": {"size": 24}},
102-
"tickfont": {"size": 18},
132+
"title": {"text": "Cycles to Failure (N)", "font": {"size": 12, "color": INK}},
133+
"tickfont": {"size": 10, "color": INK_SOFT},
103134
"type": "log",
104-
"showgrid": True,
105-
"gridwidth": 1,
106-
"gridcolor": "rgba(0,0,0,0.1)",
135+
"showgrid": False,
107136
"showline": True,
108-
"linewidth": 2,
109-
"linecolor": "black",
110-
"range": [2, 8], # 10^2 to 10^8
137+
"linewidth": 1,
138+
"linecolor": INK_SOFT,
139+
"mirror": False,
140+
"range": [2, 7],
141+
"zerolinecolor": GRID,
111142
},
112143
yaxis={
113-
"title": {"text": "Stress Amplitude (MPa)", "font": {"size": 24}},
114-
"tickfont": {"size": 18},
144+
"title": {"text": "Stress Amplitude (MPa)", "font": {"size": 12, "color": INK}},
145+
"tickfont": {"size": 10, "color": INK_SOFT},
115146
"type": "log",
116147
"showgrid": True,
117148
"gridwidth": 1,
118-
"gridcolor": "rgba(0,0,0,0.1)",
149+
"gridcolor": GRID,
119150
"showline": True,
120-
"linewidth": 2,
121-
"linecolor": "black",
122-
"range": [2.3, 2.9], # ~200 to ~800 MPa
151+
"linewidth": 1,
152+
"linecolor": INK_SOFT,
153+
"mirror": False,
154+
"range": [2.3, 2.9],
155+
"zerolinecolor": GRID,
123156
},
124157
legend={
125-
"font": {"size": 18},
158+
"font": {"size": 10, "color": INK_SOFT},
126159
"x": 0.95,
127160
"y": 0.95,
128161
"xanchor": "right",
129162
"yanchor": "top",
130-
"bgcolor": "rgba(255,255,255,0.8)",
131-
"bordercolor": "black",
132-
"borderwidth": 1,
163+
"bgcolor": "rgba(0,0,0,0)",
164+
"borderwidth": 0,
133165
},
134-
template="plotly_white",
135-
margin={"l": 100, "r": 150, "t": 100, "b": 100},
166+
margin={"l": 80, "r": 40, "t": 80, "b": 60},
136167
)
137168

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")
169+
# Save
170+
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
171+
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

0 commit comments

Comments
 (0)