{"spec_id":"scatter-regression-linear","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-regression-linear: Scatter Plot with Linear Regression\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-08-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic, seeded LCG) ----------------------------\n// mulberry32: small fixed-seed PRNG (the browser has no seeded Math.random)\nconst rand = (() => {\n  let a = 42;\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let r = Math.imul(a ^ (a >>> 15), 1 | a);\n    r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;\n    return ((r ^ (r >>> 14)) >>> 0) / 4294967296;\n  };\n})();\n\nconst n = 70;\nconst trueSlope = 12;\nconst trueIntercept = 250;\nconst noiseStd = 150;\nconst data = [];\nfor (let i = 0; i < n; i++) {\n  const adSpend = 5 + rand() * 95; // $ thousands\n  const u1 = rand();\n  const u2 = rand();\n  const gaussian = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  const salesRevenue = trueSlope * adSpend + trueIntercept + gaussian * noiseStd;\n  data.push({ x: adSpend, y: salesRevenue });\n}\n\n// --- Linear regression (ordinary least squares) -----------------------------\nconst xBar = d3.mean(data, (d) => d.x);\nconst yBar = d3.mean(data, (d) => d.y);\nconst sXX = d3.sum(data, (d) => (d.x - xBar) ** 2);\nconst sYY = d3.sum(data, (d) => (d.y - yBar) ** 2);\nconst sXY = d3.sum(data, (d) => (d.x - xBar) * (d.y - yBar));\nconst slope = sXY / sXX;\nconst intercept = yBar - slope * xBar;\nconst r = sXY / Math.sqrt(sXX * sYY);\nconst rSquared = r * r;\nconst dof = n - 2;\nconst sse = d3.sum(data, (d) => (d.y - (slope * d.x + intercept)) ** 2);\nconst stdError = Math.sqrt(sse / dof);\nconst tValue = 2.0; // two-tailed 95% CI, df ~68 (t-table converges near 2.0)\n\nconst xExtent = d3.extent(data, (d) => d.x);\nconst fitLine = d3.range(0, 101).map((i) => {\n  const x = xExtent[0] + ((xExtent[1] - xExtent[0]) * i) / 100;\n  const yHat = slope * x + intercept;\n  const margin95 = tValue * stdError * Math.sqrt(1 / n + (x - xBar) ** 2 / sXX);\n  return { x, yHat, yLow: yHat - margin95, yHigh: yHat + margin95 };\n});\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3\n  .scaleLinear()\n  .domain([xExtent[0] - 5, xExtent[1] + 5])\n  .range([0, iw]);\nconst yDomainMin = Math.min(d3.min(fitLine, (d) => d.yLow), d3.min(data, (d) => d.y));\nconst yDomainMax = Math.max(d3.max(fitLine, (d) => d.yHigh), d3.max(data, (d) => d.y));\nconst y = d3\n  .scaleLinear()\n  .domain([yDomainMin, yDomainMax])\n  .nice()\n  .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// A soft drop-shadow filter (d3-authored SVG defs) gives the stats callout a\n// subtle elevated feel instead of a flat rect-on-rect look.\nconst defs = svg.append(\"defs\");\ndefs\n  .append(\"filter\")\n  .attr(\"id\", \"stats-shadow\")\n  .attr(\"x\", \"-20%\")\n  .attr(\"y\", \"-20%\")\n  .attr(\"width\", \"140%\")\n  .attr(\"height\", \"140%\")\n  .append(\"feDropShadow\")\n  .attr(\"dx\", 0)\n  .attr(\"dy\", 2)\n  .attr(\"stdDeviation\", 3)\n  .attr(\"flood-color\", \"#000000\")\n  .attr(\"flood-opacity\", 0.28);\n\n// --- Gridlines ------------------------------------------------------------\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).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).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Confidence band (95%) -----------------------------------------------\nconst area = d3\n  .area()\n  .x((d) => x(d.x))\n  .y0((d) => y(d.yLow))\n  .y1((d) => y(d.yHigh))\n  .curve(d3.curveLinear);\ng.append(\"path\").datum(fitLine).attr(\"d\", area).attr(\"fill\", t.palette[2]).attr(\"opacity\", 0.18);\n\n// --- Scatter points ---------------------------------------------------------\ng.selectAll(\"circle\")\n  .data(data)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.x))\n  .attr(\"cy\", (d) => y(d.y))\n  .attr(\"r\", 9)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.65)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\n\n// --- Regression line --------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.x))\n  .y((d) => y(d.yHat));\ng.append(\"path\")\n  .datum(fitLine)\n  .attr(\"d\", line)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[2])\n  .attr(\"stroke-width\", 4);\n\n// --- Residual callouts (d3-specific: sort + join to surface the largest\n// deviations from the fit, giving the trend a concrete storytelling anchor) --\nconst topResiduals = data\n  .map((d) => ({ ...d, yHat: slope * d.x + intercept }))\n  .sort((a, b) => Math.abs(b.y - b.yHat) - Math.abs(a.y - a.yHat))\n  .slice(0, 3);\ng.selectAll(\".residual-line\")\n  .data(topResiduals)\n  .join(\"line\")\n  .attr(\"class\", \"residual-line\")\n  .attr(\"x1\", (d) => x(d.x))\n  .attr(\"y1\", (d) => y(d.y))\n  .attr(\"x2\", (d) => x(d.x))\n  .attr(\"y2\", (d) => y(d.yHat))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.25)\n  .attr(\"stroke-dasharray\", \"3,3\")\n  .attr(\"stroke-opacity\", 0.55);\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickFormat((d) => `$${d}K`));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat((d) => `$${d3.format(\",\")(Math.round(d))}K`));\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).attr(\"stroke-opacity\", 0.5);\n}\n\n// --- Axis labels ---------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Advertising Spend\");\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\", \"17px\")\n  .text(\"Monthly Sales Revenue\");\n\n// --- Fit statistics annotation (spec asks R²/r to be shown prominently) -------\nconst statsBox = g.append(\"g\").attr(\"transform\", \"translate(16, 16)\");\nstatsBox\n  .append(\"rect\")\n  .attr(\"width\", 250)\n  .attr(\"height\", 70)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1)\n  .attr(\"rx\", 8)\n  .style(\"filter\", \"url(#stats-shadow)\");\nstatsBox.append(\"rect\").attr(\"width\", 4).attr(\"height\", 70).attr(\"fill\", t.palette[2]).attr(\"rx\", 2);\nstatsBox\n  .append(\"text\")\n  .attr(\"x\", 20)\n  .attr(\"y\", 28)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text(`R² = ${rSquared.toFixed(2)}  (r = ${r.toFixed(2)})`);\nstatsBox\n  .append(\"text\")\n  .attr(\"x\", 20)\n  .attr(\"y\", 52)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(`y = ${slope.toFixed(1)}x + ${intercept.toFixed(0)}`);\n\n// --- Legend -------------------------------------------------------------------\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${iw - 210}, ${ih - 90})`);\nlegend\n  .append(\"circle\")\n  .attr(\"cx\", 6)\n  .attr(\"cy\", 0)\n  .attr(\"r\", 7)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.65);\nlegend.append(\"text\").attr(\"x\", 20).attr(\"y\", 5).attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\").text(\"Observed data\");\nlegend\n  .append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", 14)\n  .attr(\"y1\", 26)\n  .attr(\"y2\", 26)\n  .attr(\"stroke\", t.palette[2])\n  .attr(\"stroke-width\", 4);\nlegend.append(\"text\").attr(\"x\", 20).attr(\"y\", 31).attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\").text(\"Regression fit\");\nlegend.append(\"rect\").attr(\"x\", 0).attr(\"y\", 46).attr(\"width\", 14).attr(\"height\", 10).attr(\"fill\", t.palette[2]).attr(\"opacity\", 0.18);\nlegend.append(\"text\").attr(\"x\", 20).attr(\"y\", 56).attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\").text(\"95% CI band\");\n\n// --- Title ----------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"scatter-regression-linear · javascript · d3 · anyplot.ai\");\n"}