@czi-sds/data-viz 2.5.1 → 3.0.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.
@@ -21,12 +21,12 @@ const TitleContainer = (0, _emotion_styled.default)("div")`
21
21
  const spaces = (0, _czi_sds_components.getSpaces)(props);
22
22
  return `
23
23
  gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
24
- margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
24
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
25
25
  `;
26
26
  }}
27
27
  `;
28
28
  const ChartTitle = (0, _emotion_styled.default)("div")`
29
- ${_czi_sds_components.fontHeaderS}
29
+ ${_czi_sds_components.fontBodyMediumXs}
30
30
 
31
31
  ${(props) => {
32
32
  var _semanticColors$base;
@@ -36,36 +36,19 @@ const ChartTitle = (0, _emotion_styled.default)("div")`
36
36
  `;
37
37
  }}
38
38
  `;
39
- const StyledBadge = (0, _emotion_styled.default)("div")`
40
- ${_czi_sds_components.fontTabularSemiboldXxxxs}
41
-
42
- ${(props) => {
43
- var _semanticColors$base2, _semanticColors$base3;
44
- const spaces = (0, _czi_sds_components.getSpaces)(props);
45
- const corners = (0, _czi_sds_components.getCorners)(props);
46
- const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
47
- return `
48
- padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
49
- border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
50
- background-color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.backgroundTertiary};
51
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textSecondary};
52
- `;
53
- }}
54
- `;
55
39
  const BarContainer = (0, _emotion_styled.default)("div")`
56
40
  ${(props) => {
57
- var _semanticColors$base4;
41
+ var _semanticColors$base2;
58
42
  const { width, barHeight, isEmpty } = props;
59
43
  const corners = (0, _czi_sds_components.getCorners)(props);
60
44
  const semanticColors = (0, _czi_sds_components.getSemanticColors)(props);
61
45
  const finalWidth = typeof width === "number" ? `${width}px` : width;
62
46
  return `
63
47
  display: flex;
64
- border-radius: ${corners === null || corners === void 0 ? void 0 : corners.s}px;
48
+ border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
65
49
  width: ${finalWidth};
66
50
  height: ${barHeight}px;
67
- background-color: ${isEmpty ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.backgroundTertiary : "transparent"};
68
- overflow: hidden;
51
+ background-color: ${isEmpty ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.backgroundTertiary : "transparent"};
69
52
  `;
70
53
  }}
71
54
  `;
@@ -73,14 +56,17 @@ const BarSegment = (0, _emotion_styled.default)("div")`
73
56
  ${(props) => {
74
57
  const { color, height, percentage, opacity, disabled, isLast } = props;
75
58
  const spaces = (0, _czi_sds_components.getSpaces)(props);
59
+ const corners = (0, _czi_sds_components.getCorners)(props);
76
60
  const gap = (spaces === null || spaces === void 0 ? void 0 : spaces.xxxs) || 0;
61
+ const marginRight = percentage > 0 && !isLast ? `${gap}px` : 0;
77
62
  return `
78
63
  background-color: ${color};
64
+ border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
79
65
  height: ${height}px;
80
66
  flex-grow: ${percentage};
81
67
  flex-basis: 0;
82
68
  flex-shrink: 1;
83
- margin-right: ${percentage > 0 && !isLast ? `${gap}px` : 0};
69
+ margin-right: ${marginRight};
84
70
  opacity: ${opacity};
85
71
  transition: opacity 0.2s ease-in-out, flex-grow 0.25s ease-in-out, margin-right 0.25s ease-out;
86
72
  cursor: ${disabled ? "default" : "pointer"};
@@ -94,7 +80,7 @@ const StyledStackedBarChartWrapper = (0, _emotion_styled.default)("div")`
94
80
  return `
95
81
  display: flex;
96
82
  flex-direction: column;
97
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
83
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
98
84
  `;
99
85
  }}
100
86
  `;
@@ -124,17 +110,18 @@ const _excluded = [
124
110
  "remainingLabel",
125
111
  "remainingUnit",
126
112
  "unit",
127
- "colorGeneratorOptions"
113
+ "colorGeneratorOptions",
114
+ "maxItems"
128
115
  ];
129
116
  const calculateBadgeText = (totalItems, selectedCount) => {
130
117
  if (selectedCount === 0 || selectedCount === totalItems) return `${totalItems}`;
131
118
  return `${selectedCount} of ${totalItems}`;
132
119
  };
133
120
  const calculateLegendValue = (item, showLegendValues, legendValueFormat, globalUnit) => {
121
+ var _ref, _item$unit;
134
122
  if (!showLegendValues) return void 0;
135
123
  if (legendValueFormat === "percentage") return `${Math.round(item.percentage)}%`;
136
- const effectiveUnit = item.unit || globalUnit;
137
- return effectiveUnit ? `${item.value} ${effectiveUnit}` : `${item.value}`;
124
+ return `${item.value.toLocaleString()} ${(_ref = (_item$unit = item.unit) !== null && _item$unit !== void 0 ? _item$unit : globalUnit) !== null && _ref !== void 0 ? _ref : ""}`.trim();
138
125
  };
