{"spec_id":"campbell-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncampbell-basic: Campbell Diagram\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Prevent this file from shadowing the installed bokeh package\n_this_dir = str(Path(__file__).parent.resolve())\nsys.path = [p for p in sys.path if os.path.normpath(p) != os.path.normpath(_this_dir)]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BoxAnnotation, ColumnDataSource, HoverTool, Label, Legend, LegendItem, Range1d, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"\n\n# Data\nnp.random.seed(42)\nspeeds = np.linspace(0, 6000, 100)\n\n# Natural frequency modes (Hz) with realistic rotordynamic behavior\nmode_1_bending = 25 + 0.008 * speeds + 3.5 * np.sin(speeds / 2800 * np.pi)\nmode_2_bending = 62 - 0.006 * speeds + 2.0 * np.sin(speeds / 2200 * np.pi)\nmode_1_torsional = 85 + 0.005 * speeds\nmode_axial = 110 - 0.004 * speeds + 2.5 * np.cos(speeds / 3200 * np.pi)\nmode_3_bending = 130 + 0.010 * speeds - 4.0 * np.cos(speeds / 2600 * np.pi)\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    \"3rd Bending\": mode_3_bending,\n}\nmode_colors = IMPRINT_PALETTE[:5]\n\n# Engine order lines: frequency = order * speed / 60\nengine_orders = [1, 2, 3]\neo_frequencies = {order: order * speeds / 60 for order in engine_orders}\n\n# Find critical speed intersections via sign-change interpolation\ncritical_speeds_rpm = []\ncritical_speeds_freq = []\ncritical_speed_labels = []\ncritical_in_operating = []\n\nfor order in engine_orders:\n    eo_freq = eo_frequencies[order]\n    for mode_name, mode_freq in modes.items():\n        diff = eo_freq - mode_freq\n        sign_changes = np.where(np.diff(np.sign(diff)))[0]\n        for idx in sign_changes:\n            denom = abs(diff[idx]) + abs(diff[idx + 1])\n            if denom == 0:\n                continue\n            frac = abs(diff[idx]) / denom\n            rpm_val = speeds[idx] + frac * (speeds[idx + 1] - speeds[idx])\n            freq_val = mode_freq[idx] + frac * (mode_freq[idx + 1] - mode_freq[idx])\n            if 100 < rpm_val < 5900 and 5 < freq_val < 195:\n                critical_speeds_rpm.append(rpm_val)\n                critical_speeds_freq.append(freq_val)\n                critical_speed_labels.append(f\"{order}x × {mode_name}\")\n                critical_in_operating.append(3000 <= rpm_val <= 5000)\n\n# Y-range\nall_freqs = np.concatenate(list(modes.values()))\ny_max_data = max(np.max(all_freqs), max(critical_speeds_freq) if critical_speeds_freq else 0)\ny_max = min(int(np.ceil(y_max_data / 10) * 10) + 15, 200)\n\n# Title with font-size scaling\ntitle = \"campbell-basic · python · bokeh · anyplot.ai\"\ntitle_len = len(title)\ntitle_fontsize = f\"{round(50 * min(67 / title_len, 1.0))}pt\"\n\n# Plot\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Rotational Speed (RPM)\",\n    y_axis_label=\"Frequency (Hz)\",\n    x_range=Range1d(-100, 6300),\n    y_range=Range1d(0, y_max),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Operating range shading (3000–5000 RPM)\noperating_zone = BoxAnnotation(\n    left=3000,\n    right=5000,\n    fill_color=INK_SOFT,\n    fill_alpha=0.07,\n    line_color=INK_SOFT,\n    line_alpha=0.3,\n    line_dash=\"dotted\",\n    line_width=2,\n)\np.add_layout(operating_zone)\n\n# Operating range boundary lines\nfor rpm_boundary in [3000, 5000]:\n    boundary_line = Span(\n        location=rpm_boundary, dimension=\"height\", line_color=INK_SOFT, line_alpha=0.4, line_width=2, line_dash=\"dashed\"\n    )\n    p.add_layout(boundary_line)\n\n# Operating range label\nop_label = Label(\n    x=4000,\n    y=y_max * 0.04,\n    text=\"Operating Range (3000–5000 RPM)\",\n    text_font_size=\"18pt\",\n    text_color=INK_SOFT,\n    text_alpha=0.9,\n    text_align=\"center\",\n    text_font_style=\"bold italic\",\n)\np.add_layout(op_label)\n\n# Danger zones around critical speeds within operating range\nfor rpm_val, freq_val, in_op in zip(critical_speeds_rpm, critical_speeds_freq, critical_in_operating, strict=True):\n    if in_op:\n        danger = BoxAnnotation(\n            left=rpm_val - 120,\n            right=rpm_val + 120,\n            bottom=freq_val - 4,\n            top=freq_val + 4,\n            fill_color=ANYPLOT_AMBER,\n            fill_alpha=0.18,\n            line_color=ANYPLOT_AMBER,\n            line_alpha=0.4,\n            line_width=1,\n        )\n        p.add_layout(danger)\n\n# Natural frequency curves (Imprint palette positions 1–5)\nlegend_items = []\nfor i, (mode_name, mode_freq) in enumerate(modes.items()):\n    source = ColumnDataSource(data={\"speed\": speeds, \"freq\": mode_freq})\n    line = p.line(x=\"speed\", y=\"freq\", source=source, line_width=4, line_color=mode_colors[i], line_alpha=0.9)\n    legend_items.append(LegendItem(label=mode_name, renderers=[line]))\n\n# Engine order lines (theme-adaptive muted)\nfor order in engine_orders:\n    eo_freq = eo_frequencies[order]\n    mask = eo_freq <= y_max\n    clipped_speeds = speeds[mask]\n    clipped_freq = eo_freq[mask]\n    source = ColumnDataSource(data={\"speed\": clipped_speeds, \"freq\": clipped_freq})\n    line = p.line(\n        x=\"speed\", y=\"freq\", source=source, line_width=2, line_color=INK_MUTED, line_dash=[12, 8], line_alpha=0.75\n    )\n    legend_items.append(LegendItem(label=f\"{order}x EO\", renderers=[line]))\n\n# Engine order labels at right edge\nfor order in engine_orders:\n    freq_at_max = order * 6000 / 60\n    if freq_at_max > y_max - 5:\n        label_rpm = (y_max - 12) * 60 / order\n        label_freq = y_max - 10\n    else:\n        label_rpm = 5850\n        label_freq = freq_at_max\n    label = Label(\n        x=label_rpm,\n        y=label_freq,\n        text=f\" {order}x\",\n        text_font_size=\"20pt\",\n        text_color=INK_MUTED,\n        text_font_style=\"bold\",\n        text_baseline=\"middle\",\n    )\n    p.add_layout(label)\n\n# Critical speed markers (amber = warning semantic anchor)\nif critical_speeds_rpm:\n    crit_source = ColumnDataSource(\n        data={\n            \"rpm\": critical_speeds_rpm,\n            \"freq\": critical_speeds_freq,\n            \"label\": critical_speed_labels,\n            \"rpm_display\": [f\"{r:.0f}\" for r in critical_speeds_rpm],\n            \"freq_display\": [f\"{f:.1f}\" for f in critical_speeds_freq],\n            \"in_operating\": [\"YES — CAUTION\" if op else \"No\" for op in critical_in_operating],\n        }\n    )\n    crit_scatter = p.scatter(\n        x=\"rpm\",\n        y=\"freq\",\n        source=crit_source,\n        marker=\"diamond\",\n        size=26,\n        fill_color=ANYPLOT_AMBER,\n        line_color=INK,\n        line_width=2,\n        fill_alpha=0.95,\n    )\n    legend_items.append(LegendItem(label=\"Critical Speed\", renderers=[crit_scatter]))\n\n    hover = HoverTool(\n        renderers=[crit_scatter],\n        tooltips=[\n            (\"Intersection\", \"@label\"),\n            (\"RPM\", \"@rpm_display\"),\n            (\"Frequency\", \"@freq_display Hz\"),\n            (\"In Operating Range?\", \"@in_operating\"),\n        ],\n        mode=\"mouse\",\n    )\n    p.add_tools(hover)\n\n    # Annotate the lowest critical intersection inside the operating range\n    op_crits = [\n        (r, f, lbl)\n        for r, f, lbl, op in zip(\n            critical_speeds_rpm, critical_speeds_freq, critical_speed_labels, critical_in_operating, strict=True\n        )\n        if op\n    ]\n    if op_crits:\n        op_crits.sort(key=lambda x: x[1])\n        worst_rpm, worst_freq, worst_label = op_crits[0]\n        annotation = Label(\n            x=worst_rpm + 280,\n            y=worst_freq + 8,\n            text=f\"⚠ {worst_label} @ {worst_rpm:.0f} RPM\",\n            text_font_size=\"15pt\",\n            text_color=ANYPLOT_AMBER,\n            text_font_style=\"bold\",\n            text_alpha=0.95,\n        )\n        p.add_layout(annotation)\n\n# Legend — increased glyph size and spacing to address previous weakness\nlegend = Legend(\n    items=legend_items,\n    location=\"top_left\",\n    label_text_font_size=\"34pt\",\n    label_text_color=INK_SOFT,\n    glyph_width=60,\n    glyph_height=32,\n    spacing=14,\n    padding=20,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.92,\n    border_line_color=INK_SOFT,\n    border_line_alpha=0.5,\n    border_line_width=1,\n)\np.add_layout(legend)\n\n# Title\np.title.text_font_size = title_fontsize\np.title.align = \"center\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\n# Axis styling\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Grid\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.10\n\n# Background and border\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Save PNG via headless Chrome (Selenium)\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}