shap-svg 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -271,8 +271,34 @@ function formatFeatureLabel(name) {
271
271
  return name.replace(/_/g, " ");
272
272
  }
273
273
 
274
+ // src/core/labels.ts
275
+ var shapLabels = {
276
+ shapValue: "SHAP value",
277
+ shapValueAxis: "SHAP value (impact on model output)",
278
+ meanAbsShapValue: "mean(|SHAP value|)",
279
+ featureValue: "Feature value",
280
+ featureValueLow: "Low",
281
+ featureValueHigh: "High",
282
+ missingFeatureValue: "missing",
283
+ samples: "Instances",
284
+ sampleTotal: "\u03A3\u03C6",
285
+ sampleFallback: (sampleNumber) => `Sample ${sampleNumber}`,
286
+ baseValue: "E[f(X)]",
287
+ modelOutput: "f(x)",
288
+ otherFeatures: (count, style) => style === "sum" ? `Sum of ${count} other features` : `${count} other features`
289
+ };
290
+ function resolveLabels(labels) {
291
+ if (!labels) return shapLabels;
292
+ const resolved = { ...shapLabels };
293
+ for (const key of Object.keys(labels)) {
294
+ const value = labels[key];
295
+ if (value !== void 0) resolved[key] = value;
296
+ }
297
+ return resolved;
298
+ }
299
+
274
300
  // src/core/collapse.ts
