{"spec_id":"diagnostic-regression-panel","library":"muix","language":"javascript","code":"// anyplot.ai\n// diagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// diagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-05\nimport * as React from \"react\";\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { ChartsText } from \"@mui/x-charts/ChartsText\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"system-ui, -apple-system, 'Segoe UI', sans-serif\";\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal --------------------------\nlet seed = 20260905;\nfunction nextUniform() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction nextNormal() {\n  const u1 = Math.max(nextUniform(), 1e-9);\n  const u2 = nextUniform();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Data: dose-response study (drug dose vs biomarker response) -----------\nconst N = 56;\nconst dose = [];\nfor (let i = 0; i < N; i++) {\n  dose.push(4 + 92 * (i / (N - 1)) + (nextUniform() - 0.5) * 3);\n}\ndose.sort((a, b) => a - b);\n\nconst response = dose.map((d) => {\n  const trend = 18 + 2.9 * d - 0.012 * d * d; // mild curvature\n  const noiseScale = 2.5 + 0.11 * d; // heteroscedastic — variance grows with dose\n  return trend + nextNormal() * noiseScale;\n});\n\n// Two deliberately influential observations, so the panel has something to diagnose\nresponse[10] += 34; // large residual, moderate leverage\ndose[N - 2] = 118;\nresponse[N - 2] -= 46; // high leverage + large residual\n\n// --- Ordinary least squares (simple linear regression) ---------------------\nconst n = dose.length;\nconst p = 2; // parameters: intercept + slope\nconst doseMean = dose.reduce((a, b) => a + b, 0) / n;\nconst responseMean = response.reduce((a, b) => a + b, 0) / n;\nconst sxx = dose.reduce((acc, d) => acc + (d - doseMean) ** 2, 0);\nconst sxy = dose.reduce((acc, d, i) => acc + (d - doseMean) * (response[i] - responseMean), 0);\nconst slope = sxy / sxx;\nconst intercept = responseMean - slope * doseMean;\n\nconst fitted = dose.map((d) => intercept + slope * d);\nconst residuals = response.map((y, i) => y - fitted[i]);\nconst rss = residuals.reduce((acc, r) => acc + r * r, 0);\nconst sigma = Math.sqrt(rss / (n - p));\nconst leverage = dose.map((d) => 1 / n + (d - doseMean) ** 2 / sxx);\nconst stdResiduals = residuals.map((r, i) => r / (sigma * Math.sqrt(1 - leverage[i])));\nconst scaleLocation = stdResiduals.map((r) => Math.sqrt(Math.abs(r)));\nconst cooksD = stdResiduals.map((r, i) => (r * r * leverage[i]) / (p * (1 - leverage[i])));\n\n// Three most influential observations (highest Cook's distance), labeled by 1-based index\nconst influentialIdx = cooksD\n  .map((d, i) => [d, i])\n  .sort((a, b) => b[0] - a[0])\n  .slice(0, 3)\n  .map(([, i]) => i);\n\n// --- Normal Q-Q: theoretical quantiles vs sorted standardized residuals ----\nfunction inverseNormalCdf(pr) {\n  // Acklam's rational approximation of the standard normal quantile function\n  const a = [-3.969683028665376e1, 2.209460984245205e2, -2.759285104469687e2, 1.38357751867269e2, -3.066479806614716e1, 2.506628277459239];\n  const b = [-5.447609879822406e1, 1.615858368580409e2, -1.556989798598866e2, 6.680131188771972e1, -1.328068155288572e1];\n  const c = [-7.784894002430293e-3, -3.223964580411365e-1, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783];\n  const d = [7.784695709041462e-3, 3.224671290700398e-1, 2.445134137142996, 3.754408661907416];\n  const low = 0.02425;\n  if (pr < low) {\n    const q = Math.sqrt(-2 * Math.log(pr));\n    return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);\n  }\n  if (pr > 1 - low) {\n    const q = Math.sqrt(-2 * Math.log(1 - pr));\n    return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);\n  }\n  const q = pr - 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 / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);\n}\n\nconst qqRanked = stdResiduals.map((v, i) => [v, i]).sort((a, b) => a[0] - b[0]);\nconst theoreticalQuantiles = new Array(n);\nconst sortedStdResiduals = new Array(n);\nconst qqRankByOrigIdx = new Array(n);\nqqRanked.forEach(([v, origIdx], rank) => {\n  theoreticalQuantiles[rank] = inverseNormalCdf((rank + 0.5) / n);\n  sortedStdResiduals[rank] = v;\n  qqRankByOrigIdx[origIdx] = rank;\n});\n\n// --- LOWESS (locally weighted linear regression, tricube kernel) -----------\nfunction linspace(min, max, count) {\n  if (count <= 1) return [min];\n  const step = (max - min) / (count - 1);\n  return Array.from({ length: count }, (_, i) => min + step * i);\n}\n\nfunction lowess(xs, ys, xGrid, bandwidthFraction) {\n  const m = xs.length;\n  const k = Math.max(3, Math.round(bandwidthFraction * m));\n  return xGrid.map((x0) => {\n    const dists = xs.map((xi) => Math.abs(xi - x0));\n    const bandwidth = [...dists].sort((a, b) => a - b)[Math.min(k - 1, m - 1)] || 1e-6;\n    let sw = 0, swx = 0, swy = 0, swxx = 0, swxy = 0;\n    for (let i = 0; i < m; i++) {\n      const u = dists[i] / bandwidth;\n      if (u >= 1) continue;\n      const w = (1 - u ** 3) ** 3; // tricube weight\n      sw += w; swx += w * xs[i]; swy += w * ys[i];\n      swxx += w * xs[i] * xs[i]; swxy += w * xs[i] * ys[i];\n    }\n    const denom = sw * swxx - swx * swx;\n    if (Math.abs(denom) < 1e-9) return swy / sw;\n    const b1 = (sw * swxy - swx * swy) / denom;\n    const b0 = (swy - b1 * swx) / sw;\n    return b0 + b1 * x0;\n  });\n}\n\nconst fittedGrid = linspace(Math.min(...fitted), Math.max(...fitted), 36);\nconst residLowess = lowess(fitted, residuals, fittedGrid, 0.5);\nconst scaleLocLowess = lowess(fitted, scaleLocation, fittedGrid, 0.5);\n\n// --- Q-Q reference line: y = x across the combined data range --------------\nconst qqMin = Math.min(...theoreticalQuantiles, ...sortedStdResiduals);\nconst qqMax = Math.max(...theoreticalQuantiles, ...sortedStdResiduals);\nconst qqDiagonalGrid = [qqMin, qqMax];\n\n// --- Cook's distance contours (Residuals vs Leverage) -----------------------\nconst leverageMax = Math.max(...leverage);\nconst leverageGrid = linspace(Math.max(0.004, Math.min(...leverage) * 0.3), leverageMax * 1.15, 44);\nconst cookYCap = Math.max(3, Math.max(...stdResiduals.map(Math.abs)) * 1.35);\nfunction cookBranch(cooksLevel) {\n  return leverageGrid.map((h) => {\n    const v = Math.sqrt((cooksLevel * p * (1 - h)) / h);\n    return v > cookYCap ? null : v;\n  });\n}\nconst cookHalfPos = cookBranch(0.5);\nconst cookHalfNeg = cookHalfPos.map((v) => (v === null ? null : -v));\nconst cookOnePos = cookBranch(1.0);\nconst cookOneNeg = cookOnePos.map((v) => (v === null ? null : -v));\n\n// --- Shared styling ----------------------------------------------------------\nconst POINT_COLOR = t.palette[0]; // brand green — same marker color in all four subplots\nconst SMOOTH_COLOR = t.palette[2]; // blue trend line\nconst CONTOUR_COLOR = t.amber; // warning/threshold semantic color for Cook's distance\nconst REF_LINE_STYLE = { stroke: t.ink, strokeWidth: 1.5, strokeDasharray: \"6 5\", opacity: 0.55 };\nconst CONTOUR_LINE_STYLE = { strokeDasharray: \"5 4\" };\nconst MARKER_SIZE = 6.5;\nconst AXIS_LABEL_STYLE = { fontSize: 13 };\nconst TICK_LABEL_STYLE = { fontSize: 11 };\n\nfunction scatterSeries(id, xs, ys) {\n  return {\n    id,\n    type: \"scatter\",\n    color: POINT_COLOR,\n    markerSize: MARKER_SIZE,\n    data: xs.map((x, i) => ({ x, y: ys[i], id: i })),\n  };\n}\n\n// Labels the top-3 most-influential observations inside a chart's SVG space.\n// Must run as a child of ChartContainer to read the live x/y scales.\n// Two labels whose marker centers are closer than this (Euclidean, px) are\n// considered crowded and get staggered vertically so their index numbers\n// don't visually merge — covers both stacked and side-by-side markers.\nconst LABEL_CROWD_PX = 20;\nconst LABEL_STAGGER_PX = 13;\n\nfunction InfluentialLabels({ points }) {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const positioned = points\n    .map(({ x, y, text }) => {\n      const px = xScale(x);\n      const py = yScale(y);\n      if (px == null || py == null || Number.isNaN(px) || Number.isNaN(py)) return null;\n      return { px, py, text };\n    })\n    .filter(Boolean);\n\n  // Stagger along the direction away from the colliding neighbor, so a point\n  // below its neighbor moves further down (not toward it).\n  positioned.forEach((point, i) => {\n    let stackOffset = 0;\n    let sign = 1;\n    for (let j = 0; j < i; j++) {\n      const other = positioned[j];\n      const dist = Math.hypot(point.px - other.px, point.py - other.py);\n      if (dist < LABEL_CROWD_PX) {\n        stackOffset += 1;\n        sign = point.py >= other.py ? -1 : 1;\n      }\n    }\n    point.dy = sign * stackOffset * LABEL_STAGGER_PX;\n  });\n\n  return (\n    <React.Fragment>\n      {positioned.map(({ px, py, text, dy }) => (\n        <ChartsText\n          key={text}\n          x={px + 8}\n          y={py - 8 - dy}\n          text={text}\n          fill={t.inkSoft}\n          style={{ fontSize: 12, fontFamily: FONT, textAnchor: \"start\" }}\n        />\n      ))}\n    </React.Fragment>\n  );\n}\n\nfunction SubplotTitle({ children, height }) {\n  return (\n    <div style={{ height, display: \"flex\", alignItems: \"flex-end\", paddingBottom: 4 }}>\n      <span style={{ fontSize: 15, fontWeight: 500, color: t.inkSoft, fontFamily: FONT }}>{children}</span>\n    </div>\n  );\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const titleH = 60;\n  const subTitleH = 30;\n  const gap = 18;\n  const rowH = (H - titleH - gap) / 2;\n  const colW = (W - gap) / 2;\n  const chartH = rowH - subTitleH;\n  const chartW = colW;\n\n  // Subplot 1: Residuals vs Fitted\n  const p1Points = influentialIdx.map((i) => ({ x: fitted[i], y: residuals[i], text: String(i + 1) }));\n\n  // Subplot 2: Normal Q-Q\n  const p2Points = influentialIdx.map((i) => {\n    const rank = qqRankByOrigIdx[i];\n    return { x: theoreticalQuantiles[rank], y: sortedStdResiduals[rank], text: String(i + 1) };\n  });\n\n  // Subplot 3: Scale-Location\n  const p3Points = influentialIdx.map((i) => ({ x: fitted[i], y: scaleLocation[i], text: String(i + 1) }));\n\n  // Subplot 4: Residuals vs Leverage\n  const p4Points = influentialIdx.map((i) => ({ x: leverage[i], y: stdResiduals[i], text: String(i + 1) }));\n\n  return (\n    <div style={{ width: W, height: H, display: \"flex\", flexDirection: \"column\", fontFamily: FONT }}>\n      <div style={{ height: titleH, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <span style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          diagnostic-regression-panel · javascript · muix · anyplot.ai\n        </span>\n      </div>\n\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: `${colW}px ${colW}px`,\n          gridTemplateRows: `${rowH}px ${rowH}px`,\n          columnGap: gap,\n          rowGap: gap,\n        }}\n      >\n        {/* Panel 1: Residuals vs Fitted */}\n        <div style={{ width: chartW, height: rowH }}>\n          <SubplotTitle height={subTitleH}>Residuals vs Fitted</SubplotTitle>\n          <ChartContainer\n            width={chartW}\n            height={chartH}\n            skipAnimation\n            xAxis={[{ id: \"p1x\", scaleType: \"linear\", data: fittedGrid, label: \"Fitted Values\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            yAxis={[{ id: \"p1y\", scaleType: \"linear\", label: \"Residuals\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            series={[\n              scatterSeries(\"p1-points\", fitted, residuals),\n              { id: \"p1-smooth\", type: \"line\", color: SMOOTH_COLOR, data: residLowess, showMark: false, curve: \"natural\" },\n            ]}\n          >\n            <ChartsGrid horizontal vertical />\n            <ChartsReferenceLine y={0} lineStyle={REF_LINE_STYLE} />\n            <ScatterPlot />\n            <LinePlot />\n            <ChartsXAxis axisId=\"p1x\" />\n            <ChartsYAxis axisId=\"p1y\" />\n            <InfluentialLabels points={p1Points} />\n          </ChartContainer>\n        </div>\n\n        {/* Panel 2: Normal Q-Q */}\n        <div style={{ width: chartW, height: rowH }}>\n          <SubplotTitle height={subTitleH}>Normal Q-Q</SubplotTitle>\n          <ChartContainer\n            width={chartW}\n            height={chartH}\n            skipAnimation\n            xAxis={[{ id: \"p2x\", scaleType: \"linear\", data: qqDiagonalGrid, label: \"Theoretical Quantiles\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            yAxis={[{ id: \"p2y\", scaleType: \"linear\", label: \"Standardized Residuals\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            series={[\n              scatterSeries(\"p2-points\", theoreticalQuantiles, sortedStdResiduals),\n              { id: \"p2-diagonal\", type: \"line\", color: t.ink, data: qqDiagonalGrid, showMark: false, curve: \"linear\" },\n            ]}\n          >\n            <ChartsGrid horizontal vertical />\n            <ScatterPlot />\n            <LinePlot slotProps={{ line: { style: REF_LINE_STYLE } }} />\n            <ChartsXAxis axisId=\"p2x\" />\n            <ChartsYAxis axisId=\"p2y\" />\n            <InfluentialLabels points={p2Points} />\n          </ChartContainer>\n        </div>\n\n        {/* Panel 3: Scale-Location */}\n        <div style={{ width: chartW, height: rowH }}>\n          <SubplotTitle height={subTitleH}>Scale-Location</SubplotTitle>\n          <ChartContainer\n            width={chartW}\n            height={chartH}\n            skipAnimation\n            xAxis={[{ id: \"p3x\", scaleType: \"linear\", data: fittedGrid, label: \"Fitted Values\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            yAxis={[{ id: \"p3y\", scaleType: \"linear\", label: \"√|Standardized Residuals|\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            series={[\n              scatterSeries(\"p3-points\", fitted, scaleLocation),\n              { id: \"p3-smooth\", type: \"line\", color: SMOOTH_COLOR, data: scaleLocLowess, showMark: false, curve: \"natural\" },\n            ]}\n          >\n            <ChartsGrid horizontal vertical />\n            <ScatterPlot />\n            <LinePlot />\n            <ChartsXAxis axisId=\"p3x\" />\n            <ChartsYAxis axisId=\"p3y\" />\n            <InfluentialLabels points={p3Points} />\n          </ChartContainer>\n        </div>\n\n        {/* Panel 4: Residuals vs Leverage, with Cook's distance contours */}\n        <div style={{ width: chartW, height: rowH }}>\n          <SubplotTitle height={subTitleH}>Residuals vs Leverage</SubplotTitle>\n          <ChartContainer\n            width={chartW}\n            height={chartH}\n            skipAnimation\n            xAxis={[{ id: \"p4x\", scaleType: \"linear\", data: leverageGrid, label: \"Leverage\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            yAxis={[{ id: \"p4y\", scaleType: \"linear\", label: \"Standardized Residuals\", labelStyle: AXIS_LABEL_STYLE, tickLabelStyle: TICK_LABEL_STYLE }]}\n            series={[\n              scatterSeries(\"p4-points\", leverage, stdResiduals),\n              { id: \"p4-cook-half-pos\", type: \"line\", color: CONTOUR_COLOR, data: cookHalfPos, showMark: false, curve: \"natural\" },\n              { id: \"p4-cook-half-neg\", type: \"line\", color: CONTOUR_COLOR, data: cookHalfNeg, showMark: false, curve: \"natural\" },\n              { id: \"p4-cook-one-pos\", type: \"line\", color: CONTOUR_COLOR, data: cookOnePos, showMark: false, curve: \"natural\" },\n              { id: \"p4-cook-one-neg\", type: \"line\", color: CONTOUR_COLOR, data: cookOneNeg, showMark: false, curve: \"natural\" },\n            ]}\n          >\n            <ChartsGrid horizontal vertical />\n            <ChartsReferenceLine y={0} lineStyle={REF_LINE_STYLE} />\n            <LinePlot slotProps={{ line: { style: CONTOUR_LINE_STYLE } }} />\n            <ScatterPlot />\n            <ChartsXAxis axisId=\"p4x\" />\n            <ChartsYAxis axisId=\"p4y\" />\n            <InfluentialLabels points={p4Points} />\n          </ChartContainer>\n        </div>\n      </div>\n    </div>\n  );\n}\n"}