shap-svg 0.1.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.
@@ -0,0 +1,1566 @@
1
+ // src/core/types.ts
2
+ var UnsupportedContractVersionError = class extends Error {
3
+ constructor(received) {
4
+ super(`shap-svg supports contract_version 1, received ${JSON.stringify(received)}`);
5
+ this.received = received;
6
+ this.name = "UnsupportedContractVersionError";
7
+ }
8
+ };
9
+ var InvalidExplanationError = class extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "InvalidExplanationError";
13
+ }
14
+ };
15
+
16
+ // src/core/taxonomy.ts
17
+ function genusOf(featureName) {
18
+ const underscore = featureName.indexOf("_");
19
+ return underscore === -1 ? featureName : featureName.slice(0, underscore);
20
+ }
21
+ function groupByGenus(featureNames) {
22
+ const genera = [];
23
+ const memberIndices = [];
24
+ const seen = /* @__PURE__ */ new Map();
25
+ featureNames.forEach((name, index) => {
26
+ const genus = genusOf(name);
27
+ let slot = seen.get(genus);
28
+ if (slot === void 0) {
29
+ slot = genera.length;
30
+ seen.set(genus, slot);
31
+ genera.push(genus);
32
+ memberIndices.push([]);
33
+ }
34
+ memberIndices[slot].push(index);
35
+ });
36
+ return { genera, memberIndices };
37
+ }
38
+ function aggregateByGenus(values, data, featureNames) {
39
+ const { genera, memberIndices } = groupByGenus(featureNames);
40
+ const sumInto = (rows) => rows.map(
41
+ (row) => memberIndices.map(
42
+ (members) => members.reduce((total, index) => {
43
+ var _a;
44
+ return total + ((_a = row[index]) != null ? _a : 0);
45
+ }, 0)
46
+ )
47
+ );
48
+ return {
49
+ values: sumInto(values),
50
+ data: sumInto(data),
51
+ featureNames: genera
52
+ };
53
+ }
54
+ function groupExplanationByGenus(explanation) {
55
+ const grouped = aggregateByGenus(
56
+ explanation.values,
57
+ explanation.data,
58
+ explanation.featureNames
59
+ );
60
+ return {
61
+ ...explanation,
62
+ values: grouped.values,
63
+ data: grouped.data,
64
+ featureNames: grouped.featureNames,
65
+ nFeatures: grouped.featureNames.length
66
+ };
67
+ }
68
+
69
+ // src/core/rowSort.ts
70
+ function sortDisplayRows(display, sort, data) {
71
+ if (sort === "importance") return display;
72
+ const meanOf = (featureIndex) => data.length === 0 ? 0 : data.reduce((sum, sample) => sum + sample[featureIndex], 0) / data.length;
73
+ const keyed = display.rows.filter((row) => !row.isOtherRow).map((row, rank) => ({
74
+ row,
75
+ rank,
76
+ mean: row.featureIndex === null ? 0 : meanOf(row.featureIndex)
77
+ }));
78
+ keyed.sort((a, b) => {
79
+ const primary = sort === "name" ? a.row.label.localeCompare(b.row.label, void 0, { sensitivity: "base" }) : b.mean - a.mean;
80
+ return primary || a.rank - b.rank;
81
+ });
82
+ return {
83
+ ...display,
84
+ rows: [...keyed.map((k) => k.row), ...display.rows.filter((row) => row.isOtherRow)]
85
+ };
86
+ }
87
+
88
+ // src/core/parse.ts
89
+ var SUPPORTED_CONTRACT_VERSION = 1;
90
+ function isRecord(value) {
91
+ return typeof value === "object" && value !== null;
92
+ }
93
+ function assertFiniteRows(rows, field) {
94
+ if (!Array.isArray(rows)) {
95
+ throw new InvalidExplanationError(`${field} must be an array of Sample rows`);
96
+ }
97
+ for (let i = 0; i < rows.length; i++) {
98
+ const row = rows[i];
99
+ if (!Array.isArray(row)) {
100
+ throw new InvalidExplanationError(`${field}[${i}] must be an array of Feature values`);
101
+ }
102
+ for (let j = 0; j < row.length; j++) {
103
+ if (typeof row[j] !== "number" || !Number.isFinite(row[j])) {
104
+ throw new InvalidExplanationError(
105
+ `${field}[${i}][${j}] is ${String(row[j])}; the payload must contain only finite numbers`
106
+ );
107
+ }
108
+ }
109
+ }
110
+ }
111
+ function assertStringArray(value, field) {
112
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
113
+ throw new InvalidExplanationError(`${field} must be an array of strings`);
114
+ }
115
+ }
116
+ function selectClassValues(values, classIndex) {
117
+ if (!Array.isArray(values)) {
118
+ throw new InvalidExplanationError("values must be an array");
119
+ }
120
+ if (!Number.isInteger(classIndex) || classIndex < 0) {
121
+ throw new InvalidExplanationError(`classIndex must be a non-negative integer, received ${classIndex}`);
122
+ }
123
+ const firstRow = values[0];
124
+ const firstCell = Array.isArray(firstRow) ? firstRow[0] : void 0;
125
+ if (Array.isArray(firstCell)) {
126
+ return values.map((row, sampleIndex) => {
127
+ if (!Array.isArray(row)) {
128
+ throw new InvalidExplanationError(`values[${sampleIndex}] must be an array of Feature values`);
129
+ }
130
+ return row.map((cell, featureIndex) => {
131
+ if (!Array.isArray(cell) || classIndex >= cell.length) {
132
+ throw new InvalidExplanationError(
133
+ `values[${sampleIndex}][${featureIndex}] has no class ${classIndex}`
134
+ );
135
+ }
136
+ return cell[classIndex];
137
+ });
138
+ });
139
+ }
140
+ return values;
141
+ }
142
+ function selectBaseValues(value, nSamples, classIndex) {
143
+ if (typeof value === "number") {
144
+ return new Array(nSamples).fill(value);
145
+ }
146
+ if (!Array.isArray(value)) {
147
+ throw new InvalidExplanationError("base_values must be a number, an array, or an array of class arrays");
148
+ }
149
+ if (Array.isArray(value[0])) {
150
+ return value.map((row, sampleIndex) => {
151
+ if (!Array.isArray(row) || classIndex >= row.length) {
152
+ throw new InvalidExplanationError(`base_values[${sampleIndex}] has no class ${classIndex}`);
153
+ }
154
+ return row[classIndex];
155
+ });
156
+ }
157
+ return value;
158
+ }
159
+ function parseExplanation(input, opts = {}) {
160
+ var _a, _b;
161
+ const classIndex = (_a = opts.classIndex) != null ? _a : 1;
162
+ if (!isRecord(input) || input.contract_version !== SUPPORTED_CONTRACT_VERSION) {
163
+ throw new UnsupportedContractVersionError(isRecord(input) ? input.contract_version : void 0);
164
+ }
165
+ const e = input;
166
+ assertStringArray(e.feature_names, "feature_names");
167
+ assertFiniteRows(e.data, "data");
168
+ const values = selectClassValues(e.values, classIndex);
169
+ assertFiniteRows(values, "values");
170
+ const nSamples = values.length;
171
+ const nFeatures = e.feature_names.length;
172
+ if (e.data.length !== nSamples) {
173
+ throw new InvalidExplanationError(`data has ${e.data.length} Samples but values has ${nSamples}`);
174
+ }
175
+ for (const [name, rows] of [["values", values], ["data", e.data]]) {
176
+ for (let i = 0; i < rows.length; i++) {
177
+ if (rows[i].length !== nFeatures) {
178
+ throw new InvalidExplanationError(
179
+ `${name}[${i}] has ${rows[i].length} Features but feature_names has ${nFeatures}`
180
+ );
181
+ }
182
+ }
183
+ }
184
+ const baseValues = selectBaseValues(e.base_values, nSamples, classIndex);
185
+ if (baseValues.length !== nSamples) {
186
+ throw new InvalidExplanationError(
187
+ `base_values has ${baseValues.length} entries but there are ${nSamples} Samples`
188
+ );
189
+ }
190
+ assertFiniteRows([baseValues], "base_values");
191
+ if (e.sample_ids !== void 0) {
192
+ assertStringArray(e.sample_ids, "sample_ids");
193
+ if (e.sample_ids.length !== nSamples) {
194
+ throw new InvalidExplanationError(
195
+ `sample_ids has ${e.sample_ids.length} entries but there are ${nSamples} Samples`
196
+ );
197
+ }
198
+ }
199
+ if (e.sample_labels !== void 0) {
200
+ assertStringArray(e.sample_labels, "sample_labels");
201
+ if (e.sample_labels.length !== nSamples) {
202
+ throw new InvalidExplanationError(
203
+ `sample_labels has ${e.sample_labels.length} entries but there are ${nSamples} Samples`
204
+ );
205
+ }
206
+ }
207
+ if (e.sample_label_column !== void 0 && typeof e.sample_label_column !== "string") {
208
+ throw new InvalidExplanationError("sample_label_column must be a string");
209
+ }
210
+ if (e.output_names !== void 0) assertStringArray(e.output_names, "output_names");
211
+ return {
212
+ values,
213
+ data: e.data,
214
+ baseValues,
215
+ featureNames: e.feature_names,
216
+ sampleIds: e.sample_ids,
217
+ sampleLabels: e.sample_labels,
218
+ sampleLabelColumn: e.sample_label_column,
219
+ outputName: (_b = e.output_names) == null ? void 0 : _b[0],
220
+ nSamples,
221
+ nFeatures
222
+ };
223
+ }
224
+
225
+ // src/core/order.ts
226
+ function globalImportance(e) {
227
+ const out = new Array(e.nFeatures).fill(0);
228
+ for (const row of e.values) {
229
+ for (let j = 0; j < e.nFeatures; j++) out[j] += Math.abs(row[j]);
230
+ }
231
+ return out.map((sum) => sum / e.nSamples);
232
+ }
233
+ function orderFeatures(importance) {
234
+ return importance.map((value, index) => ({ value, index })).sort((a, b) => b.value - a.value || a.index - b.index).map((entry) => entry.index);
235
+ }
236
+
237
+ // src/core/format.ts
238
+ var MINUS = "\u2212";
239
+ var EXPONENT_THRESHOLD = 1e-3;
240
+ var PERCENT_DECIMALS = 2;
241
+ function formatShapValue(v, decimals) {
242
+ if (v === 0) return "0";
243
+ return (v < 0 ? MINUS : "+") + magnitudeOf(v, decimals);
244
+ }
245
+ function formatLevel(v, decimals) {
246
+ if (v === 0) return "0";
247
+ return (v < 0 ? MINUS : "") + magnitudeOf(v, decimals);
248
+ }
249
+ function magnitudeOf(v, decimals) {
250
+ const magnitude = Math.abs(v);
251
+ if (decimals === void 0) {
252
+ return magnitude < EXPONENT_THRESHOLD ? magnitude.toExponential(0) : String(Number(magnitude.toPrecision(3)));
253
+ }
254
+ const places = decimals === "percent" ? PERCENT_DECIMALS : decimals;
255
+ const scaled = decimals === "percent" ? magnitude * 100 : magnitude;
256
+ const unit = decimals === "percent" ? "%" : "";
257
+ if (scaled < 0.5 * 10 ** -places) return magnitude.toExponential(0);
258
+ return scaled.toFixed(places) + unit;
259
+ }
260
+ function formatFeatureLabel(name) {
261
+ return name.replace(/_/g, " ");
262
+ }
263
+
264
+ // src/core/collapse.ts
265
+ function collapseToDisplay(featureNames, importance, order, maxDisplay, faithfulOtherRow) {
266
+ const p = order.length;
267
+ if (maxDisplay >= p) {
268
+ return {
269
+ rows: order.map((index) => ({
270
+ label: formatFeatureLabel(featureNames[index]),
271
+ featureIndex: index,
272
+ value: importance[index],
273
+ isOtherRow: false
274
+ })),
275
+ collapsedCount: 0
276
+ };
277
+ }
278
+ const realCount = faithfulOtherRow ? maxDisplay - 1 : maxDisplay;
279
+ const rows = order.slice(0, realCount).map((index) => ({
280
+ label: formatFeatureLabel(featureNames[index]),
281
+ featureIndex: index,
282
+ value: importance[index],
283
+ isOtherRow: false
284
+ }));
285
+ const collapsed = order.slice(realCount);
286
+ const collapsedValue = collapsed.reduce((sum, index) => sum + importance[index], 0);
287
+ rows.push({
288
+ label: faithfulOtherRow ? `Sum of ${collapsed.length} other features` : `${collapsed.length} other features`,
289
+ featureIndex: null,
290
+ value: collapsedValue,
291
+ isOtherRow: true
292
+ });
293
+ return { rows, collapsedCount: collapsed.length };
294
+ }
295
+
296
+ // src/core/ticks.ts
297
+ var MINUS2 = "\u2212";
298
+ var PX_TO_PT = 0.75;
299
+ var MAX_TICKS = 9;
300
+ var LADDER = [1, 2, 2.5, 5, 10];
301
+ var TICK_LENGTH = 5;
302
+ var TICK_LABEL_DY = 18;
303
+ var AXIS_TITLE_DY = 38;
304
+ function tickSpace(axisPx, labelPt) {
305
+ const space = Math.floor(axisPx * PX_TO_PT / (labelPt * 3));
306
+ return Math.min(MAX_TICKS, Math.max(1, space));
307
+ }
308
+ function niceTicks(min, max, maxTicks, { integer = false } = {}) {
309
+ const span = max - min;
310
+ if (!(span > 0) || !(maxTicks > 0)) return { ticks: [], step: 0 };
311
+ const rough = span / maxTicks;
312
+ let magnitude = 10 ** Math.floor(Math.log10(rough));
313
+ let step = 0;
314
+ for (let decade = 0; decade < 4 && step === 0; decade++, magnitude *= 10) {
315
+ for (const multiple of LADDER) {
316
+ const candidate = multiple * magnitude;
317
+ if (candidate < rough * (1 - 1e-9)) continue;
318
+ if (integer && (candidate < 1 || Math.abs(candidate - Math.round(candidate)) > 1e-9)) {
319
+ continue;
320
+ }
321
+ step = candidate;
322
+ break;
323
+ }
324
+ }
325
+ if (step === 0) return { ticks: [], step: 0 };
326
+ const first = Math.ceil(min / step - 1e-9) * step;
327
+ const count = Math.floor((max - first) / step + 1e-9) + 1;
328
+ const ticks = [];
329
+ for (let i = 0; i < count; i++) {
330
+ const value = first + i * step;
331
+ ticks.push(Math.abs(value) < step * 1e-9 ? 0 : value);
332
+ }
333
+ return { ticks, step };
334
+ }
335
+ function tickLabel(value, step, percent = false) {
336
+ const scaled = percent ? value * 100 : value;
337
+ const scaledStep = percent ? step * 100 : step;
338
+ let decimals = 0;
339
+ while (decimals < 6) {
340
+ const factor = 10 ** decimals;
341
+ if (Math.abs(scaledStep * factor - Math.round(scaledStep * factor)) < 1e-9) break;
342
+ decimals++;
343
+ }
344
+ const digits = Math.abs(scaled).toFixed(decimals);
345
+ const sign = scaled < 0 && Number(digits) !== 0 ? MINUS2 : "";
346
+ return sign + digits + (percent ? "%" : "");
347
+ }
348
+
349
+ // src/core/barLayout.ts
350
+ var POSITIVE_COLOR = "#ff0051";
351
+ var NEGATIVE_COLOR = "#008bfb";
352
+ var BAR_THICKNESS_RATIO = 0.7;
353
+ var AXIS_HEIGHT = 52;
354
+ var TICK_LABEL_PT = 11;
355
+ var TITLE_PT = 13;
356
+ var X_MARGIN = 0.05;
357
+ function barXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
358
+ const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT));
359
+ return {
360
+ xTicks: ticks.map((value) => ({ value, x: toX(value), label: tickLabel(value, step) })),
361
+ xSpine: { x1: marginLeft, x2: marginLeft + plotWidth, y: plotBottom },
362
+ xTitle: {
363
+ // _bar.py:143-150 builds this from the Explanation's transform history:
364
+ // "SHAP value" -> "|SHAP value|" -> "mean(|SHAP value|)".
365
+ text: "mean(|SHAP value|)",
366
+ x: marginLeft + plotWidth / 2,
367
+ y: plotBottom + AXIS_TITLE_DY,
368
+ fontSize: TITLE_PT
369
+ }
370
+ };
371
+ }
372
+ function barLayout(rows, opts) {
373
+ const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
374
+ const plotWidth = width - marginLeft - marginRight;
375
+ const values = rows.rows.map((r) => r.value);
376
+ const dataMin = Math.min(0, ...values);
377
+ const dataMax = Math.max(0, ...values);
378
+ const dataSpan = dataMax - dataMin;
379
+ const negative = values.some((v) => v < 0);
380
+ const autoMin = negative ? dataMin - dataSpan * X_MARGIN : dataMin;
381
+ const autoMax = dataMax + dataSpan * X_MARGIN;
382
+ const buffer = (autoMax - autoMin) * X_MARGIN;
383
+ const min = negative ? autoMin - buffer : autoMin;
384
+ const max = autoMax + buffer;
385
+ const span = max - min || 1;
386
+ const toX = (v) => marginLeft + (v - min) / span * plotWidth;
387
+ const xZero = toX(0);
388
+ const barHeight = rowHeight * BAR_THICKNESS_RATIO;
389
+ const inset = (rowHeight - barHeight) / 2;
390
+ const bars = rows.rows.map((row, i) => {
391
+ const rowTop = marginTop + i * rowHeight;
392
+ const end = toX(row.value);
393
+ const positive = row.value > 0;
394
+ return {
395
+ label: row.label,
396
+ featureIndex: row.featureIndex,
397
+ isOtherRow: row.isOtherRow,
398
+ value: row.value,
399
+ x: positive ? xZero : end,
400
+ y: rowTop + inset,
401
+ width: Math.abs(end - xZero),
402
+ height: barHeight,
403
+ color: positive ? POSITIVE_COLOR : NEGATIVE_COLOR,
404
+ centerY: rowTop + rowHeight / 2
405
+ };
406
+ });
407
+ return {
408
+ bars,
409
+ xDomain: [min, max],
410
+ xZero,
411
+ plotWidth,
412
+ plotBottom: marginTop + rows.rows.length * rowHeight,
413
+ ...barXAxis(min, max, toX, marginLeft, plotWidth, marginTop + rows.rows.length * rowHeight),
414
+ zeroLine: { x: xZero, y1: marginTop, y2: marginTop + rows.rows.length * rowHeight },
415
+ height: marginTop + rows.rows.length * rowHeight + AXIS_HEIGHT
416
+ };
417
+ }
418
+
419
+ // src/core/waterfallLayout.ts
420
+ var WATERFALL_HEAD_LENGTH_PX = 8;
421
+ var BAR_THICKNESS_RATIO2 = 0.8;
422
+ var AXIS_HEIGHT2 = 52;
423
+ var WATERFALL_TICK_LABEL_DY = 18;
424
+ var WATERFALL_BASE_LABEL_DY = 36;
425
+ var TICK_LABEL_PT2 = 13;
426
+ var colorFor = (value) => value < 0 ? NEGATIVE_COLOR : POSITIVE_COLOR;
427
+ var VALUE_LABEL_GAP = 6;
428
+ var VALUE_LABEL_FONT_SIZE = 12;
429
+ var GLYPH_WIDTH_RATIO = 0.6;
430
+ var INSIDE_PADDING = 4;
431
+ function placeValueLabel(value, startX, endX, gutterX, decimals) {
432
+ const text = formatShapValue(value, decimals);
433
+ const estimatedWidth = text.length * VALUE_LABEL_FONT_SIZE * GLYPH_WIDTH_RATIO;
434
+ const base = { estimatedWidth, text };
435
+ if (estimatedWidth + 2 * INSIDE_PADDING <= Math.abs(endX - startX)) {
436
+ return { ...base, x: (startX + endX) / 2, anchor: "middle", inside: true };
437
+ }
438
+ if (value >= 0) {
439
+ return { ...base, x: endX + VALUE_LABEL_GAP, anchor: "start", inside: false };
440
+ }
441
+ const outsideLeftEdge = endX - VALUE_LABEL_GAP - estimatedWidth;
442
+ if (outsideLeftEdge >= gutterX) {
443
+ return { ...base, x: endX - VALUE_LABEL_GAP, anchor: "end", inside: false };
444
+ }
445
+ return { ...base, x: startX + VALUE_LABEL_GAP, anchor: "start", inside: false };
446
+ }
447
+ function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow) {
448
+ if (!Number.isInteger(sampleIndex) || sampleIndex < 0 || sampleIndex >= explanation.nSamples) {
449
+ throw new RangeError(
450
+ `sampleIndex must identify a Sample from 0 to ${explanation.nSamples - 1}, received ${sampleIndex}`
451
+ );
452
+ }
453
+ if (!Number.isInteger(maxDisplay) || maxDisplay <= 0) {
454
+ throw new RangeError(`maxDisplay must be a positive integer, received ${maxDisplay}`);
455
+ }
456
+ const values = explanation.values[sampleIndex];
457
+ const baseValue = explanation.baseValues[sampleIndex];
458
+ const modelOutput = baseValue + values.reduce((sum, value) => sum + value, 0);
459
+ const order = orderFeatures(values.map(Math.abs));
460
+ const visibleLimit = Math.min(maxDisplay, explanation.nFeatures);
461
+ const hasOtherRow = visibleLimit < explanation.nFeatures;
462
+ const realCount = hasOtherRow && faithfulOtherRow ? visibleLimit - 1 : visibleLimit;
463
+ const rowCount = realCount + (hasOtherRow ? 1 : 0);
464
+ let location = modelOutput;
465
+ const rows = [];
466
+ for (let rank = 0; rank < realCount; rank++) {
467
+ const featureIndex = order[rank];
468
+ const value = values[featureIndex];
469
+ location -= value;
470
+ rows.push({
471
+ label: formatFeatureLabel(explanation.featureNames[featureIndex]),
472
+ featureIndex,
473
+ isOtherRow: false,
474
+ value,
475
+ left: location,
476
+ width: value,
477
+ row: rowCount - 1 - rank,
478
+ color: colorFor(value)
479
+ });
480
+ }
481
+ const collapsed = order.slice(realCount);
482
+ if (hasOtherRow) {
483
+ const value = collapsed.reduce((sum, featureIndex) => sum + values[featureIndex], 0);
484
+ rows.push({
485
+ label: `${collapsed.length} other features`,
486
+ featureIndex: null,
487
+ isOtherRow: true,
488
+ value,
489
+ left: baseValue,
490
+ width: value,
491
+ row: 0,
492
+ color: colorFor(value)
493
+ });
494
+ }
495
+ return {
496
+ rows,
497
+ baseValue,
498
+ modelOutput,
499
+ collapsedCount: hasOtherRow ? collapsed.length : 0
500
+ };
501
+ }
502
+ function waterfallLayout(valueRows, opts) {
503
+ const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
504
+ const plotWidth = width - marginLeft - marginRight;
505
+ const coordinates = [valueRows.baseValue, valueRows.modelOutput];
506
+ for (const row of valueRows.rows) coordinates.push(row.left, row.left + row.width);
507
+ const min = Math.min(...coordinates);
508
+ const max = Math.max(...coordinates);
509
+ const span = max - min || 1;
510
+ const toX = (value) => marginLeft + (value - min) / span * plotWidth;
511
+ const barHeight = rowHeight * BAR_THICKNESS_RATIO2;
512
+ const inset = (rowHeight - barHeight) / 2;
513
+ const arrows = valueRows.rows.map((row, index) => {
514
+ const y = marginTop + index * rowHeight + inset;
515
+ const centerY = marginTop + index * rowHeight + rowHeight / 2;
516
+ const startX = toX(row.left);
517
+ const endX = toX(row.left + row.width);
518
+ const headLength = Math.min(Math.abs(endX - startX), WATERFALL_HEAD_LENGTH_PX);
519
+ const neckX = row.width < 0 ? endX + headLength : endX - headLength;
520
+ const points = [
521
+ { x: startX, y },
522
+ { x: neckX, y },
523
+ { x: endX, y: centerY },
524
+ { x: neckX, y: y + barHeight },
525
+ { x: startX, y: y + barHeight }
526
+ ];
527
+ return {
528
+ ...row,
529
+ points,
530
+ startX,
531
+ endX,
532
+ y,
533
+ centerY,
534
+ height: barHeight,
535
+ headLength,
536
+ valueLabel: placeValueLabel(row.value, startX, endX, marginLeft, opts.decimals)
537
+ };
538
+ });
539
+ const plotBottom = marginTop + valueRows.rows.length * rowHeight;
540
+ const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT2));
541
+ const individualCount = valueRows.rows.filter((row) => !row.isOtherRow).length;
542
+ const hasOtherRow = individualCount < valueRows.rows.length;
543
+ const connectors = [];
544
+ for (let index = 0; index < individualCount; index++) {
545
+ if (!hasOtherRow && index + 4 >= individualCount) continue;
546
+ connectors.push({
547
+ x: arrows[index].startX,
548
+ y1: marginTop + index * rowHeight + inset,
549
+ y2: marginTop + (index + 1) * rowHeight + inset + barHeight
550
+ });
551
+ }
552
+ return {
553
+ arrows,
554
+ connectors,
555
+ xTicks: ticks.map((value) => ({
556
+ value,
557
+ x: toX(value),
558
+ label: tickLabel(value, step, opts.decimals === "percent")
559
+ })),
560
+ axisMarks: [
561
+ {
562
+ kind: "base",
563
+ value: valueRows.baseValue,
564
+ x: toX(valueRows.baseValue),
565
+ // axvline(base_values, 0, 1 / num_features) — one row tall, not full
566
+ // height. Drawn the full height it reads as a y axis the chart has not
567
+ // got, which is the whole reason SHAP hides the left spine.
568
+ y1: plotBottom - rowHeight,
569
+ y2: plotBottom,
570
+ label: `E[f(X)] = ${formatLevel(valueRows.baseValue, opts.decimals)}`
571
+ },
572
+ {
573
+ kind: "output",
574
+ value: valueRows.modelOutput,
575
+ x: toX(valueRows.modelOutput),
576
+ // axvline(fx, 0, 1) — the full height.
577
+ y1: marginTop,
578
+ y2: plotBottom,
579
+ label: `f(x) = ${formatLevel(valueRows.modelOutput, opts.decimals)}`
580
+ }
581
+ ],
582
+ separators: valueRows.rows.map((_, index) => ({
583
+ y: marginTop + index * rowHeight + rowHeight / 2,
584
+ x1: marginLeft,
585
+ x2: marginLeft + plotWidth
586
+ })),
587
+ xDomain: [min, max],
588
+ plotWidth,
589
+ plotLeft: marginLeft,
590
+ plotRight: marginLeft + plotWidth,
591
+ plotBottom,
592
+ height: plotBottom + AXIS_HEIGHT2
593
+ };
594
+ }
595
+
596
+ // src/core/colormaps.json
597
+ var colormaps_default = {
598
+ note: "256-entry sRGB lookup tables dumped from shap.plots.colors. Interpolate linearly between entries; do not re-derive from Lch.",
599
+ red_blue: [
600
+ "#008bfb",
601
+ "#008afb",
602
+ "#0089fa",
603
+ "#0089fa",
604
+ "#0088fa",
605
+ "#0088fa",
606
+ "#0087fa",
607
+ "#0087fa",
608
+ "#0086fa",
609
+ "#0086f9",
610
+ "#0085f9",
611
+ "#0085f9",
612
+ "#0084f9",
613
+ "#0083f8",
614
+ "#0083f8",
615
+ "#0082f8",
616
+ "#0082f8",
617
+ "#0081f8",
618
+ "#0081f7",
619
+ "#0080f7",
620
+ "#0080f7",
621
+ "#007ff6",
622
+ "#007ef6",
623
+ "#007ef6",
624
+ "#007df6",
625
+ "#007df5",
626
+ "#007cf5",
627
+ "#007bf5",
628
+ "#007bf4",
629
+ "#007af4",
630
+ "#007af4",
631
+ "#0079f3",
632
+ "#0078f3",
633
+ "#0078f2",
634
+ "#0077f2",
635
+ "#0076f2",
636
+ "#0076f1",
637
+ "#0075f1",
638
+ "#0075f0",
639
+ "#0074f0",
640
+ "#0073f0",
641
+ "#0073ef",
642
+ "#0072ef",
643
+ "#0071ee",
644
+ "#0071ee",
645
+ "#0070ed",
646
+ "#006fed",
647
+ "#066fec",
648
+ "#0f6eec",
649
+ "#196deb",
650
+ "#1e6deb",
651
+ "#236cea",
652
+ "#286bea",
653
+ "#2c6be9",
654
+ "#306ae9",
655
+ "#3369e8",
656
+ "#3769e8",
657
+ "#3a68e7",
658
+ "#3c67e6",
659
+ "#3f66e6",
660
+ "#4266e5",
661
+ "#4465e5",
662
+ "#4764e4",
663
+ "#4964e3",
664
+ "#4b63e3",
665
+ "#4d62e2",
666
+ "#5061e2",
667
+ "#5261e1",
668
+ "#5460e0",
669
+ "#565fe0",
670
+ "#575edf",
671
+ "#595ede",
672
+ "#5b5dde",
673
+ "#5d5cdd",
674
+ "#5f5bdc",
675
+ "#605bdb",
676
+ "#625adb",
677
+ "#6359da",
678
+ "#6558d9",
679
+ "#6757d9",
680
+ "#6856d8",
681
+ "#6a56d7",
682
+ "#6b55d6",
683
+ "#6d54d6",
684
+ "#6e53d5",
685
+ "#6f52d4",
686
+ "#7151d3",
687
+ "#7251d2",
688
+ "#7350d2",
689
+ "#754fd1",
690
+ "#764ed0",
691
+ "#774dcf",
692
+ "#794cce",
693
+ "#7a4bce",
694
+ "#7b4acd",
695
+ "#7c49cc",
696
+ "#7d48cb",
697
+ "#7e48ca",
698
+ "#8047c9",
699
+ "#8146c8",
700
+ "#8245c7",
701
+ "#8344c7",
702
+ "#8443c6",
703
+ "#8542c5",
704
+ "#8641c4",
705
+ "#8740c3",
706
+ "#883fc2",
707
+ "#893ec1",
708
+ "#8a3cc0",
709
+ "#8b3bbf",
710
+ "#8c3abe",
711
+ "#8d39bd",
712
+ "#8e38bc",
713
+ "#8f37bb",
714
+ "#9036ba",
715
+ "#9134b9",
716
+ "#9233b8",
717
+ "#9232b8",
718
+ "#9331b7",
719
+ "#942fb6",
720
+ "#952eb4",
721
+ "#962db3",
722
+ "#972bb2",
723
+ "#972ab1",
724
+ "#9828b0",
725
+ "#9927af",
726
+ "#9a25ae",
727
+ "#9b24ae",
728
+ "#9c23ad",
729
+ "#9d22ac",
730
+ "#9e21ac",
731
+ "#a020ab",
732
+ "#a11fab",
733
+ "#a21eaa",
734
+ "#a41daa",
735
+ "#a51ca9",
736
+ "#a61ba9",
737
+ "#a719a8",
738
+ "#a918a8",
739
+ "#aa17a7",
740
+ "#ab16a7",
741
+ "#ac14a6",
742
+ "#ae13a6",
743
+ "#af11a5",
744
+ "#b010a5",
745
+ "#b10ea4",
746
+ "#b20ca4",
747
+ "#b40aa3",
748
+ "#b507a2",
749
+ "#b605a2",
750
+ "#b703a1",
751
+ "#b802a1",
752
+ "#b900a0",
753
+ "#bb00a0",
754
+ "#bc009f",
755
+ "#bd009e",
756
+ "#be009e",
757
+ "#bf009d",
758
+ "#c0009d",
759
+ "#c1009c",
760
+ "#c2009b",
761
+ "#c3009b",
762
+ "#c4009a",
763
+ "#c50099",
764
+ "#c60099",
765
+ "#c70098",
766
+ "#c90097",
767
+ "#ca0097",
768
+ "#cb0096",
769
+ "#cc0096",
770
+ "#cd0095",
771
+ "#cd0094",
772
+ "#ce0093",
773
+ "#cf0093",
774
+ "#d00092",
775
+ "#d10091",
776
+ "#d20091",
777
+ "#d30090",
778
+ "#d4008f",
779
+ "#d5008f",
780
+ "#d6008e",
781
+ "#d7008d",
782
+ "#d8008d",
783
+ "#d9008c",
784
+ "#da008b",
785
+ "#da008a",
786
+ "#db008a",
787
+ "#dc0089",
788
+ "#dd0088",
789
+ "#de0087",
790
+ "#df0087",
791
+ "#df0086",
792
+ "#e00085",
793
+ "#e10084",
794
+ "#e20084",
795
+ "#e30083",
796
+ "#e30082",
797
+ "#e40081",
798
+ "#e50081",
799
+ "#e60080",
800
+ "#e6007f",
801
+ "#e7007e",
802
+ "#e8007d",
803
+ "#e9007d",
804
+ "#e9007c",
805
+ "#ea007b",
806
+ "#eb007a",
807
+ "#eb007a",
808
+ "#ec0079",
809
+ "#ed0078",
810
+ "#ed0077",
811
+ "#ee0076",
812
+ "#ef0076",
813
+ "#ef0075",
814
+ "#f00074",
815
+ "#f10073",
816
+ "#f10072",
817
+ "#f20071",
818
+ "#f20071",
819
+ "#f30070",
820
+ "#f4006f",
821
+ "#f4006e",
822
+ "#f5006d",
823
+ "#f5006d",
824
+ "#f6006c",
825
+ "#f6006b",
826
+ "#f7006a",
827
+ "#f70069",
828
+ "#f80068",
829
+ "#f80068",
830
+ "#f90067",
831
+ "#f90066",
832
+ "#fa0065",
833
+ "#fa0064",
834
+ "#fb0063",
835
+ "#fb0062",
836
+ "#fc0062",
837
+ "#fc0061",
838
+ "#fd0060",
839
+ "#fd005f",
840
+ "#fd005e",
841
+ "#fe005d",
842
+ "#fe005c",
843
+ "#fe005c",
844
+ "#ff005b",
845
+ "#ff005a",
846
+ "#ff0059",
847
+ "#ff0058",
848
+ "#ff0057",
849
+ "#ff0056",
850
+ "#ff0055",
851
+ "#ff0055",
852
+ "#ff0054",
853
+ "#ff0053",
854
+ "#ff0052",
855
+ "#ff0051"
856
+ ],
857
+ red_white_blue: [
858
+ "#008bfb",
859
+ "#028bfb",
860
+ "#048cfb",
861
+ "#068dfb",
862
+ "#088efb",
863
+ "#0a8ffb",
864
+ "#0c90fb",
865
+ "#0e91fb",
866
+ "#1092fb",
867
+ "#1293fb",
868
+ "#1494fb",
869
+ "#1695fb",
870
+ "#1896fb",
871
+ "#1a96fb",
872
+ "#1c97fb",
873
+ "#1e98fb",
874
+ "#2099fb",
875
+ "#229afb",
876
+ "#249bfb",
877
+ "#269cfb",
878
+ "#289dfb",
879
+ "#2a9efb",
880
+ "#2c9ffb",
881
+ "#2ea0fc",
882
+ "#30a1fc",
883
+ "#32a2fc",
884
+ "#34a2fc",
885
+ "#36a3fc",
886
+ "#38a4fc",
887
+ "#3aa5fc",
888
+ "#3ca6fc",
889
+ "#3ea7fc",
890
+ "#40a8fc",
891
+ "#42a9fc",
892
+ "#44aafc",
893
+ "#46abfc",
894
+ "#48acfc",
895
+ "#4aadfc",
896
+ "#4cadfc",
897
+ "#4eaefc",
898
+ "#50affc",
899
+ "#52b0fc",
900
+ "#54b1fc",
901
+ "#56b2fc",
902
+ "#58b3fc",
903
+ "#5ab4fc",
904
+ "#5cb5fc",
905
+ "#5eb6fc",
906
+ "#60b7fc",
907
+ "#62b8fc",
908
+ "#65b8fc",
909
+ "#67b9fc",
910
+ "#69bafc",
911
+ "#6bbbfd",
912
+ "#6dbcfd",
913
+ "#6fbdfd",
914
+ "#71befd",
915
+ "#73bffd",
916
+ "#75c0fd",
917
+ "#77c1fd",
918
+ "#79c2fd",
919
+ "#7bc3fd",
920
+ "#7dc3fd",
921
+ "#7fc4fd",
922
+ "#81c5fd",
923
+ "#83c6fd",
924
+ "#85c7fd",
925
+ "#87c8fd",
926
+ "#89c9fd",
927
+ "#8bcafd",
928
+ "#8dcbfd",
929
+ "#8fccfd",
930
+ "#91cdfd",
931
+ "#93cefd",
932
+ "#95cefd",
933
+ "#97cffd",
934
+ "#99d0fd",
935
+ "#9bd1fd",
936
+ "#9dd2fd",
937
+ "#9fd3fd",
938
+ "#a1d4fd",
939
+ "#a3d5fd",
940
+ "#a5d6fe",
941
+ "#a7d7fe",
942
+ "#a9d8fe",
943
+ "#abd9fe",
944
+ "#add9fe",
945
+ "#afdafe",
946
+ "#b1dbfe",
947
+ "#b3dcfe",
948
+ "#b5ddfe",
949
+ "#b7defe",
950
+ "#b9dffe",
951
+ "#bbe0fe",
952
+ "#bde1fe",
953
+ "#bfe2fe",
954
+ "#c1e3fe",
955
+ "#c3e4fe",
956
+ "#c5e5fe",
957
+ "#c7e5fe",
958
+ "#c9e6fe",
959
+ "#cbe7fe",
960
+ "#cde8fe",
961
+ "#cfe9fe",
962
+ "#d1eafe",
963
+ "#d3ebfe",
964
+ "#d5ecfe",
965
+ "#d7edfe",
966
+ "#d9eefe",
967
+ "#dbeffe",
968
+ "#ddf0fe",
969
+ "#dff0fe",
970
+ "#e1f1ff",
971
+ "#e3f2ff",
972
+ "#e5f3ff",
973
+ "#e7f4ff",
974
+ "#e9f5ff",
975
+ "#ebf6ff",
976
+ "#edf7ff",
977
+ "#eff8ff",
978
+ "#f1f9ff",
979
+ "#f3faff",
980
+ "#f5fbff",
981
+ "#f7fbff",
982
+ "#f9fcff",
983
+ "#fbfdff",
984
+ "#fdfeff",
985
+ "#ffffff",
986
+ "#ffffff",
987
+ "#fffdfe",
988
+ "#fffbfc",
989
+ "#fff9fb",
990
+ "#fff7fa",
991
+ "#fff5f8",
992
+ "#fff3f7",
993
+ "#fff1f6",
994
+ "#ffeff4",
995
+ "#ffedf3",
996
+ "#ffebf1",
997
+ "#ffe9f0",
998
+ "#ffe7ef",
999
+ "#ffe5ed",
1000
+ "#ffe3ec",
1001
+ "#ffe1eb",
1002
+ "#ffdfe9",
1003
+ "#ffdde8",
1004
+ "#ffdbe7",
1005
+ "#ffd9e5",
1006
+ "#ffd7e4",
1007
+ "#ffd5e2",
1008
+ "#ffd3e1",
1009
+ "#ffd1e0",
1010
+ "#ffcfde",
1011
+ "#ffcddd",
1012
+ "#ffcbdc",
1013
+ "#ffc9da",
1014
+ "#ffc7d9",
1015
+ "#ffc5d7",
1016
+ "#ffc3d6",
1017
+ "#ffc1d5",
1018
+ "#ffbfd3",
1019
+ "#ffbdd2",
1020
+ "#ffbbd1",
1021
+ "#ffb9cf",
1022
+ "#ffb7ce",
1023
+ "#ffb5cc",
1024
+ "#ffb3cb",
1025
+ "#ffb1ca",
1026
+ "#ffafc8",
1027
+ "#ffadc7",
1028
+ "#ffabc6",
1029
+ "#ffa9c4",
1030
+ "#ffa7c3",
1031
+ "#ffa5c1",
1032
+ "#ffa3c0",
1033
+ "#ffa1bf",
1034
+ "#ff9fbd",
1035
+ "#ff9dbc",
1036
+ "#ff9bbb",
1037
+ "#ff99b9",
1038
+ "#ff97b8",
1039
+ "#ff95b7",
1040
+ "#ff93b5",
1041
+ "#ff91b4",
1042
+ "#ff8fb2",
1043
+ "#ff8db1",
1044
+ "#ff8bb0",
1045
+ "#ff89ae",
1046
+ "#ff87ad",
1047
+ "#ff85ac",
1048
+ "#ff83aa",
1049
+ "#ff81a9",
1050
+ "#ff7fa7",
1051
+ "#ff7da6",
1052
+ "#ff7ba5",
1053
+ "#ff79a3",
1054
+ "#ff77a2",
1055
+ "#ff75a1",
1056
+ "#ff739f",
1057
+ "#ff719e",
1058
+ "#ff6f9c",
1059
+ "#ff6d9b",
1060
+ "#ff6b9a",
1061
+ "#ff6998",
1062
+ "#ff6797",
1063
+ "#ff6596",
1064
+ "#ff6294",
1065
+ "#ff6093",
1066
+ "#ff5e92",
1067
+ "#ff5c90",
1068
+ "#ff5a8f",
1069
+ "#ff588d",
1070
+ "#ff568c",
1071
+ "#ff548b",
1072
+ "#ff5289",
1073
+ "#ff5088",
1074
+ "#ff4e87",
1075
+ "#ff4c85",
1076
+ "#ff4a84",
1077
+ "#ff4882",
1078
+ "#ff4681",
1079
+ "#ff4480",
1080
+ "#ff427e",
1081
+ "#ff407d",
1082
+ "#ff3e7c",
1083
+ "#ff3c7a",
1084
+ "#ff3a79",
1085
+ "#ff3877",
1086
+ "#ff3676",
1087
+ "#ff3475",
1088
+ "#ff3273",
1089
+ "#ff3072",
1090
+ "#ff2e71",
1091
+ "#ff2c6f",
1092
+ "#ff2a6e",
1093
+ "#ff286d",
1094
+ "#ff266b",
1095
+ "#ff246a",
1096
+ "#ff2268",
1097
+ "#ff2067",
1098
+ "#ff1e66",
1099
+ "#ff1c64",
1100
+ "#ff1a63",
1101
+ "#ff1862",
1102
+ "#ff1660",
1103
+ "#ff145f",
1104
+ "#ff125d",
1105
+ "#ff105c",
1106
+ "#ff0e5b",
1107
+ "#ff0c59",
1108
+ "#ff0a58",
1109
+ "#ff0857",
1110
+ "#ff0655",
1111
+ "#ff0454",
1112
+ "#ff0252",
1113
+ "#ff0051"
1114
+ ],
1115
+ nan_grey: "#848484"
1116
+ };
1117
+
1118
+ // src/core/colormap.ts
1119
+ var tables = colormaps_default;
1120
+ function channel(hex, offset) {
1121
+ return Number.parseInt(hex.slice(offset, offset + 2), 16);
1122
+ }
1123
+ function hexByte(value) {
1124
+ return Math.round(value).toString(16).padStart(2, "0");
1125
+ }
1126
+ function sampleColormap(name, t) {
1127
+ if (!Number.isFinite(t)) {
1128
+ throw new RangeError(`colormap position must be finite, received ${String(t)}`);
1129
+ }
1130
+ const table = tables[name];
1131
+ if (!table) throw new RangeError(`unknown colormap ${String(name)}`);
1132
+ const position = Math.max(0, Math.min(1, t)) * (table.length - 1);
1133
+ const lowerIndex = Math.floor(position);
1134
+ const upperIndex = Math.ceil(position);
1135
+ const fraction = position - lowerIndex;
1136
+ const lower = table[lowerIndex];
1137
+ const upper = table[upperIndex];
1138
+ const red = channel(lower, 1) + (channel(upper, 1) - channel(lower, 1)) * fraction;
1139
+ const green = channel(lower, 3) + (channel(upper, 3) - channel(lower, 3)) * fraction;
1140
+ const blue = channel(lower, 5) + (channel(upper, 5) - channel(lower, 5)) * fraction;
1141
+ return `#${hexByte(red)}${hexByte(green)}${hexByte(blue)}`;
1142
+ }
1143
+
1144
+ // src/core/beeswarmLayout.ts
1145
+ var BEESWARM_MISSING_COLOR = "#777777";
1146
+ var BEESWARM_ROW_HEIGHT = 0.4;
1147
+ var NBINS = 100;
1148
+ var AXIS_HEIGHT3 = 74;
1149
+ var TICK_LABEL_PT3 = 11;
1150
+ var TITLE_PT2 = 13;
1151
+ var X_MARGIN2 = 0.05;
1152
+ function percentile(values, percent) {
1153
+ const finite = values.filter(Number.isFinite).sort((a, b) => a - b);
1154
+ if (finite.length === 0) return 0;
1155
+ const position = (finite.length - 1) * percent / 100;
1156
+ const lower = Math.floor(position);
1157
+ const upper = Math.ceil(position);
1158
+ const fraction = position - lower;
1159
+ return finite[lower] + (finite[upper] - finite[lower]) * fraction;
1160
+ }
1161
+ function colorDomain(featureValues) {
1162
+ let vmin = percentile(featureValues, 5);
1163
+ let vmax = percentile(featureValues, 95);
1164
+ if (vmin === vmax) {
1165
+ vmin = percentile(featureValues, 1);
1166
+ vmax = percentile(featureValues, 99);
1167
+ }
1168
+ if (vmin === vmax) {
1169
+ const finite = featureValues.filter(Number.isFinite);
1170
+ if (finite.length > 0) {
1171
+ vmin = Math.min(...finite);
1172
+ vmax = Math.max(...finite);
1173
+ }
1174
+ }
1175
+ if (vmin > vmax) vmin = vmax;
1176
+ return [vmin, vmax];
1177
+ }
1178
+ function seededRandom(seed) {
1179
+ let state = Math.trunc(seed) >>> 0;
1180
+ return () => {
1181
+ state = state + 1831565813 >>> 0;
1182
+ let value = state;
1183
+ value = Math.imul(value ^ value >>> 15, value | 1);
1184
+ value ^= value + Math.imul(value ^ value >>> 7, value | 61);
1185
+ return ((value ^ value >>> 14) >>> 0) / 4294967296;
1186
+ };
1187
+ }
1188
+ function numpyRound(value) {
1189
+ const lower = Math.floor(value);
1190
+ if (value - lower === 0.5) return lower % 2 === 0 ? lower : lower + 1;
1191
+ return Math.round(value);
1192
+ }
1193
+ function spreadPoints(xs, rowIndex, seed) {
1194
+ const min = Math.min(...xs);
1195
+ const max = Math.max(...xs);
1196
+ const quantized = xs.map((x) => numpyRound(NBINS * (x - min) / (max - min + 1e-8)));
1197
+ const random = seededRandom(seed);
1198
+ const order = quantized.map((bin, index) => ({ bin, index, tieBreak: random() })).sort((a, b) => a.bin - b.bin || a.tieBreak - b.tieBreak).map((entry) => entry.index);
1199
+ const offsets = new Array(xs.length).fill(0);
1200
+ let layer = 0;
1201
+ let lastBin = -1;
1202
+ for (const index of order) {
1203
+ const bin = quantized[index];
1204
+ if (bin !== lastBin) layer = 0;
1205
+ offsets[index] = Math.ceil(layer / 2) * (layer % 2 * 2 - 1);
1206
+ layer += 1;
1207
+ lastBin = bin;
1208
+ }
1209
+ const maxPositiveOffset = Math.max(0, ...offsets);
1210
+ const scale = 0.9 * (BEESWARM_ROW_HEIGHT / (maxPositiveOffset + 1));
1211
+ return offsets.map((offset) => rowIndex + offset * scale);
1212
+ }
1213
+ function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSort = "importance") {
1214
+ if (!Number.isInteger(maxDisplay) || maxDisplay <= 0) {
1215
+ throw new RangeError(`maxDisplay must be a positive integer, received ${maxDisplay}`);
1216
+ }
1217
+ if (!Number.isFinite(seed)) {
1218
+ throw new RangeError(`seed must be finite, received ${String(seed)}`);
1219
+ }
1220
+ const importance = globalImportance(explanation);
1221
+ const order = orderFeatures(importance);
1222
+ const display = sortDisplayRows(
1223
+ collapseToDisplay(
1224
+ explanation.featureNames,
1225
+ importance,
1226
+ order,
1227
+ maxDisplay,
1228
+ faithfulOtherRow
1229
+ ),
1230
+ rowSort,
1231
+ explanation.data
1232
+ );
1233
+ const visibleFeatures = new Set(
1234
+ display.rows.flatMap((row) => row.featureIndex === null ? [] : [row.featureIndex])
1235
+ );
1236
+ const collapsedFeatures = order.filter((featureIndex) => !visibleFeatures.has(featureIndex));
1237
+ const rows = display.rows.map((displayRow, displayIndex) => {
1238
+ const featureIndices = displayRow.featureIndex === null ? collapsedFeatures : [displayRow.featureIndex];
1239
+ const colorFeatureIndices = displayRow.featureIndex === null && faithfulOtherRow ? collapsedFeatures.slice(0, 1) : featureIndices;
1240
+ const xs = explanation.values.map((sample) => featureIndices.reduce((sum, featureIndex) => sum + sample[featureIndex], 0));
1241
+ const featureValues = explanation.data.map((sample) => colorFeatureIndices.reduce((sum, featureIndex) => sum + sample[featureIndex], 0));
1242
+ const rowIndex = display.rows.length - 1 - displayIndex;
1243
+ const ys = spreadPoints(xs, rowIndex, seed + Math.imul(displayIndex + 1, 2654435761));
1244
+ const [vmin, vmax] = colorDomain(featureValues);
1245
+ const colorSpan = vmax - vmin;
1246
+ return {
1247
+ label: displayRow.label,
1248
+ featureIndex: displayRow.featureIndex,
1249
+ isOtherRow: displayRow.isOtherRow,
1250
+ rowIndex,
1251
+ vmin,
1252
+ vmax,
1253
+ points: xs.map((x, sampleIndex) => {
1254
+ const featureValue = featureValues[sampleIndex];
1255
+ if (!Number.isFinite(featureValue)) {
1256
+ return {
1257
+ sampleIndex,
1258
+ x,
1259
+ y: ys[sampleIndex],
1260
+ featureValue,
1261
+ colorValue: null,
1262
+ color: BEESWARM_MISSING_COLOR
1263
+ };
1264
+ }
1265
+ const colorValue = Math.max(vmin, Math.min(vmax, featureValue));
1266
+ const normalized = colorSpan === 0 ? 0 : (colorValue - vmin) / colorSpan;
1267
+ return {
1268
+ sampleIndex,
1269
+ x,
1270
+ y: ys[sampleIndex],
1271
+ featureValue,
1272
+ colorValue,
1273
+ color: sampleColormap("red_blue", normalized)
1274
+ };
1275
+ })
1276
+ };
1277
+ });
1278
+ return { rows, collapsedCount: display.collapsedCount };
1279
+ }
1280
+ function beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
1281
+ const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT3));
1282
+ return {
1283
+ xTicks: ticks.map((value) => ({ value, x: toX(value), label: tickLabel(value, step) })),
1284
+ xSpine: { x1: marginLeft, x2: marginLeft + plotWidth, y: plotBottom },
1285
+ xTitle: {
1286
+ // _labels.py:5, labels["VALUE"].
1287
+ text: "SHAP value (impact on model output)",
1288
+ x: marginLeft + plotWidth / 2,
1289
+ y: plotBottom + AXIS_TITLE_DY,
1290
+ fontSize: TITLE_PT2
1291
+ }
1292
+ };
1293
+ }
1294
+ function beeswarmLayout(valueRows, opts) {
1295
+ const { width, rowHeight, marginLeft, marginRight, marginTop, dotRadius } = opts;
1296
+ const plotWidth = width - marginLeft - marginRight;
1297
+ const values = valueRows.rows.flatMap((row) => row.points.map((point) => point.x));
1298
+ const dataMin = Math.min(0, ...values);
1299
+ const dataMax = Math.max(0, ...values);
1300
+ const margin = (dataMax - dataMin) * X_MARGIN2;
1301
+ const min = dataMin - margin;
1302
+ const max = dataMax + margin;
1303
+ const span = max - min || 1;
1304
+ const toX = (value) => marginLeft + (value - min) / span * plotWidth;
1305
+ const highestRowIndex = Math.max(0, valueRows.rows.length - 1);
1306
+ const rows = valueRows.rows.map((row) => {
1307
+ const centerY = marginTop + (highestRowIndex - row.rowIndex + 0.5) * rowHeight;
1308
+ return {
1309
+ ...row,
1310
+ centerY,
1311
+ points: row.points.map((point) => ({
1312
+ ...point,
1313
+ valueX: point.x,
1314
+ valueY: point.y,
1315
+ x: toX(point.x),
1316
+ y: centerY - (point.y - row.rowIndex) * rowHeight,
1317
+ radius: dotRadius
1318
+ }))
1319
+ };
1320
+ });
1321
+ const plotBottom = marginTop + valueRows.rows.length * rowHeight;
1322
+ return {
1323
+ rows,
1324
+ xDomain: [min, max],
1325
+ xZero: toX(0),
1326
+ ...beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom),
1327
+ plotWidth,
1328
+ plotBottom,
1329
+ height: plotBottom + AXIS_HEIGHT3
1330
+ };
1331
+ }
1332
+
1333
+ // src/core/heatmapLayout.ts
1334
+ var FX_TOP = 8;
1335
+ var FX_BOTTOM_GAP = 12;
1336
+ var SEPARATOR_GAP = 4;
1337
+ var SIDE_BAR_GAP = 10;
1338
+ var SIDE_BAR_RIGHT_INSET = 40;
1339
+ var SIDE_BAR_HEIGHT_RATIO = 0.6;
1340
+ var AXIS_HEIGHT4 = 52;
1341
+ var TICK_LABEL_PT4 = 10;
1342
+ var Y_TICK_LENGTH = 5;
1343
+ function percentile2(values, fraction) {
1344
+ if (values.length === 0) return 0;
1345
+ const sorted = [...values].sort((a, b) => a - b);
1346
+ const position = (sorted.length - 1) * fraction;
1347
+ const lower = Math.floor(position);
1348
+ const upper = Math.ceil(position);
1349
+ const weight = position - lower;
1350
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * weight;
1351
+ }
1352
+ function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "importance") {
1353
+ if (!Number.isInteger(maxDisplay) || maxDisplay <= 0) {
1354
+ throw new RangeError(`maxDisplay must be a positive integer, received ${maxDisplay}`);
1355
+ }
1356
+ const importance = globalImportance(explanation);
1357
+ const featureOrder = orderFeatures(importance);
1358
+ const display = sortDisplayRows(
1359
+ collapseToDisplay(
1360
+ explanation.featureNames,
1361
+ importance,
1362
+ featureOrder,
1363
+ maxDisplay,
1364
+ faithfulOtherRow
1365
+ ),
1366
+ rowSort,
1367
+ explanation.data
1368
+ );
1369
+ const displayedFeatures = new Set(
1370
+ display.rows.flatMap((row) => row.featureIndex === null ? [] : [row.featureIndex])
1371
+ );
1372
+ const collapsedFeatures = featureOrder.filter((index) => !displayedFeatures.has(index));
1373
+ const columns = explanation.values.map((sample, sampleIndex) => {
1374
+ var _a, _b;
1375
+ return {
1376
+ sampleIndex,
1377
+ sampleId: (_a = explanation.sampleIds) == null ? void 0 : _a[sampleIndex],
1378
+ sampleLabel: (_b = explanation.sampleLabels) == null ? void 0 : _b[sampleIndex],
1379
+ total: sample.reduce((sum, value) => sum + value, 0)
1380
+ };
1381
+ }).sort((a, b) => b.total - a.total || a.sampleIndex - b.sampleIndex);
1382
+ const uncolouredRows = display.rows.map((displayRow) => {
1383
+ const featureIndices = displayRow.featureIndex === null ? collapsedFeatures : [displayRow.featureIndex];
1384
+ return {
1385
+ displayRow,
1386
+ cells: columns.map((column) => ({
1387
+ sampleIndex: column.sampleIndex,
1388
+ value: featureIndices.reduce(
1389
+ (sum, featureIndex) => sum + explanation.values[column.sampleIndex][featureIndex],
1390
+ 0
1391
+ )
1392
+ }))
1393
+ };
1394
+ });
1395
+ const allCells = uncolouredRows.flatMap((row) => row.cells.map((cell) => cell.value));
1396
+ const lower = percentile2(allCells, 0.01);
1397
+ const upper = percentile2(allCells, 0.99);
1398
+ const limit = Math.max(-lower, upper, 0);
1399
+ const vmin = limit === 0 ? 0 : -limit;
1400
+ const vmax = limit;
1401
+ const largestImportance = Math.max(0, ...display.rows.map((row) => row.value));
1402
+ const rows = uncolouredRows.map(({ displayRow, cells }) => ({
1403
+ label: displayRow.label,
1404
+ featureIndex: displayRow.featureIndex,
1405
+ isOtherRow: displayRow.isOtherRow,
1406
+ importance: displayRow.value,
1407
+ sideBarValue: largestImportance === 0 ? 0 : displayRow.value / largestImportance,
1408
+ cells: cells.map((cell) => {
1409
+ const colorValue = limit === 0 ? 0 : Math.max(-limit, Math.min(limit, cell.value));
1410
+ const normalized = limit === 0 ? 0.5 : (colorValue + limit) / (2 * limit);
1411
+ return {
1412
+ ...cell,
1413
+ colorValue,
1414
+ color: sampleColormap("red_white_blue", normalized)
1415
+ };
1416
+ })
1417
+ }));
1418
+ return {
1419
+ rows,
1420
+ columns,
1421
+ fxLine: columns.map((column) => column.total),
1422
+ vmin,
1423
+ vmax,
1424
+ collapsedCount: display.collapsedCount,
1425
+ sampleLabelColumn: explanation.sampleLabelColumn
1426
+ };
1427
+ }
1428
+ function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom) {
1429
+ const { ticks, step } = niceTicks(
1430
+ -0.5,
1431
+ sampleCount - 0.5,
1432
+ tickSpace(plotWidth, TICK_LABEL_PT4),
1433
+ { integer: true }
1434
+ );
1435
+ return {
1436
+ xTicks: ticks.map((value) => ({
1437
+ value,
1438
+ x: marginLeft + (value + 0.5) * cellWidth,
1439
+ label: tickLabel(value, step)
1440
+ })),
1441
+ xSpine: null,
1442
+ xTitle: {
1443
+ text: "Instances",
1444
+ x: marginLeft + plotWidth / 2,
1445
+ y: plotBottom + AXIS_TITLE_DY,
1446
+ fontSize: TICK_LABEL_PT4
1447
+ }
1448
+ };
1449
+ }
1450
+ function heatmapLayout(valueRows, opts) {
1451
+ const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
1452
+ const plotWidth = width - marginLeft - marginRight;
1453
+ const cellWidth = valueRows.columns.length === 0 ? 0 : plotWidth / valueRows.columns.length;
1454
+ const gridRight = marginLeft + plotWidth;
1455
+ const plotBottom = marginTop + valueRows.rows.length * rowHeight;
1456
+ const sideBarWidth = Math.max(0, marginRight - SIDE_BAR_RIGHT_INSET - SIDE_BAR_GAP);
1457
+ const columns = valueRows.columns.map((column, index) => ({
1458
+ ...column,
1459
+ x: marginLeft + index * cellWidth,
1460
+ centerX: marginLeft + (index + 0.5) * cellWidth,
1461
+ width: cellWidth
1462
+ }));
1463
+ const rows = valueRows.rows.map((row, rowIndex) => {
1464
+ const y = marginTop + rowIndex * rowHeight;
1465
+ const centerY = y + rowHeight / 2;
1466
+ const barHeight = rowHeight * SIDE_BAR_HEIGHT_RATIO;
1467
+ return {
1468
+ ...row,
1469
+ centerY,
1470
+ cells: row.cells.map((cell, columnIndex) => ({
1471
+ ...cell,
1472
+ x: marginLeft + columnIndex * cellWidth,
1473
+ y,
1474
+ width: cellWidth,
1475
+ height: rowHeight
1476
+ })),
1477
+ sideBar: {
1478
+ value: row.sideBarValue,
1479
+ x: gridRight + SIDE_BAR_GAP,
1480
+ y: centerY - barHeight / 2,
1481
+ width: row.sideBarValue * sideBarWidth,
1482
+ height: barHeight
1483
+ }
1484
+ };
1485
+ });
1486
+ const fxMin = Math.min(0, ...valueRows.fxLine);
1487
+ const fxMax = Math.max(0, ...valueRows.fxLine);
1488
+ const fxSpan = fxMax - fxMin || 1;
1489
+ const fxBottom = Math.max(FX_TOP, marginTop - FX_BOTTOM_GAP);
1490
+ const toY = (value) => FX_TOP + (fxMax - value) / fxSpan * (fxBottom - FX_TOP);
1491
+ const fxLine = columns.map((column, index) => ({
1492
+ sampleIndex: column.sampleIndex,
1493
+ sampleId: column.sampleId,
1494
+ value: valueRows.fxLine[index],
1495
+ x: column.centerX,
1496
+ y: toY(valueRows.fxLine[index])
1497
+ }));
1498
+ const axisValues = [fxMax, 0, fxMin].filter(
1499
+ (value, index, values) => values.indexOf(value) === index
1500
+ );
1501
+ return {
1502
+ rows,
1503
+ columns,
1504
+ fxLine,
1505
+ fxAxisMarks: axisValues.map((value) => ({
1506
+ value,
1507
+ y: toY(value),
1508
+ label: formatShapValue(value)
1509
+ })),
1510
+ fxDomain: [fxMin, fxMax],
1511
+ separatorY: marginTop - SEPARATOR_GAP,
1512
+ gridLeft: marginLeft,
1513
+ gridRight,
1514
+ gridTop: marginTop,
1515
+ plotBottom,
1516
+ spines: {
1517
+ left: { x: marginLeft, y1: marginTop, y2: plotBottom },
1518
+ right: { x: gridRight, y1: marginTop, y2: plotBottom }
1519
+ },
1520
+ yTicks: rows.map((row) => ({
1521
+ y: row.centerY,
1522
+ x1: marginLeft - Y_TICK_LENGTH,
1523
+ x2: marginLeft
1524
+ })),
1525
+ ...heatmapXAxis(valueRows.columns.length, marginLeft, cellWidth, plotWidth, plotBottom),
1526
+ sampleLabelColumn: valueRows.sampleLabelColumn,
1527
+ plotWidth,
1528
+ cellWidth,
1529
+ height: plotBottom + AXIS_HEIGHT4
1530
+ };
1531
+ }
1532
+
1533
+ export {
1534
+ UnsupportedContractVersionError,
1535
+ InvalidExplanationError,
1536
+ genusOf,
1537
+ groupByGenus,
1538
+ aggregateByGenus,
1539
+ groupExplanationByGenus,
1540
+ sortDisplayRows,
1541
+ parseExplanation,
1542
+ globalImportance,
1543
+ orderFeatures,
1544
+ formatShapValue,
1545
+ formatLevel,
1546
+ formatFeatureLabel,
1547
+ collapseToDisplay,
1548
+ TICK_LENGTH,
1549
+ TICK_LABEL_DY,
1550
+ POSITIVE_COLOR,
1551
+ NEGATIVE_COLOR,
1552
+ barLayout,
1553
+ WATERFALL_HEAD_LENGTH_PX,
1554
+ WATERFALL_TICK_LABEL_DY,
1555
+ WATERFALL_BASE_LABEL_DY,
1556
+ waterfallRows,
1557
+ waterfallLayout,
1558
+ sampleColormap,
1559
+ BEESWARM_MISSING_COLOR,
1560
+ BEESWARM_ROW_HEIGHT,
1561
+ beeswarmRows,
1562
+ beeswarmLayout,
1563
+ heatmapRows,
1564
+ heatmapLayout
1565
+ };
1566
+ //# sourceMappingURL=chunk-OXFKP5I3.js.map