Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 96 additions & 70 deletions plots/line-stress-strain/implementations/python/plotnine.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
""" pyplots.ai
""" anyplot.ai
line-stress-strain: Engineering Stress-Strain Curve
Library: plotnine 0.15.3 | Python 3.14.3
Quality: 90/100 | Created: 2026-03-20
Library: plotnine 0.15.7 | Python 3.13.14
Quality: 83/100 | Updated: 2026-06-21
"""

import os
import sys

import numpy as np
import pandas as pd
from plotnine import (


# Work around naming conflict between plotnine.py script and plotnine package
script_dir = os.path.dirname(os.path.abspath(__file__))
if script_dir in sys.path:
sys.path.remove(script_dir)
if "" in sys.path:
sys.path.remove("")
if "." in sys.path:
sys.path.remove(".")

from plotnine import ( # noqa: E402
aes,
annotate,
coord_cartesian,
Expand All @@ -21,8 +35,6 @@
ggplot,
labs,
scale_color_identity,
scale_fill_identity,
scale_linetype_identity,
scale_size_identity,
scale_x_continuous,
scale_y_continuous,
Expand All @@ -31,7 +43,15 @@
)


# Data - Mild steel stress-strain curve
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
REGION_ALPHA = 0.18 if THEME == "light" else 0.13

# Imprint palette — position 1 is always the first categorical series
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

np.random.seed(42)

youngs_modulus = 210000 # MPa
Expand All @@ -40,120 +60,126 @@
fracture_strain = 0.35
necking_strain = 0.22

# Elastic region (0 to yield)
# Elastic region
elastic_strain = np.linspace(0, yield_stress / youngs_modulus, 40)
elastic_stress = youngs_modulus * elastic_strain

# Yield plateau (short flat region for mild steel)
# Yield plateau (part of the plastic region — not a separate band per spec)
plateau_strain = np.linspace(elastic_strain[-1], 0.025, 15)
plateau_stress = np.full_like(plateau_strain, yield_stress)

# Strain hardening (power law)
hardening_strain = np.linspace(0.025, necking_strain, 80)
hardening_stress = yield_stress + (uts - yield_stress) * ((hardening_strain - 0.025) / (necking_strain - 0.025)) ** 0.45

# Necking to fracture (stress decreases)
# Necking to fracture
necking_strain_vals = np.linspace(necking_strain, fracture_strain, 40)
necking_stress = (
uts - (uts - 320) * ((necking_strain_vals - necking_strain) / (fracture_strain - necking_strain)) ** 1.3
)

# Combine all regions
strain = np.concatenate([elastic_strain, plateau_strain[1:], hardening_strain[1:], necking_strain_vals[1:]])
stress = np.concatenate([elastic_stress, plateau_stress[1:], hardening_stress[1:], necking_stress[1:]])

df = pd.DataFrame({"strain": strain, "stress": stress})

# 0.2% offset line data - extended for better visibility
# 0.2% offset line
offset = 0.002
offset_strain_start = offset
elastic_end = yield_stress / youngs_modulus # ~0.00119
offset_strain_end = (yield_stress + 50) / youngs_modulus + offset

# Key points
yield_point_strain = yield_stress / youngs_modulus + offset
# Critical points
yield_point_strain = elastic_end + offset
yield_point_stress = yield_stress
uts_strain = necking_strain
uts_stress = uts
fracture_strain_pt = fracture_strain
fracture_stress_pt = necking_stress[-1]
fracture_stress_pt = float(necking_stress[-1])

df_points = pd.DataFrame(
{
"strain": [yield_point_strain, uts_strain, fracture_strain_pt],
"stress": [yield_point_stress, uts_stress, fracture_stress_pt],
"label": ["Yield Point\n(0.2% offset)", "UTS", "Fracture"],
"color": ["#C0392B", "#C0392B", "#C0392B"],
"size": [6.0, 6.0, 6.0],
"strain": [yield_point_strain, necking_strain, fracture_strain],
"stress": [yield_point_stress, uts, fracture_stress_pt],
"color": [IMPRINT[4]] * 3,
"size": [3.0, 3.0, 3.0],
}
)

# Region labels - repositioned for clarity
# Region labels: 3 regions per spec (elastic, strain hardening, necking)
# Yield plateau is a critical point, not a separate shaded band
df_regions = pd.DataFrame(
{
"strain": [0.005, 0.015, 0.13, 0.29],
"stress": [410, 215, 310, 370],
"label": ["Elastic", "Yield\nPlateau", "Strain\nHardening", "Necking"],
"color": ["#5D6D7E", "#5D6D7E", "#5D6D7E", "#5D6D7E"],
"strain": [elastic_end / 2, 0.13, 0.29],
"stress": [350, 335, 385],
"label": ["Elastic", "Strain\nHardening", "Necking"],
}
)

# Region boundary strains for shading
elastic_end = yield_stress / youngs_modulus
plateau_end = 0.025

# Plot using plotnine grammar of graphics with layered composition
plot = (
ggplot()
# Region shading using annotate("rect") - plotnine-distinctive feature
+ annotate("rect", xmin=0, xmax=elastic_end, ymin=0, ymax=440, alpha=0.15, fill="#3498DB")
+ annotate("rect", xmin=elastic_end, xmax=plateau_end, ymin=0, ymax=440, alpha=0.15, fill="#2ECC71")
+ annotate("rect", xmin=plateau_end, xmax=necking_strain, ymin=0, ymax=440, alpha=0.12, fill="#F39C12")
+ annotate("rect", xmin=necking_strain, xmax=fracture_strain, ymin=0, ymax=440, alpha=0.12, fill="#E74C3C")
# Main stress-strain curve
+ geom_line(df, aes(x="strain", y="stress"), color="#306998", size=2.8)
# 0.2% offset line using geom_segment - plotnine-distinctive
# Three region shadings: elastic, plastic (strain hardening), necking
+ annotate("rect", xmin=0, xmax=elastic_end, ymin=0, ymax=460, alpha=REGION_ALPHA, fill=IMPRINT[2])
+ annotate("rect", xmin=elastic_end, xmax=necking_strain, ymin=0, ymax=460, alpha=REGION_ALPHA, fill=IMPRINT[3])
+ annotate("rect", xmin=necking_strain, xmax=fracture_strain, ymin=0, ymax=460, alpha=REGION_ALPHA, fill=IMPRINT[4])
# Main stress-strain curve (Imprint position 1 — first categorical series)
+ geom_line(df, aes(x="strain", y="stress"), color=IMPRINT[0], size=1.0)
# 0.2% offset construction line
+ geom_segment(
aes(x=offset_strain_start, xend=offset_strain_end, y=0, yend=yield_stress + 50),
color="#C0392B",
size=1.2,
aes(x=offset, xend=offset_strain_end, y=0, yend=yield_stress + 50),
color=IMPRINT[4],
size=0.6,
linetype="dashed",
)
# Offset label near the line
+ annotate("text", x=0.012, y=60, label="0.2% offset", size=11, color="#C0392B", fontstyle="italic")
# Key points with identity scales for direct aesthetic mapping
+ geom_point(df_points, aes(x="strain", y="stress", color="color", size="size"), fill="#C0392B")
+ annotate("text", x=0.011, y=52, label="0.2% offset", size=3.5, color=INK_SOFT, fontstyle="italic")
# Critical point markers
+ geom_point(df_points, aes(x="strain", y="stress", color="color", size="size"))
+ scale_color_identity()
+ scale_size_identity()
# Point labels - larger text
+ geom_text(
df_points, aes(x="strain", y="stress", label="label"), nudge_y=32, size=15, color="#2C3E50", fontweight="bold"
# Critical point labels — individual positions for clarity near y-axis
+ annotate(
"text",
x=yield_point_strain + 0.018,
y=yield_point_stress + 22,
label="Yield Point\n(0.2% offset)",
size=3.0,
color=INK,
fontweight="bold",
)
# Region labels with identity color scale - larger text
+ geom_text(df_regions, aes(x="strain", y="stress", label="label", color="color"), size=14, fontstyle="italic")
+ scale_fill_identity()
+ scale_linetype_identity()
# Modulus annotation - larger and repositioned
+ annotate("text", x=necking_strain, y=uts + 28, label="UTS", size=3.0, color=INK, fontweight="bold")
+ annotate(
"text", x=0.03, y=140, label=f"E = {youngs_modulus // 1000} GPa", size=16, color="#306998", fontweight="bold"
"text",
x=fracture_strain - 0.012,
y=fracture_stress_pt + 28,
label="Fracture",
size=3.0,
color=INK,
fontweight="bold",
)
# Region labels
+ geom_text(df_regions, aes(x="strain", y="stress", label="label"), size=3.0, color=INK_SOFT, fontstyle="italic")
# Elastic modulus annotation
+ annotate(
"text", x=0.028, y=145, label=f"E = {youngs_modulus // 1000} GPa", size=3.5, color=IMPRINT[2], fontweight="bold"
)
+ labs(
x="Engineering Strain",
y="Engineering Stress (MPa)",
title="line-stress-strain · python · plotnine · anyplot.ai",
)
+ labs(x="Engineering Strain", y="Engineering Stress (MPa)", title="line-stress-strain · plotnine · pyplots.ai")
+ scale_x_continuous(breaks=np.arange(0, 0.40, 0.05))
+ scale_y_continuous(breaks=np.arange(0, 500, 50))
# Coordinate control - plotnine-distinctive
+ coord_cartesian(xlim=(0, 0.38), ylim=(0, 460))
+ theme_minimal()
+ theme(
figure_size=(16, 9),
plot_title=element_text(size=26, weight="bold", color="#1A2530"),
axis_title=element_text(size=22, color="#2C3E50", weight="bold"),
axis_text=element_text(size=16, color="#555555"),
figure_size=(8, 4.5),
text=element_text(size=7),
plot_title=element_text(size=12, weight="bold", color=INK),
axis_title=element_text(size=10, color=INK, weight="bold"),
axis_text=element_text(size=8, color=INK_SOFT),
panel_grid_major=element_line(color=INK, size=0.3, alpha=0.15),
panel_grid_major_x=element_blank(),
panel_grid_minor=element_blank(),
panel_grid_major_y=element_line(color="#E8E8E8", size=0.4, alpha=0.5),
plot_background=element_rect(fill="white", color="white"),
panel_background=element_rect(fill="white", color="white"),
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
panel_background=element_rect(fill=PAGE_BG),
panel_border=element_rect(color=INK_SOFT, fill=None),
axis_line=element_line(color=INK_SOFT),
)
)

# Save
plot.save("plot.png", dpi=300, verbose=False)
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)
Loading
Loading