@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 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 weight = getRecordWeight(record);
53
- if (!Number.isFinite(weight) || weight < 0) continue;
54
- const sourceValue = getSankeyChartValue(record[sourceColumn.id]);
55
- const targetValue = getSankeyChartValue(record[targetColumn.id]);
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 source = getNode(sourceColumn, sourceValue);
58
- const target = getNode(targetColumn, targetValue);
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 += weight;
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: weight,
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(data, enabledColumns, getRecordWeight);
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(SankeyRenderContext.Provider, { value: { graph, enabledColumns, hueMap }, children }) });
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 total = graph.links.reduce(
178
- (sum, link) => link.sourceNode.column.id === firstColumnId ? sum + link.value : sum,
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
- onHoverChange: setHoveredSourceName
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 ?? "") === hoveredSourceName,
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
- onHoverChange
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 numericValue = typeof payload.value === "number" ? payload.value : Number(payload.value);
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 labelX = x + width / 2;
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
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { onMouseEnter: () => onHoverChange(name), onMouseLeave: () => onHoverChange(void 0), children: [
257
- showColumnLabel && columnLabel ? /* @__PURE__ */ jsxRuntime.jsx("text", { x: columnLabelX, y: 18, textAnchor: "middle", fill: sankeyColor.nodeColor(hue), fontSize: 12, fontWeight: 600, children: columnLabel }) : null,
258
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x, y, width, height, rx: 3, fill: sankeyColor.nodeColor(hue) }),
259
- /* @__PURE__ */ jsxRuntime.jsx(
260
- "text",
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
- x: labelX,
263
- y: y - 24,
264
- textAnchor: "middle",
265
- fill: spacings.Colors.neutral5,
266
- fontSize: 11,
267
- fontFamily: "var(--font-mono)",
268
- children: name
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
- /* @__PURE__ */ jsxRuntime.jsxs("text", { x: labelX, y: y - 8, textAnchor: "middle", fill: spacings.Colors.neutral3, fontSize: 9.5, children: [
272
- value,
273
- " (",
274
- percentage,
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 halfWidth = Math.max(0, linkWidth) / 2;
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 - halfWidth}`,
298
- `C${sourceControlX},${sourceY - halfWidth} ${targetControlX},${targetY - halfWidth} ${targetX},${targetY - halfWidth}`,
299
- `L${targetX},${targetY + halfWidth}`,
300
- `C${targetControlX},${targetY + halfWidth} ${sourceControlX},${sourceY + halfWidth} ${sourceX},${sourceY + halfWidth}`,
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;