{"spec_id":"campbell-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ncampbell-basic: Campbell Diagram\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nCRITICAL_COLOR = \"#AE3030\"\n\n# Data\nnp.random.seed(42)\nspeed_rpm = np.linspace(0, 6000, 80)\nspeed_hz = speed_rpm / 60\n\n# Natural frequency modes (Hz) with realistic gyroscopic effects\nmode_1_bending = 22 + 0.004 * speed_rpm + np.sin(speed_rpm / 1200) * 1.2\nmode_2_bending = 48 - 0.003 * speed_rpm + np.cos(speed_rpm / 1800) * 1.4\nmode_1_torsional = 55 + 0.0004 * speed_rpm\nmode_axial = 75 - 0.004 * speed_rpm + np.sin(speed_rpm / 1000) * 2.0\n\norders = [1, 2, 3]\norder_freq = {order: order * speed_hz for order in orders}\n\nmodes = {\n    \"1st Bending\": mode_1_bending,\n    \"2nd Bending\": mode_2_bending,\n    \"1st Torsional\": mode_1_torsional,\n    \"Axial\": mode_axial,\n}\n\n# Find critical speed intersections\ncritical_speeds = []\ncritical_freqs = []\ncritical_labels = []\nfor order in orders:\n    eo_freq = order_freq[order]\n    for mode_name, mode_freq in modes.items():\n        diff = mode_freq - eo_freq\n        sign_changes = np.where(np.diff(np.sign(diff)))[0]\n        for idx in sign_changes:\n            frac = abs(diff[idx]) / (abs(diff[idx]) + abs(diff[idx + 1]))\n            crit_rpm = speed_rpm[idx] + frac * (speed_rpm[idx + 1] - speed_rpm[idx])\n            crit_freq = order * (crit_rpm / 60)\n            critical_speeds.append(crit_rpm)\n            critical_freqs.append(crit_freq)\n            critical_labels.append(f\"{mode_name} × {order}x\")\n\nfig = go.Figure()\n\n# Shade only the 3 most significant critical speed zones (highest frequency) — reduces clutter\ny_max = 110\nif critical_freqs:\n    top_indices = sorted(range(len(critical_freqs)), key=lambda i: critical_freqs[i], reverse=True)[:3]\n    for i in top_indices:\n        fig.add_vrect(\n            x0=critical_speeds[i] - 60,\n            x1=critical_speeds[i] + 60,\n            fillcolor=\"rgba(174,48,48,0.08)\",\n            line_width=0,\n            layer=\"below\",\n        )\n\n# Natural frequency curves\nline_dashes = [\"solid\", \"dash\", \"dot\", \"dashdot\"]\nn_mode_traces = len(modes)\nfor i, (mode_name, mode_freq) in enumerate(modes.items()):\n    fig.add_trace(\n        go.Scatter(\n            x=speed_rpm,\n            y=mode_freq,\n            mode=\"lines\",\n            name=mode_name,\n            line={\"color\": IMPRINT_PALETTE[i], \"width\": 3.5, \"dash\": line_dashes[i]},\n            hovertemplate=f\"<b>{mode_name}</b><br>Speed: %{{x:.0f}} RPM<br>Freq: %{{y:.1f}} Hz<extra></extra>\",\n        )\n    )\n\n# Engine order lines\nn_eo_traces = len(orders)\nfor order in orders:\n    label = f\"{order}x\"\n    eo_y = order_freq[order]\n    mask = eo_y <= y_max\n    fig.add_trace(\n        go.Scatter(\n            x=speed_rpm[mask],\n            y=eo_y[mask],\n            mode=\"lines\",\n            name=f\"EO {label}\",\n            line={\"color\": INK_SOFT, \"width\": 2, \"dash\": \"dash\"},\n            hovertemplate=f\"<b>EO {label}</b><br>Speed: %{{x:.0f}} RPM<br>Freq: %{{y:.1f}} Hz<extra></extra>\",\n        )\n    )\n\n# Engine order labels at 75% along visible segment\nfor order in orders:\n    eo_y = order_freq[order]\n    visible_indices = np.where(eo_y <= y_max)[0]\n    if len(visible_indices) > 0:\n        label_idx = visible_indices[int(len(visible_indices) * 0.75)]\n        fig.add_annotation(\n            x=speed_rpm[label_idx],\n            y=eo_y[label_idx],\n            text=f\"<b>{order}x</b>\",\n            showarrow=False,\n            xanchor=\"left\",\n            yanchor=\"bottom\",\n            xshift=8,\n            yshift=4,\n            font={\"size\": 11, \"color\": INK_SOFT},\n            bgcolor=ELEVATED_BG,\n            borderpad=2,\n        )\n\n# Critical speed markers\nfig.add_trace(\n    go.Scatter(\n        x=critical_speeds,\n        y=critical_freqs,\n        mode=\"markers\",\n        name=\"Critical Speed\",\n        marker={\"size\": 14, \"color\": CRITICAL_COLOR, \"symbol\": \"diamond\", \"line\": {\"width\": 2, \"color\": PAGE_BG}},\n        customdata=critical_labels,\n        hovertemplate=\"<b>Critical Speed</b><br>%{customdata}<br>Speed: %{x:.0f} RPM<br>Freq: %{y:.1f} Hz<extra></extra>\",\n    )\n)\n\n# Annotate highest-frequency and first (lowest-RPM) critical intersections\nif critical_speeds:\n    max_idx = int(np.argmax(critical_freqs))\n    fig.add_annotation(\n        x=critical_speeds[max_idx],\n        y=critical_freqs[max_idx],\n        text=f\"<b>{critical_freqs[max_idx]:.0f} Hz</b><br>{critical_labels[max_idx]}\",\n        showarrow=True,\n        arrowhead=2,\n        arrowsize=1.2,\n        arrowcolor=CRITICAL_COLOR,\n        arrowwidth=2,\n        ax=50,\n        ay=-45,\n        font={\"size\": 10, \"color\": CRITICAL_COLOR},\n        bgcolor=ELEVATED_BG,\n        bordercolor=CRITICAL_COLOR,\n        borderwidth=1.5,\n        borderpad=4,\n    )\n    min_rpm_idx = int(np.argmin(critical_speeds))\n    if min_rpm_idx != max_idx:\n        fig.add_annotation(\n            x=critical_speeds[min_rpm_idx],\n            y=critical_freqs[min_rpm_idx],\n            text=f\"<b>1st critical</b><br>{critical_speeds[min_rpm_idx]:.0f} RPM\",\n            showarrow=True,\n            arrowhead=2,\n            arrowsize=1.2,\n            arrowcolor=CRITICAL_COLOR,\n            arrowwidth=2,\n            ax=-55,\n            ay=40,\n            font={\"size\": 10, \"color\": CRITICAL_COLOR},\n            bgcolor=ELEVATED_BG,\n            bordercolor=CRITICAL_COLOR,\n            borderwidth=1.5,\n            borderpad=4,\n        )\n\n# Toggle visibility arrays (simplified: modes + criticals vs all)\ntotal_traces = n_mode_traces + n_eo_traces + 1\nall_visible = [True] * total_traces\nmodes_only = [True] * n_mode_traces + [False] * n_eo_traces + [True]\n\ntitle = \"campbell-basic · python · plotly · anyplot.ai\"\n\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    title={\"text\": title, \"font\": {\"size\": 16, \"color\": INK}, \"x\": 0.5, \"xanchor\": \"center\", \"y\": 0.97},\n    xaxis={\n        \"title\": {\"text\": \"Rotational Speed (RPM)\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 10},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"zeroline\": False,\n        \"range\": [0, 6100],\n        \"dtick\": 1000,\n        \"showline\": True,\n        \"linecolor\": INK_SOFT,\n        \"linewidth\": 1,\n        \"mirror\": False,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Frequency (Hz)\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 10},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"zeroline\": False,\n        \"range\": [0, y_max],\n        \"dtick\": 10,\n        \"showline\": True,\n        \"linecolor\": INK_SOFT,\n        \"linewidth\": 1,\n        \"mirror\": False,\n    },\n    legend={\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"x\": 0.01,\n        \"y\": 0.99,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n        \"tracegroupgap\": 0,\n        \"itemsizing\": \"constant\",\n        \"orientation\": \"v\",\n    },\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n    hovermode=\"closest\",\n    updatemenus=[\n        {\n            \"type\": \"buttons\",\n            \"direction\": \"left\",\n            \"x\": 1.0,\n            \"y\": 1.05,\n            \"xanchor\": \"right\",\n            \"yanchor\": \"top\",\n            \"buttons\": [\n                {\"label\": \"All Modes\", \"method\": \"update\", \"args\": [{\"visible\": all_visible}]},\n                {\"label\": \"Modes Only\", \"method\": \"update\", \"args\": [{\"visible\": modes_only}]},\n            ],\n            \"font\": {\"size\": 10, \"color\": INK},\n            \"bgcolor\": ELEVATED_BG,\n            \"bordercolor\": INK_SOFT,\n        }\n    ],\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}