@czi-sds/data-viz 2.3.0 → 2.4.1

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.
Files changed (39) hide show
  1. package/dist/HeatmapChart.cjs.d.ts +2 -0
  2. package/dist/HeatmapChart.cjs.js +8 -0
  3. package/dist/HeatmapChart.esm.d.ts +2 -0
  4. package/dist/HeatmapChart.esm.js +3 -0
  5. package/dist/ProteinStructureViewer.cjs.d.ts +2 -0
  6. package/dist/ProteinStructureViewer.cjs.js +14 -0
  7. package/dist/ProteinStructureViewer.esm.d.ts +2 -0
  8. package/dist/ProteinStructureViewer.esm.js +3 -0
  9. package/dist/StackedBarChart.cjs.d.ts +2 -0
  10. package/dist/StackedBarChart.cjs.js +8 -0
  11. package/dist/StackedBarChart.esm.d.ts +2 -0
  12. package/dist/StackedBarChart.esm.js +3 -0
  13. package/dist/colorScales.cjs.d.ts +2 -0
  14. package/dist/colorScales.cjs.js +6 -0
  15. package/dist/colorScales.esm.d.ts +2 -0
  16. package/dist/colorScales.esm.js +3 -0
  17. package/dist/index.cjs.d.ts +4 -772
  18. package/dist/index.cjs.js +16 -5765
  19. package/dist/index.esm.d.ts +4 -774
  20. package/dist/index.esm.js +4 -5719
  21. package/dist/shared/HeatmapChart.cjs.js +349 -0
  22. package/dist/shared/HeatmapChart.esm.js +342 -0
  23. package/dist/shared/ProteinStructureViewer.cjs.js +4790 -0
  24. package/dist/shared/ProteinStructureViewer.esm.js +4738 -0
  25. package/dist/shared/StackedBarChart.cjs.js +489 -0
  26. package/dist/shared/StackedBarChart.esm.js +483 -0
  27. package/dist/shared/colorScales.cjs.d.ts +45 -0
  28. package/dist/shared/colorScales.cjs.js +126 -0
  29. package/dist/shared/colorScales.esm.d.ts +45 -0
  30. package/dist/shared/colorScales.esm.js +103 -0
  31. package/dist/shared/index.cjs.d.ts +183 -0
  32. package/dist/shared/index.esm.d.ts +183 -0
  33. package/dist/shared/index2.cjs.d.ts +389 -0
  34. package/dist/shared/index2.esm.d.ts +391 -0
  35. package/dist/shared/index3.cjs.d.ts +166 -0
  36. package/dist/shared/index3.esm.d.ts +166 -0
  37. package/dist/shared/objectSpread2.cjs.js +124 -0
  38. package/dist/shared/objectSpread2.esm.js +85 -0
  39. package/package.json +65 -2
