3D stress analysis tool (Mohr's circle plot) using Javascript
This is an interactive browser-based tool for 3D stress analysis. Given a full stress tensor, it calculates the principal stresses and plots the three Mohr’s circles along with Von Mises and Tresca stress values.
The source code is self-contained within a single HTML file.
Live Tool
Libraries Used
- MathJax — math rendering
- Apache ECharts — interactive chart/scatter plot
- Tweakpane — input/output panels
Theory
The stress tensor
The state of stress at a point in a 3D body is fully described by the symmetric Cauchy stress tensor $\boldsymbol{\sigma}$:
\[\boldsymbol{\sigma} = \begin{pmatrix} \sigma_{11} & \sigma_{12} & \sigma_{13} \\ \sigma_{12} & \sigma_{22} & \sigma_{23} \\ \sigma_{13} & \sigma_{23} & \sigma_{33} \end{pmatrix} = \begin{pmatrix} \sigma_x & \tau_{xy} & \tau_{xz} \\ \tau_{xy} & \sigma_y & \tau_{yz} \\ \tau_{xz} & \tau_{yz} & \sigma_z \end{pmatrix}\]The diagonal entries are normal stresses; the off-diagonal entries are shear stresses. Symmetry ($\sigma_{ij} = \sigma_{ji}$) follows from moment equilibrium, leaving 6 independent components.
Principal stresses
Principal stresses $\sigma_p$ are the eigenvalues of $\boldsymbol{\sigma}$ — the normal stresses acting on planes where shear stress vanishes. They satisfy the characteristic equation:
\[\det(\boldsymbol{\sigma} - \sigma_p \mathbf{I}) = 0\]Expanding the determinant gives the cubic:
\[\sigma_p^3 - I_1\,\sigma_p^2 + I_2\,\sigma_p - I_3 = 0\]where $I_1$, $I_2$, $I_3$ are the stress invariants — scalar quantities that do not change with coordinate rotation (reference):
\[\begin{aligned} I_{1} &= \sigma_{11}+\sigma_{22}+\sigma_{33} \\ I_{2} &= \sigma_{11}\sigma_{22}+\sigma_{22}\sigma_{33}+\sigma_{33}\sigma_{11}-\sigma_{12}^{2}-\sigma_{23}^{2}-\sigma_{31}^{2} \\ I_{3} &= \det(\boldsymbol{\sigma}) = \sigma_{11}\sigma_{22}\sigma_{33}-\sigma_{11}\sigma_{23}^{2}-\sigma_{22}\sigma_{31}^{2}-\sigma_{33}\sigma_{12}^{2}+2\sigma_{12}\sigma_{23}\sigma_{31} \end{aligned}\]The cubic has three real roots (guaranteed for a symmetric real matrix). Using the trigonometric method, they are found via the auxiliary angle:
\[\phi = \frac{1}{3}\cos^{-1}\!\left(\frac{2I_{1}^{3}-9I_{1}I_{2}+27I_{3}}{2\left(I_{1}^{2}-3I_{2}\right)^{3/2}}\right)\]Principal stresses (ordered $\sigma_1 \geq \sigma_2 \geq \sigma_3$):
\[\begin{aligned} \sigma_{1} &= \frac{I_{1}}{3}+\frac{2}{3}\sqrt{I_{1}^{2}-3I_{2}}\,\cos\phi \\ \sigma_{2} &= \frac{I_{1}}{3}+\frac{2}{3}\sqrt{I_{1}^{2}-3I_{2}}\,\cos\!\left(\phi-\frac{2\pi}{3}\right) \\ \sigma_{3} &= \frac{I_{1}}{3}+\frac{2}{3}\sqrt{I_{1}^{2}-3I_{2}}\,\cos\!\left(\phi-\frac{4\pi}{3}\right) \end{aligned}\]Mohr’s circles
Each pair of principal stresses defines one circle in the $(\sigma, \tau)$ plane. The three circles have centers and radii:
\[C_{ij} = \frac{\sigma_i + \sigma_j}{2}, \qquad R_{ij} = \frac{\sigma_i - \sigma_j}{2}\]The outermost circle (between $\sigma_1$ and $\sigma_3$) gives the maximum shear stress $\tau_{\max} = R_{13}$ (Tresca criterion).
Failure criteria
\[\tau_{\max} = \frac{\sigma_1 - \sigma_3}{2} \qquad \text{(Tresca)}\] \[\sigma_{VM} = \sqrt{\frac{(\sigma_1-\sigma_2)^2+(\sigma_2-\sigma_3)^2+(\sigma_1-\sigma_3)^2}{2}} \qquad \text{(Von Mises)}\]Source Code
// Helper: generate circle coordinates for plotting
function get_circle_coordinates(x, y, r) {
let tmp = [];
for (let theta = 0; theta < 360; theta++) {
tmp[theta] = [
x + r * Math.cos(theta * (Math.PI / 180)),
y + r * Math.sin(theta * (Math.PI / 180))
];
}
return tmp;
}
// Input/output objects
const input = {
s_x: -100, s_y: -200, s_z: -150,
t_xy: 50, t_xz: 100, t_yz: 150
};
const output = {
s_1: 0, s_2: 0, s_3: 0,
R_1: 0, R_2: 0, R_3: 0,
C_1: 0, C_2: 0, C_3: 0,
s_vm: 0,
};
// Core calculation
function calc_mohr_3d(input) {
const pi = Math.PI;
const s_x = input.s_x, s_y = input.s_y, s_z = input.s_z;
const t_xy = input.t_xy, t_xz = input.t_xz, t_yz = input.t_yz;
// Stress Invariants
const I_1 = s_x + s_y + s_z;
const I_2 = s_x*s_y + s_y*s_z + s_z*s_x - t_xy**2 - t_xz**2 - t_yz**2;
const I_3 = (s_x*s_y*s_z) - s_x*t_yz**2 - s_y*t_xz**2 - s_z*t_xy**2 + (2*t_xy*t_xz*t_yz);
const phi = (1/3) * Math.acos(
(2*I_1**3 - 9*I_1*I_2 + 27*I_3) / (2*(I_1**2 - 3*I_2)**(3/2))
);
// Principal Stresses
const s_1 = (I_1/3) + (2/3)*Math.sqrt(I_1**2 - 3*I_2)*Math.cos(phi);
const s_2 = (I_1/3) + (2/3)*Math.sqrt(I_1**2 - 3*I_2)*Math.cos(phi - 2*pi/3);
const s_3 = (I_1/3) + (2/3)*Math.sqrt(I_1**2 - 3*I_2)*Math.cos(phi - 4*pi/3);
// Von Mises stress
const s_vm = Math.sqrt(0.5*((s_1-s_3)**2 + (s_2-s_3)**2 + (s_1-s_2)**2));
// Mohr's circle parameters
const C_1 = 0.5*(s_1 + s_2), R_1 = 0.5*(s_1 - s_2);
const C_2 = 0.5*(s_1 + s_3), R_2 = 0.5*(s_1 - s_3);
const C_3 = 0.5*(s_2 + s_3), R_3 = 0.5*(s_2 - s_3);
Object.assign(output, { s_1, s_2, s_3, R_1, R_2, R_3, C_1, C_2, C_3, s_vm });
// Round to 2 decimal places
for (const key in output) {
if (typeof output[key] === 'number') {
output[key] = +output[key].toFixed(2);
}
}
return output;
}
// ECharts drawing
function draw() {
const c1_coor = get_circle_coordinates(output.C_1, 0, output.R_1);
const c2_coor = get_circle_coordinates(output.C_2, 0, output.R_2);
const c3_coor = get_circle_coordinates(output.C_3, 0, output.R_3);
const myChart = echarts.init(document.getElementById('myChart'));
window.onresize = () => myChart.resize();
myChart.setOption({
maintainAspectRatio: 1,
legend: {},
animation: false,
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [
{ name: 'Max Stress', symbolSize: 5, data: c2_coor, type: 'scatter' },
{ name: 'Max Stress (plane 2,3)', symbolSize: 5, data: c1_coor, type: 'scatter' },
{ name: 'Max Stress (plane 1,2)', symbolSize: 5, data: c3_coor, type: 'scatter' },
{
name: 'Principal stress', symbolSize: 10, type: 'scatter',
data: [[output.s_1, 0, 'σ1'], [output.s_2, 0, 'σ2'], [output.s_3, 0, 'σ3']],
label: { show: true, position: 'top', formatter: '{@[2]} = {@[0]}' },
labelLine: { show: 1, type: 'dashed' }, labelLayout: { dx: -30 },
},
{
name: 'Max shear stress', symbolSize: 10, type: 'scatter',
data: [[output.C_1, output.R_1, 'τ1'], [output.C_2, output.R_2, 'τ2'], [output.C_3, output.R_3, 'τ3']],
label: { show: true, position: 'top', formatter: '{@[2]} = {@[1]}' },
labelLine: { show: 1, type: 'dashed' }, labelLayout: { dx: -30 },
},
]
});
}
// Tweakpane panels for input/output
const panel_input = new Tweakpane.Pane({ title: 'Input' });
const panel_output = new Tweakpane.Pane({ title: 'Output' });
const folder_normal = panel_input.addFolder({ title: 'Normal Stress [MPa]' });
folder_normal.addInput(input, 's_x', { label: 'σ11:' });
folder_normal.addInput(input, 's_y', { label: 'σ22:' });
folder_normal.addInput(input, 's_z', { label: 'σ33:' });
const folder_shear = panel_input.addFolder({ title: 'Shear Stress [MPa]' });
folder_shear.addInput(input, 't_xy', { label: 'τ12:' });
folder_shear.addInput(input, 't_yz', { label: 'τ23:' });
folder_shear.addInput(input, 't_xz', { label: 'τ13:' });
const folder_ps = panel_output.addFolder({ title: 'Principal Stress [MPa]' });
folder_ps.addMonitor(output, 's_1', { label: 'σ1:' });
folder_ps.addMonitor(output, 's_2', { label: 'σ2:' });
folder_ps.addMonitor(output, 's_3', { label: 'σ3:' });
const folder_shear_out = panel_output.addFolder({ title: 'Max Shear Stress [MPa]' });
folder_shear_out.addMonitor(output, 'R_1', { label: 'τ1:' });
folder_shear_out.addMonitor(output, 'R_2', { label: 'τ2:' });
folder_shear_out.addMonitor(output, 'R_3', { label: 'τ3:' });
const folder_vm = panel_output.addFolder({ title: 'Von Mises Stress [MPa]' });
folder_vm.addMonitor(output, 's_vm', { label: 'σvm:' });
calc_mohr_3d(input);
draw();
panel_input.on('change', () => {
calc_mohr_3d(input);
panel_output.refresh();
draw();
});