{"spec_id":"pdp-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// pdp-basic: Partial Dependence Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 70, bottom: 130, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic PRNG (mulberry32, fixed seed) ----------------------------\nfunction mulberry32(seed) {\n  return function () {\n    seed |= 0;\n    seed = (seed + 0x6d2b79f5) | 0;\n    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\nfunction randNormal(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\n// --- Data: PDP of house price on living area, from a gradient boosting model\nconst rand = mulberry32(42);\nconst xMin = 800;\nconst xMax = 4000;\nconst gridSize = 70;\nconst featureValues = d3.range(gridSize).map((i) => xMin + (i / (gridSize - 1)) * (xMax - xMin));\n\n// Saturating price response with a mild boosting-style wiggle, then centered at zero.\nconst centerX = (xMin + xMax) / 2;\nconst halfRange = (xMax - xMin) / 2;\nconst rawDependence = featureValues.map(\n  (x) => 165 * Math.log(x / xMin) + 4 * Math.sin((x - xMin) / 240)\n);\nconst baseline = d3.mean(rawDependence);\nconst partialDependence = rawDependence.map((v) => v - baseline);\n\n// Confidence band widens toward the sparser edges of the feature range.\nconst ciHalfWidth = featureValues.map(\n  (x) => 3 + 22 * Math.pow(Math.abs(x - centerX) / halfRange, 1.8)\n);\nconst ciLower = partialDependence.map((v, i) => v - ciHalfWidth[i]);\nconst ciUpper = partialDependence.map((v, i) => v + ciHalfWidth[i]);\n\n// Rug: training sample of feature values (approx. normal, clipped to range).\nconst rugValues = d3.range(140).map(() => {\n  const v = 1900 + 480 * randNormal(rand);\n  return Math.max(xMin, Math.min(xMax, v));\n});\n\n// ICE (individual conditional expectation): a handful of per-sample curves\n// with their own baseline offset and slope, sitting alongside the averaged PDP.\nconst iceCount = 8;\nconst iceLines = d3.range(iceCount).map(() => {\n  const offset = 12 * randNormal(rand);\n  const scale = 1 + 0.12 * randNormal(rand);\n  return partialDependence.map((v) => v * scale + offset);\n});\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleLinear().domain([xMin, xMax]).range([0, iw]);\nconst iceExtent = iceLines.flat();\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min([...ciLower, ...iceExtent]), d3.max([...ciUpper, ...iceExtent])])\n  .nice()\n  .range([ih, 0]);\n\n// Thin the rug in dense regions so ticks read as texture, not a solid blob.\nconst rugBins = d3.bin().domain(x.domain()).thresholds(40)(rugValues);\nconst maxTicksPerBin = 4;\nconst thinnedRug = rugBins.flatMap((bin) =>\n  bin.length <= maxTicksPerBin\n    ? bin\n    : d3.range(maxTicksPerBin).map((i) => bin[Math.floor((i * bin.length) / maxTicksPerBin)])\n);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Zero reference line — the PDP is centered, so this marks \"no effect vs. average\".\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(0))\n  .attr(\"y2\", y(0))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,5\");\n\n// Confidence band\nconst area = d3\n  .area()\n  .x((d, i) => x(featureValues[i]))\n  .y0((d, i) => y(ciLower[i]))\n  .y1((d, i) => y(ciUpper[i]))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\").datum(featureValues).attr(\"d\", area).attr(\"fill\", t.palette[0]).attr(\"fill-opacity\", 0.16);\n\n// ICE lines — faint per-sample curves drawn beneath the averaged PDP curve\nconst iceLine = d3\n  .line()\n  .x((d, i) => x(featureValues[i]))\n  .y((d) => y(d))\n  .curve(d3.curveMonotoneX);\ng.selectAll(\".ice\")\n  .data(iceLines)\n  .join(\"path\")\n  .attr(\"class\", \"ice\")\n  .attr(\"d\", iceLine)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-opacity\", 0.15)\n  .attr(\"stroke-width\", 1);\n\n// Partial dependence curve\nconst line = d3\n  .line()\n  .x((d, i) => x(featureValues[i]))\n  .y((d, i) => y(partialDependence[i]))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\")\n  .datum(featureValues)\n  .attr(\"d\", line)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 4);\n\n// Rug plot — distribution of observed feature values along the x-axis\ng.selectAll(\".rug\")\n  .data(thinnedRug)\n  .join(\"line\")\n  .attr(\"class\", \"rug\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", ih + 34)\n  .attr(\"y2\", ih + 48)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-opacity\", 0.45)\n  .attr(\"stroke-width\", 1.5);\n\n// Direct label for the shaded band (single series → no legend needed)\ng.append(\"text\")\n  .attr(\"x\", iw)\n  .attr(\"y\", y(ciUpper[ciUpper.length - 1]) - 12)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"prediction interval\");\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickFormat(d3.format(\",\")));\nconst yAxis = g.append(\"g\").call(\n  d3\n    .axisLeft(y)\n    .ticks(6)\n    .tickFormat((d) => (d > 0 ? \"+\" : \"\") + d3.format(\",\")(d))\n);\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\".tick line\").attr(\"y2\", 0);\nyAxis.selectAll(\".tick line\").attr(\"x2\", 0);\n\n// --- Axis labels ------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Living Area (sq ft)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Partial Dependence (Δ Predicted Price, $k)\");\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"pdp-basic · javascript · d3 · anyplot.ai\");\n"}