139
126
  const getSegmentOpacity = (index, hoveredIndex, selectedIndicesSet, selectionBehavior) => {
140
127
  const hasSelection = selectedIndicesSet.size > 0;
@@ -181,7 +168,7 @@ const applySelectionStyling = (items, selectedIndicesSet, selectionBehavior, mod
181
168
  return mode === "cumulative" ? applyCumulativeHideStyling(items, selectedIndicesSet, selectedTotal, hasRemaining, effectiveMaxAmount) : applyProportionalHideStyling(items, selectedIndicesSet, selectedTotal);
182
169
  };
183
170
  const buildLegendItems = (dataWithPercentages, options) => {
184
- const { showLegendValues, legendValueFormat, unit, hasRemaining, showLegend, remainingValue, remainingPercentage, remainingLabel, remainingUnit, theme } = options;
171
+ const { showLegendValues, legendValueFormat, unit, hasRemaining, showLegend, remainingValue, remainingPercentage, remainingLabel, remainingUnit, remainingOpensMenu, theme } = options;
185
172
  const legendItems = dataWithPercentages.map((item) => ({
186
173
  name: item.name,
187
174
  value: calculateLegendValue(item, showLegendValues, legendValueFormat, unit),
@@ -195,13 +182,13 @@ const buildLegendItems = (dataWithPercentages, options) => {
195
182
  else if (legendValueFormat === "percentage") remainingValueDisplay = `${Math.round(remainingPercentage)}%`;
196
183
  else {
197
184
  const effectiveRemainingUnit = remainingUnit || unit || "";
198
- remainingValueDisplay = effectiveRemainingUnit ? `${remainingValue} ${effectiveRemainingUnit}` : `${remainingValue}`;
185
+ remainingValueDisplay = effectiveRemainingUnit ? `${remainingValue.toLocaleString()} ${effectiveRemainingUnit}` : `${remainingValue.toLocaleString()}`;
199
186
  }
200
187
  legendItems.push({
201
188
  name: remainingLabel,
202
189
  value: remainingValueDisplay,
203
190
  color: theme === null || theme === void 0 || (_theme$palette = theme.palette) === null || _theme$palette === void 0 || (_theme$palette = _theme$palette.sds) === null || _theme$palette === void 0 || (_theme$palette = _theme$palette.base) === null || _theme$palette === void 0 ? void 0 : _theme$palette.backgroundTertiary,
204
- disabled: true
191
+ disabled: !remainingOpensMenu
205
192
  });
206
193
  }
207
194
  return legendItems;
@@ -237,10 +224,13 @@ const renderBarSegment = (item, options) => {
237
224
  }, item.key) : (0, react.cloneElement)(barSegment, { key: item.key });
238
225
  };
239
226
  const StackedBarChart = (props) => {
240
- var _theme$palette4;
241
- const { title, badge, hideBadge = false, data, width = "100%", barHeight: barHeightProp = 16, showLegend = true, showLegendValues = true, legendValueFormat = "percentage", selectedIndices = [], onSelectionChange, onSegmentMouseEnter, onSegmentMouseLeave, onLegendItemMouseEnter, onLegendItemMouseLeave, onSegmentClick, onLegendItemClick, selectionBehavior = "dim", mode = "proportional", maxAmount, remainingLabel = "Remaining", remainingUnit, unit, colorGeneratorOptions } = props, rest = require_objectSpread2._objectWithoutProperties(props, _excluded);
227
+ var _theme$palette5;
228
+ const { title, badge, hideBadge = false, data, width = "100%", barHeight: barHeightProp = 8, showLegend = true, showLegendValues = true, legendValueFormat = "percentage", selectedIndices = [], onSelectionChange, onSegmentMouseEnter, onSegmentMouseLeave, onLegendItemMouseEnter, onLegendItemMouseLeave, onSegmentClick, onLegendItemClick, selectionBehavior = "dim", mode = "proportional", maxAmount, remainingLabel = "Remaining", remainingUnit, unit, colorGeneratorOptions, maxItems } = props, rest = require_objectSpread2._objectWithoutProperties(props, _excluded);
242
229
  const barHeight = Math.max(1, barHeightProp);
243
230
  const theme = (0, _mui_material.useTheme)();
231
+ const visibleCount = mode === "cumulative" && maxItems !== void 0 && maxItems < data.length ? Math.max(0, maxItems) : data.length;
232
+ const chartData = (0, react.useMemo)(() => data.slice(0, visibleCount), [data, visibleCount]);
233
+ const hiddenItems = (0, react.useMemo)(() => data.slice(visibleCount), [data, visibleCount]);
244
234
  const [hoveredIndex, setHoveredIndex] = (0, react.useState)(null);
245
235
  const [animatedItems, setAnimatedItems] = (0, react.useState)([]);
246
236
  const [autoColorMap, setAutoColorMap] = (0, react.useState)(/* @__PURE__ */ new Map());
@@ -263,8 +253,8 @@ const StackedBarChart = (props) => {
263
253
  theme
264
254
  ]);
265
255
  (0, react.useEffect)(() => {
266
- const autoItems = data.filter((item) => !item.color);
267
- const currentDataKeys = new Set(data.map((item) => item.name));
256
+ const autoItems = chartData.filter((item) => !item.color);
257
+ const currentDataKeys = new Set(chartData.map((item) => item.name));
268
258
  const previousDataKeys = previousDataKeysRef.current;
269
259
  const previousColorOptions = previousColorOptionsRef.current;
270
260
  const colorOptionsChanged = !previousColorOptions || previousColorOptions.isDarkMode !== isDarkMode || previousColorOptions.options !== colorGeneratorOptions;
@@ -290,7 +280,7 @@ const StackedBarChart = (props) => {
290
280
  if (hasChanges) setAutoColorMap(updatedMap);
291
281
  }
292
282
  }, [
293
- data,
283
+ chartData,
294
284
  colorGeneratorOptions,
295
285
  isDarkMode,
296
286
  theme,
@@ -299,9 +289,10 @@ const StackedBarChart = (props) => {
299
289
  const displayBadge = badge !== null && badge !== void 0 ? badge : calculateBadgeText(data.length, selectedIndices.length);
300
290
  const { effectiveMaxAmount, hasRemaining, remainingValue, remainingPercentage } = (0, react.useMemo)(() => {
301
291
  const total = data.reduce((sum, item) => sum + item.value, 0);
292
+ const visibleTotal = chartData.reduce((sum, item) => sum + item.value, 0);
302
293
  const maxAmountValue = mode === "cumulative" && maxAmount ? maxAmount : total;
303
- const hasRemainingSegment = mode === "cumulative" && total < maxAmountValue;
304
- const remaining = hasRemainingSegment ? maxAmountValue - total : 0;
294
+ const hasRemainingSegment = mode === "cumulative" && visibleTotal < maxAmountValue;
295
+ const remaining = hasRemainingSegment ? maxAmountValue - visibleTotal : 0;
305
296
  return {
306
297
  effectiveMaxAmount: maxAmountValue,
307
298
  hasRemaining: hasRemainingSegment,
@@ -310,11 +301,20 @@ const StackedBarChart = (props) => {
310
301
  };
311
302
  }, [
312
303
  data,
304
+ chartData,
313
305
  mode,
314
306
  maxAmount
315
307
  ]);
308
+ const hasOverflow = hasRemaining && hiddenItems.length > 0;
309
+ const overflowIndex = data.length;
310
+ const hiddenColors = (0, react.useMemo)(() => hasOverflow ? (0, _czi_sds_components.generateDiscreteColors)(hiddenItems.length, require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, colorGeneratorOptions || {}), {}, { isDarkMode })) : [], [
311
+ hasOverflow,
312
+ hiddenItems.length,
313
+ colorGeneratorOptions,
314
+ isDarkMode
315
+ ]);
316
316
  const dataWithPercentages = (0, react.useMemo)(() => {
317
- return data.map((item, index) => {
317
+ return chartData.map((item, index) => {
318
318
  var _theme$palette3;
319
319
  const resolvedColor = item.color || autoColorMap.get(item.name) || (theme === null || theme === void 0 || (_theme$palette3 = theme.palette) === null || _theme$palette3 === void 0 || (_theme$palette3 = _theme$palette3.sds) === null || _theme$palette3 === void 0 || (_theme$palette3 = _theme$palette3.base) === null || _theme$palette3 === void 0 ? void 0 : _theme$palette3.ornamentSecondary);
320
320
  return require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, item), {}, {
@@ -327,7 +327,7 @@ const StackedBarChart = (props) => {
327
327
  });
328
328
  });
329
329
  }, [
330
- data,
330
+ chartData,
331
331
  effectiveMaxAmount,
332
332
  autoColorMap,
333
333
  theme
@@ -368,10 +368,16 @@ const StackedBarChart = (props) => {
368
368
  timeoutIds.forEach((id) => window.clearTimeout(id));
369
369
  };
370
370
  }, [dataWithPercentages, remainingPercentage]);
371
- const selectedIndicesSet = (0, react.useMemo)(() => new Set(selectedIndices), [selectedIndices]);
372
- const { styledItems, updatedRemainingPercentage, updatedRemainingValue } = (0, react.useMemo)(() => applySelectionStyling(animatedItems, selectedIndicesSet, selectionBehavior, mode, hasRemaining, effectiveMaxAmount, animatedRemainingPercentage, remainingValue), [
371
+ const toChartIndex = (0, react.useCallback)((index) => hasOverflow && index >= visibleCount ? overflowIndex : index, [
372
+ hasOverflow,
373
+ visibleCount,
374
+ overflowIndex
375
+ ]);
376
+ const hoveredChartIndex = hoveredIndex === null ? null : toChartIndex(hoveredIndex);
377
+ const selectedChartIndices = (0, react.useMemo)(() => new Set(selectedIndices.map(toChartIndex)), [selectedIndices, toChartIndex]);
378
+ const { styledItems, updatedRemainingPercentage, updatedRemainingValue } = (0, react.useMemo)(() => applySelectionStyling(animatedItems, selectedChartIndices, selectionBehavior, mode, hasRemaining, effectiveMaxAmount, animatedRemainingPercentage, remainingValue), [
373
379
  animatedItems,
374
- selectedIndicesSet,
380
+ selectedChartIndices,
375
381
  selectionBehavior,
376
382
  mode,
377
383
  hasRemaining,
@@ -392,6 +398,7 @@ const StackedBarChart = (props) => {
392
398
  remainingPercentage: displayRemainingPercentage,
393
399
  remainingLabel,
394
400
  remainingUnit,
401
+ remainingOpensMenu: hasOverflow,
395
402
  theme
396
403
  }), [
397
404
  dataWithPercentages,
@@ -404,19 +411,51 @@ const StackedBarChart = (props) => {
404
411
  displayRemainingPercentage,
405
412
  remainingLabel,
406
413
  remainingUnit,
414
+ hasOverflow,
407
415
  theme
408
416
  ]);
409
- const handleLegendItemHover = (0, react.useCallback)((_item, index) => {
417
+ const toLegendIndex = (0, react.useCallback)((index) => {
418
+ if (!hasOverflow || index < visibleCount) return index;
419
+ return index === overflowIndex ? visibleCount : index + 1;
420
+ }, [
421
+ hasOverflow,
422
+ visibleCount,
423
+ overflowIndex
424
+ ]);
425
+ const fromLegendIndex = (0, react.useCallback)((index) => {
426
+ if (!hasOverflow || index < visibleCount) return index;
427
+ return index === visibleCount ? overflowIndex : index - 1;
428
+ }, [
429
+ hasOverflow,
430
+ visibleCount,
431
+ overflowIndex
432
+ ]);
433
+ const handleLegendItemHover = (0, react.useCallback)((_item, legendIndex) => {
434
+ const index = fromLegendIndex(legendIndex);
410
435
  setHoveredIndex(index);
411
436
  if (onLegendItemMouseEnter && data[index]) onLegendItemMouseEnter(data[index], index);
412
- }, [onLegendItemMouseEnter, data]);
413
- const handleLegendItemLeave = (0, react.useCallback)((_item, index) => {
437
+ }, [
438
+ onLegendItemMouseEnter,
439
+ data,
440
+ fromLegendIndex
441
+ ]);
442
+ const handleLegendItemLeave = (0, react.useCallback)((_item, legendIndex) => {
443
+ const index = fromLegendIndex(legendIndex);
414
444
  setHoveredIndex(null);
415
445
  if (onLegendItemMouseLeave && data[index]) onLegendItemMouseLeave(data[index], index);
416
- }, [onLegendItemMouseLeave, data]);
417
- const handleLegendItemClickInternal = (0, react.useCallback)((_item, index) => {
446
+ }, [
447
+ onLegendItemMouseLeave,
448
+ data,
449
+ fromLegendIndex
450
+ ]);
451
+ const handleLegendItemClickInternal = (0, react.useCallback)((_item, legendIndex) => {
452
+ const index = fromLegendIndex(legendIndex);
418
453
  if (onLegendItemClick && data[index]) onLegendItemClick(data[index], index);
419
- }, [onLegendItemClick, data]);
454
+ }, [
455
+ onLegendItemClick,
456
+ data,
457
+ fromLegendIndex
458
+ ]);
420
459
  const handleSelectionChange = (0, react.useCallback)((newSelectedIndices) => {
421
460
  if (!onSelectionChange) return;
422
461
  onSelectionChange(newSelectedIndices, data.filter((item, itemIndex) => newSelectedIndices.includes(itemIndex)));
@@ -424,15 +463,29 @@ const StackedBarChart = (props) => {
424
463
  const handleSegmentClick = (0, react.useCallback)((index) => {
425
464
  if (onSegmentClick && data[index]) onSegmentClick(data[index], index);
426
465
  if (!onSelectionChange) return;
427
- handleSelectionChange(selectedIndicesSet.has(index) ? selectedIndices.filter((i) => i !== index) : [...selectedIndices, index]);
466
+ handleSelectionChange(selectedChartIndices.has(index) ? selectedIndices.filter((i) => i !== index) : [...selectedIndices, index]);
428
467
  }, [
429
468
  onSelectionChange,
430
469
  onSegmentClick,
431
470
  data,
432
471
  selectedIndices,
433
- selectedIndicesSet,
472
+ selectedChartIndices,
434
473
  handleSelectionChange
435
474
  ]);
475
+ const remainingMenuItems = (0, react.useMemo)(() => hasOverflow ? hiddenItems.map((item, index) => {
476
+ var _theme$palette4;
477
+ return {
478
+ name: item.name,
479
+ value: item.value,
480
+ color: item.color || hiddenColors[index] || (theme === null || theme === void 0 || (_theme$palette4 = theme.palette) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.sds) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.base) === null || _theme$palette4 === void 0 ? void 0 : _theme$palette4.ornamentSecondary),
481
+ disabled: item.disabled
482
+ };
483
+ }) : [], [
484
+ hasOverflow,
485
+ hiddenItems,
486
+ hiddenColors,
487
+ theme
488
+ ]);
436
489
  const lastVisibleIndex = (0, react.useMemo)(() => {
437
490
  for (let i = styledItems.length - 1; i >= 0; i--) if (styledItems[i].percentage > 0) return i;
438
491
  return -1;
@@ -444,8 +497,8 @@ const StackedBarChart = (props) => {
444
497
  children: [styledItems.map((item, index) => {
445
498
  return renderBarSegment(item, {
446
499
  barHeight,
447
- hoveredIndex,
448
- selectedIndicesSet,
500
+ hoveredIndex: hoveredChartIndex,
501
+ selectedIndicesSet: selectedChartIndices,
449
502
  selectionBehavior,
450
503
  isLast: index === lastVisibleIndex && !hasRemaining,
451
504
  onMouseEnter: () => {
@@ -461,25 +514,31 @@ const StackedBarChart = (props) => {
461
514
  onClick: () => !item.isExiting && handleSegmentClick(item.originalIndex)
462
515
  });
463
516
  }), previousProcessedRef.current.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BarSegment, {
464
- color: theme === null || theme === void 0 || (_theme$palette4 = theme.palette) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.sds) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.base) === null || _theme$palette4 === void 0 ? void 0 : _theme$palette4.backgroundTertiary,
517
+ color: theme === null || theme === void 0 || (_theme$palette5 = theme.palette) === null || _theme$palette5 === void 0 || (_theme$palette5 = _theme$palette5.sds) === null || _theme$palette5 === void 0 || (_theme$palette5 = _theme$palette5.base) === null || _theme$palette5 === void 0 ? void 0 : _theme$palette5.backgroundTertiary,
465
518
  percentage: displayRemainingPercentage,
466
519
  height: barHeight,
467
520
  isLast: true,
468
- opacity: 1,
521
+ opacity: hasOverflow ? getSegmentOpacity(overflowIndex, hoveredChartIndex, selectedChartIndices, selectionBehavior) : 1,
469
522
  disabled: true
470
523
  }, "remaining")]
471
524
  }), showLegend && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Legend, {
472
525
  items: legendItems,
526
+ lastItemMenuItems: remainingMenuItems,
473
527
  showValues: showLegendValues,
474
528
  onItemMouseEnter: handleLegendItemHover,
475
529
  onItemMouseLeave: handleLegendItemLeave,
476
530
  onItemClick: onLegendItemClick ? handleLegendItemClickInternal : void 0,
477
- selectedIndices,
478
- onSelectionChange: onSelectionChange ? handleSelectionChange : void 0,
479
- hoveredIndex
531
+ selectedIndices: selectedIndices.map(toLegendIndex),
532
+ onSelectionChange: onSelectionChange ? (indices) => handleSelectionChange(indices.map(fromLegendIndex)) : void 0,
533
+ hoveredIndex: hoveredIndex === null ? null : toLegendIndex(hoveredIndex)
480
534
  })] });
481
535
  if (!title) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartWrapper, require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({ width }, rest), {}, { children: chartContent }));
482
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChartWrapper, require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({ width }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TitleContainer, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTitle, { children: title }), !hideBadge && displayBadge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StyledBadge, { children: displayBadge })] }), chartContent] }));
536
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChartWrapper, require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({ width }, rest), {}, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TitleContainer, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTitle, { children: title }), !hideBadge && displayBadge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_czi_sds_components.Tag, {
537
+ label: displayBadge,
538
+ sdsStyle: "outline",
539
+ color: "neutral",
540
+ size: "small"
541
+ })] }), chartContent] }));
483
542
  };
