{"spec_id":"line-stress-strain","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nline-stress-strain: Engineering Stress-Strain Curve\nLibrary: bokeh 3.9.2 | Python 3.13.15\nQuality: 93/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import Arrow, BoxAnnotation, ColumnDataSource, HoverTool, Label, Legend, LegendItem, Span, VeeHead\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint palette — first series always brand green\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\ncolor_main = IMPRINT_PALETTE[0]  # brand green — main stress-strain curve\ncolor_yield = \"#DDCC77\"  # amber anchor — yield point / offset line (warning/threshold semantics)\ncolor_uts = IMPRINT_PALETTE[1]  # lavender — ultimate tensile strength\ncolor_fracture = IMPRINT_PALETTE[4]  # matte red — fracture point (failure semantics)\ncolor_region = INK_SOFT\n\n# Data — Mild steel tensile test simulation\nnp.random.seed(42)\n\nyoungs_modulus = 210000  # MPa\nyield_strength = 250  # MPa\nuts = 400  # MPa\nfracture_strain = 0.35\nuts_strain = 0.22\nyield_strain = yield_strength / youngs_modulus  # ~0.00119\n\n# Elastic region\nstrain_elastic = np.linspace(0, yield_strain, 60)\nstress_elastic = youngs_modulus * strain_elastic\n\n# Yield plateau and strain hardening (Ludwik-type power law)\nstrain_plastic = np.linspace(yield_strain, uts_strain, 200)\nplastic_strain = strain_plastic - yield_strain\nstress_plastic = yield_strength + (uts - yield_strength) * (plastic_strain / (uts_strain - yield_strain)) ** 0.45\n\n# Necking region (stress decreases after UTS)\nstrain_necking = np.linspace(uts_strain, fracture_strain, 80)\nnecking_progress = (strain_necking - uts_strain) / (fracture_strain - uts_strain)\nstress_necking = uts - (uts - 320) * necking_progress**0.8\n\n# Combine all regions\nstrain = np.concatenate([strain_elastic, strain_plastic, strain_necking])\nstress = np.concatenate([stress_elastic, stress_plastic, stress_necking])\n\n# Region labels for hover tooltip\nregion_labels = np.concatenate(\n    [\n        np.full(len(strain_elastic), \"Elastic\"),\n        np.full(len(strain_plastic), \"Strain Hardening\"),\n        np.full(len(strain_necking), \"Necking\"),\n    ]\n)\n\n# 0.2% offset line — extended to be clearly visible\noffset_strain_start = 0.002\noffset_strain_end = 0.004 + yield_strength / youngs_modulus\noffset_strain_line = np.linspace(offset_strain_start, offset_strain_end, 80)\noffset_stress_line = youngs_modulus * (offset_strain_line - 0.002)\nmask = offset_stress_line <= yield_strength + 30\noffset_strain_line = offset_strain_line[mask]\noffset_stress_line = offset_stress_line[mask]\n\n# Key points\nyield_point_strain = yield_strain + 0.002\nyield_point_stress = yield_strength\nuts_point_strain = uts_strain\nuts_point_stress = uts\nfracture_point_strain = fracture_strain\nfracture_point_stress = stress_necking[-1]\n\n# Elastic-slope reference line — the ideal E*strain line extended past the\n# actual yield point, so the near-vertical elastic segment reads as a\n# deliberate slope reference rather than only a text annotation\nelastic_ref_stress_end = 320\nelastic_ref_strain = np.array([0.0, elastic_ref_stress_end / youngs_modulus])\nelastic_ref_stress = np.array([0.0, elastic_ref_stress_end])\n\n# Plot\ntitle = \"line-stress-strain · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Engineering Strain (mm/mm)\",\n    y_axis_label=\"Engineering Stress (MPa)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Necking zone — lightly shaded background band (underlay, behind the curve)\n# so the fracture-bound region reads as the visual focal point of the story\np.add_layout(\n    BoxAnnotation(\n        left=uts_strain,\n        right=fracture_strain,\n        fill_color=color_fracture,\n        fill_alpha=0.07,\n        line_color=None,\n        level=\"underlay\",\n    )\n)\n\n# Subtle horizontal reference lines at yield and UTS\np.add_layout(\n    Span(\n        location=yield_strength,\n        dimension=\"width\",\n        line_color=color_yield,\n        line_alpha=0.3,\n        line_dash=\"dotted\",\n        line_width=2,\n    )\n)\np.add_layout(\n    Span(location=uts, dimension=\"width\", line_color=color_uts, line_alpha=0.3, line_dash=\"dotted\", line_width=2)\n)\n\n# Main curve with region data for HoverTool\nsource = ColumnDataSource(data={\"strain\": strain, \"stress\": stress, \"region\": region_labels})\nmain_line = p.line(x=\"strain\", y=\"stress\", source=source, line_width=6, color=color_main)\n\n# HoverTool — Bokeh-distinctive interactive feature\nhover = HoverTool(\n    renderers=[main_line],\n    tooltips=[(\"Strain\", \"@strain{0.0000}\"), (\"Stress\", \"@stress{0.1} MPa\"), (\"Region\", \"@region\")],\n    mode=\"vline\",\n    line_policy=\"nearest\",\n)\np.add_tools(hover)\n\n# 0.2% offset line\noffset_source = ColumnDataSource(data={\"strain\": offset_strain_line, \"stress\": offset_stress_line})\noffset_line = p.line(x=\"strain\", y=\"stress\", source=offset_source, line_width=4, line_dash=\"dashed\", color=color_yield)\n\n# Elastic-slope reference line — dashed, extending the E*strain line so the\n# elastic modulus has a visible slope reference in addition to the text label\nelastic_ref_line = p.line(\n    x=elastic_ref_strain, y=elastic_ref_stress, line_width=3, line_dash=\"dashed\", line_alpha=0.55, color=color_main\n)\n\n# Key points — sized for 3200x1800 canvas\nyield_glyph = p.scatter(\n    x=[yield_point_strain],\n    y=[yield_point_stress],\n    size=26,\n    color=color_yield,\n    marker=\"circle\",\n    line_color=PAGE_BG,\n    line_width=3,\n)\n\nuts_glyph = p.scatter(\n    x=[uts_point_strain],\n    y=[uts_point_stress],\n    size=26,\n    color=color_uts,\n    marker=\"triangle\",\n    line_color=PAGE_BG,\n    line_width=3,\n)\n\nfracture_glyph = p.scatter(\n    x=[fracture_point_strain],\n    y=[fracture_point_stress],\n    size=26,\n    color=color_fracture,\n    marker=\"square\",\n    line_color=PAGE_BG,\n    line_width=3,\n)\n\n# Region labels — \"Elastic\" sits near the y-axis, next to the slope\n# reference line, with a leader arrow pointing at the actual (compressed)\n# near-vertical elastic segment so the label unambiguously names it\np.add_layout(\n    Label(x=0.022, y=395, text=\"Elastic\", text_font_size=\"24pt\", text_color=color_region, text_font_style=\"italic\")\n)\np.add_layout(\n    Arrow(\n        end=VeeHead(size=14, fill_color=color_region, line_color=color_region),\n        x_start=0.020,\n        y_start=380,\n        x_end=0.0013,\n        y_end=225,\n        line_color=color_region,\n        line_alpha=0.7,\n        line_width=2,\n    )\n)\n\np.add_layout(\n    Label(\n        x=0.10, y=300, text=\"Strain Hardening\", text_font_size=\"24pt\", text_color=color_region, text_font_style=\"italic\"\n    )\n)\n\np.add_layout(\n    Label(x=0.27, y=380, text=\"Necking\", text_font_size=\"24pt\", text_color=color_region, text_font_style=\"italic\")\n)\n\n# Key point annotations — spread out to avoid left-side crowding\np.add_layout(\n    Label(\n        x=yield_point_strain + 0.02,\n        y=yield_point_stress - 55,\n        text=f\"Yield Point ({yield_point_stress} MPa)\",\n        text_font_size=\"20pt\",\n        text_color=color_yield,\n        text_font_style=\"bold\",\n    )\n)\n\np.add_layout(\n    Label(\n        x=uts_point_strain - 0.075,\n        y=uts_point_stress + 18,\n        text=f\"UTS ({uts_point_stress} MPa)\",\n        text_font_size=\"20pt\",\n        text_color=color_uts,\n        text_font_style=\"bold\",\n    )\n)\n\np.add_layout(\n    Label(\n        x=fracture_point_strain - 0.045,\n        y=fracture_point_stress - 45,\n        text=\"Fracture\",\n        text_font_size=\"20pt\",\n        text_color=color_fracture,\n        text_font_style=\"bold\",\n    )\n)\n\n# Young's modulus annotation — stacked below the \"Elastic\" label, beside the\n# dashed slope-reference line it describes\np.add_layout(\n    Label(\n        x=0.022,\n        y=340,\n        text=f\"E = {youngs_modulus // 1000} GPa\",\n        text_font_size=\"20pt\",\n        text_color=color_main,\n        text_font_style=\"bold\",\n    )\n)\n\n# Legend — elevated box, positioned lower-right away from curve congestion\nlegend = Legend(\n    items=[\n        LegendItem(label=\"Stress-Strain Curve\", renderers=[main_line]),\n        LegendItem(label=\"Elastic Slope (E)\", renderers=[elastic_ref_line]),\n        LegendItem(label=\"0.2% Offset Line\", renderers=[offset_line]),\n        LegendItem(label=\"Yield Point\", renderers=[yield_glyph]),\n        LegendItem(label=\"Ultimate Tensile Strength\", renderers=[uts_glyph]),\n        LegendItem(label=\"Fracture Point\", renderers=[fracture_glyph]),\n    ],\n    location=(2350, 550),\n)\nlegend.label_text_font_size = \"20pt\"\nlegend.label_text_color = INK_SOFT\nlegend.glyph_height = 34\nlegend.glyph_width = 34\nlegend.spacing = 12\nlegend.padding = 24\nlegend.margin = 24\nlegend.background_fill_color = ELEVATED_BG\nlegend.background_fill_alpha = 0.95\nlegend.border_line_color = INK_SOFT\nlegend.border_line_alpha = 0.4\nlegend.border_line_width = 2\np.add_layout(legend, \"center\")\n\n# Theme-adaptive chrome\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"normal\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\np.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.15\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\n\np.outline_line_color = None\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\np.y_range.start = -10\np.y_range.end = 450\np.x_range.start = -0.005\np.x_range.end = 0.38\n\n# Save — write HTML, then screenshot with headless Chrome (export_png's chromedriver\n# probe fails on this box; see prompts/library/bokeh.md)\noutput_file(f\"plot-{THEME}.html\", title=title)\nsave(p)\n\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}