Engineering Stress-Strain Curve — Chart.js

An engineering stress-strain curve visualizes the relationship between applied stress (MPa) and resulting strain (dimensionless) in a material under uniaxial tensile loading. The curve reveals distinct mechanical behavior regions — elastic deformation, yielding, strain hardening, and necking — culminating in fracture. It is the foundational plot for characterizing material mechanical properties and comparing material performance.

Engineering Stress-Strain Curve rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// line-stress-strain: Engineering Stress-Strain Curve
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 90/100 | Created: 2026-08-24

const t = window.ANYPLOT_TOKENS;
const theme = window.ANYPLOT_THEME;
const inkMuted = theme === "light" ? "#6B6A63" : "#A8A79F";

// --- Data: synthetic tensile test for mild steel (in-memory, deterministic) -
const E = 200000; // MPa, Young's modulus
const proportionalStrain = 0.00125;
const proportionalStress = E * proportionalStrain; // 250 MPa
const plateauEndStrain = 0.02;
const utsStrain = 0.22;
const utsStress = 400; // MPa
const fractureStrain = 0.3;
const fractureStress = 320; // MPa

function lcg(seed) {
  let state = seed;
  return () => {
    state = (state * 1664525 + 1013904223) % 4294967296;
    return state / 4294967296;
  };
}
const rand = lcg(42);

const curve = [];
const N = 300;
for (let i = 0; i <= N; i++) {
  const strain = (fractureStrain * i) / N;
  let stress;
  if (strain <= proportionalStrain) {
    stress = E * strain;
  } else if (strain <= plateauEndStrain) {
    // Lüders plateau: near-flat with a slight upward drift and small noise
    const frac =
      (strain - proportionalStrain) / (plateauEndStrain - proportionalStrain);
    stress = proportionalStress + 8 * frac + (rand() - 0.5) * 4;
  } else if (strain <= utsStrain) {
    // Strain hardening: power-law rise toward UTS
    const frac = (strain - plateauEndStrain) / (utsStrain - plateauEndStrain);
    stress =
      proportionalStress +
      8 +
      (utsStress - proportionalStress - 8) * Math.pow(frac, 0.45);
  } else {
    // Necking: softening toward fracture
    const frac = (strain - utsStrain) / (fractureStrain - utsStrain);
    stress = utsStress - (utsStress - fractureStress) * Math.pow(frac, 1.6);
  }
  curve.push({ x: strain, y: Math.max(stress, 0) });
}

// 0.2% offset line: parallel to the elastic slope, shifted by 0.002 strain.
// Its intersection with the curve is the yield point (offset method).
const offsetShift = 0.002;
let yieldPoint = curve[curve.length - 1];
for (const p of curve) {
  if (p.x <= offsetShift) continue;
  const offsetStress = E * (p.x - offsetShift);
  if (offsetStress >= p.y) {
    yieldPoint = p;
    break;
  }
}
const offsetLine = [
  { x: offsetShift, y: 0 },
  { x: yieldPoint.x, y: yieldPoint.y },
];

const utsPoint = { x: utsStrain, y: utsStress };
const fracturePoint = { x: fractureStrain, y: fractureStress };

// --- Layout: main curve (left) + zoomed elastic/yield detail (right) -------
const container = document.getElementById("container");
container.style.display = "flex";
container.style.flexDirection = "row";
container.style.background = t.pageBg;
container.style.boxSizing = "border-box";

function addPanel(flexGrow) {
  const wrap = document.createElement("div");
  wrap.style.cssText = `position:relative;flex:${flexGrow};width:100%;min-width:0;overflow:hidden`;
  container.appendChild(wrap);
  const canvas = document.createElement("canvas");
  wrap.appendChild(canvas);
  return canvas;
}

const mainCanvas = addPanel(7);
const zoomCanvas = addPanel(3);

// --- Text-callout plugin (core Chart.js plugin, no external deps) ----------
function calloutPlugin(callouts) {
  return {
    id: "callouts",
    afterDraw(chart) {
      const { ctx, scales } = chart;
      ctx.save();
      ctx.font = "13px sans-serif";
      ctx.textBaseline = "middle";
      for (const c of callouts) {
        const px = scales.x.getPixelForValue(c.x);
        const py = scales.y.getPixelForValue(c.y);
        ctx.fillStyle = c.color;
        ctx.textAlign = c.align || "left";
        ctx.fillText(c.text, px + (c.dx || 10), py + (c.dy || 0));
      }
      ctx.restore();
    },
  };
}

function regionLabelsPlugin() {
  return {
    id: "regionLabels",
    afterDraw(chart) {
      const { ctx, scales, chartArea } = chart;
      ctx.save();
      ctx.font = "600 15px sans-serif";
      ctx.fillStyle = t.inkSoft;
      ctx.textBaseline = "top";

      ctx.textAlign = "left";
      ctx.fillText(
        "Elastic",
        scales.x.getPixelForValue(0.006),
        chartArea.top + 8,
      );

      const plasticCenter = (plateauEndStrain + utsStrain) / 2;
      ctx.textAlign = "center";
      ctx.fillText(
        "Plastic (strain hardening)",
        scales.x.getPixelForValue(plasticCenter),
        chartArea.top + 8,
      );

      const neckingCenter = (utsStrain + fractureStrain) / 2;
      ctx.fillText(
        "Necking",
        scales.x.getPixelForValue(neckingCenter),
        chartArea.top + 8,
      );
      ctx.restore();
    },
  };
}