484
543
  //#endregion
485
544
  Object.defineProperty(exports, "StackedBarChart", {
@@ -3,7 +3,7 @@ import { n as _objectWithoutProperties, t as _objectSpread2 } from "./objectSpre
3
3
  import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import styled from "@emotion/styled";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
- import { Legend, Tooltip, TooltipTable, fontHeaderS, fontTabularSemiboldXxxxs, generateDiscreteColors, getCorners, getMode, getSemanticColors, getSpaces } from "@czi-sds/components";
6
+ import { Legend, Tag, Tooltip, TooltipTable, fontBodyMediumXs, generateDiscreteColors, getCorners, getMode, getSemanticColors, getSpaces } from "@czi-sds/components";
7
7
  import { useTheme } from "@mui/material";
8
8
  //#region src/core/StackedBarChart/style.ts
9
9
  const ChartWrapper = styled("div")`
@@ -19,12 +19,12 @@ const TitleContainer = styled("div")`
19
19
  const spaces = getSpaces(props);
20
20
  return `
21
21
  gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
22
- margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
22
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
23
23
  `;
24
24
  }}
25
25
  `;
26
26
  const ChartTitle = styled("div")`
27
- ${fontHeaderS}
27
+ ${fontBodyMediumXs}
28
28
 
29
29
  ${(props) => {
30
30
  var _semanticColors$base;
@@ -34,36 +34,19 @@ const ChartTitle = styled("div")`
34
34
  `;
35
35
  }}
