Engineering Stress-Strain Curve — Bokeh

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 Bokeh

Renders

Python source (Bokeh)

""" anyplot.ai
line-stress-strain: Engineering Stress-Strain Curve
Library: bokeh 3.9.2 | Python 3.13.15
Quality: 93/100 | Updated: 2026-08-17
"""

import os
import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import Arrow, BoxAnnotation, ColumnDataSource, HoverTool, Label, Legend, LegendItem, Span, VeeHead
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Theme
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Imprint palette — first series always brand green
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
color_main = IMPRINT_PALETTE[0]  # brand green — main stress-strain curve
color_yield = "#DDCC77"  # amber anchor — yield point / offset line (warning/threshold semantics)
color_uts = IMPRINT_PALETTE[1]  # lavender — ultimate tensile strength
color_fracture = IMPRINT_PALETTE[4]  # matte red — fracture point (failure semantics)
color_region = INK_SOFT

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

youngs_modulus = 210000  # MPa
yield_strength = 250  # MPa
uts = 400  # MPa
fracture_strain = 0.35
uts_strain = 0.22
yield_strain = yield_strength / youngs_modulus  # ~0.00119

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

# Yield plateau and strain hardening (Ludwik-type power law)
strain_plastic = np.linspace(yield_strain, uts_strain, 200)
plastic_strain = strain_plastic - yield_strain
stress_plastic = yield_strength + (uts - yield_strength) * (plastic_strain / (uts_strain - yield_strain)) ** 0.45

# Necking region (stress decreases after UTS)
strain_necking = np.linspace(uts_strain, fracture_strain, 80)
necking_progress = (strain_necking - uts_strain) / (fracture_strain - uts_strain)
stress_necking = uts - (uts - 320) * necking_progress**0.8

# Combine all regions
strain = np.concatenate([strain_elastic, strain_plastic, strain_necking])
stress = np.concatenate([stress_elastic, stress_plastic, stress_necking])

# Region labels for hover tooltip
region_labels = np.concatenate(
    [
        np.full(len(strain_elastic), "Elastic"),
        np.full(len(strain_plastic), "Strain Hardening"),
        np.full(len(strain_necking), "Necking"),
    ]
)

# 0.2% offset line — extended to be clearly visible
offset_strain_start = 0.002
offset_strain_end = 0.004 + yield_strength / youngs_modulus
offset_strain_line = np.linspace(offset_strain_start, offset_strain_end, 80)
offset_stress_line = youngs_modulus * (offset_strain_line - 0.002)
mask = offset_stress_line <= yield_strength + 30
offset_strain_line = offset_strain_line[mask]
offset_stress_line = offset_stress_line[mask]

# Key points
yield_point_strain = yield_strain + 0.002
yield_point_stress = yield_strength
uts_point_strain = uts_strain
uts_point_stress = uts
fracture_point_strain = fracture_strain
fracture_point_stress = stress_necking[-1]

# Elastic-slope reference line — the ideal E*strain line extended past the
# actual yield point, so the near-vertical elastic segment reads as a
# deliberate slope reference rather than only a text annotation
elastic_ref_stress_end = 320
elastic_ref_strain = np.array([0.0, elastic_ref_stress_end / youngs_modulus])
elastic_ref_stress = np.array([0.0, elastic_ref_stress_end])

# Plot
title = "line-stress-strain · python · bokeh · anyplot.ai"
p = figure(
    width=3200,
    height=1800,
    title=title,
    x_axis_label="Engineering Strain (mm/mm)",
    y_axis_label="Engineering Stress (MPa)",
    toolbar_location=None,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=50,
)

# Necking zone — lightly shaded background band (underlay, behind the curve)
# so the fracture-bound region reads as the visual focal point of the story
p.add_layout(
    BoxAnnotation(
        left=uts_strain,
        right=fracture_strain,
        fill_color=color_fracture,
        fill_alpha=0.07,
        line_color=None,
        level="underlay",
    )
)

# Subtle horizontal reference lines at yield and UTS
p.add_layout(
    Span(
        location=yield_strength,
        dimension="width",
        line_color=color_yield,
        line_alpha=0.3,
        line_dash="dotted",
        line_width=2,
    )
)
p.add_layout(
    Span(location=uts, dimension="width", line_color=color_uts, line_alpha=0.3, line_dash="dotted", line_width=2)
)

# Main curve with region data for HoverTool
source = ColumnDataSource(data={"strain": strain, "stress": stress, "region": region_labels})
main_line = p.line(x="strain", y="stress", source=source, line_width=6, color=color_main)

# HoverTool — Bokeh-distinctive interactive feature
hover = HoverTool(
    renderers=[main_line],
    tooltips=[("Strain", "@strain{0.0000}"), ("Stress", "@stress{0.1} MPa"), ("Region", "@region")],
    mode="vline",
    line_policy="nearest",
)
p.add_tools(hover)

# 0.2% offset line
offset_source = ColumnDataSource(data={"strain": offset_strain_line, "stress": offset_stress_line})
offset_line = p.line(x="strain", y="stress", source=offset_source, line_width=4, line_dash="dashed", color=color_yield)

# Elastic-slope reference line — dashed, extending the E*strain line so the
# elastic modulus has a visible slope reference in addition to the text label
elastic_ref_line = p.line(
    x=elastic_ref_strain, y=elastic_ref_stress, line_width=3, line_dash="dashed", line_alpha=0.55, color=color_main
)

