panelui-native 0.99.0 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +2 -0
  2. package/lib/module/components/animated-badge/index.js +24 -2
  3. package/lib/module/components/animated-badge/index.js.map +1 -1
  4. package/lib/module/components/bump-chart/index.js +1236 -0
  5. package/lib/module/components/bump-chart/index.js.map +1 -0
  6. package/lib/module/components/compare/index.js +576 -0
  7. package/lib/module/components/compare/index.js.map +1 -0
  8. package/lib/module/components/mirror-area-chart/index.js +1113 -0
  9. package/lib/module/components/mirror-area-chart/index.js.map +1 -0
  10. package/lib/module/components/sankey-chart/index.js +933 -0
  11. package/lib/module/components/sankey-chart/index.js.map +1 -0
  12. package/lib/module/components/sankey-chart/sankey-layout.js +530 -0
  13. package/lib/module/components/sankey-chart/sankey-layout.js.map +1 -0
  14. package/lib/module/icons/index.js +15 -3
  15. package/lib/module/icons/index.js.map +1 -1
  16. package/lib/module/index.js +4 -0
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/utils/chart.js +101 -0
  19. package/lib/module/utils/chart.js.map +1 -1
  20. package/lib/typescript/src/components/animated-badge/index.d.ts +5 -0
  21. package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -1
  22. package/lib/typescript/src/components/bump-chart/index.d.ts +267 -0
  23. package/lib/typescript/src/components/bump-chart/index.d.ts.map +1 -0
  24. package/lib/typescript/src/components/compare/index.d.ts +183 -0
  25. package/lib/typescript/src/components/compare/index.d.ts.map +1 -0
  26. package/lib/typescript/src/components/mirror-area-chart/index.d.ts +263 -0
  27. package/lib/typescript/src/components/mirror-area-chart/index.d.ts.map +1 -0
  28. package/lib/typescript/src/components/sankey-chart/index.d.ts +307 -0
  29. package/lib/typescript/src/components/sankey-chart/index.d.ts.map +1 -0
  30. package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts +107 -0
  31. package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts.map +1 -0
  32. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  33. package/lib/typescript/src/index.d.ts +4 -0
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/lib/typescript/src/utils/chart.d.ts +40 -0
  36. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/components/animated-badge/index.tsx +35 -2
  39. package/src/components/bump-chart/index.tsx +1455 -0
  40. package/src/components/compare/index.tsx +650 -0
  41. package/src/components/mirror-area-chart/index.tsx +1333 -0
  42. package/src/components/sankey-chart/index.tsx +1165 -0
  43. package/src/components/sankey-chart/sankey-layout.ts +645 -0
  44. package/src/icons/index.tsx +20 -6
  45. package/src/index.ts +62 -0
  46. package/src/utils/chart.ts +116 -0
