Bifurcation Diagram for Dynamical Systems — Seaborn

A bifurcation diagram shows how the steady-state behavior of a dynamical system changes as a control parameter varies. By plotting the long-term values of a state variable against a continuously varied parameter, it reveals transitions from stable fixed points through period-doubling cascades to chaotic regimes. The classic example is the logistic map, where the route to chaos is clearly visible as the growth rate parameter increases.

Bifurcation Diagram for Dynamical Systems rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
bifurcation-basic: Bifurcation Diagram for Dynamical Systems
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 91/100 | Updated: 2026-06-17
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.lines import Line2D


# Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome")
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Data — Logistic map: x(n+1) = r * x(n) * (1 - x(n))
r_values = np.linspace(2.5, 4.0, 5000)
transient = 300
n_plot = 200

r_all = np.empty(len(r_values) * n_plot)
x_all = np.empty(len(r_values) * n_plot)

idx = 0
for r in r_values:
    x = 0.5
    for _ in range(transient):
        x = r * x * (1.0 - x)
    for _ in range(n_plot):
        x = r * x * (1.0 - x)
        r_all[idx] = r
        x_all[idx] = x
        idx += 1

# Classify the route to chaos for color storytelling.
regime = np.where(r_all < 3.0, "Stable", np.where(r_all < 3.57, "Period-Doubling", "Chaos"))

df = pd.DataFrame({"r": r_all, "x": x_all, "Regime": regime})

# Imprint palette — semantic mapping: stable→brand green (calm), chaos→matte red
# (semantic anchor for the extreme/disordered regime), period-doubling→blue between.
palette = {
    "Stable": "#009E73",  # Imprint position 1 (brand) — first series
    "Period-Doubling": "#4467A3",  # Imprint position 3 (blue)
    "Chaos": "#AE3030",  # Imprint position 5 (matte red, semantic: chaotic/extreme)
}
regime_order = ["Stable", "Period-Doubling", "Chaos"]

# JointGrid with a marginal KDE — a distinctive seaborn feature that reveals the
# density structure of each regime alongside the bifurcation cascade.
sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "font.family": "sans-serif",
    },
)

g = sns.JointGrid(data=df, x="r", y="x", ratio=8, space=0.06, marginal_ticks=False)
g.figure.set_size_inches(8, 4.5)  # × dpi 400 → 3200 × 1800 px
g.figure.set_dpi(400)
g.figure.set_facecolor(PAGE_BG)

ax = g.ax_joint
ax.set_facecolor(PAGE_BG)
g.ax_marg_y.set_facecolor(PAGE_BG)

# Main plot: density-aware scatter per regime. The stable branch has few unique
# points per r → larger markers; the chaotic fan is dense → tiny markers + alpha.
for regime_name, marker_s, marker_alpha in [("Stable", 2.0, 0.85), ("Period-Doubling", 0.5, 0.55), ("Chaos", 0.5, 0.5)]:
    subset = df[df["Regime"] == regime_name]
    sns.scatterplot(
        data=subset,
        x="r",
        y="x",
        color=palette[regime_name],
        s=marker_s,
        alpha=marker_alpha,
        linewidth=0,
        edgecolor="none",
        ax=ax,
        rasterized=True,
        legend=False,
    )

# Marginal KDE on the y-axis: per-regime density of steady-state values.
for regime_name in regime_order:
    subset = df[df["Regime"] == regime_name]
    sns.kdeplot(
        y=subset["x"], color=palette[regime_name], fill=True, alpha=0.22, linewidth=1.5, ax=g.ax_marg_y, clip=(0, 1)
    )

# Remove the x-marginal — uniform r sampling adds no density insight.
g.ax_marg_x.set_visible(False)

# Annotate key period-doubling bifurcation points. Labels sit in the empty upper
# band and are spread horizontally (Period-2/4 share the gap before r≈3.45, Period-8
# to the right) so no two label boxes intersect and none collide with the legend.
bifurcation_points = [
    (3.0, "Period-2\nr ≈ 3.0", 3.02, 0.96, "left"),
    (3.449, "Period-4\nr ≈ 3.449", 3.43, 0.96, "right"),
    (3.544, "Period-8\nr ≈ 3.544", 3.60, 0.96, "left"),
]

for r_bif, label, x_text, y_text, ha in bifurcation_points:
    ax.axvline(r_bif, color=INK, linewidth=0.8, linestyle="--", alpha=0.3)
    ax.annotate(
        label,
        xy=(r_bif, y_text),
        xytext=(x_text, y_text),
        fontsize=9,
        color=INK_SOFT,
        ha=ha,
        va="center",
        arrowprops={"arrowstyle": "-", "color": INK_MUTED, "lw": 0.8},
    )

# Style
ax.set_title("bifurcation-basic · python · seaborn · anyplot.ai", fontsize=12, fontweight="medium", color=INK, pad=10)
ax.set_xlabel("Growth Rate (r)", fontsize=10, color=INK)
ax.set_ylabel("Steady-State Population (x)", fontsize=10, color=INK)
ax.set_xlim(2.5, 4.0)
ax.set_ylim(0, 1)
ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT)
sns.despine(ax=ax)
sns.despine(ax=g.ax_marg_y, bottom=True, left=True)

# Legend with readable marker proxies (the scatter markers themselves are sub-pixel).
handles = [
    Line2D(
        [0],
        [0],
        marker="o",
        linestyle="",
        markersize=7,
        markerfacecolor=palette[name],
        markeredgecolor="none",
        label=name,
    )
    for name in regime_order
]
legend = ax.legend(handles=handles, title="Regime", loc="upper left", fontsize=8, title_fontsize=9, framealpha=0.9)
legend.get_frame().set_facecolor(ELEVATED_BG)
legend.get_frame().set_edgecolor(INK_SOFT)
legend.get_title().set_color(INK)
for text in legend.get_texts():
    text.set_color(INK_SOFT)

plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

Part of Bifurcation Diagram for Dynamical Systems on anyplot.ai.

Other implementations