Mohr's Circle for Stress Analysis — Plotly

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 Plotly

Python source (Plotly)

""" anyplot.ai
mohr-circle: Mohr's Circle for Stress Analysis
Library: plotly 6.7.0 | Python 3.13.13
Quality: 92/100 | Updated: 2026-05-30
"""

import os

import numpy as np
import plotly.graph_objects as go


# Theme tokens — Imprint palette chrome
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"

# Imprint palette — differentiated roles for Mohr's circle elements
CIRCLE_COLOR = "#009E73"  # brand green — primary structural element
POINT_COLOR = "#AE3030"  # matte red — given stress state (A, B)
SIGMA_COLOR = "#4467A3"  # blue — principal stresses σ1, σ2
TAU_COLOR = "#BD8233"  # ochre — maximum shear stress
ARC_COLOR = "#C475FD"  # lavender — principal plane angle arc

# Data — stress state (MPa)
sigma_x = 80
sigma_y = -40
tau_xy = 30

# Mohr's circle parameters
center = (sigma_x + sigma_y) / 2
radius = np.sqrt(((sigma_x - sigma_y) / 2) ** 2 + tau_xy**2)
sigma_1 = center + radius
sigma_2 = center - radius
tau_max = radius

# Circle points
theta = np.linspace(0, 2 * np.pi, 360)
sigma_circle = center + radius * np.cos(theta)
tau_circle = radius * np.sin(theta)

# Stress points
point_a = (sigma_x, tau_xy)
point_b = (sigma_y, -tau_xy)

# Principal plane angle (2θp)
theta_2p = np.degrees(np.arctan2(tau_xy, (sigma_x - sigma_y) / 2))

# Plot
fig = go.Figure()

# Reference lines through center
axis_pad = radius * 0.3
fig.add_shape(
    type="line",
    x0=sigma_2 - axis_pad,
    y0=0,
    x1=sigma_1 + axis_pad,
    y1=0,
    line={"color": GRID, "width": 1.5},
    layer="below",
)
fig.add_shape(
    type="line",
    x0=center,
    y0=-radius - axis_pad,
    x1=center,
    y1=radius + axis_pad,
    line={"color": GRID, "width": 1.5},
    layer="below",
)

# Mohr's circle — brand green with subtle fill
circle_fill = "rgba(0,158,115,0.07)" if THEME == "light" else "rgba(0,158,115,0.13)"
fig.add_trace(
    go.Scatter(
        x=sigma_circle,
        y=tau_circle,
        mode="lines",
        line={"color": CIRCLE_COLOR, "width": 3.5},
        showlegend=False,
        fill="toself",
        fillcolor=circle_fill,
    )
)

# Diameter line connecting A and B
fig.add_trace(
    go.Scatter(
        x=[point_a[0], point_b[0]],
        y=[point_a[1], point_b[1]],
        mode="lines",
        line={"color": CIRCLE_COLOR, "width": 2, "dash": "dash"},
        showlegend=False,
        hoverinfo="skip",
    )
)

# Stress points A and B — given state (red)
fig.add_trace(
    go.Scatter(
        x=[point_a[0]],
        y=[point_a[1]],
        mode="markers",
        marker={"size": 14, "color": POINT_COLOR, "line": {"color": PAGE_BG, "width": 2}},
        showlegend=False,
        hovertext=f"A (σx={sigma_x}, τxy={tau_xy})",
        hoverinfo="text",
    )
)
fig.add_trace(
    go.Scatter(
        x=[point_b[0]],
        y=[point_b[1]],
        mode="markers",
        marker={"size": 14, "color": POINT_COLOR, "line": {"color": PAGE_BG, "width": 2}},
        showlegend=False,
        hovertext=f"B (σy={sigma_y}, τxy={-tau_xy})",
        hoverinfo="text",
    )
)

# Principal stresses σ1, σ2 — derived results (blue diamonds)
fig.add_trace(
    go.Scatter(
        x=[sigma_1, sigma_2],
        y=[0, 0],
        mode="markers",
        marker={"size": 14, "color": SIGMA_COLOR, "symbol": "diamond", "line": {"color": PAGE_BG, "width": 2}},
        showlegend=False,
        hovertext=[f"σ₁ = {sigma_1:.1f} MPa", f"σ₂ = {sigma_2:.1f} MPa"],
        hoverinfo="text",
    )
)

# Maximum shear stress top — ochre triangle-up
fig.add_trace(
    go.Scatter(
        x=[center],
        y=[tau_max],
        mode="markers",
        marker={"size": 14, "color": TAU_COLOR, "symbol": "triangle-up", "line": {"color": PAGE_BG, "width": 2}},
        showlegend=False,
        hovertext=[f"τmax = {tau_max:.1f} MPa"],
        hoverinfo="text",
    )
)

