{"spec_id":"line-stress-strain","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-stress-strain: Engineering Stress-Strain Curve\nLibrary: pygal 3.1.3 | Python 3.13.15\nQuality: 89/100 | Created: 2026-08-24\n\"\"\"\n\nimport os\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette (first series always brand green) + semantic anchors.\n# Series color order below matches the exact chart.add() call order further down\n# (pygal has no per-series color override, so this is how each series gets its hue).\nANYPLOT_NEUTRAL = INK  # baseline / reference-line role (theme-adaptive)\nANYPLOT_MUTED = INK_MUTED  # secondary construction-line role (theme-adaptive)\nUTS_COLOR = \"#BD8233\"  # distinct Imprint hue so UTS reads apart from the Yield dot without the legend\nSERIES_COLORS = (\"#009E73\", \"#C475FD\", \"#4467A3\", ANYPLOT_NEUTRAL, ANYPLOT_MUTED, ANYPLOT_NEUTRAL, UTS_COLOR, \"#AE3030\")\n\n# Data - mild steel tensile test: elastic modulus, 0.2% offset yield, UTS, necking to fracture\nnp.random.seed(42)\nYOUNGS_MODULUS = 200_000  # MPa (200 GPa)\nELASTIC_LIMIT_STRAIN = 250.0 / YOUNGS_MODULUS  # end of the linear region, at 250 MPa\nYIELD_STRAIN, YIELD_STRESS = 0.00325, 250.0  # 0.2% offset method: E * (strain - 0.002) = stress\nUTS_STRAIN, UTS_STRESS = 0.21, 400.0\nFRACTURE_STRAIN, FRACTURE_STRESS = 0.30, 330.0\nPLATEAU_STRAIN_END = 0.02  # Luders plateau before strain hardening ramps up\n\nelastic_strain = np.linspace(0, ELASTIC_LIMIT_STRAIN, 20)\nelastic_stress = YOUNGS_MODULUS * elastic_strain\n\nplateau_strain = np.linspace(ELASTIC_LIMIT_STRAIN, PLATEAU_STRAIN_END, 40)\nplateau_stress = YIELD_STRESS + np.random.normal(0, 1.5, plateau_strain.size)\n\nhardening_strain = np.linspace(PLATEAU_STRAIN_END, UTS_STRAIN, 140)\nhardening_progress = (hardening_strain - PLATEAU_STRAIN_END) / (UTS_STRAIN - PLATEAU_STRAIN_END)\nhardening_stress = YIELD_STRESS + (UTS_STRESS - YIELD_STRESS) * hardening_progress**0.4\nhardening_stress += np.random.normal(0, 1.5, hardening_strain.size)\n\nplastic_strain = np.concatenate([plateau_strain, hardening_strain])\nplastic_stress = np.concatenate([plateau_stress, hardening_stress])\n\nnecking_strain = np.linspace(UTS_STRAIN, FRACTURE_STRAIN, 60)\nnecking_progress = (necking_strain - UTS_STRAIN) / (FRACTURE_STRAIN - UTS_STRAIN)\nnecking_stress = UTS_STRESS - (UTS_STRESS - FRACTURE_STRESS) * necking_progress**1.3\nnecking_stress += np.random.normal(0, 1.5, necking_strain.size)\n\n# Construction lines for the 0.2% offset yield method (both capped at the same stress\n# so the parallel, 0.002-strain-shifted geometry reads clearly)\nconstruction_cap = 300.0\ntangent_strain = (0, construction_cap / YOUNGS_MODULUS)\noffset_strain = (0.002, construction_cap / YOUNGS_MODULUS + 0.002)\n\n# Title - fontsize scales down only if the string exceeds the ~67-char baseline\ntitle = \"line-stress-strain · python · pygal · anyplot.ai\"\ntitle_font_size = 66 if len(title) <= 67 else max(44, round(66 * 67 / len(title)))\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=SERIES_COLORS,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=40,\n    value_font_size=36,\n    stroke_width=5,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Engineering Strain (mm/mm)\",\n    y_title=\"Engineering Stress (MPa)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    show_x_guides=False,\n    show_y_guides=True,\n    show_dots=False,\n    margin=110,\n    margin_top=500,  # reserves a clear gutter below the title for the zoomed offset inset (see Step: Save)\n    margin_bottom=260,\n    margin_left=210,\n    margin_right=90,\n    xrange=(0, 0.33),\n    range=(0, 440),\n    x_labels_major_count=7,\n    show_minor_x_labels=False,\n    truncate_legend=-1,\n)\nchart.x_labels = [round(v, 2) for v in np.linspace(0, 0.30, 7)]\n\n# Curve, split into the three labeled mechanical regions\nchart.add(\"Elastic\", list(zip(elastic_strain, elastic_stress, strict=True)), stroke_style={\"width\": 5})\nchart.add(\n    \"Plastic (Strain Hardening)\", list(zip(plastic_strain, plastic_stress, strict=True)), stroke_style={\"width\": 5}\n)\nchart.add(\"Necking\", list(zip(necking_strain, necking_stress, strict=True)), stroke_style={\"width\": 5})\n\n# 0.2% offset construction: elastic-modulus tangent + its 0.002-strain-shifted twin\nchart.add(\n    \"Elastic Modulus (E ≈ 200 GPa)\",\n    [(tangent_strain[0], 0), (tangent_strain[1], construction_cap)],\n    stroke_style={\"width\": 4, \"dasharray\": \"18, 14\"},\n)\nchart.add(\n    \"0.2% Offset Line\",\n    [(offset_strain[0], 0), (offset_strain[1], construction_cap)],\n    stroke_style={\"width\": 4, \"dasharray\": \"18, 14\"},\n)\n\n# Critical points\nchart.add(\n    f\"Yield Point (0.2% Offset) — {YIELD_STRESS:.0f} MPa\",\n    [(YIELD_STRAIN, YIELD_STRESS)],\n    show_dots=True,\n    dots_size=11,\n    stroke=False,\n)\nchart.add(\n    f\"Ultimate Tensile Strength — {UTS_STRESS:.0f} MPa\",\n    [(UTS_STRAIN, UTS_STRESS)],\n    show_dots=True,\n    dots_size=11,\n    stroke=False,\n)\nchart.add(\n    f\"Fracture Point — {FRACTURE_STRESS:.0f} MPa\",\n    [(FRACTURE_STRAIN, FRACTURE_STRESS)],\n    show_dots=True,\n    dots_size=11,\n    stroke=False,\n)\n\n# Zoomed inset: the elastic-modulus tangent + 0.2% offset line are a ~0.35%-of-axis\n# sliver on the main linear strain axis (yield strain 0.0033 vs. fracture strain 0.30),\n# so pygal's own XY chart can't render them as distinguishable geometry at this scale.\n# Hand-draw a small self-contained zoom panel (own axes, ticks, curve) directly as SVG\n# markup and splice it into the reserved gutter below the title (margin_top=500 above).\nINSET_X, INSET_Y, INSET_W, INSET_H = 260, 160, 900, 360\nINSET_X_MAX = offset_strain[1] + 0.0004  # small headroom past the offset line's top end\nINSET_Y_MAX = construction_cap + 20\nFONT_STACK = \"Consolas, 'Liberation Mono', Menlo, Courier, monospace\"\n\n\ndef _inset_point(strain, stress):\n    x = INSET_X + (strain / INSET_X_MAX) * INSET_W\n    y = INSET_Y + INSET_H - (stress / INSET_Y_MAX) * INSET_H\n    return x, y\n\n\ndef _inset_polyline(points, color, width, dasharray=None):\n    path = \" \".join(f\"{x:.2f},{y:.2f}\" for x, y in (_inset_point(s, v) for s, v in points))\n    dash = f' stroke-dasharray=\"{dasharray}\"' if dasharray else \"\"\n    return f'<polyline points=\"{path}\" fill=\"none\" stroke=\"{color}\" stroke-width=\"{width}\"{dash} />'\n\n\ndef _build_offset_inset_svg():\n    parts = [f'<g class=\"offset-inset\" font-family=\"{FONT_STACK}\">']\n    parts.append(\n        f'<rect x=\"{INSET_X}\" y=\"{INSET_Y}\" width=\"{INSET_W}\" height=\"{INSET_H}\" '\n        f'fill=\"{PAGE_BG}\" stroke=\"{INK_MUTED}\" stroke-width=\"2\" />'\n    )\n    parts.append(\n        f'<text x=\"{INSET_X + INSET_W / 2:.2f}\" y=\"{INSET_Y - 18}\" text-anchor=\"middle\" '\n        f'fill=\"{INK}\" font-size=\"32\">Zoom: Elastic Modulus &amp; 0.2% Offset Region</text>'\n    )\n\n    # Y ticks/gridlines (stress)\n    for y_val in (0, 100, 200, 300):\n        _, py = _inset_point(0, y_val)\n        parts.append(\n            f'<line x1=\"{INSET_X}\" y1=\"{py:.2f}\" x2=\"{INSET_X + INSET_W}\" y2=\"{py:.2f}\" stroke=\"{INK_MUTED}\" stroke-width=\"1\" stroke-opacity=\"0.35\" />'\n        )\n        parts.append(\n            f'<text x=\"{INSET_X - 12}\" y=\"{py + 8:.2f}\" text-anchor=\"end\" fill=\"{INK_MUTED}\" font-size=\"24\">{y_val}</text>'\n        )\n\n    # X ticks (strain)\n    for x_val in (0.0, 0.001, 0.002, 0.003):\n        px, _ = _inset_point(x_val, 0)\n        parts.append(\n            f'<line x1=\"{px:.2f}\" y1=\"{INSET_Y}\" x2=\"{px:.2f}\" y2=\"{INSET_Y + INSET_H}\" stroke=\"{INK_MUTED}\" stroke-width=\"1\" stroke-opacity=\"0.2\" />'\n        )\n        parts.append(\n            f'<text x=\"{px:.2f}\" y=\"{INSET_Y + INSET_H + 30}\" text-anchor=\"middle\" fill=\"{INK_MUTED}\" font-size=\"24\">{x_val:.3f}</text>'\n        )\n\n    # Elastic curve + the leading sliver of the plateau, in the same colors as the main chart\n    parts.append(_inset_polyline(zip(elastic_strain, elastic_stress, strict=True), \"#009E73\", 4))\n    plateau_mask = plateau_strain <= INSET_X_MAX\n    parts.append(\n        _inset_polyline(zip(plateau_strain[plateau_mask], plateau_stress[plateau_mask], strict=True), \"#C475FD\", 4)\n    )\n\n    # 0.2% offset construction, matching the main chart's dash + colors\n    parts.append(\n        _inset_polyline([(tangent_strain[0], 0), (tangent_strain[1], construction_cap)], ANYPLOT_NEUTRAL, 3, \"12,10\")\n    )\n    parts.append(\n        _inset_polyline([(offset_strain[0], 0), (offset_strain[1], construction_cap)], ANYPLOT_MUTED, 3, \"12,10\")\n    )\n\n    # Yield point marker\n    yx, yy = _inset_point(YIELD_STRAIN, YIELD_STRESS)\n    parts.append(f'<circle cx=\"{yx:.2f}\" cy=\"{yy:.2f}\" r=\"9\" fill=\"{ANYPLOT_NEUTRAL}\" />')\n\n    parts.append(f'<text x=\"{INSET_X + 12}\" y=\"{INSET_Y + 30}\" fill=\"{INK_MUTED}\" font-size=\"24\">Stress (MPa)</text>')\n    parts.append(\n        f'<text x=\"{INSET_X + INSET_W - 12}\" y=\"{INSET_Y + INSET_H - 14}\" text-anchor=\"end\" '\n        f'fill=\"{INK_MUTED}\" font-size=\"24\">Strain (mm/mm)</text>'\n    )\n    parts.append(\"</g>\")\n    return \"\".join(parts)\n\n\n# Save: splice the hand-drawn inset into pygal's SVG before rasterizing/exporting so\n# both the PNG and the interactive HTML carry the same zoomed offset-construction panel.\nsvg_markup = chart.render().decode(\"utf-8\")\nsvg_markup = svg_markup.replace(\"</svg>\", _build_offset_inset_svg() + \"</svg>\")\n\ncairosvg.svg2png(\n    bytestring=svg_markup.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=3200, output_height=1800\n)\n\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(svg_markup.encode(\"utf-8\"))\n"}