{"spec_id":"diagnostic-regression-panel","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// diagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: fit a simple linear model that omits real curvature + heteroscedasticity,\n//     so the diagnostics below have something genuine to reveal (fixed-seed LCG,\n//     Box-Muller normal — no seeded RNG in the browser) -----------------------------\nfunction lcg(seed) {\n  let s = seed;\n  return () => {\n    s = (s * 1664525 + 1013904223) % 4294967296;\n    return s / 4294967296;\n  };\n}\nconst rand = lcg(20260905);\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst n = 90;\nconst fertilizerKgHa = [];\nconst yieldTonnesHa = [];\nfor (let i = 0; i < n; i++) {\n  const x = 10 + 190 * rand();\n  const noiseSd = 0.4 + 0.012 * x;\n  const trueYield = 2.5 + 0.055 * x - 0.00016 * x * x; // diminishing returns\n  fertilizerKgHa.push(x);\n  yieldTonnesHa.push(trueYield + randNormal() * noiseSd);\n}\n\n// --- Linear regression yield ~ fertilizer, plus diagnostic quantities --------------\nconst meanX = fertilizerKgHa.reduce((a, b) => a + b, 0) / n;\nconst meanY = yieldTonnesHa.reduce((a, b) => a + b, 0) / n;\nlet sxy = 0;\nlet sxx = 0;\nfor (let i = 0; i < n; i++) {\n  sxy += (fertilizerKgHa[i] - meanX) * (yieldTonnesHa[i] - meanY);\n  sxx += (fertilizerKgHa[i] - meanX) ** 2;\n}\nconst slope = sxy / sxx;\nconst intercept = meanY - slope * meanX;\nconst p = 2; // fitted parameters: intercept + slope\n\nconst fitted = fertilizerKgHa.map((x) => intercept + slope * x);\nconst residuals = yieldTonnesHa.map((y, i) => y - fitted[i]);\nconst leverage = fertilizerKgHa.map((x) => 1 / n + (x - meanX) ** 2 / sxx);\nconst sse = residuals.reduce((sum, r) => sum + r * r, 0);\nconst s = Math.sqrt(sse / (n - p));\nconst stdResiduals = residuals.map((r, i) => r / (s * Math.sqrt(1 - leverage[i])));\nconst cooksD = stdResiduals.map((e, i) => (e * e * leverage[i]) / (p * (1 - leverage[i])));\nconst sqrtAbsStdResiduals = stdResiduals.map((e) => Math.sqrt(Math.abs(e)));\n\nconst influential = new Set(\n  [...cooksD.keys()].sort((a, b) => cooksD[b] - cooksD[a]).slice(0, 3)\n);\n\n// Normal Q-Q: theoretical quantiles aligned back to each observation's own index,\n// so the same 3 influential observations can be labeled consistently in every panel.\nconst rankOf = [...stdResiduals.keys()].sort((a, b) => stdResiduals[a] - stdResiduals[b]);\nconst theoreticalQ = new Array(n);\nrankOf.forEach((obsIdx, rank) => {\n  theoreticalQ[obsIdx] = normInv((rank + 0.5) / n);\n});\nfunction normInv(prob) {\n  // Acklam's rational approximation of the inverse standard normal CDF.\n  const a = [-3.969683028665376e1, 2.209460984245205e2, -2.759285104469687e2, 1.383577518672690e2, -3.066479806614716e1, 2.506628277459239e0];\n  const b = [-5.447609879822406e1, 1.615858368580409e2, -1.556989798598866e2, 6.680131188771972e1, -1.328068155288572e1];\n  const c = [-7.784894002430293e-3, -3.223964580411365e-1, -2.400758277161838e0, -2.549732539343734e0, 4.374664141464968e0, 2.938163982698783e0];\n  const d = [7.784695709041462e-3, 3.224671290700398e-1, 2.445134137142996e0, 3.754408661907416e0];\n  const pLow = 0.02425;\n  if (prob < pLow) {\n    const q = Math.sqrt(-2 * Math.log(prob));\n    return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /\n      ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);\n  }\n  if (prob <= 1 - pLow) {\n    const q = prob - 0.5;\n    const r = q * q;\n    return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /\n      (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);\n  }\n  const q = Math.sqrt(-2 * Math.log(1 - prob));\n  return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /\n    ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);\n}\n\n// Cleveland LOWESS (degree-1, tricube weights, no robustness iterations) -----------\nfunction lowess(xs, ys, frac) {\n  const bandwidth = Math.max(3, Math.round(frac * xs.length));\n  return xs.map((x0) => {\n    const dist = xs.map((x) => Math.abs(x - x0));\n    const h = [...dist].sort((a, b) => a - b)[bandwidth - 1] || 1;\n    let sw = 0;\n    let swx = 0;\n    let swy = 0;\n    let swxx = 0;\n    let swxy = 0;\n    for (let i = 0; i < xs.length; i++) {\n      const u = Math.min(dist[i] / h, 1);\n      const w = (1 - u ** 3) ** 3;\n      sw += w;\n      swx += w * xs[i];\n      swy += w * ys[i];\n      swxx += w * xs[i] * xs[i];\n      swxy += w * xs[i] * ys[i];\n    }\n    const denom = sw * swxx - swx * swx;\n    const localSlope = denom !== 0 ? (sw * swxy - swx * swy) / denom : 0;\n    const localIntercept = (swy - localSlope * swx) / sw;\n    return localIntercept + localSlope * x0;\n  });\n}\nfunction smoothCurve(xs, ys) {\n  const smoothed = lowess(xs, ys, 0.6);\n  return xs\n    .map((x, i) => [x, smoothed[i]])\n    .sort((a, b) => a[0] - b[0]);\n}\n\nfunction cookContour(cooksLevel, hMax) {\n  const steps = 40;\n  const hMin = 0.006;\n  const pos = [];\n  for (let k = 0; k <= steps; k++) {\n    const h = hMin + ((hMax - hMin) * k) / steps;\n    const e = Math.sqrt((cooksLevel * p * (1 - h)) / h);\n    pos.push([h, e]);\n  }\n  const neg = pos.map(([h, e]) => [h, -e]);\n  return { pos, neg };\n}\n\n// --- Shared point styling across all four subplots --------------------------------\nconst MARKER_RADIUS = 4.5;\nfunction scatterPoints(xs, ys) {\n  return xs.map((x, i) => {\n    const point = { x, y: ys[i] };\n    if (influential.has(i)) {\n      point.marker = { symbol: \"diamond\", radius: MARKER_RADIUS + 1.5, fillColor: t.palette[4], lineColor: t.pageBg, lineWidth: 1 };\n      point.dataLabels = {\n        enabled: true,\n        format: `#${i}`,\n        y: -12,\n        style: { color: t.ink, fontSize: \"12px\", fontWeight: \"600\", textOutline: \"none\" },\n      };\n    }\n    return point;\n  });\n}\nconst BASE_MARKER = { symbol: \"diamond\", radius: MARKER_RADIUS, fillColor: t.palette[0], lineColor: t.pageBg, lineWidth: 1 };\n\n// --- Shared chart chrome ------------------------------------------------------------\nfunction baseOptions(panelTitle, xTitle, yTitle) {\n  return {\n    chart: { type: \"scatter\", backgroundColor: \"transparent\", animation: false,\n             spacing: [10, 14, 10, 10], style: { fontFamily: \"inherit\" } },\n    credits: { enabled: false },\n    title: { text: panelTitle, style: { color: t.ink, fontSize: \"16px\", fontWeight: \"600\" }, margin: 12 },\n    xAxis: { title: { text: xTitle, style: { color: t.inkSoft, fontSize: \"13px\" } },\n             lineColor: t.inkSoft, tickColor: t.inkSoft, gridLineColor: t.grid, gridLineWidth: 1,\n             labels: { style: { color: t.inkSoft, fontSize: \"12px\" } } },\n    yAxis: { title: { text: yTitle, style: { color: t.inkSoft, fontSize: \"13px\" } },\n             gridLineColor: t.grid, lineColor: t.inkSoft, tickColor: t.inkSoft, tickWidth: 1,\n             labels: { style: { color: t.inkSoft, fontSize: \"12px\" } } },\n    legend: { enabled: false },\n    plotOptions: { series: { animation: false } },\n    tooltip: {\n      enabled: true,\n      formatter: function () {\n        return `${xTitle}: ${this.x.toFixed(2)}<br/>${yTitle}: ${this.y.toFixed(2)}`;\n      },\n    },\n  };\n}\n\n// Panel 1 — Residuals vs Fitted: reveals non-linearity + heteroscedasticity ---------\nconst panel1 = baseOptions(\"Residuals vs Fitted\", \"Fitted values\", \"Residuals\");\npanel1.yAxis.plotLines = [{ value: 0, color: t.inkSoft, dashStyle: \"Dash\", width: 1.5, zIndex: 2 }];\npanel1.series = [\n  { name: \"LOWESS\", type: \"spline\", data: smoothCurve(fitted, residuals),\n    color: t.palette[2], lineWidth: 2.5, marker: { enabled: false }, enableMouseTracking: false },\n  { name: \"Residuals\", data: scatterPoints(fitted, residuals), marker: BASE_MARKER },\n];\n\n// Panel 2 — Normal Q-Q: standardized residuals vs theoretical normal quantiles -----\nconst panel2 = baseOptions(\"Normal Q-Q\", \"Theoretical Quantiles\", \"Standardized Residuals\");\nconst qMin = Math.min(...theoreticalQ, ...stdResiduals);\nconst qMax = Math.max(...theoreticalQ, ...stdResiduals);\npanel2.series = [\n  { name: \"45° reference\", type: \"line\", data: [[qMin, qMin], [qMax, qMax]],\n    color: t.inkSoft, dashStyle: \"Dash\", lineWidth: 1.5, marker: { enabled: false }, enableMouseTracking: false },\n  { name: \"Std. Residuals\", data: scatterPoints(theoreticalQ, stdResiduals), marker: BASE_MARKER },\n];\n\n// Panel 3 — Scale-Location: spread of residuals across the fitted range -------------\nconst panel3 = baseOptions(\"Scale-Location\", \"Fitted values\", \"√|Standardized Residuals|\");\npanel3.series = [\n  { name: \"LOWESS\", type: \"spline\", data: smoothCurve(fitted, sqrtAbsStdResiduals),\n    color: t.palette[2], lineWidth: 2.5, marker: { enabled: false }, enableMouseTracking: false },\n  { name: \"√|Std. Resid.|\", data: scatterPoints(fitted, sqrtAbsStdResiduals), marker: BASE_MARKER },\n];\n\n// Panel 4 — Residuals vs Leverage: Cook's distance contours flag influence ----------\nconst panel4 = baseOptions(\"Residuals vs Leverage\", \"Leverage\", \"Standardized Residuals\");\nconst hMax = Math.min(0.96, Math.max(...leverage) * 1.35);\nconst cook05 = cookContour(0.5, hMax);\nconst cook10 = cookContour(1.0, hMax);\n// Label text always renders in the high-contrast ink color (never the line's own\n// accent hue — amber-on-cream fails legibility) with a page-bg halo, and each\n// label gets a forced y-offset so the two contour labels never crowd each other\n// even where the D=0.5 and D=1.0 curves converge near the right edge.\nfunction labelLast(curve, text, yOffset) {\n  const points = curve.map(([h, e]) => [h, e]);\n  const last = points.length - 1;\n  points[last] = {\n    x: points[last][0],\n    y: points[last][1],\n    dataLabels: { enabled: true, format: text, align: \"left\", x: 6, y: yOffset,\n                  style: { color: t.ink, fontSize: \"12px\", fontWeight: \"700\" },\n                  textOutline: `3px ${t.pageBg}` },\n  };\n  return points;\n}\npanel4.yAxis.plotLines = [{ value: 0, color: t.inkSoft, dashStyle: \"Dash\", width: 1.5, zIndex: 2 }];\npanel4.series = [\n  { name: \"Cook's D = 0.5\", type: \"line\", data: labelLast(cook05.pos, \"0.5\", 14),\n    color: t.inkSoft, dashStyle: \"ShortDash\", lineWidth: 1.5, marker: { enabled: false }, enableMouseTracking: false },\n  { name: \"Cook's D = 0.5 (neg)\", type: \"line\", data: cook05.neg,\n    color: t.inkSoft, dashStyle: \"ShortDash\", lineWidth: 1.5, marker: { enabled: false }, enableMouseTracking: false },\n  { name: \"Cook's D = 1.0\", type: \"line\", data: labelLast(cook10.pos, \"1.0\", -12),\n    color: t.amber, dashStyle: \"ShortDash\", lineWidth: 1.5, marker: { enabled: false }, enableMouseTracking: false },\n  { name: \"Cook's D = 1.0 (neg)\", type: \"line\", data: cook10.neg,\n    color: t.amber, dashStyle: \"ShortDash\", lineWidth: 1.5, marker: { enabled: false }, enableMouseTracking: false },\n  { name: \"Residuals\", data: scatterPoints(leverage, stdResiduals), marker: BASE_MARKER },\n];\n\n// --- Layout: shared header + 2x2 grid of independently-mounted Highcharts panels ---\nconst root = document.getElementById(\"container\");\n\nconst header = document.createElement(\"div\");\nheader.style.cssText = `padding:18px 24px 4px; font-size:22px; font-weight:600; color:${t.ink}; font-family:inherit;`;\nheader.textContent = \"diagnostic-regression-panel · javascript · highcharts · anyplot.ai\";\nroot.appendChild(header);\n\nconst grid = document.createElement(\"div\");\ngrid.style.cssText =\n  \"display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; \" +\n  \"gap:16px; margin:4px 20px 20px; height:calc(100% - 62px);\";\nroot.appendChild(grid);\n\nconst panelIds = [\"panel-resid-fitted\", \"panel-qq\", \"panel-scale-location\", \"panel-resid-leverage\"];\npanelIds.forEach((id) => {\n  const cell = document.createElement(\"div\");\n  cell.id = id;\n  grid.appendChild(cell);\n});\n\nHighcharts.chart(\"panel-resid-fitted\", panel1);\nHighcharts.chart(\"panel-qq\", panel2);\nHighcharts.chart(\"panel-scale-location\", panel3);\nHighcharts.chart(\"panel-resid-leverage\", panel4);\n"}