{"spec_id":"mohr-circle","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nmohr-circle: Mohr's Circle for Stress Analysis\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\nimport sys\n\n\n# altair.py shadows the altair package — remove this directory from sys.path first\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path[:] = [p for p in sys.path if os.path.realpath(p or os.getcwd()) != os.path.realpath(_here)]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (Imprint style guide)\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 positions used\nBRAND = \"#009E73\"  # pos 1 — stress state points + circle line\nLAVENDER = \"#C475FD\"  # pos 2 — max shear points\nBLUE = \"#4467A3\"  # pos 3 — principal stresses + 2θp arc\n\n# Data — 2D stress state (MPa)\nsigma_x = 80\nsigma_y = -40\ntau_xy = 30\n\ncenter = (sigma_x + sigma_y) / 2\nradius = np.sqrt(((sigma_x - sigma_y) / 2) ** 2 + tau_xy**2)\nsigma_1 = center + radius\nsigma_2 = center - radius\ntau_max = radius\ntheta_p2 = np.degrees(np.arctan2(tau_xy, (sigma_x - sigma_y) / 2))\n\n# Circle outline (parametric)\nangles = np.linspace(0, 2 * np.pi, 361)\ncircle_df = pd.DataFrame(\n    {\"sigma\": center + radius * np.cos(angles), \"tau\": radius * np.sin(angles), \"order\": range(361)}\n)\n\n# Key stress points\nstress_points = pd.DataFrame(\n    [\n        {\n            \"sigma\": sigma_x,\n            \"tau\": tau_xy,\n            \"label\": f\"A ({sigma_x}, {tau_xy})\",\n            \"type\": \"Stress State\",\n            \"dx\": 14,\n            \"dy\": -14,\n        },\n        {\n            \"sigma\": sigma_y,\n            \"tau\": -tau_xy,\n            \"label\": f\"B ({sigma_y}, {-tau_xy})\",\n            \"type\": \"Stress State\",\n            \"dx\": -18,\n            \"dy\": 14,\n        },\n        {\"sigma\": sigma_1, \"tau\": 0, \"label\": f\"σ₁ = {sigma_1:.1f} MPa\", \"type\": \"Principal\", \"dx\": 12, \"dy\": -16},\n        {\"sigma\": sigma_2, \"tau\": 0, \"label\": f\"σ₂ = {sigma_2:.1f} MPa\", \"type\": \"Principal\", \"dx\": -18, \"dy\": -16},\n        {\n            \"sigma\": center,\n            \"tau\": tau_max,\n            \"label\": f\"τmax = {tau_max:.1f} MPa\",\n            \"type\": \"Max Shear\",\n            \"dx\": 12,\n            \"dy\": -14,\n        },\n        {\n            \"sigma\": center,\n            \"tau\": -tau_max,\n            \"label\": f\"−τmax = −{tau_max:.1f} MPa\",\n            \"type\": \"Max Shear\",\n            \"dx\": 12,\n            \"dy\": 18,\n        },\n    ]\n)\n\n# Diameter line A → B\ndiameter_df = pd.DataFrame({\"sigma\": [sigma_x, sigma_y], \"tau\": [tau_xy, -tau_xy]})\n\n# 2θp angle arc\narc_r = radius * 0.25\narc_angles = np.linspace(0, np.radians(theta_p2), 50)\narc_df = pd.DataFrame(\n    {\"sigma\": center + arc_r * np.cos(arc_angles), \"tau\": arc_r * np.sin(arc_angles), \"order\": range(50)}\n)\n\n# Equal-aspect domains — square inner view (460×460) so circle renders as true circle\nspan = max(sigma_1 - sigma_2, 2 * tau_max) + 30\ndomain_sigma = [center - span / 2, center + span / 2]\ndomain_tau = [-span / 2, span / 2]\n\nx_scale = alt.Scale(domain=domain_sigma)\ny_scale = alt.Scale(domain=domain_tau)\n\n# Reference lines through center\nh_rule = (\n    alt.Chart(pd.DataFrame({\"tau\": [0]}))\n    .mark_rule(color=INK_SOFT, strokeWidth=1, opacity=0.5)\n    .encode(y=alt.Y(\"tau:Q\", scale=y_scale))\n)\nv_rule = (\n    alt.Chart(pd.DataFrame({\"sigma\": [center]}))\n    .mark_rule(color=INK_SOFT, strokeWidth=1, opacity=0.4, strokeDash=[6, 4])\n    .encode(x=alt.X(\"sigma:Q\", scale=x_scale))\n)\n\n# Circle\ncircle = (\n    alt.Chart(circle_df)\n    .mark_line(color=BRAND, strokeWidth=2.5)\n    .encode(\n        x=alt.X(\"sigma:Q\", title=\"Normal Stress σ (MPa)\", scale=x_scale),\n        y=alt.Y(\"tau:Q\", title=\"Shear Stress τ (MPa)\", scale=y_scale),\n        order=\"order:Q\",\n    )\n)\n\n# Diameter line\ndiameter = (\n    alt.Chart(diameter_df)\n    .mark_line(color=BRAND, strokeWidth=1.5, strokeDash=[8, 5], opacity=0.5)\n    .encode(x=\"sigma:Q\", y=\"tau:Q\")\n)\n\n# 2θp arc — linked to principal plane rotation\narc = alt.Chart(arc_df).mark_line(color=BLUE, strokeWidth=2.5).encode(x=\"sigma:Q\", y=\"tau:Q\", order=\"order:Q\")\n\n# Angle label\nangle_lbl_df = pd.DataFrame(\n    {\n        \"sigma\": [center + arc_r * 2.2 * np.cos(np.radians(theta_p2 / 2))],\n        \"tau\": [arc_r * 2.2 * np.sin(np.radians(theta_p2 / 2))],\n    }\n)\nangle_lbl = (\n    alt.Chart(angle_lbl_df)\n    .mark_text(text=f\"2θp = {theta_p2:.1f}°\", fontSize=11, fontWeight=\"bold\", color=BLUE)\n    .encode(x=\"sigma:Q\", y=\"tau:Q\")\n)\n\n# Interactive hover selection (enhances HTML output)\nhighlight = alt.selection_point(fields=[\"type\"], on=\"pointerover\")\n\n# Stress points with Imprint palette colors and hover interaction\npoints = (\n    alt.Chart(stress_points)\n    .mark_point(filled=True, strokeWidth=2, stroke=PAGE_BG)\n    .encode(\n        x=\"sigma:Q\",\n        y=\"tau:Q\",\n        color=alt.Color(\n            \"type:N\",\n            scale=alt.Scale(domain=[\"Stress State\", \"Principal\", \"Max Shear\"], range=[BRAND, BLUE, LAVENDER]),\n            legend=alt.Legend(\n                title=None,\n                orient=\"bottom-right\",\n                direction=\"vertical\",\n                symbolSize=180,\n                symbolStrokeWidth=0,\n                labelFontSize=10,\n                padding=8,\n                offset=8,\n                cornerRadius=4,\n            ),\n        ),\n        size=alt.condition(highlight, alt.value(420), alt.value(300)),\n        opacity=alt.condition(highlight, alt.value(1.0), alt.value(0.85)),\n        tooltip=[\n            alt.Tooltip(\"label:N\", title=\"Point\"),\n            alt.Tooltip(\"sigma:Q\", title=\"σ (MPa)\", format=\".1f\"),\n            alt.Tooltip(\"tau:Q\", title=\"τ (MPa)\", format=\".1f\"),\n            alt.Tooltip(\"type:N\", title=\"Category\"),\n        ],\n    )\n    .add_params(highlight)\n)\n\n# Center point\ncenter_pt_df = pd.DataFrame({\"sigma\": [center], \"tau\": [0]})\ncenter_pt = (\n    alt.Chart(center_pt_df)\n    .mark_point(size=180, filled=True, color=INK_MUTED, strokeWidth=2, stroke=PAGE_BG)\n    .encode(x=\"sigma:Q\", y=\"tau:Q\")\n)\ncenter_lbl = (\n    alt.Chart(center_pt_df)\n    .mark_text(text=f\"C ({center:.0f}, 0)\", fontSize=10, color=INK_SOFT, dy=18)\n    .encode(x=\"sigma:Q\", y=\"tau:Q\")\n)\n\n# Annotation labels — data-space offsets (px_to_data = span per inner-view pixel)\npx_to_data = span / 460\nstress_points[\"lbl_sigma\"] = stress_points[\"sigma\"] + stress_points[\"dx\"] * px_to_data\nstress_points[\"lbl_tau\"] = stress_points[\"tau\"] - stress_points[\"dy\"] * px_to_data\nlabels = (\n    alt.Chart(stress_points)\n    .mark_text(fontSize=11, fontWeight=\"bold\", color=INK)\n    .encode(x=\"lbl_sigma:Q\", y=\"lbl_tau:Q\", text=\"label:N\")\n)\n\n# Title and subtitle\ntitle_text = \"mohr-circle · python · altair · anyplot.ai\"\nsubtitle_text = f\"2D Stress Transformation — σx={sigma_x}, σy={sigma_y}, τxy={tau_xy} MPa\"\n\n# Compose chart — square inner view (460×460) preserves circular aspect ratio\nchart = (\n    alt.layer(h_rule, v_rule, circle, diameter, arc, points, center_pt, labels, center_lbl, angle_lbl)\n    .properties(\n        width=460,\n        height=460,\n        background=PAGE_BG,\n        title=alt.Title(\n            title_text,\n            fontSize=16,\n            fontWeight=\"bold\",\n            color=INK,\n            subtitle=subtitle_text,\n            subtitleFontSize=11,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=460, continuousHeight=460)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        grid=True,\n        gridOpacity=0.12,\n        gridColor=INK,\n        domain=False,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n    .configure_title(color=INK)\n)\n\n# Save — target 2400×2400; pad with PAGE_BG if vl-convert lands short\nTW, TH = 2400, 2400\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}