Files
ai-bubble-research/src/charts/narrative_dashboard.py

295 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Narrative Dashboard — 3×3 grid telling the AI bubble story
FLAGSHIP chart: single figure combining all evidence streams
into a cohesive visual narrative.
"""
import sys
import os
# Ensure the project root is on sys.path so `src.*` imports work
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import matplotlib
matplotlib.use("Agg")
# Patch matplotlib Path.__deepcopy__ to break Python 3.14 recursion loop
# Known bug: https://github.com/matplotlib/matplotlib/issues/29280
try:
from matplotlib.path import Path
_original_path_deepcopy = Path.__deepcopy__
def _safe_path_deepcopy(self, memo):
if id(self) in memo:
return memo[id(self)]
memo[id(self)] = self
return self
Path.__deepcopy__ = _safe_path_deepcopy
except Exception:
pass
import matplotlib.pyplot as plt
import numpy as np
from src.data.market_bubbles import shiller_cape, buffett_indicator, sp500_pe
from src.data.ai_infrastructure import hyperscaler_capex_annual, nvidia_revenue
from src.data.agent_adoption import agent_survey_data, developer_ai_adoption
from src.utils.styling import (
get_theme, EXPORT_DPI, BUBBLE_ZONE, WARNING_ZONE, NORMAL_ZONE,
GRAY_DARK, GRAY_MEDIUM, BLACK, WHITE,
AGENT_GROWTH, REVENUE, DEBT, AI_SPEND, PRODUCTIVITY,
get_company_colors,
)
def plot_narrative_dashboard() -> str:
"""Generate the flagship 3×3 narrative dashboard.
Returns the output file path.
"""
plt.rcParams.update(get_theme())
fig, axes = plt.subplots(3, 3, figsize=(20, 16))
fig.set_facecolor(WHITE)
# ------------------------------------------------------------------
# ROW 1: Market Bubble Evidence
# ------------------------------------------------------------------
# Panel (0,0): Shiller CAPE
ax = axes[0, 0]
years = [d["year"] for d in shiller_cape]
values = [d["value"] for d in shiller_cape]
ax.axhspan(0, 20, alpha=0.15, color=NORMAL_ZONE)
ax.axhspan(20, 30, alpha=0.15, color=WARNING_ZONE)
ax.axhspan(30, 50, alpha=0.15, color=BUBBLE_ZONE)
ax.plot(years, values, color=GRAY_DARK, linewidth=0.8)
ax.axhline(y=17.39, color="#333", linestyle="--", linewidth=0.6)
ax.text(2024, 18.5, "mean: 17.4", fontsize=7, color=GRAY_MEDIUM)
ax.annotate(
f"{values[-1]:.1f}",
xy=(2026, values[-1]), fontsize=7, fontweight="bold",
color=BUBBLE_ZONE, xytext=(2023, values[-1] - 5),
arrowprops=dict(arrowstyle="->", color=BUBBLE_ZONE, lw=0.6),
)
ax.set_title("Shiller CAPE (18802026)", fontsize=11, fontweight="bold")
ax.set_ylabel("CAPE")
ax.set_ylim(0, 50)
ax.tick_params(labelsize=7)
ax.grid(True, alpha=0.2)
# Panel (0,1): Buffett Indicator
ax = axes[0, 1]
b_years = [d["year"] for d in buffett_indicator]
b_vals = [d["value"] for d in buffett_indicator]
ax.axhspan(0, 100, alpha=0.15, color=NORMAL_ZONE)
ax.axhspan(100, 200, alpha=0.15, color=WARNING_ZONE)
ax.axhspan(200, 300, alpha=0.15, color=BUBBLE_ZONE)
ax.plot(b_years, b_vals, color=GRAY_DARK, linewidth=0.8)
ax.axhline(y=200, color=BUBBLE_ZONE, linestyle="--", linewidth=1)
ax.text(2000, 205, "Danger: 200%", fontsize=7, color=BUBBLE_ZONE)
ax.annotate(
f"{b_vals[-1]:.0f}%",
xy=(2026, b_vals[-1]), fontsize=7, fontweight="bold",
color=BUBBLE_ZONE, xytext=(2020, b_vals[-1] + 10),
arrowprops=dict(arrowstyle="->", color=BUBBLE_ZONE, lw=0.6),
)
ax.set_title("Buffett Indicator (19752026)", fontsize=11, fontweight="bold")
ax.set_ylabel("Mkt Cap / GDP %")
ax.tick_params(labelsize=7)
ax.grid(True, alpha=0.2)
# Panel (0,2): S&P 500 P/E
ax = axes[0, 2]
pe_years = [d["year"] for d in sp500_pe]
pe_vals = [d["value"] for d in sp500_pe]
ax.plot(pe_years, pe_vals, color=GRAY_DARK, linewidth=0.8)
ax.axhline(y=17.9, color="#333", linestyle="--", linewidth=0.6)
ax.text(2020, 19, "mean: 17.9", fontsize=7, color=GRAY_MEDIUM)
ax.annotate(
f"{pe_vals[-1]:.1f}",
xy=(2026, pe_vals[-1]), fontsize=7, fontweight="bold",
color=WARNING_ZONE, xytext=(2023, pe_vals[-1] - 3),
arrowprops=dict(arrowstyle="->", color=WARNING_ZONE, lw=0.6),
)
ax.set_title("S&P 500 P/E (19502026)", fontsize=11, fontweight="bold")
ax.set_ylabel("P/E")
ax.set_ylim(0, 75)
ax.tick_params(labelsize=7)
ax.grid(True, alpha=0.2)
# ------------------------------------------------------------------
# ROW 2: AI Infrastructure Buildout
# ------------------------------------------------------------------
# Panel (1,0): Hyperscaler Capex (stacked area, 20202026)
ax = axes[1, 0]
company_colors = get_company_colors()
companies = ["Microsoft", "Alphabet", "Meta", "Amazon"]
years_annual = list(range(2020, 2027))
data = {c: [0.0] * 7 for c in companies}
for entry in hyperscaler_capex_annual:
idx = entry["year"] - 2020
if 0 <= idx < 7:
data[entry["company"]][idx] = entry["capex_billions"]
y_off = np.zeros(7)
for c in companies:
vals = np.array(data[c], dtype=float)
ax.fill_between(
years_annual, y_off, y_off + vals,
alpha=0.7, color=company_colors[c], label=c,
)
y_off += vals
ax.set_title("Hyperscaler Capex (20202026)", fontsize=11, fontweight="bold")
ax.set_ylabel("Capex $B")
ax.tick_params(labelsize=7)
ax.legend(loc="upper left", fontsize=6, framealpha=0.8)
ax.grid(True, alpha=0.2, axis="y")
# Panel (1,1): Tech Debt Spike
ax = axes[1, 1]
debt_years = [2020, 2021, 2022, 2023, 2024, 2025, 2026]
debt_vals = [25, 30, 28, 25, 30, 121, 125]
colors_debt = [GRAY_DARK] * 5 + [BUBBLE_ZONE, WARNING_ZONE]
bars = ax.bar(debt_years, debt_vals, color=colors_debt, width=0.5)
avg5 = np.mean(debt_vals[:5])
ax.axhline(y=avg5, color="#333", linestyle="--", linewidth=1)
ax.text(2022, avg5 + 3, f"pre-2025 avg: ${avg5:.0f}B",
fontsize=7, color=GRAY_MEDIUM)
ax.text(2025.5, 125 + 5, "4× spike!", fontsize=8,
fontweight="bold", color=BUBBLE_ZONE, ha="right")
ax.set_title("Tech Debt: 2025 4× Spike", fontsize=11, fontweight="bold")
ax.set_ylabel("Debt $B")
ax.set_ylim(0, 150)
ax.tick_params(labelsize=7)
# Panel (1,2): NVIDIA Data Center Revenue
ax = axes[1, 2]
dc_rev = [d.get("data_center_billions",
d.get("compute_billions", 0) + d.get("networking_billions", 0))
for d in nvidia_revenue]
quarters = list(range(len(dc_rev)))
ax.fill_between(quarters, dc_rev, alpha=0.25, color=REVENUE)
ax.plot(quarters, dc_rev, color=REVENUE, linewidth=1)
# Mark the inflection and latest
nvidia_quarters_labels = [d["fiscal_quarter"] for d in nvidia_revenue]
# Highlight 2026-Q4 (index 27)
latest_idx = len(dc_rev) - 2 # before FY2027-Q1
ax.plot(latest_idx, dc_rev[latest_idx], "o", color=REVENUE,
markersize=5)
ax.annotate(
f"${dc_rev[latest_idx]:.1f}B",
xy=(latest_idx, dc_rev[latest_idx]),
xytext=(latest_idx - 3, dc_rev[latest_idx] - 8),
fontsize=7, fontweight="bold", color=REVENUE,
arrowprops=dict(arrowstyle="->", color=REVENUE, lw=0.5),
)
ax.set_title("NVIDIA DC Revenue (Quarterly)", fontsize=11, fontweight="bold")
ax.set_ylabel("Revenue $B")
ax.tick_params(labelsize=7)
ax.set_xticks(range(0, len(quarters), 4))
ax.set_xticklabels([nvidia_quarters_labels[i].replace("FY", "")
for i in range(0, len(quarters), 4)],
rotation=45, ha="right")
ax.grid(True, alpha=0.2)
# ------------------------------------------------------------------
# ROW 3: Agent Revolution and Reality
# ------------------------------------------------------------------
# Panel (2,0): GPU Utilization Paradox
ax = axes[2, 0]
cats = ["AI Spend", "GPU Util.", "Target", "Human"]
vals = [100, 5, 65, 85]
colors_util = [AI_SPEND, BUBBLE_ZONE, NORMAL_ZONE, WARNING_ZONE]
bars = ax.barh(cats, vals, color=colors_util, height=0.5)
for bar, v in zip(bars, vals):
ax.text(v + 2, bar.get_y() + bar.get_height() / 2,
f"{v}%", va="center", fontsize=8, fontweight="bold",
color=BLACK)
ax.set_title("GPU Utilization Paradox", fontsize=11, fontweight="bold")
ax.set_xlim(0, 115)
ax.tick_params(labelsize=8)
ax.grid(True, alpha=0.2, axis="x")
# Panel (2,1): Developer AI Reality
ax = axes[2, 1]
dev_cats = ["Use AI tools", "Daily AI use", "AI code merged", "AI code has vulns"]
dev_vals = [84, 51, 22, 48]
dev_colors = [AGENT_GROWTH, AGENT_GROWTH, NORMAL_ZONE, BUBBLE_ZONE]
bars = ax.barh(dev_cats, dev_vals, color=dev_colors, height=0.5)
for bar, v in zip(bars, dev_vals):
ax.text(v + 2, bar.get_y() + bar.get_height() / 2,
f"{v}%", va="center", fontsize=8, fontweight="bold",
color=BLACK)
ax.set_title("Developer AI Reality", fontsize=11, fontweight="bold")
ax.set_xlim(0, 100)
ax.tick_params(labelsize=8)
ax.grid(True, alpha=0.2, axis="x")
# Panel (2,2): Enterprise Agent Adoption
ax = axes[2, 2]
surveys = ["LangChain", "McKinsey", "PwC"]
labels = ["In production", "Scaling agents", "Measurable value"]
adoption = [
agent_survey_data["langchain_2025"]["production"],
agent_survey_data["mckinsey_2025"]["agentic_ai_scaling"],
agent_survey_data["pwc_2025"]["measurable_productivity_value"],
]
bars = ax.barh(surveys, adoption, color=AGENT_GROWTH, height=0.5)
for bar, v, label in zip(bars, adoption, labels):
ax.text(v + 1, bar.get_y() + bar.get_height() / 2,
f"{v:.0f}% ({label})", va="center", fontsize=7,
fontweight="bold", color=BLACK)
ax.set_title("Enterprise Agent Adoption", fontsize=11, fontweight="bold")
ax.set_xlim(0, 100)
ax.tick_params(labelsize=8)
ax.grid(True, alpha=0.2, axis="x")
# ------------------------------------------------------------------
# Row labels (vertical text on the left)
# ------------------------------------------------------------------
fig.text(0.02, 0.78, "MARKET BUBBLE EVIDENCE", fontsize=10,
fontweight="bold", color=GRAY_MEDIUM, rotation=90,
va="center")
fig.text(0.02, 0.50, "AI INFRASTRUCTURE BUILDOUT", fontsize=10,
fontweight="bold", color=GRAY_MEDIUM, rotation=90,
va="center")
fig.text(0.02, 0.16, "AGENT REVOLUTION & REALITY", fontsize=10,
fontweight="bold", color=GRAY_MEDIUM, rotation=90,
va="center")
# ------------------------------------------------------------------
# Overall title
# ------------------------------------------------------------------
fig.suptitle(
"The AI Bubble and the Fundamental Value of LLMs — June 2026",
fontsize=20, fontweight="bold", y=0.98,
)
fig.subplots_adjust(
hspace=0.35, wspace=0.25,
left=0.06, right=0.98,
top=0.95, bottom=0.04,
)
out_path = "output/combined/narrative_dashboard.png"
plt.rcParams['savefig.bbox'] = None # Disable tight cropping for full 20x16 output
fig.savefig(
out_path, dpi=EXPORT_DPI,
facecolor=fig.get_facecolor(), edgecolor="none",
)
plt.close(fig)
return out_path
def main():
path = plot_narrative_dashboard()
print(f"Dashboard saved: {path}")
if __name__ == "__main__":
main()