Skip to content

Commit 397bf36

Browse files
feat(pygal): implement line-parametric (#5080)
## Implementation: `line-parametric` - pygal Implements the **pygal** version of `line-parametric`. **File:** `plots/line-parametric/implementations/pygal.py` **Parent Issue:** #4424 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23339035335)* --------- 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 0cd1f88 commit 397bf36

2 files changed

Lines changed: 408 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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

Comments
 (0)