{"spec_id":"shap-waterfall","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// shap-waterfall: SHAP Waterfall Plot for Feature Attribution\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst POSITIVE_COLOR = t.palette[4]; // #AE3030 matte red (Imprint) — SHAP convention: pushes prediction up\nconst NEGATIVE_COLOR = t.palette[2]; // #4467A3 blue (Imprint) — SHAP convention: pushes prediction down\nconst TOTAL_COLOR = t.ink; // neutral — theme-adaptive baseline/total anchor\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Loan-approval model: SHAP attribution for a single applicant's predicted\n// approval probability, ordered by descending absolute contribution.\nconst BASE_VALUE = 45.0;\nconst contributions = [\n  { feature: \"Income\", shap: 18.5 },\n  { feature: \"Missed Payments (12mo)\", shap: -14.2 },\n  { feature: \"Credit Score\", shap: 12.8 },\n  { feature: \"Debt-to-Income Ratio\", shap: -9.6 },\n  { feature: \"Credit History Length\", shap: 7.3 },\n  { feature: \"Recent Credit Inquiries\", shap: -6.1 },\n  { feature: \"Employment Length\", shap: 5.4 },\n  { feature: \"Existing Loans\", shap: -4.2 },\n  { feature: \"Savings Balance\", shap: 3.1 },\n  { feature: \"Loan Amount\", shap: -2.5 },\n  { feature: \"Open Accounts\", shap: -1.8 },\n  { feature: \"Age\", shap: 0.9 },\n];\nconst FINAL_VALUE = contributions.reduce((sum, c) => sum + c.shap, BASE_VALUE);\n\n// Build cumulative waterfall rows: a leading \"Base value\" total, one floating\n// segment per feature (already sorted by |shap| descending), a trailing\n// \"Final prediction\" total.\nlet running = BASE_VALUE;\nconst rows = [{ label: \"Base value\", start: 0, end: BASE_VALUE, kind: \"total\", displayValue: BASE_VALUE }];\nfor (const { feature, shap } of contributions) {\n  const start = running;\n  running += shap;\n  rows.push({\n    label: feature,\n    start,\n    end: running,\n    kind: shap >= 0 ? \"positive\" : \"negative\",\n    displayValue: shap,\n  });\n}\nrows.push({ label: \"Final prediction\", start: 0, end: FINAL_VALUE, kind: \"total\", displayValue: FINAL_VALUE });\n\nconst rowColor = (row) =>\n  row.kind === \"total\" ? TOTAL_COLOR : row.kind === \"positive\" ? POSITIVE_COLOR : NEGATIVE_COLOR;\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: cumulative-flow connectors + per-bar value labels --------\n// (native Chart.js plugin API — plain canvas drawing, no external package)\nconst waterfallAnnotations = {\n  id: \"shapWaterfallAnnotations\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const xScale = scales.x;\n\n    ctx.save();\n    ctx.setLineDash([5, 4]);\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 1.5;\n    for (let i = 0; i < rows.length - 1; i++) {\n      const barA = meta.data[i].getProps([\"y\", \"height\"], true);\n      const barB = meta.data[i + 1].getProps([\"y\", \"height\"], true);\n      const xPix = xScale.getPixelForValue(rows[i].end);\n      ctx.beginPath();\n      ctx.moveTo(xPix, barA.y + barA.height / 2);\n      ctx.lineTo(xPix, barB.y - barB.height / 2);\n      ctx.stroke();\n    }\n    ctx.setLineDash([]);\n\n    ctx.font = `600 15px ${Chart.defaults.font.family}`;\n    ctx.textBaseline = \"middle\";\n    ctx.textAlign = \"left\";\n    rows.forEach((row, i) => {\n      const bar = meta.data[i].getProps([\"y\"], true);\n      const rightEdge = Math.max(xScale.getPixelForValue(row.start), xScale.getPixelForValue(row.end));\n      if (row.kind === \"total\") {\n        ctx.fillStyle = t.ink;\n        ctx.fillText(`${row.displayValue.toFixed(1)}%`, rightEdge + 10, bar.y);\n      } else {\n        const sign = row.displayValue > 0 ? \"+\" : \"−\";\n        ctx.fillStyle = t.inkSoft;\n        ctx.fillText(`${sign}${Math.abs(row.displayValue).toFixed(1)}`, rightEdge + 10, bar.y);\n      }\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: rows.map((r) => r.label),\n    datasets: [\n      {\n        data: rows.map((r) => [r.start, r.end]),\n        backgroundColor: rows.map(rowColor),\n        borderRadius: 4,\n        borderSkipped: false,\n        barPercentage: 0.6,\n        categoryPercentage: 0.85,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: 60 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"shap-waterfall · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        position: \"top\",\n        onClick: () => {},\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          generateLabels: () => [\n            { text: \"Positive contribution\", fillStyle: POSITIVE_COLOR, strokeStyle: POSITIVE_COLOR, index: 0 },\n            { text: \"Negative contribution\", fillStyle: NEGATIVE_COLOR, strokeStyle: NEGATIVE_COLOR, index: 1 },\n            { text: \"Base value / prediction\", fillStyle: TOTAL_COLOR, strokeStyle: TOTAL_COLOR, index: 2 },\n          ],\n        },\n      },\n      tooltip: {\n        callbacks: {\n          title: () => \"\",\n          label: (ctx) => {\n            const row = rows[ctx.dataIndex];\n            if (row.kind === \"total\") return `${row.label}: ${row.displayValue.toFixed(1)}%`;\n            const sign = row.displayValue > 0 ? \"+\" : \"\";\n            return `${row.label}: ${sign}${row.displayValue.toFixed(1)} pts`;\n          },\n        },\n      },\n    },\n    scales: {\n      x: {\n        min: 0,\n        max: Math.ceil((Math.max(...rows.map((r) => Math.max(r.start, r.end))) * 1.1) / 10) * 10,\n        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${v}%` },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Predicted Approval Probability\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n      },\n    },\n  },\n  plugins: [waterfallAnnotations],\n});\n"}