panelui-native 0.43.0 → 0.46.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 (58) hide show
  1. package/README.md +2 -1
  2. package/lib/module/components/card/index.js +8 -154
  3. package/lib/module/components/card/index.js.map +1 -1
  4. package/lib/module/components/combobox/index.js +7 -1
  5. package/lib/module/components/combobox/index.js.map +1 -1
  6. package/lib/module/components/date-time-picker/index.js +254 -0
  7. package/lib/module/components/date-time-picker/index.js.map +1 -0
  8. package/lib/module/components/grid-item/index.js +486 -0
  9. package/lib/module/components/grid-item/index.js.map +1 -0
  10. package/lib/module/components/{kpi-chart → kpi}/index.js +62 -62
  11. package/lib/module/components/kpi/index.js.map +1 -0
  12. package/lib/module/components/line-chart/index.js +92 -10
  13. package/lib/module/components/line-chart/index.js.map +1 -1
  14. package/lib/module/components/pie-chart/index.js +625 -0
  15. package/lib/module/components/pie-chart/index.js.map +1 -0
  16. package/lib/module/components/scatter-chart/index.js +1173 -0
  17. package/lib/module/components/scatter-chart/index.js.map +1 -0
  18. package/lib/module/components/tabs/index.js +275 -33
  19. package/lib/module/components/tabs/index.js.map +1 -1
  20. package/lib/module/index.js +5 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/utils/chart.js +81 -0
  23. package/lib/module/utils/chart.js.map +1 -1
  24. package/lib/typescript/src/components/card/index.d.ts +0 -20
  25. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  26. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  27. package/lib/typescript/src/components/date-time-picker/index.d.ts +121 -0
  28. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -0
  29. package/lib/typescript/src/components/grid-item/index.d.ts +292 -0
  30. package/lib/typescript/src/components/grid-item/index.d.ts.map +1 -0
  31. package/lib/typescript/src/components/{kpi-chart → kpi}/index.d.ts +65 -65
  32. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -0
  33. package/lib/typescript/src/components/line-chart/index.d.ts +25 -1
  34. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  35. package/lib/typescript/src/components/pie-chart/index.d.ts +245 -0
  36. package/lib/typescript/src/components/pie-chart/index.d.ts.map +1 -0
  37. package/lib/typescript/src/components/scatter-chart/index.d.ts +309 -0
  38. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -0
  39. package/lib/typescript/src/components/tabs/index.d.ts +20 -2
  40. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  41. package/lib/typescript/src/index.d.ts +7 -3
  42. package/lib/typescript/src/index.d.ts.map +1 -1
  43. package/lib/typescript/src/utils/chart.d.ts +34 -0
  44. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/components/card/index.tsx +7 -209
  47. package/src/components/combobox/index.tsx +7 -1
  48. package/src/components/date-time-picker/index.tsx +375 -0
  49. package/src/components/grid-item/index.tsx +515 -0
  50. package/src/components/{kpi-chart → kpi}/index.tsx +85 -85
  51. package/src/components/line-chart/index.tsx +98 -8
  52. package/src/components/pie-chart/index.tsx +863 -0
  53. package/src/components/scatter-chart/index.tsx +1401 -0
  54. package/src/components/tabs/index.tsx +305 -35
  55. package/src/index.ts +66 -18
  56. package/src/utils/chart.ts +110 -0
  57. package/lib/module/components/kpi-chart/index.js.map +0 -1
  58. package/lib/typescript/src/components/kpi-chart/index.d.ts.map +0 -1
