{"spec_id":"campbell-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ncampbell-basic: Campbell Diagram\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_cartesian,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_point,\n    geom_rect,\n    geom_text,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_linetype_manual,\n    scale_size_identity,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens\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# anyplot categorical palette — 5 hues, canonical order, first always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\nCRITICAL_COLOR = \"#AE3030\"  # semantic red — critical speed / danger\nBAND_COLOR = \"#4467A3\"  # blue — operating range reference zone\n\n# Data — natural frequencies vs rotational speed for turbomachinery\nnp.random.seed(42)\nspeed = np.linspace(0, 6000, 80)\n\nmodes = {\n    \"1st Bending\": 18 + speed * 0.0015 + np.random.normal(0, 0.12, len(speed)),\n    \"2nd Bending\": 45 - speed * 0.002 + np.random.normal(0, 0.12, len(speed)),\n    \"1st Torsional\": 52 + speed * 0.0025 + np.random.normal(0, 0.12, len(speed)),\n    \"2nd Torsional\": 75 + speed * 0.001 + np.random.normal(0, 0.12, len(speed)),\n    \"Axial\": 90 - speed * 0.0004 + np.random.normal(0, 0.12, len(speed)),\n}\n\nmode_names = list(modes.keys())\nmode_colors = dict(zip(mode_names, IMPRINT_PALETTE, strict=True))\n\ndf_modes = pd.DataFrame(\n    [\n        {\"Speed\": s, \"Frequency\": f, \"Mode\": name}\n        for name, freqs in modes.items()\n        for s, f in zip(speed, freqs, strict=True)\n    ]\n)\n\n# Engine order lines clipped to useful frequency range (≤105 Hz)\nengine_orders = [1, 2, 3]\neo_names = [f\"{o}x EO\" for o in engine_orders]\ndf_eo = pd.DataFrame(\n    [\n        {\"Speed\": s, \"Frequency\": order * s / 60, \"Mode\": f\"{order}x EO\"}\n        for order in engine_orders\n        for s in speed\n        if order * s / 60 <= 105\n    ]\n)\n\n# Critical speed intersections (EO line crosses natural frequency curve)\ncritical_points = []\nfor order in engine_orders:\n    eo_freq = order * speed / 60\n    for freq_values in modes.values():\n        diff = eo_freq - freq_values\n        sign_changes = np.where(np.diff(np.sign(diff)))[0]\n        for idx in sign_changes:\n            s0, s1 = speed[idx], speed[idx + 1]\n            f0_eo, f1_eo = eo_freq[idx], eo_freq[idx + 1]\n            f0_m, f1_m = freq_values[idx], freq_values[idx + 1]\n            denom = (f1_eo - f0_eo) - (f1_m - f0_m)\n            if abs(denom) > 1e-10:\n                t = (f0_m - f0_eo) / denom\n                cs = s0 + t * (s1 - s0)\n                cf = f0_eo + t * (f1_eo - f0_eo)\n                if 0 < cs < 6000 and 0 < cf < 110:\n                    critical_points.append({\"Speed\": cs, \"Frequency\": cf})\ndf_critical = pd.DataFrame(critical_points)\n\n# 1x / 1st Bending critical speed — most operationally significant\neo1_freq = speed / 60\ndiff_1b = eo1_freq - modes[\"1st Bending\"]\nsc_idx = np.where(np.diff(np.sign(diff_1b)))[0]\nannot_speed = annot_freq = None\nif len(sc_idx) > 0:\n    idx = sc_idx[0]\n    denom = (eo1_freq[idx + 1] - eo1_freq[idx]) - (modes[\"1st Bending\"][idx + 1] - modes[\"1st Bending\"][idx])\n    if abs(denom) > 1e-10:\n        t = (modes[\"1st Bending\"][idx] - eo1_freq[idx]) / denom\n        annot_speed = speed[idx] + t * (speed[idx + 1] - speed[idx])\n        annot_freq = eo1_freq[idx] + t * (eo1_freq[idx + 1] - eo1_freq[idx])\n\n# Combine lines — mode lines thicker than EO lines\ndf_lines = pd.concat([df_modes, df_eo], ignore_index=True)\ndf_lines[\"_lw\"] = df_lines[\"Mode\"].apply(lambda m: 1.5 if \"EO\" not in m else 0.8)\n\n# Color / linetype mappings — all EO entries in color_map for scale coverage\ncolor_map = {**mode_colors, **dict.fromkeys(eo_names, INK_MUTED)}\nltype_map = {**dict.fromkeys(mode_names, \"solid\"), **dict.fromkeys(eo_names, \"dashed\")}\n\n# Legend shows 5 modes + one consolidated EO entry\nbreaks = mode_names + eo_names[:1]\nlabels = mode_names + [\"Engine Order (1×, 2×, 3×)\"]\n\n# Operating range band (nominal: 2000–4500 RPM)\ndf_band = pd.DataFrame([{\"xmin\": 2000, \"xmax\": 4500, \"ymin\": 0, \"ymax\": 110}])\n\n# EO italic labels along each line (geom_text size is in mm, not pt)\neo_labels = pd.DataFrame(\n    [\n        {\"Speed\": 4500, \"Frequency\": 4500 / 60 + 3, \"label\": \"1×\"},\n        {\"Speed\": 2200, \"Frequency\": 2 * 2200 / 60 + 3, \"label\": \"2×\"},\n        {\"Speed\": 1500, \"Frequency\": 3 * 1500 / 60 + 3, \"label\": \"3×\"},\n    ]\n)\n\n# Plot\ntitle = \"campbell-basic · python · plotnine · anyplot.ai\"\n\nplot = (\n    ggplot(df_lines, aes(\"Speed\", \"Frequency\", color=\"Mode\", linetype=\"Mode\", group=\"Mode\"))\n    # Operating range shading\n    + geom_rect(\n        df_band,\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        fill=BAND_COLOR,\n        alpha=0.13,\n        color=\"none\",\n        inherit_aes=False,\n    )\n    # Natural frequency curves + EO lines\n    + geom_line(aes(size=\"_lw\"))\n    + scale_size_identity()\n    # Critical speed intersection markers\n    + geom_point(\n        df_critical,\n        aes(\"Speed\", \"Frequency\"),\n        color=CRITICAL_COLOR,\n        fill=CRITICAL_COLOR,\n        size=4.0,\n        shape=\"D\",\n        stroke=0.8,\n        inherit_aes=False,\n        show_legend=False,\n    )\n    # EO order labels along diagonal lines (size in mm)\n    + geom_text(\n        eo_labels,\n        aes(\"Speed\", \"Frequency\", label=\"label\"),\n        color=INK_MUTED,\n        size=3.5,\n        fontstyle=\"italic\",\n        fontweight=\"bold\",\n        inherit_aes=False,\n        show_legend=False,\n    )\n    # Unified color/linetype scales with consolidated EO legend entry\n    + scale_color_manual(values=color_map, breaks=breaks, labels=labels)\n    + scale_linetype_manual(values=ltype_map, breaks=breaks, labels=labels)\n    + guides(color=guide_legend(override_aes={\"size\": [1.5] * 5 + [0.8]}), linetype=guide_legend())\n    + scale_x_continuous(breaks=range(0, 7000, 1000))\n    + scale_y_continuous(breaks=range(0, 111, 10))\n    + coord_cartesian(xlim=(0, 6200), ylim=(0, 108))\n    + labs(x=\"Rotational Speed (RPM)\", y=\"Natural Frequency (Hz)\", title=title)\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\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, size=0.3, alpha=0.12),\n        panel_grid_minor=element_blank(),\n        panel_border=element_blank(),\n        axis_line=element_line(color=INK_SOFT, size=0.3),\n        plot_title=element_text(size=12, ha=\"center\", face=\"bold\", color=INK),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_blank(),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.3),\n        legend_key_width=30,\n        legend_key_height=15,\n        legend_position=\"bottom\",\n        legend_direction=\"horizontal\",\n    )\n)\n\n# Annotate most operationally significant critical speed (1x / 1st Bending)\nif annot_speed is not None:\n    plot = (\n        plot\n        + annotate(\n            \"segment\",\n            x=annot_speed,\n            xend=annot_speed,\n            y=0,\n            yend=annot_freq,\n            color=CRITICAL_COLOR,\n            linetype=\"dotted\",\n            size=0.6,\n            alpha=0.7,\n        )\n        + annotate(\n            \"text\",\n            x=annot_speed + 180,\n            y=annot_freq + 5,\n            label=f\"Critical: {int(round(annot_speed))} RPM\",\n            color=CRITICAL_COLOR,\n            size=3.5,\n            ha=\"left\",\n            fontstyle=\"italic\",\n        )\n    )\n\n# Operating range label\nplot = plot + annotate(\n    \"text\", x=3250, y=104, label=\"Operating Range\", color=BAND_COLOR, size=3.5, alpha=0.8, fontweight=\"bold\"\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}