import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from pathlib import Path
from datetime import datetime, timedeltaMakeoverMonday: 2025 Market Events - Interactive Dashboard
Overview
This MakeoverMonday project transforms the 2025 market events data into an interactive dashboard. Unlike static charts, this Plotly-based visualization allows users to:
- Hover over events to see details
- Zoom into specific time periods
- Toggle between indices
- Explore trend periods interactively
Original Visualization
Source: Custom market events dataset
The original data consists of CSV files with event dates, market impacts, and trend periods. This makeover creates an interactive experience from that static data.
Dataset
# Define base path
base_path = Path.cwd()
while not (base_path / "data").exists() and base_path.parent != base_path:
base_path = base_path.parent
# Load events data
events_path = base_path / "data" / "macro_economy" / "market_events" / "events_2025.csv"
trends_path = base_path / "data" / "macro_economy" / "market_events" / "trend_periods_2025.csv"
events = pd.read_csv(events_path, parse_dates=["date"])
trends = pd.read_csv(trends_path, parse_dates=["start_date", "end_date"])
print(f"Loaded {len(events)} events and {len(trends)} trend periods")
events.head()Loaded 19 events and 12 trend periods
| date | market | category | event_label | description | sp500_change | nikkei_change | source_url | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2025-01-15 | US | INFLATION | CPI Cooled | US CPI (core cooled) -> S&P500 +1.8% (risk-on) | 1.80 | NaN | https://www.reuters.com/markets/us/inflation-r... |
| 1 | 2025-01-24 | JP | MONETARY_POLICY | BOJ Hike 0.5% | BOJ hikes policy rate to 0.5% (regime shift) | NaN | 0.0 | https://www.reuters.com/world/china/global-mar... |
| 2 | 2025-01-29 | US | MONETARY_POLICY | FOMC Hold | FOMC holds 4.25-4.50% | 0.00 | NaN | NaN |
| 3 | 2025-03-03 | US | TARIFF | Tariff Shock | Tariffs headline -> S&P -1.76%, Nasdaq -2.64% | -1.76 | NaN | https://www.reuters.com/markets/us/futures-edg... |
| 4 | 2025-04-03 | BOTH | TARIFF | Reciprocal Tariffs | Reciprocal tariffs shock -> S&P -4.88%, Nasdaq... | -4.88 | -4.0 | https://www.reuters.com/markets/asia/japans-ni... |
# Load S&P 500 from FRED parquet
sp500_path = base_path / "data" / "macro_economy" / "fred" / "SP500.parquet"
if sp500_path.exists():
sp500 = pd.read_parquet(sp500_path)
sp500['date'] = pd.to_datetime(sp500['date'])
sp500 = sp500[(sp500['date'] >= '2025-01-01') & (sp500['date'] <= '2025-12-31')]
sp500 = sp500.rename(columns={'value': 'sp500'})
else:
# Fallback: generate sample data
dates = pd.date_range('2025-01-01', '2025-12-31', freq='D')
sp500 = pd.DataFrame({
'date': dates,
'sp500': 5800 + np.cumsum(np.random.randn(len(dates)) * 30)
})
# Simulate Nikkei data (in production: use yfinance)
dates = pd.date_range('2025-01-01', '2025-12-31', freq='D')
nikkei = pd.DataFrame({
'date': dates,
'nikkei': 38000 + np.cumsum(np.random.randn(len(dates)) * 400)
})
# Merge
index_data = sp500.merge(nikkei, on='date', how='inner')
print(f"Index data: {len(index_data)} rows")Index data: 261 rows
My Makeover
What I Changed
- Static → Interactive: Converted from ggplot2-style static chart to fully interactive Plotly
- Dual Y-axis: Added secondary axis for Nikkei 225 (different scale)
- Hover Details: Event information appears on hover
- Range Selector: Added buttons for 1M, 3M, 6M, YTD views
- Annotations: Key events labeled directly on chart
Main Visualization
# Create figure with secondary y-axis
fig = make_subplots(specs=[[{"secondary_y": True}]])
# Add trend shading (US trends)
us_trends = trends[trends['market'] == 'US']
for _, trend in us_trends.iterrows():
color = "rgba(34, 197, 94, 0.15)" if trend['trend_type'] == 'UP' else "rgba(239, 68, 68, 0.15)"
fig.add_vrect(
x0=trend['start_date'].strftime('%Y-%m-%d'),
x1=trend['end_date'].strftime('%Y-%m-%d'),
fillcolor=color,
line_width=0
)
# Add S&P 500 line
fig.add_trace(
go.Scatter(
x=index_data['date'],
y=index_data['sp500'],
name="S&P 500",
line=dict(color="#2563eb", width=2),
hovertemplate="<b>S&P 500</b><br>Date: %{x|%Y-%m-%d}<br>Value: %{y:,.0f}<extra></extra>"
),
secondary_y=False
)
# Add Nikkei 225 line
fig.add_trace(
go.Scatter(
x=index_data['date'],
y=index_data['nikkei'],
name="Nikkei 225",
line=dict(color="#dc2626", width=2),
hovertemplate="<b>Nikkei 225</b><br>Date: %{x|%Y-%m-%d}<br>Value: %{y:,.0f}<extra></extra>"
),
secondary_y=True
)
# Add event markers
us_events = events[events['market'].isin(['US', 'BOTH'])]
major_events = us_events[us_events['sp500_change'].abs() > 1.5]
for _, event in major_events.iterrows():
fig.add_vline(
x=event['date'].strftime('%Y-%m-%d'),
line_dash="dash",
line_color="#94a3b8",
line_width=1
)
# Update layout
fig.update_layout(
title=dict(
text="<b>2025 Market Events: S&P 500 vs Nikkei 225</b><br><sup>Trend periods (shaded) and major events (dashed lines)</sup>",
x=0.5,
font=dict(size=18)
),
xaxis=dict(
title="",
rangeselector=dict(
buttons=list([
dict(count=1, label="1M", step="month", stepmode="backward"),
dict(count=3, label="3M", step="month", stepmode="backward"),
dict(count=6, label="6M", step="month", stepmode="backward"),
dict(step="all", label="YTD")
]),
bgcolor="rgba(255,255,255,0.9)",
bordercolor="#e2e8f0"
),
rangeslider=dict(visible=True),
type="date"
),
yaxis=dict(
title="S&P 500",
tickformat=",",
side="left"
),
yaxis2=dict(
title="Nikkei 225",
tickformat=",",
side="right"
),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
template="plotly_white",
height=550,
margin=dict(t=120, b=80)
)
fig.show()Event Impact Analysis
# Create bar chart of event impacts
events_with_impact = events[events['sp500_change'].notna()].copy()
events_with_impact['color'] = events_with_impact['sp500_change'].apply(
lambda x: '#22c55e' if x > 0 else '#ef4444'
)
events_with_impact = events_with_impact.sort_values('sp500_change')
fig2 = go.Figure()
fig2.add_trace(go.Bar(
y=events_with_impact['event_label'],
x=events_with_impact['sp500_change'],
orientation='h',
marker_color=events_with_impact['color'],
text=events_with_impact['sp500_change'].apply(lambda x: f"{x:+.1f}%"),
textposition='outside',
hovertemplate="<b>%{y}</b><br>S&P 500 Change: %{x:.2f}%<extra></extra>"
))
fig2.update_layout(
title=dict(
text="<b>S&P 500 Daily Change by Event</b>",
x=0.5
),
xaxis_title="Daily Change (%)",
yaxis_title="",
template="plotly_white",
height=450,
margin=dict(l=150)
)
# Add zero line
fig2.add_vline(x=0, line_color="#64748b", line_width=1)
fig2.show()Trend Period Summary
# Create trend period table
us_trends_display = trends[trends['market'] == 'US'].copy()
us_trends_display['duration'] = (us_trends_display['end_date'] - us_trends_display['start_date']).dt.days
us_trends_display['period'] = us_trends_display['start_date'].dt.strftime('%b %d') + ' - ' + us_trends_display['end_date'].dt.strftime('%b %d')
us_trends_display[['period', 'trend_type', 'trigger_event', 'duration', 'description']]| period | trend_type | trigger_event | duration | description | |
|---|---|---|---|---|---|
| 0 | Jan 01 - Mar 02 | UP | Post-2024 Rally | 60 | New year rally continuation before tariff conc... |
| 2 | Mar 03 - Apr 08 | DOWN | Tariff Shock | 36 | Sharp decline triggered by tariff announcement... |
| 4 | Apr 09 - Apr 09 | UP | 90-Day Pause | 0 | Single-day historic rally on tariff pause anno... |
| 6 | Apr 10 - May 12 | DOWN | Tariff Uncertainty | 32 | Post-rally pullback amid ongoing tariff uncert... |
| 7 | May 13 - Sep 16 | UP | US-China Truce | 126 | Risk-on rally following US-China tariff truce ... |
| 9 | Sep 17 - Nov 12 | UP | Fed Easing Cycle | 56 | Rally driven by Fed rate cuts starting September |
| 10 | Nov 13 - Dec 09 | DOWN | Rate Cut Doubts | 26 | Pullback as rate cut expectations fade |
| 11 | Dec 10 - Dec 31 | UP | Year-End Rally | 21 | Year-end rally following December Fed cut |
Key Takeaways
Interactive Exploration: Users can zoom into specific periods (e.g., April tariff shock) to examine price action in detail.
Dual Index Comparison: The secondary Y-axis allows direct comparison of S&P 500 and Nikkei 225 movements, revealing correlation during global events.
Trend Visualization: Shaded regions make it easy to identify bull and bear phases at a glance.
Event Context: Hover information provides instant context for each market event.
April 2025 Volatility: The tariff shock week (April 3-10) saw unprecedented volatility with a -4.88% crash followed by +9.5% rally.
This post is part of the MakeoverMonday weekly data visualization project.