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.
Files changed (46) hide show
  1. package/README.md +2 -0
  2. package/lib/module/components/animated-badge/index.js +24 -2
  3. package/lib/module/components/animated-badge/index.js.map +1 -1
  4. package/lib/module/components/bump-chart/index.js +1236 -0
  5. package/lib/module/components/bump-chart/index.js.map +1 -0
  6. package/lib/module/components/compare/index.js +576 -0
  7. package/lib/module/components/compare/index.js.map +1 -0
  8. package/lib/module/components/mirror-area-chart/index.js +1113 -0
  9. package/lib/module/components/mirror-area-chart/index.js.map +1 -0
  10. package/lib/module/components/sankey-chart/index.js +933 -0
  11. package/lib/module/components/sankey-chart/index.js.map +1 -0
  12. package/lib/module/components/sankey-chart/sankey-layout.js +530 -0
  13. package/lib/module/components/sankey-chart/sankey-layout.js.map +1 -0
  14. package/lib/module/icons/index.js +15 -3
  15. package/lib/module/icons/index.js.map +1 -1
  16. package/lib/module/index.js +4 -0
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/utils/chart.js +101 -0
  19. package/lib/module/utils/chart.js.map +1 -1
  20. package/lib/typescript/src/components/animated-badge/index.d.ts +5 -0
  21. package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -1
  22. package/lib/typescript/src/components/bump-chart/index.d.ts +267 -0
  23. package/lib/typescript/src/components/bump-chart/index.d.ts.map +1 -0
  24. package/lib/typescript/src/components/compare/index.d.ts +183 -0
  25. package/lib/typescript/src/components/compare/index.d.ts.map +1 -0
  26. package/lib/typescript/src/components/mirror-area-chart/index.d.ts +263 -0
  27. package/lib/typescript/src/components/mirror-area-chart/index.d.ts.map +1 -0
  28. package/lib/typescript/src/components/sankey-chart/index.d.ts +307 -0
  29. package/lib/typescript/src/components/sankey-chart/index.d.ts.map +1 -0
  30. package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts +107 -0
  31. package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts.map +1 -0
  32. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  33. package/lib/typescript/src/index.d.ts +4 -0
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/chart.d.ts +40 -0
  36. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/components/animated-badge/index.tsx +35 -2
  39. package/src/components/bump-chart/index.tsx +1455 -0
  40. package/src/components/compare/index.tsx +650 -0
  41. package/src/components/mirror-area-chart/index.tsx +1333 -0
  42. package/src/components/sankey-chart/index.tsx +1165 -0
  43. package/src/components/sankey-chart/sankey-layout.ts +645 -0
  44. package/src/icons/index.tsx +20 -6
  45. package/src/index.ts +62 -0
  46. package/src/utils/chart.ts +116 -0
