Skip to main content
Data Visualization
CHAPTER 16 Beginner

Time Series Visualization

Updated: May 18, 2026
5 min read

# CHAPTER 16

Time Series Visualization

1. Chapter Introduction

Time series data drives business decisions — stock prices, revenue trends, website traffic, sensor readings. This chapter covers professional time series visualization: trend lines, moving averages, seasonal patterns, and financial candlestick charts.

2. Time Series Fundamentals

python
1234567891011121314151617181920212223242526272829303132333435363738394041424344
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
import numpy as np

np.random.seed(42)
dates = pd.date_range('2022-01-01', '2024-12-31', freq='D')
n = len(dates)

# Simulate realistic data with trend + seasonality + noise
trend = np.linspace(50000, 85000, n)
seasonality = 15000 * np.sin(2 * np.pi * np.arange(n) / 365)
noise = np.random.normal(0, 3000, n)
revenue = trend + seasonality + noise

df = pd.DataFrame({'Date': dates, 'Revenue': revenue})
df = df.set_index('Date')

# Moving averages
df['MA_7']  = df['Revenue'].rolling(7).mean()
df['MA_30'] = df['Revenue'].rolling(30).mean()
df['MA_90'] = df['Revenue'].rolling(90).mean()

fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(df.index, df['Revenue'], color='#90CAF9', linewidth=0.8, alpha=0.6, label='Daily Revenue')
ax.plot(df.index, df['MA_7'],  color='#42A5F5', linewidth=1.5, label='7-day MA')
ax.plot(df.index, df['MA_30'], color='#1565C0', linewidth=2, label='30-day MA')
ax.plot(df.index, df['MA_90'], color='#FF5722', linewidth=2.5, label='90-day MA (Trend)')

# Format x-axis for dates
ax.xaxis.set_major_locator(mdates.MonthLocator(bymonth=[1, 4, 7, 10]))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
ax.xaxis.set_minor_locator(mdates.MonthLocator())
plt.setp(ax.get_xticklabels(), rotation=30, ha='right')

ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x/1000:.0f}K'))
ax.set_title('Revenue Trend with Moving Averages (2022-2024)', fontsize=14, fontweight='bold')
ax.legend(loc='upper left')
ax.grid(True, alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.savefig('time_series_ma.png', dpi=150)
plt.show()

3. Seasonal Decomposition

python
12345678910111213141516171819202122
from statsmodels.tsa.seasonal import seasonal_decompose

# Monthly data for cleaner decomposition
monthly = df['Revenue'].resample('ME').mean()
result = seasonal_decompose(monthly, model='additive', period=12)

fig, axes = plt.subplots(4, 1, figsize=(13, 12), sharex=True)

monthly.plot(ax=axes[0], title='Original Series', color='#1565C0')
result.trend.plot(ax=axes[1], title='Trend Component', color='#4CAF50')
result.seasonal.plot(ax=axes[2], title='Seasonal Component', color='#FF9800')
result.resid.plot(ax=axes[3], title='Residual (Noise)', color='#E91E63', style='.', markersize=6)

for ax in axes:
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.grid(True, alpha=0.3)

plt.suptitle('Seasonal Decomposition of Revenue', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('seasonal_decomp.png', dpi=150)
plt.show()

4. Mini Project: Stock Market Analysis

python
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
import numpy as np
from matplotlib.patches import FancyArrowPatch

np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=252, freq='B')  # Business days
returns = np.random.normal(0.0005, 0.02, 252)
prices = pd.Series(150 * np.cumprod(1 + returns), index=dates)

# Simulate OHLC data
df = pd.DataFrame({'Date': dates})
df['Close'] = prices.values
df['Open']  = df['Close'].shift(1).fillna(prices.values[0])
df['High']  = df[['Open', 'Close']].max(axis=1) * (1 + np.abs(np.random.normal(0, 0.005, 252)))
df['Low']   = df[['Open', 'Close']].min(axis=1) * (1 - np.abs(np.random.normal(0, 0.005, 252)))
df['Volume']= np.random.randint(1000000, 5000000, 252)

# Indicators
df['MA20'] = df['Close'].rolling(20).mean()
df['MA50'] = df['Close'].rolling(50).mean()
df['Upper_BB'] = df['MA20'] + 2 * df['Close'].rolling(20).std()
df['Lower_BB'] = df['MA20'] - 2 * df['Close'].rolling(20).std()

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True, gridspec_kw={'height_ratios': [3, 1]})

# Price + indicators
ax1.plot(df['Date'], df['Close'], 'b-', linewidth=1.5, label='Close Price')
ax1.plot(df['Date'], df['MA20'], '--', color='orange', linewidth=1.5, label='MA20')
ax1.plot(df['Date'], df['MA50'], '--', color='red', linewidth=1.5, label='MA50')
ax1.fill_between(df['Date'], df['Upper_BB'], df['Lower_BB'], alpha=0.1, color='blue', label='Bollinger Bands')

# Bullish/bearish annotation
max_idx = df['Close'].idxmax()
ax1.annotate(f'Peak: ${df["Close"].max():.2f}',
              xy=(max_idx, df['Close'].max()),
              xytext=(max_idx - pd.Timedelta(days=30), df['Close'].max() + 5),
              arrowprops=dict(arrowstyle='->', color='green'), color='green', fontweight='bold')

ax1.set_title('Stock Price Analysis with Technical Indicators', fontsize=13, fontweight='bold')
ax1.set_ylabel('Price ($)')
ax1.legend(loc='upper left', fontsize=9)
ax1.grid(True, alpha=0.3)
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)

# Volume
colors_vol = ['#4CAF50' if c > o else '#F44336' for c, o in zip(df['Close'], df['Open'])]
ax2.bar(df['Date'], df['Volume'], color=colors_vol, alpha=0.8, width=1)
ax2.set_ylabel('Volume')
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/1e6:.1f}M'))
ax2.set_title('Trading Volume', fontsize=11)
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
ax2.grid(True, alpha=0.3)
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)

plt.setp(ax2.get_xticklabels(), rotation=30, ha='right')
plt.tight_layout()
plt.savefig('stock_analysis.png', dpi=150, bbox_inches='tight')
plt.show()

5. Common Mistakes

  • Not formatting date axes: Matplotlib renders dates as floats by default. Always use mdates.DateFormatter() for readable date labels.
  • No resampling before decomposition: Seasonal decompose requires consistent frequency. Always resample to a regular frequency first.

6. MCQs

Question 1

Moving average purpose in time series?

Question 2

mdates.DateFormatter('%b %Y') formats as?

Question 3

Seasonal decomposition splits time series into?

Question 4

90-day MA vs 7-day MA shows?

Question 5

Bollinger Bands are calculated from?

Question 6

freq='B' in date_range creates?

Question 7

Volume bars colored green/red by?

Question 8

sharex=True in stock chart subplots?

Question 9

Residual in seasonal decomposition represents?

Question 10

resample('ME').mean() converts daily data to?

7. Interview Questions

  • Q: How do you decompose a time series into its components?
  • Q: What are Bollinger Bands and what do they indicate?

8. Summary

Time series visualization: moving averages for smoothing, seasonal decomposition for pattern separation (trend + season + noise), Bollinger Bands for volatility bounds. Format date axes with mdates. Use sharex=True to link price and volume panels. Color volume bars by daily direction (close vs open).

9. Next Chapter Recommendation

In Chapter 17: Interactive Visualization with Plotly, we create hover-enabled, zoomable, web-ready charts that bring data to life.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·