// --- Main panel: full-range curve with UTS + fracture ----------------------
const mainCallouts = [
  {
    x: utsPoint.x,
    y: utsPoint.y,
    text: "UTS",
    color: t.ink,
    align: "center",
    dx: 0,
    dy: -20,
  },
  {
    x: fracturePoint.x,
    y: fracturePoint.y,
    text: "Fracture",
    color: t.palette[4],
    align: "right",
    dx: -14,
    dy: -14,
  },
];

new Chart(mainCanvas, {
  type: "line",
  data: {
    datasets: [
      {
        label: "Mild Steel — Stress-Strain",
        data: curve,
        borderColor: t.palette[0],
        backgroundColor: "transparent",
        borderWidth: 3.5,
        pointRadius: 0,
        tension: 0,
      },
      {
        label: "Ultimate Tensile Strength (UTS)",
        data: [utsPoint],
        showLine: false,
        pointStyle: "circle",
        pointRadius: 8,
        pointBackgroundColor: t.ink,
        pointBorderColor: t.pageBg,
        pointBorderWidth: 2,
      },
      {
        label: "Fracture Point",
        data: [fracturePoint],
        showLine: false,
        pointStyle: "crossRot",
        pointRadius: 10,
        pointBorderWidth: 3,
        pointBackgroundColor: t.palette[4],
        pointBorderColor: t.palette[4],
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    layout: { padding: { top: 28, right: 12 } },
    plugins: {
      title: {
        display: true,
        text: "line-stress-strain · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 22, weight: "500" },
        padding: { top: 4, bottom: 16 },
      },
      legend: {
        position: "bottom",
        labels: { color: t.ink, font: { size: 13 }, boxWidth: 24, padding: 16 },
      },
      tooltip: { enabled: false },
    },
    scales: {
      x: {
        type: "linear",
        min: 0,
        max: fractureStrain * 1.03,
        border: { display: false },
        grid: { color: t.grid },
        ticks: {
          color: t.inkSoft,
          font: { size: 14 },
          callback: (v) => Math.round(v * 100) + "%",
        },
        title: {
          display: true,
          text: "Engineering Strain",
          color: t.ink,
          font: { size: 16 },
        },
      },
      y: {
        min: 0,
        max: utsStress * 1.15,
        border: { display: false },
        grid: { color: t.grid },
        ticks: { color: t.inkSoft, font: { size: 14 } },
        title: {
          display: true,
          text: "Engineering Stress (MPa)",
          color: t.ink,
          font: { size: 16 },
        },
      },
    },
  },
  plugins: [regionLabelsPlugin(), calloutPlugin(mainCallouts)],
});

// --- Zoom panel: elastic region + 0.2% offset yield construction -----------
const ZOOM_X_MAX = 0.03;
const ZOOM_Y_MAX = 320;
const zoomCallouts = [
  {
    x: proportionalStrain * 0.7,
    y: E * proportionalStrain * 0.7,
    text: "E ≈ 200 GPa",
    color: t.ink,
    dx: 60,
    dy: -6,
  },
  {
    x: offsetShift + (yieldPoint.x - offsetShift) / 2,
    y: yieldPoint.y / 2,
    text: "0.2% offset",
    color: inkMuted,
    dx: 10,
    dy: 0,
  },
  {
    x: yieldPoint.x,
    y: yieldPoint.y,
    text: "Yield (0.2% offset)",
    color: t.amber,
    dx: 10,
    dy: -20,
  },
];

new Chart(zoomCanvas, {
  type: "line",
  data: {
    datasets: [
      {
        label: "Stress-Strain (detail)",
        data: curve.filter((p) => p.x <= ZOOM_X_MAX * 1.2),
        borderColor: t.palette[0],
        backgroundColor: "transparent",
        borderWidth: 3.5,
        pointRadius: 0,
        tension: 0,
      },
      {
        label: "0.2% Offset Line",
        data: offsetLine,
        borderColor: inkMuted,
        backgroundColor: "transparent",
        borderWidth: 2,
        borderDash: [8, 5],
        pointRadius: 0,
      },
      {
        label: "Yield Point (0.2% Offset)",
        data: [yieldPoint],
        showLine: false,
        pointStyle: "triangle",
        pointRadius: 9,
        pointBackgroundColor: t.amber,
        pointBorderColor: t.amber,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    layout: { padding: { top: 28, left: 8 } },
    plugins: {
      title: {
        display: true,
        text: "Elastic Region & Yield Detail",
        color: t.inkSoft,
        font: { size: 16, weight: "500" },
        padding: { top: 4, bottom: 16 },
      },
      legend: { display: false },
      tooltip: { enabled: false },
    },
    scales: {
      x: {
        type: "linear",
        min: 0,
        max: ZOOM_X_MAX,
        border: { display: false },
        grid: { color: t.grid },
        ticks: {
          color: t.inkSoft,
          font: { size: 13 },
          callback: (v) => (v * 100).toFixed(1) + "%",
        },
        title: {
          display: true,
          text: "Strain",
          color: t.ink,
          font: { size: 14 },
        },
      },
      y: {
        min: 0,
        max: ZOOM_Y_MAX,
        border: { display: false },
        grid: { color: t.grid },
        ticks: { color: t.inkSoft, font: { size: 13 } },
        title: {
          display: true,
          text: "Stress (MPa)",
          color: t.ink,
          font: { size: 14 },
        },
      },
    },
  },
  plugins: [calloutPlugin(zoomCallouts)],
});

Part of Engineering Stress-Strain Curve on anyplot.ai.

Other implementations