{"spec_id":"mohr-circle","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nmohr-circle: Mohr's Circle for Stress Analysis\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_path,\n    geom_point,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_shape_manual,\n    theme,\n    theme_minimal,\n)\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — semantic assignments for stress diagram elements\nC_CIRCLE = \"#4467A3\"  # blue — structural circle outline\nC_INPUT = \"#AE3030\"  # matte red — applied input stress points (semantic: danger/input)\nC_PRINCIPAL = \"#009E73\"  # brand green — principal stress results (primary outcome)\nC_SHEAR = \"#BD8233\"  # ochre — shear stress components\n\n# Category constants for legend\nCAT_INPUT = \"Input Point\"\nCAT_PRINCIPAL = \"Principal Stress\"\nCAT_SHEAR = \"Max Shear\"\n\n# Data — steel column under combined axial and bending load\nsigma_x = 80.0  # MPa, axial + bending normal stress\nsigma_y = -30.0  # MPa, lateral normal stress\ntau_xy = 50.0  # MPa, shear stress from torsion\n\n# Mohr's Circle geometry\ncenter = (sigma_x + sigma_y) / 2.0\nradius = np.sqrt(((sigma_x - sigma_y) / 2.0) ** 2 + tau_xy**2)\nsigma_1 = center + radius\nsigma_2 = center - radius\ntau_max = radius\ntheta_p = 0.5 * np.degrees(np.arctan2(2 * tau_xy, sigma_x - sigma_y))\n\n# Circle path\ntheta = np.linspace(0, 2 * np.pi, 360)\ndf_circle = pd.DataFrame({\"sigma\": center + radius * np.cos(theta), \"tau\": radius * np.sin(theta)})\n\n# Input stress points A and B\ndf_input = pd.DataFrame(\n    {\n        \"sigma\": [sigma_x, sigma_y],\n        \"tau\": [tau_xy, -tau_xy],\n        \"category\": [CAT_INPUT, CAT_INPUT],\n        \"label\": [f\"A (σx={sigma_x:.0f}, τxy={tau_xy:.0f})\", f\"B (σy={sigma_y:.0f}, −τxy={-tau_xy:.0f})\"],\n        \"detail\": [\n            f\"Normal: {sigma_x:.0f} MPa | Shear: {tau_xy:.0f} MPa\",\n            f\"Normal: {sigma_y:.0f} MPa | Shear: {-tau_xy:.0f} MPa\",\n        ],\n    }\n)\n\n# Principal stress points (on the horizontal axis)\ndf_principal = pd.DataFrame(\n    {\n        \"sigma\": [sigma_1, sigma_2],\n        \"tau\": [0.0, 0.0],\n        \"category\": [CAT_PRINCIPAL, CAT_PRINCIPAL],\n        \"label\": [f\"σ₁ = {sigma_1:.1f}\", f\"σ₂ = {sigma_2:.1f}\"],\n        \"detail\": [f\"Max principal: {sigma_1:.1f} MPa\", f\"Min principal: {sigma_2:.1f} MPa\"],\n    }\n)\n\n# Maximum shear stress points (top and bottom of circle)\ndf_shear = pd.DataFrame(\n    {\n        \"sigma\": [center, center],\n        \"tau\": [tau_max, -tau_max],\n        \"category\": [CAT_SHEAR, CAT_SHEAR],\n        \"label\": [f\"τmax = {tau_max:.1f}\", f\"−τmax = {tau_max:.1f}\"],\n        \"detail\": [f\"Max shear: {tau_max:.1f} MPa\", f\"Min shear: {-tau_max:.1f} MPa\"],\n    }\n)\n\n# Combined points for legend-mapped layer (Input, Principal, Max Shear)\ndf_legend_points = pd.concat([df_input, df_principal, df_shear], ignore_index=True)\n\n# Center point (separate layer — different size, no legend entry needed)\ndf_center = pd.DataFrame(\n    {\n        \"sigma\": [center],\n        \"tau\": [0.0],\n        \"label\": [f\"C ({center:.0f}, 0)\"],\n        \"detail\": [f\"Mean stress: {center:.1f} MPa | Radius: {radius:.1f} MPa\"],\n    }\n)\n\n# Reference lines through center\npad = radius * 0.45\ndf_hline = pd.DataFrame({\"sigma\": [center - radius - pad, center + radius + pad], \"tau\": [0.0, 0.0]})\ndf_vline = pd.DataFrame({\"sigma\": [center, center], \"tau\": [-radius - pad, radius + pad]})\ndf_diameter = pd.DataFrame({\"sigma\": [sigma_x, sigma_y], \"tau\": [tau_xy, -tau_xy]})\n\n# Angle arc for 2θp (principal plane angle)\narc_r = radius * 0.35\narc_t = np.linspace(0, np.radians(2 * theta_p), 50)\ndf_arc = pd.DataFrame({\"sigma\": center + arc_r * np.cos(arc_t), \"tau\": arc_r * np.sin(arc_t)})\n\n# Theme-adaptive circle fill alpha — slightly stronger on dark for interior visibility\ncircle_fill_alpha = 0.12 if THEME == \"dark\" else 0.07\n\n# Title\ntitle = \"mohr-circle · python · letsplot · anyplot.ai\"\n\n# Build plot\nplot = (\n    ggplot()\n    # Reference lines through center\n    + geom_line(data=df_hline, mapping=aes(x=\"sigma\", y=\"tau\"), color=INK_MUTED, size=0.6, linetype=\"dashed\")\n    + geom_line(data=df_vline, mapping=aes(x=\"sigma\", y=\"tau\"), color=INK_MUTED, size=0.6, linetype=\"dashed\")\n    # Circle fill (subtle, theme-adaptive) then solid outline\n    + geom_polygon(\n        data=df_circle, mapping=aes(x=\"sigma\", y=\"tau\"), fill=C_CIRCLE, color=C_CIRCLE, alpha=circle_fill_alpha, size=0\n    )\n    + geom_path(data=df_circle, mapping=aes(x=\"sigma\", y=\"tau\"), color=C_CIRCLE, size=2.0, alpha=0.9)\n    # Diameter line A–B\n    + geom_line(data=df_diameter, mapping=aes(x=\"sigma\", y=\"tau\"), color=INK_MUTED, size=0.8, linetype=\"dashed\")\n    # Principal plane angle arc\n    + geom_path(data=df_arc, mapping=aes(x=\"sigma\", y=\"tau\"), color=C_SHEAR, size=1.5)\n    # Stress points with mapped color + shape — drives the built-in legend panel\n    + geom_point(\n        data=df_legend_points,\n        mapping=aes(x=\"sigma\", y=\"tau\", color=\"category\", shape=\"category\"),\n        size=7,\n        tooltips=layer_tooltips().line(\"@label\").line(\"@detail\"),\n    )\n    + scale_color_manual(values={CAT_INPUT: C_INPUT, CAT_PRINCIPAL: C_PRINCIPAL, CAT_SHEAR: C_SHEAR}, name=\"\")\n    + scale_shape_manual(values={CAT_INPUT: 16, CAT_PRINCIPAL: 18, CAT_SHEAR: 17}, name=\"\")\n    # Center marker (X cross — fixed aesthetics, separate layer)\n    + geom_point(\n        data=df_center,\n        mapping=aes(x=\"sigma\", y=\"tau\"),\n        color=INK_SOFT,\n        size=5,\n        shape=4,\n        tooltips=layer_tooltips().line(\"@label\").line(\"@detail\"),\n    )\n    # Point A label — nudged right and up\n    + geom_text(\n        data=df_input.iloc[[0]],\n        mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"),\n        color=C_INPUT,\n        size=5,\n        hjust=0,\n        nudge_x=radius * 0.05,\n        nudge_y=radius * 0.10,\n    )\n    # Point B label — nudged left and down\n    + geom_text(\n        data=df_input.iloc[[1]],\n        mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"),\n        color=C_INPUT,\n        size=5,\n        hjust=1,\n        nudge_x=-radius * 0.05,\n        nudge_y=-radius * 0.14,\n    )\n    # σ₁ label — below the point\n    + geom_text(\n        data=df_principal.iloc[[0]],\n        mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"),\n        color=C_PRINCIPAL,\n        size=5,\n        hjust=0.5,\n        nudge_y=-radius * 0.14,\n    )\n    # σ₂ label — nudged upward (above x-axis) to clear the B label in the lower-left\n    + geom_text(\n        data=df_principal.iloc[[1]],\n        mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"),\n        color=C_PRINCIPAL,\n        size=5,\n        hjust=0.5,\n        nudge_y=radius * 0.18,\n    )\n    # τmax label — nudged right, above the top marker\n    + geom_text(\n        data=df_shear.iloc[[0]],\n        mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"),\n        color=C_SHEAR,\n        size=5,\n        hjust=0,\n        nudge_x=radius * 0.12,\n        nudge_y=radius * 0.07,\n    )\n    # −τmax label\n    + geom_text(\n        data=df_shear.iloc[[1]],\n        mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"),\n        color=C_SHEAR,\n        size=5,\n        hjust=0,\n        nudge_x=radius * 0.12,\n        nudge_y=-radius * 0.07,\n    )\n    # Principal plane angle annotation\n    + geom_text(\n        data=pd.DataFrame(\n            {\n                \"sigma\": [center + arc_r * 1.5 * np.cos(np.radians(theta_p))],\n                \"tau\": [arc_r * 1.5 * np.sin(np.radians(theta_p))],\n                \"label\": [f\"2θp = {2 * theta_p:.1f}°\"],\n            }\n        ),\n        mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"),\n        color=C_SHEAR,\n        size=5,\n        hjust=0,\n    )\n    # Center label\n    + geom_text(\n        data=df_center, mapping=aes(x=\"sigma\", y=\"tau\", label=\"label\"), color=INK_SOFT, size=5, nudge_y=-radius * 0.13\n    )\n    # Axes, title, subtitle\n    + labs(\n        x=\"Normal Stress σ (MPa)\",\n        y=\"Shear Stress τ (MPa)\",\n        title=title,\n        subtitle=(\n            f\"σx = {sigma_x:.0f}, σy = {sigma_y:.0f},\"\n            f\" τxy = {tau_xy:.0f} MPa\"\n            f\" | σ₁ = {sigma_1:.1f}, σ₂ = {sigma_2:.1f},\"\n            f\" τmax = {tau_max:.1f} MPa\"\n        ),\n    )\n    + coord_fixed()\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_line(color=INK_MUTED, size=0.3),\n        panel_grid_minor=element_blank(),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        plot_subtitle=element_text(size=10, face=\"italic\", color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(color=INK_SOFT),\n        legend_title=element_text(color=INK),\n    )\n    + ggsize(600, 600)\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", scale=4, path=\".\")\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}