275
- function collapseToDisplay(featureNames, importance, order, maxDisplay, faithfulOtherRow) {
301
+ function collapseToDisplay(featureNames, importance, order, maxDisplay, faithfulOtherRow, labels = shapLabels) {
276
302
  const p = order.length;
277
303
  if (maxDisplay >= p) {
278
304
  return {
@@ -295,7 +321,7 @@ function collapseToDisplay(featureNames, importance, order, maxDisplay, faithful
295
321
  const collapsed = order.slice(realCount);
296
322
  const collapsedValue = collapsed.reduce((sum, index) => sum + importance[index], 0);
297
323
  rows.push({
298
- label: faithfulOtherRow ? `Sum of ${collapsed.length} other features` : `${collapsed.length} other features`,
324
+ label: labels.otherFeatures(collapsed.length, faithfulOtherRow ? "sum" : "count"),
299
325
  featureIndex: null,
300
326
  value: collapsedValue,
301
327
  isOtherRow: true
@@ -364,15 +390,15 @@ var AXIS_HEIGHT = 52;
364
390
  var TICK_LABEL_PT = 11;
365
391
  var TITLE_PT = 13;
366
392
  var X_MARGIN = 0.05;
367
- function barXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
393
+ function barXAxis(min, max, toX, marginLeft, plotWidth, plotBottom, title) {
368
394
  const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT));
369
395
  return {
370
396
  xTicks: ticks.map((value) => ({ value, x: toX(value), label: tickLabel(value, step) })),
371
397
  xSpine: { x1: marginLeft, x2: marginLeft + plotWidth, y: plotBottom },
372
398
  xTitle: {
373
- // _bar.py:143-150 builds this from the Explanation's transform history:
374
- // "SHAP value" -> "|SHAP value|" -> "mean(|SHAP value|)".
375
- text: "mean(|SHAP value|)",
399
+ // _bar.py:143-150 builds the default from the Explanation's transform
400
+ // history: "SHAP value" -> "|SHAP value|" -> "mean(|SHAP value|)".
401
+ text: title,
376
402
  x: marginLeft + plotWidth / 2,
377
403
  y: plotBottom + AXIS_TITLE_DY,
378
404
  fontSize: TITLE_PT
@@ -380,6 +406,7 @@ function barXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
380
406
  };
381
407
  }
382
408
  function barLayout(rows, opts) {
409
+ var _a;
383
410
  const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
384
411
  const plotWidth = width - marginLeft - marginRight;
385
412
  const values = rows.rows.map((r) => r.value);
@@ -420,7 +447,15 @@ function barLayout(rows, opts) {
420
447
  xZero,
421
448
  plotWidth,
422
449
  plotBottom: marginTop + rows.rows.length * rowHeight,
423
- ...barXAxis(min, max, toX, marginLeft, plotWidth, marginTop + rows.rows.length * rowHeight),
450
+ ...barXAxis(
451
+ min,
452
+ max,
453
+ toX,
454
+ marginLeft,
455
+ plotWidth,
456
+ marginTop + rows.rows.length * rowHeight,
457
+ ((_a = opts.labels) != null ? _a : shapLabels).meanAbsShapValue
458
+ ),
424
459
  zeroLine: { x: xZero, y1: marginTop, y2: marginTop + rows.rows.length * rowHeight },
425
460
  height: marginTop + rows.rows.length * rowHeight + AXIS_HEIGHT
426
461
  };
@@ -495,9 +530,11 @@ function ShapBar({
495
530
  classIndex = 1,
496
531
  width = 720,
497
532
  rowHeight = 26,
533
+ labels,
498
534
  onFeatureClick
499
535
  }) {
500
536
  const [hovered, setHovered] = (0, import_react.useState)(null);
537
+ const words = (0, import_react.useMemo)(() => resolveLabels(labels), [labels]);
501
538
  const layout = (0, import_react.useMemo)(() => {
502
539
  const raw = parseExplanation(explanation, { classIndex });
503
540
  const parsed = groupByGenus2 ? groupExplanationByGenus(raw) : raw;
@@ -508,23 +545,25 @@ function ShapBar({
508
545
  importance,
509
546
  order,
510
547
  maxDisplay,
511
- faithfulOtherRow
548
+ faithfulOtherRow,
549
+ words
512
550
  );
513
551
  return barLayout(rows, {
514
552
  width,
515
553
  rowHeight,
516
554
  marginLeft: 260,
517
555
  marginRight: 90,
518
- marginTop: 8
556
+ marginTop: 8,
557
+ labels: words
519
558
  });
520
- }, [groupByGenus2, explanation, maxDisplay, faithfulOtherRow, classIndex, width, rowHeight]);
559
+ }, [groupByGenus2, explanation, maxDisplay, faithfulOtherRow, classIndex, width, rowHeight, words]);
521
560
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
522
561
  "svg",
523
562
  {
524
563
  width,
525
564
  height: layout.height,
526
565
  role: "img",
527
- "aria-label": "Mean absolute SHAP value per feature",
566
+ "aria-label": `Mean absolute ${words.shapValue} per feature`,
528
567
  children: [
529
568
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
530
569
  "line",
@@ -1284,12 +1323,14 @@ var BEESWARM_MISSING_COLOR = "#777777";
1284
1323
  var BEESWARM_ROW_HEIGHT = 0.4;
1285
1324
  var NBINS = 100;
1286
1325
  var AXIS_HEIGHT2 = 52;
1287
- var COLOR_BAR = {
1288
- colormap: "red_blue",
1289
- tickLabels: ["Low", "High"],
1290
- label: "Feature value",
1291
- labelPad: 0
1292
- };
1326
+ function colorBarSpec(labels) {
1327
+ return {
1328
+ colormap: "red_blue",
1329
+ tickLabels: [labels.featureValueLow, labels.featureValueHigh],
1330
+ label: labels.featureValue,
1331
+ labelPad: 0
1332
+ };
1333
+ }
1293
1334
  var COLOR_BAR_GAP_RATIO = 0.05 / 0.8;
1294
1335
  var TICK_LABEL_PT3 = 11;
1295
1336
  var TITLE_PT2 = 13;
@@ -1355,7 +1396,7 @@ function spreadPoints(xs, rowIndex, seed) {
1355
1396
  const scale = 0.9 * (BEESWARM_ROW_HEIGHT / (maxPositiveOffset + 1));
1356
1397
  return offsets.map((offset) => rowIndex + offset * scale);
1357
1398
  }
1358
- function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSort = "importance") {
1399
+ function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSort = "importance", labels = shapLabels) {
1359
1400
  if (!Number.isInteger(maxDisplay) || maxDisplay <= 0) {
1360
1401
  throw new RangeError(`maxDisplay must be a positive integer, received ${maxDisplay}`);
1361
1402
  }
@@ -1370,7 +1411,8 @@ function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSo
1370
1411
  importance,
1371
1412
  order,
1372
1413
  maxDisplay,
1373
- faithfulOtherRow
1414
+ faithfulOtherRow,
1415
+ labels
1374
1416
  ),
1375
1417
  rowSort,
1376
1418
  explanation.data
@@ -1422,14 +1464,14 @@ function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSo
1422
1464
  });
1423
1465
  return { rows, collapsedCount: display.collapsedCount };
1424
1466
  }
1425
- function beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
1467
+ function beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom, title) {
1426
1468
  const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT3));
1427
1469
  return {
1428
1470
  xTicks: ticks.map((value) => ({ value, x: toX(value), label: tickLabel(value, step) })),
1429
1471
  xSpine: { x1: marginLeft, x2: marginLeft + plotWidth, y: plotBottom },
1430
1472
  xTitle: {
1431
- // _labels.py:5, labels["VALUE"].
1432
- text: "SHAP value (impact on model output)",
1473
+ // The default is _labels.py:5, labels["VALUE"].
1474
+ text: title,
1433
1475
  x: marginLeft + plotWidth / 2,
1434
1476
  y: plotBottom + AXIS_TITLE_DY,
1435
1477
  fontSize: TITLE_PT2
@@ -1437,14 +1479,17 @@ function beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
1437
1479
  };
1438
1480
  }
1439
1481
  function beeswarmLayout(valueRows, opts) {
1482
+ var _a;
1440
1483
  const { width, rowHeight, marginLeft, marginRight, marginTop, dotRadius } = opts;
1484
+ const labels = (_a = opts.labels) != null ? _a : shapLabels;
1485
+ const colorBar = colorBarSpec(labels);
1441
1486
  const plotBottom = marginTop + valueRows.rows.length * rowHeight;
1442
1487
  const fit = opts.colorBar ? fitColorBar({
1443
1488
  plotWidth: width - marginLeft - marginRight,
1444
1489
  available: marginRight,
1445
1490
  gapRatio: COLOR_BAR_GAP_RATIO,
1446
1491
  minGap: 0,
1447
- extent: colorBarExtent(plotBottom - marginTop, COLOR_BAR)
1492
+ extent: colorBarExtent(plotBottom - marginTop, colorBar)
1448
1493
  }) : null;
1449
1494
  const plotWidth = fit ? fit.plotWidth : width - marginLeft - marginRight;
1450
1495
  const values = valueRows.rows.flatMap((row) => row.points.map((point) => point.x));
@@ -1475,10 +1520,10 @@ function beeswarmLayout(valueRows, opts) {
1475
1520
  rows,
1476
1521
  xDomain: [min, max],
1477
1522
  xZero: toX(0),
1478
- ...beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom),
1523
+ ...beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom, labels.shapValueAxis),
1479
1524
  plotWidth,
1480
1525
  plotBottom,
1481
- colorBar: fit ? colorBarLayout(COLOR_BAR, {
1526
+ colorBar: fit ? colorBarLayout(colorBar, {
1482
1527
  x: marginLeft + plotWidth + fit.gap,
1483
1528
  y1: marginTop,
1484
1529
  y2: plotBottom
@@ -1553,14 +1598,16 @@ function ShapBeeswarm({
1553
1598
  seed = 0,
1554
1599
  dotRadius = 3,
1555
1600
  colorBar = true,
1601
+ labels,
1556
1602
  onFeatureClick
1557
1603
  }) {
1558
1604
  const [hovered, setHovered] = (0, import_react2.useState)(null);
1605
+ const words = (0, import_react2.useMemo)(() => resolveLabels(labels), [labels]);
1559
1606
  const marginTop = 8;
1560
1607
  const layout = (0, import_react2.useMemo)(() => {
1561
1608
  const raw = parseExplanation(explanation, { classIndex });
1562
1609
  const parsed = groupByGenus2 ? groupExplanationByGenus(raw) : raw;
1563
- const rows = beeswarmRows(parsed, maxDisplay, faithfulOtherRow, seed, rowSort);
1610
+ const rows = beeswarmRows(parsed, maxDisplay, faithfulOtherRow, seed, rowSort, words);
1564
1611
  return beeswarmLayout(rows, {
1565
1612
  width,
1566
1613
  rowHeight,
@@ -1568,12 +1615,14 @@ function ShapBeeswarm({
1568
1615
  marginRight: 90,
1569
1616
  marginTop,
1570
1617
  dotRadius,
1571
- colorBar
1618
+ colorBar,
1619
+ labels: words
1572
1620
  });
1573
1621
  }, [
1574
1622
  groupByGenus2,
1575
1623
  rowSort,
1576
1624
  colorBar,
1625
+ words,
1577
1626
  explanation,
1578
1627
  maxDisplay,
1579
1628
  faithfulOtherRow,
@@ -1672,8 +1721,8 @@ function ShapBeeswarm({
1672
1721
  children: [
1673
1722
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { x: 0, y: -16, width: 210, height: 54, rx: 3, fill: "#ffffff", stroke: "#cccccc" }),
1674
1723
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: 7, y: 0, fontSize: 11, fill: "#222222", children: activeRow.label }),
1675
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: 7, y: 15, fontSize: 11, fill: "#222222", children: `SHAP value: ${formatShapValue(activePoint.valueX)}` }),
1676
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: 7, y: 30, fontSize: 11, fill: "#222222", children: `Feature value: ${Number.isFinite(activePoint.featureValue) ? formatShapValue(activePoint.featureValue) : "missing"}` })
1724
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: 7, y: 15, fontSize: 11, fill: "#222222", children: `${words.shapValue}: ${formatShapValue(activePoint.valueX)}` }),
1725
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: 7, y: 30, fontSize: 11, fill: "#222222", children: `${words.featureValue}: ${Number.isFinite(activePoint.featureValue) ? formatLevel(activePoint.featureValue) : words.missingFeatureValue}` })
1677
1726
  ]
1678
1727
  }
1679
1728
  )
@@ -1704,7 +1753,7 @@ function percentile2(values, fraction) {
1704
1753
  const weight = position - lower;
1705
1754
  return sorted[lower] + (sorted[upper] - sorted[lower]) * weight;
1706
1755
  }
1707
- function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "importance") {
1756
+ function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "importance", labels = shapLabels) {
1708
1757
  if (!Number.isInteger(maxDisplay) || maxDisplay <= 0) {
1709
1758
  throw new RangeError(`maxDisplay must be a positive integer, received ${maxDisplay}`);
1710
1759
  }
@@ -1716,7 +1765,8 @@ function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "impor
1716
1765
  importance,
1717
1766
  featureOrder,
1718
1767
  maxDisplay,
1719
- faithfulOtherRow
1768
+ faithfulOtherRow,
1769
+ labels
1720
1770
  ),
