{"spec_id":"residual-plot","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// residual-plot: Residual Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Simulated linear-regression diagnostics: fitted house-price predictions\n// (in $1000s) vs. residuals, with mild heteroscedasticity (variance grows\n// with fitted value) so the fan-out pattern is visible.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction gaussian() {\n  const u1 = 1 - lcg();\n  const u2 = lcg();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\nfunction hexToRgba(hex, alpha) {\n  const h = hex.replace(\"#\", \"\");\n  const r = parseInt(h.substring(0, 2), 16);\n  const g = parseInt(h.substring(2, 4), 16);\n  const b = parseInt(h.substring(4, 6), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst n = 220;\nconst fitted = [];\nconst residuals = [];\nfor (let i = 0; i < n; i++) {\n  const value = 150 + lcg() * 450; // fitted price, $150k-$600k\n  const noiseScale = 8 + (value - 150) * 0.05; // heteroscedastic spread\n  fitted.push(value);\n  residuals.push(gaussian() * noiseScale);\n}\n\nconst mean = residuals.reduce((a, b) => a + b, 0) / n;\nconst variance = residuals.reduce((a, b) => a + (b - mean) ** 2, 0) / n;\nconst stdDev = Math.sqrt(variance);\nconst threshold = 2 * stdDev;\n\nconst normalPoints = [];\nconst outlierPoints = [];\nfor (let i = 0; i < n; i++) {\n  const point = { x: fitted[i], y: residuals[i] };\n  if (Math.abs(residuals[i]) > threshold) {\n    outlierPoints.push(point);\n  } else {\n    normalPoints.push(point);\n  }\n}\n\nconst xMin = Math.min(...fitted);\nconst xMax = Math.max(...fitted);\n\n// Rolling-mean smoothing trend (sorted by fitted value) to surface any\n// residual non-linearity — optional per spec, adds diagnostic value.\nconst sortedIdx = fitted.map((_, i) => i).sort((a, b) => fitted[a] - fitted[b]);\nconst sortedX = sortedIdx.map((i) => fitted[i]);\nconst sortedY = sortedIdx.map((i) => residuals[i]);\nconst windowSize = Math.max(15, Math.round(n * 0.12));\nconst trendPoints = sortedX.map((x, i) => {\n  const lo = Math.max(0, i - Math.floor(windowSize / 2));\n  const hi = Math.min(n, i + Math.ceil(windowSize / 2));\n  const slice = sortedY.slice(lo, hi);\n  const avg = slice.reduce((a, b) => a + b, 0) / slice.length;\n  return { x, y: avg };\n});\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"±2σ band\",\n        data: [\n          { x: xMin, y: threshold },\n          { x: xMax, y: threshold },\n        ],\n        showLine: true,\n        borderColor: t.amber,\n        borderWidth: 1.5,\n        borderDash: [6, 4],\n        pointRadius: 0,\n        fill: \"+2\",\n        backgroundColor:\n          t.pageBg === \"#1A1A17\" ? \"rgba(240,239,232,0.06)\" : \"rgba(26,26,23,0.04)\",\n      },\n      {\n        label: \"Zero reference\",\n        data: [\n          { x: xMin, y: 0 },\n          { x: xMax, y: 0 },\n        ],\n        showLine: true,\n        borderColor: t.ink,\n        borderWidth: 2,\n        pointRadius: 0,\n      },\n      {\n        label: \"−2σ band\",\n        data: [\n          { x: xMin, y: -threshold },\n          { x: xMax, y: -threshold },\n        ],\n        showLine: true,\n        borderColor: t.amber,\n        borderWidth: 1.5,\n        borderDash: [6, 4],\n        pointRadius: 0,\n      },\n      {\n        label: \"Residuals\",\n        data: normalPoints,\n        backgroundColor: hexToRgba(t.palette[0], 0.7),\n        borderColor: t.pageBg,\n        borderWidth: 1,\n        pointRadius: 6,\n        pointHoverRadius: 7,\n      },\n      {\n        label: \"Smoothed trend\",\n        data: trendPoints,\n        showLine: true,\n        borderColor: t.palette[1],\n        borderWidth: 2,\n        borderDash: [3, 3],\n        pointRadius: 0,\n        fill: false,\n        tension: 0.3,\n      },\n      {\n        label: \"Outliers (>2σ)\",\n        data: outlierPoints,\n        backgroundColor: t.palette[4],\n        borderColor: t.pageBg,\n        borderWidth: 1,\n        pointRadius: 7,\n        pointStyle: \"triangle\",\n        pointHoverRadius: 8,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"residual-plot · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          filter: (item) => item.text !== \"±2σ band\" && item.text !== \"−2σ band\",\n        },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        title: { display: true, text: \"Fitted Value ($1,000s)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft },\n      },\n      y: {\n        title: { display: true, text: \"Residual ($1,000s)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        border: { color: t.inkSoft },\n      },\n    },\n  },\n});\n"}