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