{"spec_id":"shap-waterfall","library":"echarts","language":"javascript","code":"// anyplot.ai\n// shap-waterfall: SHAP Waterfall Plot for Feature Attribution\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Credit-scoring model explaining one applicant's predicted default probability.\nconst baseValue = 0.32;\nconst finalValue = 0.385;\nconst featureContributions = [\n  { feature: \"Credit Score\", shap: -0.22 },\n  { feature: \"Recent Late Payments\", shap: 0.15 },\n  { feature: \"Debt-to-Income Ratio\", shap: 0.11 },\n  { feature: \"Credit Utilization\", shap: 0.08 },\n  { feature: \"Income\", shap: -0.06 },\n  { feature: \"Employment Length\", shap: -0.04 },\n  { feature: \"Loan Amount\", shap: 0.03 },\n  { feature: \"Number of Open Accounts\", shap: 0.02 },\n  { feature: \"Age\", shap: -0.015 },\n  { feature: \"Existing Loans\", shap: 0.01 },\n];\n\n// Smallest |SHAP| first so the largest contribution sits nearest the top,\n// right below the final-prediction bar (categories plot bottom-to-top).\nconst orderedFeatures = [...featureContributions].sort(\n  (a, b) => Math.abs(a.shap) - Math.abs(b.shap)\n);\n\n// Build the cumulative waterfall: each row knows the invisible \"placeholder\"\n// offset it stacks on top of, its own visible bar length, and the running\n// total once it has been applied (used to draw the connecting flow line).\nconst rows = [\n  {\n    name: \"Base value\",\n    placeholder: 0,\n    value: baseValue,\n    after: baseValue,\n    kind: \"anchor\",\n    label: `Base: ${baseValue.toFixed(3)}`,\n  },\n];\nlet running = baseValue;\norderedFeatures.forEach(({ feature, shap }) => {\n  const start = running;\n  const end = running + shap;\n  rows.push({\n    name: feature,\n    placeholder: Math.min(start, end),\n    value: Math.abs(shap),\n    after: end,\n    kind: shap >= 0 ? \"increase\" : \"decrease\",\n    label: `${shap >= 0 ? \"+\" : \"−\"}${Math.abs(shap).toFixed(3)}`,\n  });\n  running = end;\n});\nrows.push({\n  name: \"Final prediction\",\n  placeholder: 0,\n  value: finalValue,\n  after: finalValue,\n  kind: \"anchor\",\n  label: `Final: ${finalValue.toFixed(3)}`,\n});\n\nconst categories = rows.map((r) => r.name);\nconst placeholderData = rows.map((r) => r.placeholder);\nconst increaseData = rows.map((r) => (r.kind === \"increase\" ? r.value : \"-\"));\nconst decreaseData = rows.map((r) => (r.kind === \"decrease\" ? r.value : \"-\"));\nconst anchorData = rows.map((r) => (r.kind === \"anchor\" ? r.value : \"-\"));\nconst flowData = rows.map((r) => r.after);\n\nconst barLabel = {\n  show: true,\n  position: \"right\",\n  color: t.inkSoft,\n  fontSize: 14,\n  formatter: (params) => rows[params.dataIndex].label,\n};\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"shap-waterfall · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 18,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    data: [\"Increases prediction\", \"Decreases prediction\", \"Base / final value\"],\n    top: 60,\n    left: \"center\",\n    itemWidth: 16,\n    itemHeight: 16,\n    textStyle: { color: t.ink, fontSize: 15 },\n  },\n  grid: { left: 40, right: 150, top: 125, bottom: 70, containLabel: true },\n  xAxis: {\n    type: \"value\",\n    min: 0,\n    max: 0.75,\n    name: \"Predicted default probability\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: (v) => v.toFixed(2) },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"category\",\n    data: categories,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  series: [\n    {\n      name: \"placeholder\",\n      type: \"bar\",\n      stack: \"flow\",\n      silent: true,\n      barWidth: 34,\n      itemStyle: { color: \"transparent\" },\n      data: placeholderData,\n      tooltip: { show: false },\n    },\n    {\n      name: \"Increases prediction\",\n      type: \"bar\",\n      stack: \"flow\",\n      barWidth: 34,\n      itemStyle: { color: t.palette[4] },\n      label: barLabel,\n      data: increaseData,\n    },\n    {\n      name: \"Decreases prediction\",\n      type: \"bar\",\n      stack: \"flow\",\n      barWidth: 34,\n      itemStyle: { color: t.palette[2] },\n      label: barLabel,\n      data: decreaseData,\n    },\n    {\n      name: \"Base / final value\",\n      type: \"bar\",\n      stack: \"flow\",\n      barWidth: 34,\n      itemStyle: { color: t.ink, opacity: 0.85 },\n      label: barLabel,\n      data: anchorData,\n    },\n    {\n      name: \"flow\",\n      type: \"line\",\n      data: flowData,\n      symbol: \"none\",\n      silent: true,\n      z: 3,\n      lineStyle: { type: \"dashed\", width: 1.5, color: t.inkSoft, opacity: 0.55 },\n      tooltip: { show: false },\n    },\n  ],\n});\n"}