@@ -0,0 +1,1236 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * BumpChart — how a set of things ranked against each other over time.
5
+ *
6
+ * ```tsx
7
+ * <BumpChart data={standings} xDataKey="week" defaultHighlight="harbour">
8
+ * <BumpChart.Grid />
9
+ * <BumpChart.Line dataKey="harbour" label="Harbour" />
10
+ * <BumpChart.Line dataKey="northside" label="Northside" />
11
+ * <BumpChart.YAxis />
12
+ * <BumpChart.XAxis />
13
+ * <BumpChart.Labels />
14
+ * <BumpChart.Tooltip />
15
+ * </BumpChart>
16
+ * ```
17
+ *
18
+ * ## Positions, not values
19
+ *
20
+ * The y-axis is the rank, so the rows are evenly spaced whatever the gap
21
+ * between the underlying scores. That is the point of the chart: who passed
22
+ * whom, and when. A line chart of the same scores answers how far apart they
23
+ * were, and loses the order wherever two lines run close together.
24
+ *
25
+ * Pass the ranks directly, or pass the scores with `values="score"` and each
26
+ * column is ranked for you, highest first. `bumpRanks` is exported so a header
27
+ * or a table beside the chart can read the same order the chart drew.
28
+ *
29
+ * ## One line at a time
30
+ *
31
+ * Past three or four series, every line in its own colour is a tangle. The
32
+ * chart reads best with one line picked out: `highlight` draws that one in its
33
+ * colour and on top, and the rest in a muted grey. Tapping a name in
34
+ * `BumpChart.Labels` picks that line, and tapping it again clears it.
35
+ */
36
+ import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
37
+ import { Pressable, StyleSheet, View } from 'react-native';
38
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
39
+ import Animated, { Easing, cancelAnimation, runOnJS, runOnUI, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
40
+ import Svg, { Defs, G, Line as SvgLine, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
41
+ import { useCSSVariable } from 'uniwind';
42
+ import { Text } from "../../primitives/text.js";
43
+ import { ChartAccessibilityData } from "../../primitives/chart-accessibility.js";
44
+ import { useControllableState } from "../../primitives/controllable-state.js";
45
+ import { finiteChartNumber } from "../../primitives/finite-chart.js";
46
+ import { bumpSegment, colorStop, dotsPath, useSeriesColor, xOf } from "../../utils/chart.js";
47
+ import { cn } from "../../utils/cn.js";
48
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
49
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
50
+ const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
51
+ const PADDING = {
52
+ top: 8,
53
+ right: 10,
54
+ bottom: 32,
55
+ left: 10
56
+ };
57
+
58
+ /** Left gutter reserved when a `YAxis` is present, for `#1`…`#12`. */
59
+ const Y_AXIS_WIDTH = 34;
60
+
61
+ /** Gap between the rank labels and the first column. */
62
+ const Y_AXIS_GUTTER = 8;
63
+
64
+ /** Gap between the last column and the series names. */
65
+ const LABELS_GAP = 12;
66
+
67
+ /** Box each x label is centred in. */
68
+ const POINT_LABEL_WIDTH = 56;
69
+
70
+ /** Line height of an `xs` label, for centring one on the row it names. */
71
+ const AXIS_LABEL_HEIGHT = 16;
72
+
73
+ /** Width of the scrub readout. */
74
+ const READOUT_WIDTH = 148;
75
+
76
+ /** How far the lines that are not picked out fade back. */
77
+ const DIMMED_OPACITY = 0.5;
78
+
79
+ /** Ranks per series key, one entry per row. `null` where a series has none. */
80
+
81
+ /**
82
+ * Each series' rank in every row.
83
+ *
84
+ * With `values: 'rank'` the rows already hold ranks and they are only
85
+ * validated. With `values: 'score'` each row is ranked highest first; a tie
86
+ * keeps the order the keys are passed in, so two equal scores never draw on
87
+ * top of each other.
88
+ */
89
+ export function bumpRanks(data, keys, options = {}) {
90
+ const ranks = {};
91
+ for (const key of keys) ranks[key] = [];
92
+ for (const row of data) {
93
+ if (options.values === 'score') {
94
+ const scored = keys.map((key, order) => ({
95
+ key,
96
+ order,
97
+ score: finiteChartNumber(row[key])
98
+ })).filter(entry => entry.score !== undefined).sort((a, b) => b.score - a.score || a.order - b.order);
99
+ const place = new Map(scored.map((entry, index) => [entry.key, index + 1]));
100
+ for (const key of keys) ranks[key].push(place.get(key) ?? null);
101
+ } else {
102
+ for (const key of keys) {
103
+ const rank = finiteChartNumber(row[key]);
104
+ ranks[key].push(rank !== undefined && rank >= 1 ? rank : null);
105
+ }
106
+ }
107
+ }
108
+ return ranks;
109
+ }
110
+
111
+ /**
112
+ * Ranks as a fraction of the plot's height: `0` is the top row, `1` the
113
+ * bottom. Stored this way so a line can travel between two charts with a
114
+ * different number of rows without a jump.
115
+ */
116
+ function rankFractions(ranks, rows) {
117
+ const out = {};
118
+ for (const key of Object.keys(ranks)) {
119
+ out[key] = ranks[key].map(rank => rank === null ? null : rows <= 1 ? 0.5 : (rank - 1) / (rows - 1));
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /** A line's points at `progress` of the way between two sets of positions. */
125
+ function pointsAt(key, from, to, progress, plot) {
126
+ 'worklet';
127
+
128
+ const target = to[key];
129
+ if (!target) return [];
130
+ const origin = from[key];
131
+ const runs = [];
132
+ let run = [];
133
+ for (let i = 0; i < target.length; i += 1) {
134
+ const end = target[i];
135
+ if (end === null || end === undefined) {
136
+ if (run.length) runs.push(run);
137
+ run = [];
138
+ continue;
139
+ }
140
+ const start = origin?.[i];
141
+ const fraction = start === null || start === undefined ? end : start + (end - start) * progress;
142
+ run.push({
143
+ x: xOf(i, target.length, plot),
144
+ y: plot.top + fraction * plot.height
145
+ });
146
+ }
147
+ if (run.length) runs.push(run);
148
+ return runs;
149
+ }
150
+ const BumpChartContext = /*#__PURE__*/createContext(null);
151
+ function useChart(component) {
152
+ const context = useContext(BumpChartContext);
153
+ if (!context) {
154
+ throw new Error(`${component} must be used within a <BumpChart>`);
155
+ }
156
+ return context;
157
+ }
158
+
159
+ /** The row under the scrub, and which line is picked out. */
160
+ export function useBumpChart() {
161
+ const {
162
+ data,
163
+ activeIndexJS,
164
+ xDataKey,
165
+ ranks,
166
+ highlight,
167
+ setHighlight
168
+ } = useChart('useBumpChart');
169
+ return {
170
+ activeIndex: activeIndexJS,
171
+ activePoint: activeIndexJS >= 0 ? data[activeIndexJS] ?? null : null,
172
+ xDataKey,
173
+ ranks,
174
+ highlight,
175
+ setHighlight
176
+ };
177
+ }
178
+
179
+ /** Imperative handle: re-run the reveal on demand, for a "replay" control. */
180
+
181
+ function ChildSlot({
182
+ children
183
+ }) {
184
+ return /*#__PURE__*/_jsx(_Fragment, {
185
+ children: children
186
+ });
187
+ }
188
+ function partition(children) {
189
+ const svg = [];
190
+ const series = [];
191
+ const overlay = [];
192
+ const header = [];
193
+ Children.forEach(children, (child, index) => {
194
+ if (! /*#__PURE__*/isValidElement(child)) return;
195
+ const layer = child.type.layer ?? 'svg';
196
+ const slot = /*#__PURE__*/_jsx(ChildSlot, {
197
+ children: child
198
+ }, index);
199
+ const bucket = layer === 'header' ? header : layer === 'overlay' ? overlay : layer === 'series' ? series : svg;
200
+ bucket.push(slot);
201
+ });
202
+ return {
203
+ svg,
204
+ series,
205
+ overlay,
206
+ header
207
+ };
208
+ }
209
+
210
+ /**
211
+ * What the root needs to know about its children before it lays the plot out:
212
+ * whether an axis or the names want room at the sides, and the order the lines
213
+ * were declared in.
214
+ */
215
+ function survey(children) {
216
+ let hasYAxis = false;
217
+ let labelsWidth = 0;
218
+ const order = [];
219
+ Children.forEach(children, child => {
220
+ if (! /*#__PURE__*/isValidElement(child)) return;
221
+ const type = child.type;
222
+ const props = child.props;
223
+ if (type.axis === 'y') hasYAxis = true;
224
+ if (type.labels) labelsWidth = props.width ?? DEFAULT_LABELS_WIDTH;
225
+ if (type.line && typeof props.dataKey === 'string') order.push(props.dataKey);
226
+ });
227
+ return {
228
+ hasYAxis,
229
+ labelsWidth,
230
+ order
231
+ };
232
+ }
233
+ const BumpChartRoot = /*#__PURE__*/forwardRef(function BumpChartRoot({
234
+ className,
235
+ data,
236
+ xDataKey = 'date',
237
+ values = 'rank',
238
+ highlight: highlightProp,
239
+ defaultHighlight = null,
240
+ onHighlightChange,
241
+ status = 'ready',
242
+ aspectRatio = 1.7,
243
+ animationDuration = 700,
244
+ morphDuration = 500,
245
+ onActiveIndexChange,
246
+ accessible,
247
+ accessibilityLabel,
248
+ accessibilityHint,
249
+ accessibilityLabelForDatum,
250
+ onAccessibilityDatumPress,
251
+ compact = false,
252
+ children,
253
+ ...props
254
+ }, ref) {
255
+ const [size, setSize] = useState({
256
+ width: 0,
257
+ height: 0
258
+ });
259
+ const [registered, setRegistered] = useState([]);
260
+ const [activeIndexJS, setActiveIndexJS] = useState(-1);
261
+ const reveal = useSharedValue(0);
262
+ const morph = useSharedValue(1);
263
+ const activeIndex = useSharedValue(-1);
264
+ const reducedMotion = useReducedMotion();
265
+ const {
266
+ value: highlight,
267
+ setValue: setHighlight
268
+ } = useControllableState({
269
+ value: highlightProp,
270
+ defaultValue: defaultHighlight,
271
+ onChange: onHighlightChange
272
+ });
273
+ const registerSeries = useMemo(() => entry => setRegistered(current => {
274
+ const existing = current.find(item => item.key === entry.key);
275
+ if (existing && existing.color === entry.color && existing.label === entry.label && existing.strokeWidth === entry.strokeWidth && existing.showDots === entry.showDots) {
276
+ return current;
277
+ }
278
+ return [...current.filter(item => item.key !== entry.key), entry];
279
+ }), []);
280
+ const unregisterSeries = useMemo(() => key => setRegistered(current => current.filter(item => item.key !== key)), []);
281
+ const {
282
+ hasYAxis,
283
+ labelsWidth,
284
+ order
285
+ } = survey(children);
286
+ const orderKey = order.join('|');
287
+
288
+ /*
289
+ * Declaration order, not registration order. A line re-registers when its
290
+ * colour resolves or its label changes, which would otherwise move it to the
291
+ * end of the list — and in `score` mode the order breaks ties.
292
+ */
293
+ const series = useMemo(() => {
294
+ const declared = orderKey ? orderKey.split('|') : [];
295
+ const position = key => {
296
+ const index = declared.indexOf(key);
297
+ return index === -1 ? declared.length : index;
298
+ };
299
+ return [...registered].sort((a, b) => position(a.key) - position(b.key));
300
+ }, [registered, orderKey]);
301
+ const pad = compact ? {
302
+ top: 6,
303
+ right: 6,
304
+ bottom: 6,
305
+ left: 6
306
+ } : {
307
+ ...PADDING,
308
+ left: hasYAxis ? Y_AXIS_WIDTH + Y_AXIS_GUTTER : PADDING.left,
309
+ right: labelsWidth ? labelsWidth + LABELS_GAP : PADDING.right
310
+ };
311
+ const plot = {
312
+ left: pad.left,
313
+ top: pad.top,
314
+ width: Math.max(size.width - pad.left - pad.right, 0),
315
+ height: Math.max(size.height - pad.top - pad.bottom, 0)
316
+ };
317
+ const seriesKeys = series.map(item => item.key).join('|');
318
+ const ranks = useMemo(() => bumpRanks(data, seriesKeys ? seriesKeys.split('|') : [], {
319
+ values
320
+ }), [data, seriesKeys, values]);
321
+
322
+ /** How many places there are: the lowest rank anybody holds. */
323
+ const rows = useMemo(() => {
324
+ let most = 0;
325
+ for (const key of Object.keys(ranks)) {
326
+ for (const rank of ranks[key]) {
327
+ if (rank !== null && rank > most) most = rank;
328
+ }
329
+ }
330
+ return Math.max(most, 1);
331
+ }, [ranks]);
332
+ const fractions = useMemo(() => rankFractions(ranks, rows), [ranks, rows]);
333
+ const from = useSharedValue(fractions);
334
+ const to = useSharedValue(fractions);
335
+
336
+ /*
337
+ * A change of data moves every line from where it is drawn now to its new
338
+ * place. "Where it is drawn now" is read on the UI thread, inside the same
339
+ * worklet that starts the move, so a change landing mid-move carries on from
340
+ * the lines' actual positions instead of snapping back to the last target.
341
+ */
342
+ const loading = status === 'loading';
343
+ const settledOnce = useRef(false);
344
+ useEffect(() => {
345
+ if (!settledOnce.current || reducedMotion) {
346
+ settledOnce.current = true;
347
+ from.value = fractions;
348
+ to.value = fractions;
349
+ morph.value = 1;
350
+ return;
351
+ }
352
+ runOnUI((next, duration) => {
353
+ 'worklet';
354
+
355
+ const progress = morph.value;
356
+ const current = {};
357
+ const previousTo = to.value;
358
+ const previousFrom = from.value;
359
+ for (const key of Object.keys(next)) {
360
+ const target = previousTo[key];
361
+ const origin = previousFrom[key];
362
+ current[key] = next[key].map((_unused, index) => {
363
+ const end = target?.[index];
364
+ if (end === null || end === undefined) return null;
365
+ const start = origin?.[index];
366
+ return start === null || start === undefined ? end : start + (end - start) * progress;
367
+ });
368
+ }
369
+ from.value = current;
370
+ to.value = next;
371
+ morph.value = 0;
372
+ morph.value = withTiming(1, {
373
+ duration,
374
+ easing: Easing.inOut(Easing.cubic)
375
+ });
376
+ })(fractions, morphDuration);
377
+ }, [fractions, reducedMotion, morphDuration, from, to, morph]);
378
+ const revealed = useRef(false);
379
+ const playReveal = useMemo(() => () => {
380
+ if (reducedMotion) {
381
+ reveal.value = 1;
382
+ return;
383
+ }
384
+ reveal.value = 0;
385
+ reveal.value = withTiming(1, {
386
+ duration: animationDuration,
387
+ easing: Easing.out(Easing.cubic)
388
+ });
389
+ }, [reducedMotion, animationDuration, reveal]);
390
+ useEffect(() => {
391
+ // Going back to `loading` arms the reveal again, so a refetched chart
392
+ // draws itself in rather than appearing complete on the frame it lands.
393
+ if (loading) {
394
+ revealed.current = false;
395
+ reveal.value = 0;
396
+ return;
397
+ }
398
+ if (revealed.current || plot.width <= 0 || !data.length) return;
399
+ revealed.current = true;
400
+ playReveal();
401
+ }, [loading, plot.width, data.length, playReveal, reveal]);
402
+ useImperativeHandle(ref, () => ({
403
+ replay: playReveal
404
+ }), [playReveal]);
405
+
406
+ /*
407
+ * The reveal widens past the plot's right edge by the dots' radius, so the
408
+ * last column's dots are not cut in half when it finishes.
409
+ */
410
+ const revealWidth = plot.width + 8;
411
+ const revealStyle = useAnimatedStyle(() => ({
412
+ width: revealWidth * reveal.value + 8
413
+ }));
414
+ const handleActiveIndex = useMemo(() => index => {
415
+ setActiveIndexJS(index);
416
+ onActiveIndexChange?.(index, index >= 0 ? data[index] ?? null : null);
417
+ }, [onActiveIndexChange, data]);
418
+ const onLayout = event => {
419
+ const {
420
+ width,
421
+ height
422
+ } = event.nativeEvent.layout;
423
+ setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
424
+ width,
425
+ height
426
+ });
427
+ props.onLayout?.(event);
428
+ };
429
+ const context = useMemo(() => ({
430
+ data,
431
+ xDataKey,
432
+ plot,
433
+ pad,
434
+ status,
435
+ series,
436
+ ranks,
437
+ rows,
438
+ registerSeries,
439
+ unregisterSeries,
440
+ from,
441
+ to,
442
+ morph,
443
+ highlight,
444
+ setHighlight,
445
+ activeIndex,
446
+ activeIndexJS,
447
+ setActiveIndexJS: handleActiveIndex
448
+ }),
449
+ // `plot` and `pad` are rebuilt every render from `size`, so they are
450
+ // compared by value.
451
+ // eslint-disable-next-line react-hooks/exhaustive-deps
452
+ [data, xDataKey, plot.width, plot.height, plot.left, plot.top, pad.right, pad.bottom, status, series, ranks, rows, registerSeries, unregisterSeries, from, to, morph, highlight, setHighlight, activeIndex, activeIndexJS, handleActiveIndex]);
453
+ const {
454
+ svg,
455
+ series: seriesLayer,
456
+ overlay,
457
+ header
458
+ } = partition(children);
459
+ return /*#__PURE__*/_jsx(BumpChartContext.Provider, {
460
+ value: context,
461
+ children: /*#__PURE__*/_jsxs(View, {
462
+ ...props,
463
+ style: props.style,
464
+ className: cn('w-full', className),
465
+ children: [header, /*#__PURE__*/_jsx(ChartAccessibilityData, {
466
+ chart: "Bump chart",
467
+ data: data,
468
+ disabled: accessible === false || loading,
469
+ accessibilityLabel: accessibilityLabel,
470
+ accessibilityHint: accessibilityHint,
471
+ accessibilityLabelForDatum: accessibilityLabelForDatum,
472
+ onAccessibilityDatumPress: onAccessibilityDatumPress,
473
+ valueOf: datum => {
474
+ const index = data.indexOf(datum);
475
+ return [[xDataKey, datum[xDataKey]], ...series.map(item => {
476
+ const rank = ranks[item.key]?.[index];
477
+ return [item.label, rank == null ? null : `rank ${rank}`];
478
+ })];
479
+ }
480
+ }), /*#__PURE__*/_jsx(View, {
481
+ onLayout: onLayout,
482
+ style: {
483
+ aspectRatio
484
+ },
485
+ className: "w-full",
486
+ accessibilityElementsHidden: true,
487
+ importantForAccessibility: "no-hide-descendants",
488
+ children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
489
+ children: [/*#__PURE__*/_jsx(Svg, {
490
+ width: "100%",
491
+ height: "100%",
492
+ style: StyleSheet.absoluteFill,
493
+ children: svg
494
+ }), loading ? null : /*#__PURE__*/_jsx(Animated.View, {
495
+ pointerEvents: "none",
496
+ style: [{
497
+ position: 'absolute',
498
+ top: 0,
499
+ bottom: 0,
500
+ left: plot.left - 8,
501
+ overflow: 'hidden'
502
+ }, revealStyle],
503
+ children: /*#__PURE__*/_jsxs(Svg, {
504
+ width: size.width,
505
+ height: size.height,
506
+ style: {
507
+ position: 'absolute',
508
+ top: 0,
509
+ left: -(plot.left - 8)
510
+ },
511
+ children: [seriesLayer, /*#__PURE__*/_jsx(BumpLines, {})]
512
+ })
513
+ }), overlay]
514
+ }) : null
515
+ })]
516
+ })
517
+ });
518
+ });
519
+ BumpChartRoot.displayName = 'BumpChart';
520
+
521
+ /* -------------------------------------------------------------------------- */
522
+ /* SVG layer */
523
+ /* -------------------------------------------------------------------------- */
524
+
525
+ /** Guides down each column, so a dot can be read against the label below it. */
526
+ function BumpChartGrid({
527
+ vertical = true,
528
+ horizontal = false,
529
+ color,
530
+ dashArray = '3,5',
531
+ opacity = 1
532
+ }) {
533
+ const {
534
+ plot,
535
+ data,
536
+ rows
537
+ } = useChart('BumpChart.Grid');
538
+ const token = useCSSVariable('--color-border');
539
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
540
+ return /*#__PURE__*/_jsxs(G, {
541
+ opacity: opacity,
542
+ children: [vertical ? data.map((_unused, index) => {
543
+ const x = xOf(index, data.length, plot);
544
+ return /*#__PURE__*/_jsx(SvgLine, {
545
+ x1: x,
546
+ x2: x,
547
+ y1: plot.top,
548
+ y2: plot.top + plot.height,
549
+ stroke: stroke,
550
+ strokeDasharray: dashArray,
551
+ strokeWidth: 1
552
+ }, `x${index}`);
553
+ }) : null, horizontal ? Array.from({
554
+ length: rows
555
+ }, (_unused, index) => {
556
+ const y = plot.top + (rows <= 1 ? plot.height / 2 : plot.height * index / (rows - 1));
557
+ return /*#__PURE__*/_jsx(SvgLine, {
558
+ x1: plot.left,
559
+ x2: plot.left + plot.width,
560
+ y1: y,
561
+ y2: y,
562
+ stroke: stroke,
563
+ strokeDasharray: dashArray,
564
+ strokeWidth: 1
565
+ }, `y${index}`);
566
+ }) : null]
567
+ });
568
+ }
569
+ BumpChartGrid.displayName = 'BumpChart.Grid';
570
+ BumpChartGrid.layer = 'svg';
571
+ /**
572
+ * One series. It draws nothing itself: the lines are drawn together, so the
573
+ * one picked out can be drawn last and sit on top of every crossing.
574
+ */
575
+ function BumpChartLine({
576
+ dataKey,
577
+ label,
578
+ color,
579
+ colorIndex = 1,
580
+ strokeWidth = 1.5,
581
+ showDots = true
582
+ }) {
583
+ const {
584
+ registerSeries,
585
+ unregisterSeries
586
+ } = useChart('BumpChart.Line');
587
+ const stroke = useSeriesColor(color, colorIndex);
588
+ const name = label ?? dataKey;
589
+ useEffect(() => {
590
+ registerSeries({
591
+ key: dataKey,
592
+ color: stroke,
593
+ label: name,
594
+ strokeWidth,
595
+ showDots
596
+ });
597
+ }, [dataKey, stroke, name, strokeWidth, showDots, registerSeries]);
598
+ useEffect(() => () => unregisterSeries(dataKey), [dataKey, unregisterSeries]);
599
+ return null;
600
+ }
601
+ BumpChartLine.displayName = 'BumpChart.Line';
602
+ BumpChartLine.layer = 'series';
603
+ // Read by the root, for the order the lines were declared in.
604
+ BumpChartLine.line = true;
605
+
606
+ /** Every line, the picked-out one last. */
607
+ function BumpLines() {
608
+ const {
609
+ series,
610
+ highlight
611
+ } = useChart('BumpChart');
612
+ const muted = useCSSVariable('--color-muted-foreground');
613
+ const mutedColor = typeof muted === 'string' ? muted : '#737373';
614
+ const ordered = highlight ? [...series.filter(item => item.key !== highlight), ...series.filter(item => item.key === highlight)] : series;
615
+ return /*#__PURE__*/_jsx(G, {
616
+ children: ordered.map(item => {
617
+ const picked = highlight === item.key;
618
+ const dimmed = highlight !== null && !picked;
619
+ return /*#__PURE__*/_jsx(BumpLine, {
620
+ series: item,
621
+ color: dimmed ? mutedColor : item.color,
622
+ opacity: dimmed ? DIMMED_OPACITY : 1,
623
+ strokeWidth: picked ? item.strokeWidth + 1 : item.strokeWidth,
624
+ dotRadius: picked ? 3.5 : 2.5
625
+ }, item.key);
626
+ })
627
+ });
628
+ }
629
+ function BumpLine({
630
+ series,
631
+ color,
632
+ opacity,
633
+ strokeWidth,
634
+ dotRadius
635
+ }) {
636
+ const {
637
+ plot,
638
+ from,
639
+ to,
640
+ morph
641
+ } = useChart('BumpChart.Line');
642
+ const key = series.key;
643
+ const lineProps = useAnimatedProps(() => ({
644
+ d: pointsAt(key, from.value, to.value, morph.value, plot).map(run => bumpSegment(run)).join(' ')
645
+ }));
646
+ const dotProps = useAnimatedProps(() => {
647
+ let d = '';
648
+ for (const run of pointsAt(key, from.value, to.value, morph.value, plot)) {
649
+ d += dotsPath(run, dotRadius);
650
+ }
651
+ return {
652
+ d
653
+ };
654
+ });
655
+ return /*#__PURE__*/_jsxs(G, {
656
+ opacity: opacity,
657
+ children: [/*#__PURE__*/_jsx(AnimatedPath, {
658
+ animatedProps: lineProps,
659
+ stroke: color,
660
+ strokeWidth: strokeWidth,
661
+ strokeLinecap: "round",
662
+ strokeLinejoin: "round",
663
+ fill: "none"
664
+ }), series.showDots ? /*#__PURE__*/_jsx(AnimatedPath, {
665
+ animatedProps: dotProps,
666
+ fill: color
667
+ }) : null]
668
+ });
669
+ }
670
+ /**
671
+ * The loading state: a thin bar on every rank row with a highlight travelling
672
+ * across them. Flat, so it says where the lines will be and nothing about
673
+ * where they go.
674
+ */
675
+ function BumpChartSkeleton({
676
+ duration = 1400,
677
+ color,
678
+ rows: rowsProp
679
+ }) {
680
+ const {
681
+ plot,
682
+ status,
683
+ rows: dataRows
684
+ } = useChart('BumpChart.Skeleton');
685
+ const token = useCSSVariable('--color-skeleton');
686
+ const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
687
+ const highlight = useSeriesColor(undefined, 1);
688
+ const rows = rowsProp ?? Math.max(dataRows, 4);
689
+ const sweep = useSharedValue(0);
690
+ const reducedMotion = useReducedMotion();
691
+ const loading = status === 'loading';
692
+ useEffect(() => {
693
+ if (!loading || reducedMotion) {
694
+ cancelAnimation(sweep);
695
+ sweep.value = 0;
696
+ return;
697
+ }
698
+ sweep.value = 0;
699
+ sweep.value = withRepeat(withTiming(1, {
700
+ duration,
701
+ easing: Easing.linear
702
+ }), -1, false);
703
+ return () => cancelAnimation(sweep);
704
+ }, [loading, reducedMotion, duration, sweep]);
705
+ const animatedProps = useAnimatedProps(() => ({
706
+ x1: `${(sweep.value * 1.4 - 0.4) * 100}%`,
707
+ x2: `${(sweep.value * 1.4 - 0.4 + 0.4) * 100}%`
708
+ }));
709
+ if (!loading) return null;
710
+ const gradientId = 'panelui-bump-skeleton';
711
+ const thickness = 4;
712
+ return /*#__PURE__*/_jsxs(G, {
713
+ children: [/*#__PURE__*/_jsx(Defs, {
714
+ children: /*#__PURE__*/_jsxs(AnimatedLinearGradient, {
715
+ id: gradientId,
716
+ animatedProps: animatedProps,
717
+ y1: "0",
718
+ y2: "0",
719
+ children: [/*#__PURE__*/_jsx(Stop, {
720
+ offset: "0",
721
+ ...colorStop(base)
722
+ }), /*#__PURE__*/_jsx(Stop, {
723
+ offset: "0.5",
724
+ stopColor: highlight,
725
+ stopOpacity: 0.3
726
+ }), /*#__PURE__*/_jsx(Stop, {
727
+ offset: "1",
728
+ ...colorStop(base)
729
+ })]
730
+ })
731
+ }), Array.from({
732
+ length: rows
733
+ }, (_unused, index) => {
734
+ const y = plot.top + (rows <= 1 ? plot.height / 2 : plot.height * index / (rows - 1));
735
+ return /*#__PURE__*/_jsx(Rect, {
736
+ x: plot.left,
737
+ y: y - thickness / 2,
738
+ width: plot.width,
739
+ height: thickness,
740
+ rx: thickness / 2,
741
+ fill: `url(#${gradientId})`
742
+ }, index);
743
+ })]
744
+ });
745
+ }
746
+ BumpChartSkeleton.displayName = 'BumpChart.Skeleton';
747
+ BumpChartSkeleton.layer = 'svg';
748
+
749
+ /* -------------------------------------------------------------------------- */
750
+ /* Overlay layer */
751
+ /* -------------------------------------------------------------------------- */
752
+
753
+ /** The column labels. Real text, so they follow the theme's font and text scaling. */
754
+ function BumpChartXAxis({
755
+ ticks = 4,
756
+ format,
757
+ className
758
+ }) {
759
+ const {
760
+ data,
761
+ xDataKey,
762
+ plot,
763
+ pad
764
+ } = useChart('BumpChart.XAxis');
765
+ const labels = useMemo(() => {
766
+ if (!data.length) return [];
767
+ const count = Math.min(ticks, data.length);
768
+ const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
769
+ return Array.from({
770
+ length: count
771
+ }, (_unused, index) => {
772
+ const dataIndex = Math.round(index * step);
773
+ const datum = data[dataIndex];
774
+ if (!datum) return null;
775
+ return {
776
+ key: dataIndex,
777
+ text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? '')
778
+ };
779
+ }).filter(label => label !== null);
780
+ }, [data, ticks, format, xDataKey]);
781
+ return /*#__PURE__*/_jsx(View, {
782
+ style: {
783
+ position: 'absolute',
784
+ inset: 0,
785
+ pointerEvents: 'none'
786
+ },
787
+ className: cn(className),
788
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
789
+ size: "xs",
790
+ muted: true,
791
+ numberOfLines: 1,
792
+ style: {
793
+ position: 'absolute',
794
+ bottom: 0,
795
+ left: Math.max(0, Math.min(xOf(label.key, data.length, plot) - POINT_LABEL_WIDTH / 2, plot.left + plot.width + pad.right - POINT_LABEL_WIDTH)),
796
+ width: POINT_LABEL_WIDTH,
797
+ textAlign: 'center'
798
+ },
799
+ children: label.text
800
+ }, label.key))
801
+ });
802
+ }
803
+ BumpChartXAxis.displayName = 'BumpChart.XAxis';
804
+ BumpChartXAxis.layer = 'overlay';
805
+ /** The ranks down the side, one on every row. */
806
+ function BumpChartYAxis({
807
+ format,
808
+ className
809
+ }) {
810
+ const {
811
+ plot,
812
+ rows
813
+ } = useChart('BumpChart.YAxis');
814
+ return /*#__PURE__*/_jsx(View, {
815
+ pointerEvents: "none",
816
+ style: {
817
+ position: 'absolute',
818
+ left: 0,
819
+ top: plot.top - AXIS_LABEL_HEIGHT / 2,
820
+ height: plot.height + AXIS_LABEL_HEIGHT,
821
+ width: Math.max(plot.left - Y_AXIS_GUTTER, 0)
822
+ },
823
+ className: cn(rows <= 1 ? 'items-end justify-center' : 'items-end justify-between', className),
824
+ children: Array.from({
825
+ length: rows
826
+ }, (_unused, index) => /*#__PURE__*/_jsx(Text, {
827
+ size: "xs",
828
+ muted: true,
829
+ numberOfLines: 1,
830
+ children: format ? format(index + 1) : `#${index + 1}`
831
+ }, index))
832
+ });
833
+ }
834
+ BumpChartYAxis.displayName = 'BumpChart.YAxis';
835
+ BumpChartYAxis.layer = 'overlay';
836
+ // Read by the root, which leaves room for the ranks before laying the plot out.
837
+ BumpChartYAxis.axis = 'y';
838
+ const DEFAULT_LABELS_WIDTH = 84;
839
+ /**
840
+ * Each series' name beside its last point, so the lines can be told apart
841
+ * without a legend to look across to. The names move with their lines when
842
+ * the data changes.
843
+ */
844
+ function BumpChartLabels({
845
+ pressable = true,
846
+ className
847
+ }) {
848
+ const {
849
+ series,
850
+ plot,
851
+ pad,
852
+ highlight,
853
+ setHighlight,
854
+ from,
855
+ to,
856
+ morph,
857
+ status
858
+ } = useChart('BumpChart.Labels');
859
+ if (status === 'loading') return null;
860
+ return /*#__PURE__*/_jsx(View, {
861
+ style: StyleSheet.absoluteFill,
862
+ pointerEvents: "box-none",
863
+ children: series.map(item => /*#__PURE__*/_jsx(BumpLabel, {
864
+ series: item,
865
+ left: plot.left + plot.width + LABELS_GAP,
866
+ width: pad.right - LABELS_GAP,
867
+ plot: plot,
868
+ from: from,
869
+ to: to,
870
+ morph: morph,
871
+ picked: highlight === item.key,
872
+ dimmed: highlight !== null && highlight !== item.key,
873
+ onPress: pressable ? () => setHighlight(highlight === item.key ? null : item.key) : undefined,
874
+ className: className
875
+ }, item.key))
876
+ });
877
+ }
878
+ BumpChartLabels.displayName = 'BumpChart.Labels';
879
+ BumpChartLabels.layer = 'overlay';
880
+ // Read by the root, which keeps room for the names to the right of the plot.
881
+ BumpChartLabels.labels = true;
882
+ function BumpLabel({
883
+ series,
884
+ left,
885
+ width,
886
+ plot,
887
+ from,
888
+ to,
889
+ morph,
890
+ picked,
891
+ dimmed,
892
+ onPress,
893
+ className
894
+ }) {
895
+ const key = series.key;
896
+
897
+ // The name sits level with the line's last point, wherever that is drawn.
898
+ const style = useAnimatedStyle(() => {
899
+ const runs = pointsAt(key, from.value, to.value, morph.value, plot);
900
+ const run = runs[runs.length - 1];
901
+ const last = run?.[run.length - 1];
902
+ if (!last) return {
903
+ opacity: 0,
904
+ transform: [{
905
+ translateY: 0
906
+ }]
907
+ };
908
+ return {
909
+ opacity: 1,
910
+ transform: [{
911
+ translateY: last.y - LABEL_ROW / 2
912
+ }]
913
+ };
914
+ });
915
+ return /*#__PURE__*/_jsx(Animated.View, {
916
+ style: [{
917
+ position: 'absolute',
918
+ top: 0,
919
+ left,
920
+ width,
921
+ height: LABEL_ROW
922
+ }, style],
923
+ children: /*#__PURE__*/_jsx(Pressable, {
924
+ onPress: onPress,
925
+ disabled: !onPress,
926
+ hitSlop: {
927
+ top: 4,
928
+ bottom: 4
929
+ },
930
+ accessibilityRole: "button",
931
+ accessibilityState: {
932
+ selected: picked
933
+ },
934
+ accessibilityLabel: series.label,
935
+ className: "h-full justify-center",
936
+ children: /*#__PURE__*/_jsx(Text, {
937
+ size: "xs",
938
+ weight: picked ? 'semibold' : undefined,
939
+ muted: !picked,
940
+ numberOfLines: 1
941
+ // With nothing picked out, each name takes its line's colour, which
942
+ // is the only thing tying it to that line across the crossings.
943
+ ,
944
+ style: dimmed ? {
945
+ opacity: 0.8
946
+ } : picked ? undefined : {
947
+ color: series.color
948
+ },
949
+ className: className,
950
+ children: series.label
951
+ })
952
+ })
953
+ });
954
+ }
955
+
956
+ /** Height of one name's hit box. */
957
+ const LABEL_ROW = 20;
958
+ /**
959
+ * A scrub across the columns, and a readout listing every series in the order
960
+ * it stood at the column under the finger.
961
+ */
962
+ function BumpChartTooltip({
963
+ color,
964
+ formatRank,
965
+ formatX,
966
+ className
967
+ }) {
968
+ const {
969
+ data,
970
+ xDataKey,
971
+ plot,
972
+ series,
973
+ ranks,
974
+ highlight,
975
+ activeIndex,
976
+ activeIndexJS,
977
+ setActiveIndexJS,
978
+ status
979
+ } = useChart('BumpChart.Tooltip');
980
+ const token = useCSSVariable('--color-foreground');
981
+ const stroke = color ?? (typeof token === 'string' ? token : '#888888');
982
+ const muted = useCSSVariable('--color-muted-foreground');
983
+ const mutedColor = typeof muted === 'string' ? muted : '#737373';
984
+ const total = data.length;
985
+ const left = plot.left;
986
+ const width = plot.width;
987
+
988
+ // Declared inside the memo, next to its callers: a worklet may only call
989
+ // another worklet.
990
+ const pan = useMemo(() => {
991
+ const resolve = x => {
992
+ 'worklet';
993
+
994
+ if (total < 1 || width <= 0) return;
995
+ const ratio = total < 2 ? 0 : (x - left) / width;
996
+ const next = Math.round(Math.min(1, Math.max(0, ratio)) * (total - 1));
997
+ if (next === activeIndex.value) return;
998
+ activeIndex.value = next;
999
+ runOnJS(setActiveIndexJS)(next);
1000
+ };
1001
+ return Gesture.Pan().minDistance(0).onBegin(event => {
1002
+ 'worklet';
1003
+
1004
+ resolve(event.x);
1005
+ }).onUpdate(event => {
1006
+ 'worklet';
1007
+
1008
+ resolve(event.x);
1009
+ }).onFinalize(() => {
1010
+ 'worklet';
1011
+
1012
+ activeIndex.value = -1;
1013
+ runOnJS(setActiveIndexJS)(-1);
1014
+ });
1015
+ }, [total, left, width, activeIndex, setActiveIndexJS]);
1016
+ const crosshairStyle = useAnimatedStyle(() => {
1017
+ const index = activeIndex.value;
1018
+ return {
1019
+ opacity: index < 0 ? 0 : 0.45,
1020
+ transform: [{
1021
+ translateX: index < 0 ? 0 : xOf(index, total, plot)
1022
+ }]
1023
+ };
1024
+ });
1025
+
1026
+ /*
1027
+ * The readout sits beside the crosshair rather than over it, on whichever
1028
+ * side has room, so it never hides the column it is describing.
1029
+ */
1030
+ const readoutStyle = useAnimatedStyle(() => {
1031
+ const index = activeIndex.value;
1032
+ if (index < 0) return {
1033
+ opacity: 0
1034
+ };
1035
+ const x = xOf(index, total, plot);
1036
+ const gap = 10;
1037
+ const right = x + gap;
1038
+ const fits = right + READOUT_WIDTH <= plot.left + plot.width;
1039
+ const at = fits ? right : Math.max(plot.left, x - gap - READOUT_WIDTH);
1040
+ return {
1041
+ opacity: 1,
1042
+ transform: [{
1043
+ translateX: at
1044
+ }]
1045
+ };
1046
+ });
1047
+ const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
1048
+ const standings = useMemo(() => {
1049
+ if (activeIndexJS < 0) return [];
1050
+ return series.map(item => ({
1051
+ item,
1052
+ rank: ranks[item.key]?.[activeIndexJS] ?? null
1053
+ })).filter(entry => entry.rank !== null).sort((a, b) => a.rank - b.rank);
1054
+ }, [series, ranks, activeIndexJS]);
1055
+ const fmtRank = formatRank ?? (rank => `#${rank}`);
1056
+ const fmtX = formatX ?? (datum => String(datum[xDataKey] ?? ''));
1057
+ if (status === 'loading') return null;
1058
+ return /*#__PURE__*/_jsx(GestureDetector, {
1059
+ gesture: pan,
1060
+ children: /*#__PURE__*/_jsxs(View, {
1061
+ style: {
1062
+ position: 'absolute',
1063
+ left: 0,
1064
+ top: 0,
1065
+ bottom: 0,
1066
+ width: plot.left + plot.width + 8
1067
+ },
1068
+ children: [/*#__PURE__*/_jsx(Animated.View, {
1069
+ pointerEvents: "none",
1070
+ style: [{
1071
+ position: 'absolute',
1072
+ left: 0,
1073
+ top: plot.top,
1074
+ width: 1,
1075
+ height: plot.height,
1076
+ backgroundColor: stroke
1077
+ }, crosshairStyle]
1078
+ }), /*#__PURE__*/_jsx(Animated.View, {
1079
+ pointerEvents: "none",
1080
+ style: [{
1081
+ position: 'absolute',
1082
+ left: 0,
1083
+ top: plot.top,
1084
+ width: READOUT_WIDTH
1085
+ }, readoutStyle],
1086
+ children: active ? /*#__PURE__*/_jsxs(View, {
1087
+ className: cn('gap-0.5 rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
1088
+ children: [/*#__PURE__*/_jsx(Text, {
1089
+ size: "xs",
1090
+ muted: true,
1091
+ numberOfLines: 1,
1092
+ children: fmtX(active)
1093
+ }), standings.map(({
1094
+ item,
1095
+ rank
1096
+ }) => /*#__PURE__*/_jsxs(View, {
1097
+ className: "flex-row items-center gap-1.5",
1098
+ children: [/*#__PURE__*/_jsx(Text, {
1099
+ size: "xs",
1100
+ muted: true,
1101
+ style: {
1102
+ minWidth: 20
1103
+ },
1104
+ children: fmtRank(rank)
1105
+ }), /*#__PURE__*/_jsx(View, {
1106
+ style: {
1107
+ width: 6,
1108
+ height: 6,
1109
+ borderRadius: 3,
1110
+ backgroundColor: highlight !== null && highlight !== item.key ? mutedColor : item.color
1111
+ }
1112
+ }), /*#__PURE__*/_jsx(Text, {
1113
+ size: "xs",
1114
+ weight: highlight === item.key ? 'semibold' : 'medium',
1115
+ numberOfLines: 1,
1116
+ className: "shrink",
1117
+ children: item.label
1118
+ })]
1119
+ }, item.key))]
1120
+ }) : null
1121
+ })]
1122
+ })
1123
+ });
1124
+ }
1125
+ BumpChartTooltip.displayName = 'BumpChart.Tooltip';
1126
+ BumpChartTooltip.layer = 'overlay';
1127
+ /** A swatch and a name per series, in declaration order. */
1128
+ function BumpChartLegend({
1129
+ className,
1130
+ ...props
1131
+ }) {
1132
+ const {
1133
+ series
1134
+ } = useChart('BumpChart.Legend');
1135
+ if (!series.length) return null;
1136
+ return /*#__PURE__*/_jsx(View, {
1137
+ ...props,
1138
+ style: [{
1139
+ pointerEvents: 'none'
1140
+ }, props.style],
1141
+ className: cn('absolute right-2 top-0 flex-row flex-wrap gap-3', className),
1142
+ children: series.map(item => /*#__PURE__*/_jsx(SeriesSwatch, {
1143
+ color: item.color,
1144
+ label: item.label
1145
+ }, item.key))
1146
+ });
1147
+ }
1148
+ BumpChartLegend.displayName = 'BumpChart.Legend';
1149
+ BumpChartLegend.layer = 'overlay';
1150
+
1151
+ /** One series' colour and name. Shared by the legend and the header. */
1152
+ function SeriesSwatch({
1153
+ color,
1154
+ label
1155
+ }) {
1156
+ return /*#__PURE__*/_jsxs(View, {
1157
+ className: "flex-row items-center gap-1.5",
1158
+ children: [/*#__PURE__*/_jsx(View, {
1159
+ style: {
1160
+ backgroundColor: color
1161
+ },
1162
+ className: "h-2 w-2 rounded-full"
1163
+ }), /*#__PURE__*/_jsx(Text, {
1164
+ size: "xs",
1165
+ muted: true,
1166
+ children: label
1167
+ })]
1168
+ });
1169
+ }
1170
+
1171
+ /* -------------------------------------------------------------------------- */
1172
+ /* Header layer */
1173
+ /* -------------------------------------------------------------------------- */
1174
+
1175
+ /**
1176
+ * The strip above the plot: what the chart is of and what it currently reads.
1177
+ * The value is not derived here — pass the formatted string, from
1178
+ * `onActiveIndexChange` or `onHighlightChange` if it follows the chart.
1179
+ */
1180
+ function BumpChartHeader({
1181
+ className,
1182
+ title,
1183
+ value,
1184
+ caption,
1185
+ legend = false,
1186
+ children,
1187
+ ...props
1188
+ }) {
1189
+ const {
1190
+ series
1191
+ } = useChart('BumpChart.Header');
1192
+ const trailing = children ?? (legend && series.length ? /*#__PURE__*/_jsx(View, {
1193
+ className: "flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1",
1194
+ children: series.map(item => /*#__PURE__*/_jsx(SeriesSwatch, {
1195
+ color: item.color,
1196
+ label: item.label
1197
+ }, item.key))
1198
+ }) : null);
1199
+ return /*#__PURE__*/_jsxs(View, {
1200
+ ...props,
1201
+ className: cn('flex-row items-start justify-between gap-3 pb-3', className),
1202
+ children: [/*#__PURE__*/_jsxs(View, {
1203
+ className: "flex-1 gap-0.5",
1204
+ children: [title ? /*#__PURE__*/_jsx(Text, {
1205
+ size: "xs",
1206
+ muted: true,
1207
+ children: title
1208
+ }) : null, value ? /*#__PURE__*/_jsx(Text, {
1209
+ size: "xl",
1210
+ weight: "bold",
1211
+ children: value
1212
+ }) : null, caption ? /*#__PURE__*/_jsx(Text, {
1213
+ size: "xs",
1214
+ muted: true,
1215
+ children: caption
1216
+ }) : null]
1217
+ }), trailing ? /*#__PURE__*/_jsx(View, {
1218
+ className: "shrink pt-1",
1219
+ children: trailing
1220
+ }) : null]
1221
+ });
1222
+ }
1223
+ BumpChartHeader.displayName = 'BumpChart.Header';
1224
+ BumpChartHeader.layer = 'header';
1225
+ export const BumpChart = Object.assign(BumpChartRoot, {
1226
+ Header: BumpChartHeader,
1227
+ Grid: BumpChartGrid,
1228
+ Line: BumpChartLine,
1229
+ Skeleton: BumpChartSkeleton,
1230
+ XAxis: BumpChartXAxis,
1231
+ YAxis: BumpChartYAxis,
1232
+ Labels: BumpChartLabels,
1233
+ Tooltip: BumpChartTooltip,
1234
+ Legend: BumpChartLegend
1235
+ });
1236
+ //# sourceMappingURL=index.js.map