{"spec_id":"line-yield-curve","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-yield-curve: Yield Curve (Interest Rate Term Structure)\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (Imprint palette — theme-adaptive chrome)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\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 — AE3030 used semantically for the inverted/crisis curve\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"  # warning / caution anchor — inversion region shading\n\n# Data — U.S. Treasury yield curves: normal, inverted, and normalizing periods\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# Jan 2022 — Normal upward-sloping curve (pre-tightening)\nyields_normal = [0.08, 0.21, 0.47, 0.78, 1.18, 1.42, 1.63, 1.78, 1.78, 2.11, 2.07]\n\n# Jul 2023 — Inverted curve (peak inversion; AE3030 = semantic red for recession signal)\nyields_inverted = [5.40, 5.49, 5.52, 5.40, 4.87, 4.56, 4.18, 4.06, 3.96, 4.22, 4.03]\n\n# Jan 2025 — Normalizing curve (post-pivot)\nyields_normalizing = [4.34, 4.35, 4.30, 4.16, 4.20, 4.25, 4.38, 4.49, 4.58, 4.87, 4.81]\n\nrecords = []\nfor i, mat in enumerate(maturities):\n    records.append(\n        {\n            \"maturity\": mat,\n            \"maturity_years\": maturity_years[i],\n            \"yield_pct\": yields_normal[i],\n            \"date\": \"Jan 2022 (Normal)\",\n            \"order\": 1,\n        }\n    )\n    records.append(\n        {\n            \"maturity\": mat,\n            \"maturity_years\": maturity_years[i],\n            \"yield_pct\": yields_inverted[i],\n            \"date\": \"Jul 2023 (Inverted)\",\n            \"order\": 2,\n        }\n    )\n    records.append(\n        {\n            \"maturity\": mat,\n            \"maturity_years\": maturity_years[i],\n            \"yield_pct\": yields_normalizing[i],\n            \"date\": \"Jan 2025 (Normalizing)\",\n            \"order\": 3,\n        }\n    )\n\ndf = pd.DataFrame(records)\n\n# Inversion region — amber shading marks where short-term rates exceed long-term rates\ninversion_df = pd.DataFrame({\"x_start\": [1 / 12], \"x_end\": [7]})\n\ninversion_shade = (\n    alt.Chart(inversion_df)\n    .mark_rect(opacity=0.12, color=ANYPLOT_AMBER)\n    .encode(x=alt.X(\"x_start:Q\"), x2=\"x_end:Q\", y=alt.value(0), y2=alt.value(320))\n)\n\ninversion_label = (\n    alt.Chart(pd.DataFrame({\"x\": [0.12], \"y\": [3.0], \"text\": [\"Inversion Region\"]}))\n    .mark_text(fontSize=16, align=\"left\", fontStyle=\"italic\", color=ANYPLOT_AMBER, fontWeight=\"bold\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Series colors — Imprint: green=normal growth, red=inversion/crisis, blue=normalizing\ndate_order = [\"Jan 2022 (Normal)\", \"Jul 2023 (Inverted)\", \"Jan 2025 (Normalizing)\"]\ncolors = [IMPRINT_PALETTE[0], IMPRINT_PALETTE[4], IMPRINT_PALETTE[2]]  # #009E73, #AE3030, #4467A3\n\n# Peak annotation for the inverted curve\npeak_annotation = (\n    alt.Chart(pd.DataFrame({\"x\": [0.5], \"y\": [5.52], \"text\": [\"Peak: 5.52%\"]}))\n    .mark_text(fontSize=16, align=\"left\", dx=10, dy=-10, color=IMPRINT_PALETTE[4], fontWeight=\"bold\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Title — scaled from default 16px for 76-char string (floor 11)\ntitle_str = \"U.S. Treasury Yield Curves · line-yield-curve · python · altair · anyplot.ai\"\ntitle_fontsize = max(11, round(16 * 67 / len(title_str)))\n\n# Axis encoding\nx_axis = alt.X(\n    \"maturity_years:Q\",\n    title=\"Maturity (Years)\",\n    scale=alt.Scale(type=\"log\", domain=[0.08, 35]),\n    axis=alt.Axis(\n        values=[1 / 12, 0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30],\n        labelExpr=(\n            \"datum.value < 0.09 ? '1M' : datum.value < 0.3 ? '3M' : datum.value < 0.6 ? '6M' : datum.value + 'Y'\"\n        ),\n    ),\n)\n\ny_axis = alt.Y(\n    \"yield_pct:Q\",\n    title=\"Yield (%)\",\n    scale=alt.Scale(domain=[0, 5.9]),  # tightened from [0, 6] for better canvas utilisation\n)\n\ncolor_enc = alt.Color(\n    \"date:N\",\n    scale=alt.Scale(domain=date_order, range=colors),\n    legend=alt.Legend(\n        title=None, labelFontSize=10, labelLimit=300, orient=\"top-right\", symbolStrokeWidth=3, symbolSize=100\n    ),\n    sort=date_order,\n)\n\n# Plot layers: inversion shade + lines + points + annotation labels\nline = (\n    alt.Chart(df)\n    .mark_line(strokeWidth=3)\n    .encode(\n        x=x_axis, y=y_axis, color=color_enc, tooltip=[\"maturity:N\", \"yield_pct:Q\", \"date:N\"], order=\"maturity_years:Q\"\n    )\n)\n\npoints = (\n    alt.Chart(df)\n    .mark_point(size=100, filled=True)\n    .encode(\n        x=\"maturity_years:Q\",\n        y=\"yield_pct:Q\",\n        color=alt.Color(\"date:N\", scale=alt.Scale(domain=date_order, range=colors), legend=None, sort=date_order),\n        tooltip=[\"maturity:N\", \"yield_pct:Q\", \"date:N\"],\n    )\n)\n\n# Canvas: landscape inner view 620×320, scale_factor=4.0 → PIL-padded to exactly 3200×1800\nTW, TH = 3200, 1800\n\nchart = (\n    (inversion_shade + line + points + inversion_label + peak_annotation)\n    .properties(\n        width=620, height=320, background=PAGE_BG, title=alt.Title(title_str, fontSize=title_fontsize, anchor=\"middle\")\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, strokeWidth=0)\n    .configure_title(color=INK)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.12,\n        gridDash=[4, 4],\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=12,\n    )\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\n# Save PNG then pad to exact 3200×1800 target\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.interactive().save(f\"plot-{THEME}.html\")\n"}