{"spec_id":"residual-plot","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// residual-plot: Residual Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\n// Simulated house-price regression: fitted values (predicted price, $k) vs.\n// residuals (observed - predicted), with mild heteroscedasticity so the\n// diagnostic value of the plot is visible (funnel-shaped spread).\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nfunction gaussian(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 = 220;\nconst fitted = [];\nconst residuals = [];\nfor (let i = 0; i < n; i++) {\n  const price = 150 + rand() * 500; // fitted house price, $k\n  const noiseScale = 8 + price * 0.08; // variance grows with fitted value\n  const residual = gaussian(rand) * noiseScale;\n  fitted.push(Number(price.toFixed(1)));\n  residuals.push(Number(residual.toFixed(1)));\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 sd = Math.sqrt(variance);\nconst threshold = 2 * sd;\n\nconst inlierPoints = [];\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    inlierPoints.push(point);\n  }\n}\n\nconst xMin = Math.min(...fitted);\nconst xMax = Math.max(...fitted);\n\n// Binned-mean smoothing line: makes the heteroscedastic (funnel-shaped) trend\n// explicit without pulling in a full LOWESS implementation.\nconst sortedPoints = fitted.map((x, i) => ({ x, y: residuals[i] })).sort((a, b) => a.x - b.x);\nconst binCount = 10;\nconst binWidth = (xMax - xMin) / binCount;\nconst trendPoints = [];\nfor (let b = 0; b < binCount; b++) {\n  const lo = xMin + b * binWidth;\n  const hi = lo + binWidth;\n  const inBin = sortedPoints.filter((p) => p.x >= lo && (b === binCount - 1 ? p.x <= hi : p.x < hi));\n  if (inBin.length === 0) continue;\n  const meanX = inBin.reduce((a, p) => a + p.x, 0) / inBin.length;\n  const meanY = inBin.reduce((a, p) => a + p.y, 0) / inBin.length;\n  trendPoints.push([Number(meanX.toFixed(1)), Number(meanY.toFixed(2))]);\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"residual-plot · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    title: { text: \"Fitted Value ($k)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    min: xMin - 20,\n    max: xMax + 20,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  yAxis: {\n    title: { text: \"Residual ($k)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotLines: [\n      {\n        value: 0,\n        color: t.ink,\n        width: 2,\n        zIndex: 3,\n      },\n    ],\n    plotBands: [\n      {\n        from: -threshold,\n        to: threshold,\n        color: Highcharts.color(t.inkSoft).setOpacity(0.08).get(\"rgba\"),\n        zIndex: 0,\n      },\n    ],\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    formatter: function () {\n      return `<b>Fitted:</b> $${this.x}k<br/><b>Residual:</b> $${this.y}k`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      marker: { radius: 4, symbol: \"circle\" },\n    },\n  },\n  series: [\n    {\n      name: \"Residuals (within ±2 SD)\",\n      data: inlierPoints,\n      color: t.palette[0],\n      marker: { fillColor: Highcharts.color(t.palette[0]).setOpacity(0.5).get(\"rgba\"), lineWidth: 0 },\n    },\n    {\n      name: \"Outliers (beyond ±2 SD)\",\n      data: outlierPoints,\n      color: t.palette[4],\n      marker: { radius: 6, lineColor: t.ink, lineWidth: 1 },\n      dataLabels: {\n        enabled: true,\n        formatter: function () {\n          return (this.y < 0 ? \"-$\" : \"$\") + Math.abs(this.y).toFixed(1) + \"k\";\n        },\n        y: -12,\n        style: { color: t.ink, fontSize: \"11px\", fontWeight: \"600\", textOutline: \"none\" },\n      },\n    },\n    {\n      name: \"Trend (binned mean)\",\n      type: \"spline\",\n      data: trendPoints,\n      color: t.palette[1],\n      lineWidth: 2.5,\n      dashStyle: \"ShortDash\",\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      zIndex: 2,\n    },\n  ],\n});\n"}