panelui-native 0.61.1 → 0.63.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 (81) hide show
  1. package/lib/module/components/area-chart/index.js +14 -4
  2. package/lib/module/components/area-chart/index.js.map +1 -1
  3. package/lib/module/components/bar-chart/index.js +14 -4
  4. package/lib/module/components/bar-chart/index.js.map +1 -1
  5. package/lib/module/components/candlestick-chart/index.js +14 -4
  6. package/lib/module/components/candlestick-chart/index.js.map +1 -1
  7. package/lib/module/components/card/index.js +26 -1
  8. package/lib/module/components/card/index.js.map +1 -1
  9. package/lib/module/components/dialog/index.js +49 -3
  10. package/lib/module/components/dialog/index.js.map +1 -1
  11. package/lib/module/components/hex-chart/index.js +2 -2
  12. package/lib/module/components/hex-chart/index.js.map +1 -1
  13. package/lib/module/components/line-chart/index.js +14 -4
  14. package/lib/module/components/line-chart/index.js.map +1 -1
  15. package/lib/module/components/live-line-chart/index.js +1063 -0
  16. package/lib/module/components/live-line-chart/index.js.map +1 -0
  17. package/lib/module/components/pie-chart/index.js +2 -2
  18. package/lib/module/components/pie-chart/index.js.map +1 -1
  19. package/lib/module/components/plot/index.js +1329 -0
  20. package/lib/module/components/plot/index.js.map +1 -0
  21. package/lib/module/components/polar-area-chart/index.js +719 -0
  22. package/lib/module/components/polar-area-chart/index.js.map +1 -0
  23. package/lib/module/components/radar-chart/index.js +14 -4
  24. package/lib/module/components/radar-chart/index.js.map +1 -1
  25. package/lib/module/components/ring-chart/index.js +2 -2
  26. package/lib/module/components/ring-chart/index.js.map +1 -1
  27. package/lib/module/components/section-rail/index.js +120 -15
  28. package/lib/module/components/section-rail/index.js.map +1 -1
  29. package/lib/module/components/selection-mode/index.js +129 -18
  30. package/lib/module/components/selection-mode/index.js.map +1 -1
  31. package/lib/module/components/sortable/index.js +15 -5
  32. package/lib/module/components/sortable/index.js.map +1 -1
  33. package/lib/module/index.js +9 -0
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/utils/chart.js +40 -0
  36. package/lib/module/utils/chart.js.map +1 -1
  37. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  39. package/lib/typescript/src/components/candlestick-chart/index.d.ts +6 -3
  40. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/card/index.d.ts +48 -1
  42. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  43. package/lib/typescript/src/components/dialog/index.d.ts +47 -2
  44. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  46. package/lib/typescript/src/components/live-line-chart/index.d.ts +293 -0
  47. package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/plot/index.d.ts +471 -0
  49. package/lib/typescript/src/components/plot/index.d.ts.map +1 -0
  50. package/lib/typescript/src/components/polar-area-chart/index.d.ts +259 -0
  51. package/lib/typescript/src/components/polar-area-chart/index.d.ts.map +1 -0
  52. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
  53. package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
  54. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
  55. package/lib/typescript/src/components/selection-mode/index.d.ts +82 -6
  56. package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
  57. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  58. package/lib/typescript/src/index.d.ts +4 -0
  59. package/lib/typescript/src/index.d.ts.map +1 -1
  60. package/lib/typescript/src/utils/chart.d.ts +15 -14
  61. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/src/components/area-chart/index.tsx +14 -4
  64. package/src/components/bar-chart/index.tsx +14 -4
  65. package/src/components/candlestick-chart/index.tsx +20 -7
  66. package/src/components/card/index.tsx +32 -7
  67. package/src/components/dialog/index.tsx +55 -2
  68. package/src/components/hex-chart/index.tsx +2 -2
  69. package/src/components/line-chart/index.tsx +14 -4
  70. package/src/components/live-line-chart/index.tsx +1294 -0
  71. package/src/components/pie-chart/index.tsx +2 -2
  72. package/src/components/plot/index.tsx +1564 -0
  73. package/src/components/polar-area-chart/index.tsx +928 -0
  74. package/src/components/radar-chart/index.tsx +14 -4
  75. package/src/components/ring-chart/index.tsx +2 -2
  76. package/src/components/section-rail/index.tsx +133 -14
  77. package/src/components/selection-mode/index.tsx +163 -26
  78. package/src/components/sortable/index.tsx +15 -5
  79. package/src/index.ts +81 -0
  80. package/src/utils/chart.ts +44 -0
  81. package/theme.css +24 -0