# Maximum shear stress bottom — ochre triangle-down
fig.add_trace(
    go.Scatter(
        x=[center],
        y=[-tau_max],
        mode="markers",
        marker={"size": 14, "color": TAU_COLOR, "symbol": "triangle-down", "line": {"color": PAGE_BG, "width": 2}},
        showlegend=False,
        hovertext=[f"−τmax = {-tau_max:.1f} MPa"],
        hoverinfo="text",
    )
)

# Center point
fig.add_trace(
    go.Scatter(
        x=[center],
        y=[0],
        mode="markers",
        marker={"size": 10, "color": CIRCLE_COLOR, "symbol": "x", "line": {"width": 2}},
        showlegend=False,
        hovertext=f"C ({center:.0f}, 0)",
        hoverinfo="text",
    )
)

# Principal plane angle arc (2θp) — lavender
arc_r = radius * 0.28
arc_angles = np.linspace(0, np.radians(theta_2p), 50)
arc_x = center + arc_r * np.cos(arc_angles)
arc_y = arc_r * np.sin(arc_angles)
fig.add_trace(
    go.Scatter(
        x=arc_x, y=arc_y, mode="lines", line={"color": ARC_COLOR, "width": 2.5}, showlegend=False, hoverinfo="skip"
    )
)

# Annotations
fig.add_annotation(
    x=point_a[0],
    y=point_a[1],
    text=f"A ({sigma_x}, {tau_xy})",
    showarrow=True,
    arrowhead=0,
    arrowcolor=POINT_COLOR,
    ax=40,
    ay=-35,
    font={"size": 12, "color": POINT_COLOR},
)
fig.add_annotation(
    x=point_b[0],
    y=point_b[1],
    text=f"B ({sigma_y}, {-tau_xy})",
    showarrow=True,
    arrowhead=0,
    arrowcolor=POINT_COLOR,
    ax=-40,
    ay=35,
    font={"size": 12, "color": POINT_COLOR},
)
fig.add_annotation(
    x=sigma_1, y=0, text=f"σ₁ = {sigma_1:.1f}", showarrow=False, yshift=-28, font={"size": 12, "color": SIGMA_COLOR}
)
fig.add_annotation(
    x=sigma_2, y=0, text=f"σ₂ = {sigma_2:.1f}", showarrow=False, yshift=-28, font={"size": 12, "color": SIGMA_COLOR}
)
fig.add_annotation(
    x=center, y=tau_max, text=f"τmax = {tau_max:.1f}", showarrow=False, yshift=18, font={"size": 12, "color": TAU_COLOR}
)
fig.add_annotation(
    x=center,
    y=-tau_max,
    text=f"−τmax = {-tau_max:.1f}",
    showarrow=False,
    yshift=-18,
    font={"size": 12, "color": TAU_COLOR},
)
fig.add_annotation(
    x=center,
    y=0,
    text=f"C ({center:.0f}, 0)",
    showarrow=False,
    xshift=35,
    yshift=-18,
    font={"size": 12, "color": INK_SOFT},
)

# 2θp label near arc midpoint
mid_angle = np.radians(theta_2p / 2)
fig.add_annotation(
    x=center + arc_r * 1.4 * np.cos(mid_angle),
    y=arc_r * 1.4 * np.sin(mid_angle),
    text=f"2θp = {theta_2p:.1f}°",
    showarrow=False,
    font={"size": 11, "color": ARC_COLOR},
)

# Layout
title_text = "mohr-circle · python · plotly · anyplot.ai"
n = len(title_text)
title_fontsize = round(16 * 67 / n) if n > 67 else 16

fig.update_layout(
    autosize=False,
    title={"text": title_text, "font": {"size": title_fontsize, "color": INK}, "x": 0.5},
    xaxis={
        "title": {"text": "Normal Stress σ (MPa)", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "showgrid": True,
        "gridcolor": GRID,
        "gridwidth": 1,
        "zeroline": True,
        "zerolinecolor": INK_SOFT,
        "zerolinewidth": 1.5,
        "linecolor": INK_SOFT,
        "scaleanchor": "y",
        "scaleratio": 1,
    },
    yaxis={
        "title": {"text": "Shear Stress τ (MPa)", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "showgrid": True,
        "gridcolor": GRID,
        "gridwidth": 1,
        "zeroline": True,
        "zerolinecolor": INK_SOFT,
        "zerolinewidth": 1.5,
        "linecolor": INK_SOFT,
    },
    plot_bgcolor=PAGE_BG,
    paper_bgcolor=PAGE_BG,
    font={"color": INK},
    showlegend=False,
    margin={"l": 65, "r": 65, "t": 80, "b": 60},
)

# Save
fig.write_image(f"plot-{THEME}.png", width=600, height=600, scale=4)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

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

Other implementations