@@ -0,0 +1,1173 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ScatterChart — two quantities against each other, drawn on the UI thread.
5
+ *
6
+ * Every other chart in this library spaces its points evenly along the x-axis,
7
+ * because their x is a position: twelve months are twelve equal steps whatever
8
+ * the gaps between the dates behind them. A scatter plot is the one shape where
9
+ * that is wrong. Both coordinates are *measured*, and the reader is being asked
10
+ * to look for a relationship between them — spread the points evenly and the
11
+ * relationship is the one thing you have thrown away.
12
+ *
13
+ * So this chart carries an x-domain as well as a y-domain, and both are tweened
14
+ * when the data changes.
15
+ *
16
+ * ```tsx
17
+ * <ScatterChart data={sessions} xDataKey="spend">
18
+ * <ScatterChart.Grid />
19
+ * <ScatterChart.Points dataKey="revenue" />
20
+ * <ScatterChart.XAxis />
21
+ * <ScatterChart.YAxis />
22
+ * <ScatterChart.Tooltip />
23
+ * </ScatterChart>
24
+ * ```
25
+ *
26
+ * As elsewhere, there are two layers and the parts sort themselves into the
27
+ * right one: the geometry is SVG, and anything with text or a gesture on it is
28
+ * a React Native view over the top. SVG text ignores the platform's text
29
+ * scaling and the theme's font, and a gesture handler cannot be attached to an
30
+ * SVG node at all.
31
+ *
32
+ * **Finding a point.** A crosshair that snaps to an x index — the way a line
33
+ * chart's does — has nothing to snap to here, because there is no shared x and
34
+ * two points can sit at the same one. Instead the nearest point to the finger
35
+ * is resolved by distance, on the UI thread, and only within a radius: a touch
36
+ * in an empty corner of the plot selects nothing rather than lighting up
37
+ * whichever point happens to be least far away. The radius is generous, because
38
+ * the points are a few pixels across and a fingertip is not.
39
+ *
40
+ * Colours come from the `--color-chart-*` tokens, so a chart follows the active
41
+ * theme. Nothing here hardcodes a hex.
42
+ */
43
+ import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
44
+ import { StyleSheet, View } from 'react-native';
45
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
46
+ import Animated, { Easing, runOnJS, useAnimatedProps, useAnimatedStyle, useDerivedValue, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
47
+ import Svg, { Circle, G, Line as SvgLine } from 'react-native-svg';
48
+ import { useCSSVariable } from 'uniwind';
49
+ import { Text } from "../../primitives/text.js";
50
+ import { compactNumber, useSeriesColor, xAt, yOf } from "../../utils/chart.js";
51
+ import { cn } from "../../utils/cn.js";
52
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
53
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
54
+ const AnimatedG = Animated.createAnimatedComponent(G);
55
+
56
+ /**
57
+ * How much of the reveal is spent handing out the points' start times. The rest
58
+ * is the window each one gets, so the whole field still lands inside the one
59
+ * duration however many points there are.
60
+ */
61
+ const STAGGER = 0.4;
62
+
63
+ /** Milliseconds for the placeholder field to dissolve once the data arrives. */
64
+ const SKELETON_FADE = 220;
65
+
66
+ /** Milliseconds for a point to swell as it is selected, and settle as it is not. */
67
+ const SELECT_DURATION = 140;
68
+
69
+ /**
70
+ * A point arriving: up past its size and back to it.
71
+ *
72
+ * A dot that simply grows to its radius reads as the chart still loading right
73
+ * up to the last frame. The small overshoot is what makes it read as landing.
74
+ */
75
+ function landing(t) {
76
+ 'worklet';
77
+
78
+ const back = 1.3;
79
+ const u = t - 1;
80
+ return 1 + (back + 1) * u * u * u + back * u * u;
81
+ }
82
+
83
+ /** Room left around the plot for the axis labels and the outermost dots. */
84
+ const PADDING = {
85
+ top: 14,
86
+ right: 14,
87
+ bottom: 22,
88
+ left: 14
89
+ };
90
+
91
+ /** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
92
+ const Y_AXIS_WIDTH = 44;
93
+
94
+ /** Gap between the value labels and the plot they sit beside. */
95
+ const Y_AXIS_GUTTER = 6;
96
+
97
+ /** Line height of an `xs` label, for centring one on the grid line it names. */
98
+ const AXIS_LABEL_HEIGHT = 16;
99
+
100
+ /** Box each x label is centred in, so a long number is ellipsised not shoved. */
101
+ const POINT_LABEL_WIDTH = 56;
102
+
103
+ /** Width of the readout that floats by the selected point. */
104
+ const LABEL_WIDTH = 132;
105
+
106
+ /** How far the readout is lifted, to clear the point it describes. */
107
+ const LABEL_HEIGHT = 52;
108
+
109
+ /**
110
+ * How far from a point a touch still counts as being on it, in points.
111
+ *
112
+ * Sized for a fingertip rather than for the dot. Apple and Android both put the
113
+ * minimum comfortable target at around 44pt, and a scatter point is nearer 7 —
114
+ * without a hit radius the chart is only usable with a mouse it will never see.
115
+ */
116
+ const HIT_RADIUS = 32;
117
+
118
+ /** One plotted point, resolved back to the row it came from. */
119
+
120
+ const ScatterChartContext = /*#__PURE__*/createContext(null);
121
+ function useChart(component) {
122
+ const context = useContext(ScatterChartContext);
123
+ if (!context) {
124
+ throw new Error(`${component} must be used within a <ScatterChart>`);
125
+ }
126
+ return context;
127
+ }
128
+
129
+ /**
130
+ * The selected point, for something rendered *inside* the chart.
131
+ *
132
+ * A readout usually belongs in the card's header, which is outside this
133
+ * provider — use `onActivePointChange` for that. A hook cannot reach up out of
134
+ * the subtree it is called in.
135
+ */
136
+ export function useScatterChart() {
137
+ const {
138
+ activePoint,
139
+ xDataKey
140
+ } = useChart('useScatterChart');
141
+ return {
142
+ activePoint,
143
+ xDataKey
144
+ };
145
+ }
146
+
147
+ /** Imperative handle: re-run the reveal on demand, for a "replay" control. */
148
+
149
+ const ScatterChartRoot = /*#__PURE__*/forwardRef(function ScatterChartRoot({
150
+ className,
151
+ data,
152
+ xDataKey = 'x',
153
+ status = 'ready',
154
+ aspectRatio = 1,
155
+ animationDuration = 900,
156
+ domainDuration = 500,
157
+ xDomain,
158
+ yDomain,
159
+ onActivePointChange,
160
+ compact = false,
161
+ children,
162
+ ...props
163
+ }, ref) {
164
+ const [size, setSize] = useState({
165
+ width: 0,
166
+ height: 0
167
+ });
168
+ const [series, setSeries] = useState([]);
169
+ const [activePoint, setActivePointState] = useState(null);
170
+ const reveal = useSharedValue(0);
171
+ const xMin = useSharedValue(0);
172
+ const xMax = useSharedValue(0);
173
+ const yMin = useSharedValue(0);
174
+ const yMax = useSharedValue(0);
175
+ const activeId = useSharedValue('');
176
+ const reducedMotion = useReducedMotion();
177
+ const registerSeries = useMemo(() => (key, color) => setSeries(current => {
178
+ const existing = current.find(([k]) => k === key);
179
+ if (existing?.[1] === color) return current;
180
+ return [...current.filter(([k]) => k !== key), [key, color]];
181
+ }), []);
182
+ const unregisterSeries = useMemo(() => key => setSeries(current => current.filter(([k]) => k !== key)), []);
183
+ const hasYAxis = useMemo(() => {
184
+ let found = false;
185
+ Children.forEach(children, child => {
186
+ if (/*#__PURE__*/isValidElement(child) && child.type.axis === 'y') {
187
+ found = true;
188
+ }
189
+ });
190
+ return found;
191
+ }, [children]);
192
+ const pad = compact ? {
193
+ top: 4,
194
+ right: 4,
195
+ bottom: 4,
196
+ left: 4
197
+ } : {
198
+ ...PADDING,
199
+ left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left
200
+ };
201
+ const plot = {
202
+ left: pad.left,
203
+ top: pad.top,
204
+ width: Math.max(size.width - pad.left - pad.right, 0),
205
+ height: Math.max(size.height - pad.top - pad.bottom, 0)
206
+ };
207
+ const seriesKeys = series.map(([key]) => key).join('|');
208
+
209
+ /*
210
+ * Both extents in one pass. A scatter plot's x is a measured quantity, so
211
+ * unlike the rest of the family it needs a domain of its own rather than a
212
+ * count of positions.
213
+ *
214
+ * Neither axis is floored at zero. An area chart is floored because a
215
+ * filled region floating above the baseline reads as a shape rather than a
216
+ * quantity — but a scatter plot's subject is the *spread*, and forcing a
217
+ * cluster of values between 80 and 90 to share a frame with zero squashes
218
+ * it into a smudge in one corner and hides the very thing being plotted.
219
+ */
220
+ const extents = useMemo(() => {
221
+ const keys = seriesKeys ? seriesKeys.split('|') : [];
222
+ let lowX = Infinity;
223
+ let highX = -Infinity;
224
+ let lowY = Infinity;
225
+ let highY = -Infinity;
226
+ for (const row of data) {
227
+ const x = row[xDataKey];
228
+ if (typeof x !== 'number' || Number.isNaN(x)) continue;
229
+ for (const key of keys) {
230
+ const y = row[key];
231
+ // A row with no reading for this series is not a point at the
232
+ // origin — it is not a point at all, and must not stretch the axes.
233
+ if (typeof y !== 'number' || Number.isNaN(y)) continue;
234
+ if (x < lowX) lowX = x;
235
+ if (x > highX) highX = x;
236
+ if (y < lowY) lowY = y;
237
+ if (y > highY) highY = y;
238
+ }
239
+ }
240
+ return {
241
+ x: xDomain ?? padExtent(lowX, highX),
242
+ y: yDomain ?? padExtent(lowY, highY)
243
+ };
244
+ }, [data, xDataKey, seriesKeys, xDomain, yDomain]);
245
+ const loading = status === 'loading';
246
+ useEffect(() => {
247
+ if (loading) return;
248
+ const [x0, x1] = extents.x;
249
+ const [y0, y1] = extents.y;
250
+ // The first domain lands without a tween: there is no previous scale to
251
+ // move from, and animating up from zero reads as the numbers changing.
252
+ const first = xMin.value === 0 && xMax.value === 0 && yMin.value === 0 && yMax.value === 0;
253
+ if (first || reducedMotion) {
254
+ xMin.value = x0;
255
+ xMax.value = x1;
256
+ yMin.value = y0;
257
+ yMax.value = y1;
258
+ return;
259
+ }
260
+ xMin.value = withTiming(x0, {
261
+ duration: domainDuration
262
+ });
263
+ xMax.value = withTiming(x1, {
264
+ duration: domainDuration
265
+ });
266
+ yMin.value = withTiming(y0, {
267
+ duration: domainDuration
268
+ });
269
+ yMax.value = withTiming(y1, {
270
+ duration: domainDuration
271
+ });
272
+ }, [extents, loading, reducedMotion, domainDuration, xMin, xMax, yMin, yMax]);
273
+ const revealed = useRef(false);
274
+ const playReveal = useMemo(() => () => {
275
+ if (reducedMotion) {
276
+ reveal.value = 1;
277
+ return;
278
+ }
279
+ reveal.value = 0;
280
+ /*
281
+ * Eased out rather than in and out. Each point is given a slice of this
282
+ * one clock, so an ease that dawdles at the start spends it on the
283
+ * first few points and leaves the rest to arrive in a rush.
284
+ */
285
+ reveal.value = withTiming(1, {
286
+ duration: animationDuration,
287
+ easing: Easing.out(Easing.cubic)
288
+ });
289
+ }, [reducedMotion, animationDuration, reveal]);
290
+
291
+ /*
292
+ * Armed again every time the chart goes back to loading, so a chart that
293
+ * reloads animates on the second pass as well as the first. Left latched,
294
+ * the guard made "loading" a state the chart could only leave once.
295
+ */
296
+ useEffect(() => {
297
+ if (loading) {
298
+ revealed.current = false;
299
+ reveal.value = 0;
300
+ return;
301
+ }
302
+ if (revealed.current || plot.width <= 0 || !data.length) return;
303
+ revealed.current = true;
304
+ playReveal();
305
+ }, [loading, plot.width, data.length, playReveal, reveal]);
306
+ useImperativeHandle(ref, () => ({
307
+ replay: playReveal
308
+ }), [playReveal]);
309
+
310
+ // One place the selection lands, so the chart's own children and a readout
311
+ // outside it never disagree about which point is active.
312
+ const setActivePoint = useMemo(() => point => {
313
+ setActivePointState(point);
314
+ onActivePointChange?.(point);
315
+ }, [onActivePointChange]);
316
+ const onLayout = event => {
317
+ const {
318
+ width,
319
+ height
320
+ } = event.nativeEvent.layout;
321
+ setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
322
+ width,
323
+ height
324
+ });
325
+ props.onLayout?.(event);
326
+ };
327
+ const context = useMemo(() => ({
328
+ data,
329
+ xDataKey,
330
+ plot,
331
+ status,
332
+ series,
333
+ registerSeries,
334
+ unregisterSeries,
335
+ xMin,
336
+ xMax,
337
+ yMin,
338
+ yMax,
339
+ xExtent: extents.x,
340
+ yExtent: extents.y,
341
+ activeId,
342
+ activePoint,
343
+ setActivePoint,
344
+ reveal
345
+ }),
346
+ // `plot` is rebuilt every render from `size`, so it is compared by value.
347
+ // eslint-disable-next-line react-hooks/exhaustive-deps
348
+ [data, xDataKey, plot.width, plot.height, plot.left, plot.top, status, series, registerSeries, unregisterSeries, xMin, xMax, yMin, yMax, extents, activeId, activePoint, setActivePoint, reveal]);
349
+ const {
350
+ svg,
351
+ overlay,
352
+ header
353
+ } = partition(children);
354
+
355
+ /*
356
+ * Two views, because the header is not part of the plot. `aspectRatio` and
357
+ * the layout measurement belong to the drawing area alone — measured on the
358
+ * outer view they would take in the header too, and the plot would lose as
359
+ * much height as the readout took while still claiming the shape asked for.
360
+ */
361
+ return /*#__PURE__*/_jsx(ScatterChartContext.Provider, {
362
+ value: context,
363
+ children: /*#__PURE__*/_jsxs(View, {
364
+ ...props,
365
+ style: props.style,
366
+ className: cn('w-full', className),
367
+ children: [header, /*#__PURE__*/_jsx(View, {
368
+ onLayout: onLayout,
369
+ style: {
370
+ aspectRatio
371
+ },
372
+ className: "w-full",
373
+ children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
374
+ children: [/*#__PURE__*/_jsx(Svg, {
375
+ width: "100%",
376
+ height: "100%",
377
+ style: StyleSheet.absoluteFill,
378
+ children: svg
379
+ }), overlay]
380
+ }) : null
381
+ })]
382
+ })
383
+ });
384
+ });
385
+ ScatterChartRoot.displayName = 'ScatterChart';
386
+
387
+ /**
388
+ * An extent with a little air around it, and a usable one for the degenerate
389
+ * cases — no data at all, or every reading identical. A domain of zero width
390
+ * divides by zero and puts every point on the same edge.
391
+ */
392
+ function padExtent(min, max) {
393
+ if (min === Infinity) return [0, 1];
394
+ if (min === max) return [min - 1, max + 1];
395
+ const pad = (max - min) * 0.08;
396
+ return [min - pad, max + pad];
397
+ }
398
+
399
+ /** Sorts the children into the SVG tree and the view layer over it. */
400
+ function partition(children) {
401
+ const svg = [];
402
+ const overlay = [];
403
+ const header = [];
404
+ Children.forEach(children, (child, index) => {
405
+ if (! /*#__PURE__*/isValidElement(child)) return;
406
+ const layer = child.type.layer ?? 'svg';
407
+ (layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(
408
+ /*#__PURE__*/
409
+ // Children of a `Children.forEach` need keys of their own once they are
410
+ // put into a new array.
411
+ _jsx(ChildSlot, {
412
+ children: child
413
+ }, index));
414
+ });
415
+ return {
416
+ svg,
417
+ overlay,
418
+ header
419
+ };
420
+ }
421
+
422
+ /** Identity wrapper, purely so the partitioned arrays can carry keys. */
423
+ function ChildSlot({
424
+ children
425
+ }) {
426
+ return /*#__PURE__*/_jsx(_Fragment, {
427
+ children: children
428
+ });
429
+ }
430
+
431
+ /* -------------------------------------------------------------------------- */
432
+ /* SVG layer */
433
+ /* -------------------------------------------------------------------------- */
434
+
435
+ /** Reference lines both ways. Drawn under everything, and not part of the reveal. */
436
+ function ScatterChartGrid({
437
+ rows = 4,
438
+ columns = 4,
439
+ color,
440
+ dashArray = '4,6',
441
+ opacity = 1
442
+ }) {
443
+ const {
444
+ plot
445
+ } = useChart('ScatterChart.Grid');
446
+ const token = useCSSVariable('--color-border');
447
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
448
+ return /*#__PURE__*/_jsxs(G, {
449
+ opacity: opacity,
450
+ children: [Array.from({
451
+ length: rows + 1
452
+ }, (_unused, index) => {
453
+ const y = plot.top + plot.height / rows * index;
454
+ return /*#__PURE__*/_jsx(SvgLine, {
455
+ x1: plot.left,
456
+ x2: plot.left + plot.width,
457
+ y1: y,
458
+ y2: y,
459
+ stroke: stroke,
460
+ strokeWidth: 1,
461
+ strokeDasharray: dashArray
462
+ }, `r${index}`);
463
+ }), Array.from({
464
+ length: columns + 1
465
+ }, (_unused, index) => {
466
+ const x = plot.left + plot.width / columns * index;
467
+ return /*#__PURE__*/_jsx(SvgLine, {
468
+ x1: x,
469
+ x2: x,
470
+ y1: plot.top,
471
+ y2: plot.top + plot.height,
472
+ stroke: stroke,
473
+ strokeWidth: 1,
474
+ strokeDasharray: dashArray
475
+ }, `c${index}`);
476
+ })]
477
+ });
478
+ }
479
+ ScatterChartGrid.displayName = 'ScatterChart.Grid';
480
+ ScatterChartGrid.layer = 'svg';
481
+ /** One series, as a field of dots. */
482
+ function ScatterChartPoints({
483
+ dataKey,
484
+ color,
485
+ colorIndex = 1,
486
+ size = 4.5,
487
+ sizeKey,
488
+ sizeRange = [3, 14],
489
+ opacity = 0.75
490
+ }) {
491
+ const {
492
+ data,
493
+ xDataKey,
494
+ plot,
495
+ xMin,
496
+ xMax,
497
+ yMin,
498
+ yMax,
499
+ status,
500
+ activeId,
501
+ registerSeries,
502
+ unregisterSeries,
503
+ reveal
504
+ } = useChart('ScatterChart.Points');
505
+ const fill = useSeriesColor(color, colorIndex);
506
+ useEffect(() => {
507
+ registerSeries(dataKey, fill);
508
+ return () => unregisterSeries(dataKey);
509
+ }, [dataKey, fill, registerSeries, unregisterSeries]);
510
+ const loading = status === 'loading';
511
+
512
+ // The size scale is over the whole series, so one row's bubble means the same
513
+ // thing as another's. Recomputed only when the data or the key changes.
514
+ const sizeExtent = useMemo(() => {
515
+ if (!sizeKey) return null;
516
+ let min = Infinity;
517
+ let max = -Infinity;
518
+ for (const row of data) {
519
+ const value = row[sizeKey];
520
+ if (typeof value !== 'number' || Number.isNaN(value)) continue;
521
+ if (value < min) min = value;
522
+ if (value > max) max = value;
523
+ }
524
+ return min === Infinity ? null : [min, max];
525
+ }, [data, sizeKey]);
526
+ const points = useMemo(() => data.map((row, index) => {
527
+ const x = row[xDataKey];
528
+ const y = row[dataKey];
529
+ if (typeof x !== 'number' || Number.isNaN(x)) return null;
530
+ if (typeof y !== 'number' || Number.isNaN(y)) return null;
531
+ return {
532
+ index,
533
+ x,
534
+ y,
535
+ r: radiusFor(row[sizeKey ?? ''], sizeExtent, sizeRange, size)
536
+ };
537
+ }).filter(point => point !== null), [data, xDataKey, dataKey, sizeKey, sizeExtent, sizeRange, size]);
538
+ if (loading) return null;
539
+ return /*#__PURE__*/_jsx(G, {
540
+ children: points.map((point, order) => /*#__PURE__*/_jsx(Dot, {
541
+ id: `${dataKey}:${point.index}`,
542
+ x: point.x,
543
+ y: point.y,
544
+ r: point.r,
545
+ plot: plot,
546
+ xMin: xMin,
547
+ xMax: xMax,
548
+ yMin: yMin,
549
+ yMax: yMax,
550
+ fill: fill,
551
+ opacity: opacity,
552
+ activeId: activeId,
553
+ reveal: reveal,
554
+ order: order,
555
+ total: points.length
556
+ }, point.index))
557
+ });
558
+ }
559
+ ScatterChartPoints.displayName = 'ScatterChart.Points';
560
+ ScatterChartPoints.layer = 'svg';
561
+
562
+ /**
563
+ * A value's radius on the bubble scale.
564
+ *
565
+ * The value maps to *area* and the radius is taken from it, so a point holding
566
+ * twice the value carries twice the ink rather than four times it.
567
+ */
568
+ function radiusFor(value, extent, range, fallback) {
569
+ if (!extent || typeof value !== 'number' || Number.isNaN(value)) return fallback;
570
+ const [min, max] = extent;
571
+ const [rMin, rMax] = range;
572
+ const ratio = max === min ? 1 : (value - min) / (max - min);
573
+ const area = rMin * rMin + ratio * (rMax * rMax - rMin * rMin);
574
+ return Math.sqrt(area);
575
+ }
576
+
577
+ /**
578
+ * One point. Its position follows both domain tweens, so a data change moves
579
+ * the whole field to the new scale rather than cutting to it.
580
+ *
581
+ * It arrives by growing in place, on its own slice of the shared reveal. The
582
+ * alternative — sweeping a clip across the plot, which is what the line and
583
+ * area charts do — is right for a series read along the x-axis and wrong here:
584
+ * a wipe gives the reader a direction to read the arrival in, and a scatter
585
+ * plot has none. Position is the whole message, so a point may only ever appear
586
+ * where it belongs.
587
+ */
588
+ function Dot({
589
+ id,
590
+ x,
591
+ y,
592
+ r,
593
+ plot,
594
+ xMin,
595
+ xMax,
596
+ yMin,
597
+ yMax,
598
+ fill,
599
+ opacity,
600
+ activeId,
601
+ reveal,
602
+ order,
603
+ total
604
+ }) {
605
+ /*
606
+ * The selection, as something that moves. It was a hard switch on
607
+ * `activeId`, so the point it named jumped half as big again between one
608
+ * frame and the next while every neighbour stayed put — read as a glitch
609
+ * rather than as a response to the finger.
610
+ */
611
+ const selected = useDerivedValue(() => withTiming(activeId.value === id ? 1 : 0, {
612
+ duration: SELECT_DURATION
613
+ }));
614
+
615
+ // Where in the reveal this point starts. Spread over `STAGGER`, so the field
616
+ // settles as a field rather than switching on all at once.
617
+ const start = total > 1 ? order / total * STAGGER : 0;
618
+ const animatedProps = useAnimatedProps(() => {
619
+ const arrived = Math.max(0, Math.min(1, (reveal.value - start) / (1 - STAGGER)));
620
+ // The selected point swells and goes solid. Both, rather than one: a size
621
+ // change alone is easy to miss among neighbours, and an opacity change
622
+ // alone is invisible wherever the points already overlap.
623
+ const swell = 1 + 0.5 * selected.value;
624
+ return {
625
+ cx: xAt(x, plot, xMin.value, xMax.value),
626
+ cy: yOf(y, plot, yMin.value, yMax.value),
627
+ r: r * landing(arrived) * swell,
628
+ // Ahead of the size, so a point is legible by the time it stops moving
629
+ // rather than fading in for the whole of its arrival.
630
+ fillOpacity: Math.min(1, arrived * 2) * (opacity + (1 - opacity) * selected.value)
631
+ };
632
+ });
633
+ return /*#__PURE__*/_jsx(AnimatedCircle, {
634
+ animatedProps: animatedProps,
635
+ fill: fill
636
+ });
637
+ }
638
+ /**
639
+ * The loading state: a still field of muted dots where the data will be.
640
+ *
641
+ * Deliberately still. A shimmer over a field of dots reads as the points
642
+ * *moving*, which is the one thing a scatter plot must never appear to do —
643
+ * position is the entire message, and a loading state that implies it is
644
+ * changing is a loading state that lies.
645
+ *
646
+ * The layout is deterministic rather than random, so it does not reshuffle on
647
+ * every render of a component that may re-render several times while waiting.
648
+ *
649
+ * Still is not the same as abrupt, though. It dissolves as the real points grow
650
+ * in, and outlives the status change by exactly that long — cut at the frame the
651
+ * data lands, the placeholder disappears before anything has replaced it and the
652
+ * plot is briefly empty.
653
+ */
654
+ function ScatterChartSkeleton({
655
+ count = 24,
656
+ color
657
+ }) {
658
+ const {
659
+ plot,
660
+ status
661
+ } = useChart('ScatterChart.Skeleton');
662
+ const token = useCSSVariable('--color-skeleton');
663
+ const fill = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
664
+ const reducedMotion = useReducedMotion();
665
+ const loading = status === 'loading';
666
+ const fade = useSharedValue(1);
667
+ // Mounted a beat longer than `loading`, so there is something to fade.
668
+ const [mounted, setMounted] = useState(loading);
669
+ useEffect(() => {
670
+ if (loading) {
671
+ fade.value = 1;
672
+ setMounted(true);
673
+ return;
674
+ }
675
+ if (reducedMotion) {
676
+ setMounted(false);
677
+ return;
678
+ }
679
+ fade.value = withTiming(0, {
680
+ duration: SKELETON_FADE
681
+ }, finished => {
682
+ if (finished) runOnJS(setMounted)(false);
683
+ });
684
+ }, [loading, reducedMotion, fade]);
685
+ const dots = useMemo(() => {
686
+ // A cheap deterministic scatter: two irrational-ish strides that do not
687
+ // share a factor, so the points spread instead of falling into a lattice.
688
+ return Array.from({
689
+ length: count
690
+ }, (_unused, index) => ({
691
+ key: index,
692
+ fx: index * 0.618 % 1 * 0.92 + 0.04,
693
+ fy: index * 0.379 % 1 * 0.92 + 0.04
694
+ }));
695
+ }, [count]);
696
+ const animatedProps = useAnimatedProps(() => ({
697
+ opacity: fade.value
698
+ }));
699
+ if (!mounted) return null;
700
+ return /*#__PURE__*/_jsx(AnimatedG, {
701
+ animatedProps: animatedProps,
702
+ children: dots.map(dot => /*#__PURE__*/_jsx(Circle, {
703
+ cx: plot.left + dot.fx * plot.width,
704
+ cy: plot.top + dot.fy * plot.height,
705
+ r: 4.5,
706
+ fill: fill
707
+ }, dot.key))
708
+ });
709
+ }
710
+ ScatterChartSkeleton.displayName = 'ScatterChart.Skeleton';
711
+ ScatterChartSkeleton.layer = 'svg';
712
+
713
+ /* -------------------------------------------------------------------------- */
714
+ /* Overlay layer */
715
+ /* -------------------------------------------------------------------------- */
716
+
717
+ /**
718
+ * The x labels, evenly along the axis.
719
+ *
720
+ * Evenly spaced here — unlike a line chart's, where each label sits on the point
721
+ * it names — because this axis is a continuous scale rather than a list of
722
+ * rows. There is no point to sit on.
723
+ */
724
+ function ScatterChartXAxis({
725
+ ticks = 4,
726
+ format,
727
+ className
728
+ }) {
729
+ const {
730
+ plot,
731
+ xExtent
732
+ } = useChart('ScatterChart.XAxis');
733
+ const labels = useMemo(() => {
734
+ const [min, max] = xExtent;
735
+ if (min === 0 && max === 0) return [];
736
+ return Array.from({
737
+ length: ticks + 1
738
+ }, (_unused, index) => {
739
+ const value = min + (max - min) * index / ticks;
740
+ return {
741
+ key: index,
742
+ value,
743
+ text: format ? format(value) : compactNumber(value)
744
+ };
745
+ });
746
+ }, [xExtent, ticks, format]);
747
+ return /*#__PURE__*/_jsx(View, {
748
+ style: {
749
+ position: 'absolute',
750
+ inset: 0,
751
+ pointerEvents: 'none'
752
+ },
753
+ className: cn(className),
754
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
755
+ size: "xs",
756
+ muted: true,
757
+ numberOfLines: 1,
758
+ style: {
759
+ position: 'absolute',
760
+ bottom: 0,
761
+ // Centred on its tick, then held inside the chart. The first and
762
+ // last ticks sit on the plot's own edges, so a box centred on them
763
+ // hangs half its width off the side — the clamp slides those two
764
+ // back in rather than letting the numbers leave the frame.
765
+ left: Math.max(0, Math.min(plot.left + plot.width / ticks * label.key - POINT_LABEL_WIDTH / 2, plot.left + plot.width + PADDING.right - POINT_LABEL_WIDTH)),
766
+ width: POINT_LABEL_WIDTH,
767
+ textAlign: 'center'
768
+ },
769
+ children: label.text
770
+ }, label.key))
771
+ });
772
+ }
773
+ ScatterChartXAxis.displayName = 'ScatterChart.XAxis';
774
+ ScatterChartXAxis.layer = 'overlay';
775
+ /** Value labels down the side, one per grid line. */
776
+ function ScatterChartYAxis({
777
+ ticks = 4,
778
+ format,
779
+ className
780
+ }) {
781
+ const {
782
+ plot,
783
+ yExtent
784
+ } = useChart('ScatterChart.YAxis');
785
+ const labels = useMemo(() => {
786
+ const [min, max] = yExtent;
787
+ if (min === 0 && max === 0) return [];
788
+ return Array.from({
789
+ length: ticks + 1
790
+ }, (_unused, index) => {
791
+ const value = max - (max - min) * index / ticks;
792
+ return {
793
+ key: index,
794
+ text: format ? format(value) : compactNumber(value)
795
+ };
796
+ });
797
+ }, [yExtent, ticks, format]);
798
+ return /*#__PURE__*/_jsx(View, {
799
+ pointerEvents: "none",
800
+ style: {
801
+ position: 'absolute',
802
+ left: 0,
803
+ // Centred on the grid line each label names: the strip is lifted half a
804
+ // label and grown by a whole one, so `justify-between` lands the text's
805
+ // middle on the line rather than its top edge on the first.
806
+ top: plot.top - AXIS_LABEL_HEIGHT / 2,
807
+ height: plot.height + AXIS_LABEL_HEIGHT,
808
+ width: Math.max(plot.left - Y_AXIS_GUTTER, 0)
809
+ },
810
+ className: cn('items-end justify-between', className),
811
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
812
+ size: "xs",
813
+ muted: true,
814
+ numberOfLines: 1,
815
+ children: label.text
816
+ }, label.key))
817
+ });
818
+ }
819
+ ScatterChartYAxis.displayName = 'ScatterChart.YAxis';
820
+ ScatterChartYAxis.layer = 'overlay';
821
+ // Read by the root, which has to leave room for the labels before it lays the
822
+ // plot out.
823
+ ScatterChartYAxis.axis = 'y';
824
+ /**
825
+ * The touch target, the selection it drives, and the readout that follows it.
826
+ *
827
+ * A line chart's crosshair snaps to an x index. That is not available here:
828
+ * there is no shared x, and two points can sit on the same one. So the nearest
829
+ * point is found by distance instead — and only within `hitRadius`, so a touch
830
+ * in an empty corner selects nothing rather than lighting up whichever point is
831
+ * least far away.
832
+ *
833
+ * The search runs on the UI thread over a flat array of already-projected
834
+ * coordinates, and only the *identity* of the winner crosses back into JS, and
835
+ * only when it changes. A drag across the plot therefore costs a handful of
836
+ * re-renders rather than one per frame.
837
+ *
838
+ * Distances are compared squared. The nearest point by distance is the nearest
839
+ * by distance-squared, and a square root per point per frame buys nothing.
840
+ */
841
+ function ScatterChartTooltip({
842
+ showLabel = true,
843
+ formatX,
844
+ formatY,
845
+ formatTitle,
846
+ hitRadius = HIT_RADIUS
847
+ }) {
848
+ const {
849
+ data,
850
+ xDataKey,
851
+ plot,
852
+ series,
853
+ xExtent,
854
+ yExtent,
855
+ activeId,
856
+ activePoint,
857
+ setActivePoint,
858
+ status
859
+ } = useChart('ScatterChart.Tooltip');
860
+ const seriesKeys = series.map(([key]) => key).join('|');
861
+
862
+ /*
863
+ * Every point in the chart, projected once, as parallel arrays.
864
+ *
865
+ * Parallel arrays rather than an array of objects because this is read inside
866
+ * a worklet: Reanimated has to copy whatever the gesture captures across to
867
+ * the UI thread, and three number arrays cross far more cheaply than a few
868
+ * hundred small objects.
869
+ *
870
+ * Projected against the *settled* extents rather than the tweening shared
871
+ * values. Hit-testing against a moving scale would mean rebuilding this on
872
+ * every frame of a domain animation, and a point being half a second stale
873
+ * during a transition is not something a finger can notice.
874
+ */
875
+ const hit = useMemo(() => {
876
+ const keys = seriesKeys ? seriesKeys.split('|') : [];
877
+ const xs = [];
878
+ const ys = [];
879
+ const ids = [];
880
+ const indices = [];
881
+ const owners = [];
882
+ for (const key of keys) {
883
+ for (let index = 0; index < data.length; index += 1) {
884
+ const row = data[index];
885
+ const x = row[xDataKey];
886
+ const y = row[key];
887
+ if (typeof x !== 'number' || Number.isNaN(x)) continue;
888
+ if (typeof y !== 'number' || Number.isNaN(y)) continue;
889
+ xs.push(xAt(x, plot, xExtent[0], xExtent[1]));
890
+ ys.push(yOf(y, plot, yExtent[0], yExtent[1]));
891
+ ids.push(`${key}:${index}`);
892
+ indices.push(index);
893
+ owners.push(key);
894
+ }
895
+ }
896
+ return {
897
+ xs,
898
+ ys,
899
+ ids,
900
+ indices,
901
+ owners
902
+ };
903
+ }, [data, xDataKey, seriesKeys, plot, xExtent, yExtent]);
904
+
905
+ // Resolves an id, which is all the worklet can cheaply hand back. JS turns it
906
+ // into the row it came from.
907
+ const select = useMemo(() => id => {
908
+ if (!id) {
909
+ setActivePoint(null);
910
+ return;
911
+ }
912
+ const at = hit.ids.indexOf(id);
913
+ if (at < 0) {
914
+ setActivePoint(null);
915
+ return;
916
+ }
917
+ const index = hit.indices[at];
918
+ const key = hit.owners[at];
919
+ const datum = data[index];
920
+ if (!datum) {
921
+ setActivePoint(null);
922
+ return;
923
+ }
924
+ setActivePoint({
925
+ index,
926
+ dataKey: key,
927
+ x: datum[xDataKey],
928
+ y: datum[key],
929
+ datum
930
+ });
931
+ }, [hit, data, xDataKey, setActivePoint]);
932
+
933
+ /*
934
+ * Built in one closure, and everything it captures is a plain array, a number
935
+ * or a shared value. A worklet may only call another worklet, and the rule is
936
+ * enforced by crashing rather than by warning — so the resolver is declared
937
+ * here, next to its callers, rather than as a helper elsewhere in the file
938
+ * where it would be easy to leave un-workletised.
939
+ */
940
+ const pan = useMemo(() => {
941
+ const xs = hit.xs;
942
+ const ys = hit.ys;
943
+ const ids = hit.ids;
944
+ const limit = hitRadius * hitRadius;
945
+ const resolve = (px, py) => {
946
+ 'worklet';
947
+
948
+ let bestId = '';
949
+ let best = limit;
950
+ for (let i = 0; i < xs.length; i += 1) {
951
+ const dx = xs[i] - px;
952
+ const dy = ys[i] - py;
953
+ const distance = dx * dx + dy * dy;
954
+ if (distance <= best) {
955
+ best = distance;
956
+ bestId = ids[i];
957
+ }
958
+ }
959
+ if (bestId === activeId.value) return;
960
+ activeId.value = bestId;
961
+ runOnJS(select)(bestId);
962
+ };
963
+ const clear = () => {
964
+ 'worklet';
965
+
966
+ if (activeId.value === '') return;
967
+ activeId.value = '';
968
+ runOnJS(select)('');
969
+ };
970
+ return Gesture.Pan().minDistance(0).onBegin(event => {
971
+ 'worklet';
972
+
973
+ resolve(event.x, event.y);
974
+ }).onUpdate(event => {
975
+ 'worklet';
976
+
977
+ resolve(event.x, event.y);
978
+ }).onFinalize(() => {
979
+ 'worklet';
980
+
981
+ clear();
982
+ });
983
+ }, [hit, hitRadius, activeId, select]);
984
+
985
+ // The readout sits above the point and is clamped inside the plot, so it
986
+ // never runs off an edge at an extreme value.
987
+ const labelStyle = useAnimatedStyle(() => {
988
+ const id = activeId.value;
989
+ if (!id) return {
990
+ opacity: 0
991
+ };
992
+ const at = hit.ids.indexOf(id);
993
+ if (at < 0) return {
994
+ opacity: 0
995
+ };
996
+ const x = hit.xs[at];
997
+ const y = hit.ys[at];
998
+ const half = LABEL_WIDTH / 2;
999
+ return {
1000
+ opacity: 1,
1001
+ transform: [{
1002
+ translateX: Math.min(plot.left + plot.width - half, Math.max(plot.left + half, x)) - half
1003
+ },
1004
+ // Above the point, and pushed below it near the top of the plot where
1005
+ // there is no room above.
1006
+ {
1007
+ translateY: y - plot.top < LABEL_HEIGHT ? y + 16 : y - LABEL_HEIGHT
1008
+ }]
1009
+ };
1010
+ });
1011
+ const fmtX = formatX ?? (value => compactNumber(value));
1012
+ const fmtY = formatY ?? (value => compactNumber(value));
1013
+ if (status === 'loading') return null;
1014
+ return /*#__PURE__*/_jsx(GestureDetector, {
1015
+ gesture: pan,
1016
+ children: /*#__PURE__*/_jsx(View, {
1017
+ style: StyleSheet.absoluteFill,
1018
+ children: showLabel ? /*#__PURE__*/_jsx(Animated.View, {
1019
+ pointerEvents: "none",
1020
+ style: [{
1021
+ position: 'absolute',
1022
+ left: 0,
1023
+ top: 0,
1024
+ width: LABEL_WIDTH
1025
+ }, labelStyle],
1026
+ children: /*#__PURE__*/_jsx(View, {
1027
+ className: "items-center rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg",
1028
+ children: activePoint ? /*#__PURE__*/_jsxs(_Fragment, {
1029
+ children: [formatTitle ? /*#__PURE__*/_jsx(Text, {
1030
+ size: "xs",
1031
+ muted: true,
1032
+ numberOfLines: 1,
1033
+ children: formatTitle(activePoint.datum)
1034
+ }) : null, /*#__PURE__*/_jsxs(View, {
1035
+ className: "flex-row items-center gap-1.5",
1036
+ children: [series.length > 1 ? /*#__PURE__*/_jsx(View, {
1037
+ style: {
1038
+ backgroundColor: series.find(([key]) => key === activePoint.dataKey)?.[1] ?? undefined
1039
+ },
1040
+ className: "h-1.5 w-1.5 rounded-full"
1041
+ }) : null, /*#__PURE__*/_jsxs(Text, {
1042
+ size: "sm",
1043
+ weight: "semibold",
1044
+ numberOfLines: 1,
1045
+ children: [fmtX(activePoint.x), " \xB7 ", fmtY(activePoint.y, activePoint.dataKey)]
1046
+ })]
1047
+ })]
1048
+ }) : null
1049
+ })
1050
+ }) : null
1051
+ })
1052
+ });
1053
+ }
1054
+ ScatterChartTooltip.displayName = 'ScatterChart.Tooltip';
1055
+ ScatterChartTooltip.layer = 'overlay';
1056
+ /**
1057
+ * A swatch and a name per registered series. Sits in the top-left of the plot
1058
+ * by default — move it with `className`.
1059
+ */
1060
+ function ScatterChartLegend({
1061
+ className,
1062
+ labels,
1063
+ ...props
1064
+ }) {
1065
+ const {
1066
+ series
1067
+ } = useChart('ScatterChart.Legend');
1068
+ if (!series.length) return null;
1069
+ return /*#__PURE__*/_jsx(View, {
1070
+ className: cn('absolute left-2.5 top-0 flex-row flex-wrap items-center gap-4', className),
1071
+ ...props,
1072
+ style: [{
1073
+ pointerEvents: 'none'
1074
+ }, props.style],
1075
+ children: series.map(([key, color]) => /*#__PURE__*/_jsx(SeriesSwatch, {
1076
+ color: color,
1077
+ label: labels?.[key] ?? key
1078
+ }, key))
1079
+ });
1080
+ }
1081
+ ScatterChartLegend.displayName = 'ScatterChart.Legend';
1082
+ ScatterChartLegend.layer = 'overlay';
1083
+
1084
+ /** One series' colour and name. Shared by the legend and the header. */
1085
+ function SeriesSwatch({
1086
+ color,
1087
+ label
1088
+ }) {
1089
+ return /*#__PURE__*/_jsxs(View, {
1090
+ className: "flex-row items-center gap-1.5",
1091
+ children: [/*#__PURE__*/_jsx(View, {
1092
+ style: {
1093
+ backgroundColor: color
1094
+ },
1095
+ className: "h-2 w-2 rounded-full"
1096
+ }), /*#__PURE__*/_jsx(Text, {
1097
+ size: "xs",
1098
+ muted: true,
1099
+ children: label
1100
+ })]
1101
+ });
1102
+ }
1103
+
1104
+ /* -------------------------------------------------------------------------- */
1105
+ /* Header layer */
1106
+ /* -------------------------------------------------------------------------- */
1107
+
1108
+ /**
1109
+ * The strip above the plot: what the chart is of, what it currently reads, and
1110
+ * what the colours mean.
1111
+ *
1112
+ * The value is not derived here. A readout that follows the finger belongs to
1113
+ * whoever owns the data — take it from `onActivePointChange` and pass the
1114
+ * formatted string down, so one header can show a summary when nothing is
1115
+ * pressed and a point's values when something is.
1116
+ */
1117
+ function ScatterChartHeader({
1118
+ className,
1119
+ title,
1120
+ value,
1121
+ caption,
1122
+ labels,
1123
+ legend = false,
1124
+ children,
1125
+ ...props
1126
+ }) {
1127
+ const {
1128
+ series
1129
+ } = useChart('ScatterChart.Header');
1130
+ const trailing = children ?? (legend && series.length ? /*#__PURE__*/_jsx(View, {
1131
+ className: "flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1",
1132
+ children: series.map(([key, color]) => /*#__PURE__*/_jsx(SeriesSwatch, {
1133
+ color: color,
1134
+ label: labels?.[key] ?? key
1135
+ }, key))
1136
+ }) : null);
1137
+ return /*#__PURE__*/_jsxs(View, {
1138
+ ...props,
1139
+ className: cn('flex-row items-start justify-between gap-3 pb-3', className),
1140
+ children: [/*#__PURE__*/_jsxs(View, {
1141
+ className: "flex-1 gap-0.5",
1142
+ children: [title ? /*#__PURE__*/_jsx(Text, {
1143
+ size: "xs",
1144
+ muted: true,
1145
+ children: title
1146
+ }) : null, value ? /*#__PURE__*/_jsx(Text, {
1147
+ size: "xl",
1148
+ weight: "bold",
1149
+ children: value
1150
+ }) : null, caption ? /*#__PURE__*/_jsx(Text, {
1151
+ size: "xs",
1152
+ muted: true,
1153
+ children: caption
1154
+ }) : null]
1155
+ }), trailing ? /*#__PURE__*/_jsx(View, {
1156
+ className: "shrink pt-1",
1157
+ children: trailing
1158
+ }) : null]
1159
+ });
1160
+ }
1161
+ ScatterChartHeader.displayName = 'ScatterChart.Header';
1162
+ ScatterChartHeader.layer = 'header';
1163
+ export const ScatterChart = Object.assign(ScatterChartRoot, {
1164
+ Header: ScatterChartHeader,
1165
+ Grid: ScatterChartGrid,
1166
+ Points: ScatterChartPoints,
1167
+ Skeleton: ScatterChartSkeleton,
1168
+ XAxis: ScatterChartXAxis,
1169
+ YAxis: ScatterChartYAxis,
1170
+ Tooltip: ScatterChartTooltip,
1171
+ Legend: ScatterChartLegend
1172
+ });
1173
+ //# sourceMappingURL=index.js.map