36
36
  `;
37
- const StyledBadge = styled("div")`
38
- ${fontTabularSemiboldXxxxs}
39
-
40
- ${(props) => {
41
- var _semanticColors$base2, _semanticColors$base3;
42
- const spaces = getSpaces(props);
43
- const corners = getCorners(props);
44
- const semanticColors = getSemanticColors(props);
45
- return `
46
- padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
47
- border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
48
- background-color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.backgroundTertiary};
49
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textSecondary};
50
- `;
51
- }}
52
- `;
53
37
  const BarContainer = styled("div")`
54
38
  ${(props) => {
55
- var _semanticColors$base4;
39
+ var _semanticColors$base2;
56
40
  const { width, barHeight, isEmpty } = props;
57
41
  const corners = getCorners(props);
58
42
  const semanticColors = getSemanticColors(props);
59
43
  const finalWidth = typeof width === "number" ? `${width}px` : width;
60
44
  return `
61
45
  display: flex;
62
- border-radius: ${corners === null || corners === void 0 ? void 0 : corners.s}px;
46
+ border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
63
47
  width: ${finalWidth};
64
48
  height: ${barHeight}px;
65
- background-color: ${isEmpty ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.backgroundTertiary : "transparent"};
66
- overflow: hidden;
49
+ background-color: ${isEmpty ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.backgroundTertiary : "transparent"};
67
50
  `;
68
51
  }}
