{"spec_id":"line-stress-strain","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-stress-strain: Engineering Stress-Strain Curve\nLibrary: altair 6.2.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-21\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\nfrom PIL import Image\n\n\n# Remove the script directory from sys.path so `altair` resolves to the installed package, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\nnp = importlib.import_module(\"numpy\")\npd = importlib.import_module(\"pandas\")\n\n# Theme tokens — Imprint palette + theme-adaptive chrome\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n# Imprint categorical palette — position 1 (brand green) is always the primary series\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # main stress-strain curve\n\n# Data — mild steel tensile test (realistic mechanical properties)\nnp.random.seed(42)\nyoungs_modulus = 210000  # MPa\nyield_strength = 250  # MPa\nuts = 400  # MPa (ultimate tensile strength)\nuts_strain = 0.22\nfracture_strain = 0.35\nyield_strain = yield_strength / youngs_modulus  # ≈ 0.00119\n\n# Build curve by region: elastic → yield plateau → strain hardening → necking\nn_e, n_p, n_h, n_n = 60, 20, 120, 80\nstrain_e = np.linspace(0, yield_strain, n_e)\nstrain_p = np.linspace(yield_strain, 0.02, n_p)\nstrain_h = np.linspace(0.02, uts_strain, n_h)\nstrain_n = np.linspace(uts_strain, fracture_strain, n_n)\n\nt_h = (strain_h - 0.02) / (uts_strain - 0.02)\nt_n = (strain_n - uts_strain) / (fracture_strain - uts_strain)\n\nstress_e = youngs_modulus * strain_e\nstress_p = np.full(n_p, yield_strength) + np.random.normal(0, 1.5, n_p)\nstress_h = yield_strength + (uts - yield_strength) * (1 - (1 - t_h) ** 0.45) + np.random.normal(0, 1.5, n_h)\nstress_n = uts - (uts - 280) * t_n**1.5 + np.random.normal(0, 1.5, n_n)\n\nstrain_all = np.concatenate([strain_e, strain_p, strain_h, strain_n])\nstress_all = np.concatenate([stress_e, stress_p, stress_h, stress_n])\ndf = pd.DataFrame({\"Strain\": strain_all, \"Stress\": stress_all})\n\n# 0.2% offset line — parallel to elastic region, offset by 0.002 strain\noffset = 0.002\noffset_end = yield_strain + offset + 0.003\noffset_df = pd.DataFrame({\"Strain\": [offset, offset_end], \"Stress\": [0.0, youngs_modulus * (offset_end - offset)]})\n\n# Young's modulus slope indicator (visible dashed line in elastic region)\nslope_df = pd.DataFrame({\"Strain\": [0.0, yield_strain * 0.75], \"Stress\": [0.0, youngs_modulus * yield_strain * 0.75]})\n\n# Critical key points\nuts_idx = int(np.argmax(stress_all))\nkey_points_df = pd.DataFrame(\n    {\n        \"Strain\": [offset + yield_strength / youngs_modulus, strain_all[uts_idx], strain_all[-1]],\n        \"Stress\": [float(yield_strength), float(stress_all[uts_idx]), float(stress_all[-1])],\n        \"Point\": [\"Yield Point\", \"UTS\", \"Fracture\"],\n    }\n)\nkp = key_points_df.set_index(\"Point\")\n\n# Imprint point colors: lavender (yield), ochre (UTS peak), matte red (fracture = semantic failure)\npoint_scale = alt.Scale(\n    domain=[\"Yield Point\", \"UTS\", \"Fracture\"], range=[IMPRINT_PALETTE[1], IMPRINT_PALETTE[3], IMPRINT_PALETTE[4]]\n)\n\n# Label positions — displaced away from crowded elastic zone near the y-axis\nlabel_df = pd.DataFrame(\n    {\n        \"Strain\": [0.09, kp.loc[\"UTS\", \"Strain\"] + 0.02, kp.loc[\"Fracture\", \"Strain\"] - 0.005],\n        \"Stress\": [\n            kp.loc[\"Yield Point\", \"Stress\"] + 60,\n            kp.loc[\"UTS\", \"Stress\"] + 30,\n            kp.loc[\"Fracture\", \"Stress\"] + 35,\n        ],\n        \"text\": [\"Yield Point\\n(0.2% offset)\", f\"UTS ({uts} MPa)\", \"Fracture\"],\n        \"Point\": [\"Yield Point\", \"UTS\", \"Fracture\"],\n    }\n)\n\n# Connector lines from displaced labels to their key points\nconnector_df = pd.DataFrame(\n    {\n        \"Strain\": [kp.loc[\"Yield Point\", \"Strain\"], 0.085, kp.loc[\"UTS\", \"Strain\"], kp.loc[\"UTS\", \"Strain\"] + 0.017],\n        \"Stress\": [\n            kp.loc[\"Yield Point\", \"Stress\"],\n            kp.loc[\"Yield Point\", \"Stress\"] + 50,\n            kp.loc[\"UTS\", \"Stress\"],\n            kp.loc[\"UTS\", \"Stress\"] + 22,\n        ],\n        \"group\": [0, 0, 1, 1],\n    }\n)\n\n# Region labels and subtle vertical separators\nregion_df = pd.DataFrame(\n    {\"Strain\": [0.005, 0.11, 0.30], \"Stress\": [440, 440, 440], \"text\": [\"Elastic\", \"Strain Hardening\", \"Necking\"]}\n)\nregion_sep_df = pd.DataFrame(\n    {\"Strain\": [0.02, 0.02, uts_strain, uts_strain], \"Stress\": [425, 455, 425, 455], \"sep\": [0, 0, 1, 1]}\n)\n\n# Modulus annotation connector: slope midpoint → text label\nmodulus_conn_df = pd.DataFrame(\n    {\"Strain\": [yield_strain * 0.5, 0.023], \"Stress\": [youngs_modulus * yield_strain * 0.5, 108]}\n)\n\n# Shared axis scales\nx_scale = alt.Scale(domain=[-0.01, 0.38])\ny_scale = alt.Scale(domain=[-10, 460])\n\n# Title — 49 chars, under the 67-char baseline (no font-size scaling needed)\ntitle_str = \"line-stress-strain · python · altair · anyplot.ai\"\n\n# Nearest-point hover selection (enhances the interactive HTML experience)\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"Strain\"], empty=False)\n\n# Chart layers\ncurve = (\n    alt.Chart(df)\n    .mark_line(strokeWidth=3, color=BRAND)\n    .encode(\n        x=alt.X(\"Strain:Q\", scale=x_scale, title=\"Engineering Strain\"),\n        y=alt.Y(\"Stress:Q\", scale=y_scale, title=\"Engineering Stress (MPa)\"),\n        tooltip=[alt.Tooltip(\"Strain:Q\", format=\".4f\"), alt.Tooltip(\"Stress:Q\", format=\".1f\", title=\"Stress (MPa)\")],\n    )\n)\n\nslope_line = (\n    alt.Chart(slope_df)\n    .mark_line(strokeWidth=2.5, strokeDash=[12, 4], color=BRAND)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale))\n)\n\noffset_line = (\n    alt.Chart(offset_df)\n    .mark_line(strokeWidth=2, strokeDash=[8, 6], color=INK_SOFT)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale))\n)\n\nconnectors = (\n    alt.Chart(connector_df)\n    .mark_line(strokeWidth=1, strokeDash=[3, 3], color=INK_MUTED)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale), detail=\"group:N\")\n)\n\nmodulus_conn = (\n    alt.Chart(modulus_conn_df)\n    .mark_line(strokeWidth=1, strokeDash=[3, 3], color=BRAND)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale))\n)\n\npoints = (\n    alt.Chart(key_points_df)\n    .mark_point(filled=True, size=450, stroke=\"white\", strokeWidth=2.5)\n    .encode(\n        x=alt.X(\"Strain:Q\", scale=x_scale),\n        y=alt.Y(\"Stress:Q\", scale=y_scale),\n        color=alt.Color(\"Point:N\", scale=point_scale, legend=None),\n        opacity=alt.condition(nearest, alt.value(1.0), alt.value(0.85)),\n        tooltip=[\n            alt.Tooltip(\"Point:N\"),\n            alt.Tooltip(\"Strain:Q\", format=\".4f\"),\n            alt.Tooltip(\"Stress:Q\", format=\".1f\", title=\"Stress (MPa)\"),\n        ],\n    )\n    .add_params(nearest)\n)\n\npoint_labels = (\n    alt.Chart(label_df)\n    .mark_text(fontSize=17, fontWeight=\"bold\", lineBreak=\"\\n\")\n    .encode(\n        x=alt.X(\"Strain:Q\", scale=x_scale),\n        y=alt.Y(\"Stress:Q\", scale=y_scale),\n        text=\"text:N\",\n        color=alt.Color(\"Point:N\", scale=point_scale, legend=None),\n    )\n)\n\nregion_text = (\n    alt.Chart(region_df)\n    .mark_text(fontSize=16, fontStyle=\"italic\", fontWeight=500, color=INK_MUTED)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale), text=\"text:N\")\n)\n\nregion_seps = (\n    alt.Chart(region_sep_df)\n    .mark_line(strokeWidth=0.8, strokeDash=[4, 3], color=INK_MUTED)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale), detail=\"sep:N\")\n)\n\nmodulus_label = pd.DataFrame({\"Strain\": [0.025], \"Stress\": [100], \"text\": [f\"E = {youngs_modulus:,} MPa\"]})\nmodulus_text = (\n    alt.Chart(modulus_label)\n    .mark_text(fontSize=16, align=\"left\", fontWeight=\"bold\", color=BRAND)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale), text=\"text:N\")\n)\n\noffset_label = pd.DataFrame({\"Strain\": [offset + 0.005], \"Stress\": [40], \"text\": [\"0.2% offset\"]})\noffset_text = (\n    alt.Chart(offset_label)\n    .mark_text(fontSize=15, align=\"left\", fontStyle=\"italic\", color=INK_SOFT)\n    .encode(x=alt.X(\"Strain:Q\", scale=x_scale), y=alt.Y(\"Stress:Q\", scale=y_scale), text=\"text:N\")\n)\n\n# Combine all layers\nchart = (\n    alt.layer(\n        curve,\n        slope_line,\n        offset_line,\n        connectors,\n        modulus_conn,\n        points,\n        point_labels,\n        region_seps,\n        region_text,\n        modulus_text,\n        offset_text,\n    )\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(\n            title_str,\n            fontSize=16,\n            subtitle=\"Engineering stress-strain response: elastic, strain hardening, and necking regions\",\n            subtitleFontSize=12,\n            subtitleColor=INK_MUTED,\n            anchor=\"start\",\n            offset=12,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        grid=False,\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n    .interactive()\n)\n\n# Save PNG with canvas-size enforcement (3200 × 1800 landscape target)\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _bg_hex = PAGE_BG.lstrip(\"#\")\n    _bg_rgb = (int(_bg_hex[0:2], 16), int(_bg_hex[2:4], 16), int(_bg_hex[4:6], 16))\n    _canvas = Image.new(\"RGB\", (TW, TH), _bg_rgb)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}