{"spec_id":"campbell-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncampbell-basic: Campbell Diagram\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: this file is named pygal.py, which shadows the installed\n# pygal package when the script directory is first in sys.path.\n_here = os.path.dirname(os.path.abspath(__file__))\nif _here in sys.path:\n    sys.path.remove(_here)\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — natural frequencies of a rotor system vs rotational speed\nnp.random.seed(42)\nspeed_rpm = np.linspace(0, 6000, 80)\nspeed_hz = speed_rpm / 60\n\n# Natural frequency modes (Hz) with gyroscopic effects\nmode1 = 25 + 0.003 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))\nmode2 = 48 + 0.005 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))\nmode3 = 62 - 0.001 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))\nmode4 = 78 - 0.002 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))\nmode5 = 92 + 0.004 * speed_rpm + np.random.normal(0, 0.15, len(speed_rpm))\n\norders = [1, 2, 3]\nmodes_data = [mode1, mode2, mode3, mode4, mode5]\nmode_names = [\"1st Bending\", \"2nd Bending\", \"1st Torsional\", \"Axial\", \"2nd Torsional\"]\n\n# Find critical speed intersections\ncritical_speeds = []\ncritical_info = []\nfor order in orders:\n    eo_freq = order * speed_hz\n    for mi, mode in enumerate(modes_data):\n        diff = eo_freq - mode\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            rpm_interp = speed_rpm[idx] + frac * (speed_rpm[idx + 1] - speed_rpm[idx])\n            freq_interp = order * rpm_interp / 60\n            if 0 < rpm_interp < 6000:\n                critical_speeds.append((float(rpm_interp), float(freq_interp)))\n                critical_info.append((order, mode_names[mi]))\n\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    guide_stroke_color=INK_MUTED,\n    guide_stroke_dasharray=\"4, 6\",\n    major_guide_stroke_dasharray=\"2, 4\",\n    colors=(\n        \"#009E73\",  # 1st Bending — anyplot green\n        \"#C475FD\",  # 2nd Bending — lavender\n        \"#4467A3\",  # 1st Torsional — blue\n        \"#BD8233\",  # Axial — ochre\n        \"#AE3030\",  # 2nd Torsional — matte red\n        \"#2ABCCD\",  # 1× EO — cyan (clearly distinct from all mode colors)\n        \"#954477\",  # 2× EO — rose\n        \"#99B314\",  # 3× EO — lime\n        \"#DDCC77\",  # Critical Speeds — amber (semantic warning anchor)\n    ),\n    font_family=font,\n    title_font_family=font,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    legend_font_family=font,\n    value_font_size=36,\n    tooltip_font_size=28,\n    tooltip_font_family=font,\n    opacity=1.0,\n    opacity_hover=1.0,\n    stroke_opacity=1.0,\n    stroke_opacity_hover=1.0,\n    stroke_width=2.5,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"campbell-basic · python · pygal · anyplot.ai\",\n    x_title=\"Rotational Speed (RPM)\",\n    y_title=\"Frequency (Hz)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    legend_box_size=20,\n    stroke=True,\n    dots_size=0,\n    show_x_guides=True,\n    show_y_guides=True,\n    x_value_formatter=lambda x: f\"{x:,.0f}\",\n    value_formatter=lambda y: f\"{y:.1f}\",\n    margin_bottom=80,\n    margin_left=100,\n    margin_right=60,\n    margin_top=50,\n    x_label_rotation=0,\n    truncate_legend=-1,\n    range=(0, 130),\n    xrange=(0, 6000),\n    print_values=False,\n    print_zeroes=False,\n    tooltip_fancy_mode=True,\n    js=[],\n)\n\n# Natural frequency mode curves — solid, thick lines with cubic interpolation\nfor mode, label in zip(modes_data, mode_names, strict=True):\n    points = []\n    label_idx = int(len(speed_rpm) * 0.82)\n    for j, (r, f) in enumerate(zip(speed_rpm, mode, strict=True)):\n        if j == label_idx:\n            points.append({\"value\": (float(r), float(f)), \"label\": label})\n        else:\n            points.append((float(r), float(f)))\n    chart.add(label, points, stroke_style={\"width\": 7, \"linecap\": \"round\"}, show_dots=False, interpolate=\"cubic\")\n\n# Engine order lines — dashed with prominent width and clear patterns\neo_labels = [\"1× EO\", \"2× EO\", \"3× EO\"]\neo_dash_patterns = [\"30, 12\", \"20, 8, 6, 8\", \"12, 6\"]\nfor order, eo_label, dash in zip(orders, eo_labels, eo_dash_patterns, strict=True):\n    eo_end_rpm = min(6000.0, 130.0 * 60.0 / order)\n    eo_rpms = np.linspace(0, eo_end_rpm, 40)\n    eo_freqs = order * eo_rpms / 60.0\n    label_idx = int(len(eo_rpms) * 0.70)\n    eo_points = []\n    for j, (r, f) in enumerate(zip(eo_rpms, eo_freqs, strict=True)):\n        if j == label_idx:\n            eo_points.append({\"value\": (float(r), float(f)), \"label\": eo_label})\n        else:\n            eo_points.append((float(r), float(f)))\n    chart.add(eo_label, eo_points, stroke_style={\"width\": 6, \"dasharray\": dash, \"linecap\": \"round\"}, show_dots=False)\n\n# Critical speed markers — amber warning dots with intersection details\ncritical_points = []\nfor pt, info in zip(critical_speeds, critical_info, strict=True):\n    order, mname = info\n    critical_points.append({\"value\": pt, \"label\": f\"{mname} × {order}× EO\\n{pt[0]:.0f} RPM / {pt[1]:.1f} Hz\"})\nchart.add(\"Critical Speeds\", critical_points, stroke=False, dots_size=15)\n\n# Save — write to the script's own directory regardless of working directory\n_out = os.path.join(_here, f\"plot-{THEME}\")\nwith open(f\"{_out}.html\", \"wb\") as f:\n    f.write(chart.render())\nchart.render_to_png(f\"{_out}.png\")\n"}