@@ -0,0 +1,1329 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Plot — a chart you assemble, for the chart that is not in the box.
5
+ *
6
+ * The other charts in this library each answer one question and answer it
7
+ * completely: a line chart knows it is drawing a series over time, and every
8
+ * decision inside it follows from that. This one knows nothing. It measures a
9
+ * box, resolves a scale, and hands both to whatever marks you put in it — so a
10
+ * combination nothing here ships as its own component is still a chart you can
11
+ * build rather than a chart you have to go without.
12
+ *
13
+ * ```tsx
14
+ * <Plot data={months} xDataKey="month">
15
+ * <Plot.Grid />
16
+ * <Plot.Bars dataKey="orders" colorIndex={2} />
17
+ * <Plot.Line dataKey="revenue" />
18
+ * <Plot.YAxis />
19
+ * <Plot.XAxis />
20
+ * <Plot.Cursor />
21
+ * <Plot.Tooltip />
22
+ * </Plot>
23
+ * ```
24
+ *
25
+ * ## One scale, however many marks
26
+ *
27
+ * Every mark reads the same plot box and the same y-domain, and the domain is
28
+ * derived from all of them together. That is the whole reason to compose rather
29
+ * than to stack two charts on top of each other: two scales drawn over each
30
+ * other look like a comparison and are not one.
31
+ *
32
+ * ## Where your own marks go
33
+ *
34
+ * `Plot.Layer` drops its children into the SVG tree, and `usePlot()` gives them
35
+ * the resolved geometry — the box, the tweening domain, the reveal, the palette.
36
+ * The scale functions are worklets exported alongside this component, so a mark
37
+ * of your own is rebuilt on the UI thread on the same frames these are:
38
+ *
39
+ * ```tsx
40
+ * function Threshold({ value }: { value: number }) {
41
+ * const { plot, domainMin, domainMax } = usePlot();
42
+ * const props = useAnimatedProps(() => {
43
+ * const y = yOf(value, plot, domainMin.value, domainMax.value);
44
+ * return { d: `M${plot.left},${y}H${plot.left + plot.width}` };
45
+ * });
46
+ * return <AnimatedPath animatedProps={props} stroke="red" />;
47
+ * }
48
+ *
49
+ * <Plot data={rows}>
50
+ * <Plot.Layer><Threshold value={80} /></Plot.Layer>
51
+ * </Plot>
52
+ * ```
53
+ *
54
+ * Anything that is text or takes a touch goes in `Plot.Overlay` instead, which
55
+ * is a React Native view over the drawing. That split is not a preference: SVG
56
+ * text ignores the platform's text scaling and the theme's font, and a gesture
57
+ * handler cannot be attached to an SVG node at all.
58
+ *
59
+ * ## What it will not do for you
60
+ *
61
+ * There is no `type` prop and no set of defaults that guess at one. A chart
62
+ * assembled here is exactly the marks you wrote, in the order you wrote them —
63
+ * which is also the order they are drawn in, so a line over bars is a line
64
+ * written after them.
65
+ */
66
+ import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
67
+ import { StyleSheet, View } from 'react-native';
68
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
69
+ import Animated, { Easing, runOnJS, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
70
+ import Svg, { Circle, ClipPath, Defs, G, Line as SvgLine, Path, Rect } from 'react-native-svg';
71
+ import { useCSSVariable } from 'uniwind';
72
+ import { Text } from "../../primitives/text.js";
73
+ import { areaPath, bandOf, barPath, columnValues, compactNumber, linePath, useSeriesColor, xOf, yOf } from "../../utils/chart.js";
74
+ import { cn } from "../../utils/cn.js";
75
+
76
+ /*
77
+ * The scale functions, re-exported so a mark written outside this file can sit
78
+ * on the same geometry the built-in ones do. Every one of them is a worklet, so
79
+ * a custom mark is rebuilt on the UI thread on the frames these are — which is
80
+ * the difference between an escape hatch and a second, slower chart drawn on
81
+ * top of the first.
82
+ */
83
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
84
+ export { areaPath, bandOf, barPath, compactNumber, linePath, segment, xAt, xOf, yOf } from "../../utils/chart.js";
85
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
86
+ const AnimatedRect = Animated.createAnimatedComponent(Rect);
87
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
88
+
89
+ /** Room left around the plot for the axis labels and the markers' rings. */
90
+ const PADDING = {
91
+ top: 12,
92
+ right: 10,
93
+ bottom: 22,
94
+ left: 10
95
+ };
96
+
97
+ /** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
98
+ const Y_AXIS_WIDTH = 44;
99
+
100
+ /** Gap between the value labels and the plot they sit beside. */
101
+ const Y_AXIS_GUTTER = 6;
102
+
103
+ /** Line height of an `xs` label, for centring one on the rule it names. */
104
+ const AXIS_LABEL_HEIGHT = 16;
105
+
106
+ /** Box each x label is centred in; a longer one is ellipsised rather than shoving. */
107
+ const POINT_LABEL_WIDTH = 56;
108
+
109
+ /** Width of the readout that rides the cursor. */
110
+ const LABEL_WIDTH = 120;
111
+
112
+ /**
113
+ * Which layer a part belongs to. Read off the component itself, so composition
114
+ * stays a flat list of children instead of three nested slots whose order the
115
+ * caller has to remember.
116
+ */
117
+
118
+ /**
119
+ * How the index of a row becomes an x.
120
+ *
121
+ * `point` puts the first and last rows on the plot's own edges, which is what a
122
+ * series wants — the line should reach the frame. `band` gives every row an
123
+ * equal slice and centres it in the middle of that, which is what anything with
124
+ * width wants: a bar sitting on the edge of the plot is a bar half of which is
125
+ * outside it.
126
+ *
127
+ * Resolved from the marks by default, so a chart with bars in it is banded
128
+ * without being told.
129
+ */
130
+
131
+ /** One end of the y-domain: a number to pin it at, or `auto` to derive it. */
132
+
133
+ /** Everything a mark needs to draw itself. Read it with `usePlot()`. */
134
+
135
+ const PlotContext = /*#__PURE__*/createContext(null);
136
+ function useChart(component) {
137
+ const context = useContext(PlotContext);
138
+ if (!context) {
139
+ throw new Error(`${component} must be used within a <Plot>`);
140
+ }
141
+ return context;
142
+ }
143
+
144
+ /**
145
+ * The resolved geometry, for a mark of your own.
146
+ *
147
+ * Everything in it is either a plain number that changes on layout or a shared
148
+ * value that changes every frame, so a mark built from it animates with the
149
+ * rest of the chart rather than beside it.
150
+ */
151
+ export function usePlot() {
152
+ return useChart('usePlot');
153
+ }
154
+
155
+ /** The row under the cursor, for a readout drawn inside the plot. */
156
+ export function usePlotCursor() {
157
+ const {
158
+ data,
159
+ activeIndexJS
160
+ } = useChart('usePlotCursor');
161
+ return {
162
+ activeIndex: activeIndexJS,
163
+ activePoint: activeIndexJS >= 0 ? data[activeIndexJS] ?? null : null
164
+ };
165
+ }
166
+
167
+ /** Imperative handle: re-run the reveal, for a "replay" control. */
168
+
169
+ const PlotRoot = /*#__PURE__*/forwardRef(function PlotRoot({
170
+ className,
171
+ data,
172
+ xDataKey = 'label',
173
+ status = 'ready',
174
+ aspectRatio = 2,
175
+ animationDuration = 700,
176
+ domainDuration = 500,
177
+ yDomain,
178
+ xScale: xScaleProp,
179
+ curve = 'monotone',
180
+ onActiveIndexChange,
181
+ compact = false,
182
+ children,
183
+ ...props
184
+ }, ref) {
185
+ const [size, setSize] = useState({
186
+ width: 0,
187
+ height: 0
188
+ });
189
+ const [series, setSeries] = useState([]);
190
+ const [activeIndexJS, setActiveIndexJS] = useState(-1);
191
+ const clipId = useRef(`panelui-plot-${Math.random().toString(36).slice(2, 9)}`).current;
192
+ const reveal = useSharedValue(0);
193
+ const domainMin = useSharedValue(0);
194
+ const domainMax = useSharedValue(0);
195
+ const activeIndex = useSharedValue(-1);
196
+ const reducedMotion = useReducedMotion();
197
+ const registerSeries = useMemo(() => (key, color) => setSeries(current => {
198
+ const existing = current.find(([k]) => k === key);
199
+ if (existing?.[1] === color) return current;
200
+ return [...current.filter(([k]) => k !== key), [key, color]];
201
+ }), []);
202
+ const unregisterSeries = useMemo(() => key => setSeries(current => current.filter(([k]) => k !== key)), []);
203
+
204
+ /*
205
+ * What the children need decided before anything is laid out.
206
+ *
207
+ * Both have to be known *before* the plot box exists, and neither can be
208
+ * asked of the parts themselves — a part renders into a box that has already
209
+ * been decided. An axis given no gutter is drawn over the marks; a bar placed
210
+ * on a point scale hangs half of itself off the frame, and a bar on an axis
211
+ * that skips zero is drawn at a length the data does not support.
212
+ */
213
+ const {
214
+ hasYAxis,
215
+ hasBars
216
+ } = useMemo(() => {
217
+ let axis = false;
218
+ let bars = false;
219
+ Children.forEach(children, child => {
220
+ if (! /*#__PURE__*/isValidElement(child)) return;
221
+ const type = child.type;
222
+ if (type.axis === 'y') axis = true;
223
+ if (type.mark === 'band') bars = true;
224
+ });
225
+ return {
226
+ hasYAxis: axis,
227
+ hasBars: bars
228
+ };
229
+ }, [children]);
230
+ const xScale = xScaleProp ?? (hasBars ? 'band' : 'point');
231
+ const pad = compact ? {
232
+ top: 2,
233
+ right: 1,
234
+ bottom: 2,
235
+ left: 1
236
+ } : {
237
+ ...PADDING,
238
+ left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left
239
+ };
240
+ const plot = {
241
+ left: pad.left,
242
+ top: pad.top,
243
+ width: Math.max(size.width - pad.left - pad.right, 0),
244
+ height: Math.max(size.height - pad.top - pad.bottom, 0)
245
+ };
246
+
247
+ /*
248
+ * One extent across every registered mark, so two series share one axis and
249
+ * stay comparable. A scale per series makes two quantities orders of
250
+ * magnitude apart look alike, which is the chart lying rather than the chart
251
+ * being convenient.
252
+ */
253
+ const seriesKeys = series.map(([key]) => key).join('|');
254
+ const extent = useMemo(() => {
255
+ const lowPin = yDomain?.[0];
256
+ const highPin = yDomain?.[1];
257
+ if (typeof lowPin === 'number' && typeof highPin === 'number') {
258
+ return [lowPin, highPin];
259
+ }
260
+ const keys = seriesKeys ? seriesKeys.split('|') : [];
261
+ let min = Infinity;
262
+ let max = -Infinity;
263
+ for (const row of data) {
264
+ for (const key of keys) {
265
+ const value = row[key];
266
+ if (typeof value !== 'number' || Number.isNaN(value)) continue;
267
+ if (value < min) min = value;
268
+ if (value > max) max = value;
269
+ }
270
+ }
271
+ if (min === Infinity) {
272
+ // Nothing to measure. Fall back to whatever was pinned, and to a unit
273
+ // domain when nothing was — dividing by a span of zero is how a mark
274
+ // ends up drawn at infinity.
275
+ const low = typeof lowPin === 'number' ? lowPin : 0;
276
+ const high = typeof highPin === 'number' ? highPin : low + 1;
277
+ return [low, high];
278
+ }
279
+
280
+ /*
281
+ * A bar's length is measured from zero, so an axis that does not contain
282
+ * zero makes every bar on it a lie about its own size — six columns of
283
+ * near-identical height standing for numbers that differ by half. So a plot
284
+ * with bars in it gets zero whether the data reaches it or not, and that end
285
+ * is then left exactly where it is: headroom under a baseline lifts the bars
286
+ * off the thing they are measured from.
287
+ */
288
+ let lowRoom = 1;
289
+ let highRoom = 1;
290
+ if (hasBars) {
291
+ if (min > 0) min = 0;
292
+ if (max < 0) max = 0;
293
+ if (min === 0) lowRoom = 0;
294
+ if (max === 0) highRoom = 0;
295
+ }
296
+
297
+ // A flat series has no extent of its own; give it one so it lands on the
298
+ // middle of the plot instead of dividing by zero.
299
+ if (min === max) {
300
+ min -= 1;
301
+ max += 1;
302
+ }
303
+
304
+ /*
305
+ * Headroom goes on the derived ends only. A pinned zero that got a tenth of
306
+ * the span subtracted from it is not a pinned zero, and the baseline
307
+ * drifting off the axis is exactly what pinning it was for.
308
+ */
309
+ const headroom = (max - min) * 0.1;
310
+ return [typeof lowPin === 'number' ? lowPin : min - headroom * lowRoom, typeof highPin === 'number' ? highPin : max + headroom * highRoom];
311
+ }, [data, yDomain, seriesKeys, hasBars]);
312
+ const loading = status === 'loading';
313
+ // Nothing has said what the axis is yet — no mark has registered and no
314
+ // domain was given. Assigning one now would tween the real domain up out of a
315
+ // placeholder on the frame the first mark mounts.
316
+ const hasDomain = seriesKeys.length > 0 || yDomain !== undefined;
317
+ useEffect(() => {
318
+ if (loading || !hasDomain) return;
319
+ const [min, max] = extent;
320
+ // The first domain lands without a tween: there is no previous scale to
321
+ // move from, and animating up from zero reads as the numbers changing.
322
+ const first = domainMin.value === 0 && domainMax.value === 0;
323
+ if (first || reducedMotion) {
324
+ domainMin.value = min;
325
+ domainMax.value = max;
326
+ return;
327
+ }
328
+ domainMin.value = withTiming(min, {
329
+ duration: domainDuration
330
+ });
331
+ domainMax.value = withTiming(max, {
332
+ duration: domainDuration
333
+ });
334
+ }, [extent, loading, hasDomain, reducedMotion, domainDuration, domainMin, domainMax]);
335
+ const revealed = useRef(false);
336
+ const playReveal = useMemo(() => () => {
337
+ if (reducedMotion) {
338
+ reveal.value = 1;
339
+ return;
340
+ }
341
+ reveal.value = 0;
342
+ reveal.value = withTiming(1, {
343
+ duration: animationDuration,
344
+ easing: Easing.out(Easing.cubic)
345
+ });
346
+ }, [reducedMotion, animationDuration, reveal]);
347
+ useEffect(() => {
348
+ // Going back to `loading` arms the reveal again, so a refetch is uncovered
349
+ // rather than appearing whole on the frame the data lands.
350
+ if (loading) {
351
+ revealed.current = false;
352
+ reveal.value = 0;
353
+ return;
354
+ }
355
+ if (revealed.current || plot.width <= 0 || !data.length) return;
356
+ revealed.current = true;
357
+ playReveal();
358
+ }, [loading, plot.width, data.length, playReveal, reveal]);
359
+ useImperativeHandle(ref, () => ({
360
+ replay: playReveal
361
+ }), [playReveal]);
362
+ const c1 = useSeriesColor(undefined, 1);
363
+ const c2 = useSeriesColor(undefined, 2);
364
+ const c3 = useSeriesColor(undefined, 3);
365
+ const c4 = useSeriesColor(undefined, 4);
366
+ const c5 = useSeriesColor(undefined, 5);
367
+ const palette = useMemo(() => [c1, c2, c3, c4, c5], [c1, c2, c3, c4, c5]);
368
+ const handleActiveIndex = useMemo(() => index => {
369
+ setActiveIndexJS(index);
370
+ onActiveIndexChange?.(index, index >= 0 ? data[index] ?? null : null);
371
+ }, [onActiveIndexChange, data]);
372
+ const onLayout = event => {
373
+ const {
374
+ width,
375
+ height
376
+ } = event.nativeEvent.layout;
377
+ setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
378
+ width,
379
+ height
380
+ });
381
+ props.onLayout?.(event);
382
+ };
383
+ const context = useMemo(() => ({
384
+ data,
385
+ xDataKey,
386
+ plot,
387
+ xScale,
388
+ status,
389
+ domainMin,
390
+ domainMax,
391
+ extent,
392
+ reveal,
393
+ palette,
394
+ series,
395
+ registerSeries,
396
+ unregisterSeries,
397
+ activeIndex,
398
+ activeIndexJS,
399
+ setActiveIndexJS: handleActiveIndex,
400
+ clipId
401
+ }),
402
+ // `plot` is rebuilt every render from `size`, so it is compared by value.
403
+ // eslint-disable-next-line react-hooks/exhaustive-deps
404
+ [data, xDataKey, plot.width, plot.height, plot.left, plot.top, xScale, status, domainMin, domainMax, extent, reveal, palette, series, registerSeries, unregisterSeries, activeIndex, activeIndexJS, handleActiveIndex, clipId]);
405
+ const clipProps = useAnimatedProps(() => ({
406
+ width: plot.width * reveal.value
407
+ }));
408
+ const {
409
+ svg,
410
+ overlay,
411
+ header
412
+ } = partition(children);
413
+
414
+ /*
415
+ * Two views, because the header is not part of the plot. `aspectRatio` and
416
+ * the layout measurement belong to the drawing area alone — measured on the
417
+ * outer view they would take the header in too, and the plot would lose as
418
+ * much height as the readout took while still claiming the shape asked for.
419
+ */
420
+ return /*#__PURE__*/_jsx(PlotContext.Provider, {
421
+ value: context,
422
+ children: /*#__PURE__*/_jsxs(View, {
423
+ ...props,
424
+ style: props.style,
425
+ className: cn('w-full', className),
426
+ children: [header, /*#__PURE__*/_jsx(View, {
427
+ onLayout: onLayout,
428
+ style: {
429
+ aspectRatio
430
+ },
431
+ className: "w-full",
432
+ children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
433
+ children: [/*#__PURE__*/_jsxs(Svg, {
434
+ width: "100%",
435
+ height: "100%",
436
+ style: StyleSheet.absoluteFill,
437
+ children: [/*#__PURE__*/_jsx(Defs, {
438
+ children: /*#__PURE__*/_jsx(ClipPath, {
439
+ id: clipId,
440
+ children: /*#__PURE__*/_jsx(AnimatedRect, {
441
+ x: plot.left,
442
+ y: 0,
443
+ height: size.height,
444
+ animatedProps: clipProps
445
+ })
446
+ })
447
+ }), svg]
448
+ }), overlay]
449
+ }) : null
450
+ })]
451
+ })
452
+ });
453
+ });
454
+ PlotRoot.displayName = 'Plot';
455
+
456
+ /** Sorts the children into the SVG tree, the view layer over it, and the header. */
457
+ function partition(children) {
458
+ const svg = [];
459
+ const overlay = [];
460
+ const header = [];
461
+ Children.forEach(children, (child, index) => {
462
+ if (! /*#__PURE__*/isValidElement(child)) return;
463
+ const layer = child.type.layer ?? 'svg';
464
+ (layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(
465
+ /*#__PURE__*/
466
+ // Children of a `Children.forEach` need keys of their own once they are
467
+ // put into a new array.
468
+ _jsx(ChildSlot, {
469
+ children: child
470
+ }, index));
471
+ });
472
+ return {
473
+ svg,
474
+ overlay,
475
+ header
476
+ };
477
+ }
478
+
479
+ /** Identity wrapper, purely so the partitioned arrays can carry keys. */
480
+ function ChildSlot({
481
+ children
482
+ }) {
483
+ return /*#__PURE__*/_jsx(_Fragment, {
484
+ children: children
485
+ });
486
+ }
487
+
488
+ /**
489
+ * A mark's colour: the one it was given, else the theme token at its index.
490
+ *
491
+ * Registration happens here rather than in each mark, because every mark that
492
+ * registers does it for the same two reasons — the domain has to take it in,
493
+ * and the legend has to name it.
494
+ */
495
+ function useMark(dataKey, color, colorIndex) {
496
+ const {
497
+ palette,
498
+ registerSeries,
499
+ unregisterSeries
500
+ } = useChart('Plot mark');
501
+ const resolved = color ?? palette[(colorIndex - 1) % palette.length] ?? palette[0];
502
+ useEffect(() => {
503
+ registerSeries(dataKey, resolved);
504
+ return () => unregisterSeries(dataKey);
505
+ }, [dataKey, resolved, registerSeries, unregisterSeries]);
506
+ return resolved;
507
+ }
508
+
509
+ /* -------------------------------------------------------------------------- */
510
+ /* SVG layer */
511
+ /* -------------------------------------------------------------------------- */
512
+
513
+ /**
514
+ * Horizontal reference lines.
515
+ *
516
+ * Outside the reveal clip on purpose: the grid is the frame the chart arrives
517
+ * into, so it is already there when the marks start being uncovered.
518
+ */
519
+ function PlotGrid({
520
+ rows = 4,
521
+ color,
522
+ dashArray = '4,6',
523
+ opacity = 1
524
+ }) {
525
+ const {
526
+ plot
527
+ } = useChart('Plot.Grid');
528
+ const token = useCSSVariable('--color-border');
529
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
530
+ return /*#__PURE__*/_jsx(G, {
531
+ opacity: opacity,
532
+ children: Array.from({
533
+ length: rows + 1
534
+ }, (_, index) => {
535
+ const y = plot.top + plot.height / rows * index;
536
+ return /*#__PURE__*/_jsx(SvgLine, {
537
+ x1: plot.left,
538
+ x2: plot.left + plot.width,
539
+ y1: y,
540
+ y2: y,
541
+ stroke: stroke,
542
+ strokeWidth: 1,
543
+ strokeDasharray: dashArray
544
+ }, index);
545
+ })
546
+ });
547
+ }
548
+ PlotGrid.displayName = 'Plot.Grid';
549
+ PlotGrid.layer = 'svg';
550
+ /** A series as a stroked line. */
551
+ function PlotLine({
552
+ dataKey,
553
+ color,
554
+ colorIndex = 1,
555
+ strokeWidth = 2.5,
556
+ curve = 'monotone',
557
+ dashArray
558
+ }) {
559
+ const {
560
+ data,
561
+ plot,
562
+ xScale,
563
+ domainMin,
564
+ domainMax,
565
+ status,
566
+ clipId
567
+ } = useChart('Plot.Line');
568
+ const stroke = useMark(dataKey, color, colorIndex);
569
+
570
+ /*
571
+ * Pulled out of the worklet as a plain array. A worklet may close over numbers
572
+ * and arrays of them freely, but reading `data`'s rows — objects of mixed
573
+ * types, keyed by strings the caller chose — inside one on every frame is work
574
+ * that belongs on the JS side once.
575
+ */
576
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
577
+ const banded = xScale === 'band';
578
+ const animatedProps = useAnimatedProps(() => {
579
+ const min = domainMin.value;
580
+ const max = domainMax.value;
581
+ if (max === min || plot.width <= 0) return {
582
+ d: ''
583
+ };
584
+ if (!banded) return {
585
+ d: linePath(values, plot, min, max, curve, false)
586
+ };
587
+
588
+ /*
589
+ * A banded line is built here rather than through `linePath`, which spreads
590
+ * its points across the full width. On a band scale the points belong in
591
+ * the middle of their own slice — a line that ignored that would start half
592
+ * a slice left of the bar it is describing.
593
+ */
594
+ let d = '';
595
+ let drawn = 0;
596
+ for (let index = 0; index < values.length; index += 1) {
597
+ const value = values[index];
598
+ if (value === null || value === undefined) {
599
+ drawn = 0;
600
+ continue;
601
+ }
602
+ const x = bandOf(index, values.length, plot);
603
+ const y = yOf(value, plot, min, max);
604
+ d += `${drawn === 0 ? 'M' : 'L'}${x},${y}`;
605
+ drawn += 1;
606
+ }
607
+ return {
608
+ d
609
+ };
610
+ });
611
+ if (status === 'loading') return null;
612
+ return /*#__PURE__*/_jsx(G, {
613
+ clipPath: `url(#${clipId})`,
614
+ children: /*#__PURE__*/_jsx(AnimatedPath, {
615
+ animatedProps: animatedProps,
616
+ fill: "none",
617
+ stroke: stroke,
618
+ strokeWidth: strokeWidth,
619
+ strokeDasharray: dashArray,
620
+ strokeLinecap: "round",
621
+ strokeLinejoin: "round"
622
+ })
623
+ });
624
+ }
625
+ PlotLine.displayName = 'Plot.Line';
626
+ PlotLine.layer = 'svg';
627
+ /**
628
+ * A series as a fill down to the baseline.
629
+ *
630
+ * Written before the line it belongs under, since the order the marks are
631
+ * written is the order they are drawn.
632
+ */
633
+ function PlotArea({
634
+ dataKey,
635
+ color,
636
+ colorIndex = 1,
637
+ opacity = 0.18,
638
+ curve = 'monotone'
639
+ }) {
640
+ const {
641
+ data,
642
+ plot,
643
+ domainMin,
644
+ domainMax,
645
+ status,
646
+ clipId
647
+ } = useChart('Plot.Area');
648
+ const fill = useMark(dataKey, color, colorIndex);
649
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
650
+ const animatedProps = useAnimatedProps(() => {
651
+ const min = domainMin.value;
652
+ const max = domainMax.value;
653
+ if (max === min || plot.width <= 0) return {
654
+ d: ''
655
+ };
656
+ return {
657
+ d: areaPath(values, plot, min, max, curve, false)
658
+ };
659
+ });
660
+ if (status === 'loading') return null;
661
+ return /*#__PURE__*/_jsx(G, {
662
+ clipPath: `url(#${clipId})`,
663
+ children: /*#__PURE__*/_jsx(AnimatedPath, {
664
+ animatedProps: animatedProps,
665
+ fill: fill,
666
+ fillOpacity: opacity
667
+ })
668
+ });
669
+ }
670
+ PlotArea.displayName = 'Plot.Area';
671
+ PlotArea.layer = 'svg';
672
+ /**
673
+ * A series as columns.
674
+ *
675
+ * One path for all of them rather than one node per bar: a plot of two hundred
676
+ * periods is the same single animated prop a frame as a plot of twenty.
677
+ *
678
+ * A bar has width, so its presence puts the whole plot on a band scale unless
679
+ * the root was told otherwise — see `xScale`.
680
+ */
681
+ function PlotBars({
682
+ dataKey,
683
+ color,
684
+ colorIndex = 1,
685
+ gap = 0.35,
686
+ radius = 4,
687
+ opacity = 1
688
+ }) {
689
+ const {
690
+ data,
691
+ plot,
692
+ domainMin,
693
+ domainMax,
694
+ status,
695
+ clipId
696
+ } = useChart('Plot.Bars');
697
+ const fill = useMark(dataKey, color, colorIndex);
698
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
699
+ const total = values.length;
700
+ const slice = total > 0 ? plot.width / total : 0;
701
+ const width = Math.max(1, slice * (1 - Math.min(Math.max(gap, 0), 0.95)));
702
+ const animatedProps = useAnimatedProps(() => {
703
+ const min = domainMin.value;
704
+ const max = domainMax.value;
705
+ if (max === min || plot.width <= 0 || total === 0) return {
706
+ d: ''
707
+ };
708
+
709
+ /*
710
+ * Bars grow from the baseline, which is the domain's zero where the domain
711
+ * covers it and the nearer edge where it does not. A bar drawn from the
712
+ * bottom of a plot whose axis starts at 40 is a length that is not in the
713
+ * data.
714
+ */
715
+ const base = yOf(Math.min(Math.max(0, min), max), plot, min, max);
716
+ let path = '';
717
+ for (let index = 0; index < total; index += 1) {
718
+ const value = values[index];
719
+ if (value === null || value === undefined) continue;
720
+ const y = yOf(value, plot, min, max);
721
+ const top = Math.min(y, base);
722
+ const height = Math.abs(base - y);
723
+ if (height <= 0) continue;
724
+ path += barPath(bandOf(index, total, plot) - width / 2, top, width, height, radius, y <= base ? 'up' : 'down');
725
+ }
726
+ return {
727
+ d: path
728
+ };
729
+ });
730
+ if (status === 'loading') return null;
731
+ return /*#__PURE__*/_jsx(G, {
732
+ clipPath: `url(#${clipId})`,
733
+ children: /*#__PURE__*/_jsx(AnimatedPath, {
734
+ animatedProps: animatedProps,
735
+ fill: fill,
736
+ fillOpacity: opacity
737
+ })
738
+ });
739
+ }
740
+ PlotBars.displayName = 'Plot.Bars';
741
+ PlotBars.layer = 'svg';
742
+ // Read by the root before it resolves the scale: a mark with width cannot sit
743
+ // on a point scale without half of the first and last one leaving the plot.
744
+ PlotBars.mark = 'band';
745
+ /** A dot per row — for a series short enough that its points are worth marking. */
746
+ function PlotDots({
747
+ dataKey,
748
+ color,
749
+ colorIndex = 1,
750
+ size = 3.5,
751
+ ringWidth = 2
752
+ }) {
753
+ const {
754
+ data,
755
+ plot,
756
+ xScale,
757
+ domainMin,
758
+ domainMax,
759
+ status,
760
+ clipId
761
+ } = useChart('Plot.Dots');
762
+ const fill = useMark(dataKey, color, colorIndex);
763
+ const ringToken = useCSSVariable('--color-background');
764
+ const ring = typeof ringToken === 'string' ? ringToken : '#000000';
765
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
766
+ const banded = xScale === 'band';
767
+ return /*#__PURE__*/_jsx(G, {
768
+ clipPath: `url(#${clipId})`,
769
+ children: status === 'loading' ? null : values.map((value, index) => value === null || value === undefined ? null : /*#__PURE__*/_jsx(Dot, {
770
+ value: value,
771
+ x: banded ? bandOf(index, values.length, plot) : xOf(index, values.length, plot),
772
+ plot: plot,
773
+ domainMin: domainMin,
774
+ domainMax: domainMax,
775
+ fill: fill,
776
+ ring: ring,
777
+ size: size,
778
+ ringWidth: ringWidth
779
+ }, index))
780
+ });
781
+ }
782
+ PlotDots.displayName = 'Plot.Dots';
783
+ PlotDots.layer = 'svg';
784
+
785
+ /** One dot, whose y follows the domain tween. */
786
+ function Dot({
787
+ value,
788
+ x,
789
+ plot,
790
+ domainMin,
791
+ domainMax,
792
+ fill,
793
+ ring,
794
+ size,
795
+ ringWidth
796
+ }) {
797
+ const animatedProps = useAnimatedProps(() => ({
798
+ cy: yOf(value, plot, domainMin.value, domainMax.value)
799
+ }));
800
+ return /*#__PURE__*/_jsx(AnimatedCircle, {
801
+ animatedProps: animatedProps,
802
+ cx: x,
803
+ r: size,
804
+ fill: fill,
805
+ stroke: ring,
806
+ strokeWidth: ringWidth
807
+ });
808
+ }
809
+ /**
810
+ * Marks of your own, in the SVG tree.
811
+ *
812
+ * Whatever is inside is drawn where it is written — before the marks written
813
+ * after it, after the ones before it — and reaches the geometry through
814
+ * `usePlot()`. It is not given the geometry as an argument, because a mark that
815
+ * animates has to hold hooks of its own and a render prop is not a component.
816
+ */
817
+ function PlotLayer({
818
+ children
819
+ }) {
820
+ const {
821
+ clipId
822
+ } = useChart('Plot.Layer');
823
+ return /*#__PURE__*/_jsx(G, {
824
+ clipPath: `url(#${clipId})`,
825
+ children: children
826
+ });
827
+ }
828
+ PlotLayer.displayName = 'Plot.Layer';
829
+ PlotLayer.layer = 'svg';
830
+
831
+ /* -------------------------------------------------------------------------- */
832
+ /* Overlay layer */
833
+ /* -------------------------------------------------------------------------- */
834
+
835
+ /**
836
+ * Anything of your own that is text or takes a touch, laid over the drawing.
837
+ *
838
+ * SVG text ignores the platform's text scaling and the theme's font, and a
839
+ * gesture handler cannot be attached to an SVG node at all — so those two go
840
+ * here rather than in `Plot.Layer`.
841
+ */
842
+ function PlotOverlay({
843
+ className,
844
+ children
845
+ }) {
846
+ return /*#__PURE__*/_jsx(View, {
847
+ pointerEvents: "box-none",
848
+ style: StyleSheet.absoluteFill,
849
+ className: className,
850
+ children: children
851
+ });
852
+ }
853
+ PlotOverlay.displayName = 'Plot.Overlay';
854
+ PlotOverlay.layer = 'overlay';
855
+ /**
856
+ * A reference line across the plot — a target, a limit, an average.
857
+ *
858
+ * A view rather than an SVG line, so its label is real text and follows the
859
+ * theme. Its height is one point and its position is a transform, so it costs
860
+ * no more than the SVG line would while gaining a legible caption.
861
+ */
862
+ function PlotRule({
863
+ y,
864
+ label,
865
+ color,
866
+ className
867
+ }) {
868
+ const {
869
+ plot,
870
+ domainMin,
871
+ domainMax,
872
+ status
873
+ } = useChart('Plot.Rule');
874
+ const token = useCSSVariable('--color-muted-foreground');
875
+ const stroke = color ?? (typeof token === 'string' ? token : '#888888');
876
+ const style = useAnimatedStyle(() => {
877
+ const min = domainMin.value;
878
+ const max = domainMax.value;
879
+ if (max === min) return {
880
+ opacity: 0
881
+ };
882
+ const at = yOf(y, plot, min, max);
883
+ // Off the top or bottom of the plot is out of the chart's range, and a rule
884
+ // pinned to the edge would claim a value the axis does not cover.
885
+ const inside = at >= plot.top - 0.5 && at <= plot.top + plot.height + 0.5;
886
+ return {
887
+ opacity: inside ? 1 : 0,
888
+ transform: [{
889
+ translateY: at
890
+ }]
891
+ };
892
+ });
893
+ if (status === 'loading') return null;
894
+ return /*#__PURE__*/_jsxs(Animated.View, {
895
+ pointerEvents: "none",
896
+ style: [{
897
+ position: 'absolute',
898
+ left: plot.left,
899
+ width: plot.width,
900
+ top: 0
901
+ }, style],
902
+ className: className,
903
+ children: [/*#__PURE__*/_jsx(View, {
904
+ style: {
905
+ height: 1,
906
+ backgroundColor: stroke,
907
+ opacity: 0.5
908
+ }
909
+ }), label ? /*#__PURE__*/_jsx(Text, {
910
+ size: "xs",
911
+ muted: true,
912
+ numberOfLines: 1,
913
+ className: "self-end pt-0.5",
914
+ children: label
915
+ }) : null]
916
+ });
917
+ }
918
+ PlotRule.displayName = 'Plot.Rule';
919
+ PlotRule.layer = 'overlay';
920
+ /**
921
+ * The x labels. Real text rather than SVG text, so they follow the theme's font
922
+ * and the platform's text scaling — SVG text does neither.
923
+ */
924
+ function PlotXAxis({
925
+ ticks = 4,
926
+ format,
927
+ className
928
+ }) {
929
+ const {
930
+ data,
931
+ xDataKey,
932
+ plot,
933
+ xScale
934
+ } = useChart('Plot.XAxis');
935
+ const labels = useMemo(() => {
936
+ if (!data.length) return [];
937
+ const count = Math.min(ticks, data.length);
938
+ const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
939
+ return Array.from({
940
+ length: count
941
+ }, (_, index) => {
942
+ const dataIndex = Math.round(index * step);
943
+ const datum = data[dataIndex];
944
+ if (!datum) return null;
945
+ return {
946
+ key: dataIndex,
947
+ text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? '')
948
+ };
949
+ }).filter(label => label !== null);
950
+ }, [data, ticks, format, xDataKey]);
951
+
952
+ /*
953
+ * Each label sits on its own point rather than being spread along the axis.
954
+ * The indices `ticks` picks are not evenly spaced — twelve months shown five
955
+ * at a time gives 0, 3, 6, 8, 11 — so spreading them evenly put the ones in
956
+ * between over the wrong part of the plot.
957
+ */
958
+ return /*#__PURE__*/_jsx(View, {
959
+ style: {
960
+ position: 'absolute',
961
+ inset: 0,
962
+ pointerEvents: 'none'
963
+ },
964
+ className: cn(className),
965
+ children: labels.map(label => {
966
+ const x = xScale === 'band' ? bandOf(label.key, data.length, plot) : xOf(label.key, data.length, plot);
967
+ return /*#__PURE__*/_jsx(Text, {
968
+ size: "xs",
969
+ muted: true,
970
+ numberOfLines: 1,
971
+ style: {
972
+ position: 'absolute',
973
+ bottom: 0,
974
+ // Centred on its point, then held inside the chart. On a point
975
+ // scale the first and last sit on the plot's own edges, so a box
976
+ // centred on them hangs half its width off the side.
977
+ left: Math.max(0, Math.min(x - POINT_LABEL_WIDTH / 2, plot.left + plot.width + PADDING.right - POINT_LABEL_WIDTH)),
978
+ width: POINT_LABEL_WIDTH,
979
+ textAlign: 'center'
980
+ },
981
+ children: label.text
982
+ }, label.key);
983
+ })
984
+ });
985
+ }
986
+ PlotXAxis.displayName = 'Plot.XAxis';
987
+ PlotXAxis.layer = 'overlay';
988
+ /**
989
+ * Value labels down the side, one per grid line.
990
+ *
991
+ * Give it the same `ticks` as the grid, or the numbers name lines that are not
992
+ * there. Four is the default on both for that reason.
993
+ *
994
+ * The labels are the domain the data settles at, not the tweening one — a
995
+ * number counting through every intermediate value while the axis animates is
996
+ * noise, and the axis is the part of a chart that has to hold still enough to
997
+ * be read.
998
+ */
999
+ function PlotYAxis({
1000
+ ticks = 4,
1001
+ format,
1002
+ className
1003
+ }) {
1004
+ const {
1005
+ plot,
1006
+ extent
1007
+ } = useChart('Plot.YAxis');
1008
+ const labels = useMemo(() => {
1009
+ const [min, max] = extent;
1010
+ if (min === 0 && max === 0) return [];
1011
+ return Array.from({
1012
+ length: ticks + 1
1013
+ }, (_unused, index) => {
1014
+ const value = max - (max - min) * index / ticks;
1015
+ return {
1016
+ key: index,
1017
+ text: format ? format(value) : compactNumber(value)
1018
+ };
1019
+ });
1020
+ }, [extent, ticks, format]);
1021
+ return /*#__PURE__*/_jsx(View, {
1022
+ pointerEvents: "none",
1023
+ style: {
1024
+ position: 'absolute',
1025
+ left: 0,
1026
+ // Centred on the grid line each label names: the strip is lifted half a
1027
+ // label and grown by a whole one, so `justify-between` lands the text's
1028
+ // middle on the line rather than its top edge on the first.
1029
+ top: plot.top - AXIS_LABEL_HEIGHT / 2,
1030
+ height: plot.height + AXIS_LABEL_HEIGHT,
1031
+ width: Math.max(plot.left - Y_AXIS_GUTTER, 0)
1032
+ },
1033
+ className: cn('items-end justify-between', className),
1034
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
1035
+ size: "xs",
1036
+ muted: true,
1037
+ numberOfLines: 1,
1038
+ children: label.text
1039
+ }, label.key))
1040
+ });
1041
+ }
1042
+ PlotYAxis.displayName = 'Plot.YAxis';
1043
+ PlotYAxis.layer = 'overlay';
1044
+ // Read by the root, which has to leave room for the labels before it lays the
1045
+ // plot out.
1046
+ PlotYAxis.axis = 'y';
1047
+ /**
1048
+ * The touch handling, and the line that follows it.
1049
+ *
1050
+ * Split from the readout next door because they are separately useful: a plot
1051
+ * whose value is shown in its own header wants this and no label, and a plot
1052
+ * that highlights a bar wants this and nothing else at all. Both read the same
1053
+ * index.
1054
+ *
1055
+ * The hit area is the whole plot. A cursor you have to land on the line to
1056
+ * summon is a cursor nobody finds.
1057
+ */
1058
+ function PlotCursor({
1059
+ color,
1060
+ showLine = true
1061
+ }) {
1062
+ const {
1063
+ data,
1064
+ plot,
1065
+ xScale,
1066
+ activeIndex,
1067
+ setActiveIndexJS,
1068
+ status
1069
+ } = useChart('Plot.Cursor');
1070
+ const token = useCSSVariable('--color-foreground');
1071
+ const stroke = color ?? (typeof token === 'string' ? token : '#888888');
1072
+ const total = data.length;
1073
+ const left = plot.left;
1074
+ const width = plot.width;
1075
+ const banded = xScale === 'band';
1076
+
1077
+ /*
1078
+ * Built in one closure, and everything it captures is a number or a shared
1079
+ * value. A worklet may only call another worklet, and the rule is enforced by
1080
+ * crashing rather than by warning — so the resolver is declared next to its
1081
+ * callers instead of as a helper further down the file where it would be easy
1082
+ * to leave un-workletised.
1083
+ */
1084
+ const pan = useMemo(() => {
1085
+ const resolve = x => {
1086
+ 'worklet';
1087
+
1088
+ if (total < 1 || width <= 0) return;
1089
+ const ratio = (x - left) / width;
1090
+ const clamped = Math.min(1, Math.max(0, ratio));
1091
+ // A band scale divides the plot into `total` slices and the finger is in
1092
+ // one of them; a point scale has `total - 1` gaps between marks and the
1093
+ // finger rounds to the nearest.
1094
+ const next = banded ? Math.min(total - 1, Math.floor(clamped * total)) : Math.round(clamped * Math.max(total - 1, 0));
1095
+ if (next === activeIndex.value) return;
1096
+ activeIndex.value = next;
1097
+ // Only the index needs JS, and only when it changes — a drag across a
1098
+ // hundred rows costs a hundred re-renders at most, not one per frame for
1099
+ // the length of the gesture.
1100
+ runOnJS(setActiveIndexJS)(next);
1101
+ };
1102
+ return Gesture.Pan().minDistance(0).onBegin(event => {
1103
+ 'worklet';
1104
+
1105
+ resolve(event.x);
1106
+ }).onUpdate(event => {
1107
+ 'worklet';
1108
+
1109
+ resolve(event.x);
1110
+ }).onFinalize(() => {
1111
+ 'worklet';
1112
+
1113
+ activeIndex.value = -1;
1114
+ runOnJS(setActiveIndexJS)(-1);
1115
+ });
1116
+ }, [total, left, width, banded, activeIndex, setActiveIndexJS]);
1117
+ const lineStyle = useAnimatedStyle(() => {
1118
+ const index = activeIndex.value;
1119
+ if (index < 0) return {
1120
+ opacity: 0
1121
+ };
1122
+ const x = banded ? bandOf(index, total, plot) : xOf(index, total, plot);
1123
+ return {
1124
+ opacity: 0.45,
1125
+ transform: [{
1126
+ translateX: x
1127
+ }]
1128
+ };
1129
+ });
1130
+ if (status === 'loading') return null;
1131
+ return /*#__PURE__*/_jsx(GestureDetector, {
1132
+ gesture: pan,
1133
+ children: /*#__PURE__*/_jsx(View, {
1134
+ style: StyleSheet.absoluteFill,
1135
+ children: showLine ? /*#__PURE__*/_jsx(Animated.View, {
1136
+ pointerEvents: "none",
1137
+ style: [{
1138
+ position: 'absolute',
1139
+ left: 0,
1140
+ top: plot.top,
1141
+ width: 1,
1142
+ height: plot.height,
1143
+ backgroundColor: stroke
1144
+ }, lineStyle]
1145
+ }) : null
1146
+ })
1147
+ });
1148
+ }
1149
+ PlotCursor.displayName = 'Plot.Cursor';
1150
+ PlotCursor.layer = 'overlay';
1151
+ /**
1152
+ * The readout that rides the cursor.
1153
+ *
1154
+ * Needs a `Plot.Cursor` beside it — the cursor owns the gesture and this only
1155
+ * reads the index it resolves. On its own it never appears, which is the right
1156
+ * failure: a label with no way to move is worse than no label.
1157
+ */
1158
+ function PlotTooltip({
1159
+ formatValue,
1160
+ formatX,
1161
+ children,
1162
+ className
1163
+ }) {
1164
+ const {
1165
+ data,
1166
+ xDataKey,
1167
+ plot,
1168
+ xScale,
1169
+ series,
1170
+ activeIndex,
1171
+ activeIndexJS,
1172
+ status
1173
+ } = useChart('Plot.Tooltip');
1174
+ const total = data.length;
1175
+ const banded = xScale === 'band';
1176
+
1177
+ // Centred over the cursor but clamped inside the plot, so it never runs off
1178
+ // the edge at the first or last row.
1179
+ const style = useAnimatedStyle(() => {
1180
+ const index = activeIndex.value;
1181
+ if (index < 0) return {
1182
+ opacity: 0
1183
+ };
1184
+ const x = banded ? bandOf(index, total, plot) : xOf(index, total, plot);
1185
+ const half = LABEL_WIDTH / 2;
1186
+ const clamped = Math.min(plot.left + plot.width - half, Math.max(plot.left + half, x));
1187
+ return {
1188
+ opacity: 1,
1189
+ transform: [{
1190
+ translateX: clamped - half
1191
+ }]
1192
+ };
1193
+ });
1194
+ const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
1195
+ const fmtValue = formatValue ?? (value => compactNumber(value));
1196
+ const fmtX = formatX ?? (datum => String(datum[xDataKey] ?? ''));
1197
+ if (status === 'loading') return null;
1198
+ return /*#__PURE__*/_jsx(Animated.View, {
1199
+ pointerEvents: "none",
1200
+ style: [{
1201
+ position: 'absolute',
1202
+ left: 0,
1203
+ top: Math.max(plot.top - 4, 0),
1204
+ width: LABEL_WIDTH
1205
+ }, style],
1206
+ children: /*#__PURE__*/_jsx(View, {
1207
+ className: cn('items-center rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
1208
+ children: active ? children ? children(active, activeIndexJS) : /*#__PURE__*/_jsxs(_Fragment, {
1209
+ children: [/*#__PURE__*/_jsx(Text, {
1210
+ size: "xs",
1211
+ muted: true,
1212
+ numberOfLines: 1,
1213
+ children: fmtX(active)
1214
+ }), series.map(([key, color]) => {
1215
+ const value = active[key];
1216
+ if (typeof value !== 'number') return null;
1217
+ return /*#__PURE__*/_jsxs(View, {
1218
+ className: "flex-row items-center gap-1.5",
1219
+ children: [series.length > 1 ? /*#__PURE__*/_jsx(View, {
1220
+ style: {
1221
+ backgroundColor: color
1222
+ },
1223
+ className: "h-1.5 w-1.5 rounded-full"
1224
+ }) : null, /*#__PURE__*/_jsx(Text, {
1225
+ size: "sm",
1226
+ weight: "semibold",
1227
+ numberOfLines: 1,
1228
+ children: fmtValue(value, key)
1229
+ })]
1230
+ }, key);
1231
+ })]
1232
+ }) : null
1233
+ })
1234
+ });
1235
+ }
1236
+ PlotTooltip.displayName = 'Plot.Tooltip';
1237
+ PlotTooltip.layer = 'overlay';
1238
+
1239
+ /* -------------------------------------------------------------------------- */
1240
+ /* Header layer */
1241
+ /* -------------------------------------------------------------------------- */
1242
+
1243
+ /** The row above the drawing: what it is, and the one number worth reading. */
1244
+ function PlotHeader({
1245
+ className,
1246
+ title,
1247
+ value,
1248
+ caption,
1249
+ children,
1250
+ ...props
1251
+ }) {
1252
+ return /*#__PURE__*/_jsx(View, {
1253
+ className: cn('gap-0.5', className),
1254
+ ...props,
1255
+ children: children ?? /*#__PURE__*/_jsxs(_Fragment, {
1256
+ children: [title ? /*#__PURE__*/_jsx(Text, {
1257
+ size: "sm",
1258
+ muted: true,
1259
+ numberOfLines: 1,
1260
+ children: title
1261
+ }) : null, value ? /*#__PURE__*/_jsx(Text, {
1262
+ size: "2xl",
1263
+ weight: "semibold",
1264
+ numberOfLines: 1,
1265
+ children: value
1266
+ }) : null, caption ? /*#__PURE__*/_jsx(Text, {
1267
+ size: "xs",
1268
+ muted: true,
1269
+ numberOfLines: 1,
1270
+ children: caption
1271
+ }) : null]
1272
+ })
1273
+ });
1274
+ }
1275
+ PlotHeader.displayName = 'Plot.Header';
1276
+ PlotHeader.layer = 'header';
1277
+ /**
1278
+ * A swatch and a name per mark, taken from the marks that registered.
1279
+ *
1280
+ * In the header rather than over the drawing, because a key laid inside the
1281
+ * plot either covers a mark or is squeezed to one word a line.
1282
+ */
1283
+ function PlotLegend({
1284
+ className,
1285
+ labels,
1286
+ ...props
1287
+ }) {
1288
+ const {
1289
+ series
1290
+ } = useChart('Plot.Legend');
1291
+ if (!series.length) return null;
1292
+ return /*#__PURE__*/_jsx(View, {
1293
+ className: cn('flex-row flex-wrap items-center gap-x-3 gap-y-1 pt-1', className),
1294
+ ...props,
1295
+ children: series.map(([key, color]) => /*#__PURE__*/_jsxs(View, {
1296
+ className: "flex-row items-center gap-1.5",
1297
+ children: [/*#__PURE__*/_jsx(View, {
1298
+ style: {
1299
+ backgroundColor: color
1300
+ },
1301
+ className: "h-2 w-2 rounded-full"
1302
+ }), /*#__PURE__*/_jsx(Text, {
1303
+ size: "xs",
1304
+ muted: true,
1305
+ numberOfLines: 1,
1306
+ children: labels?.[key] ?? key
1307
+ })]
1308
+ }, key))
1309
+ });
1310
+ }
1311
+ PlotLegend.displayName = 'Plot.Legend';
1312
+ PlotLegend.layer = 'header';
1313
+ export const Plot = Object.assign(PlotRoot, {
1314
+ Header: PlotHeader,
1315
+ Legend: PlotLegend,
1316
+ Grid: PlotGrid,
1317
+ Area: PlotArea,
1318
+ Bars: PlotBars,
1319
+ Line: PlotLine,
1320
+ Dots: PlotDots,
1321
+ Rule: PlotRule,
1322
+ Layer: PlotLayer,
1323
+ Overlay: PlotOverlay,
1324
+ XAxis: PlotXAxis,
1325
+ YAxis: PlotYAxis,
1326
+ Cursor: PlotCursor,
1327
+ Tooltip: PlotTooltip
1328
+ });
1329
+ //# sourceMappingURL=index.js.map