Engineering Stress-Strain Curve — lets-plot

An engineering stress-strain curve visualizes the relationship between applied stress (MPa) and resulting strain (dimensionless) in a material under uniaxial tensile loading. The curve reveals distinct mechanical behavior regions — elastic deformation, yielding, strain hardening, and necking — culminating in fracture. It is the foundational plot for characterizing material mechanical properties and comparing material performance.

Engineering Stress-Strain Curve rendered with lets-plot

Python source (lets-plot)

""" pyplots.ai
line-stress-strain: Engineering Stress-Strain Curve
Library: letsplot 4.9.0 | Python 3.14.3
Quality: 92/100 | Created: 2026-03-20
"""

import numpy as np
import pandas as pd
from lets_plot import *
from lets_plot.export import ggsave as export_ggsave


LetsPlot.setup_html()

# Data - Mild steel tensile test simulation
np.random.seed(42)

# Material properties for mild steel
youngs_modulus = 210000  # MPa
yield_strength = 250  # MPa
uts = 400  # MPa (ultimate tensile strength)
fracture_strain = 0.35
uts_strain = 0.22
yield_strain = yield_strength / youngs_modulus  # ~0.00119

# Elastic region (0 to yield)
n_elastic = 60
strain_elastic = np.linspace(0, yield_strain, n_elastic)
stress_elastic = youngs_modulus * strain_elastic

# Yield plateau (mild steel has a distinct yield point)
n_plateau = 20
strain_plateau = np.linspace(yield_strain, 0.015, n_plateau)
stress_plateau = yield_strength + np.random.normal(0, 1.5, n_plateau)

# Strain hardening region (from end of plateau to UTS)
n_hardening = 120
strain_hardening = np.linspace(0.015, uts_strain, n_hardening)
stress_hardening = yield_strength + (uts - yield_strength) * (
    1 - np.exp(-8 * (strain_hardening - 0.015) / (uts_strain - 0.015))
)
stress_hardening += np.random.normal(0, 1.0, n_hardening)

# Necking region (UTS to fracture)
n_necking = 60
strain_necking = np.linspace(uts_strain, fracture_strain, n_necking)
stress_necking = uts - (uts - 280) * ((strain_necking - uts_strain) / (fracture_strain - uts_strain)) ** 1.5
stress_necking += np.random.normal(0, 1.5, n_necking)

# Combine all regions
strain = np.concatenate([strain_elastic, strain_plateau, strain_hardening, strain_necking])
stress = np.concatenate([stress_elastic, stress_plateau, stress_hardening, stress_necking])

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

# 0.2% offset line for yield point determination
offset_val = 0.002
offset_line_strain = np.linspace(offset_val, offset_val + yield_strength / youngs_modulus + 0.003, 50)
offset_line_stress = youngs_modulus * (offset_line_strain - offset_val)
offset_line_stress = np.clip(offset_line_stress, 0, yield_strength + 30)
df_offset = pd.DataFrame({"strain": offset_line_strain, "stress": offset_line_stress})

# Key points
yield_point_strain = offset_val + yield_strength / youngs_modulus
yield_point_stress = yield_strength
fracture_stress = stress_necking[-1]

df_points = pd.DataFrame(
    {
        "strain": [yield_point_strain, uts_strain, fracture_strain],
        "stress": [yield_point_stress, uts, fracture_stress],
        "label": [f"Yield Point ({yield_strength} MPa)", f"UTS ({uts} MPa)", f"Fracture ({fracture_stress:.0f} MPa)"],
        "type": ["Yield", "UTS", "Fracture"],
    }
)

# Consolidated annotations DataFrame
df_annotations = pd.DataFrame(
    {
        "x": [yield_point_strain + 0.012, uts_strain + 0.015, fracture_strain - 0.045, 0.008, 0.007, 0.005, 0.11, 0.29],
        "y": [yield_point_stress + 15, uts + 10, fracture_stress - 30, 130, 60, 350, 350, 350],
        "label": [
            f"Yield Point\n({yield_strength} MPa)",
            f"UTS ({uts} MPa)",
            "Fracture",
            f"E = {youngs_modulus // 1000} GPa",
            "0.2% offset",
            "Elastic",
            "Strain Hardening",
            "Necking",
        ],
        "group": ["yield", "uts", "fracture", "modulus", "offset", "region", "region", "region"],
    }
)

# Colorblind-safe palette: blue, purple, gray (avoids orange/green pair)
color_yield = "#9467BD"  # purple
color_uts = "#D62728"  # red
color_fracture = "#7F7F7F"  # gray
color_main = "#306998"  # Python blue
color_offset = "#E377C2"  # pink

