{"spec_id":"phase-diagram","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nphase-diagram: Phase Diagram (State Space Plot)\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-14\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p and p != \".\" and p != script_dir and not p.endswith(\"implementations\")]\nos.chdir(script_dir)\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, LinearColorMapper\nfrom bokeh.palettes import Viridis256\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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\"\nBRAND = \"#009E73\"\n\n# Data: Damped harmonic oscillator (simple pendulum with friction)\n# dx/dt = v, dv/dt = -omega^2 * x - gamma * v\nnp.random.seed(42)\n\nomega = 2.0\ngamma = 0.3\ndt = 0.02\nn_steps = 800\n\ntrajectories = []\ninitial_conditions = [(2.0, 0.0), (-1.5, 2.0), (0.5, -2.5), (2.5, 1.5)]\n\nfor x0, v0 in initial_conditions:\n    x_traj = [x0]\n    v_traj = [v0]\n    t_traj = [0]\n    x, v = x0, v0\n\n    for i in range(n_steps):\n        ax = -(omega**2) * x - gamma * v\n        x_new = x + v * dt\n        v_new = v + ax * dt\n        x, v = x_new, v_new\n        x_traj.append(x)\n        v_traj.append(v)\n        t_traj.append((i + 1) * dt)\n\n    trajectories.append((x_traj, v_traj, t_traj))\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"phase-diagram · bokeh · anyplot.ai\",\n    x_axis_label=\"Position x (displacement)\",\n    y_axis_label=\"Velocity dx/dt (m/s)\",\n    tools=\"pan,wheel_zoom,box_zoom,reset,hover\",\n    background_fill_color=PAGE_BG,\n    border_fill_color=PAGE_BG,\n)\n\n# Theme-adaptive styling\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\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\n\n# Grid styling\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\np.outline_line_color = INK_SOFT\n\n# Okabe-Ito colors for trajectories (first series is BRAND, then alternates)\ntraj_colors = [BRAND, \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\ncolor_mapper = LinearColorMapper(palette=Viridis256, low=0, high=1)\n\n# Plot each trajectory\nfor idx, (x_traj, v_traj, t_traj) in enumerate(trajectories):\n    t_norm = np.array(t_traj)\n    t_norm = (t_norm - t_norm.min()) / (t_norm.max() - t_norm.min())\n\n    source = ColumnDataSource(\n        data={\"x\": x_traj, \"v\": v_traj, \"t_norm\": t_norm.tolist(), \"t\": t_traj, \"traj_id\": [idx + 1] * len(x_traj)}\n    )\n\n    # Trajectory points with time-based coloring\n    scatter = p.scatter(\n        x=\"x\", y=\"v\", source=source, size=12, color={\"field\": \"t_norm\", \"transform\": color_mapper}, alpha=0.85\n    )\n\n    # Starting point marker\n    p.scatter(\n        x=[x_traj[0]],\n        y=[v_traj[0]],\n        size=25,\n        color=traj_colors[idx],\n        marker=\"circle\",\n        line_color=INK_SOFT,\n        line_width=2,\n        legend_label=f\"Start {idx + 1}: ({initial_conditions[idx][0]}, {initial_conditions[idx][1]})\",\n    )\n\n# Fixed point (equilibrium)\np.scatter(x=[0], y=[0], size=35, color=\"#E63946\", marker=\"x\", line_width=4, legend_label=\"Equilibrium (stable)\")\n\n# Zero velocity line\np.line(\n    x=[-3.5, 3.5],\n    y=[0, 0],\n    line_width=2,\n    line_dash=\"dashed\",\n    line_color=INK_SOFT,\n    alpha=0.6,\n    legend_label=\"Zero velocity (dx/dt = 0)\",\n)\n\n# Configure HoverTool\nhover = p.select_one(HoverTool)\nhover.tooltips = [\n    (\"Position (x)\", \"@x{0.00}\"),\n    (\"Velocity (dx/dt)\", \"@v{0.00}\"),\n    (\"Time (s)\", \"@t{0.0}\"),\n    (\"Trajectory\", \"@traj_id\"),\n]\n\n# Legend styling\np.legend.label_text_font_size = \"20pt\"\np.legend.label_text_color = INK_SOFT\np.legend.location = \"top_right\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.background_fill_alpha = 0.95\np.legend.border_line_color = INK_SOFT\np.legend.border_line_width = 2\np.legend.padding = 20\np.legend.spacing = 15\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with Selenium\nW, H = 4800, 2700\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)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}