@czi-sds/data-viz 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/HeatmapChart.cjs.d.ts +2 -0
- package/dist/HeatmapChart.cjs.js +8 -0
- package/dist/HeatmapChart.esm.d.ts +2 -0
- package/dist/HeatmapChart.esm.js +3 -0
- package/dist/ProteinStructureViewer.cjs.d.ts +2 -0
- package/dist/ProteinStructureViewer.cjs.js +14 -0
- package/dist/ProteinStructureViewer.esm.d.ts +2 -0
- package/dist/ProteinStructureViewer.esm.js +3 -0
- package/dist/StackedBarChart.cjs.d.ts +2 -0
- package/dist/StackedBarChart.cjs.js +8 -0
- package/dist/StackedBarChart.esm.d.ts +2 -0
- package/dist/StackedBarChart.esm.js +3 -0
- package/dist/colorScales.cjs.d.ts +2 -0
- package/dist/colorScales.cjs.js +6 -0
- package/dist/colorScales.esm.d.ts +2 -0
- package/dist/colorScales.esm.js +3 -0
- package/dist/index.cjs.d.ts +4 -772
- package/dist/index.cjs.js +16 -5765
- package/dist/index.esm.d.ts +4 -774
- package/dist/index.esm.js +4 -5719
- package/dist/shared/HeatmapChart.cjs.js +349 -0
- package/dist/shared/HeatmapChart.esm.js +342 -0
- package/dist/shared/ProteinStructureViewer.cjs.js +4780 -0
- package/dist/shared/ProteinStructureViewer.esm.js +4728 -0
- package/dist/shared/StackedBarChart.cjs.js +489 -0
- package/dist/shared/StackedBarChart.esm.js +483 -0
- package/dist/shared/colorScales.cjs.d.ts +45 -0
- package/dist/shared/colorScales.cjs.js +126 -0
- package/dist/shared/colorScales.esm.d.ts +45 -0
- package/dist/shared/colorScales.esm.js +103 -0
- package/dist/shared/index.cjs.d.ts +183 -0
- package/dist/shared/index.esm.d.ts +183 -0
- package/dist/shared/index2.cjs.d.ts +389 -0
- package/dist/shared/index2.esm.d.ts +391 -0
- package/dist/shared/index3.cjs.d.ts +166 -0
- package/dist/shared/index3.esm.d.ts +166 -0
- package/dist/shared/objectSpread2.cjs.js +124 -0
- package/dist/shared/objectSpread2.esm.js +85 -0
- package/package.json +64 -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
|
+
});
|