{"spec_id":"line-yield-curve","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-yield-curve: Yield Curve (Interest Rate Term Structure)\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-06-10\n\"\"\"\n\nimport os\nimport re\n\nimport cairosvg\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome tokens (Imprint style guide)\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — 8 hues, hybrid-v3 sort, theme-independent\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data — U.S. Treasury yield curves on three key dates\nmaturity_years = [0.083, 0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30]\n\n# Normal upward-sloping curve (Jan 2021)\nyields_normal = [0.04, 0.06, 0.07, 0.10, 0.13, 0.24, 0.44, 0.74, 1.09, 1.65, 1.87]\n\n# Flat curve (Dec 2018)\nyields_flat = [2.36, 2.40, 2.56, 2.63, 2.49, 2.46, 2.51, 2.59, 2.69, 2.87, 3.02]\n\n# Inverted curve (Mar 2023)\nyields_inverted = [4.73, 4.90, 5.09, 4.95, 4.60, 4.27, 3.85, 3.76, 3.58, 3.89, 3.70]\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=32,\n    tooltip_font_size=28,\n    stroke_width=2.5,\n    opacity=0.92,\n    opacity_hover=1.0,\n    title_font_family=\"sans-serif\",\n    label_font_family=\"sans-serif\",\n    major_label_font_family=\"sans-serif\",\n    legend_font_family=\"sans-serif\",\n    value_font_family=\"sans-serif\",\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    title=\"U.S. Treasury Yield Curves · line-yield-curve · pygal · anyplot.ai\",\n    x_title=\"Maturity (Years)\",\n    y_title=\"Yield (%)\",\n    style=custom_style,\n    show_dots=True,\n    dots_size=8,\n    stroke_style={\"width\": 4},\n    show_y_guides=True,\n    show_x_guides=False,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=4,\n    legend_box_size=36,\n    truncate_legend=-1,\n    x_value_formatter=lambda x: f\"{x:.0f}Y\" if x >= 1 else f\"{x * 12:.0f}M\",\n    y_value_formatter=lambda y: f\"{y:.2f}%\",\n    margin=80,\n    margin_top=120,\n    margin_bottom=140,\n    xrange=(0, 31),\n    range=(0, 5.5),\n    x_labels=[0.083, 0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30],\n    x_labels_major=[0.083, 1, 2, 5, 10, 20, 30],\n    show_minor_x_labels=False,\n    interpolate=\"cubic\",\n)\n\n# Imprint palette positions 1→3 for the three yield curve shapes\nnormal_points = list(zip(maturity_years, yields_normal, strict=False))\nflat_points = list(zip(maturity_years, yields_flat, strict=False))\ninverted_points = list(zip(maturity_years, yields_inverted, strict=False))\n\nchart.add(\"Jan 2021 (Normal)\", normal_points, dots_size=7)\nchart.add(\"Dec 2018 (Flat)\", flat_points, dots_size=7)\nchart.add(\"Mar 2023 (Inverted)\", inverted_points, dots_size=7)\n\n# Inversion highlight: 4th Imprint position (ochre) — oversized anchor dots at 2Y and 10Y\nspread_2y10y = 4.60 - 3.58\ninversion_pts = [(2, 4.60), (10, 3.58)]\nchart.add(\n    f\"2Y–10Y Inversion (−{spread_2y10y * 100:.0f} bps)\",\n    inversion_pts,\n    stroke_dasharray=\"15,10\",\n    dots_size=18,\n    show_dots=True,\n)\n\n# Render SVG for post-processing\nsvg_str = chart.render(is_unicode=True)\n\n# 1. Remove pygal's default chart frame border via CSS injection (DE-02 refinement)\nborder_css = \"  rect.background { stroke: none !important; } .chart-background { stroke: none !important; }\\n\"\nsvg_str = svg_str.replace(\"</style>\", border_css + \"  </style>\")\n\n# 2. Inject inversion zone shading rectangle (DE-01 / data storytelling enhancement)\n#    Coordinates derived from SVG circle positions (plot group translate: 222,196):\n#      x-scale = 93.14 px/year, x-origin = 48px; y-scale = 221.7 px/%, y-origin = 1243.62px\n#      2Y @ 4.60%: plot-local (234.3, 223.9)  |  10Y @ 3.58%: plot-local (979.4, 450.0)\n#    Semi-transparent ochre box frames the annotated 2Y–10Y spread region\ninversion_zone = (\n    '<rect x=\"234.3\" y=\"223.9\" width=\"745.1\" height=\"226.1\" '\n    'fill=\"rgba(189,130,51,0.13)\" stroke=\"rgba(189,130,51,0.45)\" '\n    'stroke-width=\"3\" stroke-dasharray=\"12,8\" />'\n)\n# Insert after the plot-area background rect (placed inside the plot group → behind data series)\nsvg_str = re.sub(\n    r'(<rect x=\"0\" y=\"0\" width=\"2898\" height=\"1268(?:\\.\\d+)?\" class=\"background\" />)', r\"\\1\\n\" + inversion_zone, svg_str\n)\n\n# Save PNG using cairosvg directly (preserves SVG post-processing)\ncairosvg.svg2png(\n    bytestring=svg_str.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=3200, output_height=1800\n)\n\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(\n        f\"\"\"<!DOCTYPE html>\n<html>\n<head>\n    <title>Yield Curve - pygal</title>\n    <style>\n        body {{ margin: 0; padding: 20px; background: {PAGE_BG}; color: {INK}; }}\n        svg {{ max-width: 100%; height: auto; }}\n    </style>\n</head>\n<body>\n    {svg_str}\n</body>\n</html>\"\"\"\n    )\n"}