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