@@ -0,0 +1,1113 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * MirrorAreaChart — two readings of one timeline, one above a baseline and one
5
+ * below it.
6
+ *
7
+ * ```tsx
8
+ * <MirrorAreaChart data={load} xDataKey="time">
9
+ * <MirrorAreaChart.Grid />
10
+ * <MirrorAreaChart.Area dataKey="jobs" label="jobs" muted />
11
+ * <MirrorAreaChart.Area dataKey="high" label="high priority" />
12
+ * <MirrorAreaChart.Area dataKey="share" label="% high" side="bottom" />
13
+ * <MirrorAreaChart.Baseline />
14
+ * <MirrorAreaChart.Tooltip />
15
+ * </MirrorAreaChart>
16
+ * ```
17
+ *
18
+ * ## Two scales, one timeline
19
+ *
20
+ * Each half has its own scale, because the two readings are usually in
21
+ * different units — a count above and a percentage below, or requests above
22
+ * and errors below. Both grow away from the baseline, so a peak in one lines
23
+ * up with whatever the other did at the same moment, and neither is squeezed
24
+ * into the other's range.
25
+ *
26
+ * Areas on the same side overlay in the order they are declared. Declare the
27
+ * whole first and the part of it second, so the part is drawn inside the whole.
28
+ */
29
+ import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from 'react';
30
+ import { StyleSheet, View } from 'react-native';
31
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
32
+ import Animated, { Easing, cancelAnimation, runOnJS, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
33
+ import Svg, { Circle, Defs, G, Line as SvgLine, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
34
+ import { useCSSVariable } from 'uniwind';
35
+ import { Text } from "../../primitives/text.js";
36
+ import { ChartAccessibilityData } from "../../primitives/chart-accessibility.js";
37
+ import { finiteChartDomain } from "../../primitives/finite-chart.js";
38
+ import { colorStop, columnValues, compactNumber, dotsPath, segment, useSeriesColor, xOf } from "../../utils/chart.js";
39
+ import { cn } from "../../utils/cn.js";
40
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
41
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
42
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
43
+ const AnimatedLine = Animated.createAnimatedComponent(SvgLine);
44
+ const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
45
+ const PADDING = {
46
+ top: 12,
47
+ right: 10,
48
+ bottom: 30,
49
+ left: 10
50
+ };
51
+
52
+ /** Left gutter reserved when a `YAxis` is present. */
53
+ const Y_AXIS_WIDTH = 40;
54
+
55
+ /** Gap between the value labels and the plot. */
56
+ const Y_AXIS_GUTTER = 6;
57
+
58
+ /** Box each x label is centred in. */
59
+ const POINT_LABEL_WIDTH = 56;
60
+
61
+ /** Line height of an `xs` label, for centring one on the line it names. */
62
+ const AXIS_LABEL_HEIGHT = 16;
63
+
64
+ /** Width of the scrub readout. */
65
+ const READOUT_WIDTH = 168;
66
+
67
+ /** Diameter of the dot riding the top edge under the crosshair. */
68
+ const DOT = 9;
69
+ const MirrorAreaChartContext = /*#__PURE__*/createContext(null);
70
+ function useChart(component) {
71
+ const context = useContext(MirrorAreaChartContext);
72
+ if (!context) {
73
+ throw new Error(`${component} must be used within a <MirrorAreaChart>`);
74
+ }
75
+ return context;
76
+ }
77
+
78
+ /** The row under the crosshair, for a header or a readout outside the chart. */
79
+ export function useMirrorAreaChart() {
80
+ const {
81
+ data,
82
+ activeIndexJS,
83
+ xDataKey
84
+ } = useChart('useMirrorAreaChart');
85
+ return {
86
+ activeIndex: activeIndexJS,
87
+ activePoint: activeIndexJS >= 0 ? data[activeIndexJS] ?? null : null,
88
+ xDataKey
89
+ };
90
+ }
91
+
92
+ /**
93
+ * A series' edge, `grow` of the way out from the baseline. `direction` is `-1`
94
+ * for the top half and `1` for the bottom. A negative value is drawn on the
95
+ * baseline: each half only grows away from it.
96
+ */
97
+ function edgeRuns(values, plot, baseline, height, max, grow, direction) {
98
+ 'worklet';
99
+
100
+ const runs = [];
101
+ let run = [];
102
+ const span = max > 0 ? max : 1;
103
+ for (let i = 0; i < values.length; i += 1) {
104
+ const value = values[i];
105
+ if (value === null || value === undefined) {
106
+ if (run.length) runs.push(run);
107
+ run = [];
108
+ continue;
109
+ }
110
+ const reach = Math.max(value, 0) / span * height * grow;
111
+ run.push({
112
+ x: xOf(i, values.length, plot),
113
+ y: baseline + direction * reach
114
+ });
115
+ }
116
+ if (run.length) runs.push(run);
117
+ return runs;
118
+ }
119
+ function edgePath(runs, curve) {
120
+ 'worklet';
121
+
122
+ let d = '';
123
+ for (const run of runs) d += `${segment(run, curve)} `;
124
+ return d;
125
+ }
126
+ function fillPath(runs, curve, baseline) {
127
+ 'worklet';
128
+
129
+ let d = '';
130
+ for (const run of runs) {
131
+ const edge = segment(run, curve);
132
+ if (!edge) continue;
133
+ const first = run[0];
134
+ const last = run[run.length - 1];
135
+ d += `${edge} L${last.x},${baseline} L${first.x},${baseline} Z `;
136
+ }
137
+ return d;
138
+ }
139
+
140
+ /** The largest value on one side, padded so a peak does not touch the edge. */
141
+ function sideExtent(data, keys, explicit) {
142
+ if (explicit) return Math.max(explicit[1], 0) || 1;
143
+ let max = 0;
144
+ for (const row of data) {
145
+ for (const key of keys) {
146
+ const value = row[key];
147
+ if (typeof value === 'number' && Number.isFinite(value) && value > max) max = value;
148
+ }
149
+ }
150
+ return max > 0 ? max * 1.08 : 1;
151
+ }
152
+
153
+ /** Imperative handle: re-run the reveal on demand, for a "replay" control. */
154
+
155
+ function ChildSlot({
156
+ children
157
+ }) {
158
+ return /*#__PURE__*/_jsx(_Fragment, {
159
+ children: children
160
+ });
161
+ }
162
+ function partition(children) {
163
+ const svg = [];
164
+ const series = [];
165
+ const overlay = [];
166
+ const header = [];
167
+ Children.forEach(children, (child, index) => {
168
+ if (! /*#__PURE__*/isValidElement(child)) return;
169
+ const layer = child.type.layer ?? 'svg';
170
+ const slot = /*#__PURE__*/_jsx(ChildSlot, {
171
+ children: child
172
+ }, index);
173
+ const bucket = layer === 'header' ? header : layer === 'overlay' ? overlay : layer === 'series' ? series : svg;
174
+ bucket.push(slot);
175
+ });
176
+ return {
177
+ svg,
178
+ series,
179
+ overlay,
180
+ header
181
+ };
182
+ }
183
+ const MirrorAreaChartRoot = /*#__PURE__*/forwardRef(function MirrorAreaChartRoot({
184
+ className,
185
+ data,
186
+ xDataKey = 'date',
187
+ split = 0.55,
188
+ topDomain,
189
+ bottomDomain,
190
+ curve = 'monotone',
191
+ status = 'ready',
192
+ aspectRatio = 1.6,
193
+ animationDuration = 700,
194
+ domainDuration = 500,
195
+ onActiveIndexChange,
196
+ accessible,
197
+ accessibilityLabel,
198
+ accessibilityHint,
199
+ accessibilityLabelForDatum,
200
+ onAccessibilityDatumPress,
201
+ compact = false,
202
+ children,
203
+ ...props
204
+ }, ref) {
205
+ const [size, setSize] = useState({
206
+ width: 0,
207
+ height: 0
208
+ });
209
+ const [registered, setRegistered] = useState([]);
210
+ const [activeIndexJS, setActiveIndexJS] = useState(-1);
211
+ const grow = useSharedValue(0);
212
+ const topMax = useSharedValue(0);
213
+ const bottomMax = useSharedValue(0);
214
+ const activeIndex = useSharedValue(-1);
215
+ const reducedMotion = useReducedMotion();
216
+ const registerSeries = useMemo(() => entry => setRegistered(current => {
217
+ const existing = current.find(item => item.key === entry.key);
218
+ if (existing && existing.color === entry.color && existing.label === entry.label && existing.side === entry.side) {
219
+ return current;
220
+ }
221
+ return [...current.filter(item => item.key !== entry.key), entry];
222
+ }), []);
223
+ const unregisterSeries = useMemo(() => key => setRegistered(current => current.filter(item => item.key !== key)), []);
224
+
225
+ /*
226
+ * The order the areas were declared in, read from the children rather than
227
+ * from registration: an area re-registers when its colour resolves, and
228
+ * appending it again would reorder the legend and the readout.
229
+ */
230
+ const {
231
+ hasYAxis,
232
+ order
233
+ } = useMemo(() => {
234
+ let axis = false;
235
+ const keys = [];
236
+ Children.forEach(children, child => {
237
+ if (! /*#__PURE__*/isValidElement(child)) return;
238
+ const type = child.type;
239
+ const childProps = child.props;
240
+ if (type.axis === 'y') axis = true;
241
+ if (type.area && typeof childProps.dataKey === 'string') keys.push(childProps.dataKey);
242
+ });
243
+ return {
244
+ hasYAxis: axis,
245
+ order: keys.join('|')
246
+ };
247
+ }, [children]);
248
+ const series = useMemo(() => {
249
+ const declared = order ? order.split('|') : [];
250
+ const position = key => {
251
+ const index = declared.indexOf(key);
252
+ return index === -1 ? declared.length : index;
253
+ };
254
+ return [...registered].sort((a, b) => position(a.key) - position(b.key));
255
+ }, [registered, order]);
256
+ const pad = compact ? {
257
+ top: 2,
258
+ right: 1,
259
+ bottom: 2,
260
+ left: 1
261
+ } : {
262
+ ...PADDING,
263
+ left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left
264
+ };
265
+ const plot = {
266
+ left: pad.left,
267
+ top: pad.top,
268
+ width: Math.max(size.width - pad.left - pad.right, 0),
269
+ height: Math.max(size.height - pad.top - pad.bottom, 0)
270
+ };
271
+ const share = Math.min(Math.max(Number.isFinite(split) ? split : 0.55, 0.1), 0.9);
272
+ const baseline = plot.top + plot.height * share;
273
+ const topKeys = series.filter(item => item.side === 'top').map(item => item.key).join('|');
274
+ const bottomKeys = series.filter(item => item.side === 'bottom').map(item => item.key).join('|');
275
+ const safeTop = finiteChartDomain(topDomain);
276
+ const safeBottom = finiteChartDomain(bottomDomain);
277
+ const topExtent = useMemo(() => sideExtent(data, topKeys ? topKeys.split('|') : [], safeTop),
278
+ // eslint-disable-next-line react-hooks/exhaustive-deps
279
+ [data, topKeys, safeTop?.[0], safeTop?.[1]]);
280
+ const bottomExtent = useMemo(() => sideExtent(data, bottomKeys ? bottomKeys.split('|') : [], safeBottom),
281
+ // eslint-disable-next-line react-hooks/exhaustive-deps
282
+ [data, bottomKeys, safeBottom?.[0], safeBottom?.[1]]);
283
+ const loading = status === 'loading';
284
+ useEffect(() => {
285
+ if (loading) return;
286
+ const first = topMax.value === 0 && bottomMax.value === 0;
287
+ if (first || reducedMotion) {
288
+ topMax.value = topExtent;
289
+ bottomMax.value = bottomExtent;
290
+ return;
291
+ }
292
+ topMax.value = withTiming(topExtent, {
293
+ duration: domainDuration
294
+ });
295
+ bottomMax.value = withTiming(bottomExtent, {
296
+ duration: domainDuration
297
+ });
298
+ }, [topExtent, bottomExtent, loading, reducedMotion, domainDuration, topMax, bottomMax]);
299
+ const revealed = useRef(false);
300
+ const playReveal = useMemo(() => () => {
301
+ if (reducedMotion) {
302
+ grow.value = 1;
303
+ return;
304
+ }
305
+ grow.value = 0;
306
+ grow.value = withTiming(1, {
307
+ duration: animationDuration,
308
+ easing: Easing.out(Easing.cubic)
309
+ });
310
+ }, [reducedMotion, animationDuration, grow]);
311
+ useEffect(() => {
312
+ // Going back to `loading` lays the bands flat again, so a refetch grows
313
+ // them out from the baseline instead of appearing complete.
314
+ if (loading) {
315
+ revealed.current = false;
316
+ cancelAnimation(grow);
317
+ grow.value = 0;
318
+ return;
319
+ }
320
+ if (revealed.current || plot.width <= 0 || !data.length) return;
321
+ revealed.current = true;
322
+ playReveal();
323
+ }, [loading, plot.width, data.length, playReveal, grow]);
324
+ useImperativeHandle(ref, () => ({
325
+ replay: playReveal
326
+ }), [playReveal]);
327
+ const handleActiveIndex = useMemo(() => index => {
328
+ setActiveIndexJS(index);
329
+ onActiveIndexChange?.(index, index >= 0 ? data[index] ?? null : null);
330
+ }, [onActiveIndexChange, data]);
331
+ const onLayout = event => {
332
+ const {
333
+ width,
334
+ height
335
+ } = event.nativeEvent.layout;
336
+ setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
337
+ width,
338
+ height
339
+ });
340
+ props.onLayout?.(event);
341
+ };
342
+ const context = useMemo(() => ({
343
+ data,
344
+ xDataKey,
345
+ plot,
346
+ pad,
347
+ baseline,
348
+ status,
349
+ curve,
350
+ series,
351
+ registerSeries,
352
+ unregisterSeries,
353
+ topMax,
354
+ bottomMax,
355
+ topExtent,
356
+ bottomExtent,
357
+ grow,
358
+ activeIndex,
359
+ activeIndexJS,
360
+ setActiveIndexJS: handleActiveIndex
361
+ }),
362
+ // `plot` and `pad` are rebuilt every render from `size`, so they are
363
+ // compared by value.
364
+ // eslint-disable-next-line react-hooks/exhaustive-deps
365
+ [data, xDataKey, plot.width, plot.height, plot.left, plot.top, pad.right, baseline, status, curve, series, registerSeries, unregisterSeries, topMax, bottomMax, topExtent, bottomExtent, grow, activeIndex, activeIndexJS, handleActiveIndex]);
366
+ const {
367
+ svg,
368
+ series: seriesLayer,
369
+ overlay,
370
+ header
371
+ } = partition(children);
372
+ return /*#__PURE__*/_jsx(MirrorAreaChartContext.Provider, {
373
+ value: context,
374
+ children: /*#__PURE__*/_jsxs(View, {
375
+ ...props,
376
+ style: props.style,
377
+ className: cn('w-full', className),
378
+ children: [header, /*#__PURE__*/_jsx(ChartAccessibilityData, {
379
+ chart: "Mirror area chart",
380
+ data: data,
381
+ disabled: accessible === false || loading,
382
+ accessibilityLabel: accessibilityLabel,
383
+ accessibilityHint: accessibilityHint,
384
+ accessibilityLabelForDatum: accessibilityLabelForDatum,
385
+ onAccessibilityDatumPress: onAccessibilityDatumPress,
386
+ valueOf: datum => [[xDataKey, datum[xDataKey]], ...series.map(item => [item.label, datum[item.key]])]
387
+ }), /*#__PURE__*/_jsx(View, {
388
+ onLayout: onLayout,
389
+ style: {
390
+ aspectRatio
391
+ },
392
+ className: "w-full",
393
+ accessibilityElementsHidden: true,
394
+ importantForAccessibility: "no-hide-descendants",
395
+ children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
396
+ children: [/*#__PURE__*/_jsxs(Svg, {
397
+ width: "100%",
398
+ height: "100%",
399
+ style: StyleSheet.absoluteFill,
400
+ children: [svg, /*#__PURE__*/_jsx(G, {
401
+ children: seriesLayer
402
+ })]
403
+ }), overlay]
404
+ }) : null
405
+ })]
406
+ })
407
+ });
408
+ });
409
+ MirrorAreaChartRoot.displayName = 'MirrorAreaChart';
410
+
411
+ /* -------------------------------------------------------------------------- */
412
+ /* SVG layer */
413
+ /* -------------------------------------------------------------------------- */
414
+
415
+ /**
416
+ * The texture behind the bands. Dots by default, which mark the plot's extent
417
+ * without drawing lines that compete with the baseline.
418
+ */
419
+ function MirrorAreaChartGrid({
420
+ variant = 'dots',
421
+ spacing,
422
+ color,
423
+ opacity = 1
424
+ }) {
425
+ const {
426
+ plot,
427
+ baseline
428
+ } = useChart('MirrorAreaChart.Grid');
429
+ const token = useCSSVariable('--color-border');
430
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
431
+ const dots = useMemo(() => {
432
+ if (variant !== 'dots') return '';
433
+ const gap = Math.max(spacing ?? 12, 4);
434
+ const points = [];
435
+ const columns = Math.floor(plot.width / gap);
436
+ const offsetX = plot.left + (plot.width - columns * gap) / 2;
437
+ // Rows are laid out from the baseline in both directions, so one row
438
+ // always lies on it and the pattern is symmetrical about it.
439
+ for (let y = baseline; y >= plot.top - 0.5; y -= gap) {
440
+ for (let c = 0; c <= columns; c += 1) points.push({
441
+ x: offsetX + c * gap,
442
+ y
443
+ });
444
+ }
445
+ for (let y = baseline + gap; y <= plot.top + plot.height + 0.5; y += gap) {
446
+ for (let c = 0; c <= columns; c += 1) points.push({
447
+ x: offsetX + c * gap,
448
+ y
449
+ });
450
+ }
451
+ return dotsPath(points, 0.9);
452
+ }, [variant, spacing, plot.left, plot.top, plot.width, plot.height, baseline]);
453
+ if (variant === 'dots') {
454
+ return /*#__PURE__*/_jsx(Path, {
455
+ d: dots,
456
+ fill: stroke,
457
+ opacity: opacity
458
+ });
459
+ }
460
+ const rules = Math.max(Math.round(spacing ?? 2), 1);
461
+ const lines = [];
462
+ for (let i = 1; i <= rules; i += 1) {
463
+ lines.push(baseline - (baseline - plot.top) * i / rules);
464
+ lines.push(baseline + (plot.top + plot.height - baseline) * i / rules);
465
+ }
466
+ return /*#__PURE__*/_jsx(G, {
467
+ opacity: opacity,
468
+ children: lines.map(y => /*#__PURE__*/_jsx(SvgLine, {
469
+ x1: plot.left,
470
+ x2: plot.left + plot.width,
471
+ y1: y,
472
+ y2: y,
473
+ stroke: stroke,
474
+ strokeDasharray: "4,6",
475
+ strokeWidth: 1
476
+ }, y))
477
+ });
478
+ }
479
+ MirrorAreaChartGrid.displayName = 'MirrorAreaChart.Grid';
480
+ MirrorAreaChartGrid.layer = 'svg';
481
+ /**
482
+ * The line both halves grow away from. Declare it after the areas so it is
483
+ * drawn over their bases.
484
+ */
485
+ function MirrorAreaChartBaseline({
486
+ color,
487
+ strokeWidth = 1,
488
+ opacity = 0.5
489
+ }) {
490
+ const {
491
+ plot,
492
+ baseline
493
+ } = useChart('MirrorAreaChart.Baseline');
494
+ const token = useCSSVariable('--color-muted-foreground');
495
+ const stroke = color ?? (typeof token === 'string' ? token : '#737373');
496
+ return /*#__PURE__*/_jsx(SvgLine, {
497
+ x1: plot.left,
498
+ x2: plot.left + plot.width,
499
+ y1: baseline,
500
+ y2: baseline,
501
+ stroke: stroke,
502
+ strokeWidth: strokeWidth,
503
+ opacity: opacity
504
+ });
505
+ }
506
+ MirrorAreaChartBaseline.displayName = 'MirrorAreaChart.Baseline';
507
+ MirrorAreaChartBaseline.layer = 'series';
508
+ /**
509
+ * One band, growing away from the baseline on its side. Areas on one side
510
+ * overlay in declaration order, so a whole declared first is drawn behind a
511
+ * part of it declared second.
512
+ */
513
+ function MirrorAreaChartArea({
514
+ dataKey,
515
+ side = 'top',
516
+ label,
517
+ color,
518
+ colorIndex = 1,
519
+ muted = false,
520
+ fillOpacity,
521
+ gradientToOpacity,
522
+ showLine = true,
523
+ strokeWidth = 1.5
524
+ }) {
525
+ const {
526
+ data,
527
+ plot,
528
+ baseline,
529
+ curve,
530
+ registerSeries,
531
+ unregisterSeries,
532
+ topMax,
533
+ bottomMax,
534
+ grow
535
+ } = useChart('MirrorAreaChart.Area');
536
+ const seriesColor = useSeriesColor(color, colorIndex);
537
+ const mutedToken = useCSSVariable('--color-muted-foreground');
538
+ const stroke = muted && !color ? typeof mutedToken === 'string' ? mutedToken : '#737373' : seriesColor;
539
+ const name = label ?? dataKey;
540
+ const gradientId = `panelui-mirror-fill-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
541
+ const outer = fillOpacity ?? (muted ? 0.22 : 0.3);
542
+ const inner = gradientToOpacity ?? outer;
543
+ useEffect(() => {
544
+ registerSeries({
545
+ key: dataKey,
546
+ color: stroke,
547
+ label: name,
548
+ side
549
+ });
550
+ }, [dataKey, stroke, name, side, registerSeries]);
551
+ useEffect(() => () => unregisterSeries(dataKey), [dataKey, unregisterSeries]);
552
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
553
+ const top = side === 'top';
554
+ const height = top ? baseline - plot.top : plot.top + plot.height - baseline;
555
+ const fillProps = useAnimatedProps(() => {
556
+ const runs = edgeRuns(values, plot, baseline, height, top ? topMax.value : bottomMax.value, grow.value, top ? -1 : 1);
557
+ return {
558
+ d: fillPath(runs, curve, baseline)
559
+ };
560
+ });
561
+ const lineProps = useAnimatedProps(() => {
562
+ const runs = edgeRuns(values, plot, baseline, height, top ? topMax.value : bottomMax.value, grow.value, top ? -1 : 1);
563
+ // Hidden while the band is flat, or the edge lines of every area are
564
+ // drawn along the baseline over the skeleton while the chart waits.
565
+ return {
566
+ d: edgePath(runs, curve),
567
+ opacity: grow.value > 0.001 ? 1 : 0
568
+ };
569
+ });
570
+ return /*#__PURE__*/_jsxs(G, {
571
+ children: [/*#__PURE__*/_jsx(Defs, {
572
+ children: /*#__PURE__*/_jsxs(LinearGradient, {
573
+ id: gradientId,
574
+ x1: "0",
575
+ y1: "0",
576
+ x2: "0",
577
+ y2: "1",
578
+ children: [/*#__PURE__*/_jsx(Stop, {
579
+ offset: "0",
580
+ stopColor: stroke,
581
+ stopOpacity: top ? outer : inner
582
+ }), /*#__PURE__*/_jsx(Stop, {
583
+ offset: "1",
584
+ stopColor: stroke,
585
+ stopOpacity: top ? inner : outer
586
+ })]
587
+ })
588
+ }), /*#__PURE__*/_jsx(AnimatedPath, {
589
+ animatedProps: fillProps,
590
+ fill: `url(#${gradientId})`
591
+ }), showLine ? /*#__PURE__*/_jsx(AnimatedPath, {
592
+ animatedProps: lineProps,
593
+ stroke: stroke,
594
+ strokeWidth: strokeWidth,
595
+ strokeLinecap: "round",
596
+ strokeLinejoin: "round",
597
+ fill: "none"
598
+ }) : null]
599
+ });
600
+ }
601
+ MirrorAreaChartArea.displayName = 'MirrorAreaChart.Area';
602
+ MirrorAreaChartArea.layer = 'series';
603
+ // Read by the root, for the order the areas were declared in.
604
+ MirrorAreaChartArea.area = true;
605
+ /**
606
+ * The loading state: a low band either side of the baseline with a highlight
607
+ * travelling across it. Flat, so it says where the bands will be and nothing
608
+ * about their shape.
609
+ */
610
+ function MirrorAreaChartSkeleton({
611
+ duration = 1400,
612
+ color
613
+ }) {
614
+ const {
615
+ plot,
616
+ baseline,
617
+ status
618
+ } = useChart('MirrorAreaChart.Skeleton');
619
+ const token = useCSSVariable('--color-skeleton');
620
+ const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
621
+ const highlight = useSeriesColor(undefined, 1);
622
+ const sweep = useSharedValue(0);
623
+ const reducedMotion = useReducedMotion();
624
+ const loading = status === 'loading';
625
+ useEffect(() => {
626
+ if (!loading || reducedMotion) {
627
+ cancelAnimation(sweep);
628
+ sweep.value = 0;
629
+ return;
630
+ }
631
+ sweep.value = 0;
632
+ sweep.value = withRepeat(withTiming(1, {
633
+ duration,
634
+ easing: Easing.linear
635
+ }), -1, false);
636
+ return () => cancelAnimation(sweep);
637
+ }, [loading, reducedMotion, duration, sweep]);
638
+ const animatedProps = useAnimatedProps(() => ({
639
+ x1: `${(sweep.value * 1.4 - 0.4) * 100}%`,
640
+ x2: `${(sweep.value * 1.4 - 0.4 + 0.4) * 100}%`
641
+ }));
642
+ if (!loading) return null;
643
+ const above = (baseline - plot.top) * 0.18;
644
+ const below = (plot.top + plot.height - baseline) * 0.18;
645
+ const gradientId = 'panelui-mirror-skeleton';
646
+ return /*#__PURE__*/_jsxs(G, {
647
+ children: [/*#__PURE__*/_jsx(Defs, {
648
+ children: /*#__PURE__*/_jsxs(AnimatedLinearGradient, {
649
+ id: gradientId,
650
+ animatedProps: animatedProps,
651
+ y1: "0",
652
+ y2: "0",
653
+ children: [/*#__PURE__*/_jsx(Stop, {
654
+ offset: "0",
655
+ ...colorStop(base)
656
+ }), /*#__PURE__*/_jsx(Stop, {
657
+ offset: "0.5",
658
+ stopColor: highlight,
659
+ stopOpacity: 0.3
660
+ }), /*#__PURE__*/_jsx(Stop, {
661
+ offset: "1",
662
+ ...colorStop(base)
663
+ })]
664
+ })
665
+ }), /*#__PURE__*/_jsx(Rect, {
666
+ x: plot.left,
667
+ y: baseline - above,
668
+ width: plot.width,
669
+ height: above + below,
670
+ fill: `url(#${gradientId})`
671
+ })]
672
+ });
673
+ }
674
+ MirrorAreaChartSkeleton.displayName = 'MirrorAreaChart.Skeleton';
675
+ MirrorAreaChartSkeleton.layer = 'svg';
676
+
677
+ /* -------------------------------------------------------------------------- */
678
+ /* Overlay layer */
679
+ /* -------------------------------------------------------------------------- */
680
+
681
+ /** The x labels. Real text, so they follow the theme's font and text scaling. */
682
+ function MirrorAreaChartXAxis({
683
+ ticks = 4,
684
+ format,
685
+ className
686
+ }) {
687
+ const {
688
+ data,
689
+ xDataKey,
690
+ plot,
691
+ pad
692
+ } = useChart('MirrorAreaChart.XAxis');
693
+ const labels = useMemo(() => {
694
+ if (!data.length) return [];
695
+ const count = Math.min(ticks, data.length);
696
+ const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
697
+ return Array.from({
698
+ length: count
699
+ }, (_unused, index) => {
700
+ const dataIndex = Math.round(index * step);
701
+ const datum = data[dataIndex];
702
+ if (!datum) return null;
703
+ return {
704
+ key: dataIndex,
705
+ text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? '')
706
+ };
707
+ }).filter(label => label !== null);
708
+ }, [data, ticks, format, xDataKey]);
709
+ return /*#__PURE__*/_jsx(View, {
710
+ style: {
711
+ position: 'absolute',
712
+ inset: 0,
713
+ pointerEvents: 'none'
714
+ },
715
+ className: cn(className),
716
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
717
+ size: "xs",
718
+ muted: true,
719
+ numberOfLines: 1,
720
+ style: {
721
+ position: 'absolute',
722
+ bottom: 0,
723
+ left: Math.max(0, Math.min(xOf(label.key, data.length, plot) - POINT_LABEL_WIDTH / 2, plot.left + plot.width + pad.right - POINT_LABEL_WIDTH)),
724
+ width: POINT_LABEL_WIDTH,
725
+ textAlign: 'center'
726
+ },
727
+ children: label.text
728
+ }, label.key))
729
+ });
730
+ }
731
+ MirrorAreaChartXAxis.displayName = 'MirrorAreaChart.XAxis';
732
+ MirrorAreaChartXAxis.layer = 'overlay';
733
+ /**
734
+ * Three labels down the side: the top half's maximum, zero on the baseline,
735
+ * and the bottom half's maximum. Two scales need two labels, and a column of
736
+ * evenly spaced numbers would suggest one.
737
+ */
738
+ function MirrorAreaChartYAxis({
739
+ formatTop,
740
+ formatBottom,
741
+ className
742
+ }) {
743
+ const {
744
+ plot,
745
+ baseline,
746
+ topExtent,
747
+ bottomExtent
748
+ } = useChart('MirrorAreaChart.YAxis');
749
+ const width = Math.max(plot.left - Y_AXIS_GUTTER, 0);
750
+ const fmtTop = formatTop ?? compactNumber;
751
+ const fmtBottom = formatBottom ?? compactNumber;
752
+ const labels = [{
753
+ key: 'top',
754
+ y: plot.top,
755
+ text: fmtTop(topExtent)
756
+ }, {
757
+ key: 'zero',
758
+ y: baseline,
759
+ text: '0'
760
+ }, {
761
+ key: 'bottom',
762
+ y: plot.top + plot.height,
763
+ text: fmtBottom(bottomExtent)
764
+ }];
765
+ return /*#__PURE__*/_jsx(View, {
766
+ style: {
767
+ position: 'absolute',
768
+ inset: 0,
769
+ pointerEvents: 'none'
770
+ },
771
+ className: cn(className),
772
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
773
+ size: "xs",
774
+ muted: true,
775
+ numberOfLines: 1,
776
+ style: {
777
+ position: 'absolute',
778
+ left: 0,
779
+ top: label.y - AXIS_LABEL_HEIGHT / 2,
780
+ width,
781
+ textAlign: 'right'
782
+ },
783
+ children: label.text
784
+ }, label.key))
785
+ });
786
+ }
787
+ MirrorAreaChartYAxis.displayName = 'MirrorAreaChart.YAxis';
788
+ MirrorAreaChartYAxis.layer = 'overlay';
789
+ // Read by the root, which leaves room for the labels before laying the plot out.
790
+ MirrorAreaChartYAxis.axis = 'y';
791
+ /**
792
+ * A dashed crosshair, a dot on the outer edge of the top half, and a readout
793
+ * beside them. The hit area is the whole plot.
794
+ */
795
+ function MirrorAreaChartTooltip({
796
+ color,
797
+ formatValue,
798
+ formatX,
799
+ formatSummary,
800
+ className
801
+ }) {
802
+ const {
803
+ data,
804
+ xDataKey,
805
+ plot,
806
+ baseline,
807
+ series,
808
+ topMax,
809
+ bottomMax,
810
+ grow,
811
+ activeIndex,
812
+ activeIndexJS,
813
+ setActiveIndexJS,
814
+ status
815
+ } = useChart('MirrorAreaChart.Tooltip');
816
+ const token = useCSSVariable('--color-foreground');
817
+ const stroke = color ?? (typeof token === 'string' ? token : '#888888');
818
+ const background = useCSSVariable('--color-background');
819
+ const ring = typeof background === 'string' ? background : 'white';
820
+ const total = data.length;
821
+ const left = plot.left;
822
+ const width = plot.width;
823
+
824
+ // Declared inside the memo, next to its callers: a worklet may only call
825
+ // another worklet.
826
+ const pan = useMemo(() => {
827
+ const resolve = x => {
828
+ 'worklet';
829
+
830
+ if (total < 1 || width <= 0) return;
831
+ const ratio = total < 2 ? 0 : (x - left) / width;
832
+ const next = Math.round(Math.min(1, Math.max(0, ratio)) * (total - 1));
833
+ if (next === activeIndex.value) return;
834
+ activeIndex.value = next;
835
+ runOnJS(setActiveIndexJS)(next);
836
+ };
837
+ return Gesture.Pan().minDistance(0).onBegin(event => {
838
+ 'worklet';
839
+
840
+ resolve(event.x);
841
+ }).onUpdate(event => {
842
+ 'worklet';
843
+
844
+ resolve(event.x);
845
+ }).onFinalize(() => {
846
+ 'worklet';
847
+
848
+ activeIndex.value = -1;
849
+ runOnJS(setActiveIndexJS)(-1);
850
+ });
851
+ }, [total, left, width, activeIndex, setActiveIndexJS]);
852
+
853
+ /*
854
+ * The dot sits on the outermost edge on the top half — the largest value of
855
+ * any top area at that point — or on the bottom half's when there is no top.
856
+ */
857
+ const topSeries = series.filter(item => item.side === 'top');
858
+ const dotSide = topSeries.length ? 'top' : 'bottom';
859
+ const dotValues = useMemo(() => {
860
+ const keys = series.filter(item => item.side === dotSide).map(item => item.key);
861
+ return data.map(row => {
862
+ let most = null;
863
+ for (const key of keys) {
864
+ const value = row[key];
865
+ if (typeof value === 'number' && Number.isFinite(value) && (most === null || value > most)) {
866
+ most = value;
867
+ }
868
+ }
869
+ return most;
870
+ });
871
+ }, [data, series, dotSide]);
872
+ const topHeight = baseline - plot.top;
873
+ const bottomHeight = plot.top + plot.height - baseline;
874
+ const dotProps = useAnimatedProps(() => {
875
+ const index = activeIndex.value;
876
+ const value = index >= 0 ? dotValues[index] : null;
877
+ if (index < 0 || value === null || value === undefined) return {
878
+ opacity: 0,
879
+ cx: 0,
880
+ cy: 0
881
+ };
882
+ const up = dotSide === 'top';
883
+ const max = up ? topMax.value : bottomMax.value;
884
+ const reach = Math.max(value, 0) / (max > 0 ? max : 1) * (up ? topHeight : bottomHeight) * grow.value;
885
+ return {
886
+ opacity: 1,
887
+ cx: xOf(index, total, plot),
888
+ cy: baseline + (up ? -reach : reach)
889
+ };
890
+ });
891
+ const crosshairProps = useAnimatedProps(() => {
892
+ const index = activeIndex.value;
893
+ const x = index < 0 ? 0 : xOf(index, total, plot);
894
+ return {
895
+ x1: x,
896
+ x2: x,
897
+ opacity: index < 0 ? 0 : 0.6
898
+ };
899
+ });
900
+
901
+ // Beside the crosshair, on whichever side has room, so it never covers the
902
+ // point it describes.
903
+ const readoutStyle = useAnimatedStyle(() => {
904
+ const index = activeIndex.value;
905
+ if (index < 0) return {
906
+ opacity: 0
907
+ };
908
+ const x = xOf(index, total, plot);
909
+ // Clear of the dot's radius and its ring, not only of the crosshair.
910
+ const gap = DOT / 2 + 10;
911
+ const fitsLeft = x - gap - READOUT_WIDTH >= plot.left;
912
+ const at = fitsLeft ? x - gap - READOUT_WIDTH : Math.min(x + gap, plot.left + plot.width - READOUT_WIDTH);
913
+ return {
914
+ opacity: 1,
915
+ transform: [{
916
+ translateX: at
917
+ }]
918
+ };
919
+ });
920
+ const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
921
+ const fmtValue = formatValue ?? (value => compactNumber(value));
922
+ const fmtX = formatX ?? (datum => String(datum[xDataKey] ?? ''));
923
+ if (status === 'loading') return null;
924
+ return /*#__PURE__*/_jsx(GestureDetector, {
925
+ gesture: pan,
926
+ children: /*#__PURE__*/_jsxs(View, {
927
+ style: StyleSheet.absoluteFill,
928
+ children: [/*#__PURE__*/_jsxs(Svg, {
929
+ width: "100%",
930
+ height: "100%",
931
+ style: StyleSheet.absoluteFill,
932
+ pointerEvents: "none",
933
+ children: [/*#__PURE__*/_jsx(AnimatedLine, {
934
+ animatedProps: crosshairProps,
935
+ y1: plot.top,
936
+ y2: plot.top + plot.height,
937
+ stroke: stroke,
938
+ strokeWidth: 1,
939
+ strokeDasharray: "3,4"
940
+ }), /*#__PURE__*/_jsx(AnimatedCircle, {
941
+ animatedProps: dotProps,
942
+ r: DOT / 2,
943
+ fill: stroke,
944
+ stroke: ring,
945
+ strokeWidth: 2
946
+ })]
947
+ }), /*#__PURE__*/_jsx(Animated.View, {
948
+ pointerEvents: "none",
949
+ style: [{
950
+ position: 'absolute',
951
+ left: 0,
952
+ top: plot.top,
953
+ width: READOUT_WIDTH
954
+ }, readoutStyle],
955
+ children: active ? /*#__PURE__*/_jsxs(View, {
956
+ className: cn('gap-0.5 rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
957
+ children: [/*#__PURE__*/_jsx(Text, {
958
+ size: "xs",
959
+ muted: true,
960
+ numberOfLines: 1,
961
+ children: fmtX(active)
962
+ }), formatSummary ? /*#__PURE__*/_jsx(Text, {
963
+ size: "xs",
964
+ weight: "semibold",
965
+ numberOfLines: 2,
966
+ children: formatSummary(active)
967
+ }) : series.map(item => {
968
+ const value = active[item.key];
969
+ if (typeof value !== 'number') return null;
970
+ return /*#__PURE__*/_jsxs(View, {
971
+ className: "flex-row items-center gap-1.5",
972
+ children: [/*#__PURE__*/_jsx(View, {
973
+ style: {
974
+ width: 6,
975
+ height: 6,
976
+ borderRadius: 3,
977
+ backgroundColor: item.color
978
+ }
979
+ }), /*#__PURE__*/_jsx(Text, {
980
+ size: "xs",
981
+ weight: "medium",
982
+ children: fmtValue(value, item.key)
983
+ }), /*#__PURE__*/_jsx(Text, {
984
+ size: "xs",
985
+ muted: true,
986
+ numberOfLines: 1,
987
+ className: "shrink",
988
+ children: item.label
989
+ })]
990
+ }, item.key);
991
+ })]
992
+ }) : null
993
+ })]
994
+ })
995
+ });
996
+ }
997
+ MirrorAreaChartTooltip.displayName = 'MirrorAreaChart.Tooltip';
998
+ MirrorAreaChartTooltip.layer = 'overlay';
999
+ /** A swatch and a name per area, top half first. */
1000
+ function MirrorAreaChartLegend({
1001
+ className,
1002
+ ...props
1003
+ }) {
1004
+ const {
1005
+ series
1006
+ } = useChart('MirrorAreaChart.Legend');
1007
+ if (!series.length) return null;
1008
+ return /*#__PURE__*/_jsx(View, {
1009
+ ...props,
1010
+ style: [{
1011
+ pointerEvents: 'none'
1012
+ }, props.style],
1013
+ className: cn('absolute right-2 top-0 flex-row flex-wrap gap-3', className),
1014
+ children: sidesInOrder(series).map(item => /*#__PURE__*/_jsx(SeriesSwatch, {
1015
+ color: item.color,
1016
+ label: item.label
1017
+ }, item.key))
1018
+ });
1019
+ }
1020
+ MirrorAreaChartLegend.displayName = 'MirrorAreaChart.Legend';
1021
+ MirrorAreaChartLegend.layer = 'overlay';
1022
+
1023
+ /** Top-half areas first, then the bottom half's, each in declaration order. */
1024
+ function sidesInOrder(series) {
1025
+ return [...series.filter(item => item.side === 'top'), ...series.filter(item => item.side === 'bottom')];
1026
+ }
1027
+
1028
+ /** One series' colour and name. Shared by the legend and the header. */
1029
+ function SeriesSwatch({
1030
+ color,
1031
+ label
1032
+ }) {
1033
+ return /*#__PURE__*/_jsxs(View, {
1034
+ className: "flex-row items-center gap-1.5",
1035
+ children: [/*#__PURE__*/_jsx(View, {
1036
+ style: {
1037
+ backgroundColor: color
1038
+ },
1039
+ className: "h-2 w-2 rounded-full"
1040
+ }), /*#__PURE__*/_jsx(Text, {
1041
+ size: "xs",
1042
+ muted: true,
1043
+ children: label
1044
+ })]
1045
+ });
1046
+ }
1047
+
1048
+ /* -------------------------------------------------------------------------- */
1049
+ /* Header layer */
1050
+ /* -------------------------------------------------------------------------- */
1051
+
1052
+ /**
1053
+ * The strip above the plot: what the chart is of and what it currently reads.
1054
+ * The value is not derived here — pass the formatted string, from
1055
+ * `onActiveIndexChange` if it follows the finger.
1056
+ */
1057
+ function MirrorAreaChartHeader({
1058
+ className,
1059
+ title,
1060
+ value,
1061
+ caption,
1062
+ legend = false,
1063
+ children,
1064
+ ...props
1065
+ }) {
1066
+ const {
1067
+ series
1068
+ } = useChart('MirrorAreaChart.Header');
1069
+ const trailing = children ?? (legend && series.length ? /*#__PURE__*/_jsx(View, {
1070
+ className: "flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1",
1071
+ children: sidesInOrder(series).map(item => /*#__PURE__*/_jsx(SeriesSwatch, {
1072
+ color: item.color,
1073
+ label: item.label
1074
+ }, item.key))
1075
+ }) : null);
1076
+ return /*#__PURE__*/_jsxs(View, {
1077
+ ...props,
1078
+ className: cn('flex-row items-start justify-between gap-3 pb-3', className),
1079
+ children: [/*#__PURE__*/_jsxs(View, {
1080
+ className: "flex-1 gap-0.5",
1081
+ children: [title ? /*#__PURE__*/_jsx(Text, {
1082
+ size: "xs",
1083
+ muted: true,
1084
+ children: title
1085
+ }) : null, value ? /*#__PURE__*/_jsx(Text, {
1086
+ size: "xl",
1087
+ weight: "bold",
1088
+ children: value
1089
+ }) : null, caption ? /*#__PURE__*/_jsx(Text, {
1090
+ size: "xs",
1091
+ muted: true,
1092
+ children: caption
1093
+ }) : null]
1094
+ }), trailing ? /*#__PURE__*/_jsx(View, {
1095
+ className: "shrink pt-1",
1096
+ children: trailing
1097
+ }) : null]
1098
+ });
1099
+ }
1100
+ MirrorAreaChartHeader.displayName = 'MirrorAreaChart.Header';
1101
+ MirrorAreaChartHeader.layer = 'header';
1102
+ export const MirrorAreaChart = Object.assign(MirrorAreaChartRoot, {
1103
+ Header: MirrorAreaChartHeader,
1104
+ Grid: MirrorAreaChartGrid,
1105
+ Area: MirrorAreaChartArea,
1106
+ Baseline: MirrorAreaChartBaseline,
1107
+ Skeleton: MirrorAreaChartSkeleton,
1108
+ XAxis: MirrorAreaChartXAxis,
1109
+ YAxis: MirrorAreaChartYAxis,
1110
+ Tooltip: MirrorAreaChartTooltip,
1111
+ Legend: MirrorAreaChartLegend
1112
+ });
1113
+ //# sourceMappingURL=index.js.map