panelui-native 0.99.0 → 0.101.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/README.md +2 -0
- package/lib/module/components/animated-badge/index.js +24 -2
- package/lib/module/components/animated-badge/index.js.map +1 -1
- package/lib/module/components/bump-chart/index.js +1236 -0
- package/lib/module/components/bump-chart/index.js.map +1 -0
- package/lib/module/components/compare/index.js +576 -0
- package/lib/module/components/compare/index.js.map +1 -0
- package/lib/module/components/mirror-area-chart/index.js +1113 -0
- package/lib/module/components/mirror-area-chart/index.js.map +1 -0
- package/lib/module/components/sankey-chart/index.js +933 -0
- package/lib/module/components/sankey-chart/index.js.map +1 -0
- package/lib/module/components/sankey-chart/sankey-layout.js +530 -0
- package/lib/module/components/sankey-chart/sankey-layout.js.map +1 -0
- package/lib/module/icons/index.js +15 -3
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +101 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/animated-badge/index.d.ts +5 -0
- package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bump-chart/index.d.ts +267 -0
- package/lib/typescript/src/components/bump-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/compare/index.d.ts +183 -0
- package/lib/typescript/src/components/compare/index.d.ts.map +1 -0
- package/lib/typescript/src/components/mirror-area-chart/index.d.ts +263 -0
- package/lib/typescript/src/components/mirror-area-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/sankey-chart/index.d.ts +307 -0
- package/lib/typescript/src/components/sankey-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts +107 -0
- package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +40 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/animated-badge/index.tsx +35 -2
- package/src/components/bump-chart/index.tsx +1455 -0
- package/src/components/compare/index.tsx +650 -0
- package/src/components/mirror-area-chart/index.tsx +1333 -0
- package/src/components/sankey-chart/index.tsx +1165 -0
- package/src/components/sankey-chart/sankey-layout.ts +645 -0
- package/src/icons/index.tsx +20 -6
- package/src/index.ts +62 -0
- package/src/utils/chart.ts +116 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SankeyChart — where a quantity came from and where it ended up.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <SankeyChart nodes={stages} links={flows}>
|
|
8
|
+
* <SankeyChart.Header title="Traffic" value="128,400" />
|
|
9
|
+
* <SankeyChart.Links />
|
|
10
|
+
* <SankeyChart.Nodes />
|
|
11
|
+
* <SankeyChart.Labels />
|
|
12
|
+
* <SankeyChart.Tooltip />
|
|
13
|
+
* </SankeyChart>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## What it answers that the other charts do not
|
|
17
|
+
*
|
|
18
|
+
* Every other chart here takes one set of things and measures them. This one
|
|
19
|
+
* takes two and says how much of the first became the second. A treemap cuts a
|
|
20
|
+
* total into its parts, a funnel counts what survived each step, a waterfall
|
|
21
|
+
* carries a balance from one figure to another — none of them can say that
|
|
22
|
+
* *this* source fed *that* destination, because none of them draws a thing that
|
|
23
|
+
* has two ends.
|
|
24
|
+
*
|
|
25
|
+
* So the question to bring to it is a routing question. Which campaigns
|
|
26
|
+
* produced which signups; which budget lines paid for which departments; what
|
|
27
|
+
* the traffic that arrived on the landing page went on to do. If the answer
|
|
28
|
+
* does not need a source *and* a target, one of the simpler charts will read
|
|
29
|
+
* better at the same size.
|
|
30
|
+
*
|
|
31
|
+
* ## The ribbon is the reading
|
|
32
|
+
*
|
|
33
|
+
* A ribbon's thickness is its value, on one scale shared by the whole diagram,
|
|
34
|
+
* so a ribbon twice as thick is twice as much wherever it is on the page. That
|
|
35
|
+
* is the only quantity here: the horizontal distance a ribbon travels is the
|
|
36
|
+
* number of columns between its ends and means nothing else, and the vertical
|
|
37
|
+
* order within a column is chosen to keep the ribbons from crossing rather than
|
|
38
|
+
* to rank anything.
|
|
39
|
+
*
|
|
40
|
+
* A node's height is what passes through it — the larger of what arrives and
|
|
41
|
+
* what leaves, which are the same number unless some of it went nowhere. Where
|
|
42
|
+
* they differ, give the node an explicit `value` to pin it; the diagram cannot
|
|
43
|
+
* infer a loss it was never told about.
|
|
44
|
+
*
|
|
45
|
+
* ## Columns are the flow's, not the caller's
|
|
46
|
+
*
|
|
47
|
+
* Nothing about the order of the `nodes` array decides where a node is drawn. A
|
|
48
|
+
* node that receives from another has to be drawn after it or its ribbon would
|
|
49
|
+
* run backwards, so the columns come out of the links. `align` only settles the
|
|
50
|
+
* cases the flow leaves open, and the default pushes every node that feeds
|
|
51
|
+
* nothing into the last column, so the diagram ends on a straight edge of
|
|
52
|
+
* destinations instead of a ragged one.
|
|
53
|
+
*
|
|
54
|
+
* ## Bad rows are dropped rather than fatal
|
|
55
|
+
*
|
|
56
|
+
* Flow data is nearly always joined together from somewhere nobody in the room
|
|
57
|
+
* owns, and it arrives with rows that name a node that is not there, carry a
|
|
58
|
+
* zero, or close a loop. A loop in particular has no left-to-right reading at
|
|
59
|
+
* all. All of them are dropped and counted, and `onDropLinks` reports how many
|
|
60
|
+
* — so a screen can say "3 rows could not be drawn" instead of going blank or
|
|
61
|
+
* quietly showing less than it was given.
|
|
62
|
+
*/
|
|
63
|
+
import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
64
|
+
import { Platform, Pressable, View } from 'react-native';
|
|
65
|
+
import Animated, { Easing, useAnimatedProps, useDerivedValue, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
66
|
+
import Svg, { G, Path, Rect } from 'react-native-svg';
|
|
67
|
+
import { useCSSVariable } from 'uniwind';
|
|
68
|
+
import { ChartAccessibilityData } from "../../primitives/chart-accessibility.js";
|
|
69
|
+
import { Text } from "../../primitives/text.js";
|
|
70
|
+
import { compactNumber, flowPath, seriesColorAt, useSeriesColor } from "../../utils/chart.js";
|
|
71
|
+
import { cn } from "../../utils/cn.js";
|
|
72
|
+
import { useDirection } from "../../hooks/use-direction.js";
|
|
73
|
+
import { useSkeletonHandoff } from "../../hooks/use-skeleton-handoff.js";
|
|
74
|
+
import { sankeyLayout } from "./sankey-layout.js";
|
|
75
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
76
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
77
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
78
|
+
const AnimatedG = Animated.createAnimatedComponent(G);
|
|
79
|
+
|
|
80
|
+
/** How tall the diagram is drawn when the caller does not say. */
|
|
81
|
+
const DEFAULT_HEIGHT = 240;
|
|
82
|
+
|
|
83
|
+
/** How thick a node's bar is. Thin enough to read as an edge the flow meets. */
|
|
84
|
+
const DEFAULT_NODE_WIDTH = 10;
|
|
85
|
+
|
|
86
|
+
/** The gap asked for between two nodes in a column. */
|
|
87
|
+
const DEFAULT_NODE_PADDING = 14;
|
|
88
|
+
|
|
89
|
+
/** Relaxation rounds. Where the arrangement stops visibly improving. */
|
|
90
|
+
const DEFAULT_ITERATIONS = 6;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* How far a ribbon's control points reach towards the middle.
|
|
94
|
+
*
|
|
95
|
+
* Exactly half, which puts both on the centre line and makes the two halves of
|
|
96
|
+
* every ribbon mirror images. Anything less leaves a visible straight section
|
|
97
|
+
* in the middle that reads as a kink where two ribbons were joined.
|
|
98
|
+
*/
|
|
99
|
+
const CURVE = 0.5;
|
|
100
|
+
|
|
101
|
+
/** A ribbon at rest. Translucent, because ribbons cross and both must be read. */
|
|
102
|
+
const LINK_OPACITY = 0.4;
|
|
103
|
+
|
|
104
|
+
/** A ribbon belonging to the selected node. */
|
|
105
|
+
const LINK_ACTIVE_OPACITY = 0.78;
|
|
106
|
+
|
|
107
|
+
/** And one that does not, once something is selected. */
|
|
108
|
+
const LINK_DIM_OPACITY = 0.08;
|
|
109
|
+
|
|
110
|
+
/** Milliseconds for one column of the flow to draw itself. */
|
|
111
|
+
const DEFAULT_DURATION = 620;
|
|
112
|
+
|
|
113
|
+
/** Milliseconds between one column starting and the next. */
|
|
114
|
+
const STAGGER = 110;
|
|
115
|
+
|
|
116
|
+
/** Milliseconds for a selection to take hold. */
|
|
117
|
+
const SELECT_DURATION = 180;
|
|
118
|
+
|
|
119
|
+
/** Space between a node's bar and its name. */
|
|
120
|
+
const LABEL_GAP = 6;
|
|
121
|
+
|
|
122
|
+
/** The smallest a label's press target is allowed to be, in points. */
|
|
123
|
+
const MIN_TARGET = 44;
|
|
124
|
+
|
|
125
|
+
/** Columns the placeholder suggests while there is no data to count. */
|
|
126
|
+
const SKELETON_COLUMNS = 3;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* What takes the geometry out of the accessibility tree, per platform.
|
|
130
|
+
*
|
|
131
|
+
* The two native props reach the DOM untranslated through react-native-svg, so
|
|
132
|
+
* on web they mean nothing and one of them draws a React warning for its
|
|
133
|
+
* casing. `aria-hidden` is what hides an `<svg>` there.
|
|
134
|
+
*/
|
|
135
|
+
const HIDDEN = Platform.OS === 'web' ? {
|
|
136
|
+
'aria-hidden': true
|
|
137
|
+
} : {
|
|
138
|
+
accessibilityElementsHidden: true,
|
|
139
|
+
importantForAccessibility: 'no-hide-descendants'
|
|
140
|
+
};
|
|
141
|
+
const SankeyChartContext = /*#__PURE__*/createContext(null);
|
|
142
|
+
function useChart(component) {
|
|
143
|
+
const context = useContext(SankeyChartContext);
|
|
144
|
+
if (!context) {
|
|
145
|
+
throw new Error(`${component} must be used within a <SankeyChart>`);
|
|
146
|
+
}
|
|
147
|
+
return context;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The selected node and what runs through it, for something drawn inside the chart. */
|
|
151
|
+
export function useSankeyChart() {
|
|
152
|
+
const {
|
|
153
|
+
nodes,
|
|
154
|
+
layout,
|
|
155
|
+
activeId
|
|
156
|
+
} = useChart('useSankeyChart');
|
|
157
|
+
return useMemo(() => {
|
|
158
|
+
const placed = activeId ? layout.nodes.find(node => node.id === activeId) : undefined;
|
|
159
|
+
if (!placed) {
|
|
160
|
+
return {
|
|
161
|
+
activeId: null,
|
|
162
|
+
activeNode: null,
|
|
163
|
+
activeValue: 0,
|
|
164
|
+
incoming: 0,
|
|
165
|
+
outgoing: 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const position = layout.nodes.indexOf(placed);
|
|
169
|
+
let incoming = 0;
|
|
170
|
+
let outgoing = 0;
|
|
171
|
+
for (const link of layout.links) {
|
|
172
|
+
if (link.target === position) incoming += link.value;
|
|
173
|
+
if (link.source === position) outgoing += link.value;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
activeId,
|
|
177
|
+
activeNode: nodes.find(node => node.id === activeId) ?? null,
|
|
178
|
+
/** What passes through it — what the bar's height is drawn from. */
|
|
179
|
+
activeValue: placed.value,
|
|
180
|
+
/** What arrives. Zero at a node the flow starts from. */
|
|
181
|
+
incoming,
|
|
182
|
+
/** What leaves. Zero at a node the flow ends at. */
|
|
183
|
+
outgoing
|
|
184
|
+
};
|
|
185
|
+
}, [nodes, layout, activeId]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Imperative handle: re-run the entrance, for a "replay" control. */
|
|
189
|
+
|
|
190
|
+
const SankeyChartRoot = /*#__PURE__*/forwardRef(function SankeyChartRoot({
|
|
191
|
+
className,
|
|
192
|
+
nodes,
|
|
193
|
+
links,
|
|
194
|
+
height = DEFAULT_HEIGHT,
|
|
195
|
+
nodeWidth = DEFAULT_NODE_WIDTH,
|
|
196
|
+
nodePadding = DEFAULT_NODE_PADDING,
|
|
197
|
+
align = 'justify',
|
|
198
|
+
iterations = DEFAULT_ITERATIONS,
|
|
199
|
+
curve = CURVE,
|
|
200
|
+
color,
|
|
201
|
+
animationDuration = DEFAULT_DURATION,
|
|
202
|
+
staggerDelay = STAGGER,
|
|
203
|
+
status = 'ready',
|
|
204
|
+
activeId: activeIdProp,
|
|
205
|
+
onActiveIdChange,
|
|
206
|
+
onDropLinks,
|
|
207
|
+
accessibilityLabel,
|
|
208
|
+
accessibilityHint,
|
|
209
|
+
accessibilityLabelForDatum,
|
|
210
|
+
onAccessibilityDatumPress,
|
|
211
|
+
children,
|
|
212
|
+
...props
|
|
213
|
+
}, ref) {
|
|
214
|
+
const [width, setWidth] = useState(0);
|
|
215
|
+
const [internalActive, setInternalActive] = useState(null);
|
|
216
|
+
const reveal = useSharedValue(0);
|
|
217
|
+
const reducedMotion = useReducedMotion();
|
|
218
|
+
const direction = useDirection();
|
|
219
|
+
const controlled = activeIdProp !== undefined;
|
|
220
|
+
const activeId = controlled ? activeIdProp : internalActive;
|
|
221
|
+
const setActiveId = useMemo(() => id => {
|
|
222
|
+
if (!controlled) setInternalActive(id);
|
|
223
|
+
onActiveIdChange?.(id);
|
|
224
|
+
}, [controlled, onActiveIdChange]);
|
|
225
|
+
const layout = useMemo(() => sankeyLayout(nodes, links, {
|
|
226
|
+
width,
|
|
227
|
+
height,
|
|
228
|
+
nodeWidth,
|
|
229
|
+
nodePadding,
|
|
230
|
+
align,
|
|
231
|
+
iterations
|
|
232
|
+
}), [nodes, links, width, height, nodeWidth, nodePadding, align, iterations]);
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* A flow reads from where it starts, and under a right-to-left layout that
|
|
236
|
+
* is the right-hand edge. Mirroring the finished layout rather than laying
|
|
237
|
+
* it out backwards keeps one set of maths under both directions — the
|
|
238
|
+
* arrangement is identical, it is only read from the other end.
|
|
239
|
+
*/
|
|
240
|
+
const mirrored = direction === 'rtl';
|
|
241
|
+
const placed = useMemo(() => {
|
|
242
|
+
if (!mirrored || !layout.nodes.length) return layout;
|
|
243
|
+
return {
|
|
244
|
+
...layout,
|
|
245
|
+
nodes: layout.nodes.map(node => ({
|
|
246
|
+
...node,
|
|
247
|
+
x0: width - node.x1,
|
|
248
|
+
x1: width - node.x0
|
|
249
|
+
}))
|
|
250
|
+
};
|
|
251
|
+
}, [layout, mirrored, width]);
|
|
252
|
+
const dropped = layout.dropped;
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
onDropLinks?.(dropped);
|
|
255
|
+
}, [dropped, onDropLinks]);
|
|
256
|
+
const c1 = useSeriesColor(color, 1);
|
|
257
|
+
const c2 = useSeriesColor(undefined, 2);
|
|
258
|
+
const c3 = useSeriesColor(undefined, 3);
|
|
259
|
+
const c4 = useSeriesColor(undefined, 4);
|
|
260
|
+
const c5 = useSeriesColor(undefined, 5);
|
|
261
|
+
const palette = useMemo(() => [c1, c2, c3, c4, c5], [c1, c2, c3, c4, c5]);
|
|
262
|
+
|
|
263
|
+
/*
|
|
264
|
+
* Coloured by the node's own position in the data rather than by its column.
|
|
265
|
+
* A colour per column would say the column means something, and it does not
|
|
266
|
+
* — it is just how far along the flow a node happens to sit.
|
|
267
|
+
*/
|
|
268
|
+
const colors = useMemo(() => placed.nodes.map((node, index) => nodes[node.index]?.color ?? seriesColorAt(palette, index)), [placed.nodes, nodes, palette]);
|
|
269
|
+
const labelFor = useMemo(() => {
|
|
270
|
+
const names = new Map(nodes.map(node => [node.id, node.label ?? node.id]));
|
|
271
|
+
return id => names.get(id) ?? id;
|
|
272
|
+
}, [nodes]);
|
|
273
|
+
|
|
274
|
+
/*
|
|
275
|
+
* One clock, with each column given the slice of it that it draws in, so
|
|
276
|
+
* the flow arrives in the order it happens rather than all at once. A
|
|
277
|
+
* shared value per column would be the same animation played n times and n
|
|
278
|
+
* more things a replay would have to find.
|
|
279
|
+
*/
|
|
280
|
+
const stagger = Math.max(0, staggerDelay);
|
|
281
|
+
const columns = Math.max(placed.columns, 1);
|
|
282
|
+
const total = animationDuration + Math.max(columns - 1, 0) * stagger;
|
|
283
|
+
const windows = useMemo(() => Array.from({
|
|
284
|
+
length: columns
|
|
285
|
+
}, (_, column) => {
|
|
286
|
+
const from = column * stagger;
|
|
287
|
+
return {
|
|
288
|
+
from: total > 0 ? from / total : 0,
|
|
289
|
+
to: total > 0 ? (from + animationDuration) / total : 1
|
|
290
|
+
};
|
|
291
|
+
}), [columns, stagger, animationDuration, total]);
|
|
292
|
+
const playReveal = useMemo(() => () => {
|
|
293
|
+
if (reducedMotion) {
|
|
294
|
+
reveal.value = 1;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
reveal.value = 0;
|
|
298
|
+
// Linear, because the shaping is per column: each one eases inside its
|
|
299
|
+
// own window, and easing the clock as well would ease it twice.
|
|
300
|
+
reveal.value = withTiming(1, {
|
|
301
|
+
duration: total,
|
|
302
|
+
easing: Easing.linear
|
|
303
|
+
});
|
|
304
|
+
}, [reducedMotion, total, reveal]);
|
|
305
|
+
const loading = status === 'loading';
|
|
306
|
+
const revealed = useRef(false);
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (loading) {
|
|
309
|
+
revealed.current = false;
|
|
310
|
+
reveal.value = 0;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (revealed.current || !placed.nodes.length) return;
|
|
314
|
+
revealed.current = true;
|
|
315
|
+
playReveal();
|
|
316
|
+
}, [loading, placed.nodes.length, playReveal, reveal]);
|
|
317
|
+
useImperativeHandle(ref, () => ({
|
|
318
|
+
replay: playReveal
|
|
319
|
+
}), [playReveal]);
|
|
320
|
+
|
|
321
|
+
// Measured on the plot's own view rather than the outer one, so a header or
|
|
322
|
+
// a footer cannot change how wide the diagram thinks it is.
|
|
323
|
+
const onLayout = event => {
|
|
324
|
+
const next = Math.round(event.nativeEvent.layout.width);
|
|
325
|
+
if (next !== width) setWidth(next);
|
|
326
|
+
};
|
|
327
|
+
const context = useMemo(() => ({
|
|
328
|
+
nodes,
|
|
329
|
+
links,
|
|
330
|
+
layout: placed,
|
|
331
|
+
width,
|
|
332
|
+
height,
|
|
333
|
+
curve,
|
|
334
|
+
colors,
|
|
335
|
+
windows,
|
|
336
|
+
reveal,
|
|
337
|
+
status,
|
|
338
|
+
activeId: activeId ?? null,
|
|
339
|
+
setActiveId,
|
|
340
|
+
labelFor
|
|
341
|
+
}), [nodes, links, placed, width, height, curve, colors, windows, reveal, status, activeId, setActiveId, labelFor]);
|
|
342
|
+
const slots = {
|
|
343
|
+
svg: [],
|
|
344
|
+
overlay: [],
|
|
345
|
+
header: [],
|
|
346
|
+
footer: []
|
|
347
|
+
};
|
|
348
|
+
/*
|
|
349
|
+
* Whether the names are on the chart decides how it is read out. With
|
|
350
|
+
* `Labels` there is a pressable row per node already, and the semantic
|
|
351
|
+
* list below would say all of it a second time; without them the diagram
|
|
352
|
+
* is pure geometry and the list is the only way through it.
|
|
353
|
+
*/
|
|
354
|
+
let labelled = false;
|
|
355
|
+
Children.forEach(children, (child, index) => {
|
|
356
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
357
|
+
const slot = child.type.slot ?? 'overlay';
|
|
358
|
+
if (child.type.displayName === 'SankeyChart.Labels') {
|
|
359
|
+
labelled = true;
|
|
360
|
+
}
|
|
361
|
+
slots[slot in slots ? slot : 'overlay'].push(/*#__PURE__*/_jsx(ChildSlot, {
|
|
362
|
+
children: child
|
|
363
|
+
}, index));
|
|
364
|
+
});
|
|
365
|
+
return /*#__PURE__*/_jsx(SankeyChartContext.Provider, {
|
|
366
|
+
value: context,
|
|
367
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
368
|
+
...props,
|
|
369
|
+
style: props.style,
|
|
370
|
+
className: cn('w-full', className),
|
|
371
|
+
children: [slots.header, /*#__PURE__*/_jsx(View, {
|
|
372
|
+
onLayout: onLayout,
|
|
373
|
+
style: {
|
|
374
|
+
height
|
|
375
|
+
},
|
|
376
|
+
className: "w-full",
|
|
377
|
+
children: width > 0 && height > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
378
|
+
children: [/*#__PURE__*/_jsx(Svg, {
|
|
379
|
+
width: width,
|
|
380
|
+
height: height,
|
|
381
|
+
...HIDDEN,
|
|
382
|
+
children: slots.svg
|
|
383
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
384
|
+
pointerEvents: "box-none",
|
|
385
|
+
style: {
|
|
386
|
+
position: 'absolute',
|
|
387
|
+
width,
|
|
388
|
+
height,
|
|
389
|
+
direction: 'ltr'
|
|
390
|
+
},
|
|
391
|
+
children: slots.overlay
|
|
392
|
+
})]
|
|
393
|
+
}) : null
|
|
394
|
+
}), slots.footer, /*#__PURE__*/_jsx(ChartAccessibilityData, {
|
|
395
|
+
chart: "Flow diagram",
|
|
396
|
+
data: nodes,
|
|
397
|
+
disabled: labelled || status === 'loading',
|
|
398
|
+
valueOf: node => [['Node', node.label ?? node.id], ['Value', placed.nodes.find(placedNode => placedNode.id === node.id)?.value]],
|
|
399
|
+
accessibilityLabel: accessibilityLabel,
|
|
400
|
+
accessibilityHint: accessibilityHint,
|
|
401
|
+
accessibilityLabelForDatum: accessibilityLabelForDatum,
|
|
402
|
+
onAccessibilityDatumPress: onAccessibilityDatumPress
|
|
403
|
+
})]
|
|
404
|
+
})
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
SankeyChartRoot.displayName = 'SankeyChart';
|
|
408
|
+
function ChildSlot({
|
|
409
|
+
children
|
|
410
|
+
}) {
|
|
411
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
412
|
+
children: children
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* How much of the entrance a column has played, eased.
|
|
418
|
+
*
|
|
419
|
+
* Ease out cubic, written out rather than called: an easing from the animation
|
|
420
|
+
* library is not a worklet and this runs on the UI thread every frame.
|
|
421
|
+
*/
|
|
422
|
+
function progress(clock, from, to) {
|
|
423
|
+
'worklet';
|
|
424
|
+
|
|
425
|
+
const range = to - from;
|
|
426
|
+
const raw = range > 0 ? (clock - from) / range : 1;
|
|
427
|
+
const clamped = raw < 0 ? 0 : raw > 1 ? 1 : raw;
|
|
428
|
+
return 1 - (1 - clamped) * (1 - clamped) * (1 - clamped);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The ribbons.
|
|
432
|
+
*
|
|
433
|
+
* Drawn before the bars so a bar sits on top of the flows that meet it, which
|
|
434
|
+
* is what gives a node a clean edge to arrive at instead of a fringe of ribbon
|
|
435
|
+
* ends poking through it.
|
|
436
|
+
*
|
|
437
|
+
* Translucent at rest, and that is not decoration. Ribbons cross — that is the
|
|
438
|
+
* shape of routed data — and an opaque one hides whatever passes under it, so
|
|
439
|
+
* the reader loses the smaller of every pair. At this opacity a crossing reads
|
|
440
|
+
* as two ribbons rather than as one with a notch in it.
|
|
441
|
+
*/
|
|
442
|
+
function SankeyChartLinks({
|
|
443
|
+
opacity = LINK_OPACITY,
|
|
444
|
+
activeOpacity = LINK_ACTIVE_OPACITY,
|
|
445
|
+
dimOpacity = LINK_DIM_OPACITY
|
|
446
|
+
}) {
|
|
447
|
+
const {
|
|
448
|
+
layout,
|
|
449
|
+
links,
|
|
450
|
+
colors,
|
|
451
|
+
curve,
|
|
452
|
+
reveal,
|
|
453
|
+
windows,
|
|
454
|
+
status,
|
|
455
|
+
activeId
|
|
456
|
+
} = useChart('SankeyChart.Links');
|
|
457
|
+
if (status === 'loading' || !layout.links.length) return null;
|
|
458
|
+
return /*#__PURE__*/_jsx(G, {
|
|
459
|
+
children: layout.links.map(link => {
|
|
460
|
+
const source = layout.nodes[link.source];
|
|
461
|
+
const target = layout.nodes[link.target];
|
|
462
|
+
if (!source || !target) return null;
|
|
463
|
+
const touching = activeId === source.id || activeId === target.id;
|
|
464
|
+
const window = windows[source.layer] ?? windows[0] ?? {
|
|
465
|
+
from: 0,
|
|
466
|
+
to: 1
|
|
467
|
+
};
|
|
468
|
+
return /*#__PURE__*/_jsx(Ribbon, {
|
|
469
|
+
x0: source.x1,
|
|
470
|
+
cy0: link.y0,
|
|
471
|
+
x1: target.x0,
|
|
472
|
+
cy1: link.y1,
|
|
473
|
+
thickness: link.width,
|
|
474
|
+
curve: curve,
|
|
475
|
+
fill: links[link.input]?.color ?? colors[link.source] ?? colors[0] ?? '#3b82f6',
|
|
476
|
+
reveal: reveal,
|
|
477
|
+
window: window,
|
|
478
|
+
opacity: activeId === null ? opacity : touching ? activeOpacity : dimOpacity
|
|
479
|
+
}, link.index);
|
|
480
|
+
})
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
SankeyChartLinks.displayName = 'SankeyChart.Links';
|
|
484
|
+
SankeyChartLinks.slot = 'svg';
|
|
485
|
+
function Ribbon({
|
|
486
|
+
x0,
|
|
487
|
+
cy0,
|
|
488
|
+
x1,
|
|
489
|
+
cy1,
|
|
490
|
+
thickness,
|
|
491
|
+
curve,
|
|
492
|
+
fill,
|
|
493
|
+
reveal,
|
|
494
|
+
window,
|
|
495
|
+
opacity
|
|
496
|
+
}) {
|
|
497
|
+
const {
|
|
498
|
+
from,
|
|
499
|
+
to
|
|
500
|
+
} = window;
|
|
501
|
+
const settled = useDerivedValue(() => withTiming(opacity, {
|
|
502
|
+
duration: SELECT_DURATION
|
|
503
|
+
}));
|
|
504
|
+
const animatedProps = useAnimatedProps(() => {
|
|
505
|
+
/*
|
|
506
|
+
* The ribbon thickens about its own centre line rather than growing from
|
|
507
|
+
* one edge, so it stays anchored where it meets the node instead of
|
|
508
|
+
* sliding down it as it arrives.
|
|
509
|
+
*/
|
|
510
|
+
const grown = thickness * progress(reveal.value, from, to);
|
|
511
|
+
return {
|
|
512
|
+
d: flowPath(x0, cy0, x1, cy1, grown, curve),
|
|
513
|
+
fillOpacity: settled.value
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
return /*#__PURE__*/_jsx(AnimatedPath, {
|
|
517
|
+
animatedProps: animatedProps,
|
|
518
|
+
fill: fill
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* The bars the ribbons run between.
|
|
523
|
+
*
|
|
524
|
+
* Solid where the ribbons are translucent, because a node is the one thing on
|
|
525
|
+
* the diagram that is not crossing anything else — it is the edge the flow
|
|
526
|
+
* arrives at, and it reads as an edge only if nothing shows through it.
|
|
527
|
+
*/
|
|
528
|
+
function SankeyChartNodes({
|
|
529
|
+
radius = 2,
|
|
530
|
+
dimOpacity = 0.25
|
|
531
|
+
}) {
|
|
532
|
+
const {
|
|
533
|
+
layout,
|
|
534
|
+
colors,
|
|
535
|
+
reveal,
|
|
536
|
+
windows,
|
|
537
|
+
status,
|
|
538
|
+
activeId
|
|
539
|
+
} = useChart('SankeyChart.Nodes');
|
|
540
|
+
if (status === 'loading' || !layout.nodes.length) return null;
|
|
541
|
+
return /*#__PURE__*/_jsx(G, {
|
|
542
|
+
children: layout.nodes.map((node, index) => {
|
|
543
|
+
const window = windows[node.layer] ?? windows[0] ?? {
|
|
544
|
+
from: 0,
|
|
545
|
+
to: 1
|
|
546
|
+
};
|
|
547
|
+
return /*#__PURE__*/_jsx(NodeBar, {
|
|
548
|
+
x: node.x0,
|
|
549
|
+
width: node.x1 - node.x0,
|
|
550
|
+
y0: node.y0,
|
|
551
|
+
y1: node.y1,
|
|
552
|
+
radius: radius,
|
|
553
|
+
fill: colors[index] ?? '#3b82f6',
|
|
554
|
+
reveal: reveal,
|
|
555
|
+
window: window,
|
|
556
|
+
opacity: activeId === null || activeId === node.id ? 1 : dimOpacity
|
|
557
|
+
}, node.id);
|
|
558
|
+
})
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
SankeyChartNodes.displayName = 'SankeyChart.Nodes';
|
|
562
|
+
SankeyChartNodes.slot = 'svg';
|
|
563
|
+
function NodeBar({
|
|
564
|
+
x,
|
|
565
|
+
width,
|
|
566
|
+
y0,
|
|
567
|
+
y1,
|
|
568
|
+
radius,
|
|
569
|
+
fill,
|
|
570
|
+
reveal,
|
|
571
|
+
window,
|
|
572
|
+
opacity
|
|
573
|
+
}) {
|
|
574
|
+
const {
|
|
575
|
+
from,
|
|
576
|
+
to
|
|
577
|
+
} = window;
|
|
578
|
+
const extent = y1 - y0;
|
|
579
|
+
const centre = (y0 + y1) / 2;
|
|
580
|
+
const settled = useDerivedValue(() => withTiming(opacity, {
|
|
581
|
+
duration: SELECT_DURATION
|
|
582
|
+
}));
|
|
583
|
+
const animatedProps = useAnimatedProps(() => {
|
|
584
|
+
// Grown about its centre, to match the ribbons meeting it.
|
|
585
|
+
const grown = extent * progress(reveal.value, from, to);
|
|
586
|
+
return {
|
|
587
|
+
y: centre - grown / 2,
|
|
588
|
+
height: grown,
|
|
589
|
+
opacity: settled.value
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
return /*#__PURE__*/_jsx(AnimatedRect, {
|
|
593
|
+
animatedProps: animatedProps,
|
|
594
|
+
x: x,
|
|
595
|
+
width: width,
|
|
596
|
+
rx: radius,
|
|
597
|
+
fill: fill
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* The names, and the press targets that go with them.
|
|
602
|
+
*
|
|
603
|
+
* Outside the bars rather than on them. A node's bar is as thick as it was
|
|
604
|
+
* asked to be — ten points by default — and no name fits inside ten points, so
|
|
605
|
+
* putting the name on the bar means widening every bar to suit the longest
|
|
606
|
+
* label and losing the width the ribbons need.
|
|
607
|
+
*
|
|
608
|
+
* Which side a name goes on is decided by the column: the last column reads
|
|
609
|
+
* inwards from the right edge, everything else outwards to the right. So the
|
|
610
|
+
* names stay inside the chart's box at both ends, instead of the leftmost and
|
|
611
|
+
* rightmost ones being clipped.
|
|
612
|
+
*
|
|
613
|
+
* The target is the label's row, not the bar. A node worth one percent of the
|
|
614
|
+
* flow is a two-point sliver and cannot be hit; the row it sits in can, and it
|
|
615
|
+
* is padded out to a proper target where the sliver is smaller than one.
|
|
616
|
+
*/
|
|
617
|
+
function SankeyChartLabels({
|
|
618
|
+
className,
|
|
619
|
+
formatValue,
|
|
620
|
+
showValue = false,
|
|
621
|
+
minHeight = 6
|
|
622
|
+
}) {
|
|
623
|
+
const {
|
|
624
|
+
layout,
|
|
625
|
+
nodes,
|
|
626
|
+
width,
|
|
627
|
+
height,
|
|
628
|
+
status,
|
|
629
|
+
activeId,
|
|
630
|
+
setActiveId,
|
|
631
|
+
labelFor
|
|
632
|
+
} = useChart('SankeyChart.Labels');
|
|
633
|
+
if (status === 'loading' || !layout.nodes.length) return null;
|
|
634
|
+
const format = formatValue ?? (value => compactNumber(value));
|
|
635
|
+
|
|
636
|
+
/*
|
|
637
|
+
* Where the neighbouring columns sit, in points across the plot.
|
|
638
|
+
*
|
|
639
|
+
* A name's row needs a bound on both sides or it runs the width of the chart
|
|
640
|
+
* and covers every row it crosses — and since the rows are absolutely
|
|
641
|
+
* positioned siblings, the last one drawn takes the touch. That is a tap on
|
|
642
|
+
* one name selecting a node two columns away, which is worse than a small
|
|
643
|
+
* target because it is wrong rather than merely hard.
|
|
644
|
+
*/
|
|
645
|
+
const edges = [];
|
|
646
|
+
for (const placed of layout.nodes) {
|
|
647
|
+
if (!edges.includes(placed.x0)) edges.push(placed.x0);
|
|
648
|
+
if (!edges.includes(placed.x1)) edges.push(placed.x1);
|
|
649
|
+
}
|
|
650
|
+
edges.sort((a, b) => a - b);
|
|
651
|
+
const nextEdge = x => edges.find(edge => edge > x + 1e-6);
|
|
652
|
+
const previousEdge = x => {
|
|
653
|
+
let found;
|
|
654
|
+
for (const edge of edges) if (edge < x - 1e-6) found = edge;
|
|
655
|
+
return found;
|
|
656
|
+
};
|
|
657
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
658
|
+
children: layout.nodes.map(node => {
|
|
659
|
+
const datum = nodes[node.index];
|
|
660
|
+
if (!datum) return null;
|
|
661
|
+
const extent = node.y1 - node.y0;
|
|
662
|
+
if (extent < minHeight) return null;
|
|
663
|
+
|
|
664
|
+
/*
|
|
665
|
+
* Which side the name goes on is decided by where the bar actually is,
|
|
666
|
+
* not by which column it belongs to. Under a right-to-left layout the
|
|
667
|
+
* finished diagram is mirrored, so the last column is the one on the
|
|
668
|
+
* left — reading the side off the column number there puts every name
|
|
669
|
+
* in a box of zero width and the chart loses all of them.
|
|
670
|
+
*/
|
|
671
|
+
const after = nextEdge(node.x1);
|
|
672
|
+
const before = previousEdge(node.x0);
|
|
673
|
+
const trailing = after === undefined;
|
|
674
|
+
const name = labelFor(node.id);
|
|
675
|
+
const value = format(node.value, datum);
|
|
676
|
+
const selected = activeId === node.id;
|
|
677
|
+
|
|
678
|
+
/*
|
|
679
|
+
* A sliver's row is padded out to a real target rather than drawn
|
|
680
|
+
* taller — growing the row would push it over its neighbours, and two
|
|
681
|
+
* overlapping targets are worse than a small one.
|
|
682
|
+
*/
|
|
683
|
+
const slack = Math.max(0, (MIN_TARGET - extent) / 2);
|
|
684
|
+
const top = Math.max(0, Math.min(node.y0, height - extent));
|
|
685
|
+
return /*#__PURE__*/_jsxs(Pressable, {
|
|
686
|
+
accessibilityRole: "button",
|
|
687
|
+
accessibilityState: {
|
|
688
|
+
selected
|
|
689
|
+
},
|
|
690
|
+
accessibilityLabel: `${name}, ${value}`,
|
|
691
|
+
hitSlop: {
|
|
692
|
+
top: slack,
|
|
693
|
+
bottom: slack
|
|
694
|
+
},
|
|
695
|
+
onPress: () => setActiveId(selected ? null : node.id),
|
|
696
|
+
style: {
|
|
697
|
+
position: 'absolute',
|
|
698
|
+
top,
|
|
699
|
+
height: extent,
|
|
700
|
+
justifyContent: 'center',
|
|
701
|
+
/*
|
|
702
|
+
* Each row takes the half of its gap nearest its own bar, so the
|
|
703
|
+
* name leaving one column and the name arriving at the next can
|
|
704
|
+
* share the space between them without sharing a touch target.
|
|
705
|
+
*/
|
|
706
|
+
...(trailing ? (() => {
|
|
707
|
+
const from = before === undefined ? 0 : (before + node.x0) / 2;
|
|
708
|
+
return {
|
|
709
|
+
left: from,
|
|
710
|
+
width: Math.max(0, node.x0 - LABEL_GAP - from),
|
|
711
|
+
alignItems: 'flex-end'
|
|
712
|
+
};
|
|
713
|
+
})() : (() => {
|
|
714
|
+
const from = node.x1 + LABEL_GAP;
|
|
715
|
+
const to = after === undefined ? width : (node.x1 + after) / 2;
|
|
716
|
+
return {
|
|
717
|
+
left: from,
|
|
718
|
+
width: Math.max(0, to - from)
|
|
719
|
+
};
|
|
720
|
+
})())
|
|
721
|
+
},
|
|
722
|
+
className: cn(className),
|
|
723
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
724
|
+
size: "xs",
|
|
725
|
+
weight: selected ? 'bold' : 'medium',
|
|
726
|
+
numberOfLines: 1,
|
|
727
|
+
style: {
|
|
728
|
+
textAlign: trailing ? 'right' : 'left'
|
|
729
|
+
},
|
|
730
|
+
children: name
|
|
731
|
+
}), showValue ? /*#__PURE__*/_jsx(Text, {
|
|
732
|
+
size: "xs",
|
|
733
|
+
muted: true,
|
|
734
|
+
numberOfLines: 1,
|
|
735
|
+
style: {
|
|
736
|
+
textAlign: trailing ? 'right' : 'left'
|
|
737
|
+
},
|
|
738
|
+
children: value
|
|
739
|
+
}) : null]
|
|
740
|
+
}, node.id);
|
|
741
|
+
})
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
SankeyChartLabels.displayName = 'SankeyChart.Labels';
|
|
745
|
+
SankeyChartLabels.slot = 'overlay';
|
|
746
|
+
/**
|
|
747
|
+
* What the selected node carries: the total through it, and what that total is
|
|
748
|
+
* made of at each end.
|
|
749
|
+
*
|
|
750
|
+
* In and out are shown separately because they are the two readings a flow
|
|
751
|
+
* diagram is for, and they are only the same number when nothing was lost. A
|
|
752
|
+
* node where they differ is the interesting one on the whole chart, and a
|
|
753
|
+
* single total would hide exactly that.
|
|
754
|
+
*
|
|
755
|
+
* Anchored beside the node and clamped to the plot, so it never leaves the box
|
|
756
|
+
* it belongs to — a card half off the edge of a phone is a card nobody can read.
|
|
757
|
+
*/
|
|
758
|
+
function SankeyChartTooltip({
|
|
759
|
+
className,
|
|
760
|
+
formatValue
|
|
761
|
+
}) {
|
|
762
|
+
const {
|
|
763
|
+
layout,
|
|
764
|
+
width,
|
|
765
|
+
height,
|
|
766
|
+
labelFor
|
|
767
|
+
} = useChart('SankeyChart.Tooltip');
|
|
768
|
+
const {
|
|
769
|
+
activeId,
|
|
770
|
+
activeValue,
|
|
771
|
+
incoming,
|
|
772
|
+
outgoing
|
|
773
|
+
} = useSankeyChart();
|
|
774
|
+
if (!activeId) return null;
|
|
775
|
+
const node = layout.nodes.find(placed => placed.id === activeId);
|
|
776
|
+
if (!node) return null;
|
|
777
|
+
const format = formatValue ?? compactNumber;
|
|
778
|
+
const CARD = 132;
|
|
779
|
+
const trailing = node.x1 + LABEL_GAP + CARD > width;
|
|
780
|
+
const left = trailing ? Math.max(0, node.x0 - LABEL_GAP - CARD) : Math.min(node.x1 + LABEL_GAP, Math.max(0, width - CARD));
|
|
781
|
+
const centre = (node.y0 + node.y1) / 2;
|
|
782
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
783
|
+
pointerEvents: "none",
|
|
784
|
+
style: {
|
|
785
|
+
position: 'absolute',
|
|
786
|
+
left,
|
|
787
|
+
width: CARD,
|
|
788
|
+
top: Math.max(0, Math.min(centre - 34, height - 68))
|
|
789
|
+
},
|
|
790
|
+
className: cn('gap-0.5 rounded-lg border border-border bg-background px-2.5 py-2 shadow-sm', className),
|
|
791
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
792
|
+
size: "xs",
|
|
793
|
+
weight: "bold",
|
|
794
|
+
numberOfLines: 1,
|
|
795
|
+
children: labelFor(activeId)
|
|
796
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
797
|
+
size: "xs",
|
|
798
|
+
weight: "semibold",
|
|
799
|
+
numberOfLines: 1,
|
|
800
|
+
children: format(activeValue)
|
|
801
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
802
|
+
size: "xs",
|
|
803
|
+
muted: true,
|
|
804
|
+
numberOfLines: 1,
|
|
805
|
+
children: `In ${format(incoming)} · Out ${format(outgoing)}`
|
|
806
|
+
})]
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
SankeyChartTooltip.displayName = 'SankeyChart.Tooltip';
|
|
810
|
+
SankeyChartTooltip.slot = 'overlay';
|
|
811
|
+
/**
|
|
812
|
+
* The loading state: a few plain bars and the ribbons between them, carrying no
|
|
813
|
+
* values.
|
|
814
|
+
*
|
|
815
|
+
* Every bar the same height and every ribbon the same thickness, deliberately.
|
|
816
|
+
* A placeholder with varied thicknesses would be an invented routing, and a
|
|
817
|
+
* reader cannot tell an invented one from a real one until it changes under
|
|
818
|
+
* them — which is worse than showing nothing, because it is showing something
|
|
819
|
+
* wrong.
|
|
820
|
+
*/
|
|
821
|
+
function SankeyChartSkeleton({
|
|
822
|
+
color
|
|
823
|
+
}) {
|
|
824
|
+
const {
|
|
825
|
+
width,
|
|
826
|
+
height,
|
|
827
|
+
curve,
|
|
828
|
+
status
|
|
829
|
+
} = useChart('SankeyChart.Skeleton');
|
|
830
|
+
const token = useCSSVariable('--color-skeleton');
|
|
831
|
+
const fill = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
832
|
+
|
|
833
|
+
// Held through the fade rather than to the frame the data lands, so the plain
|
|
834
|
+
// shape dissolves under the real one growing across it instead of leaving a
|
|
835
|
+
// blank panel between the two.
|
|
836
|
+
const {
|
|
837
|
+
mounted,
|
|
838
|
+
opacity
|
|
839
|
+
} = useSkeletonHandoff(status === 'loading');
|
|
840
|
+
const animatedProps = useAnimatedProps(() => ({
|
|
841
|
+
opacity: opacity.value
|
|
842
|
+
}));
|
|
843
|
+
const shape = useMemo(() => {
|
|
844
|
+
if (width <= 0 || height <= 0) return null;
|
|
845
|
+
const bar = DEFAULT_NODE_WIDTH;
|
|
846
|
+
const step = (width - bar) / Math.max(SKELETON_COLUMNS - 1, 1);
|
|
847
|
+
const band = height / 3;
|
|
848
|
+
const columns = Array.from({
|
|
849
|
+
length: SKELETON_COLUMNS
|
|
850
|
+
}, (_, i) => i * step);
|
|
851
|
+
const ribbons = [];
|
|
852
|
+
for (let i = 0; i < SKELETON_COLUMNS - 1; i += 1) {
|
|
853
|
+
const from = columns[i] + bar;
|
|
854
|
+
const to = columns[i + 1];
|
|
855
|
+
ribbons.push(flowPath(from, height / 3, to, height / 3, band * 0.5, curve));
|
|
856
|
+
ribbons.push(flowPath(from, height * 2 / 3, to, height * 2 / 3, band * 0.5, curve));
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
bar,
|
|
860
|
+
band,
|
|
861
|
+
columns,
|
|
862
|
+
ribbons
|
|
863
|
+
};
|
|
864
|
+
}, [width, height, curve]);
|
|
865
|
+
if (!mounted || !shape) return null;
|
|
866
|
+
return /*#__PURE__*/_jsxs(AnimatedG, {
|
|
867
|
+
animatedProps: animatedProps,
|
|
868
|
+
children: [shape.ribbons.map((d, index) => /*#__PURE__*/_jsx(Path, {
|
|
869
|
+
d: d,
|
|
870
|
+
fill: fill,
|
|
871
|
+
fillOpacity: 0.5
|
|
872
|
+
}, `ribbon-${index}`)), shape.columns.map((x, index) => /*#__PURE__*/_jsx(Rect, {
|
|
873
|
+
x: x,
|
|
874
|
+
y: height / 6,
|
|
875
|
+
width: shape.bar,
|
|
876
|
+
height: height * 2 / 3,
|
|
877
|
+
rx: 2,
|
|
878
|
+
fill: fill
|
|
879
|
+
}, `bar-${index}`))]
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
SankeyChartSkeleton.displayName = 'SankeyChart.Skeleton';
|
|
883
|
+
SankeyChartSkeleton.slot = 'svg';
|
|
884
|
+
/**
|
|
885
|
+
* The strip above the diagram: what the flow is of and what it totals.
|
|
886
|
+
*
|
|
887
|
+
* The value is not derived even though there are sources to add up, because the
|
|
888
|
+
* formatting is not the chart's to guess: 128400 is a count, a currency or a
|
|
889
|
+
* rate depending on what was routed.
|
|
890
|
+
*/
|
|
891
|
+
function SankeyChartHeader({
|
|
892
|
+
className,
|
|
893
|
+
title,
|
|
894
|
+
value,
|
|
895
|
+
caption,
|
|
896
|
+
children,
|
|
897
|
+
...props
|
|
898
|
+
}) {
|
|
899
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
900
|
+
...props,
|
|
901
|
+
className: cn('flex-row items-start justify-between gap-3 pb-3', className),
|
|
902
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
903
|
+
className: "flex-1 gap-0.5",
|
|
904
|
+
children: [title ? /*#__PURE__*/_jsx(Text, {
|
|
905
|
+
size: "xs",
|
|
906
|
+
muted: true,
|
|
907
|
+
children: title
|
|
908
|
+
}) : null, value ? /*#__PURE__*/_jsx(Text, {
|
|
909
|
+
size: "xl",
|
|
910
|
+
weight: "bold",
|
|
911
|
+
children: value
|
|
912
|
+
}) : null, caption ? /*#__PURE__*/_jsx(Text, {
|
|
913
|
+
size: "xs",
|
|
914
|
+
muted: true,
|
|
915
|
+
children: caption
|
|
916
|
+
}) : null]
|
|
917
|
+
}), children ? /*#__PURE__*/_jsx(View, {
|
|
918
|
+
className: "max-w-[55%] shrink pt-1",
|
|
919
|
+
children: children
|
|
920
|
+
}) : null]
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
SankeyChartHeader.displayName = 'SankeyChart.Header';
|
|
924
|
+
SankeyChartHeader.slot = 'header';
|
|
925
|
+
export const SankeyChart = Object.assign(SankeyChartRoot, {
|
|
926
|
+
Header: SankeyChartHeader,
|
|
927
|
+
Links: SankeyChartLinks,
|
|
928
|
+
Nodes: SankeyChartNodes,
|
|
929
|
+
Labels: SankeyChartLabels,
|
|
930
|
+
Tooltip: SankeyChartTooltip,
|
|
931
|
+
Skeleton: SankeyChartSkeleton
|
|
932
|
+
});
|
|
933
|
+
//# sourceMappingURL=index.js.map
|