{"spec_id":"line-stress-strain","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-stress-strain: Engineering Stress-Strain Curve\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 85/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 70, bottom: 100, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Material model (Al 6061-T6 tensile coupon, idealized) -----------------\n// Control points trace the classic shape: linear-elastic rise, a knee into\n// gradual strain hardening up to UTS, then softening (necking) to fracture.\nconst YOUNGS_MODULUS = 68900; // MPa\nconst CONTROL_STRAINS = [0, 0.0025, 0.004, 0.006, 0.01, 0.02, 0.04, 0.07, 0.09, 0.105, 0.12, 0.13, 0.14];\nconst CONTROL_STRESSES = [0, 172.25, 232, 271, 289, 299, 305, 308.5, 310, 304, 291, 277, 262];\nconst PROP_LIMIT_STRAIN = CONTROL_STRAINS[1];\nconst UTS_STRAIN = 0.09;\nconst UTS_STRESS = 310;\nconst FRACTURE_STRAIN = CONTROL_STRAINS[CONTROL_STRAINS.length - 1];\nconst FRACTURE_STRESS = CONTROL_STRESSES[CONTROL_STRESSES.length - 1];\nconst OFFSET_STRAIN = 0.002;\n\n// Cubic Hermite basis on frac in [0, 1].\nconst hermite = (frac, p0, p1, m0, m1) => {\n  const f2 = frac * frac;\n  const f3 = f2 * frac;\n  const h00 = 2 * f3 - 3 * f2 + 1;\n  const h10 = f3 - 2 * f2 + frac;\n  const h01 = -2 * f3 + 3 * f2;\n  const h11 = f3 - f2;\n  return h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1;\n};\n\n// Fritsch-Carlson monotone tangents: interpolates through every control\n// point smoothly without overshooting past neighboring values, including at\n// the UTS peak where the secant sign flips (tangent naturally goes to 0).\nconst monotoneTangents = (xs, ys) => {\n  const n = xs.length;\n  const secants = [];\n  for (let i = 0; i < n - 1; i++) secants.push((ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]));\n  const m = new Array(n);\n  m[0] = secants[0];\n  m[n - 1] = secants[n - 2];\n  for (let i = 1; i < n - 1; i++) {\n    m[i] = secants[i - 1] * secants[i] <= 0 ? 0 : (secants[i - 1] + secants[i]) / 2;\n  }\n  for (let i = 0; i < n - 1; i++) {\n    if (secants[i] === 0) {\n      m[i] = 0;\n      m[i + 1] = 0;\n      continue;\n    }\n    const a = m[i] / secants[i];\n    const b = m[i + 1] / secants[i];\n    const s = a * a + b * b;\n    if (s > 9) {\n      const tau = 3 / Math.sqrt(s);\n      m[i] = tau * a * secants[i];\n      m[i + 1] = tau * b * secants[i];\n    }\n  }\n  return m;\n};\n\nconst controlTangents = monotoneTangents(CONTROL_STRAINS, CONTROL_STRESSES);\n\nconst stressAt = (strain) => {\n  let i = 0;\n  while (i < CONTROL_STRAINS.length - 2 && strain > CONTROL_STRAINS[i + 1]) i++;\n  const span = CONTROL_STRAINS[i + 1] - CONTROL_STRAINS[i];\n  const frac = (strain - CONTROL_STRAINS[i]) / span;\n  return hermite(frac, CONTROL_STRESSES[i], CONTROL_STRESSES[i + 1], controlTangents[i] * span, controlTangents[i + 1] * span);\n};\n\n// --- Sample the curve (denser through the elastic/knee region) -------------\nconst SAMPLE_COUNT = 260;\nconst data = [];\nfor (let i = 0; i < SAMPLE_COUNT; i++) {\n  const frac = i / (SAMPLE_COUNT - 1);\n  const strain = FRACTURE_STRAIN * Math.pow(frac, 2.2);\n  data.push({ strain, stress: stressAt(strain) });\n}\n\n// --- 0.2% offset method: locate the yield point by bisection ---------------\nconst offsetStressAt = (strain) => YOUNGS_MODULUS * (strain - OFFSET_STRAIN);\nlet lo = PROP_LIMIT_STRAIN;\nlet hi = UTS_STRAIN;\nfor (let i = 0; i < 60; i++) {\n  const mid = (lo + hi) / 2;\n  if (stressAt(mid) - offsetStressAt(mid) > 0) lo = mid;\n  else hi = mid;\n}\nconst yieldPoint = { strain: (lo + hi) / 2, stress: stressAt((lo + hi) / 2) };\nconst utsPoint = { strain: UTS_STRAIN, stress: UTS_STRESS };\nconst fracturePoint = { strain: FRACTURE_STRAIN, stress: FRACTURE_STRESS };\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, FRACTURE_STRAIN * 1.08]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, UTS_STRESS * 1.18]).range([ih, 0]);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(7).tickFormat(d3.format(\".2f\")).tickSize(0).tickPadding(12));\nconst yAxis = g\n  .append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(\"d\")).tickSize(0).tickPadding(12));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// y-axis gridlines, subtle\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Region boundaries (elastic / strain hardening / necking) --------------\nconst regionDividers = [yieldPoint.strain, UTS_STRAIN];\ng.selectAll(\".region-divider\")\n  .data(regionDividers)\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"3,4\");\n\nconst regionLabelY = ih * 0.05;\ng.append(\"text\")\n  .attr(\"x\", 8)\n  .attr(\"y\", regionLabelY)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .style(\"letter-spacing\", \"0.02em\")\n  .text(\"ELASTIC\");\ng.append(\"text\")\n  .attr(\"x\", x((yieldPoint.strain + UTS_STRAIN) / 2))\n  .attr(\"y\", regionLabelY)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .style(\"letter-spacing\", \"0.02em\")\n  .text(\"STRAIN HARDENING\");\ng.append(\"text\")\n  .attr(\"x\", x((UTS_STRAIN + FRACTURE_STRAIN) / 2))\n  .attr(\"y\", regionLabelY)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .style(\"letter-spacing\", \"0.02em\")\n  .text(\"NECKING\");\n\n// --- Elastic modulus reference line (tangent at slope E) -------------------\n// Extended well past the proportional limit so the straight tangent visibly\n// separates from the actual (bending) curve instead of merging with it. The\n// label sits in the open plateau area rather than beside the tangent, which\n// stays narrow even when lengthened since E dwarfs the yield stress.\nconst modulusEnd = PROP_LIMIT_STRAIN * 1.6;\ng.append(\"line\")\n  .attr(\"x1\", x(0))\n  .attr(\"y1\", y(0))\n  .attr(\"x2\", x(modulusEnd))\n  .attr(\"y2\", y(YOUNGS_MODULUS * modulusEnd))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,4\")\n  .attr(\"opacity\", 0.55);\ng.append(\"text\")\n  .attr(\"x\", x(0.02))\n  .attr(\"y\", y(55))\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`E ≈ ${(YOUNGS_MODULUS / 1000).toFixed(0)} GPa (elastic slope)`);\n\n// --- 0.2% offset construction line ------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(OFFSET_STRAIN))\n  .attr(\"y1\", y(0))\n  .attr(\"x2\", x(yieldPoint.strain))\n  .attr(\"y2\", y(yieldPoint.stress))\n  .attr(\"stroke\", t.muted)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,4\");\ng.append(\"text\")\n  .attr(\"x\", x(OFFSET_STRAIN) + 6)\n  .attr(\"y\", y(0) - 12)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.muted)\n  .style(\"font-size\", \"14px\")\n  .text(\"0.2% offset\");\n\n// --- Stress-strain curve ------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.strain))\n  .y((d) => y(d.stress))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(data).attr(\"d\", line).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 4);\n\n// --- Critical points ------------------------------------------------------\nconst markerStyle = (selection) => selection.attr(\"r\", 9).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2.5);\n\ng.append(\"circle\")\n  .attr(\"cx\", x(yieldPoint.strain))\n  .attr(\"cy\", y(yieldPoint.stress))\n  .attr(\"fill\", t.palette[0])\n  .call(markerStyle);\ng.append(\"text\")\n  .attr(\"x\", x(yieldPoint.strain))\n  .attr(\"y\", y(yieldPoint.stress) - 66)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Yield (0.2% offset)\");\ng.append(\"text\")\n  .attr(\"x\", x(yieldPoint.strain))\n  .attr(\"y\", y(yieldPoint.stress) - 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`${Math.round(yieldPoint.stress)} MPa`);\n\ng.append(\"circle\").attr(\"cx\", x(utsPoint.strain)).attr(\"cy\", y(utsPoint.stress)).attr(\"fill\", t.palette[0]).call(markerStyle);\ng.append(\"text\")\n  .attr(\"x\", x(utsPoint.strain))\n  .attr(\"y\", y(utsPoint.stress) - 40)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"UTS\");\ng.append(\"text\")\n  .attr(\"x\", x(utsPoint.strain))\n  .attr(\"y\", y(utsPoint.stress) - 22)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`${Math.round(utsPoint.stress)} MPa`);\n\nconst fractureColor = t.palette[4]; // matte red — semantic anchor for failure\nconst fx = x(fracturePoint.strain);\nconst fy = y(fracturePoint.stress);\nconst crossSize = 9;\ng.append(\"line\").attr(\"x1\", fx - crossSize).attr(\"y1\", fy - crossSize).attr(\"x2\", fx + crossSize).attr(\"y2\", fy + crossSize).attr(\"stroke\", fractureColor).attr(\"stroke-width\", 3);\ng.append(\"line\").attr(\"x1\", fx - crossSize).attr(\"y1\", fy + crossSize).attr(\"x2\", fx + crossSize).attr(\"y2\", fy - crossSize).attr(\"stroke\", fractureColor).attr(\"stroke-width\", 3);\ng.append(\"text\")\n  .attr(\"x\", fx - 16)\n  .attr(\"y\", fy + 28)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Fracture\");\ng.append(\"text\")\n  .attr(\"x\", fx - 16)\n  .attr(\"y\", fy + 46)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`${Math.round(fracturePoint.stress)} MPa`);\n\n// --- Axis labels --------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Engineering strain (mm/mm)\");\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Engineering stress (MPa)\");\n\n// --- Title ------------------------------------------------------------------\nconst title = \"Al 6061-T6 Tensile Test · line-stress-strain · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(14, Math.round(22 * Math.min(1, 67 / title.length)));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}