{"spec_id":"scatter-regression-lowess","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// scatter-regression-lowess: Scatter Plot with LOWESS Regression\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Reproducible PRNG (LCG + Box-Muller for gaussian noise) ---------------\nlet lcgState = 20260909;\nfunction uniform() {\n  lcgState = (1664525 * lcgState + 1013904223) >>> 0;\n  return lcgState / 4294967296;\n}\nfunction gaussian(mean, std) {\n  const u1 = Math.max(uniform(), 1e-9);\n  const u2 = uniform();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + std * z;\n}\n\n// --- Data: ad-spend saturation curve with fatigue dip -----------------------\n// Weekly sales response to advertising spend: steep early gains, diminishing\n// returns as spend saturates the audience, then a mild ad-fatigue decline —\n// a non-monotonic relationship no single parametric curve captures cleanly.\nconst POINT_COUNT = 200;\nconst adSpend = [];\nconst weeklySales = [];\nfor (let i = 0; i < POINT_COUNT; i++) {\n  const spend = uniform() * 100;\n  const saturation = 22 + 58 * (1 - Math.exp(-spend / 22));\n  const fatigue = spend > 65 ? 0.22 * (spend - 65) : 0;\n  const sales = saturation - fatigue + gaussian(0, 5.5);\n  adSpend.push(Math.round(spend * 10) / 10);\n  weeklySales.push(Math.round(Math.max(sales, 0) * 10) / 10);\n}\n\n// --- LOWESS smoothing (local weighted linear regression) -------------------\n// Tricube-weighted local linear fit evaluated on a dense grid across the\n// x-range; frac controls the neighborhood fraction used at each grid point.\nfunction tricube(u) {\n  return u < 1 ? Math.pow(1 - Math.pow(u, 3), 3) : 0;\n}\n\nfunction lowess(xs, ys, frac, gridSize) {\n  const n = xs.length;\n  const neighbors = Math.max(2, Math.round(frac * n));\n  const xMin = Math.min(...xs);\n  const xMax = Math.max(...xs);\n  const fitted = [];\n  for (let g = 0; g < gridSize; g++) {\n    const xg = xMin + ((xMax - xMin) * g) / (gridSize - 1);\n    const distances = xs.map((xi) => Math.abs(xi - xg)).sort((a, b) => a - b);\n    const bandwidth = Math.max(distances[neighbors - 1], 1e-6);\n\n    let s0 = 0;\n    let s1 = 0;\n    let s2 = 0;\n    let sy = 0;\n    let sxy = 0;\n    for (let i = 0; i < n; i++) {\n      const w = tricube(Math.abs(xs[i] - xg) / bandwidth);\n      if (w === 0) continue;\n      s0 += w;\n      s1 += w * xs[i];\n      s2 += w * xs[i] * xs[i];\n      sy += w * ys[i];\n      sxy += w * xs[i] * ys[i];\n    }\n    const denom = s0 * s2 - s1 * s1;\n    const slope = denom !== 0 ? (s0 * sxy - s1 * sy) / denom : 0;\n    const intercept = (sy - slope * s1) / s0;\n    fitted.push([xg, intercept + slope * xg]);\n  }\n  return fitted;\n}\n\nconst lowessCurve = lowess(adSpend, weeklySales, 0.35, 120);\n\n// --- Story markers: find where the LOWESS curve peaks (saturation point) ---\n// then declines (ad fatigue) — drive the zone/band thresholds from the fitted\n// curve itself rather than a hard-coded spend value.\nconst xMax = Math.max(...adSpend);\nlet peakIndex = 0;\nfor (let i = 1; i < lowessCurve.length; i++) {\n  if (lowessCurve[i][1] > lowessCurve[peakIndex][1]) peakIndex = i;\n}\nconst peakX = lowessCurve[peakIndex][0];\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: \"scatter-regression-lowess · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"LOWESS trend, bandwidth frac = 0.35\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: {\n      text: \"Advertising Spend ($1,000s)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineWidth: 0,\n    tickWidth: 0,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotBands: [\n      {\n        from: peakX,\n        to: xMax,\n        color: Highcharts.color(t.amber).setOpacity(0.1).get(\"rgba\"),\n        label: {\n          text: \"Ad-fatigue region\",\n          align: \"right\",\n          x: -8,\n          y: 16,\n          style: { color: t.inkSoft, fontSize: \"12px\", fontStyle: \"italic\" },\n        },\n      },\n    ],\n    plotLines: [\n      {\n        value: peakX,\n        color: t.inkSoft,\n        width: 1,\n        dashStyle: \"ShortDash\",\n        label: {\n          text: `Saturation ~$${Math.round(peakX)}k`,\n          rotation: 0,\n          y: -6,\n          style: { color: t.inkSoft, fontSize: \"12px\" },\n        },\n      },\n    ],\n  },\n  yAxis: {\n    title: {\n      text: \"Weekly Sales (units)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    lineWidth: 0,\n    tickWidth: 0,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    enabled: true,\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      marker: {\n        radius: 6,\n        fillColor: Highcharts.color(t.palette[0]).setOpacity(0.7).get(\"rgba\"),\n        lineWidth: 0,\n      },\n      states: { hover: { halo: { size: 0 } } },\n    },\n  },\n  tooltip: {\n    pointFormat: \"Spend: <b>${point.x}k</b><br/>Sales: <b>{point.y}</b> units\",\n  },\n  series: [\n    {\n      type: \"scatter\",\n      name: \"Weekly observations\",\n      data: adSpend.map((x, i) => [x, weeklySales[i]]),\n      color: t.palette[0],\n    },\n    {\n      type: \"line\",\n      name: \"LOWESS smoothed trend\",\n      data: lowessCurve,\n      color: t.ink,\n      lineWidth: 3,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      zoneAxis: \"x\",\n      zones: [\n        { value: peakX, color: t.ink },\n        { color: t.amber, dashStyle: \"Dash\" },\n      ],\n    },\n  ],\n});\n"}