shap-svg 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,6 +34,9 @@ __export(index_exports, {
34
34
  beeswarmLayout: () => beeswarmLayout,
35
35
  beeswarmRows: () => beeswarmRows,
36
36
  collapseToDisplay: () => collapseToDisplay,
37
+ colorBarExtent: () => colorBarExtent,
38
+ colorBarLayout: () => colorBarLayout,
39
+ fitColorBar: () => fitColorBar,
37
40
  formatFeatureLabel: () => formatFeatureLabel,
38
41
  formatLevel: () => formatLevel,
39
42
  formatShapValue: () => formatShapValue,
@@ -45,7 +48,10 @@ __export(index_exports, {
45
48
  heatmapRows: () => heatmapRows,
46
49
  orderFeatures: () => orderFeatures,
47
50
  parseExplanation: () => parseExplanation,
51
+ resolveLabels: () => resolveLabels,
48
52
  sampleColormap: () => sampleColormap,
53
+ scalarFormatterLabels: () => scalarFormatterLabels,
54
+ shapLabels: () => shapLabels,
49
55
  sortDisplayRows: () => sortDisplayRows,
50
56
  waterfallLayout: () => waterfallLayout,
51
57
  waterfallRows: () => waterfallRows
@@ -315,8 +321,34 @@ function formatFeatureLabel(name) {
315
321
  return name.replace(/_/g, " ");
316
322
  }
317
323
 
324
+ // src/core/labels.ts
325
+ var shapLabels = {
326
+ shapValue: "SHAP value",
327
+ shapValueAxis: "SHAP value (impact on model output)",
328
+ meanAbsShapValue: "mean(|SHAP value|)",
329
+ featureValue: "Feature value",
330
+ featureValueLow: "Low",
331
+ featureValueHigh: "High",
332
+ missingFeatureValue: "missing",
333
+ samples: "Instances",
334
+ sampleTotal: "\u03A3\u03C6",
335
+ sampleFallback: (sampleNumber) => `Sample ${sampleNumber}`,
336
+ baseValue: "E[f(X)]",
337
+ modelOutput: "f(x)",
338
+ otherFeatures: (count, style) => style === "sum" ? `Sum of ${count} other features` : `${count} other features`
339
+ };
340
+ function resolveLabels(labels) {
341
+ if (!labels) return shapLabels;
342
+ const resolved = { ...shapLabels };
343
+ for (const key of Object.keys(labels)) {
344
+ const value = labels[key];
345
+ if (value !== void 0) resolved[key] = value;
346
+ }
347
+ return resolved;
348
+ }
349
+
318
350
  // src/core/collapse.ts
319
- function collapseToDisplay(featureNames, importance, order, maxDisplay, faithfulOtherRow) {
351
+ function collapseToDisplay(featureNames, importance, order, maxDisplay, faithfulOtherRow, labels = shapLabels) {
320
352
  const p = order.length;
321
353
  if (maxDisplay >= p) {
322
354
  return {
@@ -339,7 +371,7 @@ function collapseToDisplay(featureNames, importance, order, maxDisplay, faithful
339
371
  const collapsed = order.slice(realCount);
340
372
  const collapsedValue = collapsed.reduce((sum, index) => sum + importance[index], 0);
341
373
  rows.push({
342
- label: faithfulOtherRow ? `Sum of ${collapsed.length} other features` : `${collapsed.length} other features`,
374
+ label: labels.otherFeatures(collapsed.length, faithfulOtherRow ? "sum" : "count"),
343
375
  featureIndex: null,
344
376
  value: collapsedValue,
345
377
  isOtherRow: true
@@ -406,15 +438,15 @@ var AXIS_HEIGHT = 52;
406
438
  var TICK_LABEL_PT = 11;
407
439
  var TITLE_PT = 13;
408
440
  var X_MARGIN = 0.05;
409
- function barXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
441
+ function barXAxis(min, max, toX, marginLeft, plotWidth, plotBottom, title) {
410
442
  const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT));
411
443
  return {
412
444
  xTicks: ticks.map((value) => ({ value, x: toX(value), label: tickLabel(value, step) })),
413
445
  xSpine: { x1: marginLeft, x2: marginLeft + plotWidth, y: plotBottom },
414
446
  xTitle: {
415
- // _bar.py:143-150 builds this from the Explanation's transform history:
416
- // "SHAP value" -> "|SHAP value|" -> "mean(|SHAP value|)".
417
- text: "mean(|SHAP value|)",
447
+ // _bar.py:143-150 builds the default from the Explanation's transform
448
+ // history: "SHAP value" -> "|SHAP value|" -> "mean(|SHAP value|)".
449
+ text: title,
418
450
  x: marginLeft + plotWidth / 2,
419
451
  y: plotBottom + AXIS_TITLE_DY,
420
452
  fontSize: TITLE_PT
@@ -422,6 +454,7 @@ function barXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
422
454
  };
423
455
  }
424
456
  function barLayout(rows, opts) {
457
+ var _a;
425
458
  const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
426
459
  const plotWidth = width - marginLeft - marginRight;
427
460
  const values = rows.rows.map((r) => r.value);
@@ -462,7 +495,15 @@ function barLayout(rows, opts) {
462
495
  xZero,
463
496
  plotWidth,
464
497
  plotBottom: marginTop + rows.rows.length * rowHeight,
465
- ...barXAxis(min, max, toX, marginLeft, plotWidth, marginTop + rows.rows.length * rowHeight),
498
+ ...barXAxis(
499
+ min,
500
+ max,
501
+ toX,
502
+ marginLeft,
503
+ plotWidth,
504
+ marginTop + rows.rows.length * rowHeight,
505
+ ((_a = opts.labels) != null ? _a : shapLabels).meanAbsShapValue
506
+ ),
466
507
  zeroLine: { x: xZero, y1: marginTop, y2: marginTop + rows.rows.length * rowHeight },
467
508
  height: marginTop + rows.rows.length * rowHeight + AXIS_HEIGHT
468
509
  };
@@ -496,7 +537,7 @@ function placeValueLabel(value, startX, endX, gutterX, decimals) {
496
537
  }
497
538
  return { ...base, x: startX + VALUE_LABEL_GAP, anchor: "start", inside: false };
498
539
  }
499
- function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow) {
540
+ function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow, labels = shapLabels) {
500
541
  if (!Number.isInteger(sampleIndex) || sampleIndex < 0 || sampleIndex >= explanation.nSamples) {
501
542
  throw new RangeError(
502
543
  `sampleIndex must identify a Sample from 0 to ${explanation.nSamples - 1}, received ${sampleIndex}`
@@ -534,7 +575,8 @@ function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow) {
534
575
  if (hasOtherRow) {
535
576
  const value = collapsed.reduce((sum, featureIndex) => sum + values[featureIndex], 0);
536
577
  rows.push({
537
- label: `${collapsed.length} other features`,
578
+ // SHAP's waterfall counts the hidden Features whatever the collapse mode.
579
+ label: labels.otherFeatures(collapsed.length, "count"),
538
580
  featureIndex: null,
539
581
  isOtherRow: true,
540
582
  value,
@@ -552,7 +594,9 @@ function waterfallRows(explanation, sampleIndex, maxDisplay, faithfulOtherRow) {
552
594
  };
553
595
  }
554
596
  function waterfallLayout(valueRows, opts) {
597
+ var _a;
555
598
  const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
599
+ const labels = (_a = opts.labels) != null ? _a : shapLabels;
556
600
  const plotWidth = width - marginLeft - marginRight;
557
601
  const coordinates = [valueRows.baseValue, valueRows.modelOutput];
558
602
  for (const row of valueRows.rows) coordinates.push(row.left, row.left + row.width);
@@ -619,7 +663,7 @@ function waterfallLayout(valueRows, opts) {
619
663
  // got, which is the whole reason SHAP hides the left spine.
620
664
  y1: plotBottom - rowHeight,
621
665
  y2: plotBottom,
622
- label: `E[f(X)] = ${formatLevel(valueRows.baseValue, opts.decimals)}`
666
+ label: `${labels.baseValue} = ${formatLevel(valueRows.baseValue, opts.decimals)}`
623
667
  },
624
668
  {
625
669
  kind: "output",
@@ -628,7 +672,7 @@ function waterfallLayout(valueRows, opts) {
628
672
  // axvline(fx, 0, 1) — the full height.
629
673
  y1: marginTop,
630
674
  y2: plotBottom,
631
- label: `f(x) = ${formatLevel(valueRows.modelOutput, opts.decimals)}`
675
+ label: `${labels.modelOutput} = ${formatLevel(valueRows.modelOutput, opts.decimals)}`
632
676
  }
633
677
  ],
634
678
  separators: valueRows.rows.map((_, index) => ({
@@ -1193,12 +1237,120 @@ function sampleColormap(name, t) {
1193
1237
  return `#${hexByte(red)}${hexByte(green)}${hexByte(blue)}`;
1194
1238
  }
1195
1239
 
1240
+ // src/core/colorBar.ts
1241
+ var ASPECT = 80;
1242
+ var TICK_PAD = 3.5;
1243
+ var TICK_LABEL_PT3 = 11;
1244
+ var LABEL_PT = 12;
1245
+ var LABEL_THICKNESS_EM = 1.08;
1246
+ var LABEL_BASELINE_EM = 0.84;
1247
+ var CHAR_EM = 0.6;
1248
+ var STEPS = 64;
1249
+ function textWidth(text, fontSize) {
1250
+ return text.length * fontSize * CHAR_EM;
1251
+ }
1252
+ function tickColumnWidth(spec) {
1253
+ return Math.max(...spec.tickLabels.map((label) => textWidth(label, TICK_LABEL_PT3)));
1254
+ }
1255
+ function colorBarExtent(height, spec) {
1256
+ return height / ASPECT + TICK_PAD + Math.max(0, tickColumnWidth(spec) + spec.labelPad) + LABEL_PT * LABEL_THICKNESS_EM;
1257
+ }
1258
+ function fitColorBar(opts) {
1259
+ const { plotWidth, available, gapRatio, minGap, extent } = opts;
1260
+ const gapFor = (width) => Math.max(gapRatio * width, minGap);
1261
+ if (gapFor(plotWidth) + extent <= available) {
1262
+ return { plotWidth, gap: gapFor(plotWidth) };
1263
+ }
1264
+ const byRatio = (available + plotWidth - extent) / (1 + gapRatio);
1265
+ const fitted = Math.max(
1266
+ 0,
1267
+ gapRatio * byRatio >= minGap ? byRatio : plotWidth - (minGap + extent - available)
1268
+ );
1269
+ return { plotWidth: fitted, gap: gapFor(fitted) };
1270
+ }
1271
+ function colorBarLayout(spec, position) {
1272
+ const { x, y1, y2 } = position;
1273
+ const width = (y2 - y1) / ASPECT;
1274
+ const step = (y2 - y1) / STEPS;
1275
+ const steps = Array.from({ length: STEPS }, (_, index) => ({
1276
+ y: y2 - (index + 1) * step,
1277
+ // Every band but the lowest reaches half a pixel into the one below, so
1278
+ // anti-aliasing cannot open a hairline seam between them.
1279
+ height: index === 0 ? step : step + 0.5,
1280
+ color: sampleColormap(spec.colormap, index / (STEPS - 1))
1281
+ }));
1282
+ const tickX = x + width + TICK_PAD;
1283
+ const labelLeft = tickX + tickColumnWidth(spec) + spec.labelPad;
1284
+ return {
1285
+ x,
1286
+ y1,
1287
+ y2,
1288
+ width,
1289
+ steps,
1290
+ tickX,
1291
+ tickFontSize: TICK_LABEL_PT3,
1292
+ ticks: [
1293
+ { y: y2, label: spec.tickLabels[0] },
1294
+ { y: y1, label: spec.tickLabels[1] }
1295
+ ],
1296
+ ...spec.offsetText ? { offsetText: { x: tickX, y: y1 - TICK_LABEL_PT3, text: spec.offsetText } } : {},
1297
+ label: {
1298
+ x: labelLeft + LABEL_PT * LABEL_BASELINE_EM,
1299
+ y: (y1 + y2) / 2,
1300
+ text: spec.label,
1301
+ fontSize: LABEL_PT
1302
+ },
1303
+ right: Math.max(
1304
+ tickX + tickColumnWidth(spec),
1305
+ labelLeft + LABEL_PT * LABEL_THICKNESS_EM
1306
+ )
1307
+ };
1308
+ }
1309
+ var MINUS3 = "\u2212";
1310
+ var POWER_LIMITS = [-5, 6];
1311
+ function roundTo(value, decimals) {
1312
+ const factor = 10 ** decimals;
1313
+ return Math.round(value * factor) / factor;
1314
+ }
1315
+ function scalarFormatterLabels(locs) {
1316
+ const largest = Math.max(0, ...locs.map(Math.abs));
1317
+ const magnitude = largest === 0 ? 0 : Math.floor(Math.log10(largest));
1318
+ const order = magnitude <= POWER_LIMITS[0] || magnitude >= POWER_LIMITS[1] ? magnitude : 0;
1319
+ const scaled = locs.map((value) => value / 10 ** order);
1320
+ let range = Math.max(...scaled) - Math.min(...scaled);
1321
+ if (range === 0) range = Math.max(...scaled.map(Math.abs));
1322
+ if (range === 0) range = 1;
1323
+ const rangeMagnitude = Math.floor(Math.log10(range));
1324
+ const threshold = 1e-3 * 10 ** rangeMagnitude;
1325
+ let decimals = Math.max(0, 3 - rangeMagnitude);
1326
+ while (decimals >= 0) {
1327
+ const error = Math.max(...scaled.map((value) => Math.abs(value - roundTo(value, decimals))));
1328
+ if (error < threshold) decimals -= 1;
1329
+ else break;
1330
+ }
1331
+ decimals += 1;
1332
+ const labels = scaled.map((value) => {
1333
+ const text = value.toFixed(decimals);
1334
+ return Number(text) === 0 ? text.replace("-", "") : text.replace("-", MINUS3);
1335
+ });
1336
+ return order === 0 ? { labels } : { labels, offsetText: `1e${String(order).replace("-", MINUS3)}` };
1337
+ }
1338
+
1196
1339
  // src/core/beeswarmLayout.ts
1197
1340
  var BEESWARM_MISSING_COLOR = "#777777";
1198
1341
  var BEESWARM_ROW_HEIGHT = 0.4;
1199
1342
  var NBINS = 100;
1200
- var AXIS_HEIGHT3 = 74;
1201
- var TICK_LABEL_PT3 = 11;
1343
+ var AXIS_HEIGHT3 = 52;
1344
+ function colorBarSpec(labels) {
1345
+ return {
1346
+ colormap: "red_blue",
1347
+ tickLabels: [labels.featureValueLow, labels.featureValueHigh],
1348
+ label: labels.featureValue,
1349
+ labelPad: 0
1350
+ };
1351
+ }
1352
+ var COLOR_BAR_GAP_RATIO = 0.05 / 0.8;
1353
+ var TICK_LABEL_PT4 = 11;
1202
1354
  var TITLE_PT2 = 13;
1203
1355
  var X_MARGIN2 = 0.05;
1204
1356
  function percentile(values, percent) {
@@ -1262,7 +1414,7 @@ function spreadPoints(xs, rowIndex, seed) {
1262
1414
  const scale = 0.9 * (BEESWARM_ROW_HEIGHT / (maxPositiveOffset + 1));
1263
1415
  return offsets.map((offset) => rowIndex + offset * scale);
1264
1416
  }
1265
- function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSort = "importance") {
1417
+ function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSort = "importance", labels = shapLabels) {
1266
1418
  if (!Number.isInteger(maxDisplay) || maxDisplay <= 0) {
1267
1419
  throw new RangeError(`maxDisplay must be a positive integer, received ${maxDisplay}`);
1268
1420
  }
@@ -1277,7 +1429,8 @@ function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSo
1277
1429
  importance,
1278
1430
  order,
1279
1431
  maxDisplay,
1280
- faithfulOtherRow
1432
+ faithfulOtherRow,
1433
+ labels
1281
1434
  ),
1282
1435
  rowSort,
1283
1436
  explanation.data
@@ -1329,14 +1482,14 @@ function beeswarmRows(explanation, maxDisplay, faithfulOtherRow, seed = 0, rowSo
1329
1482
  });
1330
1483
  return { rows, collapsedCount: display.collapsedCount };
1331
1484
  }
1332
- function beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
1333
- const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT3));
1485
+ function beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom, title) {
1486
+ const { ticks, step } = niceTicks(min, max, tickSpace(plotWidth, TICK_LABEL_PT4));
1334
1487
  return {
1335
1488
  xTicks: ticks.map((value) => ({ value, x: toX(value), label: tickLabel(value, step) })),
1336
1489
  xSpine: { x1: marginLeft, x2: marginLeft + plotWidth, y: plotBottom },
1337
1490
  xTitle: {
1338
- // _labels.py:5, labels["VALUE"].
1339
- text: "SHAP value (impact on model output)",
1491
+ // The default is _labels.py:5, labels["VALUE"].
1492
+ text: title,
1340
1493
  x: marginLeft + plotWidth / 2,
1341
1494
  y: plotBottom + AXIS_TITLE_DY,
1342
1495
  fontSize: TITLE_PT2
@@ -1344,8 +1497,19 @@ function beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom) {
1344
1497
  };
1345
1498
  }
1346
1499
  function beeswarmLayout(valueRows, opts) {
1500
+ var _a;
1347
1501
  const { width, rowHeight, marginLeft, marginRight, marginTop, dotRadius } = opts;
1348
- const plotWidth = width - marginLeft - marginRight;
1502
+ const labels = (_a = opts.labels) != null ? _a : shapLabels;
1503
+ const colorBar = colorBarSpec(labels);
1504
+ const plotBottom = marginTop + valueRows.rows.length * rowHeight;
1505
+ const fit = opts.colorBar ? fitColorBar({
1506
+ plotWidth: width - marginLeft - marginRight,
1507
+ available: marginRight,
1508
+ gapRatio: COLOR_BAR_GAP_RATIO,
1509
+ minGap: 0,
1510
+ extent: colorBarExtent(plotBottom - marginTop, colorBar)
1511
+ }) : null;
1512
+ const plotWidth = fit ? fit.plotWidth : width - marginLeft - marginRight;
1349
1513
  const values = valueRows.rows.flatMap((row) => row.points.map((point) => point.x));
1350
1514
  const dataMin = Math.min(0, ...values);
1351
1515
  const dataMax = Math.max(0, ...values);
@@ -1370,14 +1534,18 @@ function beeswarmLayout(valueRows, opts) {
1370
1534
  }))
1371
1535
  };
1372
1536
  });
1373
- const plotBottom = marginTop + valueRows.rows.length * rowHeight;
1374
1537
  return {
1375
1538
  rows,
1376
1539
  xDomain: [min, max],
1377
1540
  xZero: toX(0),
1378
- ...beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom),
1541
+ ...beeswarmXAxis(min, max, toX, marginLeft, plotWidth, plotBottom, labels.shapValueAxis),
1379
1542
  plotWidth,
1380
1543
  plotBottom,
1544
+ colorBar: fit ? colorBarLayout(colorBar, {
1545
+ x: marginLeft + plotWidth + fit.gap,
1546
+ y1: marginTop,
1547
+ y2: plotBottom
1548
+ }) : null,
1381
1549
  height: plotBottom + AXIS_HEIGHT3
1382
1550
  };
1383
1551
  }
@@ -1390,7 +1558,9 @@ var SIDE_BAR_GAP = 10;
1390
1558
  var SIDE_BAR_RIGHT_INSET = 40;
1391
1559
  var SIDE_BAR_HEIGHT_RATIO = 0.6;
1392
1560
  var AXIS_HEIGHT4 = 52;
1393
- var TICK_LABEL_PT4 = 10;
1561
+ var TICK_LABEL_PT5 = 10;
1562
+ var COLOR_BAR_GAP_RATIO2 = 0.1 / 0.89;
1563
+ var COLOR_BAR_CLEARANCE = 8;
1394
1564
  var Y_TICK_LENGTH = 5;
1395
1565
  function percentile2(values, fraction) {
1396
1566
  if (values.length === 0) return 0;
@@ -1401,7 +1571,7 @@ function percentile2(values, fraction) {
1401
1571
  const weight = position - lower;
1402
1572
  return sorted[lower] + (sorted[upper] - sorted[lower]) * weight;
1403
1573
  }
1404
- function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "importance") {
1574
+ function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "importance", labels = shapLabels) {
1405
1575
  if (!Number.isInteger(maxDisplay) || maxDisplay <= 0) {
1406
1576
  throw new RangeError(`maxDisplay must be a positive integer, received ${maxDisplay}`);
1407
1577
  }
@@ -1413,7 +1583,8 @@ function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "impor
1413
1583
  importance,
1414
1584
  featureOrder,
1415
1585
  maxDisplay,
1416
- faithfulOtherRow
1586
+ faithfulOtherRow,
1587
+ labels
1417
1588
  ),
1418
1589
  rowSort,
1419
1590
  explanation.data
@@ -1477,11 +1648,11 @@ function heatmapRows(explanation, maxDisplay, faithfulOtherRow, rowSort = "impor
1477
1648
  sampleLabelColumn: explanation.sampleLabelColumn
1478
1649
  };
1479
1650
  }
1480
- function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom) {
1651
+ function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom, title) {
1481
1652
  const { ticks, step } = niceTicks(
1482
1653
  -0.5,
1483
1654
  sampleCount - 0.5,
1484
- tickSpace(plotWidth, TICK_LABEL_PT4),
1655
+ tickSpace(plotWidth, TICK_LABEL_PT5),
1485
1656
  { integer: true }
1486
1657
  );
1487
1658
  return {
@@ -1492,20 +1663,38 @@ function heatmapXAxis(sampleCount, marginLeft, cellWidth, plotWidth, plotBottom)
1492
1663
  })),
1493
1664
  xSpine: null,
1494
1665
  xTitle: {
1495
- text: "Instances",
1666
+ text: title,
1496
1667
  x: marginLeft + plotWidth / 2,
1497
1668
  y: plotBottom + AXIS_TITLE_DY,
1498
- fontSize: TICK_LABEL_PT4
1669
+ fontSize: TICK_LABEL_PT5
1499
1670
  }
1500
1671
  };
1501
1672
  }
1502
1673
  function heatmapLayout(valueRows, opts) {
1674
+ var _a;
1503
1675
  const { width, rowHeight, marginLeft, marginRight, marginTop } = opts;
1504
- const plotWidth = width - marginLeft - marginRight;
1505
- const cellWidth = valueRows.columns.length === 0 ? 0 : plotWidth / valueRows.columns.length;
1506
- const gridRight = marginLeft + plotWidth;
1676
+ const labels = (_a = opts.labels) != null ? _a : shapLabels;
1507
1677
  const plotBottom = marginTop + valueRows.rows.length * rowHeight;
1508
1678
  const sideBarWidth = Math.max(0, marginRight - SIDE_BAR_RIGHT_INSET - SIDE_BAR_GAP);
1679
+ const ticks = scalarFormatterLabels([valueRows.vmin, valueRows.vmax]);
1680
+ const colorBarSpec2 = {
1681
+ colormap: "red_white_blue",
1682
+ tickLabels: [ticks.labels[0], ticks.labels[1]],
1683
+ ...ticks.offsetText ? { offsetText: ticks.offsetText } : {},
1684
+ label: labels.shapValueAxis,
1685
+ labelPad: -10
1686
+ };
1687
+ const colorBarTop = FX_TOP;
1688
+ const fit = opts.colorBar ? fitColorBar({
1689
+ plotWidth: width - marginLeft - marginRight,
1690
+ available: marginRight,
1691
+ gapRatio: COLOR_BAR_GAP_RATIO2,
1692
+ minGap: SIDE_BAR_GAP + sideBarWidth + COLOR_BAR_CLEARANCE,
1693
+ extent: colorBarExtent(plotBottom - colorBarTop, colorBarSpec2)
1694
+ }) : null;
1695
+ const plotWidth = fit ? fit.plotWidth : width - marginLeft - marginRight;
1696
+ const cellWidth = valueRows.columns.length === 0 ? 0 : plotWidth / valueRows.columns.length;
1697
+ const gridRight = marginLeft + plotWidth;
1509
1698
  const columns = valueRows.columns.map((column, index) => ({
1510
1699
  ...column,
1511
1700
  x: marginLeft + index * cellWidth,
@@ -1574,10 +1763,22 @@ function heatmapLayout(valueRows, opts) {
1574
1763
  x1: marginLeft - Y_TICK_LENGTH,
1575
1764
  x2: marginLeft
1576
1765
  })),
1577
- ...heatmapXAxis(valueRows.columns.length, marginLeft, cellWidth, plotWidth, plotBottom),
1766
+ ...heatmapXAxis(
1767
+ valueRows.columns.length,
1768
+ marginLeft,
1769
+ cellWidth,
1770
+ plotWidth,
1771
+ plotBottom,
1772
+ labels.samples
1773
+ ),
1578
1774
  sampleLabelColumn: valueRows.sampleLabelColumn,
1579
1775
  plotWidth,
1580
1776
  cellWidth,
1777
+ colorBar: fit ? colorBarLayout(colorBarSpec2, {
1778
+ x: gridRight + fit.gap,
1779
+ y1: colorBarTop,
1780
+ y2: plotBottom
1781
+ }) : null,
1581
1782
  height: plotBottom + AXIS_HEIGHT4
1582
1783
  };
1583
1784
  }
@@ -1597,6 +1798,9 @@ function heatmapLayout(valueRows, opts) {
1597
1798
  beeswarmLayout,
1598
1799
  beeswarmRows,
1599
1800
  collapseToDisplay,
1801
+ colorBarExtent,
1802
+ colorBarLayout,
1803
+ fitColorBar,
1600
1804
  formatFeatureLabel,
1601
1805
  formatLevel,
1602
1806
  formatShapValue,
@@ -1608,7 +1812,10 @@ function heatmapLayout(valueRows, opts) {
1608
1812
  heatmapRows,
1609
1813
  orderFeatures,
1610
1814
  parseExplanation,
1815
+ resolveLabels,
1611
1816
  sampleColormap,
1817
+ scalarFormatterLabels,
1818
+ shapLabels,
1612
1819
  sortDisplayRows,
1613
1820
  waterfallLayout,
1614
1821
  waterfallRows