1721
1771
  rowSort,
1722
1772
  explanation.data
@@ -1780,7 +1830,7 @@ function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "impor
1780
1830
  sampleLabelColumn: explanation.sampleLabelColumn
1781
1831
  };
1782
1832
  }
1783
- function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom) {
1833
+ function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom, title) {
1784
1834
  const { ticks, step } = niceTicks(
1785
1835
  -0.5,
1786
1836
  sampleCount - 0.5,
@@ -1795,7 +1845,7 @@ function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom)
1795
1845
  })),
1796
1846
  xSpine: null,
1797
1847
  xTitle: {
1798
- text: "Instances",
1848
+ text: title,
1799
1849
  x: marginLeft + plotWidth / 2,
1800
1850
  y: plotBottom + AXIS_TITLE_DY,
1801
1851
  fontSize: TICK_LABEL_PT4
@@ -1803,15 +1853,17 @@ function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom)
1803
1853
  };
1804
1854
  }
1805
1855
  function heatmapLayout(valueRows, opts) {
1856
+ var _a;
1806
1857
  const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
1858
+ const labels = (_a = opts.labels) != null ? _a : shapLabels;
1807
1859
  const plotBottom = marginTop + valueRows.rows.length * rowHeight;
1808
1860
  const sideBarWidth = Math.max(0, marginRight - SIDE_BAR_RIGHT_INSET - SIDE_BAR_GAP);
1809
1861
  const ticks = scalarFormatterLabels([valueRows.vmin, valueRows.vmax]);
1810
- const colorBarSpec = {
1862
+ const colorBarSpec2 = {
1811
1863
  colormap: "red_white_blue",
1812
1864
  tickLabels: [ticks.labels[0], ticks.labels[1]],
1813
1865
  ...ticks.offsetText ? { offsetText: ticks.offsetText } : {},
1814
- label: "SHAP value (impact on model output)",
1866
+ label: labels.shapValueAxis,
1815
1867
  labelPad: -10
1816
1868
  };
1817
1869
  const colorBarTop = FX_TOP;
@@ -1820,7 +1872,7 @@ function heatmapLayout(valueRows, opts) {
1820
1872
  available: marginRight,
1821
1873
  gapRatio: COLOR_BAR_GAP_RATIO2,
1822
1874
  minGap: SIDE_BAR_GAP + sideBarWidth + COLOR_BAR_CLEARANCE,
1823
- extent: colorBarExtent(plotBottom - colorBarTop, colorBarSpec)
1875
+ extent: colorBarExtent(plotBottom - colorBarTop, colorBarSpec2)
1824
1876
  }) : null;
1825
1877
  const plotWidth = fit ? fit.plotWidth : width - marginLeft - marginRight;
1826
1878
  const cellWidth = valueRows.columns.length === 0 ? 0 : plotWidth / valueRows.columns.length;
@@ -1893,11 +1945,18 @@ function heatmapLayout(valueRows, opts) {
1893
1945
  x1: marginLeft - Y_TICK_LENGTH,
1894
1946
  x2: marginLeft
1895
1947
  })),
