{"spec_id":"bifurcation-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nbifurcation-basic: Bifurcation Diagram for Dynamical Systems\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\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# Data — Logistic map: x(n+1) = r * x(n) * (1 - x(n))\nr_values = np.linspace(2.5, 4.0, 5000)\ntransient = 300\nn_plot = 200\n\nr_all = np.empty(len(r_values) * n_plot)\nx_all = np.empty(len(r_values) * n_plot)\n\nidx = 0\nfor r in r_values:\n    x = 0.5\n    for _ in range(transient):\n        x = r * x * (1.0 - x)\n    for _ in range(n_plot):\n        x = r * x * (1.0 - x)\n        r_all[idx] = r\n        x_all[idx] = x\n        idx += 1\n\n# Classify the route to chaos for color storytelling.\nregime = np.where(r_all < 3.0, \"Stable\", np.where(r_all < 3.57, \"Period-Doubling\", \"Chaos\"))\n\ndf = pd.DataFrame({\"r\": r_all, \"x\": x_all, \"Regime\": regime})\n\n# Imprint palette — semantic mapping: stable→brand green (calm), chaos→matte red\n# (semantic anchor for the extreme/disordered regime), period-doubling→blue between.\npalette = {\n    \"Stable\": \"#009E73\",  # Imprint position 1 (brand) — first series\n    \"Period-Doubling\": \"#4467A3\",  # Imprint position 3 (blue)\n    \"Chaos\": \"#AE3030\",  # Imprint position 5 (matte red, semantic: chaotic/extreme)\n}\nregime_order = [\"Stable\", \"Period-Doubling\", \"Chaos\"]\n\n# JointGrid with a marginal KDE — a distinctive seaborn feature that reveals the\n# density structure of each regime alongside the bifurcation cascade.\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"font.family\": \"sans-serif\",\n    },\n)\n\ng = sns.JointGrid(data=df, x=\"r\", y=\"x\", ratio=8, space=0.06, marginal_ticks=False)\ng.figure.set_size_inches(8, 4.5)  # × dpi 400 → 3200 × 1800 px\ng.figure.set_dpi(400)\ng.figure.set_facecolor(PAGE_BG)\n\nax = g.ax_joint\nax.set_facecolor(PAGE_BG)\ng.ax_marg_y.set_facecolor(PAGE_BG)\n\n# Main plot: density-aware scatter per regime. The stable branch has few unique\n# points per r → larger markers; the chaotic fan is dense → tiny markers + alpha.\nfor regime_name, marker_s, marker_alpha in [(\"Stable\", 2.0, 0.85), (\"Period-Doubling\", 0.5, 0.55), (\"Chaos\", 0.5, 0.5)]:\n    subset = df[df[\"Regime\"] == regime_name]\n    sns.scatterplot(\n        data=subset,\n        x=\"r\",\n        y=\"x\",\n        color=palette[regime_name],\n        s=marker_s,\n        alpha=marker_alpha,\n        linewidth=0,\n        edgecolor=\"none\",\n        ax=ax,\n        rasterized=True,\n        legend=False,\n    )\n\n# Marginal KDE on the y-axis: per-regime density of steady-state values.\nfor regime_name in regime_order:\n    subset = df[df[\"Regime\"] == regime_name]\n    sns.kdeplot(\n        y=subset[\"x\"], color=palette[regime_name], fill=True, alpha=0.22, linewidth=1.5, ax=g.ax_marg_y, clip=(0, 1)\n    )\n\n# Remove the x-marginal — uniform r sampling adds no density insight.\ng.ax_marg_x.set_visible(False)\n\n# Annotate key period-doubling bifurcation points. Labels sit in the empty upper\n# band and are spread horizontally (Period-2/4 share the gap before r≈3.45, Period-8\n# to the right) so no two label boxes intersect and none collide with the legend.\nbifurcation_points = [\n    (3.0, \"Period-2\\nr ≈ 3.0\", 3.02, 0.96, \"left\"),\n    (3.449, \"Period-4\\nr ≈ 3.449\", 3.43, 0.96, \"right\"),\n    (3.544, \"Period-8\\nr ≈ 3.544\", 3.60, 0.96, \"left\"),\n]\n\nfor r_bif, label, x_text, y_text, ha in bifurcation_points:\n    ax.axvline(r_bif, color=INK, linewidth=0.8, linestyle=\"--\", alpha=0.3)\n    ax.annotate(\n        label,\n        xy=(r_bif, y_text),\n        xytext=(x_text, y_text),\n        fontsize=9,\n        color=INK_SOFT,\n        ha=ha,\n        va=\"center\",\n        arrowprops={\"arrowstyle\": \"-\", \"color\": INK_MUTED, \"lw\": 0.8},\n    )\n\n# Style\nax.set_title(\"bifurcation-basic · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK, pad=10)\nax.set_xlabel(\"Growth Rate (r)\", fontsize=10, color=INK)\nax.set_ylabel(\"Steady-State Population (x)\", fontsize=10, color=INK)\nax.set_xlim(2.5, 4.0)\nax.set_ylim(0, 1)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nsns.despine(ax=ax)\nsns.despine(ax=g.ax_marg_y, bottom=True, left=True)\n\n# Legend with readable marker proxies (the scatter markers themselves are sub-pixel).\nhandles = [\n    Line2D(\n        [0],\n        [0],\n        marker=\"o\",\n        linestyle=\"\",\n        markersize=7,\n        markerfacecolor=palette[name],\n        markeredgecolor=\"none\",\n        label=name,\n    )\n    for name in regime_order\n]\nlegend = ax.legend(handles=handles, title=\"Regime\", loc=\"upper left\", fontsize=8, title_fontsize=9, framealpha=0.9)\nlegend.get_frame().set_facecolor(ELEVATED_BG)\nlegend.get_frame().set_edgecolor(INK_SOFT)\nlegend.get_title().set_color(INK)\nfor text in legend.get_texts():\n    text.set_color(INK_SOFT)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}