Skip to content

Commit fd2be57

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

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
""" pyplots.ai
2+
phase-diagram: Phase Diagram (State Space Plot)
3+
Library: seaborn 0.13.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
import pandas as pd
10+
import seaborn as sns
11+
12+
13+
# Data: Damped harmonic oscillator phase trajectories
14+
np.random.seed(42)
15+
16+
# System parameters for damped oscillator: d²x/dt² + 2*zeta*omega*dx/dt + omega²*x = 0
17+
omega = 2 * np.pi # Natural frequency
18+
zeta = 0.15 # Damping ratio (underdamped)
19+
20+
# Generate multiple trajectories from different initial conditions
21+
t = np.linspace(0, 5, 500)
22+
trajectories = []
23+
initial_conditions = [
24+
(2.0, 0.0), # Large displacement, zero velocity
25+
(0.0, 8.0), # Zero displacement, positive velocity
26+
(-1.5, -5.0), # Negative displacement, negative velocity
27+
(1.0, 4.0), # Mixed positive
28+
]
29+
30+
for x0, v0 in initial_conditions:
31+
# Analytical solution for underdamped oscillator
32+
omega_d = omega * np.sqrt(1 - zeta**2) # Damped frequency
33+
A = np.sqrt(x0**2 + ((zeta * omega * x0 + v0) / omega_d) ** 2)
34+
phi = np.arctan2(omega_d * x0, zeta * omega * x0 + v0)
35+
36+
# Position and velocity (derivative)
37+
x = A * np.exp(-zeta * omega * t) * np.sin(omega_d * t + phi)
38+
dx_dt = (
39+
A
40+
* np.exp(-zeta * omega * t)
41+
* (-zeta * omega * np.sin(omega_d * t + phi) + omega_d * np.cos(omega_d * t + phi))
42+
)
43+
44+
trajectories.append((x, dx_dt, f"({x0}, {v0})"))
45+
46+
# Create DataFrame for seaborn
47+
data = []
48+
for x, dx_dt, label in trajectories:
49+
for i in range(len(x)):
50+
data.append({"Position (x)": x[i], "Velocity (dx/dt)": dx_dt[i], "Initial Condition": label, "Time": t[i]})
51+
df = pd.DataFrame(data)
52+
53+
# Plot
54+
sns.set_style("whitegrid")
55+
fig, ax = plt.subplots(figsize=(16, 9))
56+
57+
# Use seaborn lineplot for trajectories with color gradient by hue
58+
palette = ["#306998", "#FFD43B", "#E24A33", "#348ABD"]
59+
sns.lineplot(
60+
data=df,
61+
x="Position (x)",
62+
y="Velocity (dx/dt)",
63+
hue="Initial Condition",
64+
palette=palette,
65+
linewidth=2.5,
66+
alpha=0.9,
67+
legend=True,
68+
ax=ax,
69+
sort=False,
70+
)
71+
72+
# Add starting points as larger markers
73+
for i, (x, dx_dt, _label) in enumerate(trajectories):
74+
ax.scatter(x[0], dx_dt[0], s=250, color=palette[i], zorder=5, edgecolor="white", linewidth=2)
75+
76+
# Add fixed point (equilibrium at origin)
77+
ax.scatter(0, 0, s=300, color="black", marker="x", linewidth=4, zorder=6, label="Equilibrium")
78+
79+
# Add direction arrows on trajectories
80+
for i, (x, dx_dt, _label) in enumerate(trajectories):
81+
# Add arrows at several points along trajectory
82+
arrow_indices = [50, 150, 300]
83+
for idx in arrow_indices:
84+
if idx < len(x) - 1:
85+
dx = x[idx + 1] - x[idx]
86+
dy = dx_dt[idx + 1] - dx_dt[idx]
87+
ax.annotate(
88+
"",
89+
xy=(x[idx] + dx * 0.5, dx_dt[idx] + dy * 0.5),
90+
xytext=(x[idx], dx_dt[idx]),
91+
arrowprops={"arrowstyle": "->", "color": palette[i], "lw": 2},
92+
)
93+
94+
# Styling
95+
ax.set_xlabel("Position (x)", fontsize=20)
96+
ax.set_ylabel("Velocity (dx/dt)", fontsize=20)
97+
ax.set_title("phase-diagram · seaborn · pyplots.ai", fontsize=24)
98+
ax.tick_params(axis="both", labelsize=16)
99+
100+
# Adjust legend
101+
ax.legend(fontsize=14, loc="upper right", title="Initial Condition", title_fontsize=16)
102+
103+
# Add zero lines for reference
104+
ax.axhline(y=0, color="gray", linestyle="--", linewidth=1.5, alpha=0.5)
105+
ax.axvline(x=0, color="gray", linestyle="--", linewidth=1.5, alpha=0.5)
106+
107+
plt.tight_layout()
108+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
library: seaborn
2+
specification_id: phase-diagram
3+
created: '2025-12-31T11:05:40Z'
4+
updated: '2025-12-31T11:15:30Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617609208
7+
issue: 3004
8+
python_version: 3.13.11
9+
library_version: 0.13.2
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/seaborn/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/seaborn/plot_thumb.png
12+
preview_html: null
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent visualization of phase space dynamics with clear spiral trajectories
17+
converging to equilibrium
18+
- Well-chosen physics example (damped harmonic oscillator) that perfectly demonstrates
19+
the plot type
20+
- Starting points clearly marked with large scatter markers distinguishing each
21+
trajectory
22+
- Direction arrows effectively show time evolution along the trajectories
23+
- Reference lines (dashed gray at x=0 and y=0) help identify equilibrium crossing
24+
- Proper use of seaborn lineplot with hue for trajectory differentiation
25+
- Clean readable code with analytical solution for underdamped oscillator
26+
weaknesses:
27+
- Legend positioning and styling could be improved - the Equilibrium marker is added
28+
separately and its legend entry appears disconnected
29+
- Inner portions of spirals become dense and harder to distinguish near the origin
30+
- Could benefit from using a colorblind-friendly palette instead of custom colors

0 commit comments

Comments
 (0)