Engineering Stress-Strain Curve — MUI X Charts

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 MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// line-stress-strain: Engineering Stress-Strain Curve
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 88/100 | Created: 2026-08-24
import { LineChart } from "@mui/x-charts/LineChart";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import { ChartsText } from "@mui/x-charts/ChartsText";
import { useXScale, useYScale, useDrawingArea } from "@mui/x-charts/hooks";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// --- Material model: mild-steel tensile test (deterministic) ---------------
const E = 200000; // Young's modulus, MPa
const OFFSET = 0.002; // 0.2% strain offset for yield determination
const YIELD_STRESS = 250; // MPa, yield-plateau stress
const ELASTIC_END = YIELD_STRESS / E; // proportional limit, ≈ 0.00125
const YIELD_STRAIN = OFFSET + YIELD_STRESS / E; // where the offset line meets the plateau, ≈ 0.00325
const PLATEAU_END = 0.02; // Lüders strain — plateau gives way to strain hardening
const UTS_STRAIN = 0.2;
const UTS_STRESS = 450;
const FRACTURE_STRAIN = 0.26;
const FRACTURE_STRESS = 300;
const OFFSET_EXIT_STRAIN = OFFSET + (UTS_STRESS + 50) / E; // where the offset line exits the plot area
const Y_MAX = UTS_STRESS + 60;

// Small fixed-seed LCG — Lüders-band serration on the yield plateau
let seed = 42;
function rand() {
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}

function linspace(a, b, n) {
  const out = [];
  for (let i = 0; i < n; i += 1) out.push(a + ((b - a) * i) / (n - 1));
  return out;
}

// --- Strain samples (170 pts): dense near the elastic/yield transition,
// plus the two 0.2%-offset construction points ------------------------------
const strains = [
  ...linspace(0, ELASTIC_END, 6),
  ...linspace(ELASTIC_END, YIELD_STRAIN, 6).slice(1),
  ...linspace(YIELD_STRAIN, PLATEAU_END, 24).slice(1),
  ...linspace(PLATEAU_END, UTS_STRAIN, 90).slice(1),
  ...linspace(UTS_STRAIN, FRACTURE_STRAIN, 46).slice(1),
  OFFSET,
  OFFSET_EXIT_STRAIN,
].sort((a, b) => a - b);

function stressElasticYield(strain) {
  if (strain <= ELASTIC_END) return (strain / ELASTIC_END) * YIELD_STRESS;
  if (strain <= PLATEAU_END) return YIELD_STRESS + (rand() - 0.5) * 6;
  return null;
}

function stressHardening(strain) {
  if (strain < PLATEAU_END || strain > UTS_STRAIN) return null;
  const frac = (strain - PLATEAU_END) / (UTS_STRAIN - PLATEAU_END);
  return YIELD_STRESS + (UTS_STRESS - YIELD_STRESS) * (2 * frac - frac * frac);
}

function stressNecking(strain) {
  if (strain < UTS_STRAIN || strain > FRACTURE_STRAIN) return null;
  const frac = (strain - UTS_STRAIN) / (FRACTURE_STRAIN - UTS_STRAIN);
  return UTS_STRESS - (UTS_STRESS - FRACTURE_STRESS) * (3 * frac * frac - 2 * frac * frac * frac);
}

function stressOffsetLine(strain) {
  if (strain < OFFSET || strain > OFFSET_EXIT_STRAIN) return null;
  return E * (strain - OFFSET);
}

const dataset = strains.map((strain) => ({
  strain,
  stressElasticYield: stressElasticYield(strain),
  stressHardening: stressHardening(strain),
  stressNecking: stressNecking(strain),
  stressOffset: stressOffsetLine(strain),
}));

// Region-label anchor points, in data coordinates
const HARDENING_LABEL_X = 0.11;
const HARDENING_LABEL_Y = stressHardening(HARDENING_LABEL_X) + 45;
const NECKING_LABEL_X = 0.23;
const NECKING_LABEL_Y = stressNecking(NECKING_LABEL_X) + 45;

// --- Floating annotation text, placed in data coordinates -------------------
function DataLabel({ x, y, text, fill, fontSize = 15, anchor = "middle" }) {
  const xScale = useXScale("strain-axis");
  const yScale = useYScale("stress-axis");
  return (
    <ChartsText
      x={xScale(x)}
      y={yScale(y)}
      text={text}
      fill={fill}
      style={{ fontSize, textAnchor: anchor, dominantBaseline: "middle" }}
    />
  );
}

// Critical-point marker: solid dot with a page-bg edge, drawn above the
// reference lines so it stays visible where a line crosses the curve.
function PointMarker({ x, y, color }) {
  const xScale = useXScale("strain-axis");
  const yScale = useYScale("stress-axis");
  return <circle cx={xScale(x)} cy={yScale(y)} r={10} fill={color} stroke={t.pageBg} strokeWidth={3} />;
}

