{"spec_id":"line-yield-curve","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nline-yield-curve: Yield Curve (Interest Rate Term Structure)\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens — Imprint palette\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint categorical palette — canonical order, first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — U.S. Treasury yield curves on three dates\nmaturities = [\"1M\", \"3M\", \"6M\", \"1Y\", \"2Y\", \"3Y\", \"5Y\", \"7Y\", \"10Y\", \"20Y\", \"30Y\"]\nmaturity_years = [1 / 12, 0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30]\n\n# Normal upward-sloping curve (Jan 2018)\nyields_normal = [1.28, 1.53, 1.72, 1.89, 2.05, 2.19, 2.41, 2.55, 2.66, 2.83, 2.96]\n\n# Inverted curve (Aug 2019 — recession signal)\nyields_inverted = [2.09, 2.00, 1.92, 1.75, 1.52, 1.46, 1.44, 1.48, 1.52, 1.77, 1.97]\n\n# Steep post-pandemic curve (Mar 2021)\nyields_steep = [0.03, 0.03, 0.04, 0.07, 0.14, 0.32, 0.83, 1.18, 1.62, 2.19, 2.35]\n\n# Use ordered Categorical so color assignments follow the named order\nDATE_ORDER = [\"Jan 2018 (Normal)\", \"Aug 2019 (Inverted)\", \"Mar 2021 (Steep)\"]\n\nrows = []\nfor i in range(len(maturities)):\n    rows.append(\n        {\n            \"maturity\": maturities[i],\n            \"maturity_years\": maturity_years[i],\n            \"yield_pct\": yields_normal[i],\n            \"date\": \"Jan 2018 (Normal)\",\n        }\n    )\n    rows.append(\n        {\n            \"maturity\": maturities[i],\n            \"maturity_years\": maturity_years[i],\n            \"yield_pct\": yields_inverted[i],\n            \"date\": \"Aug 2019 (Inverted)\",\n        }\n    )\n    rows.append(\n        {\n            \"maturity\": maturities[i],\n            \"maturity_years\": maturity_years[i],\n            \"yield_pct\": yields_steep[i],\n            \"date\": \"Mar 2021 (Steep)\",\n        }\n    )\n\ndf = pd.DataFrame(rows)\ndf[\"date\"] = pd.Categorical(df[\"date\"], categories=DATE_ORDER, ordered=True)\n\n# Inversion region: shade where short-term yields exceed the 10Y baseline\nten_year_yield = yields_inverted[8]  # 10Y = 1.52%\ninv_mat = [maturity_years[i] for i in range(9)]  # 1M through 10Y\ninv_upper = [yields_inverted[i] for i in range(9)]\ninv_lower = [ten_year_yield] * 9\ninversion_df = pd.DataFrame({\"maturity_years\": inv_mat, \"y_upper\": inv_upper, \"y_lower\": inv_lower})\n\n# Sparse ticks — removes 3M/1Y crowding at short maturities\ntick_positions = [0.5, 1, 2, 5, 10, 20, 30]\ntick_labels_x = [\"6M\", \"1Y\", \"2Y\", \"5Y\", \"10Y\", \"20Y\", \"30Y\"]\n\nplot = (\n    ggplot()\n    # Inversion region highlight — ribbon between inverted curve and 10Y baseline\n    + geom_ribbon(\n        data=inversion_df,\n        mapping=aes(x=\"maturity_years\", ymin=\"y_lower\", ymax=\"y_upper\"),\n        fill=\"#AE3030\",\n        alpha=0.18,\n    )\n    # Yield curve lines with interactive tooltips\n    + geom_line(\n        data=df,\n        mapping=aes(x=\"maturity_years\", y=\"yield_pct\", color=\"date\"),\n        size=1.0,\n        tooltips=layer_tooltips()\n        .line(\"@date\")\n        .line(\"Maturity: @maturity\")\n        .line(\"Yield: @yield_pct%\"),\n    )\n    + geom_point(\n        data=df,\n        mapping=aes(x=\"maturity_years\", y=\"yield_pct\", color=\"date\"),\n        size=3.5,\n        alpha=0.85,\n    )\n    # Inversion region label — geom_text size is in mm, not pt\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=pd.DataFrame({\"x\": [1.5], \"y\": [2.15], \"label\": [\"Inversion Region\"]}),\n        color=\"#AE3030\",\n        size=4,\n        fontface=\"italic\",\n    )\n    + scale_color_manual(values=IMPRINT_PALETTE[:3])\n    + scale_x_continuous(breaks=tick_positions, labels=tick_labels_x)\n    + labs(\n        x=\"Maturity\", y=\"Yield (%)\", title=\"line-yield-curve · python · letsplot · anyplot.ai\", color=\"\"\n    )\n    + ggsize(800, 450)\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major_y=element_line(color=INK_SOFT, size=0.2),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        axis_title=element_text(color=INK, size=12),\n        axis_text=element_text(color=INK_SOFT, size=10),\n        axis_line=element_line(color=INK_SOFT),\n        plot_title=element_text(color=INK, size=16, face=\"bold\"),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(color=INK_SOFT, size=10),\n        legend_title=element_text(color=INK),\n        panel_border=element_blank(),\n        legend_position=\"top\",\n    )\n)\n\n# Save PNG (scale=4 → 800×450 × 4 = 3200×1800 px) and HTML for the current theme\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}