{"spec_id":"diagnostic-regression-panel","library":"echarts","language":"javascript","code":"// anyplot.ai\n// diagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n// The harness doesn't expose a \"muted\" token — derive it locally (see\n// default-style-guide.md \"Theme-adaptive Chrome\" semantic anchors table).\nconst inkMuted = t.theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\n\n// --- Deterministic PRNG (LCG + Box-Muller) ---------------------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function uniform() {\n    state = (1664525 * state + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nfunction makeGaussian(uniform) {\n  let spare = null;\n  return function gaussian(mean, sd) {\n    if (spare !== null) {\n      const z = spare;\n      spare = null;\n      return mean + sd * z;\n    }\n    let u1 = 0;\n    do {\n      u1 = uniform();\n    } while (u1 <= 1e-12);\n    const u2 = uniform();\n    const mag = Math.sqrt(-2 * Math.log(u1));\n    spare = mag * Math.sin(2 * Math.PI * u2);\n    return mean + sd * mag * Math.cos(2 * Math.PI * u2);\n  };\n}\n\n// --- Inverse normal CDF (Acklam's rational approximation) ------------------\nfunction qnorm(p) {\n  const a = [\n    -3.969683028665376e1, 2.209460984245205e2, -2.759285104469687e2,\n    1.38357751867269e2, -3.066479806614716e1, 2.506628277459239,\n  ];\n  const b = [\n    -5.447609879822406e1, 1.615858368580409e2, -1.556989798598866e2,\n    6.680131188771972e1, -1.328068155288572e1,\n  ];\n  const c = [\n    -7.784894002430293e-3, -3.223964580411365e-1, -2.400758277161838,\n    -2.549732539343734, 4.374664141464968, 2.938163982698783,\n  ];\n  const d = [\n    7.784695709041462e-3, 3.224671290700398e-1, 2.445134137142996,\n    3.754408661907416,\n  ];\n  const plow = 0.02425;\n  const phigh = 1 - plow;\n  if (p < plow) {\n    const q = Math.sqrt(-2 * Math.log(p));\n    return (\n      (((((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  if (p <= phigh) {\n    const q = p - 0.5;\n    const r = q * q;\n    return (\n      ((((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) *\n        q) /\n      (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1)\n    );\n  }\n  const q = Math.sqrt(-2 * Math.log(1 - p));\n  return (\n    -(((((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\n// --- Local-linear LOWESS smoother -------------------------------------------\nfunction lowess(xValues, yValues, frac) {\n  const n = xValues.length;\n  const window = Math.max(3, Math.floor(frac * n));\n  const order = xValues\n    .map((_, i) => i)\n    .sort((a, b) => xValues[a] - xValues[b]);\n  const sortedX = order.map((i) => xValues[i]);\n  const sortedY = order.map((i) => yValues[i]);\n  const fitted = new Array(n);\n  for (let i = 0; i < n; i += 1) {\n    const x0 = sortedX[i];\n    const distances = sortedX.map((x) => Math.abs(x - x0));\n    const bandwidth = [...distances].sort((a, b) => a - b)[window - 1] || 1;\n    let sumW = 0;\n    let sumWx = 0;\n    let sumWy = 0;\n    let sumWxy = 0;\n    let sumWxx = 0;\n    for (let j = 0; j < n; j += 1) {\n      const ratio = Math.min(1, distances[j] / bandwidth);\n      const w = (1 - ratio ** 3) ** 3;\n      sumW += w;\n      sumWx += w * sortedX[j];\n      sumWy += w * sortedY[j];\n      sumWxy += w * sortedX[j] * sortedY[j];\n      sumWxx += w * sortedX[j] * sortedX[j];\n    }\n    const denom = sumW * sumWxx - sumWx * sumWx;\n    const slope =\n      Math.abs(denom) < 1e-9 ? 0 : (sumW * sumWxy - sumWx * sumWy) / denom;\n    const intercept = (sumWy - slope * sumWx) / sumW;\n    fitted[i] = intercept + slope * x0;\n  }\n  return sortedX.map((x, i) => [x, fitted[i]]);\n}\n\n// --- Data: simulate a regression with heteroscedasticity, mild curvature ---\n// and two crafted high-leverage points (one influential, one not) ----------\nconst uniform = makeLcg(42);\nconst gaussian = makeGaussian(uniform);\n\nconst predictor = [];\nconst response = [];\nfor (let i = 0; i < 77; i += 1) {\n  const x = uniform() * 10;\n  const noiseSd = 0.6 + 0.35 * x;\n  const trueY = 5 + 1.8 * x + 0.15 * x * x;\n  predictor.push(x);\n  response.push(trueY + gaussian(0, noiseSd));\n}\npredictor.push(11.2);\nresponse.push(5 + 1.8 * 11.2 + 0.15 * 11.2 * 11.2 + 9.5); // high leverage + large residual -> influential\npredictor.push(11.5);\nresponse.push(5 + 1.8 * 11.5 + 0.15 * 11.5 * 11.5 + 0.3); // high leverage, small residual -> not influential\npredictor.push(9.8);\nresponse.push(5 + 1.8 * 9.8 + 0.15 * 9.8 * 9.8 - 7.0); // moderate leverage, large negative residual\nconst n = predictor.length;\n\n// --- Simple linear regression (least squares) -------------------------------\nconst meanX = predictor.reduce((a, v) => a + v, 0) / n;\nconst meanY = response.reduce((a, v) => a + v, 0) / n;\nlet sumSquaredX = 0;\nlet sumCrossXY = 0;\nfor (let i = 0; i < n; i += 1) {\n  const dx = predictor[i] - meanX;\n  sumSquaredX += dx * dx;\n  sumCrossXY += dx * (response[i] - meanY);\n}\nconst slope = sumCrossXY / sumSquaredX;\nconst intercept = meanY - slope * meanX;\n\nconst fittedValues = predictor.map((x) => intercept + slope * x);\nconst residuals = response.map((y, i) => y - fittedValues[i]);\n\nconst numParams = 2; // intercept + slope\nlet sumSquaredResid = 0;\nresiduals.forEach((r) => {\n  sumSquaredResid += r * r;\n});\nconst residualScale = Math.sqrt(sumSquaredResid / (n - numParams));\n\nconst leverage = predictor.map((x) => 1 / n + (x - meanX) ** 2 / sumSquaredX);\nconst standardizedResiduals = residuals.map(\n  (r, i) => r / (residualScale * Math.sqrt(1 - leverage[i])),\n);\nconst sqrtAbsStdResiduals = standardizedResiduals.map((r) =>\n  Math.sqrt(Math.abs(r)),\n);\nconst cooksDistance = standardizedResiduals.map(\n  (r, i) => (r * r * leverage[i]) / (numParams * (1 - leverage[i])),\n);\n\nconst rankByCooksD = predictor\n  .map((_, i) => i)\n  .sort((a, b) => cooksDistance[b] - cooksDistance[a]);\nconst influentialIdx = new Set(rankByCooksD.slice(0, 3));\n\nfunction withInfluentialLabel(x, y, obsIdx) {\n  if (!influentialIdx.has(obsIdx)) return [x, y];\n  return {\n    value: [x, y],\n    label: {\n      show: true,\n      formatter: `#${obsIdx + 1}`,\n      position: \"top\",\n      color: t.ink,\n      fontSize: 13,\n      fontWeight: \"bold\",\n    },\n  };\n}\n\n// --- Subplot 1: Residuals vs Fitted -----------------------------------------\nconst residualsVsFittedData = predictor.map((x, i) =>\n  withInfluentialLabel(fittedValues[i], residuals[i], i),\n);\nconst residualsLowess = lowess(fittedValues, residuals, 0.6);\n\n// --- Subplot 2: Normal Q-Q ---------------------------------------------------\nconst sortedByStdResid = standardizedResiduals\n  .map((_, i) => i)\n  .sort((a, b) => standardizedResiduals[a] - standardizedResiduals[b]);\nconst qqData = sortedByStdResid.map((obsIdx, rank) => {\n  const theoreticalQuantile = qnorm((rank + 0.5) / n);\n  return withInfluentialLabel(\n    theoreticalQuantile,\n    standardizedResiduals[obsIdx],\n    obsIdx,\n  );\n});\nconst theoreticalQuantiles = sortedByStdResid.map((_, rank) =>\n  qnorm((rank + 0.5) / n),\n);\nconst qqRange = [\n  Math.min(...theoreticalQuantiles),\n  Math.max(...theoreticalQuantiles),\n];\n\n// --- Subplot 3: Scale-Location -----------------------------------------------\nconst scaleLocationData = predictor.map((x, i) =>\n  withInfluentialLabel(fittedValues[i], sqrtAbsStdResiduals[i], i),\n);\nconst scaleLocationLowess = lowess(fittedValues, sqrtAbsStdResiduals, 0.6);\n\n// --- Subplot 4: Residuals vs Leverage, with Cook's distance contours --------\nconst residualsVsLeverageData = predictor.map((x, i) =>\n  withInfluentialLabel(leverage[i], standardizedResiduals[i], i),\n);\nconst maxLeverage = Math.max(...leverage);\nconst maxAbsStdResid = Math.max(...standardizedResiduals.map(Math.abs));\nconst contourHMax = Math.min(0.85, maxLeverage * 1.35);\nconst contourYCap = Math.max(6, maxAbsStdResid + 1);\n\nfunction cooksContourBranch(cooksD, sign) {\n  const hMin =\n    (cooksD * numParams) / (contourYCap * contourYCap + cooksD * numParams);\n  const points = [];\n  const steps = 50;\n  for (let i = 0; i <= steps; i += 1) {\n    const h = hMin + (contourHMax - hMin) * (i / steps);\n    if (h <= 0 || h >= 1) continue;\n    const underRoot = (cooksD * numParams * (1 - h)) / h;\n    if (underRoot < 0) continue;\n    points.push([h, sign * Math.sqrt(underRoot)]);\n  }\n  return points;\n}\n\n// --- Layout: 2x2 grid of subplots, shared figure title ----------------------\nconst gridBoxes = [\n  { left: \"9%\", right: \"54%\", top: \"13%\", bottom: \"54%\" },\n  { left: \"55%\", right: \"6%\", top: \"13%\", bottom: \"54%\" },\n  { left: \"9%\", right: \"54%\", top: \"60%\", bottom: \"6%\" },\n  { left: \"55%\", right: \"6%\", top: \"60%\", bottom: \"6%\" },\n];\nconst subplotTitles = [\n  { text: \"Residuals vs Fitted\", left: \"27%\", top: \"6%\" },\n  { text: \"Normal Q-Q\", left: \"74%\", top: \"6%\" },\n  { text: \"Scale-Location\", left: \"27%\", top: \"53%\" },\n  { text: \"Residuals vs Leverage\", left: \"74%\", top: \"53%\" },\n];\n\nconst chart = echarts.init(document.getElementById(\"container\"));\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: [\n    {\n      text: \"diagnostic-regression-panel · javascript · echarts · anyplot.ai\",\n      left: \"center\",\n      top: \"1%\",\n      textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n    },\n    ...subplotTitles.map((cfg) => ({\n      text: cfg.text,\n      left: cfg.left,\n      top: cfg.top,\n      textAlign: \"center\",\n      textStyle: { color: t.ink, fontSize: 16, fontWeight: 500 },\n    })),\n  ],\n  grid: gridBoxes.map((box) => ({ ...box, containLabel: true })),\n  xAxis: [\n    {\n      gridIndex: 0,\n      type: \"value\",\n      name: \"Fitted values\",\n      nameLocation: \"middle\",\n      nameGap: 32,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 1,\n      type: \"value\",\n      name: \"Theoretical Quantiles\",\n      nameLocation: \"middle\",\n      nameGap: 32,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 2,\n      type: \"value\",\n      name: \"Fitted values\",\n      nameLocation: \"middle\",\n      nameGap: 32,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 3,\n      type: \"value\",\n      name: \"Leverage\",\n      nameLocation: \"middle\",\n      nameGap: 32,\n      min: 0,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n  ],\n  yAxis: [\n    {\n      gridIndex: 0,\n      type: \"value\",\n      name: \"Residuals\",\n      nameLocation: \"middle\",\n      nameGap: 46,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 1,\n      type: \"value\",\n      name: \"Standardized Residuals\",\n      nameLocation: \"middle\",\n      nameGap: 46,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 2,\n      type: \"value\",\n      name: \"√|Standardized Residuals|\",\n      nameLocation: \"middle\",\n      nameGap: 46,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 3,\n      type: \"value\",\n      name: \"Standardized Residuals\",\n      nameLocation: \"middle\",\n      nameGap: 46,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n  ],\n  series: [\n    // Subplot 1: Residuals vs Fitted\n    {\n      type: \"scatter\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      data: residualsVsFittedData,\n      symbolSize: 15,\n      itemStyle: { color: t.palette[0], opacity: 0.8 },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        label: { show: false },\n        lineStyle: { color: inkMuted, type: \"dashed\", width: 1.5 },\n        data: [{ yAxis: 0 }],\n      },\n    },\n    {\n      type: \"line\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      data: residualsLowess,\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: t.palette[2], width: 3 },\n      endLabel: {\n        show: true,\n        formatter: \"LOWESS\",\n        color: t.palette[2],\n        fontSize: 12,\n      },\n    },\n    // Subplot 2: Normal Q-Q\n    {\n      type: \"scatter\",\n      xAxisIndex: 1,\n      yAxisIndex: 1,\n      data: qqData,\n      symbolSize: 15,\n      itemStyle: { color: t.palette[0], opacity: 0.8 },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        label: { show: false },\n        lineStyle: { color: inkMuted, type: \"dashed\", width: 1.5 },\n        data: [\n          [\n            { coord: [qqRange[0], qqRange[0]] },\n            { coord: [qqRange[1], qqRange[1]] },\n          ],\n        ],\n      },\n    },\n    // Subplot 3: Scale-Location\n    {\n      type: \"scatter\",\n      xAxisIndex: 2,\n      yAxisIndex: 2,\n      data: scaleLocationData,\n      symbolSize: 15,\n      itemStyle: { color: t.palette[0], opacity: 0.8 },\n    },\n    {\n      type: \"line\",\n      xAxisIndex: 2,\n      yAxisIndex: 2,\n      data: scaleLocationLowess,\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: t.palette[2], width: 3 },\n      endLabel: {\n        show: true,\n        formatter: \"LOWESS\",\n        color: t.palette[2],\n        fontSize: 12,\n      },\n    },\n    // Subplot 4: Residuals vs Leverage, with Cook's distance contours\n    {\n      type: \"scatter\",\n      xAxisIndex: 3,\n      yAxisIndex: 3,\n      data: residualsVsLeverageData,\n      symbolSize: 11,\n      itemStyle: { color: t.palette[0], opacity: 0.65 },\n    },\n    {\n      type: \"line\",\n      xAxisIndex: 3,\n      yAxisIndex: 3,\n      data: cooksContourBranch(0.5, 1),\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: inkMuted, type: \"dashed\", width: 1.5 },\n      endLabel: {\n        show: true,\n        formatter: \"D=0.5\",\n        color: inkMuted,\n        fontSize: 12,\n      },\n    },\n    {\n      type: \"line\",\n      xAxisIndex: 3,\n      yAxisIndex: 3,\n      data: cooksContourBranch(0.5, -1),\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: inkMuted, type: \"dashed\", width: 1.5 },\n    },\n    {\n      type: \"line\",\n      xAxisIndex: 3,\n      yAxisIndex: 3,\n      data: cooksContourBranch(1.0, 1),\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: t.amber, type: \"dashed\", width: 1.5 },\n      endLabel: {\n        show: true,\n        formatter: \"D=1.0\",\n        color: t.amber,\n        fontSize: 12,\n      },\n    },\n    {\n      type: \"line\",\n      xAxisIndex: 3,\n      yAxisIndex: 3,\n      data: cooksContourBranch(1.0, -1),\n      showSymbol: false,\n      silent: true,\n      lineStyle: { color: t.amber, type: \"dashed\", width: 1.5 },\n    },\n  ],\n});\n"}