{"spec_id":"line-stress-strain","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// line-stress-strain: Engineering Stress-Strain Curve\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst theme = window.ANYPLOT_THEME;\nconst inkMuted = theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\n\n// --- Data: synthetic tensile test for mild steel (in-memory, deterministic) -\nconst E = 200000; // MPa, Young's modulus\nconst proportionalStrain = 0.00125;\nconst proportionalStress = E * proportionalStrain; // 250 MPa\nconst plateauEndStrain = 0.02;\nconst utsStrain = 0.22;\nconst utsStress = 400; // MPa\nconst fractureStrain = 0.3;\nconst fractureStress = 320; // MPa\n\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst curve = [];\nconst N = 300;\nfor (let i = 0; i <= N; i++) {\n  const strain = (fractureStrain * i) / N;\n  let stress;\n  if (strain <= proportionalStrain) {\n    stress = E * strain;\n  } else if (strain <= plateauEndStrain) {\n    // Lüders plateau: near-flat with a slight upward drift and small noise\n    const frac =\n      (strain - proportionalStrain) / (plateauEndStrain - proportionalStrain);\n    stress = proportionalStress + 8 * frac + (rand() - 0.5) * 4;\n  } else if (strain <= utsStrain) {\n    // Strain hardening: power-law rise toward UTS\n    const frac = (strain - plateauEndStrain) / (utsStrain - plateauEndStrain);\n    stress =\n      proportionalStress +\n      8 +\n      (utsStress - proportionalStress - 8) * Math.pow(frac, 0.45);\n  } else {\n    // Necking: softening toward fracture\n    const frac = (strain - utsStrain) / (fractureStrain - utsStrain);\n    stress = utsStress - (utsStress - fractureStress) * Math.pow(frac, 1.6);\n  }\n  curve.push({ x: strain, y: Math.max(stress, 0) });\n}\n\n// 0.2% offset line: parallel to the elastic slope, shifted by 0.002 strain.\n// Its intersection with the curve is the yield point (offset method).\nconst offsetShift = 0.002;\nlet yieldPoint = curve[curve.length - 1];\nfor (const p of curve) {\n  if (p.x <= offsetShift) continue;\n  const offsetStress = E * (p.x - offsetShift);\n  if (offsetStress >= p.y) {\n    yieldPoint = p;\n    break;\n  }\n}\nconst offsetLine = [\n  { x: offsetShift, y: 0 },\n  { x: yieldPoint.x, y: yieldPoint.y },\n];\n\nconst utsPoint = { x: utsStrain, y: utsStress };\nconst fracturePoint = { x: fractureStrain, y: fractureStress };\n\n// --- Layout: main curve (left) + zoomed elastic/yield detail (right) -------\nconst container = document.getElementById(\"container\");\ncontainer.style.display = \"flex\";\ncontainer.style.flexDirection = \"row\";\ncontainer.style.background = t.pageBg;\ncontainer.style.boxSizing = \"border-box\";\n\nfunction addPanel(flexGrow) {\n  const wrap = document.createElement(\"div\");\n  wrap.style.cssText = `position:relative;flex:${flexGrow};width:100%;min-width:0;overflow:hidden`;\n  container.appendChild(wrap);\n  const canvas = document.createElement(\"canvas\");\n  wrap.appendChild(canvas);\n  return canvas;\n}\n\nconst mainCanvas = addPanel(7);\nconst zoomCanvas = addPanel(3);\n\n// --- Text-callout plugin (core Chart.js plugin, no external deps) ----------\nfunction calloutPlugin(callouts) {\n  return {\n    id: \"callouts\",\n    afterDraw(chart) {\n      const { ctx, scales } = chart;\n      ctx.save();\n      ctx.font = \"13px sans-serif\";\n      ctx.textBaseline = \"middle\";\n      for (const c of callouts) {\n        const px = scales.x.getPixelForValue(c.x);\n        const py = scales.y.getPixelForValue(c.y);\n        ctx.fillStyle = c.color;\n        ctx.textAlign = c.align || \"left\";\n        ctx.fillText(c.text, px + (c.dx || 10), py + (c.dy || 0));\n      }\n      ctx.restore();\n    },\n  };\n}\n\nfunction regionLabelsPlugin() {\n  return {\n    id: \"regionLabels\",\n    afterDraw(chart) {\n      const { ctx, scales, chartArea } = chart;\n      ctx.save();\n      ctx.font = \"600 15px sans-serif\";\n      ctx.fillStyle = t.inkSoft;\n      ctx.textBaseline = \"top\";\n\n      ctx.textAlign = \"left\";\n      ctx.fillText(\n        \"Elastic\",\n        scales.x.getPixelForValue(0.006),\n        chartArea.top + 8,\n      );\n\n      const plasticCenter = (plateauEndStrain + utsStrain) / 2;\n      ctx.textAlign = \"center\";\n      ctx.fillText(\n        \"Plastic (strain hardening)\",\n        scales.x.getPixelForValue(plasticCenter),\n        chartArea.top + 8,\n      );\n\n      const neckingCenter = (utsStrain + fractureStrain) / 2;\n      ctx.fillText(\n        \"Necking\",\n        scales.x.getPixelForValue(neckingCenter),\n        chartArea.top + 8,\n      );\n      ctx.restore();\n    },\n  };\n}\n\n// --- Main panel: full-range curve with UTS + fracture ----------------------\nconst mainCallouts = [\n  {\n    x: utsPoint.x,\n    y: utsPoint.y,\n    text: \"UTS\",\n    color: t.ink,\n    align: \"center\",\n    dx: 0,\n    dy: -20,\n  },\n  {\n    x: fracturePoint.x,\n    y: fracturePoint.y,\n    text: \"Fracture\",\n    color: t.palette[4],\n    align: \"right\",\n    dx: -14,\n    dy: -14,\n  },\n];\n\nnew Chart(mainCanvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: \"Mild Steel — Stress-Strain\",\n        data: curve,\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        borderWidth: 3.5,\n        pointRadius: 0,\n        tension: 0,\n      },\n      {\n        label: \"Ultimate Tensile Strength (UTS)\",\n        data: [utsPoint],\n        showLine: false,\n        pointStyle: \"circle\",\n        pointRadius: 8,\n        pointBackgroundColor: t.ink,\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 2,\n      },\n      {\n        label: \"Fracture Point\",\n        data: [fracturePoint],\n        showLine: false,\n        pointStyle: \"crossRot\",\n        pointRadius: 10,\n        pointBorderWidth: 3,\n        pointBackgroundColor: t.palette[4],\n        pointBorderColor: t.palette[4],\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 28, right: 12 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"line-stress-strain · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { top: 4, bottom: 16 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: { color: t.ink, font: { size: 13 }, boxWidth: 24, padding: 16 },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: fractureStrain * 1.03,\n        border: { display: false },\n        grid: { color: t.grid },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (v) => Math.round(v * 100) + \"%\",\n        },\n        title: {\n          display: true,\n          text: \"Engineering Strain\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n      y: {\n        min: 0,\n        max: utsStress * 1.15,\n        border: { display: false },\n        grid: { color: t.grid },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        title: {\n          display: true,\n          text: \"Engineering Stress (MPa)\",\n          color: t.ink,\n          font: { size: 16 },\n        },\n      },\n    },\n  },\n  plugins: [regionLabelsPlugin(), calloutPlugin(mainCallouts)],\n});\n\n// --- Zoom panel: elastic region + 0.2% offset yield construction -----------\nconst ZOOM_X_MAX = 0.03;\nconst ZOOM_Y_MAX = 320;\nconst zoomCallouts = [\n  {\n    x: proportionalStrain * 0.7,\n    y: E * proportionalStrain * 0.7,\n    text: \"E ≈ 200 GPa\",\n    color: t.ink,\n    dx: 60,\n    dy: -6,\n  },\n  {\n    x: offsetShift + (yieldPoint.x - offsetShift) / 2,\n    y: yieldPoint.y / 2,\n    text: \"0.2% offset\",\n    color: inkMuted,\n    dx: 10,\n    dy: 0,\n  },\n  {\n    x: yieldPoint.x,\n    y: yieldPoint.y,\n    text: \"Yield (0.2% offset)\",\n    color: t.amber,\n    dx: 10,\n    dy: -20,\n  },\n];\n\nnew Chart(zoomCanvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: \"Stress-Strain (detail)\",\n        data: curve.filter((p) => p.x <= ZOOM_X_MAX * 1.2),\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        borderWidth: 3.5,\n        pointRadius: 0,\n        tension: 0,\n      },\n      {\n        label: \"0.2% Offset Line\",\n        data: offsetLine,\n        borderColor: inkMuted,\n        backgroundColor: \"transparent\",\n        borderWidth: 2,\n        borderDash: [8, 5],\n        pointRadius: 0,\n      },\n      {\n        label: \"Yield Point (0.2% Offset)\",\n        data: [yieldPoint],\n        showLine: false,\n        pointStyle: \"triangle\",\n        pointRadius: 9,\n        pointBackgroundColor: t.amber,\n        pointBorderColor: t.amber,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 28, left: 8 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"Elastic Region & Yield Detail\",\n        color: t.inkSoft,\n        font: { size: 16, weight: \"500\" },\n        padding: { top: 4, bottom: 16 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: ZOOM_X_MAX,\n        border: { display: false },\n        grid: { color: t.grid },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          callback: (v) => (v * 100).toFixed(1) + \"%\",\n        },\n        title: {\n          display: true,\n          text: \"Strain\",\n          color: t.ink,\n          font: { size: 14 },\n        },\n      },\n      y: {\n        min: 0,\n        max: ZOOM_Y_MAX,\n        border: { display: false },\n        grid: { color: t.grid },\n        ticks: { color: t.inkSoft, font: { size: 13 } },\n        title: {\n          display: true,\n          text: \"Stress (MPa)\",\n          color: t.ink,\n          font: { size: 14 },\n        },\n      },\n    },\n  },\n  plugins: [calloutPlugin(zoomCallouts)],\n});\n"}