Skip to content

Commit c8e49ac

Browse files
feat(plotly): implement line-parametric (#5074)
## Implementation: `line-parametric` - plotly Implements the **plotly** version of `line-parametric`. **File:** `plots/line-parametric/implementations/plotly.py` **Parent Issue:** #4424 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23339035336)* --------- 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 4c92a44 commit c8e49ac

2 files changed

Lines changed: 518 additions & 0 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
""" pyplots.ai
2+
line-parametric: Parametric Curve Plot
3+
Library: plotly 6.6.0 | Python 3.14.3
4+
Quality: 87/100 | Created: 2026-03-20
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
from plotly.subplots import make_subplots
10+
11+
12+
# Data
13+
n_points = 2000
14+
n_segments = 200
15+
16+
t_lissajous = np.linspace(0, 2 * np.pi, n_points)
17+
x_lissajous = np.sin(3 * t_lissajous)
18+
y_lissajous = np.sin(2 * t_lissajous)
19+
20+
t_spiral = np.linspace(0, 4 * np.pi, n_points)
21+
x_spiral = t_spiral * np.cos(t_spiral)
22+
y_spiral = t_spiral * np.sin(t_spiral)
23+
24+
# Custom colorscale from Python Blue (#306998) through teal to coral (#E4573A)
25+
colorscale = [
26+
[0.0, "rgb(48,105,152)"],
27+
[0.15, "rgb(42,128,148)"],
28+
[0.35, "rgb(68,148,120)"],
29+
[0.55, "rgb(148,138,78)"],
30+
[0.75, "rgb(198,105,62)"],
31+
[1.0, "rgb(228,87,58)"],
32+
]
33+
34+
35+
def interpolate_color(frac):
36+
"""Interpolate RGB color from the custom colorscale at given fraction [0, 1]."""
37+
stops = [0.0, 0.15, 0.35, 0.55, 0.75, 1.0]
38+
colors = [(48, 105, 152), (42, 128, 148), (68, 148, 120), (148, 138, 78), (198, 105, 62), (228, 87, 58)]
39+
for i in range(len(stops) - 1):
40+
if frac <= stops[i + 1]:
41+
local = (frac - stops[i]) / (stops[i + 1] - stops[i])
42+
r = int(colors[i][0] + local * (colors[i + 1][0] - colors[i][0]))
43+
g = int(colors[i][1] + local * (colors[i + 1][1] - colors[i][1]))
44+
b = int(colors[i][2] + local * (colors[i + 1][2] - colors[i][2]))
45+
return f"rgb({r},{g},{b})"
46+
return f"rgb({colors[-1][0]},{colors[-1][1]},{colors[-1][2]})"
47+
48+
49+
# Plot
50+
fig = make_subplots(
51+
rows=1,
52+
cols=2,
53+
subplot_titles=(
54+
"<b>Lissajous Figure</b><br><i>x = sin(3t), y = sin(2t) — closed, self-intersecting curve</i>",
55+
"<b>Archimedean Spiral</b><br><i>x = t·cos(t), y = t·sin(t) — open, expanding outward</i>",
56+
),
57+
horizontal_spacing=0.16,
58+
)
59+
60+
# Draw smooth colored line segments for each curve
61+
for xx, yy, t_vals, col_idx, cb_x, cb_ticks, cb_labels in [
62+
(
63+
x_lissajous,
64+
y_lissajous,
65+
t_lissajous,
66+
1,
67+
0.44,
68+
[0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi],
69+
["0", "π/2", "π", "3π/2", "2π"],
70+
),
71+
(x_spiral, y_spiral, t_spiral, 2, 1.02, [0, np.pi, 2 * np.pi, 3 * np.pi, 4 * np.pi], ["0", "π", "2π", "3π", "4π"]),
72+
]:
73+
# Create smooth gradient by drawing overlapping line segments
74+
seg_size = n_points // n_segments
75+
for i in range(n_segments):
76+
start = i * seg_size
77+
end = min(start + seg_size + 1, n_points)
78+
frac = i / (n_segments - 1)
79+
color = interpolate_color(frac)
80+
fig.add_trace(
81+
go.Scatter(
82+
x=xx[start:end],
83+
y=yy[start:end],
84+
mode="lines",
85+
line={"width": 3.5, "color": color},
86+
showlegend=False,
87+
hoverinfo="skip",
88+
),
89+
row=1,
90+
col=col_idx,
91+
)
92+
93+
# Invisible scatter for colorbar and hover
94+
fig.add_trace(
95+
go.Scatter(
96+
x=xx[::10],
97+
y=yy[::10],
98+
mode="markers",
99+
marker={
100+
"size": 0.1,
101+
"opacity": 0,
102+
"color": t_vals[::10],
103+
"colorscale": colorscale,
104+
"showscale": True,
105+
"colorbar": {
106+
"title": {"text": "Parameter <i>t</i> (rad)", "font": {"size": 18}, "side": "right"},
107+
"tickvals": cb_ticks,
108+
"ticktext": cb_labels,
109+
"tickfont": {"size": 16},
110+
"len": 0.75,
111+
"x": cb_x,
112+
"thickness": 16,
113+
"outlinewidth": 0,
114+
},
115+
},
116+
hovertemplate=("t = %{customdata[0]:.3f} rad<br>x(t) = %{x:.3f}<br>y(t) = %{y:.3f}<extra></extra>"),
117+
customdata=np.column_stack([t_vals[::10]]),
118+
showlegend=False,
119+
),
120+
row=1,
121+
col=col_idx,
122+
)
123+
124+
# Start/end markers and annotations for both curves
125+
markers_config = [(x_lissajous, y_lissajous, 1, "x", "y", "0", "2π"), (x_spiral, y_spiral, 2, "x2", "y2", "0", "4π")]
126+
for xx, yy, col_idx, xref, yref, t_start, t_end in markers_config:
127+
# Start marker
128+
fig.add_trace(
129+
go.Scatter(
130+
x=[xx[0]],
131+
y=[yy[0]],
132+
mode="markers",
133+
marker={"size": 16, "color": "#306998", "symbol": "circle", "line": {"color": "white", "width": 2.5}},
134+
showlegend=False,
135+
hovertemplate=f"<b>Start</b> (t = {t_start})<extra></extra>",
136+
),
137+
row=1,
138+
col=col_idx,
139+
)
140+
fig.add_annotation(
141+
x=xx[0],
142+
y=yy[0],
143+
text=f"<b>Start</b> (t = {t_start})",
144+
showarrow=True,
145+
arrowhead=0,
146+
arrowwidth=1.5,
147+
arrowcolor="#306998",
148+
ax=55,
149+
ay=-45,
150+
font={"size": 16, "color": "#306998"},
151+
bgcolor="rgba(255,255,255,0.88)",
152+
bordercolor="#306998",
153+
borderwidth=1,
154+
borderpad=4,
155+
xref=xref,
156+
yref=yref,
157+
)
158+
# End marker
159+
fig.add_trace(
160+
go.Scatter(
161+
x=[xx[-1]],
162+
y=[yy[-1]],
163+
mode="markers",
164+
marker={"size": 16, "color": "#E4573A", "symbol": "square", "line": {"color": "white", "width": 2.5}},
165+
showlegend=False,
166+
hovertemplate=f"<b>End</b> (t = {t_end})<extra></extra>",
167+
),
168+
row=1,
169+
col=col_idx,
170+
)
171+
ax_offset = -55 if col_idx == 1 else -60
172+
ay_offset = 45 if col_idx == 1 else -35
173+
fig.add_annotation(
174+
x=xx[-1],
175+
y=yy[-1],
176+
text=f"<b>End</b> (t = {t_end})",
177+
showarrow=True,
178+
arrowhead=0,
179+
arrowwidth=1.5,
180+
arrowcolor="#E4573A",
181+
ax=ax_offset,
182+
ay=ay_offset,
183+
font={"size": 16, "color": "#E4573A"},
184+
bgcolor="rgba(255,255,255,0.88)",
185+
bordercolor="#E4573A",
186+
borderwidth=1,
187+
borderpad=4,
188+
xref=xref,
189+
yref=yref,
190+
)
191+
192+
# Style
193+
fig.update_layout(
194+
title={
195+
"text": "line-parametric · plotly · pyplots.ai",
196+
"font": {"size": 28, "color": "#2a2a2a"},
197+
"x": 0.5,
198+
"xanchor": "center",
199+
"y": 0.97,
200+
},
201+
template="plotly_white",
202+
plot_bgcolor="rgba(248,249,252,1)",
203+
paper_bgcolor="white",
204+
width=1200,
205+
height=600,
206+
margin={"l": 70, "r": 70, "t": 120, "b": 70},
207+
)
208+
209+
# Style subplot titles
210+
for annotation in fig.layout.annotations:
211+
if hasattr(annotation, "text") and ("<b>" in str(annotation.text)):
212+
annotation.font = {"size": 18, "color": "#2a2a2a"}
213+
214+
for col in [1, 2]:
215+
fig.update_xaxes(
216+
title={"text": "Horizontal Position x(t)", "font": {"size": 22, "color": "#444"}, "standoff": 12},
217+
tickfont={"size": 18, "color": "#666"},
218+
showgrid=True,
219+
gridwidth=1,
220+
gridcolor="rgba(0,0,0,0.05)",
221+
zeroline=True,
222+
zerolinewidth=1.5,
223+
zerolinecolor="rgba(0,0,0,0.15)",
224+
showline=True,
225+
linewidth=1,
226+
linecolor="rgba(0,0,0,0.18)",
227+
scaleanchor="y" if col == 1 else "y2",
228+
scaleratio=1,
229+
row=1,
230+
col=col,
231+
)
232+
fig.update_yaxes(
233+
title={"text": "Vertical Position y(t)", "font": {"size": 22, "color": "#444"}, "standoff": 12},
234+
tickfont={"size": 18, "color": "#666"},
235+
showgrid=True,
236+
gridwidth=1,
237+
gridcolor="rgba(0,0,0,0.05)",
238+
zeroline=True,
239+
zerolinewidth=1.5,
240+
zerolinecolor="rgba(0,0,0,0.15)",
241+
showline=True,
242+
linewidth=1,
243+
linecolor="rgba(0,0,0,0.18)",
244+
row=1,
245+
col=col,
246+
)
247+
248+
# Plotly-specific: interactive reset button
249+
fig.update_layout(
250+
updatemenus=[
251+
{
252+
"type": "buttons",
253+
"showactive": True,
254+
"x": 0.5,
255+
"y": -0.12,
256+
"xanchor": "center",
257+
"buttons": [
258+
{
259+
"label": "Reset View",
260+
"method": "relayout",
261+
"args": [
262+
{
263+
"xaxis.autorange": True,
264+
"yaxis.autorange": True,
265+
"xaxis2.autorange": True,
266+
"yaxis2.autorange": True,
267+
}
268+
],
269+
}
270+
],
271+
"font": {"size": 14},
272+
"bgcolor": "rgba(48,105,152,0.08)",
273+
"bordercolor": "#306998",
274+
"borderwidth": 1,
275+
}
276+
]
277+
)
278+
279+
# Save
280+
fig.write_image("plot.png", width=1600, height=900, scale=3)
281+
fig.write_html("plot.html", include_plotlyjs="cdn")

0 commit comments

Comments
 (0)