Skip to content

Commit 9fc1e6c

Browse files
feat(pygal): implement bifurcation-basic (#5107)
## Implementation: `bifurcation-basic` - pygal Implements the **pygal** version of `bifurcation-basic`. **File:** `plots/bifurcation-basic/implementations/pygal.py` **Parent Issue:** #4415 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23361206415)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 3f6cdd5 commit 9fc1e6c

2 files changed

Lines changed: 399 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
""" pyplots.ai
2+
bifurcation-basic: Bifurcation Diagram for Dynamical Systems
3+
Library: pygal 3.1.0 | Python 3.14.3
4+
Quality: 90/100 | Created: 2026-03-20
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data — logistic map x(n+1) = r * x(n) * (1 - x(n))
13+
np.random.seed(42)
14+
transient = 200
15+
iterations = 100
16+
x0 = 0.1 + np.random.uniform(-0.01, 0.01)
17+
18+
# Key bifurcation thresholds
19+
R_PERIOD2 = 3.0
20+
R_PERIOD4 = 3.449
21+
R_PERIOD8 = 3.544
22+
R_CHAOS = 3.57
23+
24+
# Variable-density sampling: more points in complex regions
25+
r_stable = np.linspace(2.5, R_PERIOD2, 250)
26+
r_periodic = np.linspace(R_PERIOD2, R_CHAOS, 500)
27+
r_chaotic = np.linspace(R_CHAOS, 4.0, 700)
28+
r_values = np.concatenate([r_stable, r_periodic, r_chaotic])
29+
30+
# Colorblind-safe palette: navy blue, burnt orange, deep violet (no blue-green confusion)
31+
regions = {
32+
"Stable Fixed Point": (2.5, R_PERIOD2, "#1b5e8a"),
33+
"Period-Doubling Cascade": (R_PERIOD2, R_CHAOS, "#d55e00"),
34+
"Chaotic Regime": (R_CHAOS, 4.0, "#7b2d8e"),
35+
}
36+
37+
region_data = {name: [] for name in regions}
38+
39+
for r in r_values:
40+
x = x0
41+
for _ in range(transient):
42+
x = r * x * (1.0 - x)
43+
for _ in range(iterations):
44+
x = r * x * (1.0 - x)
45+
for name, (lo, hi, _) in regions.items():
46+
if lo <= r < hi or (name == "Chaotic Regime" and r == 4.0):
47+
region_data[name].append(
48+
{"value": (round(float(r), 5), round(float(x), 5)), "label": f"r={r:.4f}, x={x:.4f}"}
49+
)
50+
break
51+
52+
# Downsample each region to balance visual density
53+
max_per_region = {"Stable Fixed Point": 6000, "Period-Doubling Cascade": 18000, "Chaotic Regime": 28000}
54+
for name in region_data:
55+
pts = region_data[name]
56+
cap = max_per_region[name]
57+
if len(pts) > cap:
58+
idx = np.random.choice(len(pts), cap, replace=False)
59+
idx.sort()
60+
region_data[name] = [pts[i] for i in idx]
61+
62+
# Publication-quality style with high-contrast colorblind-safe palette
63+
font = "'Helvetica Neue', 'DejaVu Sans', Helvetica, Arial, sans-serif"
64+
region_colors = tuple(c for _, (_, _, c) in regions.items())
65+
annotation_color = "#888888"
66+
all_colors = region_colors + (annotation_color,)
67+
68+
custom_style = Style(
69+
background="white",
70+
plot_background="#f7f7f7",
71+
foreground="#333333",
72+
foreground_strong="#111111",
73+
foreground_subtle="#dddddd",
74+
guide_stroke_color="#e0e0e0",
75+
guide_stroke_dasharray="3, 8",
76+
major_guide_stroke_dasharray="2, 4",
77+
colors=all_colors,
78+
font_family=font,
79+
title_font_family=font,
80+
title_font_size=52,
81+
label_font_size=40,
82+
major_label_font_size=36,
83+
legend_font_size=30,
84+
legend_font_family=font,
85+
value_font_size=26,
86+
tooltip_font_size=28,
87+
tooltip_font_family=font,
88+
opacity=0.55,
89+
opacity_hover=1.0,
90+
)
91+
92+
# Chart with pygal-specific features: secondary series, custom formatters, interpolation config
93+
chart = pygal.XY(
94+
width=4800,
95+
height=2700,
96+
style=custom_style,
97+
title="bifurcation-basic · pygal · pyplots.ai",
98+
x_title="Growth Rate Parameter (r)",
99+
y_title="Steady-State Population (xₙ)",
100+
show_legend=True,
101+
legend_at_bottom=True,
102+
legend_at_bottom_columns=4,
103+
legend_box_size=22,
104+
stroke=False,
105+
dots_size=1.8,
106+
show_x_guides=True,
107+
show_y_guides=True,
108+
show_y_minor_guides=True,
109+
x_value_formatter=lambda v: f"{v:.3f}",
110+
value_formatter=lambda v: f"{v:.4f}",
111+
margin_bottom=110,
112+
margin_left=70,
113+
margin_right=50,
114+
margin_top=55,
115+
xrange=(2.5, 4.0),
116+
range=(0.0, 1.0),
117+
print_values=False,
118+
print_zeroes=False,
119+
js=[],
120+
x_labels=[2.5, R_PERIOD2, 3.2, R_PERIOD4, R_PERIOD8, 3.7, 3.8, 4.0],
121+
x_labels_major=[R_PERIOD2, R_PERIOD4, R_PERIOD8],
122+
y_labels=[0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
123+
truncate_legend=-1,
124+
no_data_text="",
125+
show_x_labels=True,
126+
show_y_labels=True,
127+
dynamic_print_values=True,
128+
allow_interruptions=True,
129+
show_minor_x_labels=True,
130+
spacing=25,
131+
inner_radius=0,
132+
include_x_axis=True,
133+
)
134+
135+
# Add each region as a separate series with per-point tooltip metadata
136+
for name in regions:
137+
lo, hi, _ = regions[name]
138+
chart.add(
139+
f"{name} (r\u2248{lo:.1f}\u2013{hi:.2f})",
140+
region_data[name],
141+
stroke=False,
142+
show_dots=True,
143+
allow_interruptions=True,
144+
)
145+
146+
# Annotation markers at key bifurcation points — dashed vertical lines in one legend entry
147+
annotation_points = [
148+
(R_PERIOD2, "r\u22483.0: Period-2 onset"),
149+
(R_PERIOD4, "r\u22483.449: Period-4 onset"),
150+
(R_PERIOD8, "r\u22483.544: Period-8 onset"),
151+
]
152+
153+
annotation_data = []
154+
for r_val, label in annotation_points:
155+
annotation_data.append({"value": (r_val, 0.0), "label": label})
156+
annotation_data.append({"value": (r_val, 1.0), "label": label})
157+
annotation_data.append(None)
158+
159+
chart.add(
160+
"Bifurcation Points",
161+
annotation_data,
162+
stroke=True,
163+
stroke_style={"width": 2.5, "dasharray": "10, 5"},
164+
show_dots=False,
165+
dots_size=0,
166+
secondary=True,
167+
)
168+
169+
# Dual render: PNG for static preview, HTML for pygal's native SVG interactivity with tooltips
170+
chart.render_to_png("plot.png")
171+
chart.render_to_file("plot.html")

0 commit comments

Comments
 (0)