{"spec_id":"curve-bias-variance-tradeoff","library":"d3","language":"javascript","code":"// anyplot.ai\n// curve-bias-variance-tradeoff: Bias-Variance Tradeoff Curve\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-24\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 170, right: 210, bottom: 90, left: 95 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (theoretical decomposition, deterministic) -------------------------\nconst modelComplexity = d3.range(0, 20.001, 0.25);\nconst biasSquared = modelComplexity.map((c) => 5 / (1 + 0.4 * c));\nconst variance = modelComplexity.map((c) => 0.011 * c * c);\nconst irreducibleError = modelComplexity.map(() => 1.0);\nconst totalError = modelComplexity.map(\n  (c, i) => biasSquared[i] + variance[i] + irreducibleError[i]\n);\n\nlet optimalIdx = 0;\nfor (let i = 1; i < totalError.length; i++) {\n  if (totalError[i] < totalError[optimalIdx]) optimalIdx = i;\n}\nconst optimalComplexity = modelComplexity[optimalIdx];\nconst optimalError = totalError[optimalIdx];\n\n// --- Scales --------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, 20]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, d3.max(totalError) * 1.1]).nice().range([ih, 0]);\n\n// --- SVG mount -------------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Underfitting / overfitting zones (drawn first, behind everything) -----------\ng.append(\"rect\")\n  .attr(\"x\", 0).attr(\"y\", 0)\n  .attr(\"width\", x(optimalComplexity)).attr(\"height\", ih)\n  .attr(\"fill\", t.palette[1]).attr(\"opacity\", 0.08);\n\ng.append(\"rect\")\n  .attr(\"x\", x(optimalComplexity)).attr(\"y\", 0)\n  .attr(\"width\", iw - x(optimalComplexity)).attr(\"height\", ih)\n  .attr(\"fill\", t.palette[2]).attr(\"opacity\", 0.08);\n\ng.append(\"text\")\n  .attr(\"x\", x(optimalComplexity) / 2).attr(\"y\", 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\").style(\"font-style\", \"italic\")\n  .text(\"Underfitting zone\");\n\ng.append(\"text\")\n  .attr(\"x\", x(optimalComplexity) + (iw - x(optimalComplexity)) / 2).attr(\"y\", 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\").style(\"font-style\", \"italic\")\n  .text(\"Overfitting zone\");\n\n// --- Y-axis grid (subtle, full-width) ---------------------------------------------\nconst grid = g.append(\"g\").call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"));\ngrid.selectAll(\"line\").attr(\"stroke\", t.grid);\ngrid.select(\".domain\").remove();\n\n// --- Axes ----------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(5));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Optimal-complexity marker ---------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(optimalComplexity)).attr(\"x2\", x(optimalComplexity))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.ink).attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"5,4\").attr(\"opacity\", 0.6);\n\ng.append(\"circle\")\n  .attr(\"cx\", x(optimalComplexity)).attr(\"cy\", y(optimalError)).attr(\"r\", 7)\n  .attr(\"fill\", t.palette[0]).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2);\n\nconst optimalAnchor = optimalComplexity > 12 ? \"end\" : \"start\";\ng.append(\"text\")\n  .attr(\"x\", x(optimalComplexity) + (optimalAnchor === \"end\" ? -12 : 12)).attr(\"y\", 46)\n  .attr(\"text-anchor\", optimalAnchor)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"15px\").style(\"font-weight\", \"600\")\n  .text(`Optimal complexity ≈ ${optimalComplexity.toFixed(1)}`);\n\n// --- Curves ----------------------------------------------------------------------\nconst line = d3.line().x((d) => x(d.c)).y((d) => y(d.v)).curve(d3.curveMonotoneX);\nconst toSeries = (arr) => modelComplexity.map((c, i) => ({ c, v: arr[i] }));\n\nconst curves = [\n  { label: \"Bias²\", data: toSeries(biasSquared), color: t.palette[1], width: 3, dash: \"10,5\" },\n  { label: \"Variance\", data: toSeries(variance), color: t.palette[2], width: 3, dash: \"3,3\" },\n  { label: \"Irreducible error\", data: toSeries(irreducibleError), color: t.ink, width: 2.5, dash: \"2,6\" },\n  { label: \"Total error\", data: toSeries(totalError), color: t.palette[0], width: 4, dash: \"none\" },\n];\n\nfor (const s of curves) {\n  const path = g.append(\"path\")\n    .datum(s.data)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", s.width)\n    .attr(\"d\", line);\n  if (s.dash !== \"none\") path.attr(\"stroke-dasharray\", s.dash);\n}\n\n// --- Direct end-of-line labels (collision-avoided) --------------------------------\nconst endLabels = curves\n  .map((s) => ({ label: s.label, color: s.color, y: y(s.data[s.data.length - 1].v) }))\n  .sort((a, b) => a.y - b.y);\n\nconst minGap = 22;\nfor (let i = 1; i < endLabels.length; i++) {\n  if (endLabels[i].y - endLabels[i - 1].y < minGap) {\n    endLabels[i].y = endLabels[i - 1].y + minGap;\n  }\n}\n\ng.selectAll(\".end-label\")\n  .data(endLabels)\n  .join(\"text\")\n  .attr(\"x\", iw + 12).attr(\"y\", (d) => d.y + 5)\n  .attr(\"fill\", (d) => d.color).style(\"font-size\", \"16px\").style(\"font-weight\", \"600\")\n  .text((d) => d.label);\n\n// --- Formula annotation -----------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 88).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"18px\").style(\"font-style\", \"italic\")\n  .text(\"Total Error = Bias² + Variance + Irreducible Error\");\n\n// --- Axis labels -------------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2).attr(\"y\", ih + 60).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"18px\")\n  .text(\"Model Complexity (Low → High)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2).attr(\"y\", -65).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"18px\")\n  .text(\"Prediction Error\");\n\n// --- Title ------------------------------------------------------------------------\nconst titleText = \"curve-bias-variance-tradeoff · javascript · d3 · anyplot.ai\";\nconst titleRatio = titleText.length > 67 ? 67 / titleText.length : 1;\nconst titleFontSize = Math.round(22 * titleRatio);\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 44).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", `${titleFontSize}px`).style(\"font-weight\", \"600\")\n  .text(titleText);\n"}