1896
- ...heatmapXAxis(valueRows.columns.length, marginLeft, cellWidth, plotWidth, plotBottom),
1948
+ ...heatmapXAxis(
1949
+ valueRows.columns.length,
1950
+ marginLeft,
1951
+ cellWidth,
1952
+ plotWidth,
1953
+ plotBottom,
1954
+ labels.samples
1955
+ ),
1897
1956
  sampleLabelColumn: valueRows.sampleLabelColumn,
1898
1957
  plotWidth,
1899
1958
  cellWidth,
1900
- colorBar: fit ? colorBarLayout(colorBarSpec, {
1959
+ colorBar: fit ? colorBarLayout(colorBarSpec2, {
1901
1960
  x: gridRight + fit.gap,
1902
1961
  y1: colorBarTop,
1903
1962
  y2: plotBottom
@@ -1918,27 +1977,30 @@ function ShapHeatmap({
1918
1977
  width = 720,
1919
1978
  rowHeight = 26,
1920
1979
  colorBar = true,
1980
+ labels,
1921
1981
  onFeatureClick,
1922
1982
  onSampleClick
1923
1983
  }) {
1924
1984
  const [hoveredColumn, setHoveredColumn] = (0, import_react3.useState)(null);
1985
+ const words = (0, import_react3.useMemo)(() => resolveLabels(labels), [labels]);
1925
1986
  const marginTop = 72;
1926
1987
  const layout = (0, import_react3.useMemo)(() => {
1927
1988
  const raw = parseExplanation(explanation, { classIndex });
1928
1989
  const parsed = groupByGenus2 ? groupExplanationByGenus(raw) : raw;
1929
- const rows = heatmapRows(parsed, maxDisplay, faithfulOtherRow, rowSort);
1990
+ const rows = heatmapRows(parsed, maxDisplay, faithfulOtherRow, rowSort, words);
1930
1991
  return heatmapLayout(rows, {
1931
1992
  width,
1932
1993
  rowHeight,
1933
1994
  marginLeft: 260,
1934
1995
  marginRight: 100,
1935
1996
  marginTop,
1936
- colorBar
1997
+ colorBar,
1998
+ labels: words
1937
1999
  });
1938
- }, [groupByGenus2, rowSort, colorBar, explanation, maxDisplay, faithfulOtherRow, classIndex, width, rowHeight]);
2000
+ }, [groupByGenus2, rowSort, colorBar, words, explanation, maxDisplay, faithfulOtherRow, classIndex, width, rowHeight]);
1939
2001
  const activeColumn = hoveredColumn === null ? void 0 : layout.columns[hoveredColumn];
1940
2002
  const nameOf = (column) => {
1941
- if (!column.sampleLabel) return `Sample ${column.sampleIndex + 1}`;
2003
+ if (!column.sampleLabel) return words.sampleFallback(column.sampleIndex + 1);
1942
2004
  return layout.sampleLabelColumn ? `${layout.sampleLabelColumn}: ${column.sampleLabel}` : column.sampleLabel;
1943
2005
  };
1944
2006
  const tooltipWidth = activeColumn ? Math.max(190, nameOf(activeColumn).length * 6.5 + 16) : 190;
@@ -2091,7 +2153,7 @@ function ShapHeatmap({
2091
2153
  width: column.width,
2092
2154
  height: layout.plotBottom - 8,
2093
2155
  fill: "transparent",
2094
- "aria-label": `${nameOf(column)}, total SHAP value ${formatShapValue(column.total)}`,
2156
+ "aria-label": `${nameOf(column)}, total ${words.shapValue} ${formatShapValue(column.total)}`,
2095
2157
  onMouseEnter: () => setHoveredColumn(columnIndex),
2096
2158
  onMouseLeave: () => setHoveredColumn(null),
2097
2159
  onClick: () => {
@@ -2104,7 +2166,7 @@ function ShapHeatmap({
2104
2166
  activeColumn && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("g", { pointerEvents: "none", transform: `translate(${tooltipX} 10)`, children: [
2105
2167
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x: 0, y: 0, width: tooltipWidth, height: 42, rx: 3, fill: "#ffffff", stroke: "#cccccc" }),
2106
2168
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("text", { x: 7, y: 15, fontSize: 11, fill: "#222222", children: nameOf(activeColumn) }),
2107
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("text", { x: 7, y: 31, fontSize: 11, fill: "#222222", children: `\u03A3\u03C6: ${formatShapValue(activeColumn.total)}` })
2169
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("text", { x: 7, y: 31, fontSize: 11, fill: "#222222", children: `${words.sampleTotal}: ${formatShapValue(activeColumn.total)}` })
2108
2170
  ] })
2109
2171
  ] });
2110
2172
  }
@@ -2140,7 +2202,7 @@ function placeValueLabel(value, startX, endX, gutterX, decimals) {
2140
2202
  }
2141
2203
  return { ...base, x: startX + VALUE_LABEL_GAP, anchor: "start", inside: false };
2142
2204
  }
2143
- function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow) {
2205
+ function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow, labels = shapLabels) {
2144
2206
  if (!Number.isInteger(sampleIndex) || sampleIndex < 0 || sampleIndex >= explanation.nSamples) {
2145
2207
  throw new RangeError(
2146
2208
  `sampleIndex must identify a Sample from 0 to ${explanation.nSamples - 1}, received ${sampleIndex}`
@@ -2178,7 +2240,8 @@ function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow) {
2178
2240
  if (hasOtherRow) {
2179
2241
  const value = collapsed.reduce((sum, featureIndex) => sum + values[featureIndex], 0);
2180
2242
  rows.push({
2181
- label: `${collapsed.length} other features`,
2243
+ // SHAP's waterfall counts the hidden Features whatever the collapse mode.
2244
+ label: labels.otherFeatures(collapsed.length, "count"),
2182
2245
  featureIndex: null,
2183
2246
  isOtherRow: true,
2184
2247
  value,
@@ -2196,7 +2259,9 @@ function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow) {
2196
2259
  };
2197
2260
  }
2198
2261
  function waterfallLayout(valueRows, opts) {
2262
+ var _a;
2199
2263
  const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
2264
+ const labels = (_a = opts.labels) != null ? _a : shapLabels;
2200
2265
  const plotWidth = width - marginLeft - marginRight;
2201
2266
  const coordinates = [valueRows.baseValue, valueRows.modelOutput];
2202
2267
  for (const row of valueRows.rows) coordinates.push(row.left, row.left + row.width);
@@ -2263,7 +2328,7 @@ function waterfallLayout(valueRows, opts) {
2263
2328
  // got, which is the whole reason SHAP hides the left spine.
2264
2329
  y1: plotBottom - rowHeight,
2265
2330
  y2: plotBottom,
2266
- label: `E[f(X)] = ${formatLevel(valueRows.baseValue, opts.decimals)}`
2331
+ label: `${labels.baseValue} = ${formatLevel(valueRows.baseValue, opts.decimals)}`
2267
2332
  },
2268
2333
  {
2269
2334
  kind: "output",
@@ -2272,7 +2337,7 @@ function waterfallLayout(valueRows, opts) {
2272
2337
  // axvline(fx, 0, 1) — the full height.
2273
2338
  y1: marginTop,
2274
2339
  y2: plotBottom,
2275
- label: `f(x) = ${formatLevel(valueRows.modelOutput, opts.decimals)}`
2340
+ label: `${labels.modelOutput} = ${formatLevel(valueRows.modelOutput, opts.decimals)}`
2276
2341
  }
2277
2342
  ],
2278
2343
  separators: valueRows.rows.map((_, index) => ({
@@ -2301,21 +2366,24 @@ function ShapWaterfall({
2301
2366
  width = 720,
2302
2367
  rowHeight = 30,
2303
2368
  decimals = 2,
2369
+ labels,
2304
2370
  onFeatureClick
2305
2371
  }) {
2306
2372
  const [hovered, setHovered] = (0, import_react4.useState)(null);
2373
+ const words = (0, import_react4.useMemo)(() => resolveLabels(labels), [labels]);
2307
2374
  const marginTop = 34;
2308
2375
  const layout = (0, import_react4.useMemo)(() => {
2309
2376
  const raw = parseExplanation(explanation, { classIndex });
2310
2377
  const parsed = groupByGenus2 ? groupExplanationByGenus(raw) : raw;
2311
- const rows = waterfallRows(parsed, sampleIndex, maxDisplay, faithfulOtherRow);
2378
+ const rows = waterfallRows(parsed, sampleIndex, maxDisplay, faithfulOtherRow, words);
2312
2379
  return waterfallLayout(rows, {
2313
2380
  width,
2314
2381
  rowHeight,
2315
2382
  marginLeft: 260,
2316
2383
  marginRight: 110,
2317
2384
  marginTop,
2318
- decimals
2385
+ decimals,
2386
+ labels: words
2319
2387
  });
2320
2388
  }, [
2321
2389
  groupByGenus2,
@@ -2326,7 +2394,8 @@ function ShapWaterfall({
2326
2394
  classIndex,
2327
2395
  width,
2328
2396
  rowHeight,
2329
- decimals
2397
+ decimals,
2398
+ words
2330
2399
  ]);
2331
2400
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2332
2401
  "svg",