// Hand-rolled y-axis title, anchored to a fixed distance from the canvas edge
// rather than MUI X's built-in `yAxis.label` (whose offset is derived from
// `tickFontSize`, not the actual rendered width of 3-digit tick values, so it
// collides with the tick-label column for this data range).
function YAxisTitle({ text }) {
  const { top, height } = useDrawingArea();
  return (
    <ChartsText
      x={26}
      y={top + height / 2}
      text={text}
      fill={t.ink}
      style={{ fontSize: 16, angle: -90, textAnchor: "middle", dominantBaseline: "auto" }}
    />
  );
}

// --- Chart -------------------------------------------------------------
export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;
  const TITLE_HEIGHT = 60;

  return (
    <Box sx={{ width, height, display: "flex", flexDirection: "column" }}>
      <Box sx={{ height: TITLE_HEIGHT, display: "flex", alignItems: "center", justifyContent: "center" }}>
        <Typography sx={{ color: t.ink, fontSize: 22, fontWeight: 500, letterSpacing: 0.3 }}>
          line-stress-strain · javascript · muix · anyplot.ai
        </Typography>
      </Box>

      <LineChart
        width={width}
        height={height - TITLE_HEIGHT}
        dataset={dataset}
        skipAnimation
        xAxis={[
          {
            id: "strain-axis",
            dataKey: "strain",
            scaleType: "linear",
            label: "Engineering strain, ε (mm/mm)",
            min: 0,
            max: FRACTURE_STRAIN * 1.05,
            valueFormatter: (v) => v.toFixed(2),
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
            labelStyle: { fontSize: 16, fill: t.ink },
          },
        ]}
        yAxis={[
          {
            id: "stress-axis",
            min: 0,
            max: Y_MAX,
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
          },
        ]}
        series={[
          {
            id: "elasticYield",
            dataKey: "stressElasticYield",
            label: "Elastic + yield",
            color: t.palette[0],
            curve: "linear",
            connectNulls: true,
            showMark: false,
          },
          {
            id: "hardening",
            dataKey: "stressHardening",
            label: "Strain hardening",
            color: t.palette[1],
            curve: "monotoneX",
            connectNulls: true,
            showMark: false,
          },
          {
            id: "necking",
            dataKey: "stressNecking",
            label: "Necking",
            color: t.palette[4],
            curve: "monotoneX",
            connectNulls: true,
            showMark: false,
          },
          {
            id: "offsetLine",
            dataKey: "stressOffset",
            label: "0.2% offset",
            color: t.ink,
            curve: "linear",
            connectNulls: true,
            showMark: false,
          },
        ]}
        margin={{ top: 30, bottom: 90, left: 116, right: 40 }}
        sx={{
          "& .MuiLineElement-root": { strokeWidth: 3.5 },
          "& .MuiLineElement-series-offsetLine": { strokeWidth: 1.75, strokeDasharray: "10 6" },
          "& .MuiChartsAxis-line": { stroke: t.inkSoft },
          "& .MuiChartsAxis-tick": { stroke: t.inkSoft },
        }}
        slotProps={{
          legend: {
            position: { vertical: "top", horizontal: "right" },
            itemMarkWidth: 16,
            itemMarkHeight: 3,
            labelStyle: { fontSize: 14, fill: t.ink },
          },
        }}
      >
        <YAxisTitle text="Engineering stress, σ (MPa)" />

        <ChartsReferenceLine
          x={YIELD_STRAIN}
          label="Yield ≈ 250 MPa"
          labelAlign="start"
          spacing={{ x: 8, y: 16 }}
          lineStyle={{ stroke: t.inkSoft, strokeDasharray: "5 4", strokeWidth: 1.25 }}
          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}
        />
        <ChartsReferenceLine
          x={UTS_STRAIN}
          label="UTS ≈ 450 MPa"
          labelAlign="start"
          spacing={{ x: 8, y: 16 }}
          lineStyle={{ stroke: t.inkSoft, strokeDasharray: "5 4", strokeWidth: 1.25 }}
          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}
        />
        <ChartsReferenceLine
          x={FRACTURE_STRAIN}
          label="Fracture ≈ 300 MPa"
          labelAlign="start"
          spacing={{ x: 8, y: 16 }}
          lineStyle={{ stroke: t.inkSoft, strokeDasharray: "5 4", strokeWidth: 1.25 }}
          labelStyle={{ fill: t.inkSoft, fontSize: 13, textAnchor: "end" }}
        />

        <DataLabel x={0.012} y={130} text="Elastic + Yield" fill={t.palette[0]} anchor="start" />
        <DataLabel x={HARDENING_LABEL_X} y={HARDENING_LABEL_Y} text="Plastic (Strain Hardening)" fill={t.palette[1]} />
        <DataLabel x={NECKING_LABEL_X} y={NECKING_LABEL_Y} text="Necking" fill={t.palette[4]} />
        <DataLabel x={0.05} y={Y_MAX - 25} text="E ≈ 200 GPa (elastic modulus)" fill={t.inkSoft} anchor="start" fontSize={14} />

        <PointMarker x={YIELD_STRAIN} y={YIELD_STRESS} color={t.palette[0]} />
        <PointMarker x={UTS_STRAIN} y={UTS_STRESS} color={t.palette[1]} />
        <PointMarker x={FRACTURE_STRAIN} y={FRACTURE_STRESS} color={t.palette[4]} />
      </LineChart>
    </Box>
  );
}

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

Other implementations