{"spec_id":"pdp-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// pdp-basic: Partial Dependence Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Partial dependence of predicted house price on living-area square footage,\n// as if extracted from a GradientBoostingRegressor. The effect is centered at\n// zero at the median square footage so the curve reads as a relative lift.\nconst GRID_POINTS = 60;\nconst SQFT_MIN = 500;\nconst SQFT_MAX = 4000;\nconst SQFT_MEDIAN = 1500;\n\nconst featureValues = Array.from(\n  { length: GRID_POINTS },\n  (_, i) => SQFT_MIN + (i * (SQFT_MAX - SQFT_MIN)) / (GRID_POINTS - 1),\n);\n\n// Diminishing-returns effect (log-shaped), in thousands of dollars, zeroed at\n// the median so the plot shows relative lift rather than absolute price.\nconst partialDependence = featureValues.map(\n  (sqft) => 62 * Math.log(sqft / SQFT_MEDIAN),\n);\n\n// Uncertainty widens away from the bulk of the training data (fewer nearby\n// samples at the tails), a standard PDP confidence-band shape.\nconst bandHalfWidth = featureValues.map((sqft) => {\n  const distance = Math.abs(sqft - SQFT_MEDIAN) / (SQFT_MAX - SQFT_MIN);\n  return 4 + 34 * distance * distance;\n});\nconst upperBound = partialDependence.map((pd, i) => pd + bandHalfWidth[i]);\nconst lowerBound = partialDependence.map((pd, i) => pd - bandHalfWidth[i]);\n\n// Rug: a small fixed-seed LCG stands in for the square-footage distribution\n// of the training sample, clustered around the median with a long right tail.\nlet seed = 42;\nconst lcg = () => {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n};\nconst trainingSqft = Array.from({ length: 90 }, () => {\n  const u1 = lcg() || 1e-9;\n  const u2 = lcg();\n  const gaussian = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  const sample = SQFT_MEDIAN + gaussian * 420 + 260;\n  return Math.min(SQFT_MAX - 20, Math.max(SQFT_MIN + 20, sample));\n}).sort((a, b) => a - b);\nconst rugPoints = trainingSqft.map((sqft) => ({ x: sqft, y: 0.08 }));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart --------------------------------------------------------------------\nconst title = \"House Price vs. Square Footage · pdp-basic · javascript · chartjs · anyplot.ai\";\n\n// Custom plugin: callout the zero-crossing (median sq ft, where partial\n// dependence is centered at $0) so the \"centered\" framing is explicit rather\n// than only inferable from the curve shape.\nconst zeroCrossingCallout = {\n  id: \"zeroCrossingCallout\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const px = scales.x.getPixelForValue(SQFT_MEDIAN);\n    const py = scales.y.getPixelForValue(0);\n    ctx.save();\n    ctx.setLineDash([4, 4]);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1.5;\n    ctx.beginPath();\n    ctx.moveTo(px, chartArea.top);\n    ctx.lineTo(px, chartArea.bottom);\n    ctx.stroke();\n    ctx.setLineDash([]);\n    ctx.beginPath();\n    ctx.arc(px, py, 6, 0, Math.PI * 2);\n    ctx.fillStyle = t.palette[0];\n    ctx.fill();\n    ctx.lineWidth = 2;\n    ctx.strokeStyle = t.pageBg;\n    ctx.stroke();\n    ctx.fillStyle = t.ink;\n    ctx.font = \"600 15px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(`Median: ${SQFT_MEDIAN.toLocaleString()} sq ft → $0`, px + 12, py - 10);\n    ctx.restore();\n  },\n};\n\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: \"Upper bound\",\n        data: featureValues.map((x, i) => ({ x, y: upperBound[i] })),\n        borderWidth: 0,\n        pointRadius: 0,\n        fill: false,\n        tension: 0.3,\n      },\n      {\n        label: \"95% confidence band\",\n        data: featureValues.map((x, i) => ({ x, y: lowerBound[i] })),\n        borderWidth: 0,\n        pointRadius: 0,\n        backgroundColor: `${t.palette[0]}26`,\n        fill: \"-1\",\n        tension: 0.3,\n      },\n      {\n        label: \"Partial dependence\",\n        data: featureValues.map((x, i) => ({ x, y: partialDependence[i] })),\n        borderColor: t.palette[0],\n        backgroundColor: t.palette[0],\n        borderWidth: 4,\n        pointRadius: 0,\n        fill: false,\n        tension: 0.3,\n      },\n      {\n        label: \"Training data (rug)\",\n        type: \"scatter\",\n        data: rugPoints,\n        yAxisID: \"rug\",\n        pointStyle: \"line\",\n        rotation: 90,\n        radius: 9,\n        borderColor: t.inkSoft,\n        borderWidth: 1.5,\n        showLine: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 14, right: 30, bottom: 14, left: 10 } },\n    plugins: {\n      title: { display: true, text: title, color: t.ink, font: { size: 21, weight: \"700\" } },\n      legend: {\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          padding: 20,\n          filter: (item) => item.text === \"Partial dependence\" || item.text === \"95% confidence band\",\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: SQFT_MIN,\n        max: SQFT_MAX,\n        title: { display: true, text: \"Living Area (sq ft)\", color: t.ink, font: { size: 18 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n      },\n      y: {\n        title: { display: true, text: \"Partial Dependence ($k, centered)\", color: t.ink, font: { size: 18 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n      rug: {\n        type: \"linear\",\n        position: \"left\",\n        display: false,\n        min: 0,\n        max: 1,\n      },\n    },\n  },\n  plugins: [zeroCrossingCallout],\n});\n"}