{"spec_id":"shap-waterfall","library":"muix","language":"javascript","code":"// anyplot.ai\n// shap-waterfall: SHAP Waterfall Plot for Feature Attribution\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-09\nimport { BarChart } from \"@mui/x-charts/BarChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A credit-scoring model's predicted probability of loan default for one\n// applicant, decomposed into per-feature SHAP contributions. Rows are ordered\n// by descending |SHAP value| (largest driver first), matching how SHAP\n// waterfalls are conventionally read from top to bottom.\nconst BASE_VALUE = 0.55; // expected default probability across the training population\nconst FINAL_VALUE = 0.135; // this applicant's actual predicted default probability\n\nconst ROWS = [\n  { feature: \"Credit Score\", shap: -0.14, start: 0.55, end: 0.41 },\n  { feature: \"Payment History\", shap: -0.09, start: 0.41, end: 0.32 },\n  { feature: \"Debt-to-Income Ratio\", shap: -0.07, start: 0.32, end: 0.25 },\n  { feature: \"Credit Utilization\", shap: -0.05, start: 0.25, end: 0.2 },\n  { feature: \"Income\", shap: -0.04, start: 0.2, end: 0.16 },\n  { feature: \"Account Age\", shap: -0.035, start: 0.16, end: 0.125 },\n  { feature: \"Employment Length\", shap: -0.03, start: 0.125, end: 0.095 },\n  { feature: \"Number of Open Accounts\", shap: 0.025, start: 0.095, end: 0.12 },\n  { feature: \"Recent Credit Inquiries\", shap: 0.02, start: 0.12, end: 0.14 },\n  { feature: \"Delinquencies (Past 2yr)\", shap: 0.015, start: 0.14, end: 0.155 },\n  { feature: \"Loan Purpose\", shap: -0.012, start: 0.155, end: 0.143 },\n  { feature: \"Home Ownership\", shap: -0.01, start: 0.143, end: 0.133 },\n  { feature: \"Public Records\", shap: 0.008, start: 0.133, end: 0.141 },\n  { feature: \"Existing Loan Count\", shap: -0.006, start: 0.141, end: 0.135 },\n];\n\n// MUI X BarChart has no native floating-bar mode, so each waterfall segment\n// is built from a transparent \"offset\" bar (raises the stack to the lower of\n// start/end) topped by a colored \"increase\" or \"decrease\" bar sized to the\n// SHAP magnitude — the standard stacked-bar waterfall technique.\nconst CATEGORIES = ROWS.map((r) => r.feature);\nconst OFFSET = ROWS.map((r) => Math.min(r.start, r.end));\nconst INCREASE = ROWS.map((r) => (r.shap > 0 ? Math.abs(r.end - r.start) : 0));\nconst DECREASE = ROWS.map((r) => (r.shap < 0 ? Math.abs(r.end - r.start) : 0));\n\nconst RISK_UP = t.palette[4]; // matte red — positive SHAP, pushes default risk up\nconst RISK_DOWN = t.palette[2]; // blue — negative SHAP, pushes default risk down\n\nfunction formatSigned(value: number): string {\n  return `${value > 0 ? \"+\" : \"\"}${value.toFixed(3)}`;\n}\n\n// Connects the end of each bar to the start of the next — same x value, one\n// row apart — to make the cumulative \"staircase\" flow from base to final\n// value easy to trace, per the spec's \"connector line\" suggestion.\nfunction ConnectorLines() {\n  const xScale = useXScale() as any;\n  const yScale = useYScale() as any;\n  if (!xScale || !yScale) return null;\n  const bandwidth = yScale.bandwidth ? yScale.bandwidth() : 0;\n\n  return (\n    <g>\n      {ROWS.slice(0, -1).map((row, i) => {\n        const next = ROWS[i + 1];\n        const x = xScale(row.end);\n        const y1 = (yScale(row.feature) ?? 0) + bandwidth / 2;\n        const y2 = (yScale(next.feature) ?? 0) + bandwidth / 2;\n        return (\n          <line\n            key={row.feature}\n            x1={x}\n            y1={y1}\n            x2={x}\n            y2={y2}\n            stroke={t.inkSoft}\n            strokeWidth={1.25}\n            strokeDasharray=\"3 3\"\n            strokeOpacity={0.6}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"Loan Default Risk · shap-waterfall · javascript · muix · anyplot.ai\";\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const titleSize = TITLE.length > 67 ? Math.max(14, Math.round((22 * 67) / TITLE.length)) : 22;\n\n  const TITLE_H = 64;\n  const LEGEND_H = 52;\n  const chartH = H - TITLE_H - LEGEND_H;\n\n  return (\n    <Box\n      sx={{\n        width: W,\n        height: H,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        fontFamily: \"'Roboto', 'Helvetica Neue', Arial, sans-serif\",\n        boxSizing: \"border-box\",\n      }}\n    >\n      <Box sx={{ height: TITLE_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <Typography sx={{ color: t.ink, fontSize: titleSize, fontWeight: 600 }}>{TITLE}</Typography>\n      </Box>\n\n      <BarChart\n        width={W}\n        height={chartH}\n        layout=\"horizontal\"\n        skipAnimation\n        margin={{ top: 16, right: 64, bottom: 64, left: 210 }}\n        series={[\n          { id: \"offset\", data: OFFSET, stack: \"waterfall\", color: \"transparent\" },\n          {\n            id: \"increase\",\n            label: \"Increases risk\",\n            data: INCREASE,\n            stack: \"waterfall\",\n            color: RISK_UP,\n            valueFormatter: (v, ctx) => (v ? formatSigned(ROWS[ctx.dataIndex].shap) : null),\n          },\n          {\n            id: \"decrease\",\n            label: \"Decreases risk\",\n            data: DECREASE,\n            stack: \"waterfall\",\n            color: RISK_DOWN,\n            valueFormatter: (v, ctx) => (v ? formatSigned(ROWS[ctx.dataIndex].shap) : null),\n          },\n        ]}\n        xAxis={[\n          {\n            min: 0,\n            max: 0.7,\n            label: \"Predicted Default Probability\",\n            valueFormatter: (v: number) => v.toFixed(2),\n            tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n            labelStyle: { fontSize: 15, fill: t.ink },\n          },\n        ]}\n        yAxis={[\n          {\n            scaleType: \"band\",\n            data: CATEGORIES,\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          },\n        ]}\n        grid={{ vertical: true }}\n        barLabel={(item, context) => {\n          if (item.seriesId === \"offset\" || item.value == null || item.value === 0) return null;\n          if (context.bar.width < 30) return null;\n          return formatSigned(ROWS[item.dataIndex].shap);\n        }}\n        slotProps={{\n          legend: { hidden: true },\n          barLabel: { style: { fill: \"#FFFFFF\", fontSize: 12, fontWeight: 600 } },\n        }}\n        sx={{\n          \"& .MuiChartsAxis-line\": { stroke: t.inkSoft, strokeOpacity: 0.25 },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid },\n        }}\n      >\n        <ChartsReferenceLine\n          x={BASE_VALUE}\n          label={`Base rate ${BASE_VALUE.toFixed(2)}`}\n          labelAlign=\"start\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"5 4\", strokeWidth: 1.25, strokeOpacity: 0.6 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n        />\n        <ChartsReferenceLine\n          x={FINAL_VALUE}\n          label={`Prediction ${FINAL_VALUE.toFixed(3)}`}\n          labelAlign=\"start\"\n          spacing={{ x: 8, y: 26 }}\n          lineStyle={{ stroke: t.ink, strokeDasharray: \"5 4\", strokeWidth: 1.25, strokeOpacity: 0.6 }}\n          labelStyle={{ fill: t.ink, fontSize: 13, fontWeight: 600 }}\n        />\n        <ConnectorLines />\n      </BarChart>\n\n      <Box sx={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: \"28px\" }}>\n        {[\n          { label: \"Increases risk\", color: RISK_UP },\n          { label: \"Decreases risk\", color: RISK_DOWN },\n        ].map((entry) => (\n          <Box key={entry.label} sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n            <Box sx={{ width: 14, height: 14, borderRadius: \"3px\", bgcolor: entry.color }} />\n            <Typography sx={{ color: t.inkSoft, fontSize: 13, fontWeight: 500 }}>{entry.label}</Typography>\n          </Box>\n        ))}\n      </Box>\n    </Box>\n  );\n}\n"}