# Segment connector lines from key points to annotations (distinctive lets-plot feature)
df_segments = pd.DataFrame(
    {
        "x": [yield_point_strain, uts_strain, fracture_strain],
        "y": [yield_point_stress, uts, fracture_stress],
        "xend": [yield_point_strain + 0.011, uts_strain + 0.014, fracture_strain - 0.035],
        "yend": [yield_point_stress + 12, uts + 8, fracture_stress - 22],
    }
)

# Plot
plot = (
    ggplot()
    # Region background bands using geom_rect (distinctive lets-plot feature)
    + geom_rect(
        aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="region"),
        data=pd.DataFrame(
            {
                "xmin": [0, 0.015, uts_strain],
                "xmax": [0.015, uts_strain, fracture_strain],
                "ymin": [0, 0, 0],
                "ymax": [460, 460, 460],
                "region": ["Elastic", "Strain Hardening", "Necking"],
            }
        ),
        alpha=0.35,
    )
    + scale_fill_manual(
        values={
            "Elastic": "#DAE8FC",
            "Strain Hardening": "#FFF2CC",
            "Necking": "#F8D7DA",
            "Yield": color_yield,
            "UTS": color_uts,
            "Fracture": color_fracture,
        }
    )
    # Main stress-strain curve with tooltips (distinctive lets-plot feature)
    + geom_line(
        aes(x="strain", y="stress"),
        data=df,
        color=color_main,
        size=2.0,
        tooltips=layer_tooltips()
        .format("strain", ".4f")
        .format("stress", ".1f")
        .line("Strain: @strain")
        .line("Stress: @stress MPa"),
    )
    # 0.2% offset line
    + geom_line(aes(x="strain", y="stress"), data=df_offset, color=color_offset, size=1.2, linetype="dashed")
    # Segment connectors from points to labels (geom_segment - distinctive feature)
    + geom_segment(
        aes(x="x", y="y", xend="xend", yend="yend"), data=df_segments, color="#999999", size=0.6, linetype="dotted"
    )
    # Key points with tooltips (distinctive lets-plot feature)
    + geom_point(
        aes(x="strain", y="stress", fill="type"),
        data=df_points,
        color="white",
        size=7,
        shape=21,
        stroke=1.2,
        tooltips=layer_tooltips().line("@type").line("Strain: @strain").line("Stress: @stress MPa"),
    )
    + guides(fill="none")
    # Annotations - key points
    + geom_text(
        aes(x="x", y="y", label="label"),
        data=df_annotations.query("group == 'yield'"),
        size=11,
        color=color_yield,
        hjust=0,
    )
    + geom_text(
        aes(x="x", y="y", label="label"), data=df_annotations.query("group == 'uts'"), size=11, color=color_uts, hjust=0
    )
    + geom_text(
        aes(x="x", y="y", label="label"),
        data=df_annotations.query("group == 'fracture'"),
        size=11,
        color=color_fracture,
        hjust=0.5,
    )
    # Elastic modulus annotation
    + geom_text(
        aes(x="x", y="y", label="label"),
        data=df_annotations.query("group == 'modulus'"),
        size=10,
        color=color_main,
        hjust=0,
        fontface="italic",
    )
    # Offset label
    + geom_text(
        aes(x="x", y="y", label="label"),
        data=df_annotations.query("group == 'offset'"),
        size=9,
        color=color_offset,
        hjust=0,
        fontface="italic",
    )
    # Region labels
    + geom_text(
        aes(x="x", y="y", label="label"),
        data=df_annotations.query("group == 'region'"),
        size=12,
        color="#666666",
        fontface="italic",
    )
    # Styling
    + labs(
        x="Engineering Strain",
        y="Engineering Stress (MPa)",
        title="line-stress-strain \u00b7 letsplot \u00b7 pyplots.ai",
    )
    + scale_x_continuous(breaks=[0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35])
    + scale_y_continuous(breaks=[0, 50, 100, 150, 200, 250, 300, 350, 400, 450])
    + ggsize(1600, 900)
    + theme_minimal()
    + theme(
        axis_text=element_text(size=16, color="#555555"),
        axis_title=element_text(size=20, color="#333333"),
        plot_title=element_text(size=24, color="#222222", face="bold"),
        panel_grid_major_x=element_blank(),
        panel_grid_major_y=element_line(color="#E0E0E0", size=0.3),
        panel_grid_minor=element_blank(),
        plot_background=element_rect(fill="#FAFAFA", color="#FAFAFA"),
        panel_background=element_rect(fill="transparent", color="transparent"),
        axis_ticks=element_blank(),
        axis_ticks_length=0,
        plot_margin=[30, 40, 20, 20],
    )
)

# Save
export_ggsave(plot, filename="plot.png", path=".", scale=3)
export_ggsave(plot, filename="plot.html", path=".")

Part of Engineering Stress-Strain Curve on anyplot.ai.

Other implementations