shap-svg 0.2.4 → 0.3.0
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 +69 -14
- package/dist/{chunk-UVIVTJK3.js → chunk-JQBQSWIU.js} +927 -5
- package/dist/chunk-JQBQSWIU.js.map +1 -0
- package/dist/{labels-7wofev8M.d.cts → colormap-ChkaoIeg.d.cts} +88 -39
- package/dist/{labels-7wofev8M.d.ts → colormap-ChkaoIeg.d.ts} +88 -39
- package/dist/index.cjs +962 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +463 -7
- package/dist/index.d.ts +463 -7
- package/dist/index.js +73 -3
- package/dist/react.cjs +1865 -14
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +99 -5
- package/dist/react.d.ts +99 -5
- package/dist/react.js +985 -8
- package/dist/react.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-UVIVTJK3.js.map +0 -1
|
@@ -221,6 +221,10 @@ function parseExplanation(input, opts = {}) {
|
|
|
221
221
|
nFeatures
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
|
+
function sampleDisplayName(parsed, sampleIndex, words) {
|
|
225
|
+
var _a, _b;
|
|
226
|
+
return (_b = (_a = parsed.sampleLabels) == null ? void 0 : _a[sampleIndex]) != null ? _b : words.sampleFallback(sampleIndex + 1);
|
|
227
|
+
}
|
|
224
228
|
|
|
225
229
|
// src/core/order.ts
|
|
226
230
|
function globalImportance(e) {
|
|
@@ -238,6 +242,9 @@ function orderFeatures(importance) {
|
|
|
238
242
|
var MINUS = "\u2212";
|
|
239
243
|
var EXPONENT_THRESHOLD = 1e-3;
|
|
240
244
|
var PERCENT_DECIMALS = 2;
|
|
245
|
+
function formatFixed(v, decimals) {
|
|
246
|
+
return Math.abs(v).toFixed(decimals);
|
|
247
|
+
}
|
|
241
248
|
function formatShapValue(v, decimals) {
|
|
242
249
|
if (v === 0) return "0";
|
|
243
250
|
return (v < 0 ? MINUS : "+") + magnitudeOf(v, decimals);
|
|
@@ -275,7 +282,21 @@ var shapLabels = {
|
|
|
275
282
|
sampleFallback: (sampleNumber) => `Sample ${sampleNumber}`,
|
|
276
283
|
baseValue: "E[f(X)]",
|
|
277
284
|
modelOutput: "f(x)",
|
|
278
|
-
|
|
285
|
+
higher: "higher",
|
|
286
|
+
lower: "lower",
|
|
287
|
+
otherFeatures: (count, style) => style === "sum" ? `Sum of ${count} other features` : `${count} other features`,
|
|
288
|
+
absent: "Absent",
|
|
289
|
+
absentWithCount: (count) => `Absent (n = ${count})`,
|
|
290
|
+
principalComponent: (index, varianceRatio) => `SHAP PC${index} (${formatFixed(varianceRatio * 100, 0)}% of SHAP variance)`,
|
|
291
|
+
cumulativeShapValue: "Model output",
|
|
292
|
+
clusterDistance: "Clustering cutoff",
|
|
293
|
+
interactionScore: (score) => `interaction ${formatFixed(score, 2)}`,
|
|
294
|
+
weakInteraction: "no strong interaction found",
|
|
295
|
+
trend: "Median trend",
|
|
296
|
+
tableCaption: "Chart data",
|
|
297
|
+
componentTracksTotal: (r) => `tracks \u03A3\u03C6, r = ${formatFixed(r, 2)}`,
|
|
298
|
+
colorScale: (what) => `Colour: ${what}`,
|
|
299
|
+
suppliedComponent: (index) => `Dimension ${index}`
|
|
279
300
|
};
|
|
280
301
|
function resolveLabels(labels) {
|
|
281
302
|
if (!labels) return shapLabels;
|
|
@@ -319,6 +340,140 @@ function collapseToDisplay(featureNames, importance, order, maxDisplay, faithful
|
|
|
319
340
|
return { rows, collapsedCount: collapsed.length };
|
|
320
341
|
}
|
|
321
342
|
|
|
343
|
+
// src/core/hclust.ts
|
|
344
|
+
var MAX_DISTANCE = 1;
|
|
345
|
+
function absCorrelation(a, b) {
|
|
346
|
+
const n = a.length;
|
|
347
|
+
let meanA = 0;
|
|
348
|
+
let meanB = 0;
|
|
349
|
+
for (let i = 0; i < n; i++) {
|
|
350
|
+
meanA += a[i];
|
|
351
|
+
meanB += b[i];
|
|
352
|
+
}
|
|
353
|
+
meanA /= n;
|
|
354
|
+
meanB /= n;
|
|
355
|
+
let cov = 0;
|
|
356
|
+
let varA = 0;
|
|
357
|
+
let varB = 0;
|
|
358
|
+
for (let i = 0; i < n; i++) {
|
|
359
|
+
const da = a[i] - meanA;
|
|
360
|
+
const db = b[i] - meanB;
|
|
361
|
+
cov += da * db;
|
|
362
|
+
varA += da * da;
|
|
363
|
+
varB += db * db;
|
|
364
|
+
}
|
|
365
|
+
if (!(varA > 0) || !(varB > 0)) return 0;
|
|
366
|
+
return Math.min(1, Math.abs(cov / Math.sqrt(varA * varB)));
|
|
367
|
+
}
|
|
368
|
+
function correlationDistances(columns) {
|
|
369
|
+
const k = columns.length;
|
|
370
|
+
const out = Array.from({ length: k }, () => new Array(k).fill(0));
|
|
371
|
+
for (let i = 0; i < k; i++) {
|
|
372
|
+
for (let j = i + 1; j < k; j++) {
|
|
373
|
+
const distance = MAX_DISTANCE - absCorrelation(columns[i], columns[j]);
|
|
374
|
+
out[i][j] = distance;
|
|
375
|
+
out[j][i] = distance;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function averageLinkage(distances) {
|
|
381
|
+
const k = distances.length;
|
|
382
|
+
if (k < 2) return [];
|
|
383
|
+
const members = /* @__PURE__ */ new Map();
|
|
384
|
+
for (let i = 0; i < k; i++) members.set(i, [i]);
|
|
385
|
+
const between = (a, b) => {
|
|
386
|
+
let sum = 0;
|
|
387
|
+
for (const i of a) for (const j of b) sum += distances[i][j];
|
|
388
|
+
return sum / (a.length * b.length);
|
|
389
|
+
};
|
|
390
|
+
const linkage = [];
|
|
391
|
+
for (let step = 0; step < k - 1; step++) {
|
|
392
|
+
const ids = [...members.keys()].sort((a, b) => a - b);
|
|
393
|
+
let bestLeft = ids[0];
|
|
394
|
+
let bestRight = ids[1];
|
|
395
|
+
let bestDistance = Infinity;
|
|
396
|
+
for (let a = 0; a < ids.length; a++) {
|
|
397
|
+
for (let b = a + 1; b < ids.length; b++) {
|
|
398
|
+
const d = between(members.get(ids[a]), members.get(ids[b]));
|
|
399
|
+
if (d < bestDistance - 1e-12) {
|
|
400
|
+
bestDistance = d;
|
|
401
|
+
bestLeft = ids[a];
|
|
402
|
+
bestRight = ids[b];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const merged = [...members.get(bestLeft), ...members.get(bestRight)];
|
|
407
|
+
members.delete(bestLeft);
|
|
408
|
+
members.delete(bestRight);
|
|
409
|
+
members.set(k + step, merged);
|
|
410
|
+
linkage.push([bestLeft, bestRight, bestDistance, merged.length]);
|
|
411
|
+
}
|
|
412
|
+
return linkage;
|
|
413
|
+
}
|
|
414
|
+
function copheneticDistances(linkage) {
|
|
415
|
+
const k = linkage.length + 1;
|
|
416
|
+
const out = Array.from({ length: k }, () => new Array(k).fill(0));
|
|
417
|
+
const leaves = /* @__PURE__ */ new Map();
|
|
418
|
+
for (let i = 0; i < k; i++) leaves.set(i, [i]);
|
|
419
|
+
linkage.forEach((row, step) => {
|
|
420
|
+
const left = leaves.get(row[0]);
|
|
421
|
+
const right = leaves.get(row[1]);
|
|
422
|
+
for (const i of left) for (const j of right) {
|
|
423
|
+
out[i][j] = row[2];
|
|
424
|
+
out[j][i] = row[2];
|
|
425
|
+
}
|
|
426
|
+
leaves.set(k + step, [...left, ...right]);
|
|
427
|
+
});
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
function leafOrder(linkage, leafValues) {
|
|
431
|
+
var _a;
|
|
432
|
+
const k = linkage.length + 1;
|
|
433
|
+
if (k === 1) return [0];
|
|
434
|
+
const peak = /* @__PURE__ */ new Map();
|
|
435
|
+
const children = /* @__PURE__ */ new Map();
|
|
436
|
+
for (let i = 0; i < k; i++) peak.set(i, (_a = leafValues[i]) != null ? _a : 0);
|
|
437
|
+
linkage.forEach((row, step) => {
|
|
438
|
+
var _a2, _b;
|
|
439
|
+
const id = k + step;
|
|
440
|
+
children.set(id, [row[0], row[1]]);
|
|
441
|
+
peak.set(id, Math.max((_a2 = peak.get(row[0])) != null ? _a2 : 0, (_b = peak.get(row[1])) != null ? _b : 0));
|
|
442
|
+
});
|
|
443
|
+
const out = [];
|
|
444
|
+
const walk = (id) => {
|
|
445
|
+
var _a2, _b;
|
|
446
|
+
const pair = children.get(id);
|
|
447
|
+
if (!pair) {
|
|
448
|
+
out.push(id);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const [left, right] = pair;
|
|
452
|
+
const heavierFirst = ((_a2 = peak.get(left)) != null ? _a2 : 0) >= ((_b = peak.get(right)) != null ? _b : 0) ? [left, right] : [right, left];
|
|
453
|
+
walk(heavierFirst[0]);
|
|
454
|
+
walk(heavierFirst[1]);
|
|
455
|
+
};
|
|
456
|
+
walk(k + linkage.length - 1);
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
function relaxSortOrder(cophenetic, clusterOrder, cutoff, importanceOrder) {
|
|
460
|
+
const taken = /* @__PURE__ */ new Set();
|
|
461
|
+
const out = [];
|
|
462
|
+
for (const index of importanceOrder) {
|
|
463
|
+
if (taken.has(index)) continue;
|
|
464
|
+
taken.add(index);
|
|
465
|
+
out.push(index);
|
|
466
|
+
for (const neighbour of clusterOrder) {
|
|
467
|
+
if (taken.has(neighbour)) continue;
|
|
468
|
+
if (cophenetic[index][neighbour] <= cutoff) {
|
|
469
|
+
taken.add(neighbour);
|
|
470
|
+
out.push(neighbour);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return out;
|
|
475
|
+
}
|
|
476
|
+
|
|
322
477
|
// src/core/ticks.ts
|
|
323
478
|
var MINUS2 = "\u2212";
|
|
324
479
|
var PX_TO_PT = 0.75;
|
|
@@ -375,6 +530,41 @@ function tickLabel(value, step, percent = false) {
|
|
|
375
530
|
// src/core/barLayout.ts
|
|
376
531
|
var POSITIVE_COLOR = "#ff0051";
|
|
377
532
|
var NEGATIVE_COLOR = "#008bfb";
|
|
533
|
+
var CLUSTERING_POOL = 50;
|
|
534
|
+
function clusteredOrder(input) {
|
|
535
|
+
const { parsed, importanceOrder, mode, cutoff } = input;
|
|
536
|
+
const poolSize = Array.isArray(mode) ? mode.length + 1 : CLUSTERING_POOL;
|
|
537
|
+
const pool = importanceOrder.slice(0, Math.min(poolSize, importanceOrder.length));
|
|
538
|
+
let linkage;
|
|
539
|
+
if (Array.isArray(mode)) {
|
|
540
|
+
for (const row of mode) {
|
|
541
|
+
if (row.length !== 4) {
|
|
542
|
+
throw new RangeError("a clustering matrix must have rows of width 4");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
linkage = mode;
|
|
546
|
+
} else {
|
|
547
|
+
const source = mode === "shap" ? parsed.values : parsed.data;
|
|
548
|
+
const columns = pool.map((j) => source.map((row) => row[j]));
|
|
549
|
+
linkage = averageLinkage(correlationDistances(columns));
|
|
550
|
+
}
|
|
551
|
+
if (linkage.length === 0) return { order: importanceOrder, linkage };
|
|
552
|
+
const importanceWithinPool = pool.map((_, position) => position);
|
|
553
|
+
const meanAbs = pool.map((j) => {
|
|
554
|
+
let sum = 0;
|
|
555
|
+
for (const row of parsed.values) sum += Math.abs(row[j]);
|
|
556
|
+
return sum / parsed.nSamples;
|
|
557
|
+
});
|
|
558
|
+
const clusterPositions = leafOrder(linkage, meanAbs);
|
|
559
|
+
const relaxed = relaxSortOrder(
|
|
560
|
+
copheneticDistances(linkage),
|
|
561
|
+
clusterPositions,
|
|
562
|
+
cutoff,
|
|
563
|
+
importanceWithinPool
|
|
564
|
+
);
|
|
565
|
+
const order = [...relaxed.map((position) => pool[position]), ...importanceOrder.slice(pool.length)];
|
|
566
|
+
return { order, linkage };
|
|
567
|
+
}
|
|
378
568
|
var BAR_THICKNESS_RATIO = 0.7;
|
|
379
569
|
var AXIS_HEIGHT = 52;
|
|
380
570
|
var TICK_LABEL_PT = 11;
|
|
@@ -1178,6 +1368,7 @@ function sampleColormap(name, t) {
|
|
|
1178
1368
|
const blue = channel(lower, 5) + (channel(upper, 5) - channel(lower, 5)) * fraction;
|
|
1179
1369
|
return `#${hexByte(red)}${hexByte(green)}${hexByte(blue)}`;
|
|
1180
1370
|
}
|
|
1371
|
+
var UNCOLOURED = "#1f77b4";
|
|
1181
1372
|
|
|
1182
1373
|
// src/core/colorBar.ts
|
|
1183
1374
|
var ASPECT = 80;
|
|
@@ -1240,6 +1431,7 @@ function colorBarLayout(spec, position) {
|
|
|
1240
1431
|
x: labelLeft + LABEL_PT * LABEL_BASELINE_EM,
|
|
1241
1432
|
y: (y1 + y2) / 2,
|
|
1242
1433
|
text: spec.label,
|
|
1434
|
+
...spec.labelParts ? { parts: spec.labelParts } : {},
|
|
1243
1435
|
fontSize: LABEL_PT
|
|
1244
1436
|
},
|
|
1245
1437
|
right: Math.max(
|
|
@@ -1277,22 +1469,37 @@ function scalarFormatterLabels(locs) {
|
|
|
1277
1469
|
});
|
|
1278
1470
|
return order === 0 ? { labels } : { labels, offsetText: `1e${String(order).replace("-", MINUS3)}` };
|
|
1279
1471
|
}
|
|
1472
|
+
function colorScaleTitleParts(title, taxon2) {
|
|
1473
|
+
const at = title.indexOf(taxon2);
|
|
1474
|
+
if (at < 0) return void 0;
|
|
1475
|
+
return [
|
|
1476
|
+
{ text: title.slice(0, at) },
|
|
1477
|
+
{ text: taxon2, italic: true },
|
|
1478
|
+
{ text: title.slice(at + taxon2.length) }
|
|
1479
|
+
];
|
|
1480
|
+
}
|
|
1280
1481
|
|
|
1281
1482
|
// src/core/tooltip.ts
|
|
1282
|
-
var
|
|
1483
|
+
var GLYPH_PX = { body: 6.5, axis: 5.6 };
|
|
1283
1484
|
var PADDING_X = 7;
|
|
1284
1485
|
var OFFSET = 8;
|
|
1285
1486
|
var RAISE = 24;
|
|
1286
1487
|
function placeTooltip(opts) {
|
|
1287
1488
|
const { anchorX, anchorY, lines, lineHeight, minWidth, chartWidth, chartHeight } = opts;
|
|
1288
1489
|
const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
1289
|
-
const width = Math.max(minWidth, longest *
|
|
1490
|
+
const width = Math.max(minWidth, longest * GLYPH_PX.body + 2 * PADDING_X);
|
|
1290
1491
|
const height = lines.length * lineHeight + lineHeight * 0.6;
|
|
1291
1492
|
const right = anchorX + OFFSET;
|
|
1292
1493
|
const x = right + width <= chartWidth ? right : Math.max(0, anchorX - OFFSET - width);
|
|
1293
1494
|
const y = Math.max(0, Math.min(anchorY - RAISE, chartHeight - height));
|
|
1294
1495
|
return { x, y, width, height };
|
|
1295
1496
|
}
|
|
1497
|
+
var runsToText = (runs) => runs.map((run) => run.text).join("");
|
|
1498
|
+
var namedValue = (name, value) => [
|
|
1499
|
+
{ text: name, italic: true },
|
|
1500
|
+
{ text: `: ${value}` }
|
|
1501
|
+
];
|
|
1502
|
+
var TOOLTIP_LINE_HEIGHT = 15;
|
|
1296
1503
|
|
|
1297
1504
|
// src/core/beeswarmLayout.ts
|
|
1298
1505
|
var BEESWARM_MISSING_COLOR = "#777777";
|
|
@@ -1741,6 +1948,686 @@ function heatmapLayout(valueRows, opts) {
|
|
|
1741
1948
|
};
|
|
1742
1949
|
}
|
|
1743
1950
|
|
|
1951
|
+
// src/core/stats.ts
|
|
1952
|
+
function pearson(a, b) {
|
|
1953
|
+
const n = a.length;
|
|
1954
|
+
if (n < 2) return 0;
|
|
1955
|
+
let meanA = 0;
|
|
1956
|
+
let meanB = 0;
|
|
1957
|
+
for (let i = 0; i < n; i++) {
|
|
1958
|
+
meanA += a[i];
|
|
1959
|
+
meanB += b[i];
|
|
1960
|
+
}
|
|
1961
|
+
meanA /= n;
|
|
1962
|
+
meanB /= n;
|
|
1963
|
+
let cov = 0;
|
|
1964
|
+
let varA = 0;
|
|
1965
|
+
let varB = 0;
|
|
1966
|
+
for (let i = 0; i < n; i++) {
|
|
1967
|
+
const da = a[i] - meanA;
|
|
1968
|
+
const db = b[i] - meanB;
|
|
1969
|
+
cov += da * db;
|
|
1970
|
+
varA += da * da;
|
|
1971
|
+
varB += db * db;
|
|
1972
|
+
}
|
|
1973
|
+
if (!(varA > 0) || !(varB > 0)) return 0;
|
|
1974
|
+
return cov / Math.sqrt(varA * varB);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/core/interactions.ts
|
|
1978
|
+
var MAX_WINDOW = 50;
|
|
1979
|
+
function interactionWindowSize(sampleCount) {
|
|
1980
|
+
return Math.max(Math.min(Math.floor(sampleCount / 10), MAX_WINDOW), 1);
|
|
1981
|
+
}
|
|
1982
|
+
function eligibleInteractionFeature(column) {
|
|
1983
|
+
return column.some((value) => Math.abs(value) >= 1e-8) && column.some((value) => value !== column[0]);
|
|
1984
|
+
}
|
|
1985
|
+
function interactionScores(featureIndex, values, data) {
|
|
1986
|
+
var _a, _b;
|
|
1987
|
+
const sampleCount = data.length;
|
|
1988
|
+
const featureCount = (_b = (_a = data[0]) == null ? void 0 : _a.length) != null ? _b : 0;
|
|
1989
|
+
const scores = new Array(featureCount).fill(0);
|
|
1990
|
+
if (sampleCount < 2) return scores;
|
|
1991
|
+
const order = Array.from({ length: sampleCount }, (_, i) => i).sort(
|
|
1992
|
+
(a, b) => data[a][featureIndex] - data[b][featureIndex] || a - b
|
|
1993
|
+
);
|
|
1994
|
+
const sortedShap = order.map((i) => values[i][featureIndex]);
|
|
1995
|
+
const window = interactionWindowSize(sampleCount);
|
|
1996
|
+
const windowCount = Math.ceil(sampleCount / window);
|
|
1997
|
+
for (let j = 0; j < featureCount; j++) {
|
|
1998
|
+
if (j === featureIndex) continue;
|
|
1999
|
+
const sortedOther = order.map((i) => data[i][j]);
|
|
2000
|
+
if (!eligibleInteractionFeature(sortedOther)) continue;
|
|
2001
|
+
let sum = 0;
|
|
2002
|
+
for (let start = 0; start < sampleCount; start += window) {
|
|
2003
|
+
const shapSlice = sortedShap.slice(start, start + window);
|
|
2004
|
+
const otherSlice = sortedOther.slice(start, start + window);
|
|
2005
|
+
sum += Math.abs(pearson(shapSlice, otherSlice));
|
|
2006
|
+
}
|
|
2007
|
+
scores[j] = sum / windowCount;
|
|
2008
|
+
}
|
|
2009
|
+
return scores;
|
|
2010
|
+
}
|
|
2011
|
+
function strongestInteraction(featureIndex, values, data) {
|
|
2012
|
+
const scores = interactionScores(featureIndex, values, data);
|
|
2013
|
+
let best = -1;
|
|
2014
|
+
let bestScore = -1;
|
|
2015
|
+
scores.forEach((score, index) => {
|
|
2016
|
+
if (index === featureIndex) return;
|
|
2017
|
+
const column = data.map((row) => row[index]);
|
|
2018
|
+
if (eligibleInteractionFeature(column) && score > bestScore) {
|
|
2019
|
+
bestScore = score;
|
|
2020
|
+
best = index;
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
return best === -1 ? null : { index: best, score: bestScore };
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// src/core/scatterLayout.ts
|
|
2027
|
+
var MIN_TREND_SAMPLES = 4;
|
|
2028
|
+
var FLAT_DOMAIN_FACTOR = 10;
|
|
2029
|
+
function scatterPoints(parsed, featureIndex) {
|
|
2030
|
+
const absent = [];
|
|
2031
|
+
const detected = [];
|
|
2032
|
+
for (let i = 0; i < parsed.nSamples; i++) {
|
|
2033
|
+
const point = {
|
|
2034
|
+
sampleIndex: i,
|
|
2035
|
+
value: parsed.data[i][featureIndex],
|
|
2036
|
+
shap: parsed.values[i][featureIndex]
|
|
2037
|
+
};
|
|
2038
|
+
(point.value > 0 ? detected : absent).push(point);
|
|
2039
|
+
}
|
|
2040
|
+
detected.sort((a, b) => a.value - b.value || a.sampleIndex - b.sampleIndex);
|
|
2041
|
+
return { absent, detected };
|
|
2042
|
+
}
|
|
2043
|
+
function logDomain(detected) {
|
|
2044
|
+
if (detected.length === 0) return [1, 10];
|
|
2045
|
+
const low = detected[0].value;
|
|
2046
|
+
const high = detected[detected.length - 1].value;
|
|
2047
|
+
if (low === high) return [low / FLAT_DOMAIN_FACTOR, high * FLAT_DOMAIN_FACTOR];
|
|
2048
|
+
return [low, high];
|
|
2049
|
+
}
|
|
2050
|
+
function median(sorted) {
|
|
2051
|
+
const middle = Math.floor(sorted.length / 2);
|
|
2052
|
+
return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
2053
|
+
}
|
|
2054
|
+
function binnedMedianTrend(detected) {
|
|
2055
|
+
if (detected.length < MIN_TREND_SAMPLES) return [];
|
|
2056
|
+
const window = interactionWindowSize(detected.length);
|
|
2057
|
+
const out = [];
|
|
2058
|
+
for (let start = 0; start < detected.length; start += window) {
|
|
2059
|
+
const slice = detected.slice(start, start + window);
|
|
2060
|
+
if (slice.length === 0) continue;
|
|
2061
|
+
out.push({
|
|
2062
|
+
value: median(slice.map((p) => p.value)),
|
|
2063
|
+
shap: median([...slice.map((p) => p.shap)].sort((a, b) => a - b))
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
return out;
|
|
2067
|
+
}
|
|
2068
|
+
var MARGIN = { right: 24, top: 16, bottom: 56 };
|
|
2069
|
+
var ABSENT_BAND = 56;
|
|
2070
|
+
var ABSENT_GAP = 40;
|
|
2071
|
+
var DOT_RADIUS = 4;
|
|
2072
|
+
var TICK_LABEL_PT6 = 11;
|
|
2073
|
+
var Y_TITLE_X = 14;
|
|
2074
|
+
var sampleName = (parsed, sampleIndex, words) => sampleDisplayName(parsed, sampleIndex, words);
|
|
2075
|
+
function scatterTooltipLines(parsed, sampleIndex, featureIndex, colorFeatureIndex, words) {
|
|
2076
|
+
const abundance = (index) => {
|
|
2077
|
+
const value = parsed.data[sampleIndex][index];
|
|
2078
|
+
return namedValue(
|
|
2079
|
+
formatFeatureLabel(parsed.featureNames[index]),
|
|
2080
|
+
value > 0 ? formatLevel(value) : words.absent
|
|
2081
|
+
);
|
|
2082
|
+
};
|
|
2083
|
+
const lines = [
|
|
2084
|
+
[{ text: sampleName(parsed, sampleIndex, words) }],
|
|
2085
|
+
abundance(featureIndex)
|
|
2086
|
+
];
|
|
2087
|
+
if (colorFeatureIndex !== null) lines.push(abundance(colorFeatureIndex));
|
|
2088
|
+
lines.push([
|
|
2089
|
+
{
|
|
2090
|
+
text: `${words.shapValue}: ${formatShapValue(parsed.values[sampleIndex][featureIndex])}`
|
|
2091
|
+
}
|
|
2092
|
+
]);
|
|
2093
|
+
return lines;
|
|
2094
|
+
}
|
|
2095
|
+
function wrapToWidth(text, maxWidth) {
|
|
2096
|
+
const words = text.split(" ");
|
|
2097
|
+
const lines = [];
|
|
2098
|
+
let line = "";
|
|
2099
|
+
for (const word of words) {
|
|
2100
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
2101
|
+
if (line && candidate.length * GLYPH_PX.axis > maxWidth) {
|
|
2102
|
+
lines.push(line);
|
|
2103
|
+
line = word;
|
|
2104
|
+
} else {
|
|
2105
|
+
line = candidate;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (line) lines.push(line);
|
|
2109
|
+
return lines;
|
|
2110
|
+
}
|
|
2111
|
+
function scatterGeometry(input) {
|
|
2112
|
+
var _a;
|
|
2113
|
+
const {
|
|
2114
|
+
parsed,
|
|
2115
|
+
featureIndex,
|
|
2116
|
+
width,
|
|
2117
|
+
height,
|
|
2118
|
+
colorFeature,
|
|
2119
|
+
colorFeatureMinScore,
|
|
2120
|
+
colorBar = true,
|
|
2121
|
+
xScale,
|
|
2122
|
+
trend,
|
|
2123
|
+
colormap = "red_blue"
|
|
2124
|
+
} = input;
|
|
2125
|
+
const words = (_a = input.labels) != null ? _a : resolveLabels();
|
|
2126
|
+
const { absent, detected } = scatterPoints(parsed, featureIndex);
|
|
2127
|
+
const shapValues = parsed.values.map((row) => row[featureIndex]);
|
|
2128
|
+
const shapMin = Math.min(0, ...shapValues);
|
|
2129
|
+
const shapMax = Math.max(0, ...shapValues);
|
|
2130
|
+
const domainMin = shapMin === shapMax ? shapMin - 1 : shapMin;
|
|
2131
|
+
const domainMax = shapMin === shapMax ? shapMax + 1 : shapMax;
|
|
2132
|
+
const plotTop = MARGIN.top;
|
|
2133
|
+
const plotBottom = height - MARGIN.bottom;
|
|
2134
|
+
const initialYTicks = niceTicks(
|
|
2135
|
+
domainMin,
|
|
2136
|
+
domainMax,
|
|
2137
|
+
tickSpace(plotBottom - plotTop, TICK_LABEL_PT6)
|
|
2138
|
+
);
|
|
2139
|
+
const yStep = initialYTicks.step || 1;
|
|
2140
|
+
const yMin = Math.floor(domainMin / yStep) * yStep;
|
|
2141
|
+
const yMax = Math.ceil(domainMax / yStep) * yStep;
|
|
2142
|
+
const yTickValues = niceTicks(
|
|
2143
|
+
yMin,
|
|
2144
|
+
yMax,
|
|
2145
|
+
tickSpace(plotBottom - plotTop, TICK_LABEL_PT6)
|
|
2146
|
+
).ticks;
|
|
2147
|
+
const yTickLabel = (value) => {
|
|
2148
|
+
const normalized = Number(tickLabel(value, yStep).replace("\u2212", "-"));
|
|
2149
|
+
return formatShapValue(value !== 0 && normalized === 0 ? value : normalized);
|
|
2150
|
+
};
|
|
2151
|
+
const widestYTick = Math.max(...yTickValues.map((value) => yTickLabel(value).length), 1) * GLYPH_PX.body;
|
|
2152
|
+
const plotLeft = Math.max(70, Y_TITLE_X + 13 + 8 + widestYTick + 8);
|
|
2153
|
+
const plotRight = width - (colorBar ? 90 : MARGIN.right);
|
|
2154
|
+
const detectedLeft = absent.length > 0 ? plotLeft + ABSENT_BAND + ABSENT_GAP : plotLeft;
|
|
2155
|
+
const shapSpan = yMax - yMin || 1;
|
|
2156
|
+
const toY = (shap) => plotBottom - (shap - yMin) / shapSpan * (plotBottom - plotTop);
|
|
2157
|
+
const yTicks = yTickValues.map((value) => ({ value, y: toY(value), label: yTickLabel(value) }));
|
|
2158
|
+
const [lowValue, highValue] = logDomain(detected);
|
|
2159
|
+
const useLog = xScale === "log" && lowValue > 0;
|
|
2160
|
+
const scaleOf = (v) => useLog ? Math.log10(v) : v;
|
|
2161
|
+
const lowScaled = scaleOf(lowValue);
|
|
2162
|
+
const highScaled = scaleOf(highValue);
|
|
2163
|
+
const scaledSpan = highScaled - lowScaled || 1;
|
|
2164
|
+
const toX = (v) => detectedLeft + (scaleOf(v) - lowScaled) / scaledSpan * (plotRight - detectedLeft);
|
|
2165
|
+
let colorFeatureIndex = null;
|
|
2166
|
+
let colorNote = "";
|
|
2167
|
+
let byOutput = false;
|
|
2168
|
+
if (colorFeature === "auto") {
|
|
2169
|
+
const best = strongestInteraction(featureIndex, parsed.values, parsed.data);
|
|
2170
|
+
if (best && best.score >= colorFeatureMinScore) {
|
|
2171
|
+
colorFeatureIndex = best.index;
|
|
2172
|
+
colorNote = words.interactionScore(best.score);
|
|
2173
|
+
} else {
|
|
2174
|
+
colorNote = words.weakInteraction;
|
|
2175
|
+
}
|
|
2176
|
+
} else if (colorFeature === "output") {
|
|
2177
|
+
byOutput = true;
|
|
2178
|
+
} else if (typeof colorFeature === "number" && colorFeature !== featureIndex) {
|
|
2179
|
+
colorFeatureIndex = colorFeature;
|
|
2180
|
+
}
|
|
2181
|
+
const outputs = parsed.values.map(
|
|
2182
|
+
(row, i) => parsed.baseValues[i] + row.reduce((sum, value) => sum + value, 0)
|
|
2183
|
+
);
|
|
2184
|
+
let colorLow = 0;
|
|
2185
|
+
let colorHigh = 1;
|
|
2186
|
+
const clipTo = (column) => {
|
|
2187
|
+
const sorted = [...column].sort((a, b) => a - b);
|
|
2188
|
+
const at = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))];
|
|
2189
|
+
colorLow = at(0.05);
|
|
2190
|
+
colorHigh = at(0.95);
|
|
2191
|
+
if (colorLow === colorHigh) {
|
|
2192
|
+
colorLow = sorted[0];
|
|
2193
|
+
colorHigh = sorted[sorted.length - 1];
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
if (colorFeatureIndex !== null) {
|
|
2197
|
+
clipTo(parsed.data.map((row) => row[colorFeatureIndex]));
|
|
2198
|
+
} else if (byOutput) {
|
|
2199
|
+
clipTo(outputs);
|
|
2200
|
+
}
|
|
2201
|
+
const colorOf = (sampleIndex) => {
|
|
2202
|
+
if (colorFeatureIndex === null && !byOutput) return UNCOLOURED;
|
|
2203
|
+
const value = byOutput ? outputs[sampleIndex] : parsed.data[sampleIndex][colorFeatureIndex];
|
|
2204
|
+
const t = colorHigh === colorLow ? 0.5 : (value - colorLow) / (colorHigh - colorLow);
|
|
2205
|
+
return sampleColormap(colormap, t);
|
|
2206
|
+
};
|
|
2207
|
+
const colorFeatureLabel = colorFeatureIndex === null ? null : formatFeatureLabel(parsed.featureNames[colorFeatureIndex]);
|
|
2208
|
+
const scaleSubject = colorFeatureLabel ? `${colorFeatureLabel} \xB7 ${words.featureValue}${colorNote ? ` (${colorNote})` : ""}` : words.modelOutput;
|
|
2209
|
+
const scaleParts = colorFeatureLabel ? colorScaleTitleParts(words.colorScale(scaleSubject), colorFeatureLabel) : void 0;
|
|
2210
|
+
const colorBarGeometry = colorFeatureIndex === null && !byOutput || !colorBar ? null : colorBarLayout({
|
|
2211
|
+
colormap,
|
|
2212
|
+
tickLabels: byOutput ? [formatLevel(colorLow), formatLevel(colorHigh)] : [words.featureValueLow, words.featureValueHigh],
|
|
2213
|
+
label: words.colorScale(scaleSubject),
|
|
2214
|
+
...scaleParts ? { labelParts: scaleParts } : {},
|
|
2215
|
+
labelPad: 0
|
|
2216
|
+
}, { x: plotRight + 18, y1: plotTop, y2: plotBottom });
|
|
2217
|
+
const absentCenter = plotLeft + ABSENT_BAND / 2;
|
|
2218
|
+
const absentPoints = absent.map((p, i) => ({
|
|
2219
|
+
// Spread the band's points so identical SHAP values do not stack invisibly.
|
|
2220
|
+
cx: absentCenter + (i % 5 - 2) * (DOT_RADIUS + 1),
|
|
2221
|
+
cy: toY(p.shap),
|
|
2222
|
+
color: colorOf(p.sampleIndex),
|
|
2223
|
+
sampleIndex: p.sampleIndex
|
|
2224
|
+
}));
|
|
2225
|
+
const detectedPoints = detected.map((p) => ({
|
|
2226
|
+
cx: toX(p.value),
|
|
2227
|
+
cy: toY(p.shap),
|
|
2228
|
+
color: colorOf(p.sampleIndex),
|
|
2229
|
+
sampleIndex: p.sampleIndex
|
|
2230
|
+
}));
|
|
2231
|
+
const trendPoints = trend ? binnedMedianTrend(detected) : [];
|
|
2232
|
+
const trendPath = trendPoints.length >= 2 ? trendPoints.map((p, i) => `${i === 0 ? "M" : "L"}${toX(p.value)},${toY(p.shap)}`).join(" ") : null;
|
|
2233
|
+
const absentMeanY = absent.length > 0 ? toY(absent.reduce((sum, p) => sum + p.shap, 0) / absent.length) : null;
|
|
2234
|
+
const { ticks, step } = niceTicks(
|
|
2235
|
+
lowScaled,
|
|
2236
|
+
highScaled,
|
|
2237
|
+
tickSpace(plotRight - detectedLeft, TICK_LABEL_PT6)
|
|
2238
|
+
);
|
|
2239
|
+
const xTicks = ticks.map((t) => ({
|
|
2240
|
+
x: detectedLeft + (t - lowScaled) / scaledSpan * (plotRight - detectedLeft),
|
|
2241
|
+
label: useLog ? formatLevel(10 ** t) : tickLabel(t, step)
|
|
2242
|
+
}));
|
|
2243
|
+
return {
|
|
2244
|
+
absentPoints,
|
|
2245
|
+
detectedPoints,
|
|
2246
|
+
absentLabelLines: absent.length > 0 ? wrapToWidth(words.absentWithCount(absent.length), ABSENT_BAND + ABSENT_GAP - 4) : [],
|
|
2247
|
+
absentMeanY,
|
|
2248
|
+
zeroRuleY: toY(0),
|
|
2249
|
+
trendPath,
|
|
2250
|
+
colorFeatureIndex,
|
|
2251
|
+
colorFeatureLabel,
|
|
2252
|
+
colorNote,
|
|
2253
|
+
colorBar: colorBarGeometry,
|
|
2254
|
+
xTicks,
|
|
2255
|
+
yTicks,
|
|
2256
|
+
plotLeft,
|
|
2257
|
+
plotRight,
|
|
2258
|
+
plotTop,
|
|
2259
|
+
plotBottom,
|
|
2260
|
+
xTitle: {
|
|
2261
|
+
unit: words.featureValue,
|
|
2262
|
+
text: formatFeatureLabel(parsed.featureNames[featureIndex]),
|
|
2263
|
+
x: (plotLeft + plotRight) / 2,
|
|
2264
|
+
y: plotBottom + AXIS_TITLE_DY
|
|
2265
|
+
},
|
|
2266
|
+
yTitle: words.shapValue,
|
|
2267
|
+
yTitleX: Y_TITLE_X
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// src/core/pca.ts
|
|
2272
|
+
var DEFAULT_ITERATIONS = 200;
|
|
2273
|
+
var EPSILON = 1e-12;
|
|
2274
|
+
function centerColumns(values) {
|
|
2275
|
+
var _a, _b;
|
|
2276
|
+
const rows = values.length;
|
|
2277
|
+
const cols = (_b = (_a = values[0]) == null ? void 0 : _a.length) != null ? _b : 0;
|
|
2278
|
+
const means = new Array(cols).fill(0);
|
|
2279
|
+
for (const row of values) for (let j = 0; j < cols; j++) means[j] += row[j];
|
|
2280
|
+
for (let j = 0; j < cols; j++) means[j] /= rows;
|
|
2281
|
+
return values.map((row) => row.map((v, j) => v - means[j]));
|
|
2282
|
+
}
|
|
2283
|
+
function leadingComponent(m, iterations) {
|
|
2284
|
+
var _a, _b;
|
|
2285
|
+
const cols = (_b = (_a = m[0]) == null ? void 0 : _a.length) != null ? _b : 0;
|
|
2286
|
+
let v = Array.from({ length: cols }, (_, j) => 1 / (j + 1));
|
|
2287
|
+
normalise(v);
|
|
2288
|
+
for (let step = 0; step < iterations; step++) {
|
|
2289
|
+
const w = m.map((row) => {
|
|
2290
|
+
let sum = 0;
|
|
2291
|
+
for (let j = 0; j < cols; j++) sum += row[j] * v[j];
|
|
2292
|
+
return sum;
|
|
2293
|
+
});
|
|
2294
|
+
const next = new Array(cols).fill(0);
|
|
2295
|
+
m.forEach((row, i) => {
|
|
2296
|
+
const weight = w[i];
|
|
2297
|
+
for (let j = 0; j < cols; j++) next[j] += row[j] * weight;
|
|
2298
|
+
});
|
|
2299
|
+
if (!normalise(next)) return new Array(cols).fill(0);
|
|
2300
|
+
v = next;
|
|
2301
|
+
}
|
|
2302
|
+
return v;
|
|
2303
|
+
}
|
|
2304
|
+
function normalise(v) {
|
|
2305
|
+
let norm = 0;
|
|
2306
|
+
for (const x of v) norm += x * x;
|
|
2307
|
+
norm = Math.sqrt(norm);
|
|
2308
|
+
if (!(norm > EPSILON)) return false;
|
|
2309
|
+
for (let j = 0; j < v.length; j++) v[j] /= norm;
|
|
2310
|
+
return true;
|
|
2311
|
+
}
|
|
2312
|
+
function fixSign(component) {
|
|
2313
|
+
let peak = 0;
|
|
2314
|
+
for (const x of component) if (Math.abs(x) > Math.abs(peak)) peak = x;
|
|
2315
|
+
if (peak < 0) for (let j = 0; j < component.length; j++) component[j] = -component[j];
|
|
2316
|
+
}
|
|
2317
|
+
function project(m, component) {
|
|
2318
|
+
return m.map((row) => {
|
|
2319
|
+
let sum = 0;
|
|
2320
|
+
for (let j = 0; j < row.length; j++) sum += row[j] * component[j];
|
|
2321
|
+
return sum;
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
function shapPca(values, iterations = DEFAULT_ITERATIONS) {
|
|
2325
|
+
if (values.length === 0) return { coords: [], varianceRatios: [0, 0] };
|
|
2326
|
+
const centered = centerColumns(values);
|
|
2327
|
+
let total = 0;
|
|
2328
|
+
for (const row of centered) for (const v of row) total += v * v;
|
|
2329
|
+
const first = leadingComponent(centered, iterations);
|
|
2330
|
+
fixSign(first);
|
|
2331
|
+
const xs = project(centered, first);
|
|
2332
|
+
const residual = centered.map(
|
|
2333
|
+
(row, i) => row.map((v, j) => v - xs[i] * first[j])
|
|
2334
|
+
);
|
|
2335
|
+
const second = leadingComponent(residual, iterations);
|
|
2336
|
+
fixSign(second);
|
|
2337
|
+
const ys = project(residual, second);
|
|
2338
|
+
const energy = (projected) => projected.reduce((sum, v) => sum + v * v, 0);
|
|
2339
|
+
const ratio = (projected) => total > EPSILON ? energy(projected) / total : 0;
|
|
2340
|
+
return {
|
|
2341
|
+
coords: xs.map((x, i) => [x, ys[i]]),
|
|
2342
|
+
varianceRatios: [ratio(xs), ratio(ys)]
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/core/embeddingLayout.ts
|
|
2347
|
+
var MARGIN2 = { left: 64, right: 24, top: 20, bottom: 52 };
|
|
2348
|
+
var COLOR_BAR_MARGIN = 96;
|
|
2349
|
+
var MEANING_MIN_R = 0.7;
|
|
2350
|
+
function embeddingLayout(input) {
|
|
2351
|
+
var _a;
|
|
2352
|
+
const { parsed, width, height, colorBy, coords, colormap = "red_blue", colorBar = true } = input;
|
|
2353
|
+
const words = (_a = input.labels) != null ? _a : resolveLabels();
|
|
2354
|
+
if (coords && coords.length !== parsed.nSamples) {
|
|
2355
|
+
throw new RangeError(
|
|
2356
|
+
`coords must hold one position per Sample: expected ${parsed.nSamples}, received ${coords.length}`
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
const projection = coords ? null : shapPca(parsed.values);
|
|
2360
|
+
const positions = coords != null ? coords : projection.coords;
|
|
2361
|
+
const plotLeft = MARGIN2.left;
|
|
2362
|
+
const showColorBar = colorBar && colorBy !== "none";
|
|
2363
|
+
const plotRight = width - (showColorBar ? COLOR_BAR_MARGIN : MARGIN2.right);
|
|
2364
|
+
const plotTop = MARGIN2.top;
|
|
2365
|
+
const plotBottom = height - MARGIN2.bottom;
|
|
2366
|
+
const axisRange = (axis) => {
|
|
2367
|
+
const all = positions.map((p) => p[axis]);
|
|
2368
|
+
const low = Math.min(...all);
|
|
2369
|
+
const high = Math.max(...all);
|
|
2370
|
+
return low === high ? [low - 1, high + 1] : [low, high];
|
|
2371
|
+
};
|
|
2372
|
+
const [xLow, xHigh] = axisRange(0);
|
|
2373
|
+
const [yLow, yHigh] = axisRange(1);
|
|
2374
|
+
const toX = (v) => plotLeft + (v - xLow) / (xHigh - xLow) * (plotRight - plotLeft);
|
|
2375
|
+
const toY = (v) => plotBottom - (v - yLow) / (yHigh - yLow) * (plotBottom - plotTop);
|
|
2376
|
+
const colourValues = colorBy === "none" ? null : parsed.values.map(
|
|
2377
|
+
(row) => colorBy === "sum" ? row.reduce((sum, v) => sum + v, 0) : row[colorBy]
|
|
2378
|
+
);
|
|
2379
|
+
const colourLow = colourValues ? Math.min(...colourValues) : 0;
|
|
2380
|
+
const colourHigh = colourValues ? Math.max(...colourValues) : 1;
|
|
2381
|
+
const points = positions.map((position, i) => ({
|
|
2382
|
+
cx: toX(position[0]),
|
|
2383
|
+
cy: toY(position[1]),
|
|
2384
|
+
coordinates: position,
|
|
2385
|
+
sampleIndex: i,
|
|
2386
|
+
color: colourValues ? sampleColormap(
|
|
2387
|
+
colormap,
|
|
2388
|
+
colourHigh === colourLow ? 0.5 : (colourValues[i] - colourLow) / (colourHigh - colourLow)
|
|
2389
|
+
) : UNCOLOURED
|
|
2390
|
+
}));
|
|
2391
|
+
const title = (index) => projection ? words.principalComponent(index, projection.varianceRatios[index - 1]) : words.suppliedComponent(index);
|
|
2392
|
+
const colouredTaxon = typeof colorBy === "number" ? formatFeatureLabel(parsed.featureNames[colorBy]) : null;
|
|
2393
|
+
const colourLabel = colorBy === "sum" ? words.sampleTotal : colouredTaxon ? `${colouredTaxon} \xB7 ${words.shapValue}` : "";
|
|
2394
|
+
const colourParts = colouredTaxon ? colorScaleTitleParts(words.colorScale(colourLabel), colouredTaxon) : void 0;
|
|
2395
|
+
const inside = (value, low, high) => value > low && value < high;
|
|
2396
|
+
const totals = parsed.values.map((row) => row.reduce((sum, value) => sum + value, 0));
|
|
2397
|
+
const meaningOf = (axis) => {
|
|
2398
|
+
const r = pearson(positions.map((position) => position[axis]), totals);
|
|
2399
|
+
return Math.abs(r) >= MEANING_MIN_R ? words.componentTracksTotal(r) : null;
|
|
2400
|
+
};
|
|
2401
|
+
return {
|
|
2402
|
+
points,
|
|
2403
|
+
xMeaning: meaningOf(0),
|
|
2404
|
+
yMeaning: meaningOf(1),
|
|
2405
|
+
zeroX: inside(0, xLow, xHigh) ? toX(0) : null,
|
|
2406
|
+
zeroY: inside(0, yLow, yHigh) ? toY(0) : null,
|
|
2407
|
+
xTitle: title(1),
|
|
2408
|
+
yTitle: title(2),
|
|
2409
|
+
plotLeft,
|
|
2410
|
+
plotRight,
|
|
2411
|
+
plotTop,
|
|
2412
|
+
plotBottom,
|
|
2413
|
+
colorBar: showColorBar && colourValues ? colorBarLayout(
|
|
2414
|
+
{
|
|
2415
|
+
colormap,
|
|
2416
|
+
// The actual range, not "Low" and "High": these are signed
|
|
2417
|
+
// SHAP values and their sign is the thing worth reading.
|
|
2418
|
+
tickLabels: [formatShapValue(colourLow), formatShapValue(colourHigh)],
|
|
2419
|
+
label: words.colorScale(colourLabel),
|
|
2420
|
+
...colourParts ? { labelParts: colourParts } : {},
|
|
2421
|
+
labelPad: 0
|
|
2422
|
+
},
|
|
2423
|
+
{ x: plotRight + 18, y1: plotTop, y2: plotBottom }
|
|
2424
|
+
) : null
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/core/decisionLayout.ts
|
|
2429
|
+
var AXIS_HEIGHT5 = 52;
|
|
2430
|
+
var MARGIN3 = { left: 220, right: 40, top: 16 };
|
|
2431
|
+
var X_MARGIN3 = 0.02;
|
|
2432
|
+
var MAX_OPACITY = 0.9;
|
|
2433
|
+
var MIN_OPACITY = 0.08;
|
|
2434
|
+
var OPACITY_FLOOR_AT = 400;
|
|
2435
|
+
function decisionLayout(input) {
|
|
2436
|
+
var _a;
|
|
2437
|
+
const { parsed, width, rowHeight, maxDisplay, colormap = "red_blue" } = input;
|
|
2438
|
+
const indices = (_a = input.sampleIndices) != null ? _a : parsed.values.map((_, i) => i);
|
|
2439
|
+
const descending = orderFeatures(globalImportance(parsed));
|
|
2440
|
+
const shown = descending.slice(0, Math.min(maxDisplay, descending.length));
|
|
2441
|
+
const hidden = descending.slice(shown.length);
|
|
2442
|
+
const bottomUp = [...shown].reverse();
|
|
2443
|
+
const paths = indices.map((sampleIndex) => {
|
|
2444
|
+
const row = parsed.values[sampleIndex];
|
|
2445
|
+
const hiddenSum = hidden.reduce((sum, j) => sum + row[j], 0);
|
|
2446
|
+
const values = [parsed.baseValues[sampleIndex] + hiddenSum];
|
|
2447
|
+
for (const j of bottomUp) values.push(values[values.length - 1] + row[j]);
|
|
2448
|
+
return { sampleIndex, values };
|
|
2449
|
+
});
|
|
2450
|
+
const base = indices.length === 0 ? 0 : indices.reduce((sum, sampleIndex) => sum + parsed.baseValues[sampleIndex], 0) / indices.length;
|
|
2451
|
+
let reach = 0;
|
|
2452
|
+
for (const path of paths) {
|
|
2453
|
+
for (const value of path.values) reach = Math.max(reach, Math.abs(value - base));
|
|
2454
|
+
}
|
|
2455
|
+
reach = reach || 1;
|
|
2456
|
+
const pad = reach * 2 * X_MARGIN3;
|
|
2457
|
+
const xDomain = [base - reach - pad, base + reach + pad];
|
|
2458
|
+
const plotLeft = MARGIN3.left;
|
|
2459
|
+
const plotRight = width - MARGIN3.right;
|
|
2460
|
+
const plotTop = MARGIN3.top;
|
|
2461
|
+
const plotBottom = plotTop + bottomUp.length * rowHeight;
|
|
2462
|
+
const toX = (v) => plotLeft + (v - xDomain[0]) / (xDomain[1] - xDomain[0]) * (plotRight - plotLeft);
|
|
2463
|
+
const toY = (step) => plotBottom - step * rowHeight;
|
|
2464
|
+
const pathOpacity = MIN_OPACITY + (MAX_OPACITY - MIN_OPACITY) * Math.max(0, 1 - Math.max(0, indices.length - 1) / OPACITY_FLOOR_AT);
|
|
2465
|
+
return {
|
|
2466
|
+
rowLabels: bottomUp.map((j) => parsed.featureNames[j]),
|
|
2467
|
+
rowY: bottomUp.map((_, i) => plotBottom - i * rowHeight - rowHeight / 2),
|
|
2468
|
+
paths: paths.map(({ sampleIndex, values }) => {
|
|
2469
|
+
const end = values[values.length - 1];
|
|
2470
|
+
const t = (end - xDomain[0]) / (xDomain[1] - xDomain[0]);
|
|
2471
|
+
return {
|
|
2472
|
+
sampleIndex,
|
|
2473
|
+
values,
|
|
2474
|
+
points: values.map((value, step) => ({ x: toX(value), y: toY(step) })),
|
|
2475
|
+
color: sampleColormap(colormap, t)
|
|
2476
|
+
};
|
|
2477
|
+
}),
|
|
2478
|
+
pathOpacity,
|
|
2479
|
+
xDomain,
|
|
2480
|
+
baseValue: base,
|
|
2481
|
+
baseValueX: toX(base),
|
|
2482
|
+
plotLeft,
|
|
2483
|
+
plotRight,
|
|
2484
|
+
plotTop,
|
|
2485
|
+
plotBottom,
|
|
2486
|
+
height: plotBottom + AXIS_HEIGHT5
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/core/forceLayout.ts
|
|
2491
|
+
var MIN_LABEL_SHARE = 0.05;
|
|
2492
|
+
var MARGIN4 = { left: 16, right: 16 };
|
|
2493
|
+
function forceLayout(input) {
|
|
2494
|
+
var _a;
|
|
2495
|
+
const { parsed, sampleIndex, width, height, maxDisplay, faithfulOtherRow = false } = input;
|
|
2496
|
+
const words = (_a = input.labels) != null ? _a : resolveLabels();
|
|
2497
|
+
const row = parsed.values[sampleIndex];
|
|
2498
|
+
const baseValue = parsed.baseValues[sampleIndex];
|
|
2499
|
+
const modelOutput = row.reduce((sum, v) => sum + v, 0) + baseValue;
|
|
2500
|
+
const magnitudes = row.map((v) => Math.abs(v));
|
|
2501
|
+
const order = magnitudes.map((value, index) => ({ value, index })).sort((a, b) => b.value - a.value || a.index - b.index).map((entry) => entry.index);
|
|
2502
|
+
const display = collapseToDisplay(
|
|
2503
|
+
parsed.featureNames,
|
|
2504
|
+
row,
|
|
2505
|
+
order,
|
|
2506
|
+
maxDisplay,
|
|
2507
|
+
faithfulOtherRow,
|
|
2508
|
+
words
|
|
2509
|
+
);
|
|
2510
|
+
const totalEffect = display.rows.reduce((sum, r) => sum + Math.abs(r.value), 0) || 1;
|
|
2511
|
+
const usable = Math.max(0, width - MARGIN4.left - MARGIN4.right);
|
|
2512
|
+
const scale = usable / totalEffect;
|
|
2513
|
+
const positives = display.rows.filter((r) => r.value > 0).sort((a, b) => b.value - a.value);
|
|
2514
|
+
const negatives = display.rows.filter((r) => r.value <= 0).sort((a, b) => a.value - b.value);
|
|
2515
|
+
const positiveTotal = positives.reduce((sum, r) => sum + Math.abs(r.value), 0);
|
|
2516
|
+
const meetingX = MARGIN4.left + positiveTotal * scale;
|
|
2517
|
+
const barHeight = Math.max(12, height - 48);
|
|
2518
|
+
const barY = 24;
|
|
2519
|
+
const segments = [];
|
|
2520
|
+
let cursor = meetingX;
|
|
2521
|
+
for (const r of positives) {
|
|
2522
|
+
const segmentWidth = Math.abs(r.value) * scale;
|
|
2523
|
+
cursor -= segmentWidth;
|
|
2524
|
+
segments.push({
|
|
2525
|
+
label: r.label,
|
|
2526
|
+
featureIndex: r.featureIndex,
|
|
2527
|
+
isOtherRow: r.isOtherRow,
|
|
2528
|
+
value: r.value,
|
|
2529
|
+
x: cursor,
|
|
2530
|
+
width: segmentWidth,
|
|
2531
|
+
color: POSITIVE_COLOR,
|
|
2532
|
+
labelled: Math.abs(r.value) / totalEffect >= MIN_LABEL_SHARE
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
cursor = meetingX;
|
|
2536
|
+
for (const r of negatives) {
|
|
2537
|
+
const segmentWidth = Math.abs(r.value) * scale;
|
|
2538
|
+
segments.push({
|
|
2539
|
+
label: r.label,
|
|
2540
|
+
featureIndex: r.featureIndex,
|
|
2541
|
+
isOtherRow: r.isOtherRow,
|
|
2542
|
+
value: r.value,
|
|
2543
|
+
x: cursor,
|
|
2544
|
+
width: segmentWidth,
|
|
2545
|
+
color: NEGATIVE_COLOR,
|
|
2546
|
+
labelled: Math.abs(r.value) / totalEffect >= MIN_LABEL_SHARE
|
|
2547
|
+
});
|
|
2548
|
+
cursor += segmentWidth;
|
|
2549
|
+
}
|
|
2550
|
+
const outputSpan = modelOutput - baseValue;
|
|
2551
|
+
return {
|
|
2552
|
+
segments,
|
|
2553
|
+
baseValue,
|
|
2554
|
+
modelOutput,
|
|
2555
|
+
meetingX,
|
|
2556
|
+
baseValueX: meetingX - outputSpan * scale,
|
|
2557
|
+
barY,
|
|
2558
|
+
barHeight
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// src/core/tableRows.ts
|
|
2563
|
+
var taxon = (name) => ({ text: formatFeatureLabel(name), italic: true });
|
|
2564
|
+
function scatterTableRows(parsed, featureIndex, split, words) {
|
|
2565
|
+
const featureName = taxon(parsed.featureNames[featureIndex]);
|
|
2566
|
+
const rows = [...split.absent, ...split.detected].sort((a, b) => a.sampleIndex - b.sampleIndex).map((point) => [
|
|
2567
|
+
sampleDisplayName(parsed, point.sampleIndex, words),
|
|
2568
|
+
point.value > 0 ? formatLevel(point.value) : words.absent,
|
|
2569
|
+
formatShapValue(point.shap)
|
|
2570
|
+
]);
|
|
2571
|
+
return {
|
|
2572
|
+
caption: words.tableCaption,
|
|
2573
|
+
columns: ["Sample", featureName, words.shapValue],
|
|
2574
|
+
rows
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
function embeddingTableRows(layout, parsed, words) {
|
|
2578
|
+
return {
|
|
2579
|
+
caption: words.tableCaption,
|
|
2580
|
+
columns: ["Sample", layout.xTitle, layout.yTitle],
|
|
2581
|
+
rows: layout.points.map((point) => [
|
|
2582
|
+
sampleDisplayName(parsed, point.sampleIndex, words),
|
|
2583
|
+
formatLevel(point.coordinates[0]),
|
|
2584
|
+
formatLevel(point.coordinates[1])
|
|
2585
|
+
])
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
function decisionTableRows(layout, parsed, words) {
|
|
2589
|
+
return {
|
|
2590
|
+
caption: words.tableCaption,
|
|
2591
|
+
columns: ["Sample", words.baseValue, ...layout.rowLabels.map(taxon)],
|
|
2592
|
+
rows: layout.paths.map((path) => [
|
|
2593
|
+
sampleDisplayName(parsed, path.sampleIndex, words),
|
|
2594
|
+
...path.values.map((value) => formatLevel(value))
|
|
2595
|
+
])
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
function forceTableRows(layout, words) {
|
|
2599
|
+
return {
|
|
2600
|
+
caption: words.tableCaption,
|
|
2601
|
+
columns: ["Feature", words.shapValue],
|
|
2602
|
+
rows: layout.segments.map((segment) => [
|
|
2603
|
+
// The Other features row is a count, not a taxon, so it stays upright.
|
|
2604
|
+
segment.isOtherRow ? segment.label : taxon(segment.label),
|
|
2605
|
+
formatShapValue(segment.value)
|
|
2606
|
+
])
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// src/core/dendrogram.ts
|
|
2611
|
+
function dendrogramCoords(leafPositions, linkage) {
|
|
2612
|
+
if (linkage.length === 0) return [];
|
|
2613
|
+
const leafCount = leafPositions.length;
|
|
2614
|
+
const out = [];
|
|
2615
|
+
const walk = (node) => {
|
|
2616
|
+
if (node < leafCount) return [leafPositions[node], 0];
|
|
2617
|
+
const row = linkage[node - leafCount];
|
|
2618
|
+
const [leftX, leftY] = walk(row[0]);
|
|
2619
|
+
const [rightX, rightY] = walk(row[1]);
|
|
2620
|
+
const height = row[2];
|
|
2621
|
+
out.push({
|
|
2622
|
+
xs: [leftX, leftX, rightX, rightX],
|
|
2623
|
+
ys: [leftY, height, height, rightY]
|
|
2624
|
+
});
|
|
2625
|
+
return [(leftX + rightX) / 2, height];
|
|
2626
|
+
};
|
|
2627
|
+
walk(leafCount + linkage.length - 1);
|
|
2628
|
+
return out;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
1744
2631
|
export {
|
|
1745
2632
|
UnsupportedContractVersionError,
|
|
1746
2633
|
InvalidExplanationError,
|
|
@@ -1750,18 +2637,27 @@ export {
|
|
|
1750
2637
|
groupExplanationByGenus,
|
|
1751
2638
|
sortDisplayRows,
|
|
1752
2639
|
parseExplanation,
|
|
2640
|
+
sampleDisplayName,
|
|
1753
2641
|
globalImportance,
|
|
1754
2642
|
orderFeatures,
|
|
2643
|
+
formatFixed,
|
|
1755
2644
|
formatShapValue,
|
|
1756
2645
|
formatLevel,
|
|
1757
2646
|
formatFeatureLabel,
|
|
1758
2647
|
shapLabels,
|
|
1759
2648
|
resolveLabels,
|
|
1760
2649
|
collapseToDisplay,
|
|
2650
|
+
correlationDistances,
|
|
2651
|
+
averageLinkage,
|
|
2652
|
+
copheneticDistances,
|
|
2653
|
+
leafOrder,
|
|
2654
|
+
relaxSortOrder,
|
|
1761
2655
|
TICK_LENGTH,
|
|
1762
2656
|
TICK_LABEL_DY,
|
|
1763
2657
|
POSITIVE_COLOR,
|
|
1764
2658
|
NEGATIVE_COLOR,
|
|
2659
|
+
CLUSTERING_POOL,
|
|
2660
|
+
clusteredOrder,
|
|
1765
2661
|
barLayout,
|
|
1766
2662
|
WATERFALL_HEAD_LENGTH_PX,
|
|
1767
2663
|
WATERFALL_TICK_LABEL_DY,
|
|
@@ -1769,16 +2665,42 @@ export {
|
|
|
1769
2665
|
waterfallRows,
|
|
1770
2666
|
waterfallLayout,
|
|
1771
2667
|
sampleColormap,
|
|
2668
|
+
UNCOLOURED,
|
|
1772
2669
|
colorBarExtent,
|
|
1773
2670
|
fitColorBar,
|
|
1774
2671
|
colorBarLayout,
|
|
1775
2672
|
scalarFormatterLabels,
|
|
2673
|
+
colorScaleTitleParts,
|
|
2674
|
+
GLYPH_PX,
|
|
1776
2675
|
placeTooltip,
|
|
2676
|
+
runsToText,
|
|
2677
|
+
namedValue,
|
|
2678
|
+
TOOLTIP_LINE_HEIGHT,
|
|
1777
2679
|
BEESWARM_MISSING_COLOR,
|
|
1778
2680
|
BEESWARM_ROW_HEIGHT,
|
|
1779
2681
|
beeswarmRows,
|
|
1780
2682
|
beeswarmLayout,
|
|
1781
2683
|
heatmapRows,
|
|
1782
|
-
heatmapLayout
|
|
2684
|
+
heatmapLayout,
|
|
2685
|
+
interactionWindowSize,
|
|
2686
|
+
interactionScores,
|
|
2687
|
+
strongestInteraction,
|
|
2688
|
+
scatterPoints,
|
|
2689
|
+
logDomain,
|
|
2690
|
+
binnedMedianTrend,
|
|
2691
|
+
ABSENT_BAND,
|
|
2692
|
+
DOT_RADIUS,
|
|
2693
|
+
scatterTooltipLines,
|
|
2694
|
+
wrapToWidth,
|
|
2695
|
+
scatterGeometry,
|
|
2696
|
+
shapPca,
|
|
2697
|
+
embeddingLayout,
|
|
2698
|
+
decisionLayout,
|
|
2699
|
+
forceLayout,
|
|
2700
|
+
scatterTableRows,
|
|
2701
|
+
embeddingTableRows,
|
|
2702
|
+
decisionTableRows,
|
|
2703
|
+
forceTableRows,
|
|
2704
|
+
dendrogramCoords
|
|
1783
2705
|
};
|
|
1784
|
-
//# sourceMappingURL=chunk-
|
|
2706
|
+
//# sourceMappingURL=chunk-JQBQSWIU.js.map
|