{"spec_id":"scatter-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-basic: Basic Scatter Plot\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 94/100 | Updated: 2026-06-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 60, bottom: 100, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (deterministic LCG seed=42, marketing spend vs sales revenue) ----\nlet seed = 42;\nfunction lcgRand() {\n  seed = (1664525 * seed + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\nfunction lcgRandn() {\n  const u1 = lcgRand() + 1e-10;\n  const u2 = lcgRand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst data = Array.from({ length: 100 }, () => {\n  const spend = 10 + lcgRand() * 140;\n  const sales = Math.max(0.3, 0.8 + spend * 0.035 + lcgRandn() * 1.2);\n  return { x: spend, y: sales };\n});\n\n// --- OLS regression and Pearson r ------------------------------------------\nconst n = data.length;\nconst sumX = data.reduce((s, d) => s + d.x, 0);\nconst sumY = data.reduce((s, d) => s + d.y, 0);\nconst sumXY = data.reduce((s, d) => s + d.x * d.y, 0);\nconst sumX2 = data.reduce((s, d) => s + d.x * d.x, 0);\nconst sumY2 = data.reduce((s, d) => s + d.y * d.y, 0);\nconst slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);\nconst intercept = (sumY - slope * sumX) / n;\nconst r = (n * sumXY - sumX * sumY) /\n  Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));\n\n// --- 95% CI band statistics ------------------------------------------------\nconst xMean = sumX / n;\nconst sxx = data.reduce((s, d) => s + (d.x - xMean) ** 2, 0);\nconst sse = data.reduce((s, d) => {\n  const yhat = intercept + slope * d.x;\n  return s + (d.y - yhat) ** 2;\n}, 0);\nconst see = Math.sqrt(sse / (n - 2));\nconst tCrit = 1.984; // t(98, 0.975) for 95% CI\n\n// --- SVG mount -------------------------------------------------------------\nconst svg = d3.select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ----------------------------------------------------------------\nconst x = d3.scaleLinear()\n  .domain([0, d3.max(data, (d) => d.x) * 1.04]).nice().range([0, iw]);\nconst y = d3.scaleLinear()\n  .domain([0, d3.max(data, (d) => d.y) * 1.04]).nice().range([ih, 0]);\n\n// --- Grid lines (both axes, floating-grid — no domain line) ----------------\ng.append(\"g\").attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickSize(-ih).tickFormat(\"\"))\n  .call((ax) => ax.select(\".domain\").remove())\n  .call((ax) => ax.selectAll(\"line\").attr(\"stroke\", t.grid));\n\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((ax) => ax.select(\".domain\").remove())\n  .call((ax) => ax.selectAll(\"line\").attr(\"stroke\", t.grid));\n\n// --- 95% CI band using d3.area() (D3-native analytical layer) --------------\nconst xDom = x.domain();\nconst bandData = d3.range(0, 101).map((i) => {\n  const xv = xDom[0] + (xDom[1] - xDom[0]) * i / 100;\n  const yhat = intercept + slope * xv;\n  const ci = tCrit * see * Math.sqrt(1 / n + (xv - xMean) ** 2 / sxx);\n  return { x: xv, y0: Math.max(0, yhat - ci), y1: yhat + ci };\n});\n\ng.append(\"path\")\n  .datum(bandData)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.12)\n  .attr(\"stroke\", \"none\")\n  .attr(\"d\", d3.area()\n    .x((d) => x(d.x))\n    .y0((d) => y(d.y0))\n    .y1((d) => y(d.y1)));\n\n// --- OLS trend line --------------------------------------------------------\ng.append(\"path\")\n  .datum([\n    { x: xDom[0], y: intercept + slope * xDom[0] },\n    { x: xDom[1], y: intercept + slope * xDom[1] },\n  ])\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3)\n  .attr(\"stroke-opacity\", 0.8)\n  .attr(\"d\", d3.line().x((d) => x(d.x)).y((d) => y(d.y)));\n\n// --- Marginal rug marks (subtle distribution indicators on both axes) ------\ng.selectAll(\".rug-x\").data(data).join(\"line\")\n  .attr(\"class\", \"rug-x\")\n  .attr(\"x1\", (d) => x(d.x)).attr(\"x2\", (d) => x(d.x))\n  .attr(\"y1\", ih + 4).attr(\"y2\", ih + 12)\n  .attr(\"stroke\", t.palette[0]).attr(\"stroke-opacity\", 0.35).attr(\"stroke-width\", 1);\n\ng.selectAll(\".rug-y\").data(data).join(\"line\")\n  .attr(\"class\", \"rug-y\")\n  .attr(\"x1\", -4).attr(\"x2\", -12)\n  .attr(\"y1\", (d) => y(d.y)).attr(\"y2\", (d) => y(d.y))\n  .attr(\"stroke\", t.palette[0]).attr(\"stroke-opacity\", 0.35).attr(\"stroke-width\", 1);\n\n// --- Scatter markers -------------------------------------------------------\ng.selectAll(\"circle\").data(data).join(\"circle\")\n  .attr(\"cx\", (d) => x(d.x))\n  .attr(\"cy\", (d) => y(d.y))\n  .attr(\"r\", 7)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.7)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- X axis (floating — domain removed for clean open look) ----------------\ng.append(\"g\").attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickFormat((d) => `${d}K`))\n  .call((ax) => ax.select(\".domain\").remove())\n  .call((ax) => ax.selectAll(\".tick text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\"))\n  .call((ax) => ax.selectAll(\".tick line\").remove());\n\n// --- Y axis (floating — domain removed for clean open look) ----------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickFormat((d) => `$${d.toFixed(1)}M`))\n  .call((ax) => ax.select(\".domain\").remove())\n  .call((ax) => ax.selectAll(\".tick text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\"))\n  .call((ax) => ax.selectAll(\".tick line\").remove());\n\n// --- Correlation + CI annotation with elevated-bg callout box (upper-right)\nconst annotG = g.append(\"g\");\nannotG.append(\"rect\")\n  .attr(\"x\", iw - 205).attr(\"y\", 4)\n  .attr(\"width\", 201).attr(\"height\", 56)\n  .attr(\"rx\", 5).attr(\"ry\", 5)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-opacity\", 0.3)\n  .attr(\"stroke-width\", 1);\nannotG.append(\"text\")\n  .attr(\"x\", iw - 14).attr(\"y\", 28)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\").style(\"font-weight\", \"600\")\n  .text(`r ≈ ${r.toFixed(2)}`);\nannotG.append(\"text\")\n  .attr(\"x\", iw - 14).attr(\"y\", 50)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"95% confidence band\");\n\n// --- Axis labels -----------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2).attr(\"y\", height - 18)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"22px\").text(\"Marketing Spend ($K)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(33,${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"22px\").text(\"Sales Revenue ($M)\");\n\n// --- Title -----------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"28px\").style(\"font-weight\", \"600\")\n  .text(\"scatter-basic · javascript · d3 · anyplot.ai\");\n"}