{"spec_id":"scatter-regression-lowess","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-regression-lowess: Scatter Plot with LOWESS Regression\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Weekly ad-spend campaigns and their conversion rate: response rises with\n// spend, plateaus, then dips slightly at very high spend (ad fatigue) — a\n// non-linear pattern with no obvious closed-form model, well suited to LOWESS.\nfunction makeRng(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (1664525 * state + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rng = makeRng(42);\nfunction gaussian() {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst n = 150;\nconst adSpend = [];\nfor (let i = 0; i < n; i++) {\n  adSpend.push(1 + (99 * i) / (n - 1) + (rng() - 0.5) * 0.6);\n}\nadSpend.sort((a, b) => a - b);\n\nconst conversionRate = adSpend.map((x) => {\n  const rise = 8 * (1 - Math.exp(-x / 20));\n  const fatigue = x > 70 ? 0.015 * Math.pow(x - 70, 1.5) : 0;\n  return 2 + rise - fatigue + gaussian() * 0.9;\n});\n\n// --- LOWESS (locally weighted regression with tricube weights + two\n//     bisquare robustness iterations, following Cleveland 1979) -------------\nfunction lowess(xs, ys, frac, iterations) {\n  const count = xs.length;\n  const windowSize = Math.max(2, Math.round(frac * count));\n  let robustWeights = new Array(count).fill(1);\n  let fitted = new Array(count).fill(0);\n\n  for (let iter = 0; iter <= iterations; iter++) {\n    for (let i = 0; i < count; i++) {\n      const xi = xs[i];\n      const distances = xs.map((x) => Math.abs(x - xi));\n      const bandwidth = [...distances].sort((a, b) => a - b)[windowSize - 1] || 1e-9;\n\n      let sumW = 0;\n      let sumWX = 0;\n      let sumWY = 0;\n      let sumWXX = 0;\n      let sumWXY = 0;\n      for (let j = 0; j < count; j++) {\n        const d = distances[j] / bandwidth;\n        if (d >= 1) continue;\n        const w = Math.pow(1 - Math.pow(d, 3), 3) * robustWeights[j];\n        sumW += w;\n        sumWX += w * xs[j];\n        sumWY += w * ys[j];\n        sumWXX += w * xs[j] * xs[j];\n        sumWXY += w * xs[j] * ys[j];\n      }\n\n      const denom = sumW * sumWXX - sumWX * sumWX;\n      let slope = 0;\n      let intercept = sumWY / sumW;\n      if (Math.abs(denom) > 1e-9) {\n        slope = (sumW * sumWXY - sumWX * sumWY) / denom;\n        intercept = (sumWY - slope * sumWX) / sumW;\n      }\n      fitted[i] = intercept + slope * xi;\n    }\n\n    if (iter < iterations) {\n      const absResiduals = ys.map((y, i) => Math.abs(y - fitted[i]));\n      const sortedAbs = [...absResiduals].sort((a, b) => a - b);\n      const mid = Math.floor(count / 2);\n      const mad = count % 2 !== 0 ? sortedAbs[mid] : (sortedAbs[mid - 1] + sortedAbs[mid]) / 2;\n      const scale = 6 * mad || 1e-9;\n      robustWeights = ys.map((y, i) => {\n        const u = (y - fitted[i]) / scale;\n        return Math.abs(u) < 1 ? Math.pow(1 - u * u, 2) : 0;\n      });\n    }\n  }\n\n  return fitted;\n}\n\nconst lowessFit = lowess(adSpend, conversionRate, 0.35, 2);\nconst curvePoints = adSpend.map((x, i) => ({ x, y: lowessFit[i] }));\n\n// --- Helpers -----------------------------------------------------------------\nfunction withAlpha(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst TITLE_TEXT =\n  \"Ad Spend vs. Conversion Rate · scatter-regression-lowess · javascript · chartjs · anyplot.ai\";\nconst TITLE_FONT_SIZE = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        type: \"scatter\",\n        label: \"Weekly campaigns\",\n        data: adSpend.map((x, i) => ({ x, y: conversionRate[i] })),\n        backgroundColor: withAlpha(t.palette[0], 0.6),\n        borderColor: t.pageBg,\n        borderWidth: 1,\n        pointRadius: 5,\n        pointHoverRadius: 6,\n      },\n      {\n        type: \"line\",\n        label: \"LOWESS fit (frac = 0.35)\",\n        data: curvePoints,\n        borderColor: t.palette[1],\n        backgroundColor: t.palette[1],\n        borderWidth: 3.5,\n        pointRadius: 0,\n        fill: false,\n        tension: 0,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: TITLE_TEXT,\n        color: t.ink,\n        font: { size: TITLE_FONT_SIZE, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"top\",\n        align: \"end\",\n        labels: { color: t.ink, font: { size: 16 }, boxWidth: 24, usePointStyle: true },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Weekly Ad Spend ($1,000s)\", color: t.ink, font: { size: 18 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Conversion Rate (%)\", color: t.ink, font: { size: 18 } },\n      },\n    },\n  },\n});\n"}