Skip to content

Commit 2098b96

Browse files
feat(altair): implement phase-diagram (#3059)
## Implementation: `phase-diagram` - altair Implements the **altair** version of `phase-diagram`. **File:** `plots/phase-diagram/implementations/altair.py` **Parent Issue:** #3004 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617651700)* --------- 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 346f299 commit 2098b96

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
""" pyplots.ai
2+
phase-diagram: Phase Diagram (State Space Plot)
3+
Library: altair 6.0.0 | Python 3.13.11
4+
Quality: 92/100 | Created: 2025-12-31
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data: Damped pendulum simulation
13+
# The pendulum equation: d2x/dt2 = -sin(x) - gamma * dx/dt
14+
np.random.seed(42)
15+
16+
# Simulate damped pendulum from multiple initial conditions
17+
dt = 0.02
18+
gamma = 0.15 # Damping coefficient
19+
20+
trajectories = []
21+
colors = ["#306998", "#FFD43B", "#E55934", "#9BC53D"] # Python blue, yellow, + colorblind-safe
22+
23+
initial_conditions = [
24+
(2.5, 0.0), # High initial displacement, zero velocity
25+
(-2.0, 1.5), # Negative displacement, positive velocity
26+
(0.5, 2.0), # Low displacement, high velocity
27+
(1.5, -1.5), # Moderate displacement, negative velocity
28+
]
29+
30+
for idx, (x0, v0) in enumerate(initial_conditions):
31+
x, v = x0, v0
32+
trajectory_x = [x]
33+
trajectory_v = [v]
34+
35+
# Simulate for 500 steps
36+
for _ in range(500):
37+
# Euler method for damped pendulum: d2x/dt2 = -sin(x) - gamma * dx/dt
38+
a = -np.sin(x) - gamma * v
39+
v = v + a * dt
40+
x = x + v * dt
41+
trajectory_x.append(x)
42+
trajectory_v.append(v)
43+
44+
# Store trajectory with time-based ordering
45+
for i, (px, pv) in enumerate(zip(trajectory_x, trajectory_v, strict=True)):
46+
trajectories.append(
47+
{
48+
"x": px,
49+
"dx_dt": pv,
50+
"trajectory": f"IC {idx + 1}: ({x0:.1f}, {v0:.1f})",
51+
"order": i,
52+
"color": colors[idx],
53+
"time_normalized": i / len(trajectory_x),
54+
}
55+
)
56+
57+
df = pd.DataFrame(trajectories)
58+
59+
# Create phase diagram
60+
base = (
61+
alt.Chart(df)
62+
.mark_line(strokeWidth=2.5, opacity=0.85)
63+
.encode(
64+
x=alt.X("x:Q", title="Position (x)", axis=alt.Axis(titleFontSize=22, labelFontSize=18)),
65+
y=alt.Y("dx_dt:Q", title="Velocity (dx/dt)", axis=alt.Axis(titleFontSize=22, labelFontSize=18)),
66+
color=alt.Color(
67+
"trajectory:N",
68+
title="Initial Condition",
69+
scale=alt.Scale(range=colors),
70+
legend=alt.Legend(titleFontSize=18, labelFontSize=16, symbolStrokeWidth=3),
71+
),
72+
order=alt.Order("order:Q"),
73+
detail="trajectory:N",
74+
)
75+
.properties(
76+
width=1600,
77+
height=900,
78+
title=alt.Title(
79+
text="Damped Pendulum Phase Space",
80+
subtitle="phase-diagram \u00b7 altair \u00b7 pyplots.ai",
81+
fontSize=28,
82+
subtitleFontSize=20,
83+
anchor="middle",
84+
),
85+
)
86+
)
87+
88+
# Add starting points as markers
89+
start_points = df[df["order"] == 0]
90+
points = (
91+
alt.Chart(start_points)
92+
.mark_point(size=400, filled=True, opacity=1.0)
93+
.encode(x="x:Q", y="dx_dt:Q", color=alt.Color("trajectory:N", scale=alt.Scale(range=colors), legend=None))
94+
)
95+
96+
# Add equilibrium point marker at origin
97+
equilibrium = pd.DataFrame([{"x": 0, "y": 0}])
98+
eq_point = (
99+
alt.Chart(equilibrium).mark_point(shape="cross", size=600, strokeWidth=4, color="black").encode(x="x:Q", y="y:Q")
100+
)
101+
102+
# Combine layers
103+
chart = (
104+
(base + points + eq_point)
105+
.configure_axis(grid=True, gridOpacity=0.3, gridDash=[3, 3])
106+
.configure_view(strokeWidth=0)
107+
.configure_legend(orient="right", padding=20)
108+
)
109+
110+
# Save as PNG (4800x2700 at scale_factor=3)
111+
chart.save("plot.png", scale_factor=3.0)
112+
113+
# Save interactive HTML version
114+
chart.interactive().save("plot.html")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
library: altair
2+
specification_id: phase-diagram
3+
created: '2025-12-31T11:09:12Z'
4+
updated: '2025-12-31T11:19:43Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617651700
7+
issue: 3004
8+
python_version: 3.13.11
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/phase-diagram/altair/plot.html
13+
quality_score: 92
14+
review:
15+
strengths:
16+
- Excellent demonstration of phase space dynamics with four trajectories showing
17+
spiral convergence to equilibrium
18+
- Outstanding use of Altair declarative grammar with layered chart composition (base
19+
lines + starting points + equilibrium marker)
20+
- Starting point markers and equilibrium cross provide clear visual anchors for
21+
interpreting trajectories
22+
- Clean well-structured code following KISS principles with proper Euler method
23+
simulation
24+
- Both PNG and interactive HTML outputs provided
25+
weaknesses:
26+
- Title format should have spec-id as main title per SC-06 requirement
27+
- Axis labels lack units (radians and rad/s for physics context)

0 commit comments

Comments
 (0)