{"spec_id":"line-yield-curve","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-yield-curve: Yield Curve (Interest Rate Term Structure)\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-10\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 60, bottom: 85, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: U.S. Treasury yield curves at three key dates ---\nconst maturities = [\n  { label: \"1M\",  years: 0.083 },\n  { label: \"3M\",  years: 0.25  },\n  { label: \"6M\",  years: 0.5   },\n  { label: \"1Y\",  years: 1     },\n  { label: \"2Y\",  years: 2     },\n  { label: \"3Y\",  years: 3     },\n  { label: \"5Y\",  years: 5     },\n  { label: \"7Y\",  years: 7     },\n  { label: \"10Y\", years: 10    },\n  { label: \"20Y\", years: 20    },\n  { label: \"30Y\", years: 30    },\n];\n\nconst curves = [\n  {\n    date: \"Jan 2022\",\n    shape: \"Normal\",\n    color: t.palette[0],  // brand green\n    strokeWidth: 2.5,\n    dashArray: null,\n    yields: [0.05, 0.32, 0.52, 0.79, 1.15, 1.43, 1.63, 1.78, 1.87, 2.17, 2.22],\n  },\n  {\n    date: \"Jan 2023\",\n    shape: \"Flat\",\n    color: t.palette[2],  // blue — financial convention (yield curves use blue for flat/transitional)\n    strokeWidth: 2.5,\n    dashArray: \"8,5\",\n    yields: [4.30, 4.65, 4.87, 4.72, 4.41, 4.20, 3.95, 3.82, 3.68, 3.87, 3.74],\n  },\n  {\n    date: \"Jul 2023\",\n    shape: \"Inverted\",\n    color: t.palette[4],  // matte red — semantic: recession signal\n    strokeWidth: 2.5,\n    dashArray: null,\n    yields: [5.50, 5.47, 5.53, 5.44, 4.92, 4.65, 4.37, 4.25, 3.96, 3.87, 3.82],\n  },\n];\n\ncurves.forEach(c => {\n  c.data = maturities.map((m, i) => ({ label: m.label, years: m.years, yield: c.yields[i] }));\n});\n\n// --- SVG mount ---\nconst svg = d3.select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---\n// Log scale places clustered short maturities with appropriate visual spacing\nconst x = d3.scaleLog().domain([0.07, 35]).range([0, iw]);\n\nconst allYields = curves.flatMap(c => c.yields);\nconst y = d3.scaleLinear()\n  .domain([Math.min(...allYields) - 0.25, Math.max(...allYields) + 0.25])\n  .nice()\n  .range([ih, 0]);\n\n// --- Horizontal gridlines (financial publication style: subtle, horizontal only) ---\ny.ticks(6).forEach(tick => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", y(tick)).attr(\"y2\", y(tick))\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1);\n});\n\n// --- Inversion zone shading ---\n// Shade between the Jul 2023 inverted curve and its 10Y yield baseline (1M–10Y)\nconst invCurve = curves[2];\nconst tenYrYield = invCurve.data.find(d => d.label === \"10Y\").yield;  // 3.96\n\nconst invShadeData = invCurve.data.slice(0, 9);  // 1M through 10Y\n\ng.append(\"path\")\n  .datum(invShadeData)\n  .attr(\"d\", d3.area()\n    .x(d => x(d.years))\n    .y0(y(tenYrYield))\n    .y1(d => y(d.yield))\n    .curve(d3.curveCatmullRom.alpha(0.5))\n  )\n  .attr(\"fill\", t.palette[4])\n  .attr(\"fill-opacity\", 0.09)\n  .attr(\"stroke\", \"none\");\n\n// Dashed reference line at the inverted curve's 10Y yield\ng.append(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", y(tenYrYield)).attr(\"y2\", y(tenYrYield))\n  .attr(\"stroke\", t.palette[4])\n  .attr(\"stroke-width\", 1)\n  .attr(\"stroke-dasharray\", \"4,4\")\n  .attr(\"opacity\", 0.45);\n\n// Label the 10Y reference line at its right endpoint so viewers understand the baseline\ng.append(\"text\").attr(\"x\", iw + 6).attr(\"y\", y(tenYrYield) - 4)\n  .attr(\"text-anchor\", \"start\").attr(\"fill\", t.palette[4]).attr(\"opacity\", 0.65)\n  .style(\"font-size\", \"11px\").text(\"10Y\");\ng.append(\"text\").attr(\"x\", iw + 6).attr(\"y\", y(tenYrYield) + 10)\n  .attr(\"text-anchor\", \"start\").attr(\"fill\", t.palette[4]).attr(\"opacity\", 0.65)\n  .style(\"font-size\", \"11px\").text(\"3.96%\");\n\n// d3.bisectLeft: find the 2Y maturity index to anchor the inversion annotation\nconst matYears = maturities.map(m => m.years);\nconst ann2YIdx = d3.bisectLeft(matYears, 2);\nconst annPt = invCurve.data[ann2YIdx];\ng.append(\"text\")\n  .attr(\"x\", x(annPt.years))\n  .attr(\"y\", y(annPt.yield) - 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.palette[4])\n  .attr(\"opacity\", 0.75)\n  .style(\"font-size\", \"11px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Inverted\");\n\n// --- Curves: lines + data point markers ---\nconst lineGen = d3.line()\n  .x(d => x(d.years))\n  .y(d => y(d.yield))\n  .curve(d3.curveCatmullRom.alpha(0.5));\n\ncurves.forEach(curve => {\n  const path = g.append(\"path\")\n    .datum(curve.data)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", curve.color)\n    .attr(\"stroke-width\", curve.strokeWidth)\n    .attr(\"d\", lineGen);\n\n  if (curve.dashArray) path.attr(\"stroke-dasharray\", curve.dashArray);\n\n  const dotsG = g.append(\"g\");\n  curve.data.forEach(d => {\n    dotsG.append(\"circle\")\n      .attr(\"cx\", x(d.years))\n      .attr(\"cy\", y(d.yield))\n      .attr(\"r\", 4)\n      .attr(\"fill\", curve.color)\n      .attr(\"stroke\", t.pageBg)\n      .attr(\"stroke-width\", 2);\n  });\n});\n\n// --- X-axis with explicit maturity tick labels ---\nconst xAxisG = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3.axisBottom(x)\n      .tickValues(maturities.map(m => m.years))\n      .tickFormat(d => {\n        const match = maturities.find(m => Math.abs(m.years / d - 1) < 0.01);\n        return match ? match.label : \"\";\n      })\n  );\nxAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxisG.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Y-axis ---\nconst yAxisG = g.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickFormat(d => `${d.toFixed(1)}%`));\nyAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAxisG.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Axis labels ---\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Maturity\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(ih / 2))\n  .attr(\"y\", -68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Yield (%)\");\n\n// --- Legend (inset, upper-right corner of plot area) ---\nconst legPad = 14;\nconst legLineH = 30;\nconst legW = 225;\nconst legH = curves.length * legLineH + legPad * 2;\nconst legG = g.append(\"g\").attr(\"transform\", `translate(${iw - legW - legPad},${legPad})`);\n\nlegG.append(\"rect\")\n  .attr(\"width\", legW).attr(\"height\", legH)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"rx\", 4)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\ncurves.forEach((curve, i) => {\n  const cy = legPad + i * legLineH + legLineH / 2;\n\n  const ll = legG.append(\"line\")\n    .attr(\"x1\", legPad).attr(\"y1\", cy)\n    .attr(\"x2\", legPad + 28).attr(\"y2\", cy)\n    .attr(\"stroke\", curve.color)\n    .attr(\"stroke-width\", 2.5);\n\n  if (curve.dashArray) ll.attr(\"stroke-dasharray\", curve.dashArray);\n\n  legG.append(\"circle\")\n    .attr(\"cx\", legPad + 14).attr(\"cy\", cy)\n    .attr(\"r\", 3.5)\n    .attr(\"fill\", curve.color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.5);\n\n  legG.append(\"text\")\n    .attr(\"x\", legPad + 36)\n    .attr(\"y\", cy + 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(`${curve.date} — ${curve.shape}`);\n});\n\n// --- Title ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-yield-curve · javascript · d3 · anyplot.ai\");\n"}