{"spec_id":"phase-diagram","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nphase-diagram: Phase Diagram (State Space Plot)\nLibrary: letsplot 4.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-14\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nos.chdir(script_dir)\n\nLetsPlot.setup_html()\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\"\n\n# Okabe-Ito colors\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data - Damped harmonic oscillator with distinct parameters\n# dx/dt = v, dv/dt = -omega^2 * x - gamma * v\nnp.random.seed(42)\n\nomega = 1.5  # Natural frequency (different from bokeh's 2.0)\ngamma = 0.5  # Higher damping coefficient (different from bokeh's 0.3)\n\n# Three trajectories from different initial conditions\ntrajectories = []\n\ninitial_conditions = [\n    (2.5, 1.0, \"High position, rising\"),\n    (-2.0, -2.5, \"Left position, falling\"),\n    (0.8, 3.0, \"Near center, rising fast\"),\n]\n\nfor x0, v0, label in initial_conditions:\n    t = np.linspace(0, 12, 400)\n    dt = t[1] - t[0]\n\n    x = np.zeros_like(t)\n    v = np.zeros_like(t)\n    x[0], v[0] = x0, v0\n\n    # Euler integration for damped harmonic oscillator\n    for i in range(1, len(t)):\n        x[i] = x[i - 1] + v[i - 1] * dt\n        v[i] = v[i - 1] + (-(omega**2) * x[i - 1] - gamma * v[i - 1]) * dt\n\n    traj_df = pd.DataFrame({\"x\": x, \"dx_dt\": v, \"time\": t, \"trajectory\": label})\n    trajectories.append(traj_df)\n\ndf = pd.concat(trajectories, ignore_index=True)\n\n# Create phase diagram\nanyplot_theme = theme(\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_SOFT, size=0.3, linetype=\"solid\"),\n    axis_title=element_text(color=INK, size=20),\n    axis_text=element_text(color=INK_SOFT, size=16),\n    plot_title=element_text(color=INK, size=24),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=14),\n    legend_title=element_text(color=INK, size=16),\n    legend_position=\"right\",\n)\n\nplot = (\n    ggplot(df, aes(x=\"x\", y=\"dx_dt\", color=\"trajectory\"))\n    + geom_path(size=1.3, alpha=0.85)\n    + geom_point(\n        mapping=aes(x=\"x\", y=\"dx_dt\"),\n        data=df.groupby(\"trajectory\").head(1),\n        size=7,\n        shape=21,\n        fill=PAGE_BG,\n        stroke=2.5,\n        color=INK_SOFT,\n    )\n    # Mark the fixed point (equilibrium at origin)\n    + geom_point(\n        mapping=aes(x=\"x\", y=\"dx_dt\"),\n        data=pd.DataFrame({\"x\": [0], \"dx_dt\": [0]}),\n        color=INK_SOFT,\n        size=10,\n        shape=4,\n        stroke=3,\n        inherit_aes=False,\n    )\n    + scale_color_manual(values=IMPRINT)\n    + labs(\n        x=\"Position (x)\", y=\"Velocity (dx/dt)\", title=\"phase-diagram · letsplot · anyplot.ai\", color=\"Initial Condition\"\n    )\n    + theme_minimal()\n    + anyplot_theme\n    + ggsize(1600, 900)\n)\n\n# Save PNG (scale 3x for 4800 x 2700)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=3)\n\n# Save interactive HTML\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}