69
52
  `;
@@ -71,14 +54,17 @@ const BarSegment = styled("div")`
71
54
  ${(props) => {
72
55
  const { color, height, percentage, opacity, disabled, isLast } = props;
73
56
  const spaces = getSpaces(props);
57
+ const corners = getCorners(props);
74
58
  const gap = (spaces === null || spaces === void 0 ? void 0 : spaces.xxxs) || 0;
59
+ const marginRight = percentage > 0 && !isLast ? `${gap}px` : 0;
75
60
  return `
76
61
  background-color: ${color};
62
+ border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
77
63
  height: ${height}px;
78
64
  flex-grow: ${percentage};
79
65
  flex-basis: 0;
80
66
  flex-shrink: 1;
81
- margin-right: ${percentage > 0 && !isLast ? `${gap}px` : 0};
67
+ margin-right: ${marginRight};
82
68
  opacity: ${opacity};
83
69
  transition: opacity 0.2s ease-in-out, flex-grow 0.25s ease-in-out, margin-right 0.25s ease-out;
84
70
  cursor: ${disabled ? "default" : "pointer"};
@@ -92,7 +78,7 @@ const StyledStackedBarChartWrapper = styled("div")`
92
78
  return `
93
79
  display: flex;
94
80
  flex-direction: column;
95
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
81
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
96
82
  `;
97
83
  }}
