{"spec_id":"pp-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// pp-basic: Probability-Probability (P-P) Plot\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-09\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (deterministic LCG, no browser RNG) ---\nlet seed = 42;\nfunction rand() {\n  seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\nfunction randn() {\n  const u = rand() || 1e-10;\n  const v = rand();\n  return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);\n}\n\n// 200 samples from a slightly right-skewed distribution (normal + quadratic term)\n// Context: manufacturing process measurements — testing normality assumption\nconst N = 200;\nconst raw = Array.from({ length: N }, () => {\n  const z = randn();\n  return z + 0.45 * z * z - 0.45; // right-skewed, bias-corrected\n});\n\n// Fit normal parameters to the sample\nconst sampleMean = raw.reduce((a, b) => a + b, 0) / N;\nconst sampleStd = Math.sqrt(raw.reduce((s, x) => s + (x - sampleMean) ** 2, 0) / (N - 1));\n\n// Sort and compute empirical CDF (Hazen plotting position: (i + 0.5) / N)\nconst sorted = [...raw].sort((a, b) => a - b);\nconst empirical = sorted.map((_, i) => (i + 0.5) / N);\n\n// Normal CDF via error function approximation (Abramowitz & Stegun 7.1.26)\nfunction erf(z) {\n  const a1 = 0.254829592,\n    a2 = -0.284496736,\n    a3 = 1.421413741;\n  const a4 = -1.453152027,\n    a5 = 1.061405429,\n    q = 0.3275911;\n  const sign = z < 0 ? -1 : 1;\n  const ta = 1 / (1 + q * Math.abs(z));\n  const y = 1 - (((((a5 * ta + a4) * ta + a3) * ta + a2) * ta + a1) * ta) * Math.exp(-z * z);\n  return sign * y;\n}\nfunction normCDF(x, mu, sigma) {\n  return 0.5 * (1 + erf((x - mu) / (sigma * Math.SQRT2)));\n}\n\nconst theoretical = sorted.map((val) => normCDF(val, sampleMean, sampleStd));\nconst points = theoretical.map((th, i) => ({ th, em: empirical[i] }));\n\n// Max absolute departure from the diagonal — the key diagnostic insight\nconst maxDevIdx = points.reduce(\n  (best, p, i) => (Math.abs(p.em - p.th) > Math.abs(points[best].em - points[best].th) ? i : best),\n  0,\n);\nconst mdp = points[maxDevIdx];\n\n// --- Layout ---\nconst margin = { top: 95, right: 70, bottom: 110, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\n// Clip path — keeps deviation band and trend line inside plot bounds\nsvg\n  .append(\"defs\")\n  .append(\"clipPath\")\n  .attr(\"id\", \"pp-clip\")\n  .append(\"rect\")\n  .attr(\"width\", iw)\n  .attr(\"height\", ih);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---\nconst x = d3.scaleLinear().domain([0, 1]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, 1]).range([ih, 0]);\n\n// --- Gridlines (both axes for scatter) ---\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(x.ticks(5))\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(5))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Deviation band: d3.area between empirical curve and 45° diagonal ---\n// Fills the departure region so the S-curve story is immediately visible\nconst areaGen = d3\n  .area()\n  .x((d) => x(d.th))\n  .y0((d) => y(d.th)) // baseline: perfect-fit diagonal\n  .y1((d) => y(d.em)) // actual: empirical CDF\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(points)\n  .attr(\"d\", areaGen)\n  .attr(\"clip-path\", \"url(#pp-clip)\")\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.13)\n  .attr(\"stroke\", \"none\");\n\n// --- 45-degree reference line (perfect fit) ---\ng.append(\"line\")\n  .attr(\"x1\", x(0))\n  .attr(\"y1\", y(0))\n  .attr(\"x2\", x(1))\n  .attr(\"y2\", y(1))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"10,7\")\n  .attr(\"opacity\", 0.75);\n\n// --- Data points ---\ng.selectAll(\"circle\")\n  .data(points)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => x(d.th))\n  .attr(\"cy\", (d) => y(d.em))\n  .attr(\"r\", 4.5)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"opacity\", 0.65)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1);\n\n// --- Highlight ring at maximum departure point ---\nconst mdpPx = x(mdp.th);\nconst mdpPy = y(mdp.em);\n\ng.append(\"circle\")\n  .attr(\"cx\", mdpPx)\n  .attr(\"cy\", mdpPy)\n  .attr(\"r\", 10)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2)\n  .attr(\"opacity\", 0.9);\n\n// --- Annotation: callout for maximum departure ---\nconst goRight = mdpPx < iw / 2;\nconst goUp = mdpPy >= ih / 3;\nconst aX = goRight ? mdpPx + 130 : mdpPx - 130;\nconst aY = goUp ? mdpPy - 75 : mdpPy + 75;\nconst anchor = goRight ? \"start\" : \"end\";\nconst connX = goRight ? mdpPx + 14 : mdpPx - 14;\nconst connY = goUp ? mdpPy - 14 : mdpPy + 14;\n\ng.append(\"line\")\n  .attr(\"x1\", connX)\n  .attr(\"y1\", connY)\n  .attr(\"x2\", aX)\n  .attr(\"y2\", aY + (goUp ? 32 : -32))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"opacity\", 0.65);\n\ng.append(\"text\")\n  .attr(\"x\", aX)\n  .attr(\"y\", aY)\n  .attr(\"text-anchor\", anchor)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"max departure\");\n\ng.append(\"text\")\n  .attr(\"x\", aX)\n  .attr(\"y\", aY + 22)\n  .attr(\"text-anchor\", anchor)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(`Δ = ${(Math.abs(mdp.em - mdp.th) * 100).toFixed(1)} pp`);\n\n// --- Axes ---\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(5).tickFormat(d3.format(\".1f\")));\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(\".1f\")));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n  ax.selectAll(\".tick line\").remove();\n}\n\n// --- Axis labels ---\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 22)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Theoretical Normal Probability\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2))\n  .attr(\"y\", 28)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Empirical Probability\");\n\n// --- Title ---\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"pp-basic · javascript · d3 · anyplot.ai\");\n"}