diff --git a/src/charts/spending_debt.py b/src/charts/spending_debt.py index d7c5c55..abea5b0 100644 --- a/src/charts/spending_debt.py +++ b/src/charts/spending_debt.py @@ -3,7 +3,11 @@ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np -from src.data.ai_infrastructure import hyperscaler_capex_annual, hyperscaler_capex_meta +from src.data.ai_infrastructure import ( + hyperscaler_capex_annual, + hyperscaler_capex_quarterly, + hyperscaler_capex_meta, +) from src.utils.styling import get_theme, EXPORT_DPI, get_company_colors, GRAY_DARK from src.utils.export import save_chart_tight @@ -14,47 +18,84 @@ def plot_hyperscaler_capex() -> str: company_colors = get_company_colors() companies = ["Microsoft", "Alphabet", "Meta", "Amazon"] - years = list(range(2020, 2027)) - # Organize data by company - data = {c: [] for c in companies} + # Build combined timeline: annual 2020-2023 + quarterly 2024-Q1 through 2026-Q1 + # Periods: 2020, 2021, 2022, 2023, 2024-Q1, 2024-Q2, 2024-Q3, 2024-Q4, + # 2025-Q1, 2025-Q2, 2026-Q1 + annual_years = [2020, 2021, 2022, 2023] + quarterly_periods = [ + (2024, "Q1"), (2024, "Q2"), (2024, "Q3"), (2024, "Q4"), + (2025, "Q1"), (2025, "Q2"), + (2026, "Q1"), + ] + + x_labels = [str(y) for y in annual_years] + [ + f"{y}-Q{q}" for y, q in quarterly_periods + ] + n_periods = len(x_labels) + x_positions = list(range(n_periods)) + + # Organize annual data by company (2020-2023) + annual_data = {c: {} for c in companies} for entry in hyperscaler_capex_annual: - data[entry["company"]].append(entry["capex_billions"]) + if entry["year"] in annual_years: + annual_data[entry["company"]][entry["year"]] = entry["capex_billions"] - # Fill missing years with 0 + # Organize quarterly data by company + quarterly_data = {c: {} for c in companies} + for entry in hyperscaler_capex_quarterly: + key = (entry["year"], entry["quarter"]) + quarterly_data[entry["company"]][key] = entry["capex_billions"] + + # Build per-company value arrays + data = {} for c in companies: - while len(data[c]) < len(years): - data[c].append(0) + vals = [] + # Annual portion + for y in annual_years: + vals.append(annual_data[c].get(y, 0)) + # Quarterly portion + for y, q in quarterly_periods: + vals.append(quarterly_data[c].get((y, q), 0)) + data[c] = np.array(vals) # Stacked area - y_offset = np.zeros(len(years)) - totals = [] + y_offset = np.zeros(n_periods) for c in companies: - values = np.array(data[c]) - ax.fill_between(years, y_offset, y_offset + values, + values = data[c] + ax.fill_between(x_positions, y_offset, y_offset + values, alpha=0.8, color=company_colors.get(c, "#666"), label=c) - ax.plot(years, y_offset + values, color=company_colors.get(c, "#666"), linewidth=1) + ax.plot(x_positions, y_offset + values, color=company_colors.get(c, "#666"), linewidth=1) y_offset += values - totals.append(sum(values)) # Total annotations - for i, (year, total) in enumerate(zip(years, y_offset)): - label = f"${total:.0f}B" - if year >= 2026: - label += "*" - ax.text(year, total + 5, label, ha="center", fontsize=9, fontweight="bold") + for i, (pos, label, total) in enumerate(zip(x_positions, x_labels, y_offset)): + total_label = f"${total:.0f}B" + # Mark 2026 as projected + if label.startswith("2026"): + total_label += "*" + # Only annotate every period (avoid crowding) + ax.text(pos, total + 4, total_label, ha="center", fontsize=9, fontweight="bold") - # Dashed vertical line at 2026 to mark guided/projected boundary - ax.axvline(x=2025.5, color=GRAY_DARK, linestyle="--", alpha=0.4, linewidth=1) + # Dashed vertical line between last quarterly data (2026-Q1) and remaining 2026 + ax.axvline(x=10.5, color=GRAY_DARK, linestyle="--", alpha=0.4, linewidth=1) - ax.set_title("Hyperscaler AI Infrastructure Capex — 2020 to 2026", fontsize=16, fontweight="bold") - ax.set_xlabel("Year", fontsize=12) + ax.set_xticks(x_positions) + ax.set_xticklabels(x_labels, rotation=45, ha="right", fontsize=9) + ax.set_title("Hyperscaler AI Infrastructure Capex — 2020 to 2026-Q1", + fontsize=16, fontweight="bold") + ax.set_xlabel("Period", fontsize=12) ax.set_ylabel("Capex (Billions USD)", fontsize=12) ax.legend(loc="upper left", fontsize=10) ax.grid(True, alpha=0.3) ax.text(0.02, 0.97, "*2026 = guided/projected", transform=ax.transAxes, fontsize=9, style="italic", color="gray", va="top") + # Granularity note + ax.text(0.50, 0.03, "2020-2023: annual | 2024-2026-Q1: quarterly", + transform=ax.transAxes, fontsize=9, style="italic", + color="gray", ha="center", va="bottom") + # AI-related capex share note ax.text(0.98, 0.03, "80-90% of 2025/2026 capex is AI-related", transform=ax.transAxes, fontsize=9, style="italic",