{"spec_id":"scatter-regression-linear","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-regression-linear: Scatter Plot with Linear Regression\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 89/100 | Created: 2026-08-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic fixed-seed LCG) -------------------------\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nfunction randNormal(rand) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst rand = lcg(42);\nconst n = 70;\nconst trueSlope = 1.8;\nconst trueIntercept = 18;\nconst noiseSd = 14;\n\nconst adSpend = [];\nconst salesRevenue = [];\nfor (let i = 0; i < n; i++) {\n  const x = 5 + rand() * 45; // $5k-$50k monthly ad spend\n  const y = trueSlope * x + trueIntercept + randNormal(rand) * noiseSd;\n  adSpend.push(x);\n  salesRevenue.push(y);\n}\n\n// --- Least-squares fit + 95% confidence band --------------------------------\nconst xMean = adSpend.reduce((a, b) => a + b, 0) / n;\nconst yMean = salesRevenue.reduce((a, b) => a + b, 0) / n;\nlet sXY = 0;\nlet sXX = 0;\nfor (let i = 0; i < n; i++) {\n  sXY += (adSpend[i] - xMean) * (salesRevenue[i] - yMean);\n  sXX += (adSpend[i] - xMean) ** 2;\n}\nconst slope = sXY / sXX;\nconst intercept = yMean - slope * xMean;\n\nlet ssRes = 0;\nlet ssTot = 0;\nfor (let i = 0; i < n; i++) {\n  const yHat = slope * adSpend[i] + intercept;\n  ssRes += (salesRevenue[i] - yHat) ** 2;\n  ssTot += (salesRevenue[i] - yMean) ** 2;\n}\nconst rSquared = 1 - ssRes / ssTot;\nconst seEstimate = Math.sqrt(ssRes / (n - 2));\nconst tCritical = 1.995; // t(0.975, df=68) — 95% CI\n\nconst xMin = Math.min(...adSpend);\nconst xMax = Math.max(...adSpend);\nconst steps = 40;\nconst regressionPoints = [];\nconst ciUpperPoints = [];\nconst ciLowerPoints = [];\nfor (let i = 0; i <= steps; i++) {\n  const x0 = xMin + ((xMax - xMin) * i) / steps;\n  const yHat = slope * x0 + intercept;\n  const sePred = seEstimate * Math.sqrt(1 / n + (x0 - xMean) ** 2 / sXX);\n  const margin = tCritical * sePred;\n  regressionPoints.push({ x: x0, y: yHat });\n  ciUpperPoints.push({ x: x0, y: yHat + margin });\n  ciLowerPoints.push({ x: x0, y: yHat - margin });\n}\n\nconst scatterPoints = adSpend.map((x, i) => ({ x, y: salesRevenue[i] }));\n\n// --- Colors (Imprint palette) -----------------------------------------------\nfunction withAlpha(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\nconst pointColor = withAlpha(t.palette[0], 0.65);\nconst lineColor = t.palette[1];\nconst mutedInk = t.theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\"; // muted anchor (confidence-band fill)\nconst bandColor = withAlpha(mutedInk, 0.18);\n\n// --- Regression-equation callout (annotation per spec notes) ----------------\nconst equationText = `y = ${slope.toFixed(2)}x + ${intercept.toFixed(1)}`;\nconst calloutAnchorIdx = Math.round(steps * 0.62);\nconst calloutPlugin = {\n  id: \"regressionCallout\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const xScale = chart.scales.x;\n    const yScale = chart.scales.y;\n    const anchor = regressionPoints[calloutAnchorIdx];\n    const px = xScale.getPixelForValue(anchor.x);\n    const py = yScale.getPixelForValue(anchor.y);\n\n    const boxW = 200;\n    const boxH = 42;\n    let boxX = px + 26;\n    let boxY = py - boxH - 26;\n    if (boxX + boxW > chartArea.right) boxX = px - boxW - 26;\n    if (boxY < chartArea.top) boxY = py + 26;\n    const anchorX = boxX < px ? boxX + boxW : boxX;\n    const anchorY = boxY + boxH / 2 < py ? boxY + boxH : boxY;\n\n    ctx.save();\n    ctx.strokeStyle = lineColor;\n    ctx.lineWidth = 1.5;\n    ctx.beginPath();\n    ctx.moveTo(px, py);\n    ctx.lineTo(anchorX, anchorY);\n    ctx.stroke();\n\n    ctx.fillStyle = lineColor;\n    ctx.beginPath();\n    ctx.arc(px, py, 4, 0, Math.PI * 2);\n    ctx.fill();\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, 6);\n    ctx.fill();\n    ctx.stroke();\n\n    ctx.fillStyle = t.ink;\n    ctx.font = \"600 15px sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(equationText, boxX + boxW / 2, boxY + boxH / 2);\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  plugins: [calloutPlugin],\n  data: {\n    datasets: [\n      {\n        label: \"\",\n        type: \"line\",\n        data: ciUpperPoints,\n        borderWidth: 0,\n        pointRadius: 0,\n        fill: false,\n        tension: 0,\n      },\n      {\n        label: \"95% Confidence Interval\",\n        type: \"line\",\n        data: ciLowerPoints,\n        borderWidth: 0,\n        pointRadius: 0,\n        fill: \"-1\",\n        backgroundColor: bandColor,\n        tension: 0,\n      },\n      {\n        label: \"Linear regression\",\n        type: \"line\",\n        data: regressionPoints,\n        borderColor: lineColor,\n        borderWidth: 3.5,\n        pointRadius: 0,\n        fill: false,\n        tension: 0,\n      },\n      {\n        label: \"Observations\",\n        data: scatterPoints,\n        backgroundColor: pointColor,\n        borderColor: t.pageBg,\n        borderWidth: 1,\n        pointRadius: 7,\n        pointHoverRadius: 7,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"scatter-regression-linear · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 6 },\n      },\n      subtitle: {\n        display: true,\n        text: [`R² = ${rSquared.toFixed(3)}`],\n        color: t.inkSoft,\n        font: { size: 16 },\n        padding: { bottom: 16 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          filter: (item) => item.text !== \"\",\n        },\n      },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Monthly Ad Spend ($1,000s)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Monthly Sales Revenue ($1,000s)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}