{"spec_id":"scatter-regression-polynomial","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-regression-polynomial: Scatter Plot with Polynomial Regression\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 94/100 | Created: 2026-08-11\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fixed-seed LCG — the browser has no seeded RNG\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\n// Fertilizer application vs. crop yield — classic diminishing-returns curve:\n// yield rises with fertilizer, then plateaus and slightly declines (over-application).\nconst n = 85;\nconst data = [];\nfor (let i = 0; i < n; i++) {\n  const fertilizer = 5 + rand() * 195;\n  const trueYield =\n    1.8 + 0.095 * fertilizer - 0.00032 * fertilizer * fertilizer;\n  // Box-Muller normal noise, inlined (only used here)\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const noise = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  const yieldTons = Math.max(0.2, trueYield + noise * 0.9);\n  data.push({ fertilizer, yieldTons });\n}\ndata.sort((a, b) => a.fertilizer - b.fertilizer);\n\n// --- Polynomial regression (least squares via normal equations) ------------\nfunction polyfit(xs, ys, degree) {\n  const m = degree + 1;\n  const xtx = Array.from({ length: m }, () => new Array(m).fill(0));\n  const xty = new Array(m).fill(0);\n  for (let i = 0; i < xs.length; i++) {\n    const powers = new Array(2 * m - 1);\n    powers[0] = 1;\n    for (let p = 1; p < powers.length; p++) powers[p] = powers[p - 1] * xs[i];\n    for (let r = 0; r < m; r++) {\n      xty[r] += powers[r] * ys[i];\n      for (let c = 0; c < m; c++) xtx[r][c] += powers[r + c];\n    }\n  }\n  for (let col = 0; col < m; col++) {\n    let pivot = col;\n    for (let r = col + 1; r < m; r++) {\n      if (Math.abs(xtx[r][col]) > Math.abs(xtx[pivot][col])) pivot = r;\n    }\n    [xtx[col], xtx[pivot]] = [xtx[pivot], xtx[col]];\n    [xty[col], xty[pivot]] = [xty[pivot], xty[col]];\n    for (let r = col + 1; r < m; r++) {\n      const factor = xtx[r][col] / xtx[col][col];\n      for (let c = col; c < m; c++) xtx[r][c] -= factor * xtx[col][c];\n      xty[r] -= factor * xty[col];\n    }\n  }\n  const coeffs = new Array(m).fill(0);\n  for (let r = m - 1; r >= 0; r--) {\n    let sum = xty[r];\n    for (let c = r + 1; c < m; c++) sum -= xtx[r][c] * coeffs[c];\n    coeffs[r] = sum / xtx[r][r];\n  }\n  return coeffs; // [c0, c1, c2] low-to-high degree\n}\nfunction evalPoly(coeffs, x) {\n  let result = 0;\n  let xp = 1;\n  for (const c of coeffs) {\n    result += c * xp;\n    xp *= x;\n  }\n  return result;\n}\n\nconst xs = data.map((d) => d.fertilizer);\nconst ys = data.map((d) => d.yieldTons);\nconst degree = 2;\nconst coeffs = polyfit(xs, ys, degree);\nconst predictions = xs.map((x) => evalPoly(coeffs, x));\nconst yMean = d3.mean(ys);\nconst ssRes = d3.sum(predictions.map((p, i) => (ys[i] - p) ** 2));\nconst ssTot = d3.sum(ys.map((y) => (y - yMean) ** 2));\nconst r2 = 1 - ssRes / ssTot;\nconst residualSE = Math.sqrt(ssRes / (n - degree - 1));\n\nconst curveSteps = 100;\nconst [xMin, xMax] = d3.extent(xs);\nconst curve = d3.range(curveSteps + 1).map((i) => {\n  const x = xMin + ((xMax - xMin) * i) / curveSteps;\n  const y = evalPoly(coeffs, x);\n  return { x, yLow: y - 1.96 * residualSE, y, yHigh: y + 1.96 * residualSE };\n});\n\n// --- Layout -------------------------------------------------------------\nconst margin = { top: 130, right: 70, bottom: 100, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst x = d3\n  .scaleLinear()\n  .domain([0, xMax * 1.03])\n  .nice()\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max([...ys, ...curve.map((d) => d.yHigh)]) * 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 (both axes — scatter plot) ----------------------------------\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickSize(-ih).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.append(\"g\")\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// --- Confidence band (95%, from residual std. error) ------------------------\nconst bandArea = d3\n  .area()\n  .x((d) => x(d.x))\n  .y0((d) => y(d.yLow))\n  .y1((d) => y(d.yHigh))\n  .curve(d3.curveBasis);\ng.append(\"path\")\n  .datum(curve)\n  .attr(\"d\", bandArea)\n  .attr(\"fill\", t.palette[1])\n  .attr(\"opacity\", 0.11);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(7));\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// --- Scatter points -----------------------------------------------------\ng.selectAll(\"circle\")\n  .data(data)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.fertilizer))\n  .attr(\"cy\", (d) => y(d.yieldTons))\n  .attr(\"r\", 7)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.65)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.8);\n\n// --- Fitted polynomial curve --------------------------------------------\nconst curveLine = d3\n  .line()\n  .x((d) => x(d.x))\n  .y((d) => y(d.y))\n  .curve(d3.curveBasis);\ng.append(\"path\")\n  .datum(curve)\n  .attr(\"d\", curveLine)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 3.5);\n\n// --- Hover guide (interactive HTML export only) -----------------------------\n// d3-array's bisector locates the nearest point along x in O(log n); a static\n// screenshot never fires mouse events, so this only activates in plot-*.html.\nconst bisectFertilizer = d3.bisector((d) => d.fertilizer).left;\nconst focus = g.append(\"g\").style(\"display\", \"none\");\nfocus\n  .append(\"line\")\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-dasharray\", \"4,3\");\nfocus\n  .append(\"circle\")\n  .attr(\"r\", 9)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5);\nconst focusLabel = focus\n  .append(\"text\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\");\n\ng.append(\"rect\")\n  .attr(\"width\", iw)\n  .attr(\"height\", ih)\n  .attr(\"fill\", \"none\")\n  .attr(\"pointer-events\", \"all\")\n  .on(\"mouseenter\", () => focus.style(\"display\", null))\n  .on(\"mouseleave\", () => focus.style(\"display\", \"none\"))\n  .on(\"mousemove\", (event) => {\n    const x0 = x.invert(d3.pointer(event)[0]);\n    const i = bisectFertilizer(data, x0, 1);\n    const prev = data[i - 1];\n    const next = data[i] || prev;\n    const d = x0 - prev.fertilizer > next.fertilizer - x0 ? next : prev;\n    const px = x(d.fertilizer);\n    const nearRightEdge = iw - px < 140;\n    focus.attr(\"transform\", `translate(${px},0)`);\n    focus.select(\"circle\").attr(\"cy\", y(d.yieldTons));\n    focusLabel\n      .attr(\"text-anchor\", nearRightEdge ? \"end\" : \"start\")\n      .attr(\"x\", nearRightEdge ? -14 : 14)\n      .attr(\"y\", y(d.yieldTons) - 16)\n      .text(`${d.fertilizer.toFixed(0)} kg → ${d.yieldTons.toFixed(2)} t`);\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\", \"18px\")\n  .text(\"Fertilizer Application (kg/hectare)\");\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Crop Yield (tons/hectare)\");\n\n// --- R² + equation annotation ------------------------------------------\nconst [b, mCoef, aCoef] = coeffs;\nconst sign = (v) => (v >= 0 ? \"+\" : \"−\");\nconst equation = `y = ${aCoef.toFixed(4)}x² ${sign(mCoef)} ${Math.abs(mCoef).toFixed(3)}x ${sign(b)} ${Math.abs(b).toFixed(2)}`;\n\nconst box = g.append(\"g\").attr(\"transform\", \"translate(16, 16)\");\nbox\n  .append(\"rect\")\n  .attr(\"width\", 372)\n  .attr(\"height\", 112)\n  .attr(\"rx\", 10)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid);\nbox\n  .append(\"text\")\n  .attr(\"x\", 22)\n  .attr(\"y\", 40)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(`R² = ${r2.toFixed(3)}`);\nbox\n  .append(\"text\")\n  .attr(\"x\", 22)\n  .attr(\"y\", 76)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(equation);\nbox\n  .append(\"text\")\n  .attr(\"x\", 22)\n  .attr(\"y\", 98)\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"opacity\", 0.8)\n  .style(\"font-size\", \"13px\")\n  .text(\"Degree-2 (quadratic) fit, 95% band\");\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 58)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"42px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-regression-polynomial · javascript · d3 · anyplot.ai\");\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 100)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"17px\")\n  .text(\"Diminishing returns: crop yield vs. fertilizer application\");\n"}