panelui-native 0.22.5 → 0.25.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 (135) hide show
  1. package/README.md +8 -3
  2. package/lib/module/components/area-chart/index.js +826 -0
  3. package/lib/module/components/area-chart/index.js.map +1 -0
  4. package/lib/module/components/avatar/index.js +1 -1
  5. package/lib/module/components/avatar/index.js.map +1 -1
  6. package/lib/module/components/bar-chart/index.js +853 -0
  7. package/lib/module/components/bar-chart/index.js.map +1 -0
  8. package/lib/module/components/bottom-sheet/index.js +207 -23
  9. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  10. package/lib/module/components/breadcrumb/index.js +275 -0
  11. package/lib/module/components/breadcrumb/index.js.map +1 -0
  12. package/lib/module/components/chip/index.js +2 -2
  13. package/lib/module/components/direction/index.js +13 -19
  14. package/lib/module/components/direction/index.js.map +1 -1
  15. package/lib/module/components/frame/index.js +17 -0
  16. package/lib/module/components/frame/index.js.map +1 -1
  17. package/lib/module/components/heatmap-chart/index.js +28 -8
  18. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  19. package/lib/module/components/input-group/index.js +2 -2
  20. package/lib/module/components/input-group/index.js.map +1 -1
  21. package/lib/module/components/line-chart/index.js +4 -146
  22. package/lib/module/components/line-chart/index.js.map +1 -1
  23. package/lib/module/components/loader/index.js +758 -0
  24. package/lib/module/components/loader/index.js.map +1 -0
  25. package/lib/module/components/map/basemap.js +265 -0
  26. package/lib/module/components/map/basemap.js.map +1 -0
  27. package/lib/module/components/map/index.js +850 -0
  28. package/lib/module/components/map/index.js.map +1 -0
  29. package/lib/module/components/map/maplibre.js +87 -0
  30. package/lib/module/components/map/maplibre.js.map +1 -0
  31. package/lib/module/components/message/index.js +8 -7
  32. package/lib/module/components/message/index.js.map +1 -1
  33. package/lib/module/components/otp-input/index.js +2 -2
  34. package/lib/module/components/progress/index.js +8 -2
  35. package/lib/module/components/progress/index.js.map +1 -1
  36. package/lib/module/components/ring-chart/index.js +423 -0
  37. package/lib/module/components/ring-chart/index.js.map +1 -0
  38. package/lib/module/components/section-rail/index.js +2 -2
  39. package/lib/module/components/separator/index.js +76 -22
  40. package/lib/module/components/separator/index.js.map +1 -1
  41. package/lib/module/components/shimmer/index.js +10 -1
  42. package/lib/module/components/shimmer/index.js.map +1 -1
  43. package/lib/module/components/slider/index.js +19 -5
  44. package/lib/module/components/slider/index.js.map +1 -1
  45. package/lib/module/components/steps/index.js +1 -1
  46. package/lib/module/components/switch/index.js +6 -1
  47. package/lib/module/components/switch/index.js.map +1 -1
  48. package/lib/module/components/typography/index.js +5 -0
  49. package/lib/module/components/typography/index.js.map +1 -1
  50. package/lib/module/hooks/use-direction.js +43 -0
  51. package/lib/module/hooks/use-direction.js.map +1 -0
  52. package/lib/module/icons/index.js +117 -2
  53. package/lib/module/icons/index.js.map +1 -1
  54. package/lib/module/index.js +8 -2
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/primitives/text.js +30 -10
  57. package/lib/module/primitives/text.js.map +1 -1
  58. package/lib/module/utils/chart.js +279 -0
  59. package/lib/module/utils/chart.js.map +1 -0
  60. package/lib/typescript/src/components/area-chart/index.d.ts +197 -0
  61. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -0
  62. package/lib/typescript/src/components/bar-chart/index.d.ts +228 -0
  63. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -0
  64. package/lib/typescript/src/components/bottom-sheet/index.d.ts +75 -4
  65. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  66. package/lib/typescript/src/components/breadcrumb/index.d.ts +100 -0
  67. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -0
  68. package/lib/typescript/src/components/direction/index.d.ts +11 -10
  69. package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
  70. package/lib/typescript/src/components/frame/index.d.ts +17 -0
  71. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  72. package/lib/typescript/src/components/heatmap-chart/index.d.ts +22 -3
  73. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/line-chart/index.d.ts +1 -0
  75. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  76. package/lib/typescript/src/components/loader/index.d.ts +38 -0
  77. package/lib/typescript/src/components/loader/index.d.ts.map +1 -0
  78. package/lib/typescript/src/components/map/basemap.d.ts +76 -0
  79. package/lib/typescript/src/components/map/basemap.d.ts.map +1 -0
  80. package/lib/typescript/src/components/map/index.d.ts +367 -0
  81. package/lib/typescript/src/components/map/index.d.ts.map +1 -0
  82. package/lib/typescript/src/components/map/maplibre.d.ts +193 -0
  83. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -0
  84. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/ring-chart/index.d.ts +156 -0
  87. package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -0
  88. package/lib/typescript/src/components/separator/index.d.ts +36 -1
  89. package/lib/typescript/src/components/separator/index.d.ts.map +1 -1
  90. package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/typography/index.d.ts +6 -0
  94. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  95. package/lib/typescript/src/hooks/use-direction.d.ts +20 -0
  96. package/lib/typescript/src/hooks/use-direction.d.ts.map +1 -0
  97. package/lib/typescript/src/icons/index.d.ts +5 -0
  98. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  99. package/lib/typescript/src/index.d.ts +9 -3
  100. package/lib/typescript/src/index.d.ts.map +1 -1
  101. package/lib/typescript/src/primitives/text.d.ts.map +1 -1
  102. package/lib/typescript/src/utils/chart.d.ts +96 -0
  103. package/lib/typescript/src/utils/chart.d.ts.map +1 -0
  104. package/package.json +9 -1
  105. package/src/components/area-chart/index.tsx +994 -0
  106. package/src/components/avatar/index.tsx +1 -1
  107. package/src/components/bar-chart/index.tsx +1048 -0
  108. package/src/components/bottom-sheet/index.tsx +280 -20
  109. package/src/components/breadcrumb/index.tsx +365 -0
  110. package/src/components/chip/index.tsx +2 -2
  111. package/src/components/direction/index.tsx +20 -21
  112. package/src/components/frame/index.tsx +17 -0
  113. package/src/components/heatmap-chart/index.tsx +58 -11
  114. package/src/components/input-group/index.tsx +2 -2
  115. package/src/components/line-chart/index.tsx +13 -181
  116. package/src/components/loader/index.tsx +812 -0
  117. package/src/components/map/basemap.ts +304 -0
  118. package/src/components/map/index.tsx +1118 -0
  119. package/src/components/map/maplibre.ts +201 -0
  120. package/src/components/message/index.tsx +8 -7
  121. package/src/components/otp-input/index.tsx +2 -2
  122. package/src/components/progress/index.tsx +10 -2
  123. package/src/components/ring-chart/index.tsx +543 -0
  124. package/src/components/section-rail/index.tsx +2 -2
  125. package/src/components/separator/index.tsx +77 -19
  126. package/src/components/shimmer/index.tsx +10 -1
  127. package/src/components/slider/index.tsx +20 -5
  128. package/src/components/steps/index.tsx +1 -1
  129. package/src/components/switch/index.tsx +6 -1
  130. package/src/components/typography/index.tsx +5 -0
  131. package/src/hooks/use-direction.ts +45 -0
  132. package/src/icons/index.tsx +66 -0
  133. package/src/index.ts +84 -0
  134. package/src/primitives/text.tsx +25 -7
  135. package/src/utils/chart.ts +355 -0
