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/README.md +38 -0
- package/dist/{chunk-ZWO4CN22.js → chunk-4X6PMJYT.js} +91 -35
- package/dist/chunk-4X6PMJYT.js.map +1 -0
- package/dist/index.cjs +92 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -7
- package/dist/index.d.ts +15 -7
- package/dist/index.js +5 -1
- package/dist/{format-wEav_cPe.d.cts → labels-7wofev8M.d.cts} +44 -1
- package/dist/{format-wEav_cPe.d.ts → labels-7wofev8M.d.ts} +44 -1
- package/dist/react.cjs +120 -51
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +29 -5
- package/dist/react.d.ts +29 -5
- package/dist/react.js +35 -18
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZWO4CN22.js.map +0 -1
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:
|
|
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
|
|
374
|
-
// "SHAP value" -> "|SHAP value|" -> "mean(|SHAP value|)".
|
|
375
|
-
text:
|
|
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(
|
|
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":
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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:
|
|
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,
|
|
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(
|
|
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:
|
|
1676
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("text", { x: 7, y: 30, fontSize: 11, fill: "#222222", children:
|
|
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:
|
|
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
|
|
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:
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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",
|