{"spec_id":"shap-summary","library":"d3","language":"javascript","code":"// anyplot.ai\n// shap-summary: SHAP Summary Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 190, bottom: 90, left: 250 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic PRNG (LCG + Box-Muller) -----------------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randNorm() {\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: SHAP values explaining a used-car resale-price model -------------\n// Each feature has a raw value distribution and a linear-ish relationship to\n// its SHAP contribution (impact on predicted price, USD), plus noise so the\n// swarm shows realistic spread rather than a clean line.\nconst N_SAMPLES = 170;\n\nconst featureSpecs = [\n  { label: \"Mileage (1,000 km)\", mean: 65, sd: 26, min: 5, slope: -34, noiseSd: 480 },\n  { label: \"Car Age (years)\", mean: 6, sd: 3.2, min: 0, slope: -410, noiseSd: 560 },\n  { label: \"Prior Accidents\", mean: 0.28, sd: 0.45, min: 0, max: 1, binary: true, effectTrue: -2550, effectFalse: 380, noiseSd: 430 },\n  { label: \"Engine Size (L)\", mean: 2.2, sd: 0.7, min: 1.0, slope: 940, noiseSd: 470 },\n  { label: \"Brand Prestige Score\", mean: 5.5, sd: 2.0, min: 1, max: 10, slope: 470, noiseSd: 510 },\n  { label: \"Horsepower (hp)\", mean: 180, sd: 55, min: 70, slope: 8.6, noiseSd: 460 },\n  { label: \"Fuel Efficiency (km/L)\", mean: 14, sd: 3.4, min: 5, slope: 90, noiseSd: 520 },\n  { label: \"Previous Owners\", mean: 2.1, sd: 1.1, min: 0, max: 6, slope: -240, noiseSd: 480 },\n];\n\nconst features = featureSpecs.map((spec) => {\n  const values = [];\n  const shaps = [];\n  for (let i = 0; i < N_SAMPLES; i++) {\n    let value;\n    let shap;\n    if (spec.binary) {\n      value = rand() < spec.mean ? 1 : 0;\n      shap = (value ? spec.effectTrue : spec.effectFalse) + randNorm() * spec.noiseSd;\n    } else {\n      value = spec.mean + randNorm() * spec.sd;\n      if (spec.min !== undefined) value = Math.max(spec.min, value);\n      if (spec.max !== undefined) value = Math.min(spec.max, value);\n      shap = spec.slope * (value - spec.mean) + randNorm() * spec.noiseSd;\n    }\n    values.push(value);\n    shaps.push(shap);\n  }\n  const vMin = d3.min(values);\n  const vMax = d3.max(values);\n  const meanAbsShap = d3.mean(shaps, (s) => Math.abs(s));\n  return {\n    label: spec.label,\n    samples: values.map((value, i) => ({\n      value,\n      shap: shaps[i],\n      norm: vMax > vMin ? (value - vMin) / (vMax - vMin) : 0.5,\n    })),\n    meanAbsShap,\n  };\n});\n\n// Most important feature first (top row).\nfeatures.sort((a, b) => b.meanAbsShap - a.meanAbsShap);\n\n// --- Scales -------------------------------------------------------------------\nconst allShaps = features.flatMap((f) => f.samples.map((s) => s.shap));\nconst shapExtent = d3.extent(allShaps);\nconst shapPad = (shapExtent[1] - shapExtent[0]) * 0.06;\nconst x = d3.scaleLinear()\n  .domain([shapExtent[0] - shapPad, shapExtent[1] + shapPad])\n  .nice()\n  .range([0, iw]);\n\nconst y = d3.scaleBand()\n  .domain(features.map((f) => f.label))\n  .range([0, ih])\n  .paddingInner(0.35);\n\nconst colorFor = d3.interpolateRgbBasis(t.seq); // low = brand green, high = blue\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// Feature-value color legend gradient (defined once, used by the legend bar).\nconst grad = svg.append(\"defs\").append(\"linearGradient\")\n  .attr(\"id\", \"shap-value-gradient\")\n  .attr(\"x1\", \"0%\").attr(\"y1\", \"100%\").attr(\"x2\", \"0%\").attr(\"y2\", \"0%\");\nd3.range(0, 1.0001, 0.1).forEach((p) => {\n  grad.append(\"stop\").attr(\"offset\", `${p * 100}%`).attr(\"stop-color\", colorFor(p));\n});\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Zero reference line -----------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(0)).attr(\"x2\", x(0))\n  .attr(\"y1\", -12).attr(\"y2\", ih + 12)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"5,5\");\n\n// --- Beeswarm rows ------------------------------------------------------------\nconst rowH = y.bandwidth();\nconst radius = 4.4;\nconst halfSpan = rowH / 2 - radius - 2;\nconst step = radius * 1.15;\nconst maxSteps = Math.max(1, Math.floor(halfSpan / step));\n\nfunction layoutSwarm(samples) {\n  // Greedy 1-D beeswarm: place points ordered by x, nudging each vertically\n  // away from the row centerline until it clears every already-placed point.\n  const withX = samples.map((s) => ({ ...s, px: x(s.shap) })).sort((a, b) => a.px - b.px);\n  const placed = [];\n  for (const p of withX) {\n    let chosen = 0;\n    let found = false;\n    for (let k = 0; k <= maxSteps && !found; k++) {\n      const candidates = k === 0 ? [0] : [k * step, -k * step];\n      for (const dy of candidates) {\n        const clash = placed.some((q) => {\n          const dx = q.px - p.px;\n          const ddy = q.dy - dy;\n          return Math.sqrt(dx * dx + ddy * ddy) < radius * 2 - 0.4;\n        });\n        if (!clash) {\n          chosen = dy;\n          found = true;\n          break;\n        }\n      }\n    }\n    p.dy = found ? chosen : (halfSpan * (placed.length % 2 === 0 ? 1 : -1));\n    placed.push(p);\n  }\n  return placed;\n}\n\nconst rows = g.selectAll(\".feature-row\")\n  .data(features)\n  .join(\"g\")\n  .attr(\"class\", \"feature-row\")\n  .attr(\"transform\", (f) => `translate(0,${y(f.label) + rowH / 2})`);\n\nrows.each(function (f) {\n  const points = layoutSwarm(f.samples);\n  d3.select(this)\n    .selectAll(\"circle\")\n    .data(points)\n    .join(\"circle\")\n    .attr(\"cx\", (p) => p.px)\n    .attr(\"cy\", (p) => p.dy)\n    .attr(\"r\", radius)\n    .attr(\"fill\", (p) => colorFor(p.norm))\n    .attr(\"fill-opacity\", 0.82)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 0.4);\n});\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(7).tickSize(6));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickSize(0));\nyAxis.select(\".domain\").remove();\nyAxis.selectAll(\"text\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .attr(\"dx\", \"-4px\");\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"SHAP value (impact on predicted resale price, USD)\");\n\n// --- Color legend (feature value: low -> high) --------------------------------\nconst legendX = iw + 60;\nconst legendH = Math.min(ih * 0.55, 320);\nconst legendY = (ih - legendH) / 2;\n\ng.append(\"rect\")\n  .attr(\"x\", legendX).attr(\"y\", legendY)\n  .attr(\"width\", 16).attr(\"height\", legendH)\n  .attr(\"fill\", \"url(#shap-value-gradient)\")\n  .attr(\"rx\", 3);\n\ng.append(\"text\")\n  .attr(\"x\", legendX + 26).attr(\"y\", legendY + 6)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text(\"High\");\n\ng.append(\"text\")\n  .attr(\"x\", legendX + 26).attr(\"y\", legendY + legendH)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text(\"Low\");\n\ng.append(\"text\")\n  .attr(\"x\", legendX - 2).attr(\"y\", legendY - 22)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Feature\");\ng.append(\"text\")\n  .attr(\"x\", legendX - 2).attr(\"y\", legendY - 6)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"value\");\n\n// --- Title ----------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Used Car Resale Price · shap-summary · javascript · d3 · anyplot.ai\");\n\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 76)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Features ranked by mean |SHAP value| — gradient boosting model, n = \" + N_SAMPLES + \" samples\");\n"}