@@ -0,0 +1,483 @@
1
+ "use client";
2
+ import { n as _objectWithoutProperties, t as _objectSpread2 } from "./objectSpread2.esm.js";
3
+ import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import styled from "@emotion/styled";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Legend, Tooltip, TooltipTable, fontHeaderS, fontTabularSemiboldXxxxs, generateDiscreteColors, getCorners, getMode, getSemanticColors, getSpaces } from "@czi-sds/components";
7
+ import { useTheme } from "@mui/material";
8
+ //#region src/core/StackedBarChart/style.ts
9
+ const ChartWrapper = styled("div")`
10
+ display: flex;
11
+ flex-direction: column;
12
+ width: ${(props) => typeof props.width === "number" ? `${props.width}px` : props.width};
13
+ `;
14
+ const TitleContainer = styled("div")`
15
+ display: flex;
16
+ align-items: center;
17
+
18
+ ${(props) => {
19
+ const spaces = getSpaces(props);
20
+ return `
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;
23
+ `;
24
+ }}
25
+ `;
26
+ const ChartTitle = styled("div")`
27
+ ${fontHeaderS}
28
+
29
+ ${(props) => {
30
+ var _semanticColors$base;
31
+ const semanticColors = getSemanticColors(props);
32
+ return `
33
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textPrimary};
34
+ `;
35
+ }}
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
+ const BarContainer = styled("div")`
54
+ ${(props) => {
55
+ var _semanticColors$base4;
56
+ const { width, barHeight, isEmpty } = props;
57
+ const corners = getCorners(props);
58
+ const semanticColors = getSemanticColors(props);
59
+ const finalWidth = typeof width === "number" ? `${width}px` : width;
60
+ return `
61
+ display: flex;
62
+ border-radius: ${corners === null || corners === void 0 ? void 0 : corners.s}px;
63
+ width: ${finalWidth};
64
+ 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;
67
+ `;
68
+ }}
69
+ `;
70
+ const BarSegment = styled("div")`
71
+ ${(props) => {
72
+ const { color, height, percentage, opacity, disabled, isLast } = props;
73
+ const spaces = getSpaces(props);
74
+ const gap = (spaces === null || spaces === void 0 ? void 0 : spaces.xxxs) || 0;
75
+ return `
76
+ background-color: ${color};
77
+ height: ${height}px;
78
+ flex-grow: ${percentage};
79
+ flex-basis: 0;
80
+ flex-shrink: 1;
81
+ margin-right: ${percentage > 0 && !isLast ? `${gap}px` : 0};
82
+ opacity: ${opacity};
83
+ transition: opacity 0.2s ease-in-out, flex-grow 0.25s ease-in-out, margin-right 0.25s ease-out;
84
+ cursor: ${disabled ? "default" : "pointer"};
85
+ pointer-events: ${disabled ? "none" : "auto"};
86
+ `;
87
+ }}
88
+ `;
89
+ const StyledStackedBarChartWrapper = styled("div")`
90
+ ${(props) => {
91
+ const spaces = getSpaces(props);
92
+ return `
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
96
+ `;
97
+ }}
98
+ `;
99
+ //#endregion
100
+ //#region src/core/StackedBarChart/index.tsx
101
+ const _excluded = [
102
+ "title",
103
+ "badge",
104
+ "hideBadge",
105
+ "data",
106
+ "width",
107
+ "barHeight",
108
+ "showLegend",
109
+ "showLegendValues",
110
+ "legendValueFormat",
111
+ "selectedIndices",
112
+ "onSelectionChange",
113
+ "onSegmentMouseEnter",
114
+ "onSegmentMouseLeave",
115
+ "onLegendItemMouseEnter",
116
+ "onLegendItemMouseLeave",
117
+ "onSegmentClick",
118
+ "onLegendItemClick",
119
+ "selectionBehavior",
120
+ "mode",
121
+ "maxAmount",
122
+ "remainingLabel",
123
+ "remainingUnit",
124
+ "unit",
125
+ "colorGeneratorOptions"
126
+ ];
127
+ const calculateBadgeText = (totalItems, selectedCount) => {
128
+ if (selectedCount === 0 || selectedCount === totalItems) return `${totalItems}`;
129
+ return `${selectedCount} of ${totalItems}`;
130
+ };
131
+ const calculateLegendValue = (item, showLegendValues, legendValueFormat, globalUnit) => {
132
+ if (!showLegendValues) return void 0;
133
+ if (legendValueFormat === "percentage") return `${Math.round(item.percentage)}%`;
134
+ const effectiveUnit = item.unit || globalUnit;
135
+ return effectiveUnit ? `${item.value} ${effectiveUnit}` : `${item.value}`;
136
+ };
137
+ const getSegmentOpacity = (index, hoveredIndex, selectedIndicesSet, selectionBehavior) => {
138
+ const hasSelection = selectedIndicesSet.size > 0;
139
+ if (selectionBehavior === "hide") return 1;
140
+ if (hoveredIndex !== null) return hoveredIndex === index || selectedIndicesSet.has(index) ? 1 : .2;
141
+ if (hasSelection) return selectedIndicesSet.has(index) ? 1 : .2;
142
+ return 1;
143
+ };
144
+ const applyCumulativeHideStyling = (items, selectedIndicesSet, selectedTotal, hasRemaining, effectiveMaxAmount) => {
145
+ const newRemainingValue = hasRemaining ? effectiveMaxAmount - selectedTotal : 0;
146
+ const newRemainingPercentage = hasRemaining ? newRemainingValue / effectiveMaxAmount * 100 : 0;
147
+ return {
148
+ styledItems: items.map((item) => {
149
+ const isSelected = selectedIndicesSet.has(item.originalIndex);
150
+ return _objectSpread2(_objectSpread2({}, item), {}, { percentage: isSelected ? item.percentage : 0 });
151
+ }),
152
+ updatedRemainingPercentage: newRemainingPercentage,
153
+ updatedRemainingValue: newRemainingValue
154
+ };
155
+ };
156
+ const applyProportionalHideStyling = (items, selectedIndicesSet, selectedTotal) => {
157
+ if (selectedTotal === 0) return {
158
+ styledItems: items.map((item) => _objectSpread2(_objectSpread2({}, item), {}, { percentage: 0 })),
159
+ updatedRemainingPercentage: 0,
160
+ updatedRemainingValue: 0
161
+ };
162
+ return {
163
+ styledItems: items.map((item) => {
164
+ const newPercentage = selectedIndicesSet.has(item.originalIndex) ? item.value / selectedTotal * 100 : 0;
165
+ return _objectSpread2(_objectSpread2({}, item), {}, { percentage: newPercentage });
166
+ }),
167
+ updatedRemainingPercentage: 0,
168
+ updatedRemainingValue: 0
169
+ };
170
+ };
171
+ const applySelectionStyling = (items, selectedIndicesSet, selectionBehavior, mode, hasRemaining, effectiveMaxAmount, baseRemainingPercentage, baseRemainingValue) => {
172
+ const hasSelection = selectedIndicesSet.size > 0;
173
+ if (selectionBehavior !== "hide" || !hasSelection) return {
174
+ styledItems: items,
175
+ updatedRemainingPercentage: baseRemainingPercentage,
176
+ updatedRemainingValue: baseRemainingValue
177
+ };
178
+ const selectedTotal = items.filter((item) => selectedIndicesSet.has(item.originalIndex)).reduce((sum, item) => sum + item.value, 0);
179
+ return mode === "cumulative" ? applyCumulativeHideStyling(items, selectedIndicesSet, selectedTotal, hasRemaining, effectiveMaxAmount) : applyProportionalHideStyling(items, selectedIndicesSet, selectedTotal);
180
+ };
181
+ const buildLegendItems = (dataWithPercentages, options) => {
182
+ const { showLegendValues, legendValueFormat, unit, hasRemaining, showLegend, remainingValue, remainingPercentage, remainingLabel, remainingUnit, theme } = options;
183
+ const legendItems = dataWithPercentages.map((item) => ({
184
+ name: item.name,
185
+ value: calculateLegendValue(item, showLegendValues, legendValueFormat, unit),
186
+ color: item.color,
187
+ disabled: item.disabled
188
+ }));
189
+ if (hasRemaining && showLegend) {
190
+ var _theme$palette;
191
+ let remainingValueDisplay;
192
+ if (!showLegendValues) remainingValueDisplay = void 0;
193
+ else if (legendValueFormat === "percentage") remainingValueDisplay = `${Math.round(remainingPercentage)}%`;
194
+ else {
195
+ const effectiveRemainingUnit = remainingUnit || unit || "";
196
+ remainingValueDisplay = effectiveRemainingUnit ? `${remainingValue} ${effectiveRemainingUnit}` : `${remainingValue}`;
197
+ }
198
+ legendItems.push({
199
+ name: remainingLabel,
200
+ value: remainingValueDisplay,
201
+ 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
203
+ });
204
+ }
205
+ return legendItems;
206
+ };
207
+ const renderBarSegment = (item, options) => {
208
+ const { barHeight, hoveredIndex, selectedIndicesSet, selectionBehavior, isLast, onMouseEnter, onMouseLeave, onClick } = options;
209
+ const isHidden = selectionBehavior === "hide" && item.percentage === 0;
210
+ const barSegment = /* @__PURE__ */ jsx(BarSegment, {
211
+ color: item.color,
212
+ percentage: item.isEntering || item.isExiting ? 0 : item.percentage,
213
+ height: barHeight,
214
+ opacity: getSegmentOpacity(item.originalIndex, hoveredIndex, selectedIndicesSet, selectionBehavior),
215
+ isLast,
216
+ "data-hide": isHidden,
217
+ onMouseEnter,
218
+ onMouseLeave,
219
+ onClick
220
+ });
221
+ return item.tooltip !== void 0 ? /* @__PURE__ */ jsx(Tooltip, {
222
+ title: null,
223
+ componentSlot: /* @__PURE__ */ jsx(TooltipTable, _objectSpread2({}, item.tooltip)),
224
+ placement: "top",
225
+ open: hoveredIndex === item.originalIndex && !isHidden,
226
+ disableInteractive: true,
227
+ slotProps: { popper: {
228
+ disablePortal: false,
229
+ modifiers: [{
230
+ name: "offset",
231
+ options: { offset: [0, 0] }
232
+ }]
233
+ } },
234
+ children: barSegment
235
+ }, item.key) : cloneElement(barSegment, { key: item.key });
236
+ };
237
+ 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);
240
+ const barHeight = Math.max(1, barHeightProp);
241
+ const theme = useTheme();
242
+ const [hoveredIndex, setHoveredIndex] = useState(null);
243
+ const [animatedItems, setAnimatedItems] = useState([]);
244
+ const [autoColorMap, setAutoColorMap] = useState(/* @__PURE__ */ new Map());
245
+ const [animatedRemainingPercentage, setAnimatedRemainingPercentage] = useState(0);
246
+ const previousProcessedRef = useRef([]);
247
+ const previousDataKeysRef = useRef(/* @__PURE__ */ new Set());
248
+ const previousColorOptionsRef = useRef(null);
249
+ const isDarkMode = useMemo(() => getMode({ theme }) === "dark", [theme]);
250
+ const generateColorMap = useCallback((items) => {
251
+ const palette = generateDiscreteColors(items.length, _objectSpread2(_objectSpread2({}, colorGeneratorOptions || {}), {}, { isDarkMode }));
252
+ const newColorMap = /* @__PURE__ */ new Map();
253
+ items.forEach((item, index) => {
254
+ var _theme$palette2;
255
+ newColorMap.set(item.name, palette[index] || (theme === null || theme === void 0 || (_theme$palette2 = theme.palette) === null || _theme$palette2 === void 0 || (_theme$palette2 = _theme$palette2.sds) === null || _theme$palette2 === void 0 || (_theme$palette2 = _theme$palette2.base) === null || _theme$palette2 === void 0 ? void 0 : _theme$palette2.ornamentSecondary));
256
+ });
257
+ return newColorMap;
258
+ }, [
259
+ colorGeneratorOptions,
260
+ isDarkMode,
261
+ theme
262
+ ]);
263
+ useEffect(() => {
264
+ const autoItems = data.filter((item) => !item.color);
265
+ const currentDataKeys = new Set(data.map((item) => item.name));
266
+ const previousDataKeys = previousDataKeysRef.current;
267
+ const previousColorOptions = previousColorOptionsRef.current;
268
+ const colorOptionsChanged = !previousColorOptions || previousColorOptions.isDarkMode !== isDarkMode || previousColorOptions.options !== colorGeneratorOptions;
269
+ previousDataKeysRef.current = currentDataKeys;
270
+ previousColorOptionsRef.current = {
271
+ options: colorGeneratorOptions,
272
+ isDarkMode
273
+ };
274
+ if (autoItems.length === 0) {
275
+ if (autoColorMap.size > 0) setAutoColorMap(/* @__PURE__ */ new Map());
276
+ return;
277
+ }
278
+ const hasNewItems = autoItems.some((item) => !previousDataKeys.has(item.name));
279
+ if (autoColorMap.size === 0 || hasNewItems || colorOptionsChanged) setAutoColorMap(generateColorMap(autoItems));
280
+ else {
281
+ const autoKeys = new Set(autoItems.map((item) => item.name));
282
+ const updatedMap = new Map(autoColorMap);
283
+ let hasChanges = false;
284
+ for (const key of Array.from(updatedMap.keys())) if (!autoKeys.has(key)) {
285
+ updatedMap.delete(key);
286
+ hasChanges = true;
287
+ }
288
+ if (hasChanges) setAutoColorMap(updatedMap);
289
+ }
290
+ }, [
291
+ data,
292
+ colorGeneratorOptions,
293
+ isDarkMode,
294
+ theme,
295
+ generateColorMap
296
+ ]);
297
+ const displayBadge = badge !== null && badge !== void 0 ? badge : calculateBadgeText(data.length, selectedIndices.length);
298
+ const { effectiveMaxAmount, hasRemaining, remainingValue, remainingPercentage } = useMemo(() => {
299
+ const total = data.reduce((sum, item) => sum + item.value, 0);
300
+ const maxAmountValue = mode === "cumulative" && maxAmount ? maxAmount : total;
301
+ const hasRemainingSegment = mode === "cumulative" && total < maxAmountValue;
302
+ const remaining = hasRemainingSegment ? maxAmountValue - total : 0;
303
+ return {
304
+ effectiveMaxAmount: maxAmountValue,
305
+ hasRemaining: hasRemainingSegment,
306
+ remainingValue: remaining,
307
+ remainingPercentage: hasRemainingSegment ? remaining / maxAmountValue * 100 : 0
308
+ };
309
+ }, [
310
+ data,
311
+ mode,
312
+ maxAmount
313
+ ]);
314
+ const dataWithPercentages = useMemo(() => {
315
+ return data.map((item, index) => {
316
+ var _theme$palette3;
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
+ return _objectSpread2(_objectSpread2({}, item), {}, {
319
+ color: resolvedColor,
320
+ percentage: effectiveMaxAmount > 0 ? item.value / effectiveMaxAmount * 100 : 0,
321
+ key: item.name,
322
+ originalIndex: index,
323
+ isEntering: false,
324
+ isExiting: false
325
+ });
326
+ });
327
+ }, [
328
+ data,
329
+ effectiveMaxAmount,
330
+ autoColorMap,
331
+ theme
332
+ ]);
333
+ useEffect(() => {
334
+ const prevItems = previousProcessedRef.current;
335
+ const nextItems = dataWithPercentages;
336
+ const nextItemsMap = new Map(nextItems.map((item) => [item.key, item]));
337
+ if (prevItems.length === 0) {
338
+ setAnimatedItems(nextItems);
339
+ setAnimatedRemainingPercentage(remainingPercentage);
340
+ previousProcessedRef.current = nextItems;
341
+ return;
342
+ }
343
+ const step1Items = [...prevItems];
344
+ let currentInsertionIndex = 0;
345
+ for (const item of nextItems) {
346
+ const step1Index = step1Items.findIndex((step1Item) => step1Item.key === item.key);
347
+ if (step1Index === -1) {
348
+ step1Items.splice(currentInsertionIndex, 0, _objectSpread2(_objectSpread2({}, item), {}, { isEntering: true }));
349
+ currentInsertionIndex++;
350
+ } else currentInsertionIndex = step1Index + 1;
351
+ }
352
+ setAnimatedItems(step1Items);
353
+ const timeoutIds = [];
354
+ timeoutIds.push(window.setTimeout(() => {
355
+ setAnimatedItems(step1Items.map((item) => {
356
+ var _nextItemsMap$get;
357
+ return (_nextItemsMap$get = nextItemsMap.get(item.key)) !== null && _nextItemsMap$get !== void 0 ? _nextItemsMap$get : _objectSpread2(_objectSpread2({}, item), {}, { isExiting: true });
358
+ }));
359
+ setAnimatedRemainingPercentage(remainingPercentage);
360
+ timeoutIds.push(window.setTimeout(() => {
361
+ setAnimatedItems(nextItems);
362
+ previousProcessedRef.current = nextItems;
363
+ }, 350));
364
+ }, 0));
365
+ return () => {
366
+ timeoutIds.forEach((id) => window.clearTimeout(id));
367
+ };
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), [
371
+ animatedItems,
372
+ selectedIndicesSet,
373
+ selectionBehavior,
374
+ mode,
375
+ hasRemaining,
376
+ effectiveMaxAmount,
377
+ animatedRemainingPercentage,
378
+ remainingValue
379
+ ]);
380
+ const isHideMode = selectionBehavior === "hide" && selectedIndices.length > 0;
381
+ const displayRemainingPercentage = isHideMode ? updatedRemainingPercentage : animatedRemainingPercentage;
382
+ const displayRemainingValue = isHideMode ? updatedRemainingValue : remainingValue;
383
+ const legendItems = useMemo(() => buildLegendItems(dataWithPercentages, {
384
+ showLegendValues,
385
+ legendValueFormat,
386
+ unit,
387
+ hasRemaining,
388
+ showLegend,
389
+ remainingValue: displayRemainingValue,
390
+ remainingPercentage: displayRemainingPercentage,
391
+ remainingLabel,
392
+ remainingUnit,
393
+ theme
394
+ }), [
395
+ dataWithPercentages,
396
+ showLegendValues,
397
+ legendValueFormat,
398
+ unit,
399
+ hasRemaining,
400
+ showLegend,
401
+ displayRemainingValue,
402
+ displayRemainingPercentage,
403
+ remainingLabel,
404
+ remainingUnit,
405
+ theme
406
+ ]);
407
+ const handleLegendItemHover = useCallback((_item, index) => {
408
+ setHoveredIndex(index);
409
+ if (onLegendItemMouseEnter && data[index]) onLegendItemMouseEnter(data[index], index);
410
+ }, [onLegendItemMouseEnter, data]);
411
+ const handleLegendItemLeave = useCallback((_item, index) => {
412
+ setHoveredIndex(null);
413
+ if (onLegendItemMouseLeave && data[index]) onLegendItemMouseLeave(data[index], index);
414
+ }, [onLegendItemMouseLeave, data]);
415
+ const handleLegendItemClickInternal = useCallback((_item, index) => {
416
+ if (onLegendItemClick && data[index]) onLegendItemClick(data[index], index);
417
+ }, [onLegendItemClick, data]);
418
+ const handleSelectionChange = useCallback((newSelectedIndices) => {
419
+ if (!onSelectionChange) return;
420
+ onSelectionChange(newSelectedIndices, data.filter((item, itemIndex) => newSelectedIndices.includes(itemIndex)));
421
+ }, [onSelectionChange, data]);
422
+ const handleSegmentClick = useCallback((index) => {
423
+ if (onSegmentClick && data[index]) onSegmentClick(data[index], index);
424
+ if (!onSelectionChange) return;
425
+ handleSelectionChange(selectedIndicesSet.has(index) ? selectedIndices.filter((i) => i !== index) : [...selectedIndices, index]);
426
+ }, [
427
+ onSelectionChange,
428
+ onSegmentClick,
429
+ data,
430
+ selectedIndices,
431
+ selectedIndicesSet,
432
+ handleSelectionChange
433
+ ]);
434
+ const lastVisibleIndex = useMemo(() => {
435
+ for (let i = styledItems.length - 1; i >= 0; i--) if (styledItems[i].percentage > 0) return i;
436
+ return -1;
437
+ }, [styledItems]);
438
+ const chartContent = /* @__PURE__ */ jsxs(StyledStackedBarChartWrapper, { children: [/* @__PURE__ */ jsxs(BarContainer, {
439
+ width,
440
+ barHeight,
441
+ isEmpty: styledItems.length === 0 && !hasRemaining,
442
+ children: [styledItems.map((item, index) => {
443
+ return renderBarSegment(item, {
444
+ barHeight,
445
+ hoveredIndex,
446
+ selectedIndicesSet,
447
+ selectionBehavior,
448
+ isLast: index === lastVisibleIndex && !hasRemaining,
449
+ onMouseEnter: () => {
450
+ if (!item.isExiting) {
451
+ setHoveredIndex(item.originalIndex);
452
+ if (onSegmentMouseEnter && data[item.originalIndex]) onSegmentMouseEnter(data[item.originalIndex], item.originalIndex);
453
+ }
454
+ },
455
+ onMouseLeave: () => {
456
+ setHoveredIndex(null);
457
+ if (onSegmentMouseLeave && data[item.originalIndex]) onSegmentMouseLeave(data[item.originalIndex], item.originalIndex);
458
+ },
459
+ onClick: () => !item.isExiting && handleSegmentClick(item.originalIndex)
460
+ });
461
+ }), 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,
463
+ percentage: displayRemainingPercentage,
464
+ height: barHeight,
465
+ isLast: true,
466
+ opacity: 1,
467
+ disabled: true
468
+ }, "remaining")]
469
+ }), showLegend && /* @__PURE__ */ jsx(Legend, {
470
+ items: legendItems,
471
+ showValues: showLegendValues,
472
+ onItemMouseEnter: handleLegendItemHover,
473
+ onItemMouseLeave: handleLegendItemLeave,
474
+ onItemClick: onLegendItemClick ? handleLegendItemClickInternal : void 0,
475
+ selectedIndices,
476
+ onSelectionChange: onSelectionChange ? handleSelectionChange : void 0,
477
+ hoveredIndex
478
+ })] });
479
+ 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] }));
481
+ };
482
+ //#endregion
483
+ export { StackedBarChart as t };
@@ -0,0 +1,45 @@
1
+ //#region src/common/colorScales.d.ts
2
+ /** RGB triplet with each channel in the range 0-255. */
3
+ type Rgb = [number, number, number];
4
+ /** A stop on a continuous color scale, positioned at `t` in the range 0-1. */
5
+ interface ContinuousColorStop {
6
+ t: number;
7
+ color: string;
8
+ }
9
+ /**
10
+ * A band on a stepped color scale. `threshold` is the upper bound of the band
11
+ * on the normalized 0-1 value range; `label` is rendered above the band in the
12
+ * legend.
13
+ */
14
+ interface SteppedColorStop {
15
+ threshold: number;
16
+ color: string;
17
+ label: string;
18
+ }
19
+ /**
20
+ * How values map to colors, both in a visualization and in its legend. A
21
+ * continuous scale interpolates between its stops and renders as a gradient
22
+ * bar; a stepped scale renders as discrete bands.
23
+ */
24
+ type ColorScale = {
25
+ kind: "continuous";
26
+ stops: ContinuousColorStop[];
27
+ } | {
28
+ kind: "stepped";
29
+ stops: SteppedColorStop[];
30
+ };
31
+ /**
32
+ * Perceptually uniform sequential scale (matplotlib's plasma), the default for
33
+ * value overlays.
34
+ */
35
+ declare const PLASMA_COLOR_SCALE: ColorScale;
36
+ /**
37
+ * Colors a raw value against a scale, normalizing it into `min`-`max` first.
38
+ * Returns null when the value sits at or below `min`, or when the range is
39
+ * degenerate, so callers can fall back to a neutral color.
40
+ */
41
+ declare function sampleColorScale(scale: ColorScale, value: number, max: number, min?: number): Rgb | null;
42
+ /** CSS `linear-gradient` spanning a continuous scale, for the legend bar. */
43
+ declare function toCssGradient(stops: ContinuousColorStop[]): string;
44
+ //#endregion
45
+ export { sampleColorScale as a, SteppedColorStop as i, ContinuousColorStop as n, toCssGradient as o, PLASMA_COLOR_SCALE as r, ColorScale as t };
@@ -0,0 +1,126 @@
1
+ "use client";
2
+ //#region src/common/colorScales.ts
3
+ /**
4
+ * Perceptually uniform sequential scale (matplotlib's plasma), the default for
5
+ * value overlays.
6
+ */
7
+ const PLASMA_COLOR_SCALE = {
8
+ kind: "continuous",
9
+ stops: [
10
+ {
11
+ color: "#0D0887",
12
+ t: 0
13
+ },
14
+ {
15
+ color: "#7E03A8",
16
+ t: .25
17
+ },
18
+ {
19
+ color: "#CC4778",
20
+ t: .5
21
+ },
22
+ {
23
+ color: "#F89441",
24
+ t: .75
25
+ },
26
+ {
27
+ color: "#F0F921",
28
+ t: 1
29
+ }
30
+ ]
31
+ };
32
+ /** Parses `#RRGGBB` (or `RRGGBB`) into an RGB triplet. */
33
+ function hexToRgb(hex) {
34
+ const h = hex.replace("#", "");
35
+ return [
36
+ parseInt(h.substring(0, 2), 16),
37
+ parseInt(h.substring(2, 4), 16),
38
+ parseInt(h.substring(4, 6), 16)
39
+ ];
40
+ }
41
+ /** Linearly interpolates a continuous scale at `t`, clamped to 0-1. */
42
+ function sampleContinuousScale(stops, t) {
43
+ if (stops.length === 0) return [
44
+ 0,
45
+ 0,
46
+ 0
47
+ ];
48
+ const first = stops[0];
49
+ const last = stops[stops.length - 1];
50
+ const clamped = Math.min(1, Math.max(0, t));
51
+ let lo = first;
52
+ let hi = last;
53
+ for (let i = 0; i < stops.length - 1; i++) {
54
+ const current = stops[i];
55
+ const next = stops[i + 1];
56
+ if (clamped >= current.t && clamped <= next.t) {
57
+ lo = current;
58
+ hi = next;
59
+ break;
60
+ }
61
+ }
62
+ const loRgb = hexToRgb(lo.color);
63
+ const hiRgb = hexToRgb(hi.color);
64
+ const f = hi.t === lo.t ? 0 : (clamped - lo.t) / (hi.t - lo.t);
65
+ return [
66
+ Math.round(loRgb[0] + f * (hiRgb[0] - loRgb[0])),
67
+ Math.round(loRgb[1] + f * (hiRgb[1] - loRgb[1])),
68
+ Math.round(loRgb[2] + f * (hiRgb[2] - loRgb[2]))
69
+ ];
70
+ }
71
+ /**
72
+ * Picks the band a normalized value falls into on a stepped scale.
73
+ *
74
+ * A value lands in the first band whose threshold it falls short of, which puts
75
+ * each threshold at the bottom of the band above it: against thresholds of 0.5,
76
+ * 0.7 and 0.9, a value of exactly 0.7 reads as the 0.7-0.9 band, not the one
77
+ * below.
78
+ */
79
+ function sampleSteppedScale(stops, t) {
80
+ for (const stop of stops) if (t < stop.threshold) return hexToRgb(stop.color);
81
+ const last = stops[stops.length - 1];
82
+ return last ? hexToRgb(last.color) : [
83
+ 0,
84
+ 0,
85
+ 0
86
+ ];
87
+ }
88
+ /**
89
+ * Colors a raw value against a scale, normalizing it into `min`-`max` first.
90
+ * Returns null when the value sits at or below `min`, or when the range is
91
+ * degenerate, so callers can fall back to a neutral color.
92
+ */
93
+ function sampleColorScale(scale, value, max, min = 0) {
94
+ if (max <= min || value <= min) return null;
95
+ const t = Math.min(1, (value - min) / (max - min));
96
+ return scale.kind === "continuous" ? sampleContinuousScale(scale.stops, t) : sampleSteppedScale(scale.stops, t);
97
+ }
98
+ /** CSS `linear-gradient` spanning a continuous scale, for the legend bar. */
99
+ function toCssGradient(stops) {
100
+ return `linear-gradient(to right, ${stops.map((stop) => `${stop.color} ${stop.t * 100}%`).join(", ")})`;
101
+ }
102
+ //#endregion
103
+ Object.defineProperty(exports, "PLASMA_COLOR_SCALE", {
104
+ enumerable: true,
105
+ get: function() {
106
+ return PLASMA_COLOR_SCALE;
107
+ }
108
+ });
109
+ Object.defineProperty(exports, "sampleColorScale", {
110
+ enumerable: true,
111
+ get: function() {
112
+ return sampleColorScale;
113
+ }
114
+ });
115
+ Object.defineProperty(exports, "sampleSteppedScale", {
116
+ enumerable: true,
117
+ get: function() {
118
+ return sampleSteppedScale;
119
+ }
120
+ });
121
+ Object.defineProperty(exports, "toCssGradient", {
122
+ enumerable: true,
123
+ get: function() {
124
+ return toCssGradient;
125
+ }
126
+ });