{"spec_id":"shap-waterfall","library":"d3","language":"javascript","code":"// anyplot.ai\n// shap-waterfall: SHAP Waterfall Plot for Feature Attribution\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Data — SHAP contributions for a single loan-default-risk prediction,\n// ordered by absolute magnitude (largest contribution first / at top).\nconst baseValue = 0.32;\nconst contributions = [\n  { feature: \"Credit score (620)\", shapValue: -0.18 },\n  { feature: \"Debt-to-income ratio (48%)\", shapValue: 0.14 },\n  { feature: \"Recent credit inquiries (5)\", shapValue: 0.09 },\n  { feature: \"Employment length (1.5 yrs)\", shapValue: 0.07 },\n  { feature: \"Credit history length (3 yrs)\", shapValue: 0.05 },\n  { feature: \"Late payments, 12mo (2)\", shapValue: 0.04 },\n  { feature: \"Annual income ($42k)\", shapValue: -0.03 },\n  { feature: \"Loan amount ($15k)\", shapValue: 0.02 },\n  { feature: \"Age (29)\", shapValue: -0.015 },\n  { feature: \"Existing debt ($8k)\", shapValue: 0.01 },\n];\n\nlet cursor = baseValue;\nconst rows = contributions.map((d) => {\n  const start = cursor;\n  const end = cursor + d.shapValue;\n  cursor = end;\n  return { ...d, start, end };\n});\nconst finalValue = cursor;\n\n// Plot\nconst margin = { top: 185, right: 90, bottom: 100, left: 320 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\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\nconst allValues = [baseValue, finalValue, ...rows.flatMap((r) => [r.start, r.end])];\nconst domainMin = d3.min(allValues);\nconst domainMax = d3.max(allValues);\nconst domainPad = (domainMax - domainMin) * 0.18;\nconst x = d3.scaleLinear()\n  .domain([domainMin - domainPad, domainMax + domainPad])\n  .nice()\n  .range([0, iw]);\n\nconst y = d3.scaleBand()\n  .domain(rows.map((d) => d.feature))\n  .range([0, ih])\n  .padding(0.38);\n\nconst POSITIVE = t.palette[4]; // matte red — increases predicted risk\nconst NEGATIVE = t.palette[2]; // blue — decreases predicted risk\n\n// Style — vertical gridlines aligned to the x-axis ticks\ng.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickSize(-ih).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// Connector lines linking each bar's end to the next bar's start\nfor (let i = 0; i < rows.length - 1; i++) {\n  g.append(\"line\")\n    .attr(\"x1\", x(rows[i].end))\n    .attr(\"x2\", x(rows[i].end))\n    .attr(\"y1\", y(rows[i].feature) + y.bandwidth())\n    .attr(\"y2\", y(rows[i + 1].feature))\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-dasharray\", \"4,4\");\n}\n\n// Base-value and final-prediction reference lines\ng.append(\"line\")\n  .attr(\"x1\", x(baseValue)).attr(\"x2\", x(baseValue))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 2).attr(\"stroke-dasharray\", \"6,5\");\n\ng.append(\"line\")\n  .attr(\"x1\", x(finalValue)).attr(\"x2\", x(finalValue))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.ink).attr(\"stroke-width\", 2.5);\n\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + x(baseValue)).attr(\"y\", 145)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(`Base value  E[f(x)] = ${baseValue.toFixed(3)}`);\n\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + x(finalValue)).attr(\"y\", 168)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-weight\", \"600\")\n  .style(\"font-size\", \"15px\")\n  .text(`Predicted risk  f(x) = ${finalValue.toFixed(3)}`);\n\n// Waterfall bars\ng.selectAll(\"rect.bar\").data(rows).join(\"rect\").attr(\"class\", \"bar\")\n  .attr(\"x\", (d) => x(Math.min(d.start, d.end)))\n  .attr(\"y\", (d) => y(d.feature))\n  .attr(\"width\", (d) => Math.abs(x(d.end) - x(d.start)))\n  .attr(\"height\", y.bandwidth())\n  .attr(\"fill\", (d) => (d.shapValue >= 0 ? POSITIVE : NEGATIVE));\n\n// Numeric SHAP value beside each bar segment\ng.selectAll(\"text.value\").data(rows).join(\"text\").attr(\"class\", \"value\")\n  .attr(\"x\", (d) => x(d.end) + (d.shapValue >= 0 ? 10 : -10))\n  .attr(\"y\", (d) => y(d.feature) + y.bandwidth() / 2)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", (d) => (d.shapValue >= 0 ? \"start\" : \"end\"))\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text((d) => `${d.shapValue >= 0 ? \"+\" : \"\"}${d.shapValue.toFixed(3)}`);\n\n// Axes\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickFormat(d3.format(\".2f\")));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickSize(0));\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\nyAxis.select(\".domain\").remove();\n\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2).attr(\"y\", height - 34)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Predicted probability of loan default\");\n\n// Legend\nconst legendItems = [\n  { label: \"Increases risk\", color: POSITIVE },\n  { label: \"Decreases risk\", color: NEGATIVE },\n];\nconst legend = svg.append(\"g\")\n  .attr(\"transform\", `translate(${width - margin.right - 260},${100})`);\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(${i * 150},0)`);\n  row.append(\"rect\").attr(\"width\", 20).attr(\"height\", 20).attr(\"rx\", 3).attr(\"fill\", item.color);\n  row.append(\"text\").attr(\"x\", 28).attr(\"y\", 15).attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\").text(item.label);\n});\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"Loan Default Risk · shap-waterfall · javascript · d3 · anyplot.ai\");\n\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 85)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Feature attribution for loan application #4821\");\n"}