Skip to content

Commit 468c53a

Browse files
feat(plotly): implement phase-diagram (#3051)
## Implementation: `phase-diagram` - plotly Implements the **plotly** version of `phase-diagram`. **File:** `plots/phase-diagram/implementations/plotly.py` **Parent Issue:** #3004 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617610863)* --------- 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 e095d0b commit 468c53a

2 files changed

Lines changed: 214 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
""" pyplots.ai
2+
phase-diagram: Phase Diagram (State Space Plot)
3+
Library: plotly 6.5.0 | Python 3.13.11
4+
Quality: 93/100 | Created: 2025-12-31
5+
"""
6+
7+
import numpy as np
8+
import plotly.graph_objects as go
9+
10+
11+
# Data - Damped pendulum showing spiral convergence to equilibrium
12+
np.random.seed(42)
13+
14+
# Parameters for damped harmonic oscillator: d²x/dt² + 2ζω₀(dx/dt) + ω₀²x = 0
15+
omega_0 = 2.0 # Natural frequency
16+
zeta = 0.15 # Damping ratio (underdamped: 0 < zeta < 1)
17+
18+
# Time array
19+
t = np.linspace(0, 15, 1500)
20+
dt = t[1] - t[0]
21+
22+
# Analytical solution for underdamped oscillator
23+
omega_d = omega_0 * np.sqrt(1 - zeta**2) # Damped frequency
24+
25+
# Initial conditions: x(0) = 2.0, dx/dt(0) = 0
26+
x0 = 2.0
27+
v0 = 0.0
28+
29+
# Solution: x(t) = A * exp(-ζω₀t) * cos(ωd*t - φ)
30+
A = np.sqrt(x0**2 + ((v0 + zeta * omega_0 * x0) / omega_d) ** 2)
31+
phi = np.arctan2((v0 + zeta * omega_0 * x0) / omega_d, x0)
32+
33+
x = A * np.exp(-zeta * omega_0 * t) * np.cos(omega_d * t - phi)
34+
dx_dt = -zeta * omega_0 * A * np.exp(-zeta * omega_0 * t) * np.cos(omega_d * t - phi) - omega_d * A * np.exp(
35+
-zeta * omega_0 * t
36+
) * np.sin(omega_d * t - phi)
37+
38+
# Second trajectory with different initial condition (for basin structure)
39+
x0_2 = -1.5
40+
v0_2 = 3.0
41+
A2 = np.sqrt(x0_2**2 + ((v0_2 + zeta * omega_0 * x0_2) / omega_d) ** 2)
42+
phi2 = np.arctan2((v0_2 + zeta * omega_0 * x0_2) / omega_d, x0_2)
43+
44+
x2 = A2 * np.exp(-zeta * omega_0 * t) * np.cos(omega_d * t - phi2)
45+
dx_dt_2 = -zeta * omega_0 * A2 * np.exp(-zeta * omega_0 * t) * np.cos(omega_d * t - phi2) - omega_d * A2 * np.exp(
46+
-zeta * omega_0 * t
47+
) * np.sin(omega_d * t - phi2)
48+
49+
# Create figure
50+
fig = go.Figure()
51+
52+
# Main trajectory with color gradient for time evolution
53+
fig.add_trace(
54+
go.Scatter(
55+
x=x,
56+
y=dx_dt,
57+
mode="lines+markers",
58+
name="Trajectory 1 (x₀=2.0, v₀=0)",
59+
line=dict(color="#306998", width=3),
60+
marker=dict(size=4, color=t, colorscale="Blues", showscale=False),
61+
hovertemplate="x: %{x:.2f}<br>dx/dt: %{y:.2f}<extra></extra>",
62+
)
63+
)
64+
65+
# Second trajectory
66+
fig.add_trace(
67+
go.Scatter(
68+
x=x2,
69+
y=dx_dt_2,
70+
mode="lines+markers",
71+
name="Trajectory 2 (x₀=-1.5, v₀=3.0)",
72+
line=dict(color="#FFD43B", width=3),
73+
marker=dict(size=4, color=t, colorscale="YlOrBr", showscale=False),
74+
hovertemplate="x: %{x:.2f}<br>dx/dt: %{y:.2f}<extra></extra>",
75+
)
76+
)
77+
78+
# Mark the fixed point (equilibrium at origin)
79+
fig.add_trace(
80+
go.Scatter(
81+
x=[0],
82+
y=[0],
83+
mode="markers",
84+
name="Fixed Point (Stable)",
85+
marker=dict(size=18, color="#E53935", symbol="x", line=dict(width=3)),
86+
hovertemplate="Equilibrium<br>x=0, dx/dt=0<extra></extra>",
87+
)
88+
)
89+
90+
# Mark initial conditions
91+
fig.add_trace(
92+
go.Scatter(
93+
x=[x[0], x2[0]],
94+
y=[dx_dt[0], dx_dt_2[0]],
95+
mode="markers",
96+
name="Initial Conditions",
97+
marker=dict(size=14, color="#4CAF50", symbol="circle"),
98+
hovertemplate="Initial: x=%{x:.2f}, dx/dt=%{y:.2f}<extra></extra>",
99+
)
100+
)
101+
102+
# Add direction arrows using annotations
103+
arrow_indices = [200, 500, 900]
104+
for idx in arrow_indices:
105+
# Arrow for trajectory 1
106+
fig.add_annotation(
107+
x=x[idx],
108+
y=dx_dt[idx],
109+
ax=x[idx - 30],
110+
ay=dx_dt[idx - 30],
111+
xref="x",
112+
yref="y",
113+
axref="x",
114+
ayref="y",
115+
showarrow=True,
116+
arrowhead=2,
117+
arrowsize=2,
118+
arrowwidth=2,
119+
arrowcolor="#306998",
120+
)
121+
# Arrow for trajectory 2
122+
fig.add_annotation(
123+
x=x2[idx],
124+
y=dx_dt_2[idx],
125+
ax=x2[idx - 30],
126+
ay=dx_dt_2[idx - 30],
127+
xref="x",
128+
yref="y",
129+
axref="x",
130+
ayref="y",
131+
showarrow=True,
132+
arrowhead=2,
133+
arrowsize=2,
134+
arrowwidth=2,
135+
arrowcolor="#FFD43B",
136+
)
137+
138+
# Update layout
139+
fig.update_layout(
140+
title=dict(
141+
text="Damped Harmonic Oscillator · phase-diagram · plotly · pyplots.ai",
142+
font=dict(size=28),
143+
x=0.5,
144+
xanchor="center",
145+
),
146+
xaxis=dict(
147+
title=dict(text="Position x", font=dict(size=22)),
148+
tickfont=dict(size=18),
149+
gridcolor="rgba(0,0,0,0.1)",
150+
gridwidth=1,
151+
zeroline=True,
152+
zerolinecolor="rgba(0,0,0,0.3)",
153+
zerolinewidth=2,
154+
),
155+
yaxis=dict(
156+
title=dict(text="Velocity dx/dt", font=dict(size=22)),
157+
tickfont=dict(size=18),
158+
gridcolor="rgba(0,0,0,0.1)",
159+
gridwidth=1,
160+
zeroline=True,
161+
zerolinecolor="rgba(0,0,0,0.3)",
162+
zerolinewidth=2,
163+
),
164+
legend=dict(
165+
font=dict(size=16),
166+
x=0.02,
167+
y=0.98,
168+
bgcolor="rgba(255,255,255,0.9)",
169+
bordercolor="rgba(0,0,0,0.2)",
170+
borderwidth=1,
171+
),
172+
template="plotly_white",
173+
plot_bgcolor="white",
174+
margin=dict(l=80, r=40, t=100, b=80),
175+
)
176+
177+
# Save as PNG (4800x2700 px)
178+
fig.write_image("plot.png", width=1600, height=900, scale=3)
179+
180+
# Save interactive HTML
181+
fig.write_html("plot.html", include_plotlyjs=True, full_html=True)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
library: plotly
2+
specification_id: phase-diagram
3+
created: '2025-12-31T11:06:07Z'
4+
updated: '2025-12-31T11:15:34Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617610863
7+
issue: 3004
8+
python_version: 3.13.11
9+
library_version: 6.5.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/plotly/plot.html
13+
quality_score: 93
14+
review:
15+
strengths:
16+
- Excellent physics example with analytically correct damped harmonic oscillator
17+
solution
18+
- Multiple trajectories clearly demonstrate basin of attraction structure
19+
- Direction arrows effectively show time evolution without cluttering the plot
20+
- Color scheme provides excellent contrast and is colorblind-accessible
21+
- Title format correctly follows spec requirements
22+
- Hover templates provide useful interactivity showing exact x and dx/dt values
23+
- Fixed point and initial conditions clearly marked with distinct symbols
24+
- Clean, well-organized code structure
25+
weaknesses:
26+
- Marker size on trajectory lines (size=4) could be slightly larger for better visibility
27+
at full resolution
28+
- Grid opacity (0.1) is very subtle; 0.2-0.3 would provide better reference without
29+
being distracting
30+
- Axis labels lack units (though dimensionless is acceptable for theoretical phase
31+
diagrams)
32+
- Does not fully leverage Plotly animation features that could show trajectory evolution
33+
over time

0 commit comments

Comments
 (0)