Skip to content

Commit 5e8a153

Browse files
feat(plotly): implement timeseries-decomposition (#3019)
## Implementation: `timeseries-decomposition` - plotly Implements the **plotly** version of `timeseries-decomposition`. **File:** `plots/timeseries-decomposition/implementations/plotly.py` **Parent Issue:** #2992 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20617478986)* --------- 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 07dbba3 commit 5e8a153

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
""" pyplots.ai
2+
timeseries-decomposition: Time Series Decomposition Plot
3+
Library: plotly 6.5.0 | Python 3.13.11
4+
Quality: 91/100 | Created: 2025-12-31
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
import plotly.graph_objects as go
10+
from plotly.subplots import make_subplots
11+
from statsmodels.tsa.seasonal import seasonal_decompose
12+
13+
14+
# Data: Monthly airline passengers (classic time series with trend and seasonality)
15+
np.random.seed(42)
16+
17+
# Create 10 years of monthly data (120 points)
18+
dates = pd.date_range(start="2014-01-01", periods=120, freq="MS")
19+
20+
# Generate realistic airline passenger data with trend, seasonality, and noise
21+
trend = np.linspace(100, 250, 120) # Growing trend
22+
seasonal = 30 * np.sin(2 * np.pi * np.arange(120) / 12) # Annual cycle (peak in summer)
23+
noise = np.random.normal(0, 10, 120)
24+
passengers = trend + seasonal + noise
25+
26+
# Create time series and decompose
27+
ts = pd.Series(passengers, index=dates)
28+
decomposition = seasonal_decompose(ts, model="additive", period=12)
29+
30+
# Create subplots (4 rows, shared x-axis)
31+
fig = make_subplots(
32+
rows=4,
33+
cols=1,
34+
shared_xaxes=True,
35+
vertical_spacing=0.08,
36+
subplot_titles=("Original", "Trend", "Seasonal", "Residual"),
37+
)
38+
39+
# Color scheme
40+
primary_color = "#306998" # Python Blue
41+
secondary_color = "#FFD43B" # Python Yellow
42+
43+
# Original series
44+
fig.add_trace(
45+
go.Scatter(x=dates, y=ts.values, mode="lines", line=dict(color=primary_color, width=2.5), name="Original"),
46+
row=1,
47+
col=1,
48+
)
49+
50+
# Trend component
51+
fig.add_trace(
52+
go.Scatter(
53+
x=dates,
54+
y=decomposition.trend,
55+
mode="lines",
56+
line=dict(color="#2E8B57", width=3), # Sea green for trend
57+
name="Trend",
58+
),
59+
row=2,
60+
col=1,
61+
)
62+
63+
# Seasonal component
64+
fig.add_trace(
65+
go.Scatter(
66+
x=dates,
67+
y=decomposition.seasonal,
68+
mode="lines",
69+
line=dict(color="#E07020", width=2.5), # Orange for seasonal
70+
name="Seasonal",
71+
),
72+
row=3,
73+
col=1,
74+
)
75+
76+
# Residual component
77+
fig.add_trace(
78+
go.Scatter(
79+
x=dates,
80+
y=decomposition.resid,
81+
mode="lines",
82+
line=dict(color="#8B4789", width=2), # Purple for residual
83+
name="Residual",
84+
),
85+
row=4,
86+
col=1,
87+
)
88+
89+
# Update layout for large canvas
90+
fig.update_layout(
91+
title=dict(text="timeseries-decomposition · plotly · pyplots.ai", font=dict(size=32), x=0.5, xanchor="center"),
92+
template="plotly_white",
93+
showlegend=False,
94+
height=900,
95+
width=1600,
96+
margin=dict(l=100, r=60, t=100, b=80),
97+
)
98+
99+
# Update all y-axes
100+
y_axis_titles = ["Passengers (thousands)", "Trend", "Seasonal Component", "Residual"]
101+
for i, title in enumerate(y_axis_titles, 1):
102+
fig.update_yaxes(
103+
title=dict(text=title, font=dict(size=20)),
104+
tickfont=dict(size=16),
105+
gridcolor="rgba(128, 128, 128, 0.2)",
106+
gridwidth=1,
107+
row=i,
108+
col=1,
109+
)
110+
111+
# Update x-axes
112+
for i in range(1, 5):
113+
fig.update_xaxes(tickfont=dict(size=16), gridcolor="rgba(128, 128, 128, 0.2)", gridwidth=1, row=i, col=1)
114+
115+
# Bottom x-axis label
116+
fig.update_xaxes(title=dict(text="Date", font=dict(size=20)), row=4, col=1)
117+
118+
# Update subplot titles font size
119+
for annotation in fig.layout.annotations:
120+
annotation.font.size = 22
121+
122+
# Save as PNG (4800 x 2700 px)
123+
fig.write_image("plot.png", width=1600, height=900, scale=3)
124+
125+
# Save interactive HTML version
126+
fig.write_html("plot.html", include_plotlyjs=True, full_html=True)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
library: plotly
2+
specification_id: timeseries-decomposition
3+
created: '2025-12-31T10:55:31Z'
4+
updated: '2025-12-31T11:53:53Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20617478986
7+
issue: 2992
8+
python_version: 3.13.11
9+
library_version: 6.5.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/timeseries-decomposition/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/timeseries-decomposition/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/timeseries-decomposition/plotly/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Clean four-panel layout with well-differentiated components using distinct colors
17+
- Proper use of statsmodels seasonal_decompose for authentic time series decomposition
18+
- 'Title follows required format exactly: timeseries-decomposition · plotly · pyplots.ai'
19+
- Excellent use of Plotly subplots with shared x-axes for time alignment
20+
- Realistic airline passenger data scenario with clear trend and seasonality
21+
weaknesses:
22+
- Y-axis label for Original subplot says Passengers (thousands) but other subplots
23+
lack units context
24+
- Trend subplot y-axis label just says Trend without indicating the unit is still
25+
passengers

0 commit comments

Comments
 (0)