# Key points — sized for 3200x1800 canvas
yield_glyph = p.scatter(
    x=[yield_point_strain],
    y=[yield_point_stress],
    size=26,
    color=color_yield,
    marker="circle",
    line_color=PAGE_BG,
    line_width=3,
)

uts_glyph = p.scatter(
    x=[uts_point_strain],
    y=[uts_point_stress],
    size=26,
    color=color_uts,
    marker="triangle",
    line_color=PAGE_BG,
    line_width=3,
)

fracture_glyph = p.scatter(
    x=[fracture_point_strain],
    y=[fracture_point_stress],
    size=26,
    color=color_fracture,
    marker="square",
    line_color=PAGE_BG,
    line_width=3,
)

# Region labels — "Elastic" sits near the y-axis, next to the slope
# reference line, with a leader arrow pointing at the actual (compressed)
# near-vertical elastic segment so the label unambiguously names it
p.add_layout(
    Label(x=0.022, y=395, text="Elastic", text_font_size="24pt", text_color=color_region, text_font_style="italic")
)
p.add_layout(
    Arrow(
        end=VeeHead(size=14, fill_color=color_region, line_color=color_region),
        x_start=0.020,
        y_start=380,
        x_end=0.0013,
        y_end=225,
        line_color=color_region,
        line_alpha=0.7,
        line_width=2,
    )
)

p.add_layout(
    Label(
        x=0.10, y=300, text="Strain Hardening", text_font_size="24pt", text_color=color_region, text_font_style="italic"
    )
)

p.add_layout(
    Label(x=0.27, y=380, text="Necking", text_font_size="24pt", text_color=color_region, text_font_style="italic")
)

# Key point annotations — spread out to avoid left-side crowding
p.add_layout(
    Label(
        x=yield_point_strain + 0.02,
        y=yield_point_stress - 55,
        text=f"Yield Point ({yield_point_stress} MPa)",
        text_font_size="20pt",
        text_color=color_yield,
        text_font_style="bold",
    )
)

p.add_layout(
    Label(
        x=uts_point_strain - 0.075,
        y=uts_point_stress + 18,
        text=f"UTS ({uts_point_stress} MPa)",
        text_font_size="20pt",
        text_color=color_uts,
        text_font_style="bold",
    )
)

p.add_layout(
    Label(
        x=fracture_point_strain - 0.045,
        y=fracture_point_stress - 45,
        text="Fracture",
        text_font_size="20pt",
        text_color=color_fracture,
        text_font_style="bold",
    )
)

# Young's modulus annotation — stacked below the "Elastic" label, beside the
# dashed slope-reference line it describes
p.add_layout(
    Label(
        x=0.022,
        y=340,
        text=f"E = {youngs_modulus // 1000} GPa",
        text_font_size="20pt",
        text_color=color_main,
        text_font_style="bold",
    )
)

# Legend — elevated box, positioned lower-right away from curve congestion
legend = Legend(
    items=[
        LegendItem(label="Stress-Strain Curve", renderers=[main_line]),
        LegendItem(label="Elastic Slope (E)", renderers=[elastic_ref_line]),
        LegendItem(label="0.2% Offset Line", renderers=[offset_line]),
        LegendItem(label="Yield Point", renderers=[yield_glyph]),
        LegendItem(label="Ultimate Tensile Strength", renderers=[uts_glyph]),
        LegendItem(label="Fracture Point", renderers=[fracture_glyph]),
    ],
    location=(2350, 550),
)
legend.label_text_font_size = "20pt"
legend.label_text_color = INK_SOFT
legend.glyph_height = 34
legend.glyph_width = 34
legend.spacing = 12
legend.padding = 24
legend.margin = 24
legend.background_fill_color = ELEVATED_BG
legend.background_fill_alpha = 0.95
legend.border_line_color = INK_SOFT
legend.border_line_alpha = 0.4
legend.border_line_width = 2
p.add_layout(legend, "center")

# Theme-adaptive chrome
p.title.text_font_size = "50pt"
p.title.text_font_style = "normal"
p.title.text_color = INK
p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT
p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None

p.xgrid.grid_line_alpha = 0.15
p.ygrid.grid_line_alpha = 0.15
p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK

p.outline_line_color = None
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG

p.y_range.start = -10
p.y_range.end = 450
p.x_range.start = -0.005
p.x_range.end = 0.38

# Save — write HTML, then screenshot with headless Chrome (export_png's chromedriver
# probe fails on this box; see prompts/library/bokeh.md)
output_file(f"plot-{THEME}.html", title=title)
save(p)

W, H = 3200, 1800
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
driver.execute_cdp_cmd(
    "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
)
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/line-stress-strain/bokeh/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "line-stress-strain",
  "language": "python",
  "library": "bokeh",
  "page": "https://anyplot.ai/line-stress-strain/python/bokeh",
  "hub": "https://anyplot.ai/line-stress-strain",
  "code_json": "https://api.anyplot.ai/specs/line-stress-strain/bokeh/code",
  "spec_json": "https://api.anyplot.ai/specs/line-stress-strain",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/bokeh/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/bokeh/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/bokeh/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/bokeh/plot-dark.html",
  "quality_score": 93.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

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

Other implementations