{"spec_id":"bifurcation-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbifurcation-basic: Bifurcation Diagram for Dynamical Systems\nLibrary: altair 6.2.1 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — semantic mapping: stable→green, period-doubling→amber(caution), chaos→red\nSTABLE_COLOR = \"#009E73\"  # Imprint position 1 — brand green = stable / ok\nPERIOD_COLOR = \"#DDCC77\"  # Imprint amber anchor — warning / transition\nCHAOS_COLOR = \"#AE3030\"  # Imprint position 5 — matte red = bad / chaos\n\n# Data — Logistic map: x(n+1) = r * x(n) * (1 - x(n))\nnp.random.seed(42)\nr_values = np.linspace(2.5, 4.0, 2000)\ntransient = 200\niterations = 100\n\nr_all = []\nx_all = []\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(iterations):\n        x = r * x * (1.0 - x)\n        r_all.append(r)\n        x_all.append(x)\n\ndf = pd.DataFrame({\"r\": r_all, \"x\": x_all})\n\n# Classify regions for semantic coloring\ndf[\"region\"] = np.where(df[\"r\"] < 3.0, \"Stable\", np.where(df[\"r\"] < 3.57, \"Period-doubling\", \"Chaotic\"))\n\n# Key bifurcation points — staggered y to avoid label overlap near the chaos onset\nbifurcation_points = pd.DataFrame(\n    {\n        \"r\": [3.0, 3.449, 3.544, 3.5699],\n        \"label\": [\"Period 2 (r≈3.0)\", \"Period 4 (r≈3.45)\", \"Period 8\", \"Chaos onset\"],\n        \"y\": [0.90, 0.90, 0.55, 0.82],\n    }\n)\n\n# Selection for interactive crosshair on nearest point\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"r\"], empty=False)\n\n# Semantic color scale from Imprint palette\nregion_color = alt.Scale(\n    domain=[\"Stable\", \"Period-doubling\", \"Chaotic\"], range=[STABLE_COLOR, PERIOD_COLOR, CHAOS_COLOR]\n)\n\n# Base scatter layer — density-adapted size and opacity for 200K points\npoints = (\n    alt.Chart(df)\n    .mark_circle(size=1.5, opacity=0.18)\n    .encode(\n        x=alt.X(\n            \"r:Q\",\n            title=\"Growth Rate (r)\",\n            scale=alt.Scale(domain=[2.5, 4.0], nice=False),\n            axis=alt.Axis(tickCount=7, titleColor=INK, labelColor=INK_SOFT, domain=False),\n        ),\n        y=alt.Y(\n            \"x:Q\",\n            title=\"Steady-State Population (x)\",\n            scale=alt.Scale(domain=[0, 1.0], nice=False),\n            axis=alt.Axis(tickCount=6, titleColor=INK, labelColor=INK_SOFT, domain=False),\n        ),\n        color=alt.Color(\n            \"region:N\",\n            scale=region_color,\n            legend=alt.Legend(\n                title=\"Regime\",\n                titleFontSize=10,\n                labelFontSize=10,\n                orient=\"top-right\",\n                offset=-10,\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                padding=8,\n                cornerRadius=4,\n                labelColor=INK_SOFT,\n                titleColor=INK,\n            ),\n        ),\n        tooltip=[alt.Tooltip(\"r:Q\", title=\"r\", format=\".4f\"), alt.Tooltip(\"x:Q\", title=\"x\", format=\".4f\"), \"region:N\"],\n    )\n)\n\n# Transparent layer for nearest-point selection (voronoi-like hover)\nvoronoi = (\n    alt.Chart(df.sample(n=5000, random_state=42))\n    .mark_point(size=1, opacity=0)\n    .encode(x=\"r:Q\", y=\"x:Q\")\n    .add_params(nearest)\n)\n\n# Vertical crosshair following pointer\ncrosshair = (\n    alt.Chart(df.sample(n=5000, random_state=42))\n    .mark_rule(color=INK_SOFT, strokeWidth=1.5, opacity=0.5)\n    .encode(x=\"r:Q\")\n    .transform_filter(nearest)\n)\n\n# Dashed vertical rules at bifurcation points\nrules = (\n    alt.Chart(bifurcation_points)\n    .mark_rule(strokeDash=[5, 4], strokeWidth=1.2, opacity=0.30, color=INK_MUTED)\n    .encode(x=\"r:Q\")\n)\n\n# Rotated text labels at bifurcation points — fontSize=12 for clear readability at full res\nlabels = (\n    alt.Chart(bifurcation_points)\n    .mark_text(fontSize=12, fontWeight=\"bold\", color=INK_SOFT, angle=270, align=\"left\", dx=0, dy=-8)\n    .encode(x=\"r:Q\", y=\"y:Q\", text=\"label:N\")\n)\n\n# Compose layers with theme-adaptive chrome\nchart = (\n    (points + voronoi + crosshair + rules + labels)\n    .properties(\n        width=620,\n        height=275,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"bifurcation-basic · python · altair · anyplot.ai\",\n            fontSize=16,\n            fontWeight=\"bold\",\n            color=INK,\n            subtitle=\"Logistic map period-doubling cascade from stability to chaos\",\n            subtitleFontSize=11,\n            subtitleColor=INK_SOFT,\n            subtitlePadding=4,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titlePadding=10,\n        grid=True,\n        gridOpacity=0.15,\n        gridColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n    )\n    .configure_title(color=INK)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\n# Save PNG then pad to exactly 3200×1800 (landscape target)\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        \"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.save(f\"plot-{THEME}.html\")\n"}