|
| 1 | +""" pyplots.ai |
| 2 | +line-parametric: Parametric Curve Plot |
| 3 | +Library: pygal 3.1.0 | Python 3.14.3 |
| 4 | +Quality: 80/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 — parametric curves with parameter t |
| 13 | +n_points = 1000 |
| 14 | + |
| 15 | +# Lissajous figure: x = sin(3t), y = sin(2t), t ∈ [0, 2π] |
| 16 | +t_liss = np.linspace(0, 2 * np.pi, n_points) |
| 17 | +lissajous_x = np.sin(3 * t_liss) |
| 18 | +lissajous_y = np.sin(2 * t_liss) |
| 19 | + |
| 20 | +# Spiral: x = t·cos(t), y = t·sin(t), t ∈ [0, 4π], normalized to [-1,1] |
| 21 | +t_spiral = np.linspace(0, 4 * np.pi, n_points) |
| 22 | +raw_spiral_x = t_spiral * np.cos(t_spiral) |
| 23 | +raw_spiral_y = t_spiral * np.sin(t_spiral) |
| 24 | +spiral_max = max(np.abs(raw_spiral_x).max(), np.abs(raw_spiral_y).max()) |
| 25 | +spiral_x = raw_spiral_x / spiral_max |
| 26 | +spiral_y = raw_spiral_y / spiral_max |
| 27 | + |
| 28 | +# Split each curve into 4 segments for smooth color gradient |
| 29 | +n_segs = 4 |
| 30 | +seg_size = n_points // n_segs |
| 31 | + |
| 32 | +# Lissajous gradient: deep blue → steel blue → burnt orange → warm amber |
| 33 | +liss_colors = ["#08306b", "#2171b5", "#d94701", "#fd8d3c"] |
| 34 | +liss_labels = ["Lissajous 0 → π/2", "Lissajous π/2 → π", "Lissajous π → 3π/2", "Lissajous 3π/2 → 2π"] |
| 35 | + |
| 36 | +# Spiral gradient: deep purple → orchid → teal → forest green |
| 37 | +spi_colors = ["#4a1486", "#807dba", "#006d5b", "#41ab5d"] |
| 38 | +spi_labels = ["Spiral 0 → π", "Spiral π → 2π", "Spiral 2π → 3π", "Spiral 3π → 4π"] |
| 39 | + |
| 40 | +all_colors = liss_colors + spi_colors + ["#d62728", "#d62728", "#1a1a1a", "#1a1a1a"] |
| 41 | + |
| 42 | +# Style — polished design with explicit font sizing |
| 43 | +font = "DejaVu Sans, Helvetica, Arial, sans-serif" |
| 44 | + |
| 45 | +custom_style = Style( |
| 46 | + background="white", |
| 47 | + plot_background="#f7f7f7", |
| 48 | + foreground="#2d2d2d", |
| 49 | + foreground_strong="#1a1a1a", |
| 50 | + foreground_subtle="#d9d9d9", |
| 51 | + guide_stroke_color="#e8e8e8", |
| 52 | + major_guide_stroke_color="#d0d0d0", |
| 53 | + colors=tuple(all_colors), |
| 54 | + font_family=font, |
| 55 | + title_font_family=font, |
| 56 | + title_font_size=52, |
| 57 | + label_font_size=36, |
| 58 | + major_label_font_size=32, |
| 59 | + legend_font_size=30, |
| 60 | + legend_font_family=font, |
| 61 | + value_font_size=28, |
| 62 | + tooltip_font_size=28, |
| 63 | + tooltip_font_family=font, |
| 64 | + opacity=0.95, |
| 65 | + opacity_hover=1.0, |
| 66 | + stroke_opacity=1.0, |
| 67 | + stroke_opacity_hover=1.0, |
| 68 | + stroke_width=8, |
| 69 | +) |
| 70 | + |
| 71 | + |
| 72 | +# Custom tooltip formatter showing parameter value |
| 73 | +def fmt_tooltip(x, y, t_val, curve): |
| 74 | + return f"{curve}: t={t_val:.2f} → ({x:.3f}, {y:.3f})" |
| 75 | + |
| 76 | + |
| 77 | +# Chart — square 3600×3600 for geometric accuracy |
| 78 | +chart = pygal.XY( |
| 79 | + width=3600, |
| 80 | + height=3600, |
| 81 | + style=custom_style, |
| 82 | + title="line-parametric · pygal · pyplots.ai", |
| 83 | + x_title="Horizontal Position x(t)", |
| 84 | + y_title="Vertical Position y(t)", |
| 85 | + show_legend=True, |
| 86 | + legend_at_bottom=True, |
| 87 | + legend_at_bottom_columns=4, |
| 88 | + legend_box_size=22, |
| 89 | + stroke=True, |
| 90 | + show_dots=False, |
| 91 | + show_x_guides=True, |
| 92 | + show_y_guides=True, |
| 93 | + x_value_formatter=lambda v: f"{v:.1f}", |
| 94 | + value_formatter=lambda v: f"{v:.1f}", |
| 95 | + margin_bottom=200, |
| 96 | + margin_left=100, |
| 97 | + margin_right=80, |
| 98 | + margin_top=80, |
| 99 | + truncate_legend=-1, |
| 100 | + print_values=False, |
| 101 | + xrange=(-1.3, 1.3), |
| 102 | + range=(-1.3, 1.3), |
| 103 | + js=[], |
| 104 | + include_x_axis=True, |
| 105 | + dots_size=0, |
| 106 | +) |
| 107 | + |
| 108 | +# Lissajous — 4 segments for smooth cool-to-warm gradient |
| 109 | +for k in range(n_segs): |
| 110 | + start = k * seg_size |
| 111 | + end = min((k + 1) * seg_size + 1, n_points) |
| 112 | + seg_data = [ |
| 113 | + { |
| 114 | + "value": (float(lissajous_x[i]), float(lissajous_y[i])), |
| 115 | + "label": fmt_tooltip(lissajous_x[i], lissajous_y[i], t_liss[i], "Lissajous"), |
| 116 | + } |
| 117 | + for i in range(start, end) |
| 118 | + ] |
| 119 | + chart.add( |
| 120 | + liss_labels[k], seg_data, stroke_style={"width": 10, "linecap": "round", "linejoin": "round"}, show_dots=False |
| 121 | + ) |
| 122 | + |
| 123 | +# Spiral — 4 segments for smooth purple-to-green gradient |
| 124 | +for k in range(n_segs): |
| 125 | + start = k * seg_size |
| 126 | + end = min((k + 1) * seg_size + 1, n_points) |
| 127 | + seg_data = [ |
| 128 | + { |
| 129 | + "value": (float(spiral_x[i]), float(spiral_y[i])), |
| 130 | + "label": fmt_tooltip(spiral_x[i], spiral_y[i], t_spiral[i], "Spiral"), |
| 131 | + } |
| 132 | + for i in range(start, end) |
| 133 | + ] |
| 134 | + chart.add( |
| 135 | + spi_labels[k], seg_data, stroke_style={"width": 8, "linecap": "round", "linejoin": "round"}, show_dots=False |
| 136 | + ) |
| 137 | + |
| 138 | +# Start points — large red dots at t=0, offset slightly so both are visible |
| 139 | +# Lissajous starts at (0, 0), Spiral starts at (0, 0) — show side by side |
| 140 | +chart.add( |
| 141 | + "▶ Lissajous start", |
| 142 | + [{"value": (float(lissajous_x[0]), float(lissajous_y[0])), "label": "Lissajous t=0"}], |
| 143 | + stroke=False, |
| 144 | + dots_size=30, |
| 145 | + show_dots=True, |
| 146 | +) |
| 147 | +chart.add( |
| 148 | + "▶ Spiral start", |
| 149 | + [{"value": (float(spiral_x[0]) + 0.03, float(spiral_y[0]) + 0.03), "label": "Spiral t=0 (origin)"}], |
| 150 | + stroke=False, |
| 151 | + dots_size=26, |
| 152 | + show_dots=True, |
| 153 | +) |
| 154 | + |
| 155 | +# End points — dark square markers at curve endpoints (distinct positions) |
| 156 | +chart.add( |
| 157 | + "◼ Lissajous end", |
| 158 | + [{"value": (float(lissajous_x[-1]), float(lissajous_y[-1])), "label": "Lissajous t=2π"}], |
| 159 | + stroke=False, |
| 160 | + dots_size=26, |
| 161 | + show_dots=True, |
| 162 | +) |
| 163 | +chart.add( |
| 164 | + "◼ Spiral end", |
| 165 | + [{"value": (float(spiral_x[-1]), float(spiral_y[-1])), "label": "Spiral t=4π"}], |
| 166 | + stroke=False, |
| 167 | + dots_size=26, |
| 168 | + show_dots=True, |
| 169 | +) |
| 170 | + |
| 171 | +# Save |
| 172 | +chart.render_to_png("plot.png") |
| 173 | +chart.render_to_file("plot.html") |
0 commit comments