Mohr's Circle for Stress Analysis — Chart.js

Mohr's Circle is a graphical method used to determine principal stresses, maximum shear stress, and stress transformations from a given 2D stress state. A circle is drawn on a normal stress (σ) vs. shear stress (τ) plane, with the center at ((σx + σy) / 2, 0) and a radius derived from the stress components. It is an essential tool in mechanical and civil engineering for visualizing how stress components change under coordinate rotation.

Mohr's Circle for Stress Analysis rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// mohr-circle: Mohr's Circle for Stress Analysis
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 92/100 | Created: 2026-08-26

//# anyplot-orientation: square
const t = window.ANYPLOT_TOKENS;

// --- Stress state (MPa) ------------------------------------------------------
const sigmaX = 80;
const sigmaY = 20;
const tauXY = 30;

const center = (sigmaX + sigmaY) / 2;
const radius = Math.sqrt(((sigmaX - sigmaY) / 2) ** 2 + tauXY ** 2);

const sigma1 = center + radius;
const sigma2 = center - radius;
const tauMax = radius;

const pointA = { x: sigmaX, y: tauXY };
const pointB = { x: sigmaY, y: -tauXY };

// Parametric circle outline, drawn as a line dataset.
const circlePoints = Array.from({ length: 145 }, (_, i) => {
  const angle = (i / 144) * 2 * Math.PI;
  return { x: center + radius * Math.cos(angle), y: radius * Math.sin(angle) };
});

const pad = radius * 0.4;
const axisMin = center - radius - pad;
const axisMax = center + radius + pad;
const axisHalfSpan = radius + pad;

// --- Mount -------------------------------------------------------------------
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);

// --- Custom plugins ----------------------------------------------------------
// Chart.js has no per-axis "equal aspect" option. Without one, a stress circle
// with different sigma/tau extents renders as an ellipse. This plugin measures
// pixels-per-unit on both axes after layout and expands the tighter axis's
// range to match, so the circle stays a true circle. Guarded by $aspectDone so
// the single corrective chart.update() it triggers doesn't recurse.
const equalAspectPlugin = {
  id: "equalAspect",
  afterLayout(chart) {
    if (chart.$aspectDone) return;
    const { chartArea, scales } = chart;
    const xs = scales.x;
    const ys = scales.y;
    const pxPerUnitX = chartArea.width / (xs.max - xs.min);
    const pxPerUnitY = chartArea.height / (ys.max - ys.min);
    if (pxPerUnitX < pxPerUnitY) {
      const yCenter = (ys.min + ys.max) / 2;
      const halfRange = chartArea.height / pxPerUnitX / 2;
      ys.options.min = yCenter - halfRange;
      ys.options.max = yCenter + halfRange;
    } else {
      const xCenter = (xs.min + xs.max) / 2;
      const halfRange = chartArea.width / pxPerUnitY / 2;
      xs.options.min = xCenter - halfRange;
      xs.options.max = xCenter + halfRange;
    }
    chart.$aspectDone = true;
    chart.update("none");
  },
};

// Chart.js core has no chart type for labeled reference points or angle arcs,
// so this plugin draws the Mohr's-circle-specific reading aids (point labels,
// the center marker, and the 2*theta_p arc from the reference axis to point A)
// directly on the canvas via the public afterDatasetsDraw hook.
const annotationPlugin = {
  id: "mohrAnnotations",
  afterDatasetsDraw(chart) {
    const { ctx, scales } = chart;
    const px = (v) => scales.x.getPixelForValue(v);
    const py = (v) => scales.y.getPixelForValue(v);
    const cx = px(center);
    const cy = py(0);
    const unitPx = Math.abs(px(center + 1) - cx);

    ctx.save();

    // Center marker
    ctx.beginPath();
    ctx.arc(cx, cy, 5, 0, 2 * Math.PI);
    ctx.fillStyle = t.inkSoft;
    ctx.fill();

    // Angle arc: reference axis (toward sigma1) -> radius to point A
    const arcRadiusPx = radius * 0.32 * unitPx;
    const angleRef = Math.atan2(py(0) - cy, px(sigma1) - cx);
    const angleA = Math.atan2(py(tauXY) - cy, px(sigmaX) - cx);
    const ccw = angleA < angleRef;
    ctx.beginPath();
    ctx.setLineDash([4, 3]);
    ctx.lineWidth = 2;
    ctx.strokeStyle = t.inkSoft;
    ctx.arc(cx, cy, arcRadiusPx, angleRef, angleA, ccw);
    ctx.stroke();
    ctx.setLineDash([]);

    const midAngle = (angleRef + angleA) / 2;
    ctx.font = "500 15px -apple-system, sans-serif";
    ctx.fillStyle = t.inkSoft;
    ctx.textAlign = "left";
    ctx.textBaseline = "middle";
    ctx.fillText("2θp", cx + Math.cos(midAngle) * (arcRadiusPx + 26), cy + Math.sin(midAngle) * (arcRadiusPx + 26));

    // Point labels
    ctx.font = "600 17px -apple-system, sans-serif";
    ctx.fillStyle = t.ink;

    ctx.textAlign = "left";
    ctx.textBaseline = "bottom";
    ctx.fillText("A", px(pointA.x) + 14, py(pointA.y) - 10);

    ctx.textAlign = "left";
    ctx.textBaseline = "top";
    ctx.fillText("B", px(pointB.x) + 14, py(pointB.y) + 10);

    ctx.textAlign = "center";
    ctx.textBaseline = "top";
    ctx.fillText("σ1", px(sigma1), cy + 16);
    ctx.fillText("σ2", px(sigma2), cy + 16);

    ctx.textAlign = "center";
    ctx.textBaseline = "bottom";
    ctx.fillText("τmax", px(center), py(tauMax) - 14);
    ctx.textBaseline = "top";
    ctx.fillText("τmin", px(center), py(-tauMax) + 14);

    ctx.restore();
  },
};

