{"spec_id":"line-loss-training","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-loss-training: Training Loss Curve\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 132, right: 70, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: deterministic training run (fixed-seed LCG, no Math.random) ------\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\n\nconst epochs = d3.range(1, 61);\nconst overfitStart = 30;\nconst history = epochs.map((epoch) => {\n  const smooth = 2.7 * Math.exp(-epoch / 17) + 0.06;\n  const trainLoss = Math.max(0.02, smooth + (rand() - 0.5) * 0.03);\n  const overfitTerm =\n    epoch > overfitStart ? 0.0011 * (epoch - overfitStart) ** 2 : 0;\n  const valLoss = Math.max(\n    0.02,\n    smooth + 0.1 + overfitTerm + (rand() - 0.5) * 0.05,\n  );\n  return { epoch, trainLoss, valLoss };\n});\n\nconst minValPoint = history.reduce((best, d) =>\n  d.valLoss < best.valLoss ? d : best,\n);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain(d3.extent(epochs)).range([0, iw]);\nconst maxLoss = d3.max(history, (d) => Math.max(d.trainLoss, d.valLoss));\nconst y = d3\n  .scaleLinear()\n  .domain([0, maxLoss * 1.08])\n  .nice()\n  .range([ih, 0]);\n\n// --- SVG mount ------------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (y-axis only) -----------------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Axes -------------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(10).tickFormat(d3.format(\"d\")));\nconst yAxis = g\n  .append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(\".1f\")));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\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(\"Epoch\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Cross-Entropy Loss\");\n\n// --- Reference marker: epoch of minimum validation loss ------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(minValPoint.epoch))\n  .attr(\"x2\", x(minValPoint.epoch))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,5\")\n  .attr(\"opacity\", 0.5);\n\ng.append(\"text\")\n  .attr(\"x\", x(minValPoint.epoch))\n  .attr(\"y\", -32)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`min val loss · epoch ${minValPoint.epoch}`);\n\n// --- Generalization-gap fill: d3.area() between train/val after the overfit point\nconst gapData = history.filter((d) => d.epoch >= overfitStart);\nconst gapArea = d3\n  .area()\n  .x((d) => x(d.epoch))\n  .y0((d) => y(d.trainLoss))\n  .y1((d) => y(d.valLoss))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(gapData)\n  .attr(\"fill\", t.palette[1])\n  .attr(\"fill-opacity\", 0.12)\n  .attr(\"stroke\", \"none\")\n  .attr(\"d\", gapArea);\n\n// --- Loss curves ----------------------------------------------------------------\nconst line = (accessor) =>\n  d3\n    .line()\n    .x((d) => x(d.epoch))\n    .y((d) => y(accessor(d)))\n    .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(history)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\n    \"d\",\n    line((d) => d.trainLoss),\n  );\n\ng.append(\"path\")\n  .datum(history)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\n    \"d\",\n    line((d) => d.valLoss),\n  );\n\ng.append(\"circle\")\n  .attr(\"cx\", x(minValPoint.epoch))\n  .attr(\"cy\", y(minValPoint.valLoss))\n  .attr(\"r\", 8)\n  .attr(\"fill\", t.pageBg)\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 3);\n\n// --- Legend -----------------------------------------------------------------------\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${iw - 210}, 10)`);\nconst legendItems = [\n  { label: \"Training loss\", color: t.palette[0] },\n  { label: \"Validation loss\", color: t.palette[1] },\n];\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0, ${i * 30})`);\n  row\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 26)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", item.color)\n    .attr(\"stroke-width\", 3.5);\n  row\n    .append(\"text\")\n    .attr(\"x\", 36)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n});\n\n// --- Title ------------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-loss-training · javascript · d3 · anyplot.ai\");\n"}