{"spec_id":"mohr-circle","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// mohr-circle: Mohr's Circle for Stress Analysis\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Stress state (MPa) ------------------------------------------------------\nconst sigmaX = 80;\nconst sigmaY = 20;\nconst tauXY = 30;\n\nconst center = (sigmaX + sigmaY) / 2;\nconst radius = Math.sqrt(((sigmaX - sigmaY) / 2) ** 2 + tauXY ** 2);\n\nconst sigma1 = center + radius;\nconst sigma2 = center - radius;\nconst tauMax = radius;\n\nconst pointA = { x: sigmaX, y: tauXY };\nconst pointB = { x: sigmaY, y: -tauXY };\n\n// Parametric circle outline, drawn as a line dataset.\nconst circlePoints = Array.from({ length: 145 }, (_, i) => {\n  const angle = (i / 144) * 2 * Math.PI;\n  return { x: center + radius * Math.cos(angle), y: radius * Math.sin(angle) };\n});\n\nconst pad = radius * 0.4;\nconst axisMin = center - radius - pad;\nconst axisMax = center + radius + pad;\nconst axisHalfSpan = radius + pad;\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugins ----------------------------------------------------------\n// Chart.js has no per-axis \"equal aspect\" option. Without one, a stress circle\n// with different sigma/tau extents renders as an ellipse. This plugin measures\n// pixels-per-unit on both axes after layout and expands the tighter axis's\n// range to match, so the circle stays a true circle. Guarded by $aspectDone so\n// the single corrective chart.update() it triggers doesn't recurse.\nconst equalAspectPlugin = {\n  id: \"equalAspect\",\n  afterLayout(chart) {\n    if (chart.$aspectDone) return;\n    const { chartArea, scales } = chart;\n    const xs = scales.x;\n    const ys = scales.y;\n    const pxPerUnitX = chartArea.width / (xs.max - xs.min);\n    const pxPerUnitY = chartArea.height / (ys.max - ys.min);\n    if (pxPerUnitX < pxPerUnitY) {\n      const yCenter = (ys.min + ys.max) / 2;\n      const halfRange = chartArea.height / pxPerUnitX / 2;\n      ys.options.min = yCenter - halfRange;\n      ys.options.max = yCenter + halfRange;\n    } else {\n      const xCenter = (xs.min + xs.max) / 2;\n      const halfRange = chartArea.width / pxPerUnitY / 2;\n      xs.options.min = xCenter - halfRange;\n      xs.options.max = xCenter + halfRange;\n    }\n    chart.$aspectDone = true;\n    chart.update(\"none\");\n  },\n};\n\n// Chart.js core has no chart type for labeled reference points or angle arcs,\n// so this plugin draws the Mohr's-circle-specific reading aids (point labels,\n// the center marker, and the 2*theta_p arc from the reference axis to point A)\n// directly on the canvas via the public afterDatasetsDraw hook.\nconst annotationPlugin = {\n  id: \"mohrAnnotations\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const px = (v) => scales.x.getPixelForValue(v);\n    const py = (v) => scales.y.getPixelForValue(v);\n    const cx = px(center);\n    const cy = py(0);\n    const unitPx = Math.abs(px(center + 1) - cx);\n\n    ctx.save();\n\n    // Center marker\n    ctx.beginPath();\n    ctx.arc(cx, cy, 5, 0, 2 * Math.PI);\n    ctx.fillStyle = t.inkSoft;\n    ctx.fill();\n\n    // Angle arc: reference axis (toward sigma1) -> radius to point A\n    const arcRadiusPx = radius * 0.32 * unitPx;\n    const angleRef = Math.atan2(py(0) - cy, px(sigma1) - cx);\n    const angleA = Math.atan2(py(tauXY) - cy, px(sigmaX) - cx);\n    const ccw = angleA < angleRef;\n    ctx.beginPath();\n    ctx.setLineDash([4, 3]);\n    ctx.lineWidth = 2;\n    ctx.strokeStyle = t.inkSoft;\n    ctx.arc(cx, cy, arcRadiusPx, angleRef, angleA, ccw);\n    ctx.stroke();\n    ctx.setLineDash([]);\n\n    const midAngle = (angleRef + angleA) / 2;\n    ctx.font = \"500 15px -apple-system, sans-serif\";\n    ctx.fillStyle = t.inkSoft;\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(\"2θp\", cx + Math.cos(midAngle) * (arcRadiusPx + 26), cy + Math.sin(midAngle) * (arcRadiusPx + 26));\n\n    // Point labels\n    ctx.font = \"600 17px -apple-system, sans-serif\";\n    ctx.fillStyle = t.ink;\n\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(\"A\", px(pointA.x) + 14, py(pointA.y) - 10);\n\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"top\";\n    ctx.fillText(\"B\", px(pointB.x) + 14, py(pointB.y) + 10);\n\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"top\";\n    ctx.fillText(\"σ1\", px(sigma1), cy + 16);\n    ctx.fillText(\"σ2\", px(sigma2), cy + 16);\n\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(\"τmax\", px(center), py(tauMax) - 14);\n    ctx.textBaseline = \"top\";\n    ctx.fillText(\"τmin\", px(center), py(-tauMax) + 14);\n\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Mohr's circle\",\n        data: circlePoints,\n        showLine: true,\n        fill: false,\n        tension: 0,\n        borderColor: t.palette[0],\n        borderWidth: 3,\n        pointRadius: 0,\n      },\n      {\n        label: \"_diameter\",\n        data: [pointA, pointB],\n        showLine: true,\n        borderColor: t.inkSoft,\n        borderWidth: 1.5,\n        borderDash: [6, 5],\n        pointRadius: 0,\n      },\n      {\n        label: \"_axis-h\",\n        data: [\n          { x: axisMin, y: 0 },\n          { x: axisMax, y: 0 },\n        ],\n        showLine: true,\n        borderColor: t.grid,\n        borderWidth: 1.5,\n        pointRadius: 0,\n      },\n      {\n        label: \"_axis-v\",\n        data: [\n          { x: center, y: -axisHalfSpan },\n          { x: center, y: axisHalfSpan },\n        ],\n        showLine: true,\n        borderColor: t.grid,\n        borderWidth: 1.5,\n        pointRadius: 0,\n      },\n      {\n        label: \"Stress points A, B\",\n        data: [pointA, pointB],\n        pointRadius: 9,\n        pointHoverRadius: 9,\n        backgroundColor: t.palette[1],\n        borderColor: t.pageBg,\n        borderWidth: 2,\n      },\n      {\n        label: \"Principal stresses σ1, σ2\",\n        data: [\n          { x: sigma1, y: 0 },\n          { x: sigma2, y: 0 },\n        ],\n        pointRadius: 9,\n        pointHoverRadius: 9,\n        backgroundColor: t.palette[2],\n        borderColor: t.pageBg,\n        borderWidth: 2,\n      },\n      {\n        label: \"Max/min shear τmax\",\n        data: [\n          { x: center, y: tauMax },\n          { x: center, y: -tauMax },\n        ],\n        pointRadius: 9,\n        pointHoverRadius: 9,\n        backgroundColor: t.palette[3],\n        borderColor: t.pageBg,\n        borderWidth: 2,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 30, right: 40, bottom: 10, left: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"mohr-circle · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.ink,\n          font: { size: 15 },\n          usePointStyle: true,\n          filter: (item) => !item.text.startsWith(\"_\"),\n        },\n      },\n      tooltip: {\n        filter: (item) => !item.dataset.label.startsWith(\"_\"),\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: axisMin,\n        max: axisMax,\n        title: { display: true, text: \"Normal Stress σ (MPa)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n      y: {\n        type: \"linear\",\n        min: -axisHalfSpan,\n        max: axisHalfSpan,\n        title: { display: true, text: \"Shear Stress τ (MPa)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n    },\n  },\n  plugins: [equalAspectPlugin, annotationPlugin],\n});\n"}