Bifurcation Diagram for Dynamical Systems — lets-plot

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 lets-plot

Python source (lets-plot)

""" anyplot.ai
bifurcation-basic: Bifurcation Diagram for Dynamical Systems
Library: letsplot 4.10.1 | Python 3.13.14
Quality: 92/100 | Updated: 2026-06-17
"""

import os

import numpy as np
import pandas as pd
from lets_plot import (
    LetsPlot,
    aes,
    coord_cartesian,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_point,
    geom_segment,
    geom_text,
    ggplot,
    ggsave,
    ggsize,
    guides,
    labs,
    sampling_pick,
    scale_color_gradient,
    scale_x_continuous,
    scale_y_continuous,
    theme,
    theme_minimal,
)


LetsPlot.setup_html()

THEME = os.getenv("ANYPLOT_THEME", "light")

# Theme-adaptive chrome tokens
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"
GRID_COLOR = "rgba(26, 26, 23, 0.15)" if THEME == "light" else "rgba(240, 239, 232, 0.15)"

# Imprint sequential colormap: brand green → blue (single-polarity continuous)
IMPRINT_SEQ_LOW = "#009E73"
IMPRINT_SEQ_HIGH = "#4467A3"

# Data — Logistic map: x(n+1) = r * x(n) * (1 - x(n))
# Denser sampling in the chaotic regime for richer visualization
r_stable = np.linspace(2.5, 3.45, 600)
r_chaotic = np.linspace(3.45, 4.0, 1600)
r_values = np.concatenate([r_stable, r_chaotic])
transient = 250
iterations = 100

r_all = []
x_all = []

for r in r_values:
    x = 0.5
    for _ in range(transient):
        x = r * x * (1.0 - x)
    for _ in range(iterations):
        x = r * x * (1.0 - x)
        r_all.append(r)
        x_all.append(x)

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

# Key bifurcation points with dashed guide lines
bif_r = [3.0, 3.449, 3.544, 3.5699]
segments_df = pd.DataFrame({"r": bif_r, "ymin": [0.0] * 4, "ymax": [1.0] * 4})

# Stagger labels at different y positions to avoid overlap
labels_df = pd.DataFrame(
    {
        "r": [3.0, 3.449, 3.58, 3.61],
        "x": [0.93, 0.83, 0.73, 0.63],
        "label": ["Period-2", "Period-4", "Period-8", "Chaos"],
    }
)

# Feigenbaum constant annotation near onset of chaos
feigen_df = pd.DataFrame({"r": [3.5699], "x": [0.05], "label": ["δ ≈ 4.669 (Feigenbaum)"]})

plot = (
    ggplot(df, aes(x="r", y="x", color="r"))
    + geom_point(size=0.4, alpha=0.35, tooltips="none", show_legend=False, sampling=sampling_pick(n=220000))
    # Imprint sequential colormap for the continuous r parameter
    + scale_color_gradient(low=IMPRINT_SEQ_LOW, high=IMPRINT_SEQ_HIGH, guide="none")
    + geom_segment(
        aes(x="r", y="ymin", xend="r", yend="ymax"),
        data=segments_df,
        color=INK_SOFT,
        size=0.3,
        linetype="dashed",
        inherit_aes=False,
        tooltips="none",
    )
    + geom_text(
        aes(x="r", y="x", label="label"), data=labels_df, size=4, color=INK_SOFT, hjust=0.5, vjust=0, inherit_aes=False
    )
    + geom_text(
        aes(x="r", y="x", label="label"),
        data=feigen_df,
        size=4.5,
        color=INK_MUTED,
        hjust=0,
        vjust=1,
        fontface="italic",
        nudge_x=0.02,
        inherit_aes=False,
    )
    + guides(color="none")
    + labs(
        x="Growth Rate (r)",
        y="Population (x)",
        title="bifurcation-basic · python · letsplot · anyplot.ai",
        caption="Logistic map: x(n+1) = r · x(n) · (1 − x(n))",
    )
    + scale_x_continuous(breaks=[2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0], expand=[0.02, 0], format=".2f")
    + scale_y_continuous(breaks=[0.0, 0.2, 0.4, 0.6, 0.8, 1.0], expand=[0.02, 0], format=".1f")
    + coord_cartesian(ylim=[0, 1])
    # Canvas: ggsize(800, 450) × scale=4 → 3200×1800 px (landscape)
    + ggsize(800, 450)
    + theme_minimal()
    + theme(
        axis_text=element_text(size=10, color=INK_SOFT),
        axis_title=element_text(size=12, color=INK),
        plot_title=element_text(size=16, color=INK, face="bold"),
        plot_caption=element_text(size=9, color=INK_MUTED, face="italic"),
        panel_grid_major_x=element_line(color=GRID_COLOR, size=0.2),
        panel_grid_major_y=element_line(color=GRID_COLOR, size=0.15),
        panel_grid_minor=element_blank(),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        axis_ticks=element_line(color=INK_SOFT, size=0.3),
        axis_line=element_line(color=INK_SOFT, size=0.4),
        plot_margin=[30, 40, 20, 20],
    )
)

# Save PNG (scale=4 → 3200×1800 px) and interactive HTML
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

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

Other implementations