{"spec_id":"scatter-regression-polynomial","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-regression-polynomial: Scatter Plot with Polynomial Regression\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 94/100 | Created: 2026-08-11\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME || \"light\";\nconst MUTED = THEME === \"light\" ? \"#6B6A63\" : \"#A8A79F\"; // Imprint muted anchor (theme-adaptive)\n\n// Tiny deterministic LCG + Box-Muller — the browser has no seeded RNG\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction gaussian() {\n  const u1 = Math.max(lcg(), 1e-9);\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Data: advertising spend vs. revenue lift — an inverted-U (\"diminishing\n// returns then over-saturation\") shape that only a degree-2 fit captures.\nconst N_POINTS = 90;\nconst points = [];\nfor (let i = 0; i < N_POINTS; i++) {\n  const spend = 2 + 96 * (i / (N_POINTS - 1)) + (lcg() - 0.5) * 3;\n  const trueLift = -0.045 * spend * spend + 5.2 * spend + 15;\n  const lift = trueLift + gaussian() * 14;\n  points.push({ x: spend, y: lift });\n}\n\n// Least-squares polynomial fit (degree 2) via normal equations\nfunction polyfit(pts, degree) {\n  const cols = degree + 1;\n  const XtX = Array.from({ length: cols }, () => new Array(cols).fill(0));\n  const Xty = new Array(cols).fill(0);\n  for (const { x, y } of pts) {\n    const powers = new Array(cols);\n    let p = 1;\n    for (let k = 0; k < cols; k++) {\n      powers[k] = p;\n      p *= x;\n    }\n    for (let i = 0; i < cols; i++) {\n      Xty[i] += powers[i] * y;\n      for (let j = 0; j < cols; j++) XtX[i][j] += powers[i] * powers[j];\n    }\n  }\n  // Gaussian elimination with partial pivoting\n  const M = XtX.map((row, i) => [...row, Xty[i]]);\n  for (let col = 0; col < cols; col++) {\n    let pivotRow = col;\n    for (let r = col + 1; r < cols; r++) {\n      if (Math.abs(M[r][col]) > Math.abs(M[pivotRow][col])) pivotRow = r;\n    }\n    [M[col], M[pivotRow]] = [M[pivotRow], M[col]];\n    const pivot = M[col][col];\n    for (let c = col; c <= cols; c++) M[col][c] /= pivot;\n    for (let r = 0; r < cols; r++) {\n      if (r === col) continue;\n      const factor = M[r][col];\n      for (let c = col; c <= cols; c++) M[r][c] -= factor * M[col][c];\n    }\n  }\n  return M.map((row) => row[cols]);\n}\n\nconst [c0, c1, c2] = polyfit(points, 2);\nconst predict = (x) => c0 + c1 * x + c2 * x * x;\n\n// Goodness of fit + residual spread (for the prediction band)\nconst yMean = points.reduce((s, p) => s + p.y, 0) / points.length;\nlet ssRes = 0;\nlet ssTot = 0;\nfor (const { x, y } of points) {\n  ssRes += (y - predict(x)) ** 2;\n  ssTot += (y - yMean) ** 2;\n}\nconst r2 = 1 - ssRes / ssTot;\nconst residualStd = Math.sqrt(ssRes / (points.length - 3));\n\n// Fitted curve + ±1.96σ prediction band, sampled on a fine grid\nconst xMin = Math.min(...points.map((p) => p.x));\nconst xMax = Math.max(...points.map((p) => p.x));\nconst CURVE_STEPS = 60;\nconst curve = [];\nconst bandUpper = [];\nconst bandLower = [];\nfor (let i = 0; i <= CURVE_STEPS; i++) {\n  const x = xMin + ((xMax - xMin) * i) / CURVE_STEPS;\n  const yHat = predict(x);\n  curve.push({ x, y: yHat });\n  bandUpper.push({ x, y: yHat + 1.96 * residualStd });\n  bandLower.push({ x, y: yHat - 1.96 * residualStd });\n}\n\nfunction hexToRgba(hex, alpha) {\n  const clean = hex.replace(\"#\", \"\");\n  const r = parseInt(clean.substring(0, 2), 16);\n  const g = parseInt(clean.substring(2, 4), 16);\n  const b = parseInt(clean.substring(4, 6), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst signedTerm = (value, symbol) =>\n  `${value >= 0 ? \"+\" : \"−\"} ${Math.abs(value).toFixed(4)}${symbol}`;\nconst equation = `y = ${c2.toFixed(4)}x² ${signedTerm(c1, \"x\")} ${signedTerm(c0, \"\")}`;\nconst BAND_SWATCH = hexToRgba(MUTED, 0.55); // opaque enough to read in the legend chip (fill itself stays soft at 0.18)\n\n// Mount\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Inline plugin: equation + R² badge, drawn inside the plot area\nconst fitBadge = {\n  id: \"fitBadge\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const text1 = equation;\n    const text2 = `R² = ${r2.toFixed(3)}`;\n\n    ctx.save();\n    ctx.font = \"600 20px sans-serif\";\n    const w1 = ctx.measureText(text1).width;\n    ctx.font = \"600 22px sans-serif\";\n    const w2 = ctx.measureText(text2).width;\n    const boxW = Math.max(w1, w2) + 40;\n    const boxH = 84;\n    const boxX = chartArea.right - boxW - 24;\n    const boxY = chartArea.top + 24;\n\n    ctx.fillStyle = t.elevatedBg;\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 1;\n    ctx.beginPath();\n    ctx.roundRect(boxX, boxY, boxW, boxH, 10);\n    ctx.fill();\n    ctx.stroke();\n\n    ctx.textBaseline = \"middle\";\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"600 20px sans-serif\";\n    ctx.fillText(text1, boxX + 20, boxY + 30);\n    ctx.fillStyle = t.ink;\n    ctx.font = \"600 22px sans-serif\";\n    ctx.fillText(text2, boxX + 20, boxY + 62);\n    ctx.restore();\n  },\n};\n\n// Chart — scatter points + prediction band (behind) + fitted curve (on top)\nnew Chart(canvas, {\n  type: \"scatter\",\n  plugins: [fitBadge],\n  data: {\n    datasets: [\n      {\n        label: \"_bandUpper\",\n        type: \"line\",\n        data: bandUpper,\n        borderWidth: 0,\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        label: \"95% prediction band\",\n        type: \"line\",\n        data: bandLower,\n        borderWidth: 0,\n        pointRadius: 0,\n        backgroundColor: hexToRgba(MUTED, 0.18),\n        fill: \"-1\",\n      },\n      {\n        label: \"Ad campaigns (spend vs. revenue lift)\",\n        data: points,\n        backgroundColor: hexToRgba(t.palette[0], 0.65),\n        borderColor: t.pageBg,\n        borderWidth: 1,\n        pointRadius: 7,\n        pointHoverRadius: 7,\n      },\n      {\n        label: \"Quadratic fit (degree 2)\",\n        type: \"line\",\n        data: curve,\n        borderColor: t.palette[1],\n        backgroundColor: \"transparent\",\n        borderWidth: 3.5,\n        pointRadius: 0,\n        tension: 0.2,\n        fill: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 8, right: 16 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"scatter-regression-polynomial · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 26 },\n        padding: { top: 10, bottom: 20 },\n      },\n      // Legend lives below the plot (not stacked under the title) so the top\n      // region stays open for just the title + equation badge.\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          pointStyle: \"circle\",\n          boxWidth: 10,\n          padding: 24,\n          filter: (item) => !item.text.startsWith(\"_\"),\n          generateLabels(chart) {\n            const items = Chart.defaults.plugins.legend.labels.generateLabels(chart);\n            for (const item of items) {\n              if (item.text === \"95% prediction band\") {\n                item.fillStyle = BAND_SWATCH;\n                item.strokeStyle = BAND_SWATCH;\n              }\n            }\n            return items;\n          },\n        },\n      },\n      tooltip: {\n        callbacks: {\n          label(ctx) {\n            if (ctx.dataset.label !== \"Ad campaigns (spend vs. revenue lift)\") {\n              return ctx.dataset.label;\n            }\n            const { x, y } = ctx.raw;\n            const residual = y - predict(x);\n            const sign = residual >= 0 ? \"+\" : \"−\";\n            return `spend $${x.toFixed(1)}k → lift $${y.toFixed(1)}k (${sign}${Math.abs(residual).toFixed(1)} vs. fit)`;\n          },\n        },\n      },\n    },\n    scales: {\n      x: {\n        title: {\n          display: true,\n          text: \"Advertising Spend ($ thousands)\",\n          color: t.ink,\n          font: { size: 18 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n      y: {\n        title: {\n          display: true,\n          text: \"Revenue Lift ($ thousands)\",\n          color: t.ink,\n          font: { size: 18 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}