98
84
  `;
@@ -122,17 +108,18 @@ const _excluded = [
122
108
  "remainingLabel",
123
109
  "remainingUnit",
124
110
  "unit",
125
- "colorGeneratorOptions"
111
+ "colorGeneratorOptions",
112
+ "maxItems"
126
113
  ];
127
114
  const calculateBadgeText = (totalItems, selectedCount) => {
128
115
  if (selectedCount === 0 || selectedCount === totalItems) return `${totalItems}`;
129
116
  return `${selectedCount} of ${totalItems}`;
130
117
  };
131
118
  const calculateLegendValue = (item, showLegendValues, legendValueFormat, globalUnit) => {
119
+ var _ref, _item$unit;
132
120
  if (!showLegendValues) return void 0;
133
121
  if (legendValueFormat === "percentage") return `${Math.round(item.percentage)}%`;
134
- const effectiveUnit = item.unit || globalUnit;
135
- return effectiveUnit ? `${item.value} ${effectiveUnit}` : `${item.value}`;
122
+ return `${item.value.toLocaleString()} ${(_ref = (_item$unit = item.unit) !== null && _item$unit !== void 0 ? _item$unit : globalUnit) !== null && _ref !== void 0 ? _ref : ""}`.trim();
136
123
  };
137
124
  const getSegmentOpacity = (index, hoveredIndex, selectedIndicesSet, selectionBehavior) => {
138
125
  const hasSelection = selectedIndicesSet.size > 0;
@@ -179,7 +166,7 @@ const applySelectionStyling = (items, selectedIndicesSet, selectionBehavior, mod
179
166
  return mode === "cumulative" ? applyCumulativeHideStyling(items, selectedIndicesSet, selectedTotal, hasRemaining, effectiveMaxAmount) : applyProportionalHideStyling(items, selectedIndicesSet, selectedTotal);
180
167
  };
181
168
  const buildLegendItems = (dataWithPercentages, options) => {
182
- const { showLegendValues, legendValueFormat, unit, hasRemaining, showLegend, remainingValue, remainingPercentage, remainingLabel, remainingUnit, theme } = options;
169
+ const { showLegendValues, legendValueFormat, unit, hasRemaining, showLegend, remainingValue, remainingPercentage, remainingLabel, remainingUnit, remainingOpensMenu, theme } = options;
183
170
  const legendItems = dataWithPercentages.map((item) => ({
184
171
  name: item.name,
185
172
  value: calculateLegendValue(item, showLegendValues, legendValueFormat, unit),
@@ -193,13 +180,13 @@ const buildLegendItems = (dataWithPercentages, options) => {
193
180
  else if (legendValueFormat === "percentage") remainingValueDisplay = `${Math.round(remainingPercentage)}%`;
194
181
  else {
195
182
  const effectiveRemainingUnit = remainingUnit || unit || "";
196
- remainingValueDisplay = effectiveRemainingUnit ? `${remainingValue} ${effectiveRemainingUnit}` : `${remainingValue}`;
183
+ remainingValueDisplay = effectiveRemainingUnit ? `${remainingValue.toLocaleString()} ${effectiveRemainingUnit}` : `${remainingValue.toLocaleString()}`;
197
184
  }
198
185
  legendItems.push({
199
186
  name: remainingLabel,
200
187
  value: remainingValueDisplay,
201
188
  color: theme === null || theme === void 0 || (_theme$palette = theme.palette) === null || _theme$palette === void 0 || (_theme$palette = _theme$palette.sds) === null || _theme$palette === void 0 || (_theme$palette = _theme$palette.base) === null || _theme$palette === void 0 ? void 0 : _theme$palette.backgroundTertiary,
202
- disabled: true
189
+ disabled: !remainingOpensMenu
203
190
  });
204
191
  }
205
192
  return legendItems;
@@ -235,10 +222,13 @@ const renderBarSegment = (item, options) => {
235
222
  }, item.key) : cloneElement(barSegment, { key: item.key });
236
223
  };
237
224
  const StackedBarChart = (props) => {
238
- var _theme$palette4;
239
- const { title, badge, hideBadge = false, data, width = "100%", barHeight: barHeightProp = 16, showLegend = true, showLegendValues = true, legendValueFormat = "percentage", selectedIndices = [], onSelectionChange, onSegmentMouseEnter, onSegmentMouseLeave, onLegendItemMouseEnter, onLegendItemMouseLeave, onSegmentClick, onLegendItemClick, selectionBehavior = "dim", mode = "proportional", maxAmount, remainingLabel = "Remaining", remainingUnit, unit, colorGeneratorOptions } = props, rest = _objectWithoutProperties(props, _excluded);
225
+ var _theme$palette5;
226
+ const { title, badge, hideBadge = false, data, width = "100%", barHeight: barHeightProp = 8, showLegend = true, showLegendValues = true, legendValueFormat = "percentage", selectedIndices = [], onSelectionChange, onSegmentMouseEnter, onSegmentMouseLeave, onLegendItemMouseEnter, onLegendItemMouseLeave, onSegmentClick, onLegendItemClick, selectionBehavior = "dim", mode = "proportional", maxAmount, remainingLabel = "Remaining", remainingUnit, unit, colorGeneratorOptions, maxItems } = props, rest = _objectWithoutProperties(props, _excluded);
240
227
  const barHeight = Math.max(1, barHeightProp);
241
228
  const theme = useTheme();
229
+ const visibleCount = mode === "cumulative" && maxItems !== void 0 && maxItems < data.length ? Math.max(0, maxItems) : data.length;
230
+ const chartData = useMemo(() => data.slice(0, visibleCount), [data, visibleCount]);
231
+ const hiddenItems = useMemo(() => data.slice(visibleCount), [data, visibleCount]);
242
232
  const [hoveredIndex, setHoveredIndex] = useState(null);
243
233
  const [animatedItems, setAnimatedItems] = useState([]);
244
234
  const [autoColorMap, setAutoColorMap] = useState(/* @__PURE__ */ new Map());
@@ -261,8 +251,8 @@ const StackedBarChart = (props) => {
261
251
  theme
262
252
  ]);
263
253
  useEffect(() => {
264
- const autoItems = data.filter((item) => !item.color);
265
- const currentDataKeys = new Set(data.map((item) => item.name));
254
+ const autoItems = chartData.filter((item) => !item.color);
255
+ const currentDataKeys = new Set(chartData.map((item) => item.name));
266
256
  const previousDataKeys = previousDataKeysRef.current;
267
257
  const previousColorOptions = previousColorOptionsRef.current;
268
258
  const colorOptionsChanged = !previousColorOptions || previousColorOptions.isDarkMode !== isDarkMode || previousColorOptions.options !== colorGeneratorOptions;
@@ -288,7 +278,7 @@ const StackedBarChart = (props) => {
288
278
  if (hasChanges) setAutoColorMap(updatedMap);
289
279
  }
290
280
  }, [
291
- data,
281
+ chartData,
292
282
  colorGeneratorOptions,
293
283
  isDarkMode,
294
284
  theme,
@@ -297,9 +287,10 @@ const StackedBarChart = (props) => {
297
287
  const displayBadge = badge !== null && badge !== void 0 ? badge : calculateBadgeText(data.length, selectedIndices.length);
298
288
  const { effectiveMaxAmount, hasRemaining, remainingValue, remainingPercentage } = useMemo(() => {
299
289
  const total = data.reduce((sum, item) => sum + item.value, 0);
290
+ const visibleTotal = chartData.reduce((sum, item) => sum + item.value, 0);
300
291
  const maxAmountValue = mode === "cumulative" && maxAmount ? maxAmount : total;
301
- const hasRemainingSegment = mode === "cumulative" && total < maxAmountValue;
302
- const remaining = hasRemainingSegment ? maxAmountValue - total : 0;
292
+ const hasRemainingSegment = mode === "cumulative" && visibleTotal < maxAmountValue;
293
+ const remaining = hasRemainingSegment ? maxAmountValue - visibleTotal : 0;
303
294
  return {
304
295
  effectiveMaxAmount: maxAmountValue,
305
296
  hasRemaining: hasRemainingSegment,
@@ -308,11 +299,20 @@ const StackedBarChart = (props) => {
308
299
  };
309
300
  }, [
310
301
  data,
302
+ chartData,
311
303
  mode,
312
304
  maxAmount
313
305
  ]);
306
+ const hasOverflow = hasRemaining && hiddenItems.length > 0;
307
+ const overflowIndex = data.length;
308
+ const hiddenColors = useMemo(() => hasOverflow ? generateDiscreteColors(hiddenItems.length, _objectSpread2(_objectSpread2({}, colorGeneratorOptions || {}), {}, { isDarkMode })) : [], [
309
+ hasOverflow,
310
+ hiddenItems.length,
311
+ colorGeneratorOptions,
312
+ isDarkMode
313
+ ]);
314
314
  const dataWithPercentages = useMemo(() => {
315
- return data.map((item, index) => {
315
+ return chartData.map((item, index) => {
316
316
  var _theme$palette3;
317
317
  const resolvedColor = item.color || autoColorMap.get(item.name) || (theme === null || theme === void 0 || (_theme$palette3 = theme.palette) === null || _theme$palette3 === void 0 || (_theme$palette3 = _theme$palette3.sds) === null || _theme$palette3 === void 0 || (_theme$palette3 = _theme$palette3.base) === null || _theme$palette3 === void 0 ? void 0 : _theme$palette3.ornamentSecondary);
318
318
  return _objectSpread2(_objectSpread2({}, item), {}, {
@@ -325,7 +325,7 @@ const StackedBarChart = (props) => {
325
325
  });
326
326
  });
327
327
  }, [
328
- data,
328
+ chartData,
329
329
  effectiveMaxAmount,
330
330
  autoColorMap,
331
331
  theme
@@ -366,10 +366,16 @@ const StackedBarChart = (props) => {
366
366
  timeoutIds.forEach((id) => window.clearTimeout(id));
367
367
  };
368
368
  }, [dataWithPercentages, remainingPercentage]);
369
- const selectedIndicesSet = useMemo(() => new Set(selectedIndices), [selectedIndices]);
370
- const { styledItems, updatedRemainingPercentage, updatedRemainingValue } = useMemo(() => applySelectionStyling(animatedItems, selectedIndicesSet, selectionBehavior, mode, hasRemaining, effectiveMaxAmount, animatedRemainingPercentage, remainingValue), [
369
+ const toChartIndex = useCallback((index) => hasOverflow && index >= visibleCount ? overflowIndex : index, [
370
+ hasOverflow,
371
+ visibleCount,
372
+ overflowIndex
373
+ ]);
374
+ const hoveredChartIndex = hoveredIndex === null ? null : toChartIndex(hoveredIndex);
375
+ const selectedChartIndices = useMemo(() => new Set(selectedIndices.map(toChartIndex)), [selectedIndices, toChartIndex]);
376
+ const { styledItems, updatedRemainingPercentage, updatedRemainingValue } = useMemo(() => applySelectionStyling(animatedItems, selectedChartIndices, selectionBehavior, mode, hasRemaining, effectiveMaxAmount, animatedRemainingPercentage, remainingValue), [
371
377
  animatedItems,
372
- selectedIndicesSet,
378
+ selectedChartIndices,
373
379
  selectionBehavior,
374
380
  mode,
375
381
  hasRemaining,
@@ -390,6 +396,7 @@ const StackedBarChart = (props) => {
390
396
  remainingPercentage: displayRemainingPercentage,
391
397
  remainingLabel,
392
398
  remainingUnit,
399
+ remainingOpensMenu: hasOverflow,
393
400
  theme
394
401
  }), [
395
402
  dataWithPercentages,
@@ -402,19 +409,51 @@ const StackedBarChart = (props) => {
402
409
  displayRemainingPercentage,
403
410
  remainingLabel,
404
411
  remainingUnit,
412
+ hasOverflow,
405
413
  theme
406
414
  ]);
407
- const handleLegendItemHover = useCallback((_item, index) => {
415
+ const toLegendIndex = useCallback((index) => {
416
+ if (!hasOverflow || index < visibleCount) return index;
417
+ return index === overflowIndex ? visibleCount : index + 1;
418
+ }, [
419
+ hasOverflow,
420
+ visibleCount,
421
+ overflowIndex
422
+ ]);
423
+ const fromLegendIndex = useCallback((index) => {
424
+ if (!hasOverflow || index < visibleCount) return index;
425
+ return index === visibleCount ? overflowIndex : index - 1;
426
+ }, [
427
+ hasOverflow,
428
+ visibleCount,
429
+ overflowIndex
430
+ ]);
431
+ const handleLegendItemHover = useCallback((_item, legendIndex) => {
432
+ const index = fromLegendIndex(legendIndex);
408
433
  setHoveredIndex(index);
409
434
  if (onLegendItemMouseEnter && data[index]) onLegendItemMouseEnter(data[index], index);
410
- }, [onLegendItemMouseEnter, data]);
411
- const handleLegendItemLeave = useCallback((_item, index) => {
435
+ }, [
436
+ onLegendItemMouseEnter,
437
+ data,
438
+ fromLegendIndex
439
+ ]);
440
+ const handleLegendItemLeave = useCallback((_item, legendIndex) => {
441
+ const index = fromLegendIndex(legendIndex);
412
442
  setHoveredIndex(null);
413
443
  if (onLegendItemMouseLeave && data[index]) onLegendItemMouseLeave(data[index], index);
414
- }, [onLegendItemMouseLeave, data]);
415
- const handleLegendItemClickInternal = useCallback((_item, index) => {
444
+ }, [
445
+ onLegendItemMouseLeave,
446
+ data,
447
+ fromLegendIndex
448
+ ]);
449
+ const handleLegendItemClickInternal = useCallback((_item, legendIndex) => {
450
+ const index = fromLegendIndex(legendIndex);
416
451
  if (onLegendItemClick && data[index]) onLegendItemClick(data[index], index);
417
- }, [onLegendItemClick, data]);
452
+ }, [
453
+ onLegendItemClick,
454
+ data,
455
+ fromLegendIndex
456
+ ]);
418
457
  const handleSelectionChange = useCallback((newSelectedIndices) => {
419
458
  if (!onSelectionChange) return;
420
459
  onSelectionChange(newSelectedIndices, data.filter((item, itemIndex) => newSelectedIndices.includes(itemIndex)));
@@ -422,15 +461,29 @@ const StackedBarChart = (props) => {
422
461
  const handleSegmentClick = useCallback((index) => {
423
462
  if (onSegmentClick && data[index]) onSegmentClick(data[index], index);
424
463
  if (!onSelectionChange) return;
425
- handleSelectionChange(selectedIndicesSet.has(index) ? selectedIndices.filter((i) => i !== index) : [...selectedIndices, index]);
464
+ handleSelectionChange(selectedChartIndices.has(index) ? selectedIndices.filter((i) => i !== index) : [...selectedIndices, index]);
426
465
  }, [
427
466
  onSelectionChange,
428
467
  onSegmentClick,
429
468
  data,
430
469
  selectedIndices,
431
- selectedIndicesSet,
470
+ selectedChartIndices,
432
471
  handleSelectionChange
433
472
  ]);
473
+ const remainingMenuItems = useMemo(() => hasOverflow ? hiddenItems.map((item, index) => {
474
+ var _theme$palette4;
475
+ return {
476
+ name: item.name,
477
+ value: item.value,
478
+ color: item.color || hiddenColors[index] || (theme === null || theme === void 0 || (_theme$palette4 = theme.palette) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.sds) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.base) === null || _theme$palette4 === void 0 ? void 0 : _theme$palette4.ornamentSecondary),
479
+ disabled: item.disabled
480
+ };
481
+ }) : [], [
482
+ hasOverflow,
483
+ hiddenItems,
484
+ hiddenColors,
485
+ theme
486
+ ]);
434
487
  const lastVisibleIndex = useMemo(() => {
435
488
  for (let i = styledItems.length - 1; i >= 0; i--) if (styledItems[i].percentage > 0) return i;
436
489
  return -1;
@@ -442,8 +495,8 @@ const StackedBarChart = (props) => {
442
495
  children: [styledItems.map((item, index) => {
443
496
  return renderBarSegment(item, {
444
497
  barHeight,
445
- hoveredIndex,
446
- selectedIndicesSet,
498
+ hoveredIndex: hoveredChartIndex,
499
+ selectedIndicesSet: selectedChartIndices,
447
500
  selectionBehavior,
448
501
  isLast: index === lastVisibleIndex && !hasRemaining,
449
502
  onMouseEnter: () => {
@@ -459,25 +512,31 @@ const StackedBarChart = (props) => {
459
512
  onClick: () => !item.isExiting && handleSegmentClick(item.originalIndex)
460
513
  });
461
514
  }), previousProcessedRef.current.length > 0 && /* @__PURE__ */ jsx(BarSegment, {
462
- color: theme === null || theme === void 0 || (_theme$palette4 = theme.palette) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.sds) === null || _theme$palette4 === void 0 || (_theme$palette4 = _theme$palette4.base) === null || _theme$palette4 === void 0 ? void 0 : _theme$palette4.backgroundTertiary,
515
+ color: theme === null || theme === void 0 || (_theme$palette5 = theme.palette) === null || _theme$palette5 === void 0 || (_theme$palette5 = _theme$palette5.sds) === null || _theme$palette5 === void 0 || (_theme$palette5 = _theme$palette5.base) === null || _theme$palette5 === void 0 ? void 0 : _theme$palette5.backgroundTertiary,
463
516
  percentage: displayRemainingPercentage,
464
517
  height: barHeight,
465
518
  isLast: true,
466
- opacity: 1,
519
+ opacity: hasOverflow ? getSegmentOpacity(overflowIndex, hoveredChartIndex, selectedChartIndices, selectionBehavior) : 1,
467
520
  disabled: true
468
521
  }, "remaining")]
469
522
  }), showLegend && /* @__PURE__ */ jsx(Legend, {
470
523
  items: legendItems,
524
+ lastItemMenuItems: remainingMenuItems,
471
525
  showValues: showLegendValues,
472
526
  onItemMouseEnter: handleLegendItemHover,
473
527
  onItemMouseLeave: handleLegendItemLeave,
474
528
  onItemClick: onLegendItemClick ? handleLegendItemClickInternal : void 0,
475
- selectedIndices,
476
- onSelectionChange: onSelectionChange ? handleSelectionChange : void 0,
477
- hoveredIndex
529
+ selectedIndices: selectedIndices.map(toLegendIndex),
530
+ onSelectionChange: onSelectionChange ? (indices) => handleSelectionChange(indices.map(fromLegendIndex)) : void 0,
531
+ hoveredIndex: hoveredIndex === null ? null : toLegendIndex(hoveredIndex)
478
532
  })] });
479
533
  if (!title) return /* @__PURE__ */ jsx(ChartWrapper, _objectSpread2(_objectSpread2({ width }, rest), {}, { children: chartContent }));
480
- return /* @__PURE__ */ jsxs(ChartWrapper, _objectSpread2(_objectSpread2({ width }, rest), {}, { children: [/* @__PURE__ */ jsxs(TitleContainer, { children: [/* @__PURE__ */ jsx(ChartTitle, { children: title }), !hideBadge && displayBadge && /* @__PURE__ */ jsx(StyledBadge, { children: displayBadge })] }), chartContent] }));
534
+ return /* @__PURE__ */ jsxs(ChartWrapper, _objectSpread2(_objectSpread2({ width }, rest), {}, { children: [/* @__PURE__ */ jsxs(TitleContainer, { children: [/* @__PURE__ */ jsx(ChartTitle, { children: title }), !hideBadge && displayBadge && /* @__PURE__ */ jsx(Tag, {
535
+ label: displayBadge,
536
+ sdsStyle: "outline",
537
+ color: "neutral",
538
+ size: "small"
539
+ })] }), chartContent] }));
481
540
  };
482
541
  //#endregion
483
542
  export { StackedBarChart as t };
@@ -64,7 +64,7 @@ interface StackedBarChartProps extends HTMLAttributes<HTMLDivElement> {
64
64
  /**
65
65
  * Height of the bar in pixels. Values below 1 are clamped to 1, so the bar
66
66
  * cannot disappear.
67
- * @default 16
67
+ * @default 8
68
68
  */
69
69
  barHeight?: number;
70
70
  /**
@@ -139,6 +139,12 @@ interface StackedBarChartProps extends HTMLAttributes<HTMLDivElement> {
139
139
  * between it and the sum of the data is drawn as the Remaining segment.
140
140
  */
141
141
  maxAmount?: number;
142
+ /**
143
+ * Cumulative mode only. How many items are drawn as their own segment. The
144
+ * rest fold into the Remaining segment, whose legend item opens a menu
145
+ * listing them.
146
+ */
147
+ maxItems?: number;
142
148
  /**
143
149
  * Name of the grey gap segment in cumulative mode.
144
150
  * @default "Remaining"
@@ -64,7 +64,7 @@ interface StackedBarChartProps extends HTMLAttributes<HTMLDivElement> {
64
64
  /**
65
65
  * Height of the bar in pixels. Values below 1 are clamped to 1, so the bar
66
66
  * cannot disappear.
67
- * @default 16
67
+ * @default 8
68
68
  */
69
69
  barHeight?: number;
70
70
  /**
@@ -139,6 +139,12 @@ interface StackedBarChartProps extends HTMLAttributes<HTMLDivElement> {
139
139
  * between it and the sum of the data is drawn as the Remaining segment.
140
140
  */
141
141
  maxAmount?: number;
142
+ /**
143
+ * Cumulative mode only. How many items are drawn as their own segment. The
144
+ * rest fold into the Remaining segment, whose legend item opens a menu
145
+ * listing them.
146
+ */
147
+ maxItems?: number;
142
148
  /**
143
149
  * Name of the grey gap segment in cumulative mode.
144
150
  * @default "Remaining"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@czi-sds/data-viz",
3
- "version": "2.5.1",
3
+ "version": "3.0.0",
4
4
  "main": "dist/index.cjs.js",
5
5
  "types": "dist/index.cjs.d.ts",
6
6
  "module": "dist/index.esm.js",
@@ -121,7 +121,7 @@
121
121
  "namespace-check": "tsc --p tsconfig-namespace-check.json"
122
122
  },
123
123
  "peerDependencies": {
124
- "@czi-sds/components": "^24.0.0",
124
+ "@czi-sds/components": "^25.0.0",
125
125
  "@emotion/styled": "^11.11.0",
126
126
  "@mui/material": "^9.0.0",
127
127
  "@phosphor-icons/react": "^2.1.10",
@@ -157,5 +157,5 @@
157
157
  "echarts": "^6.0.0",
158
158
  "lodash": "^4.17.21"
159
159
  },
160
- "gitHead": "aa0c756f82d9b899e9e7cc66ab2a6d1684ceb1f7"
160
+ "gitHead": "d38c18e3aca773081cb5c0413dbfefb10da774b8"
161
161
  }