panelui-native 0.86.0 → 0.87.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.
@@ -0,0 +1,1266 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * BubbleChart — one labelled circle per row, on two measured axes, with a third
5
+ * quantity on each circle's area.
6
+ *
7
+ * ```tsx
8
+ * <BubbleChart data={teams} xDataKey="efficiency" yDataKey="performance"
9
+ * sizeKey="headcount" labelKey="team">
10
+ * <BubbleChart.Grid />
11
+ * <BubbleChart.Bubbles />
12
+ * <BubbleChart.Labels />
13
+ * <BubbleChart.XAxis />
14
+ * <BubbleChart.YAxis />
15
+ * <BubbleChart.Tooltip />
16
+ * </BubbleChart>
17
+ * ```
18
+ *
19
+ * ## When this and not a scatter plot
20
+ *
21
+ * `ScatterChart` also maps a third quantity onto point area, through `sizeKey`,
22
+ * and it is the right component for a *series* of observations: many points of
23
+ * one colour, where the shape of the cloud is the finding and no single dot
24
+ * needs a name.
25
+ *
26
+ * This one is for a handful of named things. Each row is its own circle with
27
+ * its own colour and its own label written inside it, so the chart can be read
28
+ * entity by entity rather than as a distribution. Eight teams, twelve products,
29
+ * six regions — where the reader wants to find one of them and see where it
30
+ * sits.
31
+ *
32
+ * ## Area, not radius
33
+ *
34
+ * `sizeKey` maps to a circle's area. Doubling a radius quadruples the ink, so a
35
+ * chart that scaled the radius would show a doubled value as four times the
36
+ * size, and the reader would believe the picture. The scale runs over the whole
37
+ * data set, so one bubble's size means the same thing as another's.
38
+ *
39
+ * ## Labels are text, not SVG
40
+ *
41
+ * The names inside the bubbles are React Native `Text` in a layer over the
42
+ * plot, so they follow the theme's font and the platform's text scaling — SVG
43
+ * text does neither. A bubble too small to hold its own label is left without
44
+ * one rather than given an unreadable one; the readout still names it.
45
+ */
46
+ import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
47
+ import { StyleSheet, View } from 'react-native';
48
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
49
+ import Animated, { Easing, runOnJS, useAnimatedProps, useAnimatedStyle, useDerivedValue, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
50
+ import Svg, { Circle, G, Line as SvgLine } from 'react-native-svg';
51
+ import { useCSSVariable } from 'uniwind';
52
+ import { useSkeletonHandoff } from "../../hooks/use-skeleton-handoff.js";
53
+ import { ChartAccessibilityData } from "../../primitives/chart-accessibility.js";
54
+ import { Text } from "../../primitives/text.js";
55
+ import { bubbleRadius, compactNumber, niceDomain, useSeriesColor, xAt, yOf } from "../../utils/chart.js";
56
+ import { cn } from "../../utils/cn.js";
57
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
58
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
59
+ const AnimatedG = Animated.createAnimatedComponent(G);
60
+
61
+ /**
62
+ * How much of the reveal is spent handing out the bubbles' start times. The
63
+ * rest is the window each one gets, so the whole field still lands inside the
64
+ * one duration however many there are.
65
+ */
66
+ const STAGGER = 0.4;
67
+
68
+ /** Milliseconds for a bubble to swell as it is selected, and settle as it is not. */
69
+ const SELECT_DURATION = 140;
70
+
71
+ /**
72
+ * A bubble arriving: up past its size and back to it.
73
+ *
74
+ * A circle that simply grows to its radius reads as the chart still loading
75
+ * right up to the last frame. The small overshoot is what makes it read as
76
+ * landing.
77
+ */
78
+ function landing(t) {
79
+ 'worklet';
80
+
81
+ const back = 1.3;
82
+ const u = t - 1;
83
+ return 1 + (back + 1) * u * u * u + back * u * u;
84
+ }
85
+
86
+ /** Room left around the plot for the axis labels and the outermost bubbles. */
87
+ const PADDING = {
88
+ top: 18,
89
+ right: 18,
90
+ bottom: 22,
91
+ left: 18
92
+ };
93
+
94
+ /** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
95
+ const Y_AXIS_WIDTH = 44;
96
+
97
+ /** Gap between the value labels and the plot they sit beside. */
98
+ const Y_AXIS_GUTTER = 6;
99
+
100
+ /** Line height of an `xs` label, for centring one on the grid line it names. */
101
+ const AXIS_LABEL_HEIGHT = 16;
102
+
103
+ /** Box each axis label is centred in, so a long number is ellipsised not shoved. */
104
+ const AXIS_LABEL_WIDTH = 56;
105
+
106
+ /** Width of the readout that floats by the selected bubble. */
107
+ const LABEL_WIDTH = 132;
108
+
109
+ /** Gap between the readout and the edge of the bubble it describes. */
110
+ const LABEL_GAP = 10;
111
+
112
+ /** Line box a bubble's own label is laid out in. */
113
+ const BUBBLE_LABEL_HEIGHT = 16;
114
+
115
+ /**
116
+ * Smallest radius a bubble may have and still be given its label. Below this
117
+ * the name is wider than the circle and reads as text lying on the plot rather
118
+ * than as the bubble's own.
119
+ *
120
+ * Ten points, which is a twenty-point circle: enough for the initial or the
121
+ * short code a bubble chart's labels usually are. Set higher and the smallest
122
+ * bubble in an ordinary set silently loses its name, which reads as a bug
123
+ * rather than as a decision.
124
+ */
125
+ const LABEL_MIN_RADIUS = 10;
126
+
127
+ /** Floor on the touch target, for a chart whose smallest bubbles are tiny. */
128
+ const HIT_RADIUS = 22;
129
+
130
+ /** How many colours the ramp cycles through. */
131
+ const PALETTE_SIZE = 5;
132
+
133
+ /** Steps each axis is rounded out to. Matches the labels an axis draws. */
134
+ const AXIS_STEPS = 2;
135
+
136
+ /** One bubble, resolved back to the row it came from. */
137
+
138
+ /** A bubble with its geometry resolved. Shared by every part. */
139
+
140
+ const BubbleChartContext = /*#__PURE__*/createContext(null);
141
+ function useChart(component) {
142
+ const context = useContext(BubbleChartContext);
143
+ if (!context) {
144
+ throw new Error(`${component} must be used within a <BubbleChart>`);
145
+ }
146
+ return context;
147
+ }
148
+
149
+ /**
150
+ * The bubble under the finger, for something rendered *inside* the chart. A
151
+ * readout in the card's header is outside this provider — use
152
+ * `onActivePointChange` for that.
153
+ */
154
+ export function useBubbleChart() {
155
+ const {
156
+ bubbles,
157
+ activeIndexJS
158
+ } = useChart('useBubbleChart');
159
+ const active = bubbles.find(bubble => bubble.index === activeIndexJS) ?? null;
160
+ return {
161
+ activeIndex: activeIndexJS,
162
+ activePoint: active
163
+ };
164
+ }
165
+
166
+ /**
167
+ * An extent widened out to round numbers, and a usable one for the degenerate
168
+ * cases — no data at all, or every reading identical. A domain of zero width
169
+ * divides by zero and puts every bubble on the same edge.
170
+ *
171
+ * Rounded rather than padded by a fraction: a fraction of the data's own span
172
+ * ends the axis at 52.7, which is true and which nobody was looking for. Two
173
+ * steps, matching the labels each axis draws by default, so the middle one is
174
+ * round as well as the ends.
175
+ */
176
+ function padExtent(min, max) {
177
+ if (min === Infinity) return [0, 1];
178
+ if (min === max) return [min - 1, max + 1];
179
+ return niceDomain(min, max, AXIS_STEPS);
180
+ }
181
+
182
+ /** Imperative handle: re-run the grow-in, for a "replay" control. */
183
+
184
+ function partition(children) {
185
+ const svg = [];
186
+ const overlay = [];
187
+ const header = [];
188
+ const footer = [];
189
+ Children.forEach(children, (child, index) => {
190
+ if (! /*#__PURE__*/isValidElement(child)) return;
191
+ const layer = child.type.layer ?? 'svg';
192
+ const slot = /*#__PURE__*/_jsx(ChildSlot, {
193
+ children: child
194
+ }, index);
195
+ const into = layer === 'header' ? header : layer === 'footer' ? footer : layer === 'overlay' ? overlay : svg;
196
+ into.push(slot);
197
+ });
198
+ return {
199
+ svg,
200
+ overlay,
201
+ header,
202
+ footer
203
+ };
204
+ }
205
+ function ChildSlot({
206
+ children
207
+ }) {
208
+ return /*#__PURE__*/_jsx(_Fragment, {
209
+ children: children
210
+ });
211
+ }
212
+ const BubbleChartRoot = /*#__PURE__*/forwardRef(function BubbleChartRoot({
213
+ className,
214
+ data,
215
+ xDataKey = 'x',
216
+ yDataKey = 'y',
217
+ sizeKey,
218
+ labelKey,
219
+ colorKey,
220
+ sizeRange = [10, 28],
221
+ status = 'ready',
222
+ aspectRatio = 1,
223
+ animationDuration = 800,
224
+ domainDuration = 500,
225
+ xDomain,
226
+ yDomain,
227
+ onActivePointChange,
228
+ accessible,
229
+ accessibilityLabel,
230
+ accessibilityHint,
231
+ accessibilityLabelForDatum,
232
+ onAccessibilityDatumPress,
233
+ children,
234
+ ...props
235
+ }, ref) {
236
+ const [size, setSize] = useState({
237
+ width: 0,
238
+ height: 0
239
+ });
240
+ const [activeIndexJS, setActiveIndexJS] = useState(-1);
241
+ const reveal = useSharedValue(0);
242
+ const xMin = useSharedValue(0);
243
+ const xMax = useSharedValue(0);
244
+ const yMin = useSharedValue(0);
245
+ const yMax = useSharedValue(0);
246
+ const activeIndex = useSharedValue(-1);
247
+ const reducedMotion = useReducedMotion();
248
+
249
+ /*
250
+ * The whole ramp, resolved once. Every bubble is its own category here
251
+ * rather than a member of a series, so the colour is chosen by row index
252
+ * and there is nothing to register.
253
+ */
254
+ const chart1 = useSeriesColor(undefined, 1);
255
+ const chart2 = useSeriesColor(undefined, 2);
256
+ const chart3 = useSeriesColor(undefined, 3);
257
+ const chart4 = useSeriesColor(undefined, 4);
258
+ const chart5 = useSeriesColor(undefined, 5);
259
+ const palette = useMemo(() => [chart1, chart2, chart3, chart4, chart5], [chart1, chart2, chart3, chart4, chart5]);
260
+ const hasYAxis = useMemo(() => {
261
+ let found = false;
262
+ Children.forEach(children, child => {
263
+ if (/*#__PURE__*/isValidElement(child) && child.type.axis === 'y') {
264
+ found = true;
265
+ }
266
+ });
267
+ return found;
268
+ }, [children]);
269
+
270
+ /*
271
+ * A circle is drawn about its centre, so every edge of the plot has to hold
272
+ * back the largest radius or the outermost bubble is cropped by it — and
273
+ * the largest bubble is the one carrying the largest value, which is the
274
+ * last one that should be half missing. `sizeRange` is the ceiling, so this
275
+ * is known before anything is measured.
276
+ */
277
+ const reach = sizeRange[1];
278
+ const pad = {
279
+ top: Math.max(PADDING.top, reach),
280
+ right: Math.max(PADDING.right, reach),
281
+ bottom: Math.max(PADDING.bottom, reach),
282
+ left: Math.max(hasYAxis ? Y_AXIS_WIDTH : PADDING.left, reach)
283
+ };
284
+ const plot = {
285
+ left: pad.left,
286
+ top: pad.top,
287
+ width: Math.max(size.width - pad.left - pad.right, 0),
288
+ height: Math.max(size.height - pad.top - pad.bottom, 0)
289
+ };
290
+ const extents = useMemo(() => {
291
+ let lowX = Infinity;
292
+ let highX = -Infinity;
293
+ let lowY = Infinity;
294
+ let highY = -Infinity;
295
+ for (const row of data) {
296
+ const x = row[xDataKey];
297
+ const y = row[yDataKey];
298
+ // A row missing either coordinate is not a bubble at all, and must not
299
+ // stretch the axes towards an origin it never had.
300
+ if (typeof x !== 'number' || Number.isNaN(x)) continue;
301
+ if (typeof y !== 'number' || Number.isNaN(y)) continue;
302
+ if (x < lowX) lowX = x;
303
+ if (x > highX) highX = x;
304
+ if (y < lowY) lowY = y;
305
+ if (y > highY) highY = y;
306
+ }
307
+ return {
308
+ x: xDomain ?? padExtent(lowX, highX),
309
+ y: yDomain ?? padExtent(lowY, highY)
310
+ };
311
+ }, [data, xDataKey, yDataKey, xDomain, yDomain]);
312
+
313
+ /*
314
+ * The size scale runs over the whole data set, so one bubble's area means
315
+ * the same thing as another's. Without a `sizeKey` there is nothing to
316
+ * scale and every bubble takes the middle of the range — which is a
317
+ * scatter plot with names on it, and an honest one.
318
+ */
319
+ const sizeExtent = useMemo(() => {
320
+ if (!sizeKey) return null;
321
+ let min = Infinity;
322
+ let max = -Infinity;
323
+ for (const row of data) {
324
+ const value = row[sizeKey];
325
+ if (typeof value !== 'number' || Number.isNaN(value)) continue;
326
+ if (value < min) min = value;
327
+ if (value > max) max = value;
328
+ }
329
+ return min === Infinity ? null : [min, max];
330
+ }, [data, sizeKey]);
331
+
332
+ /*
333
+ * Resolved once, in the root, because four parts need exactly this list and
334
+ * three of them would otherwise derive it again: the circles, the labels
335
+ * over them, the hit test under them and the legend beside them all have to
336
+ * agree about where a bubble is and how big it is.
337
+ */
338
+ const bubbles = useMemo(() => {
339
+ const middle = (sizeRange[0] + sizeRange[1]) / 2;
340
+ const out = [];
341
+ data.forEach((datum, index) => {
342
+ const x = datum[xDataKey];
343
+ const y = datum[yDataKey];
344
+ if (typeof x !== 'number' || Number.isNaN(x)) return;
345
+ if (typeof y !== 'number' || Number.isNaN(y)) return;
346
+ const raw = sizeKey ? datum[sizeKey] : undefined;
347
+ const value = typeof raw === 'number' && !Number.isNaN(raw) ? raw : null;
348
+ const r = sizeExtent && value !== null ? bubbleRadius(value, sizeExtent, sizeRange) : middle;
349
+ const explicit = colorKey ? datum[colorKey] : undefined;
350
+ const color = typeof explicit === 'string' ? explicit : typeof explicit === 'number' ? palette[(Math.round(explicit) - 1 + PALETTE_SIZE) % PALETTE_SIZE] ?? palette[0] : palette[index % PALETTE_SIZE] ?? palette[0];
351
+ out.push({
352
+ index,
353
+ x,
354
+ y,
355
+ r,
356
+ size: value,
357
+ label: labelKey ? String(datum[labelKey] ?? '') : '',
358
+ color,
359
+ datum
360
+ });
361
+ });
362
+ return out;
363
+ }, [data, xDataKey, yDataKey, sizeKey, labelKey, colorKey, sizeExtent, sizeRange, palette]);
364
+ const loading = status === 'loading';
365
+ useEffect(() => {
366
+ if (loading) return;
367
+ const [x0, x1] = extents.x;
368
+ const [y0, y1] = extents.y;
369
+ // The first domain lands without a tween: there is no previous scale to
370
+ // move from, and animating up from zero reads as the numbers changing.
371
+ const first = xMin.value === 0 && xMax.value === 0 && yMin.value === 0 && yMax.value === 0;
372
+ if (first || reducedMotion) {
373
+ xMin.value = x0;
374
+ xMax.value = x1;
375
+ yMin.value = y0;
376
+ yMax.value = y1;
377
+ return;
378
+ }
379
+ xMin.value = withTiming(x0, {
380
+ duration: domainDuration
381
+ });
382
+ xMax.value = withTiming(x1, {
383
+ duration: domainDuration
384
+ });
385
+ yMin.value = withTiming(y0, {
386
+ duration: domainDuration
387
+ });
388
+ yMax.value = withTiming(y1, {
389
+ duration: domainDuration
390
+ });
391
+ }, [extents, loading, reducedMotion, domainDuration, xMin, xMax, yMin, yMax]);
392
+ const revealed = useRef(false);
393
+ const playReveal = useMemo(() => () => {
394
+ if (reducedMotion) {
395
+ reveal.value = 1;
396
+ return;
397
+ }
398
+ reveal.value = 0;
399
+ /*
400
+ * Eased out rather than in and out. Each bubble is given a slice of
401
+ * this one clock, so an ease that dawdles at the start spends it on the
402
+ * first few and leaves the rest to arrive in a rush.
403
+ */
404
+ reveal.value = withTiming(1, {
405
+ duration: animationDuration,
406
+ easing: Easing.out(Easing.cubic)
407
+ });
408
+ }, [reducedMotion, animationDuration, reveal]);
409
+ useEffect(() => {
410
+ if (loading) {
411
+ revealed.current = false;
412
+ reveal.value = 0;
413
+ return;
414
+ }
415
+ if (revealed.current || plot.width <= 0 || !bubbles.length) return;
416
+ revealed.current = true;
417
+ playReveal();
418
+ }, [loading, plot.width, bubbles.length, playReveal, reveal]);
419
+ useImperativeHandle(ref, () => ({
420
+ replay: playReveal
421
+ }), [playReveal]);
422
+
423
+ // One place the selection lands, so the chart's own children and a readout
424
+ // outside it never disagree about which bubble is active.
425
+ const setActivePoint = useMemo(() => point => {
426
+ setActiveIndexJS(point ? point.index : -1);
427
+ onActivePointChange?.(point);
428
+ }, [onActivePointChange]);
429
+ const onLayout = event => {
430
+ const {
431
+ width,
432
+ height
433
+ } = event.nativeEvent.layout;
434
+ setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
435
+ width,
436
+ height
437
+ });
438
+ props.onLayout?.(event);
439
+ };
440
+ const context = useMemo(() => ({
441
+ data,
442
+ xDataKey,
443
+ yDataKey,
444
+ labelKey,
445
+ plot,
446
+ status,
447
+ bubbles,
448
+ xMin,
449
+ xMax,
450
+ yMin,
451
+ yMax,
452
+ xExtent: extents.x,
453
+ yExtent: extents.y,
454
+ reveal,
455
+ activeIndex,
456
+ activeIndexJS,
457
+ setActivePoint
458
+ }),
459
+ // `plot` is rebuilt every render from `size`, so it is compared by value.
460
+ // eslint-disable-next-line react-hooks/exhaustive-deps
461
+ [data, xDataKey, yDataKey, labelKey, plot.width, plot.height, plot.left, plot.top, status, bubbles, xMin, xMax, yMin, yMax, extents, reveal, activeIndex, activeIndexJS, setActivePoint]);
462
+ const {
463
+ svg,
464
+ overlay,
465
+ header,
466
+ footer
467
+ } = partition(children);
468
+
469
+ /*
470
+ * Two views, because the header is not part of the plot. `aspectRatio` and
471
+ * the layout measurement belong to the drawing area alone — measured on the
472
+ * outer view they would take in the header too, and the plot would lose as
473
+ * much height as the readout took while still claiming the shape asked for.
474
+ */
475
+ return /*#__PURE__*/_jsx(BubbleChartContext.Provider, {
476
+ value: context,
477
+ children: /*#__PURE__*/_jsxs(View, {
478
+ ...props,
479
+ style: props.style,
480
+ className: cn('w-full', className),
481
+ children: [header, /*#__PURE__*/_jsx(ChartAccessibilityData, {
482
+ chart: "Bubble chart",
483
+ data: data,
484
+ disabled: accessible === false || loading,
485
+ accessibilityLabel: accessibilityLabel,
486
+ accessibilityHint: accessibilityHint,
487
+ accessibilityLabelForDatum: accessibilityLabelForDatum,
488
+ onAccessibilityDatumPress: onAccessibilityDatumPress,
489
+ valueOf: datum => {
490
+ const pairs = [[xDataKey, datum[xDataKey]], [yDataKey, datum[yDataKey]]];
491
+ if (labelKey) pairs.unshift([labelKey, datum[labelKey]]);
492
+ if (sizeKey) pairs.push([sizeKey, datum[sizeKey]]);
493
+ return pairs;
494
+ }
495
+ }), /*#__PURE__*/_jsx(View, {
496
+ onLayout: onLayout,
497
+ style: {
498
+ aspectRatio
499
+ },
500
+ className: "w-full",
501
+ accessibilityElementsHidden: true,
502
+ importantForAccessibility: "no-hide-descendants",
503
+ children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
504
+ children: [/*#__PURE__*/_jsx(Svg, {
505
+ width: "100%",
506
+ height: "100%",
507
+ style: StyleSheet.absoluteFill,
508
+ children: svg
509
+ }), overlay]
510
+ }) : null
511
+ }), footer]
512
+ })
513
+ });
514
+ });
515
+ BubbleChartRoot.displayName = 'BubbleChart';
516
+
517
+ /* -------------------------------------------------------------------------- */
518
+ /* SVG layer */
519
+ /* -------------------------------------------------------------------------- */
520
+
521
+ /** Reference lines both ways, so a bubble can be placed against two numbers. */
522
+ function BubbleChartGrid({
523
+ rows = 5,
524
+ columns = 5,
525
+ color,
526
+ opacity = 1
527
+ }) {
528
+ const {
529
+ plot
530
+ } = useChart('BubbleChart.Grid');
531
+ const token = useCSSVariable('--color-border');
532
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
533
+ const horizontals = Array.from({
534
+ length: rows + 1
535
+ }, (_unused, i) => i / rows);
536
+ const verticals = Array.from({
537
+ length: columns + 1
538
+ }, (_unused, i) => i / columns);
539
+ return /*#__PURE__*/_jsxs(G, {
540
+ opacity: opacity,
541
+ children: [horizontals.map(fraction => /*#__PURE__*/_jsx(SvgLine, {
542
+ x1: plot.left,
543
+ x2: plot.left + plot.width,
544
+ y1: plot.top + plot.height * fraction,
545
+ y2: plot.top + plot.height * fraction,
546
+ stroke: stroke,
547
+ strokeWidth: 1
548
+ }, `h${fraction}`)), verticals.map(fraction => /*#__PURE__*/_jsx(SvgLine, {
549
+ x1: plot.left + plot.width * fraction,
550
+ x2: plot.left + plot.width * fraction,
551
+ y1: plot.top,
552
+ y2: plot.top + plot.height,
553
+ stroke: stroke,
554
+ strokeWidth: 1
555
+ }, `v${fraction}`))]
556
+ });
557
+ }
558
+ BubbleChartGrid.displayName = 'BubbleChart.Grid';
559
+ BubbleChartGrid.layer = 'svg';
560
+ /** The circles. */
561
+ function BubbleChartBubbles({
562
+ opacity = 0.9,
563
+ color
564
+ }) {
565
+ const {
566
+ bubbles,
567
+ plot,
568
+ status,
569
+ xMin,
570
+ xMax,
571
+ yMin,
572
+ yMax,
573
+ reveal,
574
+ activeIndex
575
+ } = useChart('BubbleChart.Bubbles');
576
+ if (status === 'loading') return null;
577
+ return /*#__PURE__*/_jsx(G, {
578
+ children: bubbles.map((bubble, order) => /*#__PURE__*/_jsx(Bubble, {
579
+ bubble: bubble,
580
+ plot: plot,
581
+ xMin: xMin,
582
+ xMax: xMax,
583
+ yMin: yMin,
584
+ yMax: yMax,
585
+ fill: color ?? bubble.color,
586
+ opacity: opacity,
587
+ activeIndex: activeIndex,
588
+ reveal: reveal,
589
+ order: order,
590
+ total: bubbles.length
591
+ }, bubble.index))
592
+ });
593
+ }
594
+ BubbleChartBubbles.displayName = 'BubbleChart.Bubbles';
595
+ BubbleChartBubbles.layer = 'svg';
596
+
597
+ /**
598
+ * One bubble. Its position follows both domain tweens, so a data change moves
599
+ * the whole field to the new scale rather than cutting to it.
600
+ *
601
+ * It arrives by growing in place, on its own slice of the shared reveal. A wipe
602
+ * across the plot — which is what the line and area charts do — gives the
603
+ * reader a direction to read the arrival in, and a field of bubbles has none:
604
+ * position is the whole message, so a bubble may only ever appear where it
605
+ * belongs.
606
+ */
607
+ function Bubble({
608
+ bubble,
609
+ plot,
610
+ xMin,
611
+ xMax,
612
+ yMin,
613
+ yMax,
614
+ fill,
615
+ opacity,
616
+ activeIndex,
617
+ reveal,
618
+ order,
619
+ total
620
+ }) {
621
+ const {
622
+ index,
623
+ x,
624
+ y,
625
+ r
626
+ } = bubble;
627
+
628
+ /*
629
+ * The selection, as something that moves. A hard switch made the bubble it
630
+ * named jump between one frame and the next while every neighbour stayed put,
631
+ * which reads as a glitch rather than as a response to the finger.
632
+ */
633
+ const selected = useDerivedValue(() => withTiming(activeIndex.value === index ? 1 : 0, {
634
+ duration: SELECT_DURATION
635
+ }));
636
+
637
+ // Where in the reveal this bubble starts. Spread over `STAGGER`, so the field
638
+ // settles as a field rather than switching on all at once.
639
+ const start = total > 1 ? order / total * STAGGER : 0;
640
+ const animatedProps = useAnimatedProps(() => {
641
+ const arrived = Math.max(0, Math.min(1, (reveal.value - start) / (1 - STAGGER)));
642
+ // The selected bubble swells and goes solid. Both, rather than one: a size
643
+ // change alone is easy to miss among neighbours, and an opacity change
644
+ // alone is invisible wherever the circles already overlap.
645
+ const swell = 1 + 0.12 * selected.value;
646
+ return {
647
+ cx: xAt(x, plot, xMin.value, xMax.value),
648
+ cy: yOf(y, plot, yMin.value, yMax.value),
649
+ r: r * landing(arrived) * swell,
650
+ // Ahead of the size, so a bubble is legible by the time it stops moving
651
+ // rather than fading in for the whole of its arrival.
652
+ fillOpacity: Math.min(1, arrived * 2) * (opacity + (1 - opacity) * selected.value)
653
+ };
654
+ });
655
+ return /*#__PURE__*/_jsx(AnimatedCircle, {
656
+ animatedProps: animatedProps,
657
+ fill: fill
658
+ });
659
+ }
660
+ /**
661
+ * The loading state: a still field of muted circles where the data will be.
662
+ *
663
+ * Deliberately still. A shimmer over a field of circles reads as them *moving*,
664
+ * which is the one thing this chart must never appear to do — position is the
665
+ * entire message, and a loading state that implies it is changing is a loading
666
+ * state that lies.
667
+ *
668
+ * The layout is deterministic rather than random, so it does not reshuffle on
669
+ * every render of a component that may re-render several times while waiting.
670
+ * It dissolves as the real bubbles grow in, and outlives the status change by
671
+ * exactly that long: cut at the frame the data lands, the placeholder would
672
+ * disappear before anything had replaced it.
673
+ */
674
+ function BubbleChartSkeleton({
675
+ count = 7,
676
+ color
677
+ }) {
678
+ const {
679
+ plot,
680
+ status
681
+ } = useChart('BubbleChart.Skeleton');
682
+ const token = useCSSVariable('--color-skeleton');
683
+ const fill = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
684
+ const {
685
+ mounted,
686
+ opacity: fade
687
+ } = useSkeletonHandoff(status === 'loading');
688
+ const circles = useMemo(() =>
689
+ // Two irrational-ish strides that do not share a factor, so the circles
690
+ // spread instead of falling into a lattice.
691
+ Array.from({
692
+ length: count
693
+ }, (_unused, index) => ({
694
+ key: index,
695
+ fx: index * 0.618 % 1 * 0.84 + 0.08,
696
+ fy: index * 0.379 % 1 * 0.84 + 0.08,
697
+ r: 16 + index * 0.472 % 1 * 12
698
+ })), [count]);
699
+ const animatedProps = useAnimatedProps(() => ({
700
+ opacity: fade.value
701
+ }));
702
+ if (!mounted) return null;
703
+ return /*#__PURE__*/_jsx(AnimatedG, {
704
+ animatedProps: animatedProps,
705
+ children: circles.map(circle => /*#__PURE__*/_jsx(Circle, {
706
+ cx: plot.left + circle.fx * plot.width,
707
+ cy: plot.top + circle.fy * plot.height,
708
+ r: circle.r,
709
+ fill: fill
710
+ }, circle.key))
711
+ });
712
+ }
713
+ BubbleChartSkeleton.displayName = 'BubbleChart.Skeleton';
714
+ BubbleChartSkeleton.layer = 'svg';
715
+
716
+ /* -------------------------------------------------------------------------- */
717
+ /* Overlay layer */
718
+ /* -------------------------------------------------------------------------- */
719
+
720
+ /**
721
+ * The names, written inside the circles.
722
+ *
723
+ * Real text over the plot rather than SVG text, so they follow the theme's font
724
+ * and the platform's text scaling. Each one rides the same domain tweens the
725
+ * circle under it does, so a label never lags the bubble it belongs to.
726
+ *
727
+ * A bubble too small to hold its name is left without one. Shrinking the text
728
+ * to fit would make it unreadable on exactly the bubbles the reader is
729
+ * squinting at already; the readout names those instead.
730
+ */
731
+ function BubbleChartLabels({
732
+ minRadius = LABEL_MIN_RADIUS,
733
+ format,
734
+ className
735
+ }) {
736
+ const {
737
+ bubbles,
738
+ plot,
739
+ status,
740
+ xMin,
741
+ xMax,
742
+ yMin,
743
+ yMax,
744
+ reveal
745
+ } = useChart('BubbleChart.Labels');
746
+ if (status === 'loading') return null;
747
+ return /*#__PURE__*/_jsx(View, {
748
+ style: {
749
+ position: 'absolute',
750
+ inset: 0,
751
+ pointerEvents: 'none'
752
+ },
753
+ children: bubbles.filter(bubble => bubble.r >= minRadius && (format || bubble.label)).map((bubble, order) => /*#__PURE__*/_jsx(BubbleLabel, {
754
+ bubble: bubble,
755
+ text: format ? format(bubble) : bubble.label,
756
+ plot: plot,
757
+ xMin: xMin,
758
+ xMax: xMax,
759
+ yMin: yMin,
760
+ yMax: yMax,
761
+ reveal: reveal,
762
+ order: order,
763
+ total: bubbles.length,
764
+ className: className
765
+ }, bubble.index))
766
+ });
767
+ }
768
+ BubbleChartLabels.displayName = 'BubbleChart.Labels';
769
+ BubbleChartLabels.layer = 'overlay';
770
+ function BubbleLabel({
771
+ bubble,
772
+ text,
773
+ plot,
774
+ xMin,
775
+ xMax,
776
+ yMin,
777
+ yMax,
778
+ reveal,
779
+ order,
780
+ total,
781
+ className
782
+ }) {
783
+ const {
784
+ x,
785
+ y,
786
+ r
787
+ } = bubble;
788
+ const width = r * 2;
789
+ const start = total > 1 ? order / total * STAGGER : 0;
790
+ const style = useAnimatedStyle(() => {
791
+ const arrived = Math.max(0, Math.min(1, (reveal.value - start) / (1 - STAGGER)));
792
+ return {
793
+ opacity: Math.max(0, arrived * 2 - 1),
794
+ transform: [{
795
+ translateX: xAt(x, plot, xMin.value, xMax.value) - width / 2
796
+ }, {
797
+ translateY: yOf(y, plot, yMin.value, yMax.value) - BUBBLE_LABEL_HEIGHT / 2
798
+ }]
799
+ };
800
+ });
801
+ return /*#__PURE__*/_jsx(Animated.View, {
802
+ pointerEvents: "none",
803
+ style: [{
804
+ position: 'absolute',
805
+ left: 0,
806
+ top: 0,
807
+ width,
808
+ height: BUBBLE_LABEL_HEIGHT
809
+ }, style],
810
+ children: /*#__PURE__*/_jsx(Text, {
811
+ size: "xs",
812
+ weight: "medium",
813
+ numberOfLines: 1
814
+ // White on the fill, which is a chart colour in every theme rather than
815
+ // a surface — so this is the one place a literal is right: the token
816
+ // that reads on a card would vanish on the bubble.
817
+ ,
818
+ className: cn('text-center text-white', className),
819
+ children: text
820
+ })
821
+ });
822
+ }
823
+ /**
824
+ * The x labels, evenly along the axis.
825
+ *
826
+ * Evenly spaced, because this axis is a continuous scale rather than a list of
827
+ * rows. There is no bubble for a label to sit under.
828
+ */
829
+ function BubbleChartXAxis({
830
+ ticks = 2,
831
+ format,
832
+ className
833
+ }) {
834
+ const {
835
+ plot,
836
+ xExtent
837
+ } = useChart('BubbleChart.XAxis');
838
+ const labels = useMemo(() => {
839
+ const [min, max] = xExtent;
840
+ if (min === 0 && max === 0) return [];
841
+ return Array.from({
842
+ length: ticks + 1
843
+ }, (_unused, index) => {
844
+ const value = min + (max - min) * index / ticks;
845
+ return {
846
+ key: index,
847
+ text: format ? format(value) : compactNumber(value)
848
+ };
849
+ });
850
+ }, [xExtent, ticks, format]);
851
+ return /*#__PURE__*/_jsx(View, {
852
+ style: {
853
+ position: 'absolute',
854
+ inset: 0,
855
+ pointerEvents: 'none'
856
+ },
857
+ className: cn(className),
858
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
859
+ size: "xs",
860
+ muted: true,
861
+ numberOfLines: 1,
862
+ style: {
863
+ position: 'absolute',
864
+ bottom: 0,
865
+ // Centred on its tick, then held inside the chart. The first and
866
+ // last ticks sit on the plot's own edges, so a box centred on them
867
+ // hangs half its width off the side — the clamp slides those two
868
+ // back in rather than letting the numbers leave the frame.
869
+ left: Math.max(0, Math.min(plot.left + plot.width / ticks * label.key - AXIS_LABEL_WIDTH / 2, plot.left + plot.width + PADDING.right - AXIS_LABEL_WIDTH)),
870
+ width: AXIS_LABEL_WIDTH,
871
+ textAlign: 'center'
872
+ },
873
+ children: label.text
874
+ }, label.key))
875
+ });
876
+ }
877
+ BubbleChartXAxis.displayName = 'BubbleChart.XAxis';
878
+ BubbleChartXAxis.layer = 'overlay';
879
+ /** Value labels down the side, one per grid line. Reserves its own gutter. */
880
+ function BubbleChartYAxis({
881
+ ticks = 2,
882
+ format,
883
+ className
884
+ }) {
885
+ const {
886
+ plot,
887
+ yExtent
888
+ } = useChart('BubbleChart.YAxis');
889
+ const labels = useMemo(() => {
890
+ const [min, max] = yExtent;
891
+ if (min === 0 && max === 0) return [];
892
+ return Array.from({
893
+ length: ticks + 1
894
+ }, (_unused, index) => {
895
+ const value = max - (max - min) * index / ticks;
896
+ return {
897
+ key: index,
898
+ text: format ? format(value) : compactNumber(value)
899
+ };
900
+ });
901
+ }, [yExtent, ticks, format]);
902
+ return /*#__PURE__*/_jsx(View, {
903
+ pointerEvents: "none",
904
+ style: {
905
+ position: 'absolute',
906
+ left: 0,
907
+ // Centred on the grid line each label names: the strip is lifted half a
908
+ // label and grown by a whole one, so `justify-between` lands the text's
909
+ // middle on the line rather than its top edge on the first.
910
+ top: plot.top - AXIS_LABEL_HEIGHT / 2,
911
+ height: plot.height + AXIS_LABEL_HEIGHT,
912
+ width: Math.max(plot.left - Y_AXIS_GUTTER, 0)
913
+ },
914
+ className: cn('items-end justify-between', className),
915
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
916
+ size: "xs",
917
+ muted: true,
918
+ numberOfLines: 1,
919
+ children: label.text
920
+ }, label.key))
921
+ });
922
+ }
923
+ BubbleChartYAxis.displayName = 'BubbleChart.YAxis';
924
+ BubbleChartYAxis.layer = 'overlay';
925
+ // Read by the root, which has to leave room for the labels before it lays the
926
+ // plot out.
927
+ BubbleChartYAxis.axis = 'y';
928
+ /**
929
+ * The touch target, the selection it drives, and the readout that follows it.
930
+ *
931
+ * A touch picks the nearest bubble whose own circle — or the `hitRadius` floor,
932
+ * whichever is larger — reaches the finger. Nearest rather than topmost,
933
+ * because where bubbles overlap the one drawn last is not the one being aimed
934
+ * at.
935
+ *
936
+ * The search runs on the UI thread over flat arrays of already-projected
937
+ * coordinates, and only the *index* of the winner crosses back into JS, and
938
+ * only when it changes. A drag across the plot therefore costs a handful of
939
+ * re-renders rather than one per frame.
940
+ *
941
+ * Distances are compared squared. The nearest bubble by distance is the nearest
942
+ * by distance-squared, and a square root per bubble per frame buys nothing.
943
+ */
944
+ function BubbleChartTooltip({
945
+ showLabel = true,
946
+ formatX,
947
+ formatY,
948
+ formatSize,
949
+ hitRadius = HIT_RADIUS,
950
+ className
951
+ }) {
952
+ const {
953
+ bubbles,
954
+ plot,
955
+ xExtent,
956
+ yExtent,
957
+ activeIndex,
958
+ activeIndexJS,
959
+ setActivePoint,
960
+ status
961
+ } = useChart('BubbleChart.Tooltip');
962
+
963
+ /*
964
+ * Every bubble, projected once, as parallel arrays.
965
+ *
966
+ * Parallel arrays rather than an array of objects because this is read inside
967
+ * a worklet: Reanimated has to copy whatever the gesture captures across to
968
+ * the UI thread, and four number arrays cross far more cheaply than a few
969
+ * hundred small objects.
970
+ *
971
+ * Projected against the *settled* extents rather than the tweening shared
972
+ * values. Hit-testing against a moving scale would mean rebuilding this on
973
+ * every frame of a domain animation, and a bubble being half a second stale
974
+ * during a transition is not something a finger can notice.
975
+ */
976
+ const hit = useMemo(() => {
977
+ const xs = [];
978
+ const ys = [];
979
+ const rs = [];
980
+ const limits = [];
981
+ const indices = [];
982
+ for (const bubble of bubbles) {
983
+ xs.push(xAt(bubble.x, plot, xExtent[0], xExtent[1]));
984
+ ys.push(yOf(bubble.y, plot, yExtent[0], yExtent[1]));
985
+ rs.push(bubble.r);
986
+ const reach = Math.max(bubble.r, hitRadius);
987
+ limits.push(reach * reach);
988
+ indices.push(bubble.index);
989
+ }
990
+ return {
991
+ xs,
992
+ ys,
993
+ rs,
994
+ limits,
995
+ indices
996
+ };
997
+ }, [bubbles, plot, xExtent, yExtent, hitRadius]);
998
+ const select = useMemo(() => index => {
999
+ if (index < 0) {
1000
+ setActivePoint(null);
1001
+ return;
1002
+ }
1003
+ setActivePoint(bubbles.find(bubble => bubble.index === index) ?? null);
1004
+ }, [bubbles, setActivePoint]);
1005
+
1006
+ /*
1007
+ * Built in one closure, and everything it captures is a plain array, a number
1008
+ * or a shared value. A worklet may only call another worklet, and the rule is
1009
+ * enforced by crashing rather than by warning — so the resolver is declared
1010
+ * here, next to its callers, rather than as a helper elsewhere in the file
1011
+ * where it would be easy to leave un-workletised.
1012
+ */
1013
+ const pan = useMemo(() => {
1014
+ const xs = hit.xs;
1015
+ const ys = hit.ys;
1016
+ const limits = hit.limits;
1017
+ const indices = hit.indices;
1018
+ const resolve = (px, py) => {
1019
+ 'worklet';
1020
+
1021
+ let bestIndex = -1;
1022
+ let best = Infinity;
1023
+ for (let i = 0; i < xs.length; i += 1) {
1024
+ const dx = xs[i] - px;
1025
+ const dy = ys[i] - py;
1026
+ const distance = dx * dx + dy * dy;
1027
+ // Each bubble reaches as far as it is big, so a large one is not
1028
+ // beaten by a small one that happens to be marginally nearer.
1029
+ if (distance <= limits[i] && distance < best) {
1030
+ best = distance;
1031
+ bestIndex = indices[i];
1032
+ }
1033
+ }
1034
+ if (bestIndex === activeIndex.value) return;
1035
+ activeIndex.value = bestIndex;
1036
+ runOnJS(select)(bestIndex);
1037
+ };
1038
+ const clear = () => {
1039
+ 'worklet';
1040
+
1041
+ if (activeIndex.value === -1) return;
1042
+ activeIndex.value = -1;
1043
+ runOnJS(select)(-1);
1044
+ };
1045
+ return Gesture.Pan().minDistance(0).onBegin(event => {
1046
+ 'worklet';
1047
+
1048
+ resolve(event.x, event.y);
1049
+ }).onUpdate(event => {
1050
+ 'worklet';
1051
+
1052
+ resolve(event.x, event.y);
1053
+ }).onFinalize(() => {
1054
+ 'worklet';
1055
+
1056
+ clear();
1057
+ });
1058
+ }, [hit, activeIndex, select]);
1059
+
1060
+ /*
1061
+ * The readout's own height, measured rather than assumed. It decides whether
1062
+ * the readout fits above the bubble, and how tall it is depends on whether
1063
+ * the row has a label and a size — a constant would either overlap a
1064
+ * three-line readout or reserve room a one-line one never uses.
1065
+ */
1066
+ const labelHeight = useSharedValue(0);
1067
+
1068
+ /*
1069
+ * Above the bubble, clear of its edge rather than of its centre, and clamped
1070
+ * inside the plot. Lifted by a constant it landed *on* the larger circles —
1071
+ * which are exactly the ones a finger is most likely to be resting on, so the
1072
+ * readout was hidden under the hand that summoned it.
1073
+ *
1074
+ * Where there is no room above, it goes below instead. Sliding it down to the
1075
+ * top edge of the plot would leave it over the bubble again.
1076
+ */
1077
+ const labelStyle = useAnimatedStyle(() => {
1078
+ const index = activeIndex.value;
1079
+ if (index < 0) return {
1080
+ opacity: 0
1081
+ };
1082
+ const at = hit.indices.indexOf(index);
1083
+ if (at < 0) return {
1084
+ opacity: 0
1085
+ };
1086
+ const x = hit.xs[at];
1087
+ const y = hit.ys[at];
1088
+ const r = hit.rs[at];
1089
+ const half = LABEL_WIDTH / 2;
1090
+ const tall = labelHeight.value;
1091
+ const above = y - r - LABEL_GAP - tall;
1092
+ const below = y + r + LABEL_GAP;
1093
+ const top = above >= plot.top ? above : below;
1094
+ return {
1095
+ opacity: 1,
1096
+ transform: [{
1097
+ translateX: Math.min(plot.left + plot.width - half, Math.max(plot.left + half, x)) - half
1098
+ }, {
1099
+ translateY: top
1100
+ }]
1101
+ };
1102
+ });
1103
+ const active = bubbles.find(bubble => bubble.index === activeIndexJS) ?? null;
1104
+ const fmtX = formatX ?? compactNumber;
1105
+ const fmtY = formatY ?? compactNumber;
1106
+ const fmtSize = formatSize ?? compactNumber;
1107
+ if (status === 'loading') return null;
1108
+ return /*#__PURE__*/_jsx(GestureDetector, {
1109
+ gesture: pan,
1110
+ children: /*#__PURE__*/_jsx(View, {
1111
+ style: StyleSheet.absoluteFill,
1112
+ children: showLabel ? /*#__PURE__*/_jsx(Animated.View, {
1113
+ pointerEvents: "none",
1114
+ style: [{
1115
+ position: 'absolute',
1116
+ left: 0,
1117
+ top: 0,
1118
+ width: LABEL_WIDTH
1119
+ }, labelStyle],
1120
+ children: active ? /*#__PURE__*/_jsxs(View, {
1121
+ onLayout: event => {
1122
+ labelHeight.value = event.nativeEvent.layout.height;
1123
+ },
1124
+ className: cn('rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
1125
+ children: [active.label ? /*#__PURE__*/_jsxs(View, {
1126
+ className: "flex-row items-center gap-1.5",
1127
+ children: [/*#__PURE__*/_jsx(View, {
1128
+ style: {
1129
+ width: 6,
1130
+ height: 6,
1131
+ borderRadius: 3,
1132
+ backgroundColor: active.color
1133
+ }
1134
+ }), /*#__PURE__*/_jsx(Text, {
1135
+ size: "xs",
1136
+ weight: "medium",
1137
+ numberOfLines: 1,
1138
+ children: active.label
1139
+ })]
1140
+ }) : null, /*#__PURE__*/_jsxs(Text, {
1141
+ size: "xs",
1142
+ muted: true,
1143
+ numberOfLines: 1,
1144
+ children: [fmtX(active.x), ", ", fmtY(active.y)]
1145
+ }), active.size !== null ? /*#__PURE__*/_jsx(Text, {
1146
+ size: "xs",
1147
+ muted: true,
1148
+ numberOfLines: 1,
1149
+ children: fmtSize(active.size)
1150
+ }) : null]
1151
+ }) : null
1152
+ }) : null
1153
+ })
1154
+ });
1155
+ }
1156
+ BubbleChartTooltip.displayName = 'BubbleChart.Tooltip';
1157
+ BubbleChartTooltip.layer = 'overlay';
1158
+ /**
1159
+ * A swatch and a name per bubble, for a chart whose circles are too small to
1160
+ * carry their own labels.
1161
+ *
1162
+ * Drawn **under** the plot rather than floating in a corner of it. A key that
1163
+ * overlays the drawing area competes with the bubbles for the space they are
1164
+ * plotted in, and on a square chart there is no corner that is reliably empty —
1165
+ * the position of a bubble is the data, so nowhere can be reserved for it.
1166
+ *
1167
+ * It lists rows rather than series, because in this chart a row *is* a
1168
+ * category. Use it instead of `BubbleChart.Labels`, not beside it — the same
1169
+ * names twice is the legend telling the reader what the plot already says.
1170
+ */
1171
+ function BubbleChartLegend({
1172
+ className,
1173
+ limit = 8,
1174
+ ...props
1175
+ }) {
1176
+ const {
1177
+ bubbles
1178
+ } = useChart('BubbleChart.Legend');
1179
+ const shown = bubbles.filter(bubble => bubble.label).slice(0, limit);
1180
+ if (!shown.length) return null;
1181
+ return /*#__PURE__*/_jsx(View, {
1182
+ ...props,
1183
+ style: [{
1184
+ pointerEvents: 'none'
1185
+ }, props.style],
1186
+ className: cn('flex-row flex-wrap items-center justify-center gap-x-3 gap-y-1 pt-3', className),
1187
+ children: shown.map(bubble => /*#__PURE__*/_jsxs(View, {
1188
+ className: "flex-row items-center gap-1.5",
1189
+ children: [/*#__PURE__*/_jsx(View, {
1190
+ style: {
1191
+ backgroundColor: bubble.color
1192
+ },
1193
+ className: "h-2 w-2 rounded-full"
1194
+ }), /*#__PURE__*/_jsx(Text, {
1195
+ size: "xs",
1196
+ muted: true,
1197
+ children: bubble.label
1198
+ })]
1199
+ }, bubble.index))
1200
+ });
1201
+ }
1202
+ BubbleChartLegend.displayName = 'BubbleChart.Legend';
1203
+ BubbleChartLegend.layer = 'footer';
1204
+
1205
+ /* -------------------------------------------------------------------------- */
1206
+ /* Header layer */
1207
+ /* -------------------------------------------------------------------------- */
1208
+
1209
+ /**
1210
+ * The strip above the plot: what the chart is of, what it currently reads, and
1211
+ * what the size of a circle means.
1212
+ *
1213
+ * The caption earns its place here more than on most charts. Two axes and an
1214
+ * area is three quantities, and a reader who is not told what the area is has
1215
+ * no way to work it out from the picture.
1216
+ *
1217
+ * The value is not derived here. A readout that follows the finger belongs to
1218
+ * whoever owns the data — take it from `onActivePointChange` and pass the
1219
+ * formatted string down.
1220
+ */
1221
+ function BubbleChartHeader({
1222
+ className,
1223
+ title,
1224
+ value,
1225
+ caption,
1226
+ children,
1227
+ ...props
1228
+ }) {
1229
+ return /*#__PURE__*/_jsxs(View, {
1230
+ ...props,
1231
+ className: cn('flex-row items-start justify-between gap-3 pb-3', className),
1232
+ children: [/*#__PURE__*/_jsxs(View, {
1233
+ className: "flex-1 gap-0.5",
1234
+ children: [title ? /*#__PURE__*/_jsx(Text, {
1235
+ size: "xs",
1236
+ muted: true,
1237
+ children: title
1238
+ }) : null, value ? /*#__PURE__*/_jsx(Text, {
1239
+ size: "xl",
1240
+ weight: "bold",
1241
+ children: value
1242
+ }) : null, caption ? /*#__PURE__*/_jsx(Text, {
1243
+ size: "xs",
1244
+ muted: true,
1245
+ children: caption
1246
+ }) : null]
1247
+ }), children ? /*#__PURE__*/_jsx(View, {
1248
+ className: "shrink pt-1",
1249
+ children: children
1250
+ }) : null]
1251
+ });
1252
+ }
1253
+ BubbleChartHeader.displayName = 'BubbleChart.Header';
1254
+ BubbleChartHeader.layer = 'header';
1255
+ export const BubbleChart = Object.assign(BubbleChartRoot, {
1256
+ Header: BubbleChartHeader,
1257
+ Grid: BubbleChartGrid,
1258
+ Bubbles: BubbleChartBubbles,
1259
+ Labels: BubbleChartLabels,
1260
+ Skeleton: BubbleChartSkeleton,
1261
+ XAxis: BubbleChartXAxis,
1262
+ YAxis: BubbleChartYAxis,
1263
+ Tooltip: BubbleChartTooltip,
1264
+ Legend: BubbleChartLegend
1265
+ });
1266
+ //# sourceMappingURL=index.js.map