{"spec_id":"learning-curve-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// learning-curve-basic: Model Learning Curve\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 80, bottom: 110, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: simulated sklearn-style learning_curve() output -----------------\n// Fixed-seed LCG (no RNG in the browser) — reproducible fold noise.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (1664525 * state + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\nfunction gaussian() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Digit-classification model: accuracy vs. number of training samples.\nconst trainSizes = [200, 500, 900, 1400, 2000, 2700, 3500, 4400];\nconst folds = 6;\n\nfunction trendMean(size, start, end, scale) {\n  return start + (end - start) * (1 - Math.exp(-size / scale));\n}\n\nfunction foldScores(meanFn, stdFn, size) {\n  const mean = meanFn(size);\n  const std = stdFn(size);\n  const scores = [];\n  for (let f = 0; f < folds; f++) {\n    scores.push(Math.min(1, Math.max(0, mean + std * gaussian())));\n  }\n  return scores;\n}\n\nconst trainMeanFn = (size) => trendMean(size, 0.995, 0.935, 1300);\nconst trainStdFn = (size) => 0.006 + 0.02 * Math.exp(-size / 1800);\nconst valMeanFn = (size) => trendMean(size, 0.7, 0.925, 1300);\nconst valStdFn = (size) => 0.012 + 0.05 * Math.exp(-size / 1800);\n\nfunction summarize(scores) {\n  const mean = scores.reduce((a, b) => a + b, 0) / scores.length;\n  const variance = scores.reduce((a, b) => a + (b - mean) ** 2, 0) / scores.length;\n  return { mean, std: Math.sqrt(variance) };\n}\n\nconst trainStats = trainSizes.map((s) => summarize(foldScores(trainMeanFn, trainStdFn, s)));\nconst valStats = trainSizes.map((s) => summarize(foldScores(valMeanFn, valStdFn, s)));\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// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain(d3.extent(trainSizes)).range([0, iw]);\nconst yMin = d3.min([...trainStats, ...valStats], (d) => d.mean - d.std) - 0.03;\nconst yMax = d3.max([...trainStats, ...valStats], (d) => d.mean + d.std) + 0.03;\nconst y = d3.scaleLinear().domain([yMin, yMax]).nice().range([ih, 0]);\n\n// --- Gridlines (y-axis only, subtle) ------------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid);\n\n// --- Confidence bands (±1 std across folds) -----------------------------------\nconst bandColor = (color) => color;\nconst trainArea = d3\n  .area()\n  .x((d, i) => x(trainSizes[i]))\n  .y0((d) => y(d.mean - d.std))\n  .y1((d) => y(d.mean + d.std))\n  .curve(d3.curveMonotoneX);\nconst valArea = d3\n  .area()\n  .x((d, i) => x(trainSizes[i]))\n  .y0((d) => y(d.mean - d.std))\n  .y1((d) => y(d.mean + d.std))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(trainStats).attr(\"d\", trainArea).attr(\"fill\", t.palette[0]).attr(\"opacity\", 0.15);\ng.append(\"path\").datum(valStats).attr(\"d\", valArea).attr(\"fill\", t.palette[1]).attr(\"opacity\", 0.15);\n\n// --- Lines ---------------------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d, i) => x(trainSizes[i]))\n  .y((d) => y(d.mean))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(trainStats).attr(\"d\", line).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 3.5);\ng.append(\"path\").datum(valStats).attr(\"d\", line).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[1]).attr(\"stroke-width\", 3.5);\n\n// --- Markers ---------------------------------------------------------------------\ng.selectAll(\".train-dot\")\n  .data(trainStats)\n  .join(\"circle\")\n  .attr(\"cx\", (d, i) => x(trainSizes[i]))\n  .attr(\"cy\", (d) => y(d.mean))\n  .attr(\"r\", 8.5)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\ng.selectAll(\".val-dot\")\n  .data(valStats)\n  .join(\"circle\")\n  .attr(\"cx\", (d, i) => x(trainSizes[i]))\n  .attr(\"cy\", (d) => y(d.mean))\n  .attr(\"r\", 8.5)\n  .attr(\"fill\", t.palette[1])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues(trainSizes).tickFormat(d3.format(\",\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(\".0%\")));\n\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// --- Axis labels ---------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Training Set Size (samples)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -95)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Accuracy\");\n\n// --- Legend (free space: top-right, above the converging curves) ---------------\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${iw - 300},10)`);\nlegend\n  .append(\"rect\")\n  .attr(\"x\", -16)\n  .attr(\"y\", -14)\n  .attr(\"width\", 250)\n  .attr(\"height\", 82)\n  .attr(\"rx\", 10)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid);\nconst legendItems = [\n  { label: \"Training score\", color: t.palette[0] },\n  { label: \"Validation score\", color: t.palette[1] },\n];\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 34})`);\n  row.append(\"line\").attr(\"x1\", 0).attr(\"x2\", 28).attr(\"y1\", 0).attr(\"y2\", 0).attr(\"stroke\", item.color).attr(\"stroke-width\", 3.5);\n  row\n    .append(\"text\")\n    .attr(\"x\", 38)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(item.label);\n});\n\n// --- Annotation: call out the narrowing train/validation gap --------------------\n// Placed in the empty lower-middle whitespace, well clear of the legend and\n// the axis labels, with a dashed leader pointing at the converged tail.\nconst lastIdx = trainSizes.length - 1;\nconst gapX = x(trainSizes[lastIdx]);\nconst gapMidY = (y(trainStats[lastIdx].mean) + y(valStats[lastIdx].mean)) / 2;\nconst labelX = iw * 0.5;\nconst labelY = ih * 0.6;\ng.append(\"line\")\n  .attr(\"x1\", labelX + 8)\n  .attr(\"y1\", labelY - 8)\n  .attr(\"x2\", gapX - 16)\n  .attr(\"y2\", gapMidY + 6)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1)\n  .attr(\"stroke-dasharray\", \"3,3\");\ng.append(\"text\")\n  .attr(\"x\", labelX)\n  .attr(\"y\", labelY)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Gap narrows as training data grows\");\n\n// --- Title -----------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"27px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"learning-curve-basic · javascript · d3 · anyplot.ai\");\n"}