@mastra/playground-ui 42.0.0-alpha.14 → 42.0.0-alpha.15
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/CHANGELOG.md +35 -0
- package/dist/components/SankeyChart.cjs.js +346 -46
- package/dist/components/SankeyChart.cjs.js.map +1 -1
- package/dist/components/SankeyChart.es.js +348 -49
- package/dist/components/SankeyChart.es.js.map +1 -1
- package/dist/src/ds/components/SankeyChart/index.d.ts +2 -2
- package/dist/src/ds/components/SankeyChart/sankey-chart-utils.d.ts +32 -1
- package/dist/src/ds/components/SankeyChart/sankey-chart.d.ts +4 -2
- package/dist/src/ds/components/SankeyChart/sankey-context.d.ts +6 -1
- package/dist/src/ds/components/SankeyChart/use-sankey-chart-measurements.d.ts +14 -0
- package/dist/style.css +8 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @mastra/playground-ui
|
|
2
2
|
|
|
3
|
+
## 42.0.0-alpha.15
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added separate Sankey node identity, display label, display value, and layout weight accessors, plus constrained labels with full hover text. Stable layout weights can now keep node positions fixed while current record weights animate bar and ribbon sizes. ([#19871](https://github.com/mastra-ai/mastra/pull/19871))
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<Sankey
|
|
11
|
+
data={records}
|
|
12
|
+
columns={columns}
|
|
13
|
+
getRecordNodeId={(record, column) => String(record[`${column.id}Id`])}
|
|
14
|
+
getRecordNodeLabel={(record, column) => String(record[`${column.id}Label`])}
|
|
15
|
+
getRecordNodeValue={(record, column) => Number(record[`${column.id}Count`])}
|
|
16
|
+
getRecordWeight={record => Number(record.count)}
|
|
17
|
+
getRecordLayoutWeight={record => Number(record.windowMaxCount)}
|
|
18
|
+
>
|
|
19
|
+
<SankeyChart />
|
|
20
|
+
</Sankey>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Added optional Sankey node activation with mouse and keyboard support, including per-node eligibility. ([#19896](https://github.com/mastra-ai/mastra/pull/19896))
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<SankeyChart
|
|
27
|
+
onNodeClick={({ column, value }) => openDrilldown(column.id, value)}
|
|
28
|
+
isNodeClickable={({ value }) => drillableNodeIds.has(value)}
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [[`f901b73`](https://github.com/mastra-ai/mastra/commit/f901b7363b8d21849b98c167bae168fa11d3edcf)]:
|
|
35
|
+
- @mastra/client-js@1.33.0-alpha.13
|
|
36
|
+
- @mastra/react@1.3.0-alpha.13
|
|
37
|
+
|
|
3
38
|
## 42.0.0-alpha.14
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
5
5
|
const jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
const React = require('react');
|
|
7
7
|
const sankeyColor = require('../sankeyColor-DONd5cg9.cjs');
|
|
8
|
+
const reactDom = require('react-dom');
|
|
8
9
|
const recharts = require('recharts');
|
|
9
10
|
const spacings = require('../spacings-BP8L49kn.cjs');
|
|
10
11
|
const utils_cn = require('../cn-vA5sWo9W.cjs');
|
|
@@ -28,17 +29,17 @@ function reorderSankeyChartColumns(columns, startIndex, endIndex) {
|
|
|
28
29
|
reorderedColumns.splice(endIndex, 0, movedColumn);
|
|
29
30
|
return reorderedColumns;
|
|
30
31
|
}
|
|
31
|
-
function buildSankeyChartGraph(data, columns, getRecordWeight = () => 1) {
|
|
32
|
+
function buildSankeyChartGraph(data, columns, getRecordWeight = () => 1, getRecordNodeId, getRecordNodeLabel, getRecordNodeValue, getRecordLayoutWeight) {
|
|
32
33
|
if (columns.length < 2 || data.length === 0) return EMPTY_GRAPH;
|
|
33
34
|
const nodes = [];
|
|
34
35
|
const nodeIndexes = /* @__PURE__ */ new Map();
|
|
35
36
|
const linksById = /* @__PURE__ */ new Map();
|
|
36
|
-
const getNode = (column, value) => {
|
|
37
|
+
const getNode = (column, value, label, displayValue) => {
|
|
37
38
|
const id = createNodeId(column.id, value);
|
|
38
39
|
const existingIndex = nodeIndexes.get(id);
|
|
39
40
|
const existingNode = existingIndex === void 0 ? void 0 : nodes[existingIndex];
|
|
40
41
|
if (existingIndex !== void 0 && existingNode) return { node: existingNode, index: existingIndex };
|
|
41
|
-
const node = { id, name: String(value), column, value };
|
|
42
|
+
const node = { id, name: String(value), label, column, value, displayValue };
|
|
42
43
|
const index = nodes.length;
|
|
43
44
|
nodes.push(node);
|
|
44
45
|
nodeIndexes.set(id, index);
|
|
@@ -49,24 +50,46 @@ function buildSankeyChartGraph(data, columns, getRecordWeight = () => 1) {
|
|
|
49
50
|
const targetColumn = columns[columnIndex + 1];
|
|
50
51
|
if (!sourceColumn || !targetColumn) continue;
|
|
51
52
|
for (const record of data) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const displayWeight = getRecordWeight(record);
|
|
54
|
+
const layoutWeight = getRecordLayoutWeight?.(record) ?? displayWeight;
|
|
55
|
+
if (!Number.isFinite(displayWeight) || displayWeight < 0 || !Number.isFinite(layoutWeight) || layoutWeight < 0) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const sourceRecordValue = getSankeyChartValue(record[sourceColumn.id]);
|
|
59
|
+
const targetRecordValue = getSankeyChartValue(record[targetColumn.id]);
|
|
60
|
+
if (sourceRecordValue === void 0 || targetRecordValue === void 0) continue;
|
|
61
|
+
const sourceValue = getRecordNodeId ? getSankeyChartValue(getRecordNodeId(record, sourceColumn)) : sourceRecordValue;
|
|
62
|
+
const targetValue = getRecordNodeId ? getSankeyChartValue(getRecordNodeId(record, targetColumn)) : targetRecordValue;
|
|
56
63
|
if (sourceValue === void 0 || targetValue === void 0) continue;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
64
|
+
const sourceLabel = getRecordNodeLabel?.(record, sourceColumn) ?? String(sourceRecordValue);
|
|
65
|
+
const targetLabel = getRecordNodeLabel?.(record, targetColumn) ?? String(targetRecordValue);
|
|
66
|
+
const sourceDisplayValue = getRecordNodeValue?.(record, sourceColumn);
|
|
67
|
+
const targetDisplayValue = getRecordNodeValue?.(record, targetColumn);
|
|
68
|
+
const source = getNode(
|
|
69
|
+
sourceColumn,
|
|
70
|
+
sourceValue,
|
|
71
|
+
sourceLabel,
|
|
72
|
+
sourceDisplayValue !== void 0 && Number.isFinite(sourceDisplayValue) && sourceDisplayValue >= 0 ? sourceDisplayValue : void 0
|
|
73
|
+
);
|
|
74
|
+
const target = getNode(
|
|
75
|
+
targetColumn,
|
|
76
|
+
targetValue,
|
|
77
|
+
targetLabel,
|
|
78
|
+
targetDisplayValue !== void 0 && Number.isFinite(targetDisplayValue) && targetDisplayValue >= 0 ? targetDisplayValue : void 0
|
|
79
|
+
);
|
|
59
80
|
const id = `${source.node.id}->${target.node.id}`;
|
|
60
81
|
const existingLink = linksById.get(id);
|
|
61
82
|
if (existingLink) {
|
|
62
|
-
existingLink.value +=
|
|
83
|
+
existingLink.value += layoutWeight;
|
|
84
|
+
existingLink.displayValue += displayWeight;
|
|
63
85
|
existingLink.records.push(record);
|
|
64
86
|
} else {
|
|
65
87
|
linksById.set(id, {
|
|
66
88
|
id,
|
|
67
89
|
source: source.index,
|
|
68
90
|
target: target.index,
|
|
69
|
-
value:
|
|
91
|
+
value: layoutWeight,
|
|
92
|
+
displayValue: displayWeight,
|
|
70
93
|
sourceNode: source.node,
|
|
71
94
|
targetNode: target.node,
|
|
72
95
|
records: [record]
|
|
@@ -76,6 +99,70 @@ function buildSankeyChartGraph(data, columns, getRecordWeight = () => 1) {
|
|
|
76
99
|
}
|
|
77
100
|
return { nodes, links: [...linksById.values()] };
|
|
78
101
|
}
|
|
102
|
+
function buildFixedSankeyGeometry(graph, {
|
|
103
|
+
top,
|
|
104
|
+
bottom,
|
|
105
|
+
left,
|
|
106
|
+
right,
|
|
107
|
+
nodePadding
|
|
108
|
+
}) {
|
|
109
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
110
|
+
const nodesByColumn = /* @__PURE__ */ new Map();
|
|
111
|
+
const currentNodeWeights = getSankeyChartCurrentNodeWeights(graph);
|
|
112
|
+
for (const node of graph.nodes) {
|
|
113
|
+
const columnNodes = nodesByColumn.get(node.column.id) ?? [];
|
|
114
|
+
columnNodes.push(node);
|
|
115
|
+
nodesByColumn.set(node.column.id, columnNodes);
|
|
116
|
+
}
|
|
117
|
+
const columns = [...nodesByColumn.values()];
|
|
118
|
+
const slotHeights = columns.map(
|
|
119
|
+
(columnNodes) => Math.max(0, (bottom - top - nodePadding * Math.max(0, columnNodes.length - 1)) / columnNodes.length)
|
|
120
|
+
);
|
|
121
|
+
const maximumNodeHeight = Math.min(...slotHeights) * 0.6;
|
|
122
|
+
columns.forEach((columnNodes, columnIndex) => {
|
|
123
|
+
const slotHeight = slotHeights[columnIndex] ?? 0;
|
|
124
|
+
const columnTotal = columnNodes.reduce(
|
|
125
|
+
(total, node) => total + (node.displayValue ?? currentNodeWeights.get(node.id) ?? 0),
|
|
126
|
+
0
|
|
127
|
+
);
|
|
128
|
+
const x = columns.length > 1 ? left + (right - left) * columnIndex / (columns.length - 1) : left;
|
|
129
|
+
columnNodes.forEach((node, index) => {
|
|
130
|
+
const value = node.displayValue ?? currentNodeWeights.get(node.id) ?? 0;
|
|
131
|
+
const centerY = top + index * (slotHeight + nodePadding) + slotHeight / 2;
|
|
132
|
+
const height = columnTotal > 0 ? maximumNodeHeight * (value / columnTotal) : 0;
|
|
133
|
+
nodes.set(node.id, { x, centerY, y: centerY - height / 2, height });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
const links = /* @__PURE__ */ new Map();
|
|
137
|
+
const outgoingTotals = /* @__PURE__ */ new Map();
|
|
138
|
+
const incomingTotals = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const link of graph.links) {
|
|
140
|
+
outgoingTotals.set(link.sourceNode.id, (outgoingTotals.get(link.sourceNode.id) ?? 0) + link.displayValue);
|
|
141
|
+
incomingTotals.set(link.targetNode.id, (incomingTotals.get(link.targetNode.id) ?? 0) + link.displayValue);
|
|
142
|
+
}
|
|
143
|
+
const sourceOffsets = new Map([...nodes].map(([id, geometry]) => [id, geometry.y]));
|
|
144
|
+
const targetOffsets = new Map([...nodes].map(([id, geometry]) => [id, geometry.y]));
|
|
145
|
+
for (const link of graph.links) {
|
|
146
|
+
const sourceGeometry = nodes.get(link.sourceNode.id);
|
|
147
|
+
const targetGeometry = nodes.get(link.targetNode.id);
|
|
148
|
+
if (!sourceGeometry || !targetGeometry) continue;
|
|
149
|
+
const sourceTotal = outgoingTotals.get(link.sourceNode.id) ?? 0;
|
|
150
|
+
const targetTotal = incomingTotals.get(link.targetNode.id) ?? 0;
|
|
151
|
+
const sourceWidth = sourceTotal > 0 ? sourceGeometry.height * (link.displayValue / sourceTotal) : 0;
|
|
152
|
+
const targetWidth = targetTotal > 0 ? targetGeometry.height * (link.displayValue / targetTotal) : 0;
|
|
153
|
+
const sourceOffset = sourceOffsets.get(link.sourceNode.id) ?? sourceGeometry.y;
|
|
154
|
+
const targetOffset = targetOffsets.get(link.targetNode.id) ?? targetGeometry.y;
|
|
155
|
+
links.set(link.id, {
|
|
156
|
+
sourceY: sourceOffset + sourceWidth / 2,
|
|
157
|
+
targetY: targetOffset + targetWidth / 2,
|
|
158
|
+
sourceWidth,
|
|
159
|
+
targetWidth
|
|
160
|
+
});
|
|
161
|
+
sourceOffsets.set(link.sourceNode.id, sourceOffset + sourceWidth);
|
|
162
|
+
targetOffsets.set(link.targetNode.id, targetOffset + targetWidth);
|
|
163
|
+
}
|
|
164
|
+
return { nodes, links };
|
|
165
|
+
}
|
|
79
166
|
function getSankeyChartNodeWeights(graph) {
|
|
80
167
|
const incomingWeights = /* @__PURE__ */ new Map();
|
|
81
168
|
const outgoingWeights = /* @__PURE__ */ new Map();
|
|
@@ -87,6 +174,17 @@ function getSankeyChartNodeWeights(graph) {
|
|
|
87
174
|
graph.nodes.map((node) => [node.id, Math.max(incomingWeights.get(node.id) ?? 0, outgoingWeights.get(node.id) ?? 0)])
|
|
88
175
|
);
|
|
89
176
|
}
|
|
177
|
+
function getSankeyChartCurrentNodeWeights(graph) {
|
|
178
|
+
const incomingWeights = /* @__PURE__ */ new Map();
|
|
179
|
+
const outgoingWeights = /* @__PURE__ */ new Map();
|
|
180
|
+
for (const link of graph.links) {
|
|
181
|
+
outgoingWeights.set(link.sourceNode.id, (outgoingWeights.get(link.sourceNode.id) ?? 0) + link.displayValue);
|
|
182
|
+
incomingWeights.set(link.targetNode.id, (incomingWeights.get(link.targetNode.id) ?? 0) + link.displayValue);
|
|
183
|
+
}
|
|
184
|
+
return new Map(
|
|
185
|
+
graph.nodes.map((node) => [node.id, Math.max(incomingWeights.get(node.id) ?? 0, outgoingWeights.get(node.id) ?? 0)])
|
|
186
|
+
);
|
|
187
|
+
}
|
|
90
188
|
function getSankeyChartCurveSelection(link) {
|
|
91
189
|
return {
|
|
92
190
|
source: { column: link.sourceNode.column, value: link.sourceNode.value },
|
|
@@ -94,6 +192,9 @@ function getSankeyChartCurveSelection(link) {
|
|
|
94
192
|
records: link.records
|
|
95
193
|
};
|
|
96
194
|
}
|
|
195
|
+
function getSankeyChartNodeSelection(node) {
|
|
196
|
+
return { column: node.column, value: node.value };
|
|
197
|
+
}
|
|
97
198
|
function createNodeId(columnId, value) {
|
|
98
199
|
return JSON.stringify([columnId, typeof value, value]);
|
|
99
200
|
}
|
|
@@ -109,6 +210,10 @@ function Sankey({
|
|
|
109
210
|
visibleColumnIds,
|
|
110
211
|
onVisibleColumnIdsChange,
|
|
111
212
|
getRecordWeight,
|
|
213
|
+
getRecordLayoutWeight,
|
|
214
|
+
getRecordNodeId,
|
|
215
|
+
getRecordNodeLabel,
|
|
216
|
+
getRecordNodeValue,
|
|
112
217
|
getColumnHue
|
|
113
218
|
}) {
|
|
114
219
|
const columnIds = columns.map((column) => column.id);
|
|
@@ -117,7 +222,15 @@ function Sankey({
|
|
|
117
222
|
const orderedColumns = orderColumns(columns, columnOrder ?? internalOrder);
|
|
118
223
|
const visibleIds = new Set(visibleColumnIds ?? internalVisibleIds);
|
|
119
224
|
const enabledColumns = orderedColumns.filter((column) => visibleIds.has(column.id));
|
|
120
|
-
const graph = buildSankeyChartGraph(
|
|
225
|
+
const graph = buildSankeyChartGraph(
|
|
226
|
+
data,
|
|
227
|
+
enabledColumns,
|
|
228
|
+
getRecordWeight,
|
|
229
|
+
getRecordNodeId,
|
|
230
|
+
getRecordNodeLabel,
|
|
231
|
+
getRecordNodeValue,
|
|
232
|
+
getRecordLayoutWeight
|
|
233
|
+
);
|
|
121
234
|
const defaultHueMap = sankeyColor.buildSankeyHueMap(graph.nodes.map((node) => String(node.value)));
|
|
122
235
|
const hueMap = Object.fromEntries(
|
|
123
236
|
graph.nodes.map((node) => [
|
|
@@ -146,7 +259,13 @@ function Sankey({
|
|
|
146
259
|
setColumnOrder(nextColumns.map((column) => column.id));
|
|
147
260
|
};
|
|
148
261
|
const controlColumns = orderedColumns.map((column) => ({ ...column, visible: visibleIds.has(column.id) }));
|
|
149
|
-
return /* @__PURE__ */ jsxRuntime.jsx(SankeyControlsContext.Provider, { value: { columns: controlColumns, toggleColumn, reorderColumns }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
262
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SankeyControlsContext.Provider, { value: { columns: controlColumns, toggleColumn, reorderColumns }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
263
|
+
SankeyRenderContext.Provider,
|
|
264
|
+
{
|
|
265
|
+
value: { graph, enabledColumns, hueMap, usesFixedGeometry: getRecordLayoutWeight !== void 0 },
|
|
266
|
+
children
|
|
267
|
+
}
|
|
268
|
+
) });
|
|
150
269
|
}
|
|
151
270
|
function useSankey() {
|
|
152
271
|
const context = React.useContext(SankeyControlsContext);
|
|
@@ -165,17 +284,64 @@ function orderColumns(columns, order) {
|
|
|
165
284
|
);
|
|
166
285
|
}
|
|
167
286
|
|
|
287
|
+
function useSankeyChartMeasurements({
|
|
288
|
+
graph,
|
|
289
|
+
height,
|
|
290
|
+
margin,
|
|
291
|
+
usesFixedGeometry
|
|
292
|
+
}) {
|
|
293
|
+
const chartContainerRef = React.useRef(null);
|
|
294
|
+
const [measuredHeight, setMeasuredHeight] = React.useState(typeof height === "number" ? height : 320);
|
|
295
|
+
const [measuredWidth, setMeasuredWidth] = React.useState(800);
|
|
296
|
+
React.useEffect(() => {
|
|
297
|
+
const element = chartContainerRef.current;
|
|
298
|
+
if (!element) return;
|
|
299
|
+
const updateDimensions = () => {
|
|
300
|
+
if (element.offsetHeight > 0) setMeasuredHeight(element.offsetHeight);
|
|
301
|
+
if (element.offsetWidth > 0) setMeasuredWidth(element.offsetWidth);
|
|
302
|
+
};
|
|
303
|
+
updateDimensions();
|
|
304
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
305
|
+
const observer = new ResizeObserver(updateDimensions);
|
|
306
|
+
observer.observe(element);
|
|
307
|
+
return () => observer.disconnect();
|
|
308
|
+
}, [height]);
|
|
309
|
+
const fixedGeometry = React.useMemo(
|
|
310
|
+
() => usesFixedGeometry ? buildFixedSankeyGeometry(graph, {
|
|
311
|
+
top: Number(margin?.top ?? 64),
|
|
312
|
+
bottom: measuredHeight - Number(margin?.bottom ?? 12),
|
|
313
|
+
left: Number(margin?.left ?? 160),
|
|
314
|
+
right: measuredWidth - Number(margin?.right ?? 160) - 7,
|
|
315
|
+
nodePadding: 8
|
|
316
|
+
}) : void 0,
|
|
317
|
+
[graph, margin?.bottom, margin?.left, margin?.right, margin?.top, measuredHeight, measuredWidth, usesFixedGeometry]
|
|
318
|
+
);
|
|
319
|
+
return { chartContainerRef, fixedGeometry };
|
|
320
|
+
}
|
|
321
|
+
|
|
168
322
|
function SankeyChart({
|
|
169
323
|
height = 320,
|
|
170
324
|
className,
|
|
171
325
|
margin = { top: 64, right: 160, bottom: 12, left: 160 },
|
|
172
|
-
onCurveClick
|
|
326
|
+
onCurveClick,
|
|
327
|
+
onNodeClick,
|
|
328
|
+
isNodeClickable
|
|
173
329
|
}) {
|
|
174
|
-
const { graph, enabledColumns, hueMap } = useSankeyRenderContext();
|
|
330
|
+
const { graph, enabledColumns, hueMap, usesFixedGeometry } = useSankeyRenderContext();
|
|
331
|
+
const { chartContainerRef, fixedGeometry } = useSankeyChartMeasurements({
|
|
332
|
+
graph,
|
|
333
|
+
height,
|
|
334
|
+
margin,
|
|
335
|
+
usesFixedGeometry
|
|
336
|
+
});
|
|
175
337
|
const [hoveredSourceName, setHoveredSourceName] = React.useState();
|
|
338
|
+
const [focusedSourceName, setFocusedSourceName] = React.useState();
|
|
339
|
+
const activeSourceName = hoveredSourceName ?? focusedSourceName;
|
|
176
340
|
const firstColumnId = enabledColumns[0]?.id;
|
|
177
|
-
const
|
|
178
|
-
|
|
341
|
+
const lastColumnId = enabledColumns.at(-1)?.id;
|
|
342
|
+
const nodeWeights = getSankeyChartNodeWeights(graph);
|
|
343
|
+
const total = graph.nodes.reduce(
|
|
344
|
+
(sum, node) => node.column.id === firstColumnId ? sum + (node.displayValue ?? nodeWeights.get(node.id) ?? 0) : sum,
|
|
179
345
|
0
|
|
180
346
|
);
|
|
181
347
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: utils_cn.cn("min-w-0", className), children: graph.links.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -185,7 +351,7 @@ function SankeyChart({
|
|
|
185
351
|
style: { height },
|
|
186
352
|
children: "Select at least two columns with data to display a flow"
|
|
187
353
|
}
|
|
188
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
354
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { ref: chartContainerRef, style: { height }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
189
355
|
recharts.ResponsiveContainer,
|
|
190
356
|
{
|
|
191
357
|
width: "100%",
|
|
@@ -201,26 +367,51 @@ function SankeyChart({
|
|
|
201
367
|
node: (props) => {
|
|
202
368
|
const node = graph.nodes[props.index];
|
|
203
369
|
const showColumnLabel = node ? graph.nodes.findIndex((candidate) => candidate.column.id === node.column.id) === props.index : false;
|
|
370
|
+
const nodeGeometry = node ? fixedGeometry?.nodes.get(node.id) : void 0;
|
|
371
|
+
const selection = node ? getSankeyChartNodeSelection(node) : void 0;
|
|
372
|
+
const clickable = Boolean(
|
|
373
|
+
onNodeClick && selection && (isNodeClickable === void 0 || isNodeClickable(selection))
|
|
374
|
+
);
|
|
204
375
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
205
376
|
SankeyNode,
|
|
206
377
|
{
|
|
207
378
|
...props,
|
|
379
|
+
x: nodeGeometry?.x ?? props.x,
|
|
380
|
+
y: nodeGeometry?.y ?? props.y,
|
|
381
|
+
height: nodeGeometry?.height ?? props.height,
|
|
208
382
|
hueMap,
|
|
209
383
|
columnLabel: node?.column.label,
|
|
384
|
+
label: node?.label,
|
|
385
|
+
nodeValue: node?.displayValue,
|
|
386
|
+
layoutValue: nodeGeometry ? void 0 : node ? nodeWeights.get(node.id) : void 0,
|
|
210
387
|
total,
|
|
211
388
|
showColumnLabel,
|
|
212
|
-
|
|
389
|
+
isFirstColumn: node?.column.id === firstColumnId,
|
|
390
|
+
isLastColumn: node?.column.id === lastColumnId,
|
|
391
|
+
onFocusChange: setFocusedSourceName,
|
|
392
|
+
onHoverChange: setHoveredSourceName,
|
|
393
|
+
clickable,
|
|
394
|
+
onSelect: () => {
|
|
395
|
+
if (selection && clickable) onNodeClick?.(selection);
|
|
396
|
+
}
|
|
213
397
|
}
|
|
214
398
|
);
|
|
215
399
|
},
|
|
216
400
|
link: (props) => {
|
|
217
401
|
const link = graph.links[props.index];
|
|
402
|
+
const linkGeometry = link ? fixedGeometry?.links.get(link.id) : void 0;
|
|
218
403
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
219
404
|
SankeyLink,
|
|
220
405
|
{
|
|
221
406
|
...props,
|
|
407
|
+
sourceY: linkGeometry?.sourceY ?? props.sourceY,
|
|
408
|
+
targetY: linkGeometry?.targetY ?? props.targetY,
|
|
409
|
+
sourceWidth: linkGeometry?.sourceWidth,
|
|
410
|
+
targetWidth: linkGeometry?.targetWidth,
|
|
222
411
|
hueMap,
|
|
223
|
-
highlighted: String(props.payload.source.name ?? "") ===
|
|
412
|
+
highlighted: String(props.payload.source.name ?? "") === activeSourceName,
|
|
413
|
+
displayValue: link?.displayValue,
|
|
414
|
+
layoutValue: link?.value,
|
|
224
415
|
onHoverChange: setHoveredSourceName,
|
|
225
416
|
clickable: onCurveClick !== void 0,
|
|
226
417
|
onSelect: () => {
|
|
@@ -242,40 +433,142 @@ function SankeyNode({
|
|
|
242
433
|
payload,
|
|
243
434
|
hueMap,
|
|
244
435
|
columnLabel,
|
|
436
|
+
label,
|
|
437
|
+
nodeValue,
|
|
438
|
+
layoutValue,
|
|
245
439
|
total,
|
|
246
440
|
showColumnLabel,
|
|
247
|
-
|
|
441
|
+
isFirstColumn,
|
|
442
|
+
isLastColumn,
|
|
443
|
+
clickable,
|
|
444
|
+
onFocusChange,
|
|
445
|
+
onHoverChange,
|
|
446
|
+
onSelect
|
|
248
447
|
}) {
|
|
249
448
|
const name = typeof payload.name === "string" || typeof payload.name === "number" ? String(payload.name) : "";
|
|
250
|
-
const
|
|
449
|
+
const displayLabel = label ?? name;
|
|
450
|
+
const descriptionIndex = displayLabel.indexOf("\n");
|
|
451
|
+
const visibleDisplayLabel = descriptionIndex >= 0 ? displayLabel.slice(0, descriptionIndex) : displayLabel;
|
|
452
|
+
const description = descriptionIndex >= 0 ? displayLabel.slice(descriptionIndex + 1) : void 0;
|
|
453
|
+
const accessibleLabel = displayLabel.replaceAll("\n", ". ");
|
|
454
|
+
const visibleLabel = truncateNodeLabel(visibleDisplayLabel);
|
|
455
|
+
const tooltipId = React.useId();
|
|
456
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
457
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
458
|
+
const [tooltipPosition, setTooltipPosition] = React.useState();
|
|
459
|
+
const numericValue = nodeValue ?? (typeof payload.value === "number" ? payload.value : Number(payload.value));
|
|
251
460
|
const value = Number.isFinite(numericValue) ? String(numericValue) : "";
|
|
252
461
|
const percentage = total > 0 && Number.isFinite(numericValue) ? Math.round(numericValue / total * 100) : 0;
|
|
253
|
-
const
|
|
462
|
+
const visibleHeight = scaleSankeyDimension(height, numericValue, layoutValue);
|
|
463
|
+
const visibleY = y + (height - visibleHeight) / 2;
|
|
464
|
+
const textAnchor = isFirstColumn ? "start" : isLastColumn ? "end" : "middle";
|
|
465
|
+
const labelX = isFirstColumn ? x : isLastColumn ? x + width : x + width / 2;
|
|
254
466
|
const columnLabelX = x + width / 2;
|
|
255
467
|
const hue = hueMap[name] ?? 0;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
468
|
+
const isTooltipVisible = Boolean(description && tooltipPosition && (isHovered || isFocused));
|
|
469
|
+
const showTooltipAt = (target) => {
|
|
470
|
+
const rect = target.getBoundingClientRect();
|
|
471
|
+
const placement = rect.top < 120 ? "below" : "above";
|
|
472
|
+
setTooltipPosition({
|
|
473
|
+
left: Math.min(Math.max(rect.left, 16), Math.max(window.innerWidth - 336, 16)),
|
|
474
|
+
top: placement === "above" ? rect.top - 8 : rect.bottom + 8,
|
|
475
|
+
placement
|
|
476
|
+
});
|
|
477
|
+
};
|
|
478
|
+
const handleKeyDown = (event) => {
|
|
479
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
480
|
+
event.preventDefault();
|
|
481
|
+
onSelect();
|
|
482
|
+
};
|
|
483
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
484
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
485
|
+
"g",
|
|
261
486
|
{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
487
|
+
"aria-describedby": description ? tooltipId : void 0,
|
|
488
|
+
"aria-label": `${accessibleLabel}: ${value} ${numericValue === 1 ? "trace" : "traces"} (${percentage}%)`,
|
|
489
|
+
className: "outline-hidden focus-visible:[&>rect]:stroke-neutral6 focus-visible:[&>rect]:stroke-2",
|
|
490
|
+
onClick: clickable ? onSelect : void 0,
|
|
491
|
+
onKeyDown: clickable ? handleKeyDown : void 0,
|
|
492
|
+
role: clickable ? "button" : void 0,
|
|
493
|
+
onFocus: (event) => {
|
|
494
|
+
onFocusChange(name);
|
|
495
|
+
setIsFocused(true);
|
|
496
|
+
showTooltipAt(event.currentTarget);
|
|
497
|
+
},
|
|
498
|
+
onBlur: () => {
|
|
499
|
+
onFocusChange(void 0);
|
|
500
|
+
setIsFocused(false);
|
|
501
|
+
},
|
|
502
|
+
onMouseEnter: (event) => {
|
|
503
|
+
onHoverChange(name);
|
|
504
|
+
setIsHovered(true);
|
|
505
|
+
showTooltipAt(event.currentTarget);
|
|
506
|
+
},
|
|
507
|
+
onMouseLeave: () => {
|
|
508
|
+
onHoverChange(void 0);
|
|
509
|
+
setIsHovered(false);
|
|
510
|
+
},
|
|
511
|
+
style: { cursor: clickable ? "pointer" : void 0 },
|
|
512
|
+
tabIndex: 0,
|
|
513
|
+
children: [
|
|
514
|
+
/* @__PURE__ */ jsxRuntime.jsx("title", { children: displayLabel }),
|
|
515
|
+
showColumnLabel && columnLabel ? /* @__PURE__ */ jsxRuntime.jsx("text", { x: columnLabelX, y: 18, textAnchor: "middle", fill: sankeyColor.nodeColor(hue), fontSize: 12, fontWeight: 600, children: columnLabel }) : null,
|
|
516
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x, y: visibleY, width, height: visibleHeight, rx: 3, fill: sankeyColor.nodeColor(hue) }),
|
|
517
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
518
|
+
"text",
|
|
519
|
+
{
|
|
520
|
+
x: labelX,
|
|
521
|
+
y: y - 24,
|
|
522
|
+
textAnchor,
|
|
523
|
+
fill: spacings.Colors.neutral5,
|
|
524
|
+
fontSize: 11,
|
|
525
|
+
fontFamily: "var(--font-mono)",
|
|
526
|
+
children: visibleLabel
|
|
527
|
+
}
|
|
528
|
+
),
|
|
529
|
+
/* @__PURE__ */ jsxRuntime.jsxs("text", { x: labelX, y: y - 8, textAnchor, fill: spacings.Colors.neutral3, fontSize: 9.5, children: [
|
|
530
|
+
value,
|
|
531
|
+
" (",
|
|
532
|
+
percentage,
|
|
533
|
+
"%)"
|
|
534
|
+
] })
|
|
535
|
+
]
|
|
269
536
|
}
|
|
270
537
|
),
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
538
|
+
description && isTooltipVisible && tooltipPosition ? reactDom.createPortal(
|
|
539
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
540
|
+
"div",
|
|
541
|
+
{
|
|
542
|
+
"aria-label": `${visibleDisplayLabel}: ${description}`,
|
|
543
|
+
className: "pointer-events-none fixed z-50 rounded-md border border-border1 bg-surface5 p-2 text-xs leading-4 text-neutral6 shadow-elevated",
|
|
544
|
+
id: tooltipId,
|
|
545
|
+
role: "tooltip",
|
|
546
|
+
style: {
|
|
547
|
+
left: tooltipPosition.left,
|
|
548
|
+
maxWidth: "min(20rem, calc(100vw - 2rem))",
|
|
549
|
+
top: tooltipPosition.top,
|
|
550
|
+
transform: tooltipPosition.placement === "above" ? "translateY(-100%)" : void 0,
|
|
551
|
+
width: "max-content"
|
|
552
|
+
},
|
|
553
|
+
children: [
|
|
554
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium", children: visibleDisplayLabel }),
|
|
555
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap text-neutral4", children: description })
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
),
|
|
559
|
+
document.body
|
|
560
|
+
) : null
|
|
277
561
|
] });
|
|
278
562
|
}
|
|
563
|
+
function scaleSankeyDimension(size, displayValue, layoutValue) {
|
|
564
|
+
if (displayValue === void 0 || layoutValue === void 0 || layoutValue <= 0) return size;
|
|
565
|
+
return size * Math.min(Math.max(displayValue / layoutValue, 0), 1);
|
|
566
|
+
}
|
|
567
|
+
function truncateNodeLabel(label) {
|
|
568
|
+
const maximumLength = 23;
|
|
569
|
+
if (label.length <= maximumLength) return label;
|
|
570
|
+
return `${label.slice(0, maximumLength - 1).trimEnd()}…`;
|
|
571
|
+
}
|
|
279
572
|
function SankeyLink({
|
|
280
573
|
sourceX,
|
|
281
574
|
targetX,
|
|
@@ -288,16 +581,22 @@ function SankeyLink({
|
|
|
288
581
|
payload,
|
|
289
582
|
hueMap,
|
|
290
583
|
highlighted,
|
|
584
|
+
displayValue,
|
|
585
|
+
layoutValue,
|
|
586
|
+
sourceWidth,
|
|
587
|
+
targetWidth,
|
|
291
588
|
clickable,
|
|
292
589
|
onHoverChange,
|
|
293
590
|
onSelect
|
|
294
591
|
}) {
|
|
295
|
-
const
|
|
592
|
+
const visibleWidth = scaleSankeyDimension(linkWidth, displayValue, layoutValue);
|
|
593
|
+
const sourceHalfWidth = Math.max(0, sourceWidth ?? visibleWidth) / 2;
|
|
594
|
+
const targetHalfWidth = Math.max(0, targetWidth ?? visibleWidth) / 2;
|
|
296
595
|
const path = [
|
|
297
|
-
`M${sourceX},${sourceY -
|
|
298
|
-
`C${sourceControlX},${sourceY -
|
|
299
|
-
`L${targetX},${targetY +
|
|
300
|
-
`C${targetControlX},${targetY +
|
|
596
|
+
`M${sourceX},${sourceY - sourceHalfWidth}`,
|
|
597
|
+
`C${sourceControlX},${sourceY - sourceHalfWidth} ${targetControlX},${targetY - targetHalfWidth} ${targetX},${targetY - targetHalfWidth}`,
|
|
598
|
+
`L${targetX},${targetY + targetHalfWidth}`,
|
|
599
|
+
`C${targetControlX},${targetY + targetHalfWidth} ${sourceControlX},${sourceY + sourceHalfWidth} ${sourceX},${sourceY + sourceHalfWidth}`,
|
|
301
600
|
"Z"
|
|
302
601
|
].join(" ");
|
|
303
602
|
const sourceName = String(payload.source.name ?? "");
|
|
@@ -348,6 +647,7 @@ exports.Sankey = Sankey;
|
|
|
348
647
|
exports.SankeyChart = SankeyChart;
|
|
349
648
|
exports.buildSankeyChartGraph = buildSankeyChartGraph;
|
|
350
649
|
exports.getSankeyChartCurveSelection = getSankeyChartCurveSelection;
|
|
650
|
+
exports.getSankeyChartNodeSelection = getSankeyChartNodeSelection;
|
|
351
651
|
exports.getSankeyChartNodeWeights = getSankeyChartNodeWeights;
|
|
352
652
|
exports.getSankeyChartValue = getSankeyChartValue;
|
|
353
653
|
exports.reorderSankeyChartColumns = reorderSankeyChartColumns;
|