// --- Chart ---------------------------------------------------------------
new Chart(canvas, {
  type: "scatter",
  data: {
    datasets: [
      {
        label: "Mohr's circle",
        data: circlePoints,
        showLine: true,
        fill: false,
        tension: 0,
        borderColor: t.palette[0],
        borderWidth: 3,
        pointRadius: 0,
      },
      {
        label: "_diameter",
        data: [pointA, pointB],
        showLine: true,
        borderColor: t.inkSoft,
        borderWidth: 1.5,
        borderDash: [6, 5],
        pointRadius: 0,
      },
      {
        label: "_axis-h",
        data: [
          { x: axisMin, y: 0 },
          { x: axisMax, y: 0 },
        ],
        showLine: true,
        borderColor: t.grid,
        borderWidth: 1.5,
        pointRadius: 0,
      },
      {
        label: "_axis-v",
        data: [
          { x: center, y: -axisHalfSpan },
          { x: center, y: axisHalfSpan },
        ],
        showLine: true,
        borderColor: t.grid,
        borderWidth: 1.5,
        pointRadius: 0,
      },
      {
        label: "Stress points A, B",
        data: [pointA, pointB],
        pointRadius: 9,
        pointHoverRadius: 9,
        backgroundColor: t.palette[1],
        borderColor: t.pageBg,
        borderWidth: 2,
      },
      {
        label: "Principal stresses σ1, σ2",
        data: [
          { x: sigma1, y: 0 },
          { x: sigma2, y: 0 },
        ],
        pointRadius: 9,
        pointHoverRadius: 9,
        backgroundColor: t.palette[2],
        borderColor: t.pageBg,
        borderWidth: 2,
      },
      {
        label: "Max/min shear τmax",
        data: [
          { x: center, y: tauMax },
          { x: center, y: -tauMax },
        ],
        pointRadius: 9,
        pointHoverRadius: 9,
        backgroundColor: t.palette[3],
        borderColor: t.pageBg,
        borderWidth: 2,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    layout: { padding: { top: 30, right: 40, bottom: 10, left: 10 } },
    plugins: {
      title: {
        display: true,
        text: "mohr-circle · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 22, weight: "500" },
        padding: { bottom: 20 },
      },
      legend: {
        position: "bottom",
        labels: {
          color: t.ink,
          font: { size: 15 },
          usePointStyle: true,
          filter: (item) => !item.text.startsWith("_"),
        },
      },
      tooltip: {
        filter: (item) => !item.dataset.label.startsWith("_"),
      },
    },
    scales: {
      x: {
        type: "linear",
        min: axisMin,
        max: axisMax,
        title: { display: true, text: "Normal Stress σ (MPa)", color: t.ink, font: { size: 16 } },
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { color: t.grid },
      },
      y: {
        type: "linear",
        min: -axisHalfSpan,
        max: axisHalfSpan,
        title: { display: true, text: "Shear Stress τ (MPa)", color: t.ink, font: { size: 16 } },
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { color: t.grid },
      },
    },
  },
  plugins: [equalAspectPlugin, annotationPlugin],
});

Part of Mohr's Circle for Stress Analysis on anyplot.ai.

Other implementations