@@ -0,0 +1,853 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * BarChart — categories compared by length, drawn and animated on the UI thread.
5
+ *
6
+ * Composed the same way `LineChart` is: the grid, each series, the axes and the
7
+ * readout are separate children, so a chart that wants no grid simply does not
8
+ * have one.
9
+ *
10
+ * ```tsx
11
+ * <BarChart data={revenue} xDataKey="month">
12
+ * <BarChart.Grid />
13
+ * <BarChart.Bar dataKey="revenue" />
14
+ * <BarChart.XAxis />
15
+ * <BarChart.Tooltip />
16
+ * </BarChart>
17
+ * ```
18
+ *
19
+ * ## What is different from a line
20
+ *
21
+ * **The baseline is zero, and not negotiable.** A line's job is to show change,
22
+ * so it may crop its axis to the range the data actually occupies. A bar's job
23
+ * is to compare *lengths*, and a bar cropped at the bottom is a length that
24
+ * lies — twice as tall no longer means twice as much. So the domain always
25
+ * reaches zero unless `yDomain` says otherwise, and saying otherwise is opting
26
+ * into a chart that misreads.
27
+ *
28
+ * **Bands, not points.** A line has a point at each x; a bar owns a slice of
29
+ * width around it. `barGap` is the fraction of that slice left empty, so bars
30
+ * stay proportional at any width instead of needing a pixel gap that is wrong
31
+ * on half of them.
32
+ *
33
+ * **Every series is one path.** A `Bar` draws all its rectangles as subpaths of
34
+ * a single animated path, split in two so the band under the finger keeps full
35
+ * ink while the rest fade. Fifty bars is two animated props a frame rather
36
+ * than fifty, and the corners are drawn as a path because a bar is rounded on
37
+ * the end it grows towards and square on the end it grows from — which `rx`
38
+ * cannot express, since it rounds all four corners or none.
39
+ */
40
+ import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
41
+ import { StyleSheet, View } from 'react-native';
42
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
43
+ import Animated, { Easing, runOnJS, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
44
+ import Svg, { G, Line as SvgLine, Path } from 'react-native-svg';
45
+ import { useCSSVariable } from 'uniwind';
46
+ import { Text } from "../../primitives/text.js";
47
+ import { barPath, columnValues, compactNumber, useSeriesColor } from "../../utils/chart.js";
48
+ import { cn } from "../../utils/cn.js";
49
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
50
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
51
+
52
+ /** Room left around the plot for the axis labels. */
53
+ const PADDING = {
54
+ top: 12,
55
+ right: 10,
56
+ bottom: 22,
57
+ left: 10
58
+ };
59
+
60
+ /**
61
+ * Sideways, the category names run down the left instead of along the bottom,
62
+ * so the room has to come off that side rather than off the bottom. Reserved
63
+ * rather than overlaid: a name drawn on top of the bars is unreadable against
64
+ * them and makes the bars unreadable too.
65
+ */
66
+ const PADDING_SIDEWAYS = {
67
+ top: 6,
68
+ right: 10,
69
+ bottom: 6,
70
+ left: 68
71
+ };
72
+
73
+ /** Width the readout is laid out at, so it can be clamped inside the plot. */
74
+ const LABEL_WIDTH = 132;
75
+
76
+ /** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
77
+ const Y_AXIS_WIDTH = 40;
78
+ const BarChartContext = /*#__PURE__*/createContext(null);
79
+ function useChart(component) {
80
+ const context = useContext(BarChartContext);
81
+ if (!context) {
82
+ throw new Error(`${component} must be used within a <BarChart>`);
83
+ }
84
+ return context;
85
+ }
86
+
87
+ /**
88
+ * The band under the finger, for something rendered *inside* the chart. A
89
+ * readout in the card's header is outside this provider — use
90
+ * `onActiveIndexChange` for that.
91
+ */
92
+ export function useBarChart() {
93
+ const {
94
+ data,
95
+ activeIndexJS,
96
+ xDataKey
97
+ } = useChart('useBarChart');
98
+ return {
99
+ activeIndex: activeIndexJS,
100
+ activePoint: activeIndexJS >= 0 ? data[activeIndexJS] ?? null : null,
101
+ xDataKey
102
+ };
103
+ }
104
+
105
+ /** Imperative handle: re-run the grow-in, for a "replay" control. */
106
+
107
+ function partition(children) {
108
+ const svg = [];
109
+ const overlay = [];
110
+ Children.forEach(children, (child, index) => {
111
+ if (! /*#__PURE__*/isValidElement(child)) return;
112
+ const layer = child.type.layer ?? 'svg';
113
+ const slot = /*#__PURE__*/_jsx(ChildSlot, {
114
+ children: child
115
+ }, index);
116
+ (layer === 'overlay' ? overlay : svg).push(slot);
117
+ });
118
+ return {
119
+ svg,
120
+ overlay
121
+ };
122
+ }
123
+ function ChildSlot({
124
+ children
125
+ }) {
126
+ return /*#__PURE__*/_jsx(_Fragment, {
127
+ children: children
128
+ });
129
+ }
130
+ const BarChartRoot = /*#__PURE__*/forwardRef(function BarChartRoot({
131
+ className,
132
+ data,
133
+ xDataKey = 'name',
134
+ status = 'ready',
135
+ aspectRatio = 2,
136
+ animationDuration = 1100,
137
+ domainDuration = 500,
138
+ yDomain,
139
+ orientation = 'vertical',
140
+ stacked = false,
141
+ barGap = 0.2,
142
+ barWidth,
143
+ stackGap = 0,
144
+ cornerRadius = 4,
145
+ minBarLength = 0,
146
+ fadedOpacity = 0.3,
147
+ onActiveIndexChange,
148
+ compact = false,
149
+ children,
150
+ ...props
151
+ }, ref) {
152
+ const [size, setSize] = useState({
153
+ width: 0,
154
+ height: 0
155
+ });
156
+ const [series, setSeries] = useState([]);
157
+ const [activeIndexJS, setActiveIndexJS] = useState(-1);
158
+ const reveal = useSharedValue(0);
159
+ const domainMin = useSharedValue(0);
160
+ const domainMax = useSharedValue(0);
161
+ const activeIndex = useSharedValue(-1);
162
+ const reducedMotion = useReducedMotion();
163
+ const registerSeries = useMemo(() => (key, color) => setSeries(current => {
164
+ const existing = current.find(([k]) => k === key);
165
+ if (existing?.[1] === color) return current;
166
+ return [...current.filter(([k]) => k !== key), [key, color]];
167
+ }), []);
168
+ const unregisterSeries = useMemo(() => key => setSeries(current => current.filter(([k]) => k !== key)), []);
169
+
170
+ /*
171
+ * Whether an axis is asking for room. It has to be known before the plot is
172
+ * laid out, and only the root sees the children early enough to ask — the
173
+ * axis itself renders into a box that has already been decided.
174
+ */
175
+ const hasYAxis = useMemo(() => {
176
+ let found = false;
177
+ Children.forEach(children, child => {
178
+ if (/*#__PURE__*/isValidElement(child) && child.type.axis === 'y') {
179
+ found = true;
180
+ }
181
+ });
182
+ return found;
183
+ }, [children]);
184
+ const pad = compact ? {
185
+ top: 2,
186
+ right: 1,
187
+ bottom: 2,
188
+ left: 1
189
+ } : orientation === 'horizontal' ? PADDING_SIDEWAYS : {
190
+ ...PADDING,
191
+ left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left
192
+ };
193
+ const plot = {
194
+ left: pad.left,
195
+ top: pad.top,
196
+ width: Math.max(size.width - pad.left - pad.right, 0),
197
+ height: Math.max(size.height - pad.top - pad.bottom, 0)
198
+ };
199
+ const seriesKeys = series.map(([key]) => key).join('|');
200
+ const extent = useMemo(() => {
201
+ if (yDomain) return yDomain;
202
+ const keys = seriesKeys ? seriesKeys.split('|') : [];
203
+ let min = 0;
204
+ let max = 0;
205
+ for (const row of data) {
206
+ // Stacked, the tall thing is the total; grouped, it is the tallest bar.
207
+ // Reading the same number for both would crop a stack at its largest
208
+ // segment and clip everything above it.
209
+ let positive = 0;
210
+ let negative = 0;
211
+ for (const key of keys) {
212
+ const value = row[key];
213
+ if (typeof value !== 'number' || Number.isNaN(value)) continue;
214
+ if (stacked) {
215
+ if (value >= 0) positive += value;else negative += value;
216
+ } else {
217
+ if (value > positive) positive = value;
218
+ if (value < negative) negative = value;
219
+ }
220
+ }
221
+ if (positive > max) max = positive;
222
+ if (negative < min) min = negative;
223
+ }
224
+ if (min === 0 && max === 0) return [0, 1];
225
+ // Headroom above the tallest bar only. The zero end is left exactly where
226
+ // it is: padding it would lift the bars off their own baseline.
227
+ return [min === 0 ? 0 : min * 1.1, max === 0 ? 0 : max * 1.1];
228
+ }, [data, yDomain, seriesKeys, stacked]);
229
+ const loading = status === 'loading';
230
+ useEffect(() => {
231
+ if (loading) return;
232
+ const [min, max] = extent;
233
+ const first = domainMin.value === 0 && domainMax.value === 0;
234
+ if (first || reducedMotion) {
235
+ domainMin.value = min;
236
+ domainMax.value = max;
237
+ return;
238
+ }
239
+ domainMin.value = withTiming(min, {
240
+ duration: domainDuration
241
+ });
242
+ domainMax.value = withTiming(max, {
243
+ duration: domainDuration
244
+ });
245
+ }, [extent, loading, reducedMotion, domainDuration, domainMin, domainMax]);
246
+ const revealed = useRef(false);
247
+ const playReveal = useMemo(() => () => {
248
+ if (reducedMotion) {
249
+ reveal.value = 1;
250
+ return;
251
+ }
252
+ reveal.value = 0;
253
+ reveal.value = withTiming(1, {
254
+ duration: animationDuration,
255
+ easing: Easing.bezier(0.85, 0, 0.15, 1)
256
+ });
257
+ }, [reducedMotion, animationDuration, reveal]);
258
+ useEffect(() => {
259
+ if (revealed.current || loading || plot.width <= 0 || !data.length) return;
260
+ revealed.current = true;
261
+ playReveal();
262
+ }, [loading, plot.width, data.length, playReveal]);
263
+ useImperativeHandle(ref, () => ({
264
+ replay: playReveal
265
+ }), [playReveal]);
266
+ const handleActiveIndex = useMemo(() => index => {
267
+ setActiveIndexJS(index);
268
+ onActiveIndexChange?.(index, index >= 0 ? data[index] ?? null : null);
269
+ }, [onActiveIndexChange, data]);
270
+ const onLayout = event => {
271
+ const {
272
+ width,
273
+ height
274
+ } = event.nativeEvent.layout;
275
+ setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
276
+ width,
277
+ height
278
+ });
279
+ props.onLayout?.(event);
280
+ };
281
+ const context = useMemo(() => ({
282
+ data,
283
+ xDataKey,
284
+ plot,
285
+ status,
286
+ orientation,
287
+ stacked,
288
+ barGap,
289
+ barWidth,
290
+ stackGap,
291
+ cornerRadius,
292
+ minBarLength,
293
+ fadedOpacity,
294
+ series,
295
+ registerSeries,
296
+ unregisterSeries,
297
+ domainMin,
298
+ domainMax,
299
+ extent,
300
+ reveal,
301
+ activeIndex,
302
+ activeIndexJS,
303
+ setActiveIndexJS: handleActiveIndex
304
+ }),
305
+ // `plot` is rebuilt every render from `size`, so it is compared by value.
306
+ // eslint-disable-next-line react-hooks/exhaustive-deps
307
+ [data, xDataKey, plot.width, plot.height, plot.left, plot.top, status, orientation, stacked, barGap, barWidth, stackGap, cornerRadius, minBarLength, fadedOpacity, series, registerSeries, unregisterSeries, domainMin, domainMax, extent, reveal, activeIndex, activeIndexJS, handleActiveIndex]);
308
+ const {
309
+ svg,
310
+ overlay
311
+ } = partition(children);
312
+ return /*#__PURE__*/_jsx(BarChartContext.Provider, {
313
+ value: context,
314
+ children: /*#__PURE__*/_jsx(View, {
315
+ ...props,
316
+ onLayout: onLayout,
317
+ style: [{
318
+ aspectRatio
319
+ }, props.style],
320
+ className: cn('w-full', className),
321
+ children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
322
+ children: [/*#__PURE__*/_jsx(Svg, {
323
+ width: "100%",
324
+ height: "100%",
325
+ style: StyleSheet.absoluteFill,
326
+ children: svg
327
+ }), overlay]
328
+ }) : null
329
+ })
330
+ });
331
+ });
332
+ BarChartRoot.displayName = 'BarChart';
333
+
334
+ /* -------------------------------------------------------------------------- */
335
+ /* SVG layer */
336
+ /* -------------------------------------------------------------------------- */
337
+
338
+ /**
339
+ * Lines across the value axis, so a bar can be read against a number rather
340
+ * than only against the bar beside it.
341
+ */
342
+ function BarChartGrid({
343
+ rows = 4,
344
+ color,
345
+ dashArray = '4,6',
346
+ opacity = 1
347
+ }) {
348
+ const {
349
+ plot,
350
+ orientation
351
+ } = useChart('BarChart.Grid');
352
+ const token = useCSSVariable('--color-border');
353
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
354
+ const lines = Array.from({
355
+ length: rows + 1
356
+ }, (_unused, index) => index / rows);
357
+ return /*#__PURE__*/_jsx(G, {
358
+ opacity: opacity,
359
+ children: lines.map(fraction => orientation === 'vertical' ? /*#__PURE__*/_jsx(SvgLine, {
360
+ x1: plot.left,
361
+ x2: plot.left + plot.width,
362
+ y1: plot.top + plot.height * fraction,
363
+ y2: plot.top + plot.height * fraction,
364
+ stroke: stroke,
365
+ strokeDasharray: dashArray,
366
+ strokeWidth: 1
367
+ }, fraction) : /*#__PURE__*/_jsx(SvgLine, {
368
+ x1: plot.left + plot.width * fraction,
369
+ x2: plot.left + plot.width * fraction,
370
+ y1: plot.top,
371
+ y2: plot.top + plot.height,
372
+ stroke: stroke,
373
+ strokeDasharray: dashArray,
374
+ strokeWidth: 1
375
+ }, fraction))
376
+ });
377
+ }
378
+ BarChartGrid.displayName = 'BarChart.Grid';
379
+ BarChartGrid.layer = 'svg';
380
+ /**
381
+ * One series of bars.
382
+ *
383
+ * Drawn as two paths rather than one rectangle per band: the band under the
384
+ * finger, and everything else. That is the fewest animated props that can
385
+ * still dim the rest — one path could not, since a path has one opacity, and
386
+ * a view per bar would be one animated prop per bar for the same picture.
387
+ */
388
+ function BarChartBar({
389
+ dataKey,
390
+ color,
391
+ colorIndex = 1,
392
+ cornerRadius
393
+ }) {
394
+ const {
395
+ data,
396
+ plot,
397
+ status,
398
+ orientation,
399
+ stacked,
400
+ barGap,
401
+ barWidth,
402
+ stackGap,
403
+ cornerRadius: chartRadius,
404
+ minBarLength,
405
+ fadedOpacity,
406
+ series,
407
+ registerSeries,
408
+ unregisterSeries,
409
+ domainMin,
410
+ domainMax,
411
+ reveal,
412
+ activeIndex
413
+ } = useChart('BarChart.Bar');
414
+ const fill = useSeriesColor(color, colorIndex);
415
+ const radius = cornerRadius ?? chartRadius;
416
+ useEffect(() => {
417
+ registerSeries(dataKey, fill);
418
+ return () => unregisterSeries(dataKey);
419
+ }, [dataKey, fill, registerSeries, unregisterSeries]);
420
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
421
+
422
+ /*
423
+ * What each bar in this series sits on. Grouped, that is nothing — every bar
424
+ * starts at zero. Stacked, it is the running total of the series registered
425
+ * *before* this one, which is why registration order is the stacking order.
426
+ */
427
+ const baselines = useMemo(() => {
428
+ if (!stacked) return null;
429
+ const below = series.slice(0, series.findIndex(([key]) => key === dataKey));
430
+ if (!below.length) return null;
431
+ return data.map(row => {
432
+ let total = 0;
433
+ for (const [key] of below) {
434
+ const value = row[key];
435
+ if (typeof value === 'number' && !Number.isNaN(value)) total += value;
436
+ }
437
+ return total;
438
+ });
439
+ }, [stacked, series, dataKey, data]);
440
+ const seriesIndex = Math.max(0, series.findIndex(([key]) => key === dataKey));
441
+ const seriesCount = Math.max(1, series.length);
442
+ const loading = status === 'loading';
443
+ const total = data.length;
444
+ const horizontal = orientation === 'horizontal';
445
+
446
+ /*
447
+ * Both paths come out of one builder, filtered by whether the band is the
448
+ * active one. Two passes over the data a frame is still cheaper than the
449
+ * bookkeeping needed to build both at once, and it keeps the geometry in
450
+ * exactly one place.
451
+ */
452
+ const build = wantActive => () => {
453
+ 'worklet';
454
+
455
+ if (!total || plot.width <= 0) {
456
+ return {
457
+ d: '',
458
+ opacity: 1
459
+ };
460
+ }
461
+
462
+ // The category axis runs across the plot when the bars grow up, and down
463
+ // it when they grow right. `along` is that axis; `across` is the value one.
464
+ const along = horizontal ? plot.height : plot.width;
465
+ const across = horizontal ? plot.width : plot.height;
466
+ const alongStart = horizontal ? plot.top : plot.left;
467
+ const acrossStart = horizontal ? plot.left : plot.top;
468
+ const band = along / total;
469
+ // The slot a single bar gets: the band, less the gap, divided between the
470
+ // series when they stand side by side.
471
+ const usable = band * (1 - barGap);
472
+ const slot = stacked ? usable : usable / seriesCount;
473
+ const thickness = Math.min(barWidth ?? slot, slot);
474
+ const min = domainMin.value;
475
+ const max = domainMax.value;
476
+ const range = max - min || 1;
477
+ const grow = reveal.value;
478
+ const active = activeIndex.value;
479
+
480
+ /*
481
+ * Where a value sits along the value axis. Vertical counts down from the
482
+ * top, horizontal counts up from the left — the same scale read in
483
+ * opposite directions, which is the only thing orientation changes.
484
+ */
485
+ const project = value => {
486
+ 'worklet';
487
+
488
+ const fraction = (value - min) / range;
489
+ return horizontal ? acrossStart + fraction * across : acrossStart + across - fraction * across;
490
+ };
491
+ let d = '';
492
+ for (let i = 0; i < total; i++) {
493
+ if (i === active !== wantActive) continue;
494
+ const value = values[i];
495
+ if (value === null || value === undefined) continue;
496
+ const base = baselines?.[i] ?? 0;
497
+ /*
498
+ * Staggered by band, but every bar still finishes inside the one
499
+ * duration: the window each gets is what is left after the stagger, so
500
+ * a chart of fifty bands does not take fifty times as long to arrive.
501
+ */
502
+ const start = total > 1 ? i / total * 0.45 : 0;
503
+ const eased = Math.max(0, Math.min(1, (grow - start) / 0.55));
504
+ const shown = loading ? 0 : value * eased;
505
+ const zero = project(base);
506
+ const tip = project(base + shown);
507
+ const shrink = stacked && stackGap > 0 ? stackGap : 0;
508
+ let length = Math.abs(tip - zero) - shrink;
509
+ // A value that rounds to nothing still happened, and a bar of zero
510
+ // length says it did not.
511
+ if (minBarLength > 0 && shown !== 0 && length < minBarLength) {
512
+ length = minBarLength;
513
+ }
514
+ if (length <= 0) continue;
515
+ const offset = (stacked ? 0 : seriesIndex * slot) + (band - usable) / 2 + (slot - thickness) / 2;
516
+ const lead = alongStart + i * band + offset;
517
+ const positive = shown >= 0;
518
+ d += horizontal ? barPath(positive ? zero : zero - length, lead, length, thickness, radius, positive ? 'right' : 'left') : barPath(lead, positive ? zero - length : zero, thickness, length, radius, positive ? 'up' : 'down');
519
+ }
520
+
521
+ // Dimming only happens while something *is* active; with nothing under the
522
+ // finger every bar is at full ink, which is the resting state.
523
+ const dim = !wantActive && active >= 0 ? fadedOpacity : 1;
524
+ return {
525
+ d,
526
+ opacity: dim
527
+ };
528
+ };
529
+ const restProps = useAnimatedProps(build(false));
530
+ const activeProps = useAnimatedProps(build(true));
531
+ return /*#__PURE__*/_jsxs(G, {
532
+ children: [/*#__PURE__*/_jsx(AnimatedPath, {
533
+ animatedProps: restProps,
534
+ fill: fill
535
+ }), /*#__PURE__*/_jsx(AnimatedPath, {
536
+ animatedProps: activeProps,
537
+ fill: fill
538
+ })]
539
+ });
540
+ }
541
+ BarChartBar.displayName = 'BarChart.Bar';
542
+ BarChartBar.layer = 'svg';
543
+
544
+ /* -------------------------------------------------------------------------- */
545
+ /* Overlay layer */
546
+ /* -------------------------------------------------------------------------- */
547
+
548
+ /**
549
+ * The category labels, one under each band it has room for. Real text rather
550
+ * than SVG text, so they follow the theme's font and the platform's text
551
+ * scaling — SVG text does neither.
552
+ */
553
+ function BarChartXAxis({
554
+ ticks = 6,
555
+ format,
556
+ className
557
+ }) {
558
+ const {
559
+ data,
560
+ xDataKey,
561
+ plot,
562
+ orientation
563
+ } = useChart('BarChart.XAxis');
564
+ const labels = useMemo(() => {
565
+ if (!data.length) return [];
566
+ const count = Math.min(ticks, data.length);
567
+ const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
568
+ return Array.from({
569
+ length: count
570
+ }, (_unused, index) => {
571
+ const dataIndex = Math.round(index * step);
572
+ const datum = data[dataIndex];
573
+ if (!datum) return null;
574
+ return {
575
+ key: dataIndex,
576
+ text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? '')
577
+ };
578
+ }).filter(label => label !== null);
579
+ }, [data, ticks, format, xDataKey]);
580
+ if (orientation === 'horizontal') return null;
581
+ return /*#__PURE__*/_jsx(View, {
582
+ pointerEvents: "none",
583
+ style: {
584
+ position: 'absolute',
585
+ left: plot.left,
586
+ right: PADDING.right,
587
+ bottom: 0,
588
+ height: PADDING.bottom
589
+ },
590
+ className: cn('flex-row items-center justify-between', className),
591
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
592
+ size: "xs",
593
+ muted: true,
594
+ numberOfLines: 1,
595
+ children: label.text
596
+ }, label.key))
597
+ });
598
+ }
599
+ BarChartXAxis.displayName = 'BarChart.XAxis';
600
+ BarChartXAxis.layer = 'overlay';
601
+ /** Value labels down the side, aligned to the grid lines. */
602
+ function BarChartYAxis({
603
+ ticks = 4,
604
+ format,
605
+ className
606
+ }) {
607
+ const {
608
+ plot,
609
+ data,
610
+ xDataKey,
611
+ orientation,
612
+ extent
613
+ } = useChart('BarChart.YAxis');
614
+
615
+ /*
616
+ * Read off the settled domain rather than the shared values the paths use.
617
+ * A label is text, and text is JS — following the tween would re-render on
618
+ * every frame of it to redraw a number nobody can read while it moves.
619
+ */
620
+ const horizontal = orientation === 'horizontal';
621
+ const labels = useMemo(() => {
622
+ if (horizontal) {
623
+ // Sideways, the side of the chart is the category axis.
624
+ return data.map((row, index) => ({
625
+ key: index,
626
+ text: String(row[xDataKey] ?? '')
627
+ }));
628
+ }
629
+ const [min, max] = extent;
630
+ if (min === 0 && max === 0) return [];
631
+ return Array.from({
632
+ length: ticks + 1
633
+ }, (_unused, index) => {
634
+ const value = max - (max - min) * index / ticks;
635
+ return {
636
+ key: index,
637
+ text: format ? format(value) : compactNumber(value)
638
+ };
639
+ });
640
+ }, [extent, ticks, format, horizontal, data, xDataKey]);
641
+
642
+ /*
643
+ * Sideways the labels sit in the gutter the plot already left for them, one
644
+ * band each — `flex-1` per row rather than spacing them edge to edge, so
645
+ * every name lands beside its own bar instead of only the first and last
646
+ * doing so. Upright there are no bands to line up with, so the ticks space
647
+ * themselves against the grid.
648
+ */
649
+ return /*#__PURE__*/_jsx(View, {
650
+ pointerEvents: "none",
651
+ style: {
652
+ position: 'absolute',
653
+ left: 0,
654
+ top: plot.top,
655
+ height: plot.height,
656
+ width: horizontal ? Math.max(plot.left - 8, 0) : undefined
657
+ },
658
+ className: cn(horizontal ? 'items-end' : 'justify-between', className),
659
+ children: labels.map(label => /*#__PURE__*/_jsx(View, {
660
+ className: horizontal ? 'flex-1 justify-center' : undefined,
661
+ children: /*#__PURE__*/_jsx(Text, {
662
+ size: "xs",
663
+ muted: true,
664
+ numberOfLines: 1,
665
+ children: label.text
666
+ })
667
+ }, label.key))
668
+ });
669
+ }
670
+ BarChartYAxis.displayName = 'BarChart.YAxis';
671
+ BarChartYAxis.layer = 'overlay';
672
+ // Read by the root, which has to leave room for the labels before it
673
+ // lays the plot out — an axis drawn over the plot is unreadable, and
674
+ // makes what it is drawn over unreadable too.
675
+ BarChartYAxis.axis = 'y';
676
+ /**
677
+ * The readout, and the gesture that drives it.
678
+ *
679
+ * There is no crosshair. A line needs one because a point on a line has no
680
+ * width of its own to point at; a bar is already the thing being pointed at,
681
+ * so highlighting it and dimming the rest says the same thing without drawing
682
+ * a line through the chart.
683
+ *
684
+ * The hit area is the whole plot. A readout you have to land on the bar to
685
+ * summon is a readout nobody finds — and the thinner the bars, the truer that
686
+ * gets.
687
+ */
688
+ function BarChartTooltip({
689
+ formatValue,
690
+ formatX,
691
+ className
692
+ }) {
693
+ const {
694
+ data,
695
+ xDataKey,
696
+ plot,
697
+ series,
698
+ orientation,
699
+ activeIndex,
700
+ activeIndexJS,
701
+ setActiveIndexJS,
702
+ status
703
+ } = useChart('BarChart.Tooltip');
704
+ const total = data.length;
705
+ const horizontal = orientation === 'horizontal';
706
+ const left = plot.left;
707
+ const top = plot.top;
708
+ const width = plot.width;
709
+ const height = plot.height;
710
+
711
+ /*
712
+ * Declared inside the memo, next to its callers: a worklet may only call
713
+ * another worklet, and the rule is enforced by crashing rather than warning.
714
+ */
715
+ const pan = useMemo(() => {
716
+ const resolve = (x, y) => {
717
+ 'worklet';
718
+
719
+ if (!total) return;
720
+ const span = horizontal ? height : width;
721
+ const offset = (horizontal ? y - top : x - left) / (span || 1);
722
+ // Bands, not points: the finger is inside whichever slice it lands on,
723
+ // which is a floor rather than a round to the nearest centre.
724
+ const next = Math.max(0, Math.min(total - 1, Math.floor(offset * total)));
725
+ if (next === activeIndex.value) return;
726
+ activeIndex.value = next;
727
+ runOnJS(setActiveIndexJS)(next);
728
+ };
729
+ return Gesture.Pan().minDistance(0).onBegin(event => {
730
+ 'worklet';
731
+
732
+ resolve(event.x, event.y);
733
+ }).onUpdate(event => {
734
+ 'worklet';
735
+
736
+ resolve(event.x, event.y);
737
+ }).onFinalize(() => {
738
+ 'worklet';
739
+
740
+ activeIndex.value = -1;
741
+ runOnJS(setActiveIndexJS)(-1);
742
+ });
743
+ }, [total, left, top, width, height, horizontal, activeIndex, setActiveIndexJS]);
744
+
745
+ // The readout centres over its band and is clamped inside the plot, so it
746
+ // never runs off the edge at the first or last one.
747
+ const labelStyle = useAnimatedStyle(() => {
748
+ const index = activeIndex.value;
749
+ if (index < 0 || !total) return {
750
+ opacity: 0
751
+ };
752
+ const band = (horizontal ? plot.height : plot.width) / total;
753
+ const centre = (horizontal ? plot.top : plot.left) + band * (index + 0.5);
754
+ const half = LABEL_WIDTH / 2;
755
+ const clamped = Math.min(plot.left + plot.width - half, Math.max(plot.left + half, horizontal ? plot.left + plot.width / 2 : centre));
756
+ return {
757
+ opacity: 1,
758
+ transform: [{
759
+ translateX: clamped - half
760
+ }]
761
+ };
762
+ });
763
+ const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
764
+ const fmtValue = formatValue ?? (value => compactNumber(value));
765
+ const fmtX = formatX ?? (datum => String(datum[xDataKey] ?? ''));
766
+ if (status === 'loading') return null;
767
+ return /*#__PURE__*/_jsx(GestureDetector, {
768
+ gesture: pan,
769
+ children: /*#__PURE__*/_jsx(View, {
770
+ style: StyleSheet.absoluteFill,
771
+ children: /*#__PURE__*/_jsx(Animated.View, {
772
+ pointerEvents: "none",
773
+ style: [{
774
+ position: 'absolute',
775
+ left: 0,
776
+ top: 0,
777
+ width: LABEL_WIDTH
778
+ }, labelStyle],
779
+ children: active ? /*#__PURE__*/_jsxs(View, {
780
+ className: cn('rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
781
+ children: [/*#__PURE__*/_jsx(Text, {
782
+ size: "xs",
783
+ muted: true,
784
+ numberOfLines: 1,
785
+ children: fmtX(active)
786
+ }), series.map(([key, color]) => {
787
+ const value = active[key];
788
+ if (typeof value !== 'number') return null;
789
+ return /*#__PURE__*/_jsxs(View, {
790
+ className: "flex-row items-center gap-1.5",
791
+ children: [/*#__PURE__*/_jsx(View, {
792
+ style: {
793
+ width: 6,
794
+ height: 6,
795
+ borderRadius: 3,
796
+ backgroundColor: color
797
+ }
798
+ }), /*#__PURE__*/_jsx(Text, {
799
+ size: "xs",
800
+ weight: "medium",
801
+ children: fmtValue(value, key)
802
+ })]
803
+ }, key);
804
+ })]
805
+ }) : null
806
+ })
807
+ })
808
+ });
809
+ }
810
+ BarChartTooltip.displayName = 'BarChart.Tooltip';
811
+ BarChartTooltip.layer = 'overlay';
812
+ /** A swatch and a name per series, in the order the series were declared. */
813
+ function BarChartLegend({
814
+ className,
815
+ labels,
816
+ ...props
817
+ }) {
818
+ const {
819
+ series
820
+ } = useChart('BarChart.Legend');
821
+ if (!series.length) return null;
822
+ return /*#__PURE__*/_jsx(View, {
823
+ ...props,
824
+ pointerEvents: "none",
825
+ className: cn('absolute right-2 top-1 flex-row gap-3', className),
826
+ children: series.map(([key, color]) => /*#__PURE__*/_jsxs(View, {
827
+ className: "flex-row items-center gap-1.5",
828
+ children: [/*#__PURE__*/_jsx(View, {
829
+ style: {
830
+ width: 8,
831
+ height: 8,
832
+ borderRadius: 2,
833
+ backgroundColor: color
834
+ }
835
+ }), /*#__PURE__*/_jsx(Text, {
836
+ size: "xs",
837
+ muted: true,
838
+ children: labels?.[key] ?? key
839
+ })]
840
+ }, key))
841
+ });
842
+ }
843
+ BarChartLegend.displayName = 'BarChart.Legend';
844
+ BarChartLegend.layer = 'overlay';
845
+ export const BarChart = Object.assign(BarChartRoot, {
846
+ Grid: BarChartGrid,
847
+ Bar: BarChartBar,
848
+ XAxis: BarChartXAxis,
849
+ YAxis: BarChartYAxis,
850
+ Tooltip: BarChartTooltip,
851
+ Legend: BarChartLegend
852
+ });
853
+ //# sourceMappingURL=index.js.map