panelui-native 0.63.0 → 0.65.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 (158) hide show
  1. package/README.md +5 -0
  2. package/lib/module/components/badge/index.js +1 -1
  3. package/lib/module/components/badge/index.js.map +1 -1
  4. package/lib/module/components/bar-chart/index.js +39 -2
  5. package/lib/module/components/bar-chart/index.js.map +1 -1
  6. package/lib/module/components/bottom-sheet/index.js +2 -2
  7. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +19 -9
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/carousel/index.js +28 -14
  11. package/lib/module/components/carousel/index.js.map +1 -1
  12. package/lib/module/components/checkbox/index.js +2 -3
  13. package/lib/module/components/checkbox/index.js.map +1 -1
  14. package/lib/module/components/dialog/index.js +2 -2
  15. package/lib/module/components/dialog/index.js.map +1 -1
  16. package/lib/module/components/drawer/index.js +2 -2
  17. package/lib/module/components/drawer/index.js.map +1 -1
  18. package/lib/module/components/fab/index.js +4 -3
  19. package/lib/module/components/fab/index.js.map +1 -1
  20. package/lib/module/components/field/index.js +41 -6
  21. package/lib/module/components/field/index.js.map +1 -1
  22. package/lib/module/components/flow/flow-accessibility.js +84 -0
  23. package/lib/module/components/flow/flow-accessibility.js.map +1 -0
  24. package/lib/module/components/flow/index.js +100 -39
  25. package/lib/module/components/flow/index.js.map +1 -1
  26. package/lib/module/components/form/use-form.js +93 -66
  27. package/lib/module/components/form/use-form.js.map +1 -1
  28. package/lib/module/components/heatmap-chart/index.js +2 -2
  29. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  30. package/lib/module/components/hex-chart/index.js +2 -2
  31. package/lib/module/components/hex-chart/index.js.map +1 -1
  32. package/lib/module/components/kpi/index.js +3 -1
  33. package/lib/module/components/kpi/index.js.map +1 -1
  34. package/lib/module/components/line-chart/index.js +3 -3
  35. package/lib/module/components/line-chart/index.js.map +1 -1
  36. package/lib/module/components/map/index.js +71 -8
  37. package/lib/module/components/map/index.js.map +1 -1
  38. package/lib/module/components/map/map-accessibility.js +20 -0
  39. package/lib/module/components/map/map-accessibility.js.map +1 -0
  40. package/lib/module/components/map/maplibre.js.map +1 -1
  41. package/lib/module/components/menu/index.js +4 -1
  42. package/lib/module/components/menu/index.js.map +1 -1
  43. package/lib/module/components/number-input/index.js +1 -23
  44. package/lib/module/components/number-input/index.js.map +1 -1
  45. package/lib/module/components/number-input/number-input-math.js +44 -0
  46. package/lib/module/components/number-input/number-input-math.js.map +1 -0
  47. package/lib/module/components/plot/index.js +2 -2
  48. package/lib/module/components/plot/index.js.map +1 -1
  49. package/lib/module/components/radio-group/index.js +1 -2
  50. package/lib/module/components/radio-group/index.js.map +1 -1
  51. package/lib/module/components/sortable/index.js +5 -55
  52. package/lib/module/components/sortable/index.js.map +1 -1
  53. package/lib/module/components/sortable/reorder.js +63 -0
  54. package/lib/module/components/sortable/reorder.js.map +1 -0
  55. package/lib/module/components/swipe/index.js +4 -4
  56. package/lib/module/components/swipe/index.js.map +1 -1
  57. package/lib/module/components/switch/index.js +36 -3
  58. package/lib/module/components/switch/index.js.map +1 -1
  59. package/lib/module/components/time-picker/accessibility.js +21 -0
  60. package/lib/module/components/time-picker/accessibility.js.map +1 -0
  61. package/lib/module/components/time-picker/index.js +34 -8
  62. package/lib/module/components/time-picker/index.js.map +1 -1
  63. package/lib/module/components/waterfall-chart/index.js +1325 -0
  64. package/lib/module/components/waterfall-chart/index.js.map +1 -0
  65. package/lib/module/index.js +1 -0
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/primitives/modal-isolation-store.js +29 -0
  68. package/lib/module/primitives/modal-isolation-store.js.map +1 -0
  69. package/lib/module/primitives/portal.js +36 -4
  70. package/lib/module/primitives/portal.js.map +1 -1
  71. package/lib/module/providers/panel-ui-provider.js +27 -6
  72. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  73. package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  75. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  76. package/lib/typescript/src/components/carousel/index.d.ts +1 -1
  77. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
  78. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  79. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  80. package/lib/typescript/src/components/field/index.d.ts +8 -1
  81. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/flow/flow-accessibility.d.ts +49 -0
  83. package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
  84. package/lib/typescript/src/components/flow/index.d.ts +14 -2
  85. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  87. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  88. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
  90. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/map/index.d.ts +13 -5
  92. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
  94. package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
  95. package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
  96. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
  97. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
  99. package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
  100. package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
  101. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  104. package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
  105. package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
  106. package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/switch/index.d.ts +3 -3
  108. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  109. package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
  110. package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
  111. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  112. package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
  113. package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
  114. package/lib/typescript/src/index.d.ts +2 -1
  115. package/lib/typescript/src/index.d.ts.map +1 -1
  116. package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
  117. package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
  118. package/lib/typescript/src/primitives/portal.d.ts +6 -0
  119. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  120. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  121. package/package.json +5 -2
  122. package/src/components/badge/index.tsx +4 -1
  123. package/src/components/bar-chart/index.tsx +40 -3
  124. package/src/components/bottom-sheet/index.tsx +3 -3
  125. package/src/components/button/index.tsx +15 -21
  126. package/src/components/carousel/index.tsx +42 -20
  127. package/src/components/checkbox/index.tsx +1 -2
  128. package/src/components/dialog/index.tsx +3 -3
  129. package/src/components/drawer/index.tsx +3 -3
  130. package/src/components/fab/index.tsx +8 -12
  131. package/src/components/field/index.tsx +52 -10
  132. package/src/components/flow/flow-accessibility.ts +109 -0
  133. package/src/components/flow/index.tsx +162 -50
  134. package/src/components/form/use-form.ts +101 -65
  135. package/src/components/heatmap-chart/index.tsx +2 -3
  136. package/src/components/hex-chart/index.tsx +2 -3
  137. package/src/components/kpi/index.tsx +3 -1
  138. package/src/components/line-chart/index.tsx +3 -2
  139. package/src/components/map/index.tsx +110 -7
  140. package/src/components/map/map-accessibility.ts +41 -0
  141. package/src/components/map/maplibre.ts +2 -0
  142. package/src/components/menu/index.tsx +4 -1
  143. package/src/components/number-input/index.tsx +1 -26
  144. package/src/components/number-input/number-input-math.ts +50 -0
  145. package/src/components/plot/index.tsx +2 -1
  146. package/src/components/radio-group/index.tsx +1 -2
  147. package/src/components/sortable/index.tsx +12 -64
  148. package/src/components/sortable/reorder.ts +88 -0
  149. package/src/components/swipe/index.tsx +7 -4
  150. package/src/components/switch/index.tsx +42 -6
  151. package/src/components/time-picker/accessibility.ts +24 -0
  152. package/src/components/time-picker/index.tsx +48 -2
  153. package/src/components/waterfall-chart/index.tsx +1589 -0
  154. package/src/index.ts +23 -0
  155. package/src/primitives/modal-isolation-store.ts +31 -0
  156. package/src/primitives/portal.tsx +34 -4
  157. package/src/providers/panel-ui-provider.tsx +41 -9
  158. package/theme.css +30 -3
@@ -0,0 +1,1325 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * WaterfallChart — how a run of changes carried one total to another.
5
+ *
6
+ * Composed the same way the other charts are: the grid, the bars, the
7
+ * connectors, the axes and the readout are separate children, so a chart that
8
+ * wants no grid simply does not have one.
9
+ *
10
+ * ```tsx
11
+ * <WaterfallChart data={bridge}>
12
+ * <WaterfallChart.Grid />
13
+ * <WaterfallChart.Connectors />
14
+ * <WaterfallChart.Bars />
15
+ * <WaterfallChart.XAxis />
16
+ * <WaterfallChart.Tooltip />
17
+ * </WaterfallChart>
18
+ * ```
19
+ *
20
+ * ## What the shape is asserting
21
+ *
22
+ * **Every bar but a total floats.** A step's bar starts where the previous one
23
+ * ended and reaches as far as its own value carries it, so the gap under it is
24
+ * the running total it is acting on. That floating is the entire point: a bar
25
+ * chart of the same numbers would compare the changes against each other, and
26
+ * this compares each of them against the balance it moved.
27
+ *
28
+ * **A total is anchored to zero.** It is a reading rather than a change, so it
29
+ * is measured from the baseline like an ordinary bar and drawn in a neutral
30
+ * colour. Marking the opening and closing steps `total` is what gives the run
31
+ * two ends to be a bridge between.
32
+ *
33
+ * **Three colours, and no more.** Up, down, and total. A fourth would have to
34
+ * mean something the reader has to be told, and the one thing this chart has
35
+ * going for it is that the direction of a bar is legible before its label is.
36
+ *
37
+ * **The connectors are the sequence.** Without them the bars are a row of
38
+ * floating rectangles at unexplained heights; the line from one bar's end to
39
+ * the next bar's start is what says the second continues the first.
40
+ */
41
+ import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
42
+ import { StyleSheet, View } from 'react-native';
43
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
44
+ import Animated, { Easing, cancelAnimation, runOnJS, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
45
+ import Svg, { Defs, G, Line as SvgLine, LinearGradient, Path, Stop } from 'react-native-svg';
46
+ import { useCSSVariable } from 'uniwind';
47
+ import { Text } from "../../primitives/text.js";
48
+ import { barPath, compactNumber } from "../../utils/chart.js";
49
+ import { cn } from "../../utils/cn.js";
50
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
51
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
52
+ const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
53
+
54
+ /** Room left around the plot for the axis labels. */
55
+ const PADDING = {
56
+ top: 12,
57
+ right: 10,
58
+ bottom: 22,
59
+ left: 10
60
+ };
61
+
62
+ /**
63
+ * Sideways, the step names run down the left instead of along the bottom, so
64
+ * the room has to come off that side. Reserved rather than overlaid: a name
65
+ * drawn on top of the bars is unreadable against them and makes the bars
66
+ * unreadable too.
67
+ */
68
+ const PADDING_SIDEWAYS = {
69
+ top: 6,
70
+ right: 10,
71
+ bottom: 6,
72
+ left: 76
73
+ };
74
+
75
+ /** Width the readout is laid out at, so it can be clamped inside the plot. */
76
+ const LABEL_WIDTH = 148;
77
+
78
+ /** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
79
+ const Y_AXIS_WIDTH = 44;
80
+
81
+ /** Line height of an `xs` label, for centring one on the grid line it names. */
82
+ const AXIS_LABEL_HEIGHT = 16;
83
+
84
+ /** Fallbacks, only reached if the theme CSS was never imported. */
85
+ const FALLBACK_RISE = '#10b981';
86
+ const FALLBACK_FALL = '#ef4444';
87
+ const FALLBACK_TOTAL = '#262626';
88
+
89
+ /** Which of the three roles a step's bar is drawn in. */
90
+
91
+ /** One step, resolved against the running total it acts on. */
92
+
93
+ const WaterfallChartContext = /*#__PURE__*/createContext(null);
94
+ function useChart(component) {
95
+ const context = useContext(WaterfallChartContext);
96
+ if (!context) {
97
+ throw new Error(`${component} must be used within a <WaterfallChart>`);
98
+ }
99
+ return context;
100
+ }
101
+
102
+ /**
103
+ * The step under the finger, for something rendered *inside* the chart. A
104
+ * readout in the card's header is outside this provider — use
105
+ * `onActiveIndexChange` for that.
106
+ */
107
+ export function useWaterfallChart() {
108
+ const {
109
+ steps,
110
+ activeIndexJS
111
+ } = useChart('useWaterfallChart');
112
+ return {
113
+ activeIndex: activeIndexJS,
114
+ activeStep: activeIndexJS >= 0 ? steps[activeIndexJS] ?? null : null
115
+ };
116
+ }
117
+
118
+ /**
119
+ * The running totals every step sits on.
120
+ *
121
+ * Split out because it is the one piece of the chart that is pure arithmetic
122
+ * over the data, and every part that draws anything needs the same answer —
123
+ * two parts deriving it separately is two chances for the bars and the
124
+ * connectors to disagree about where a step ended.
125
+ */
126
+ export function waterfallSteps(data) {
127
+ const steps = [];
128
+ let running = 0;
129
+ for (const datum of data) {
130
+ const value = typeof datum.value === 'number' && !Number.isNaN(datum.value) ? datum.value : 0;
131
+ if (datum.total) {
132
+ // A total is a reading, so it is measured from the baseline and the
133
+ // running figure becomes whatever it reads.
134
+ running += value;
135
+ steps.push({
136
+ datum,
137
+ label: datum.label,
138
+ value: running,
139
+ kind: 'total',
140
+ start: 0,
141
+ end: running
142
+ });
143
+ continue;
144
+ }
145
+ const start = running;
146
+ running += value;
147
+ steps.push({
148
+ datum,
149
+ label: datum.label,
150
+ value,
151
+ kind: value < 0 ? 'fall' : 'rise',
152
+ start,
153
+ end: running
154
+ });
155
+ }
156
+ return steps;
157
+ }
158
+
159
+ /** Imperative handle: re-run the grow-in, for a "replay" control. */
160
+
161
+ function partition(children) {
162
+ const svg = [];
163
+ const overlay = [];
164
+ const header = [];
165
+ Children.forEach(children, (child, index) => {
166
+ if (! /*#__PURE__*/isValidElement(child)) return;
167
+ const layer = child.type.layer ?? 'svg';
168
+ const slot = /*#__PURE__*/_jsx(ChildSlot, {
169
+ children: child
170
+ }, index);
171
+ (layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(slot);
172
+ });
173
+ return {
174
+ svg,
175
+ overlay,
176
+ header
177
+ };
178
+ }
179
+ function ChildSlot({
180
+ children
181
+ }) {
182
+ return /*#__PURE__*/_jsx(_Fragment, {
183
+ children: children
184
+ });
185
+ }
186
+ const WaterfallChartRoot = /*#__PURE__*/forwardRef(function WaterfallChartRoot({
187
+ className,
188
+ data,
189
+ status = 'ready',
190
+ aspectRatio = 2,
191
+ animationDuration = 700,
192
+ domainDuration = 500,
193
+ yDomain,
194
+ orientation = 'vertical',
195
+ barGap = 0.34,
196
+ barWidth,
197
+ cornerRadius = 4,
198
+ minBarLength = 2,
199
+ fadedOpacity = 0.3,
200
+ riseColor,
201
+ fallColor,
202
+ totalColor,
203
+ onActiveIndexChange,
204
+ compact = false,
205
+ children,
206
+ ...props
207
+ }, ref) {
208
+ const [size, setSize] = useState({
209
+ width: 0,
210
+ height: 0
211
+ });
212
+ const [activeIndexJS, setActiveIndexJS] = useState(-1);
213
+ const reveal = useSharedValue(0);
214
+ const domainMin = useSharedValue(0);
215
+ const domainMax = useSharedValue(0);
216
+ const activeIndex = useSharedValue(-1);
217
+ const reducedMotion = useReducedMotion();
218
+ const riseToken = useCSSVariable('--color-success');
219
+ const fallToken = useCSSVariable('--color-destructive');
220
+ const totalToken = useCSSVariable('--color-chart-1');
221
+ const colors = useMemo(() => ({
222
+ rise: riseColor ?? (typeof riseToken === 'string' ? riseToken : FALLBACK_RISE),
223
+ fall: fallColor ?? (typeof fallToken === 'string' ? fallToken : FALLBACK_FALL),
224
+ total: totalColor ?? (typeof totalToken === 'string' ? totalToken : FALLBACK_TOTAL)
225
+ }), [riseColor, fallColor, totalColor, riseToken, fallToken, totalToken]);
226
+ const steps = useMemo(() => waterfallSteps(data), [data]);
227
+
228
+ /*
229
+ * Whether an axis is asking for room. It has to be known before the plot is
230
+ * laid out, and only the root sees the children early enough to ask — the
231
+ * axis itself renders into a box that has already been decided.
232
+ */
233
+ const hasYAxis = useMemo(() => {
234
+ let found = false;
235
+ Children.forEach(children, child => {
236
+ if (/*#__PURE__*/isValidElement(child) && child.type.axis === 'y') {
237
+ found = true;
238
+ }
239
+ });
240
+ return found;
241
+ }, [children]);
242
+ const pad = compact ? {
243
+ top: 2,
244
+ right: 1,
245
+ bottom: 2,
246
+ left: 1
247
+ } : orientation === 'horizontal' ? PADDING_SIDEWAYS : {
248
+ ...PADDING,
249
+ left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left
250
+ };
251
+ const plot = {
252
+ left: pad.left,
253
+ top: pad.top,
254
+ width: Math.max(size.width - pad.left - pad.right, 0),
255
+ height: Math.max(size.height - pad.top - pad.bottom, 0)
256
+ };
257
+ const extent = useMemo(() => {
258
+ if (yDomain) return yDomain;
259
+
260
+ /*
261
+ * Both ends of every bar, not just the values. A step's bar occupies the
262
+ * span between two running totals, and a domain taken from the changes
263
+ * alone would be the range of the *deltas* — which on any run that
264
+ * climbs before it falls is a fraction of the height the bars need.
265
+ */
266
+ let min = 0;
267
+ let max = 0;
268
+ for (const step of steps) {
269
+ const low = Math.min(step.start, step.end);
270
+ const high = Math.max(step.start, step.end);
271
+ if (low < min) min = low;
272
+ if (high > max) max = high;
273
+ }
274
+ if (min === 0 && max === 0) return [0, 1];
275
+ // Headroom past the furthest bar only. The zero end is left exactly where
276
+ // it is: padding it would lift the run off its own baseline.
277
+ return [min === 0 ? 0 : min * 1.1, max === 0 ? 0 : max * 1.1];
278
+ }, [steps, yDomain]);
279
+ const loading = status === 'loading';
280
+ useEffect(() => {
281
+ if (loading) return;
282
+ const [min, max] = extent;
283
+ const first = domainMin.value === 0 && domainMax.value === 0;
284
+ if (first || reducedMotion) {
285
+ domainMin.value = min;
286
+ domainMax.value = max;
287
+ return;
288
+ }
289
+ domainMin.value = withTiming(min, {
290
+ duration: domainDuration
291
+ });
292
+ domainMax.value = withTiming(max, {
293
+ duration: domainDuration
294
+ });
295
+ }, [extent, loading, reducedMotion, domainDuration, domainMin, domainMax]);
296
+ const revealed = useRef(false);
297
+ const playReveal = useMemo(() => () => {
298
+ if (reducedMotion) {
299
+ reveal.value = 1;
300
+ return;
301
+ }
302
+ reveal.value = 0;
303
+ reveal.value = withTiming(1, {
304
+ duration: animationDuration,
305
+ easing: Easing.out(Easing.cubic)
306
+ });
307
+ }, [reducedMotion, animationDuration, reveal]);
308
+ useEffect(() => {
309
+ /*
310
+ * Going back to `loading` arms the reveal again. Without this a chart that
311
+ * is refetched comes back fully drawn on the frame the data lands, which
312
+ * reads as the loading state having been for nothing.
313
+ */
314
+ if (loading) {
315
+ revealed.current = false;
316
+ reveal.value = 0;
317
+ return;
318
+ }
319
+ if (revealed.current || plot.width <= 0 || !steps.length) return;
320
+ revealed.current = true;
321
+ playReveal();
322
+ }, [loading, plot.width, steps.length, playReveal, reveal]);
323
+ useImperativeHandle(ref, () => ({
324
+ replay: playReveal
325
+ }), [playReveal]);
326
+ const handleActiveIndex = useMemo(() => index => {
327
+ setActiveIndexJS(index);
328
+ onActiveIndexChange?.(index, index >= 0 ? steps[index] ?? null : null);
329
+ }, [onActiveIndexChange, steps]);
330
+ const onLayout = event => {
331
+ const {
332
+ width,
333
+ height
334
+ } = event.nativeEvent.layout;
335
+ setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
336
+ width,
337
+ height
338
+ });
339
+ props.onLayout?.(event);
340
+ };
341
+ const context = useMemo(() => ({
342
+ data,
343
+ steps,
344
+ plot,
345
+ status,
346
+ orientation,
347
+ barGap,
348
+ barWidth,
349
+ cornerRadius,
350
+ minBarLength,
351
+ fadedOpacity,
352
+ colors,
353
+ domainMin,
354
+ domainMax,
355
+ extent,
356
+ reveal,
357
+ activeIndex,
358
+ activeIndexJS,
359
+ setActiveIndexJS: handleActiveIndex
360
+ }),
361
+ // `plot` is rebuilt every render from `size`, so it is compared by value.
362
+ // eslint-disable-next-line react-hooks/exhaustive-deps
363
+ [data, steps, plot.width, plot.height, plot.left, plot.top, status, orientation, barGap, barWidth, cornerRadius, minBarLength, fadedOpacity, colors, domainMin, domainMax, extent, reveal, activeIndex, activeIndexJS, handleActiveIndex]);
364
+ const {
365
+ svg,
366
+ overlay,
367
+ header
368
+ } = partition(children);
369
+
370
+ /*
371
+ * Two views, because the header is not part of the plot. `aspectRatio` and
372
+ * the layout measurement belong to the drawing area alone — measured on the
373
+ * outer view they would take in the header too, and the plot would lose as
374
+ * much height as the readout took while still claiming the shape asked for.
375
+ */
376
+ return /*#__PURE__*/_jsx(WaterfallChartContext.Provider, {
377
+ value: context,
378
+ children: /*#__PURE__*/_jsxs(View, {
379
+ ...props,
380
+ style: props.style,
381
+ className: cn('w-full', className),
382
+ children: [header, /*#__PURE__*/_jsx(View, {
383
+ onLayout: onLayout,
384
+ style: {
385
+ aspectRatio
386
+ },
387
+ className: "w-full",
388
+ children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
389
+ children: [/*#__PURE__*/_jsx(Svg, {
390
+ width: "100%",
391
+ height: "100%",
392
+ style: StyleSheet.absoluteFill,
393
+ children: svg
394
+ }), overlay]
395
+ }) : null
396
+ })]
397
+ })
398
+ });
399
+ });
400
+ WaterfallChartRoot.displayName = 'WaterfallChart';
401
+
402
+ /* -------------------------------------------------------------------------- */
403
+ /* Geometry */
404
+ /* -------------------------------------------------------------------------- */
405
+
406
+ /** The band measurements every drawn part reads the same way. */
407
+
408
+ /**
409
+ * How the plot divides between the steps.
410
+ *
411
+ * A worklet because the bars rebuild on the UI thread every frame the domain is
412
+ * tweening, and shared with the parts that only run on JS so a label and the
413
+ * bar it names can never land on different centres.
414
+ */
415
+ function bandsOf(plot, total, horizontal, barGap, barWidth) {
416
+ 'worklet';
417
+
418
+ const along = horizontal ? plot.height : plot.width;
419
+ const across = horizontal ? plot.width : plot.height;
420
+ const band = along / Math.max(1, total);
421
+ const usable = band * (1 - barGap);
422
+ return {
423
+ along,
424
+ across,
425
+ alongStart: horizontal ? plot.top : plot.left,
426
+ acrossStart: horizontal ? plot.left : plot.top,
427
+ band,
428
+ thickness: Math.min(barWidth ?? usable, usable)
429
+ };
430
+ }
431
+
432
+ /* -------------------------------------------------------------------------- */
433
+ /* SVG layer */
434
+ /* -------------------------------------------------------------------------- */
435
+
436
+ /**
437
+ * Lines across the value axis, so a bar can be read against a number rather
438
+ * than only against the bar beside it.
439
+ */
440
+ function WaterfallChartGrid({
441
+ rows = 4,
442
+ color,
443
+ dashArray = '4,6',
444
+ opacity = 1
445
+ }) {
446
+ const {
447
+ plot,
448
+ orientation
449
+ } = useChart('WaterfallChart.Grid');
450
+ const token = useCSSVariable('--color-border');
451
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
452
+ const lines = Array.from({
453
+ length: rows + 1
454
+ }, (_unused, index) => index / rows);
455
+ return /*#__PURE__*/_jsx(G, {
456
+ opacity: opacity,
457
+ children: lines.map(fraction => orientation === 'vertical' ? /*#__PURE__*/_jsx(SvgLine, {
458
+ x1: plot.left,
459
+ x2: plot.left + plot.width,
460
+ y1: plot.top + plot.height * fraction,
461
+ y2: plot.top + plot.height * fraction,
462
+ stroke: stroke,
463
+ strokeDasharray: dashArray,
464
+ strokeWidth: 1
465
+ }, fraction) : /*#__PURE__*/_jsx(SvgLine, {
466
+ x1: plot.left + plot.width * fraction,
467
+ x2: plot.left + plot.width * fraction,
468
+ y1: plot.top,
469
+ y2: plot.top + plot.height,
470
+ stroke: stroke,
471
+ strokeDasharray: dashArray,
472
+ strokeWidth: 1
473
+ }, fraction))
474
+ });
475
+ }
476
+ WaterfallChartGrid.displayName = 'WaterfallChart.Grid';
477
+ WaterfallChartGrid.layer = 'svg';
478
+ /**
479
+ * The bars.
480
+ *
481
+ * Six animated paths a frame rather than one per step: one per role, so the
482
+ * three colours can be three fills, and each of those split into the bar under
483
+ * the finger and the rest, so the others can dim without every bar carrying its
484
+ * own opacity. A run of forty steps costs the same as a run of four.
485
+ *
486
+ * Each bar grows from its own `start` towards its `end` rather than up from the
487
+ * baseline. A step is a movement from one balance to another, and growing it
488
+ * from zero would animate a quantity the chart is not claiming.
489
+ */
490
+ function WaterfallChartBars({
491
+ cornerRadius
492
+ }) {
493
+ const {
494
+ steps,
495
+ plot,
496
+ status,
497
+ orientation,
498
+ barGap,
499
+ barWidth,
500
+ cornerRadius: chartRadius,
501
+ minBarLength,
502
+ fadedOpacity,
503
+ colors,
504
+ domainMin,
505
+ domainMax,
506
+ reveal,
507
+ activeIndex
508
+ } = useChart('WaterfallChart.Bars');
509
+ const radius = cornerRadius ?? chartRadius;
510
+ const loading = status === 'loading';
511
+ const total = steps.length;
512
+ const horizontal = orientation === 'horizontal';
513
+
514
+ /*
515
+ * All six paths come out of one builder, filtered by role and by whether the
516
+ * bar is the active one. Six passes over the data a frame is still cheaper
517
+ * than the bookkeeping needed to build them all at once, and it keeps the
518
+ * geometry in exactly one place.
519
+ */
520
+ const build = (kind, wantActive) => () => {
521
+ 'worklet';
522
+
523
+ if (!total || plot.width <= 0) {
524
+ return {
525
+ d: '',
526
+ opacity: 1
527
+ };
528
+ }
529
+ const bands = bandsOf(plot, total, horizontal, barGap, barWidth);
530
+ const min = domainMin.value;
531
+ const max = domainMax.value;
532
+ const range = max - min || 1;
533
+ const grow = reveal.value;
534
+ const active = activeIndex.value;
535
+
536
+ /*
537
+ * Where a value sits along the value axis. Vertical counts down from the
538
+ * top, horizontal counts up from the left — the same scale read in
539
+ * opposite directions, which is the only thing orientation changes.
540
+ */
541
+ const project = value => {
542
+ 'worklet';
543
+
544
+ const fraction = (value - min) / range;
545
+ return horizontal ? bands.acrossStart + fraction * bands.across : bands.acrossStart + bands.across - fraction * bands.across;
546
+ };
547
+ let d = '';
548
+ for (let i = 0; i < total; i++) {
549
+ const step = steps[i];
550
+ if (!step || step.kind !== kind) continue;
551
+ if (i === active !== wantActive) continue;
552
+
553
+ /*
554
+ * Staggered by step, but every bar still finishes inside the one
555
+ * duration: the window each gets is what is left after the stagger, so a
556
+ * run of forty steps does not take forty times as long to arrive.
557
+ */
558
+ const startAt = total > 1 ? i / total * 0.45 : 0;
559
+ const eased = Math.max(0, Math.min(1, (grow - startAt) / 0.55));
560
+ const reached = loading ? step.start : step.start + (step.end - step.start) * eased;
561
+ const from = project(step.start);
562
+ const to = project(reached);
563
+ let length = Math.abs(to - from);
564
+ // A step that rounds to nothing still happened, and a bar of zero length
565
+ // says it did not. Held back until the grow-in has actually started, so
566
+ // the stub does not appear before the bar it belongs to.
567
+ if (minBarLength > 0 && eased > 0 && !loading && length < minBarLength) {
568
+ length = minBarLength;
569
+ }
570
+ if (length <= 0) continue;
571
+ const lead = bands.alongStart + i * bands.band + (bands.band - bands.thickness) / 2;
572
+ // Which way the bar points is the direction of the change, not the sign
573
+ // of the value — a fall from 900 to 400 points down whatever those two
574
+ // numbers are, and a total always points away from the baseline.
575
+ const forward = reached >= step.start;
576
+ d += horizontal ? barPath(forward ? from : from - length, lead, length, bands.thickness, radius, forward ? 'right' : 'left') : barPath(lead, forward ? from - length : from, bands.thickness, length, radius, forward ? 'up' : 'down');
577
+ }
578
+
579
+ // Dimming only happens while something *is* active; with nothing under the
580
+ // finger every bar is at full ink, which is the resting state.
581
+ const dim = !wantActive && active >= 0 ? fadedOpacity : 1;
582
+ return {
583
+ d,
584
+ opacity: dim
585
+ };
586
+ };
587
+ const riseRest = useAnimatedProps(build('rise', false));
588
+ const riseActive = useAnimatedProps(build('rise', true));
589
+ const fallRest = useAnimatedProps(build('fall', false));
590
+ const fallActive = useAnimatedProps(build('fall', true));
591
+ const totalRest = useAnimatedProps(build('total', false));
592
+ const totalActive = useAnimatedProps(build('total', true));
593
+ return /*#__PURE__*/_jsxs(G, {
594
+ children: [/*#__PURE__*/_jsx(AnimatedPath, {
595
+ animatedProps: riseRest,
596
+ fill: colors.rise
597
+ }), /*#__PURE__*/_jsx(AnimatedPath, {
598
+ animatedProps: riseActive,
599
+ fill: colors.rise
600
+ }), /*#__PURE__*/_jsx(AnimatedPath, {
601
+ animatedProps: fallRest,
602
+ fill: colors.fall
603
+ }), /*#__PURE__*/_jsx(AnimatedPath, {
604
+ animatedProps: fallActive,
605
+ fill: colors.fall
606
+ }), /*#__PURE__*/_jsx(AnimatedPath, {
607
+ animatedProps: totalRest,
608
+ fill: colors.total
609
+ }), /*#__PURE__*/_jsx(AnimatedPath, {
610
+ animatedProps: totalActive,
611
+ fill: colors.total
612
+ })]
613
+ });
614
+ }
615
+ WaterfallChartBars.displayName = 'WaterfallChart.Bars';
616
+ WaterfallChartBars.layer = 'svg';
617
+ /**
618
+ * The lines from each bar's end to the next bar's start.
619
+ *
620
+ * Drawn under the bars, and reaching the full width of both bands rather than
621
+ * only the gap between them, so the ends are hidden behind the bars they touch
622
+ * and the line reads as passing behind the run instead of stopping short of it.
623
+ *
624
+ * They arrive with the reveal, each one held back until the bar on its left has
625
+ * finished growing — a connector drawn to a bar that is not there yet points at
626
+ * nothing.
627
+ */
628
+ function WaterfallChartConnectors({
629
+ color,
630
+ dashArray = '3,4',
631
+ strokeWidth = 1,
632
+ opacity = 1
633
+ }) {
634
+ const {
635
+ steps,
636
+ plot,
637
+ status,
638
+ orientation,
639
+ barGap,
640
+ barWidth,
641
+ domainMin,
642
+ domainMax,
643
+ reveal
644
+ } = useChart('WaterfallChart.Connectors');
645
+ const token = useCSSVariable('--color-muted-foreground');
646
+ const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.4)');
647
+ const total = steps.length;
648
+ const horizontal = orientation === 'horizontal';
649
+ const loading = status === 'loading';
650
+ const animatedProps = useAnimatedProps(() => {
651
+ 'worklet';
652
+
653
+ if (total < 2 || plot.width <= 0 || loading) return {
654
+ d: ''
655
+ };
656
+ const bands = bandsOf(plot, total, horizontal, barGap, barWidth);
657
+ const min = domainMin.value;
658
+ const max = domainMax.value;
659
+ const range = max - min || 1;
660
+ const grow = reveal.value;
661
+ const project = value => {
662
+ 'worklet';
663
+
664
+ const fraction = (value - min) / range;
665
+ return horizontal ? bands.acrossStart + fraction * bands.across : bands.acrossStart + bands.across - fraction * bands.across;
666
+ };
667
+ let d = '';
668
+ for (let i = 0; i < total - 1; i++) {
669
+ const step = steps[i];
670
+ if (!step) continue;
671
+
672
+ // The same window the bar on the left is growing in. The connector only
673
+ // starts once that bar has arrived at the level it is drawn at.
674
+ const startAt = total > 1 ? i / total * 0.45 : 0;
675
+ const eased = Math.max(0, Math.min(1, (grow - startAt) / 0.55));
676
+ if (eased <= 0) continue;
677
+ const level = project(step.end);
678
+ const from = bands.alongStart + i * bands.band + (bands.band - bands.thickness) / 2;
679
+ const to = bands.alongStart + (i + 2) * bands.band - (bands.band - bands.thickness) / 2;
680
+ // Extends as the next bar arrives, so the line and the bar it is
681
+ // reaching for grow together rather than the line waiting drawn.
682
+ const reach = from + (to - from) * eased;
683
+ d += horizontal ? `M${level},${from}L${level},${reach}` : `M${from},${level}L${reach},${level}`;
684
+ }
685
+ return {
686
+ d
687
+ };
688
+ });
689
+ if (total < 2) return null;
690
+ return /*#__PURE__*/_jsx(AnimatedPath, {
691
+ animatedProps: animatedProps,
692
+ stroke: stroke,
693
+ strokeWidth: strokeWidth,
694
+ strokeDasharray: dashArray,
695
+ strokeOpacity: opacity,
696
+ fill: "none"
697
+ });
698
+ }
699
+ WaterfallChartConnectors.displayName = 'WaterfallChart.Connectors';
700
+ WaterfallChartConnectors.layer = 'svg';
701
+
702
+ /** How much of the value axis a placeholder bar takes. */
703
+ const SKELETON_LENGTH = 0.18;
704
+
705
+ /** Steps to draw when there is no data yet to count them from. */
706
+ const SKELETON_BARS = 6;
707
+ /**
708
+ * The loading state: a row of short, equal stubs on the baseline, with a
709
+ * highlight travelling across them.
710
+ *
711
+ * Equal and on the baseline on purpose. Placeholder bars at differing heights
712
+ * are a run the reader has no way to tell from the real one until it changes
713
+ * under them, and floating them would invent a set of running totals — which is
714
+ * the one thing this chart exists to report.
715
+ */
716
+ function WaterfallChartSkeleton({
717
+ bars,
718
+ duration = 1400,
719
+ color
720
+ }) {
721
+ const {
722
+ plot,
723
+ status,
724
+ orientation,
725
+ steps,
726
+ barGap,
727
+ barWidth,
728
+ cornerRadius
729
+ } = useChart('WaterfallChart.Skeleton');
730
+ const token = useCSSVariable('--color-skeleton');
731
+ const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
732
+ const highlightToken = useCSSVariable('--color-chart-1');
733
+ const highlight = typeof highlightToken === 'string' ? highlightToken : FALLBACK_TOTAL;
734
+ const sweep = useSharedValue(0);
735
+ const reducedMotion = useReducedMotion();
736
+ const loading = status === 'loading';
737
+ useEffect(() => {
738
+ if (!loading || reducedMotion) {
739
+ cancelAnimation(sweep);
740
+ sweep.value = 0;
741
+ return;
742
+ }
743
+ sweep.value = 0;
744
+ sweep.value = withRepeat(withTiming(1, {
745
+ duration,
746
+ easing: Easing.linear
747
+ }), -1, false);
748
+ return () => cancelAnimation(sweep);
749
+ }, [loading, reducedMotion, duration, sweep]);
750
+
751
+ // The band travels by moving the gradient's own endpoints, so the whole
752
+ // effect is two numbers changing on the UI thread.
753
+ const animatedProps = useAnimatedProps(() => ({
754
+ x1: `${(sweep.value * 1.4 - 0.4) * 100}%`,
755
+ x2: `${(sweep.value * 1.4 - 0.4 + 0.4) * 100}%`
756
+ }));
757
+ const horizontal = orientation === 'horizontal';
758
+ const total = Math.max(1, bars ?? (steps.length || SKELETON_BARS));
759
+ const d = useMemo(() => {
760
+ if (plot.width <= 0 || plot.height <= 0) return '';
761
+ const bands = bandsOf(plot, total, horizontal, barGap, barWidth);
762
+ const length = bands.across * SKELETON_LENGTH;
763
+ let path = '';
764
+ for (let i = 0; i < total; i += 1) {
765
+ const lead = bands.alongStart + i * bands.band + (bands.band - bands.thickness) / 2;
766
+ path += horizontal ? barPath(plot.left, lead, length, bands.thickness, cornerRadius, 'right') : barPath(lead, plot.top + plot.height - length, bands.thickness, length, cornerRadius, 'up');
767
+ }
768
+ return path;
769
+ }, [plot, horizontal, total, barGap, barWidth, cornerRadius]);
770
+ if (!loading || !d) return null;
771
+ const gradientId = 'panelui-waterfall-skeleton';
772
+ return /*#__PURE__*/_jsxs(G, {
773
+ children: [/*#__PURE__*/_jsx(Defs, {
774
+ children: /*#__PURE__*/_jsxs(AnimatedLinearGradient, {
775
+ id: gradientId,
776
+ animatedProps: animatedProps,
777
+ y1: "0",
778
+ y2: "0",
779
+ children: [/*#__PURE__*/_jsx(Stop, {
780
+ offset: "0",
781
+ stopColor: base
782
+ }), /*#__PURE__*/_jsx(Stop, {
783
+ offset: "0.5",
784
+ stopColor: highlight,
785
+ stopOpacity: 0.35
786
+ }), /*#__PURE__*/_jsx(Stop, {
787
+ offset: "1",
788
+ stopColor: base
789
+ })]
790
+ })
791
+ }), /*#__PURE__*/_jsx(Path, {
792
+ d: d,
793
+ fill: `url(#${gradientId})`
794
+ })]
795
+ });
796
+ }
797
+ WaterfallChartSkeleton.displayName = 'WaterfallChart.Skeleton';
798
+ WaterfallChartSkeleton.layer = 'svg';
799
+
800
+ /* -------------------------------------------------------------------------- */
801
+ /* Overlay layer */
802
+ /* -------------------------------------------------------------------------- */
803
+
804
+ /**
805
+ * Narrowest a step label may be drawn before the axis starts dropping some.
806
+ * Roughly three characters at `xs`, which is what a short name needs.
807
+ */
808
+ const MIN_BAND_LABEL = 34;
809
+ /**
810
+ * The step names, one under each band it has room for. Real text rather than
811
+ * SVG text, so they follow the theme's font and the platform's text scaling —
812
+ * SVG text does neither.
813
+ */
814
+ function WaterfallChartXAxis({
815
+ ticks,
816
+ format,
817
+ className
818
+ }) {
819
+ const {
820
+ steps,
821
+ plot,
822
+ orientation
823
+ } = useChart('WaterfallChart.XAxis');
824
+ const labels = useMemo(() => {
825
+ if (!steps.length) return [];
826
+
827
+ /*
828
+ * Every step, unless they will not fit. The axis asks the plot how much
829
+ * room there is and only thins when the answer is not enough — a fixed tick
830
+ * count drops names that had room to be drawn.
831
+ */
832
+ const room = Math.max(1, Math.floor(plot.width / MIN_BAND_LABEL));
833
+ const count = Math.max(1, Math.min(ticks ?? room, steps.length));
834
+
835
+ // Every nth band, rather than a fractional step rounded to the nearest
836
+ // index — rounding lands on the same band twice and skips its neighbour.
837
+ const stride = Math.ceil(steps.length / count);
838
+ const picked = [];
839
+ for (let index = 0; index < steps.length; index += stride) {
840
+ const step = steps[index];
841
+ if (!step) continue;
842
+ picked.push({
843
+ key: index,
844
+ text: format ? format(step, index) : step.label
845
+ });
846
+ }
847
+ return picked;
848
+ }, [steps, ticks, format, plot.width]);
849
+ if (orientation === 'horizontal') return null;
850
+
851
+ /*
852
+ * One box per band, exactly the band's width. Tiling them rather than giving
853
+ * each a fixed width means they can never overlap each other and the first
854
+ * and last can never hang off the ends of the plot — the row of labels
855
+ * occupies precisely the space the bars do.
856
+ */
857
+ const bandWidth = steps.length > 0 ? plot.width / steps.length : 0;
858
+ return /*#__PURE__*/_jsx(View, {
859
+ style: {
860
+ position: 'absolute',
861
+ inset: 0,
862
+ pointerEvents: 'none'
863
+ },
864
+ className: cn(className),
865
+ children: labels.map(label => /*#__PURE__*/_jsx(Text, {
866
+ size: "xs",
867
+ muted: true,
868
+ numberOfLines: 1,
869
+ style: {
870
+ position: 'absolute',
871
+ bottom: 0,
872
+ left: plot.left + bandWidth * label.key,
873
+ width: bandWidth,
874
+ textAlign: 'center'
875
+ },
876
+ children: label.text
877
+ }, label.key))
878
+ });
879
+ }
880
+ WaterfallChartXAxis.displayName = 'WaterfallChart.XAxis';
881
+ WaterfallChartXAxis.layer = 'overlay';
882
+ /** Value labels down the side, aligned to the grid lines. */
883
+ function WaterfallChartYAxis({
884
+ ticks = 4,
885
+ format,
886
+ className
887
+ }) {
888
+ const {
889
+ plot,
890
+ steps,
891
+ orientation,
892
+ extent
893
+ } = useChart('WaterfallChart.YAxis');
894
+ const horizontal = orientation === 'horizontal';
895
+
896
+ /*
897
+ * Read off the settled domain rather than the shared values the paths use.
898
+ * A label is text, and text is JS — following the tween would re-render on
899
+ * every frame of it to redraw a number nobody can read while it moves.
900
+ */
901
+ const labels = useMemo(() => {
902
+ if (horizontal) {
903
+ // Sideways, the side of the chart is the step axis.
904
+ return steps.map((step, index) => ({
905
+ key: index,
906
+ text: step.label
907
+ }));
908
+ }
909
+ const [min, max] = extent;
910
+ if (min === 0 && max === 0) return [];
911
+ return Array.from({
912
+ length: ticks + 1
913
+ }, (_unused, index) => {
914
+ const value = max - (max - min) * index / ticks;
915
+ return {
916
+ key: index,
917
+ text: format ? format(value) : compactNumber(value)
918
+ };
919
+ });
920
+ }, [extent, ticks, format, horizontal, steps]);
921
+
922
+ /*
923
+ * Sideways the labels sit in the gutter the plot already left for them, one
924
+ * band each — `flex-1` per row rather than spacing them edge to edge, so
925
+ * every name lands beside its own bar instead of only the first and last
926
+ * doing so. Upright there are no bands to line up with, so the ticks space
927
+ * themselves against the grid.
928
+ */
929
+ return /*#__PURE__*/_jsx(View, {
930
+ pointerEvents: "none",
931
+ style: {
932
+ position: 'absolute',
933
+ left: 0,
934
+ // Upright, each label is centred on the grid line it names: the strip
935
+ // is lifted half a label and grown by a whole one, so `justify-between`
936
+ // lands the text's middle on the line rather than its top edge on the
937
+ // first and its bottom edge on the last.
938
+ top: horizontal ? plot.top : plot.top - AXIS_LABEL_HEIGHT / 2,
939
+ height: horizontal ? plot.height : plot.height + AXIS_LABEL_HEIGHT,
940
+ width: horizontal ? Math.max(plot.left - 8, 0) : undefined
941
+ },
942
+ className: cn(horizontal ? 'items-end' : 'justify-between', className),
943
+ children: labels.map(label => /*#__PURE__*/_jsx(View, {
944
+ className: horizontal ? 'flex-1 justify-center' : undefined,
945
+ children: /*#__PURE__*/_jsx(Text, {
946
+ size: "xs",
947
+ muted: true,
948
+ numberOfLines: 1,
949
+ children: label.text
950
+ })
951
+ }, label.key))
952
+ });
953
+ }
954
+ WaterfallChartYAxis.displayName = 'WaterfallChart.YAxis';
955
+ WaterfallChartYAxis.layer = 'overlay';
956
+ // Read by the root, which has to leave room for the labels before it lays the
957
+ // plot out — an axis drawn over the plot is unreadable, and makes what it is
958
+ // drawn over unreadable too.
959
+ WaterfallChartYAxis.axis = 'y';
960
+ /**
961
+ * The change each step made, written at the far end of its bar.
962
+ *
963
+ * Signed, because on this chart the sign is the reading: a bar's direction
964
+ * already says which way it went, and a label that drops the sign makes the
965
+ * two directions look like the same number twice.
966
+ *
967
+ * Only drawn upright. Sideways the bars run across a plot whose width is a
968
+ * phone's, and a number at the end of one has nowhere to go that is not on top
969
+ * of the bar or off the chart.
970
+ */
971
+ function WaterfallChartValues({
972
+ format,
973
+ className
974
+ }) {
975
+ const {
976
+ steps,
977
+ plot,
978
+ orientation,
979
+ status,
980
+ barGap,
981
+ barWidth,
982
+ extent
983
+ } = useChart('WaterfallChart.Values');
984
+ if (orientation === 'horizontal' || status === 'loading' || !steps.length) return null;
985
+ if (plot.width <= 0 || plot.height <= 0) return null;
986
+ const bands = bandsOf(plot, steps.length, false, barGap, barWidth);
987
+ const [min, max] = extent;
988
+ const range = max - min || 1;
989
+ return /*#__PURE__*/_jsx(View, {
990
+ style: {
991
+ position: 'absolute',
992
+ inset: 0,
993
+ pointerEvents: 'none'
994
+ },
995
+ children: steps.map((step, index) => {
996
+ const fraction = (step.end - min) / range;
997
+ const y = plot.top + plot.height - fraction * plot.height;
998
+ const forward = step.end >= step.start;
999
+ const text = format ? format(step, index) : step.kind === 'total' ? compactNumber(step.value) : `${step.value > 0 ? '+' : ''}${compactNumber(step.value)}`;
1000
+ return /*#__PURE__*/_jsx(Text, {
1001
+ size: "xs",
1002
+ weight: "medium",
1003
+ numberOfLines: 1,
1004
+ className: cn(className),
1005
+ style: {
1006
+ position: 'absolute',
1007
+ // Sat off the growing end of the bar, on whichever side that is.
1008
+ // A label inside a bar is unreadable on a short one and a label
1009
+ // always above is inside the plot's ceiling on the tall one.
1010
+ top: forward ? y - AXIS_LABEL_HEIGHT - 2 : y + 2,
1011
+ left: plot.left + bands.band * index,
1012
+ width: bands.band,
1013
+ textAlign: 'center'
1014
+ },
1015
+ children: text
1016
+ }, index);
1017
+ })
1018
+ });
1019
+ }
1020
+ WaterfallChartValues.displayName = 'WaterfallChart.Values';
1021
+ WaterfallChartValues.layer = 'overlay';
1022
+ /**
1023
+ * The readout, and the drag that drives it.
1024
+ *
1025
+ * It reports two numbers, because a step on this chart has two: what it changed
1026
+ * by, and what the balance stood at afterwards. The second is the one a bar's
1027
+ * position encodes and its length does not, so a readout that only gave the
1028
+ * change would leave the reader converting the height back by eye.
1029
+ *
1030
+ * The hit area is the whole plot. A readout you have to land on the bar to
1031
+ * summon is a readout nobody finds — and the bars here are narrower than a bar
1032
+ * chart's, since the gap between them is what the connectors run through.
1033
+ */
1034
+ function WaterfallChartTooltip({
1035
+ formatValue,
1036
+ formatTotal,
1037
+ className
1038
+ }) {
1039
+ const {
1040
+ steps,
1041
+ plot,
1042
+ orientation,
1043
+ colors,
1044
+ activeIndex,
1045
+ activeIndexJS,
1046
+ setActiveIndexJS,
1047
+ status
1048
+ } = useChart('WaterfallChart.Tooltip');
1049
+ const total = steps.length;
1050
+ const horizontal = orientation === 'horizontal';
1051
+ const left = plot.left;
1052
+ const top = plot.top;
1053
+ const width = plot.width;
1054
+ const height = plot.height;
1055
+
1056
+ /*
1057
+ * The readout's own height, measured rather than assumed. Sideways it has to
1058
+ * be clamped inside the plot vertically, and how tall it is depends on
1059
+ * whether the running total line is shown.
1060
+ */
1061
+ const labelHeight = useSharedValue(0);
1062
+
1063
+ /*
1064
+ * Declared inside the memo, next to its callers: a worklet may only call
1065
+ * another worklet, and the rule is enforced by crashing rather than warning.
1066
+ */
1067
+ const pan = useMemo(() => {
1068
+ const resolve = (x, y) => {
1069
+ 'worklet';
1070
+
1071
+ if (!total) return;
1072
+ const span = horizontal ? height : width;
1073
+ const offset = (horizontal ? y - top : x - left) / (span || 1);
1074
+ // Bands, not points: the finger is inside whichever slice it lands on,
1075
+ // which is a floor rather than a round to the nearest centre.
1076
+ const next = Math.max(0, Math.min(total - 1, Math.floor(offset * total)));
1077
+ if (next === activeIndex.value) return;
1078
+ activeIndex.value = next;
1079
+ runOnJS(setActiveIndexJS)(next);
1080
+ };
1081
+ return Gesture.Pan().minDistance(0).onBegin(event => {
1082
+ 'worklet';
1083
+
1084
+ resolve(event.x, event.y);
1085
+ }).onUpdate(event => {
1086
+ 'worklet';
1087
+
1088
+ resolve(event.x, event.y);
1089
+ }).onFinalize(() => {
1090
+ 'worklet';
1091
+
1092
+ activeIndex.value = -1;
1093
+ runOnJS(setActiveIndexJS)(-1);
1094
+ });
1095
+ }, [total, left, top, width, height, horizontal, activeIndex, setActiveIndexJS]);
1096
+
1097
+ // The readout centres over its band and is clamped inside the plot, so it
1098
+ // never runs off the edge at the first or last one. Which axis it follows is
1099
+ // the axis the bands run along.
1100
+ const labelStyle = useAnimatedStyle(() => {
1101
+ const index = activeIndex.value;
1102
+ if (index < 0 || !total) return {
1103
+ opacity: 0
1104
+ };
1105
+ const band = (horizontal ? plot.height : plot.width) / total;
1106
+ const centre = (horizontal ? plot.top : plot.left) + band * (index + 0.5);
1107
+ const half = LABEL_WIDTH / 2;
1108
+ const clamped = Math.min(plot.left + plot.width - half, Math.max(plot.left + half, horizontal ? plot.left + plot.width / 2 : centre));
1109
+ if (!horizontal) {
1110
+ return {
1111
+ opacity: 1,
1112
+ transform: [{
1113
+ translateX: clamped - half
1114
+ }]
1115
+ };
1116
+ }
1117
+ const tall = labelHeight.value;
1118
+ const y = Math.min(plot.top + Math.max(plot.height - tall, 0), Math.max(plot.top, centre - tall / 2));
1119
+ return {
1120
+ opacity: 1,
1121
+ transform: [{
1122
+ translateX: clamped - half
1123
+ }, {
1124
+ translateY: y
1125
+ }]
1126
+ };
1127
+ });
1128
+ const active = activeIndexJS >= 0 ? steps[activeIndexJS] : null;
1129
+ const fmtValue = formatValue ?? (step => step.kind === 'total' ? compactNumber(step.value) : `${step.value > 0 ? '+' : ''}${compactNumber(step.value)}`);
1130
+ const fmtTotal = formatTotal ?? (step => step.kind === 'total' ? null : compactNumber(step.end));
1131
+ if (status === 'loading') return null;
1132
+ const runningTotal = active ? fmtTotal(active) : null;
1133
+ return /*#__PURE__*/_jsx(GestureDetector, {
1134
+ gesture: pan,
1135
+ children: /*#__PURE__*/_jsx(View, {
1136
+ style: StyleSheet.absoluteFill,
1137
+ children: /*#__PURE__*/_jsx(Animated.View, {
1138
+ pointerEvents: "none",
1139
+ style: [{
1140
+ position: 'absolute',
1141
+ left: 0,
1142
+ top: 0,
1143
+ width: LABEL_WIDTH
1144
+ }, labelStyle],
1145
+ children: active ? /*#__PURE__*/_jsxs(View, {
1146
+ onLayout: event => {
1147
+ labelHeight.value = event.nativeEvent.layout.height;
1148
+ },
1149
+ className: cn('rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
1150
+ children: [/*#__PURE__*/_jsx(Text, {
1151
+ size: "xs",
1152
+ muted: true,
1153
+ numberOfLines: 1,
1154
+ children: active.label
1155
+ }), /*#__PURE__*/_jsxs(View, {
1156
+ className: "flex-row items-center gap-1.5",
1157
+ children: [/*#__PURE__*/_jsx(View, {
1158
+ style: {
1159
+ width: 6,
1160
+ height: 6,
1161
+ borderRadius: 3,
1162
+ backgroundColor: active.datum.color ?? colors[active.kind]
1163
+ }
1164
+ }), /*#__PURE__*/_jsx(Text, {
1165
+ size: "xs",
1166
+ weight: "medium",
1167
+ children: fmtValue(active)
1168
+ })]
1169
+ }), runningTotal ? /*#__PURE__*/_jsx(Text, {
1170
+ size: "xs",
1171
+ muted: true,
1172
+ numberOfLines: 1,
1173
+ children: runningTotal
1174
+ }) : null]
1175
+ }) : null
1176
+ })
1177
+ })
1178
+ });
1179
+ }
1180
+ WaterfallChartTooltip.displayName = 'WaterfallChart.Tooltip';
1181
+ WaterfallChartTooltip.layer = 'overlay';
1182
+
1183
+ /** One role's colour and name. Shared by the legend and the header. */
1184
+ function RoleSwatch({
1185
+ color,
1186
+ label
1187
+ }) {
1188
+ return /*#__PURE__*/_jsxs(View, {
1189
+ className: "flex-row items-center gap-1.5",
1190
+ children: [/*#__PURE__*/_jsx(View, {
1191
+ style: {
1192
+ backgroundColor: color
1193
+ },
1194
+ className: "h-2 w-2 rounded-full"
1195
+ }), /*#__PURE__*/_jsx(Text, {
1196
+ size: "xs",
1197
+ muted: true,
1198
+ children: label
1199
+ })]
1200
+ });
1201
+ }
1202
+ /**
1203
+ * A swatch and a name for each role the run actually contains.
1204
+ *
1205
+ * Three entries at most, and only the ones present — a run with no totals in it
1206
+ * listing a "Total" colour is a key to a colour that is not on the chart.
1207
+ */
1208
+ function WaterfallChartLegend({
1209
+ className,
1210
+ labels,
1211
+ ...props
1212
+ }) {
1213
+ const {
1214
+ steps,
1215
+ colors
1216
+ } = useChart('WaterfallChart.Legend');
1217
+ const present = useMemo(() => {
1218
+ const order = ['rise', 'fall', 'total'];
1219
+ const seen = new Set(steps.map(step => step.kind));
1220
+ return order.filter(kind => seen.has(kind));
1221
+ }, [steps]);
1222
+ if (!present.length) return null;
1223
+ const names = {
1224
+ rise: labels?.rise ?? 'Increase',
1225
+ fall: labels?.fall ?? 'Decrease',
1226
+ total: labels?.total ?? 'Total'
1227
+ };
1228
+ return /*#__PURE__*/_jsx(View, {
1229
+ ...props,
1230
+ style: [{
1231
+ pointerEvents: 'none'
1232
+ }, props.style],
1233
+ className: cn('absolute right-2 top-1 flex-row gap-3', className),
1234
+ children: present.map(kind => /*#__PURE__*/_jsx(RoleSwatch, {
1235
+ color: colors[kind],
1236
+ label: names[kind]
1237
+ }, kind))
1238
+ });
1239
+ }
1240
+ WaterfallChartLegend.displayName = 'WaterfallChart.Legend';
1241
+ WaterfallChartLegend.layer = 'overlay';
1242
+
1243
+ /* -------------------------------------------------------------------------- */
1244
+ /* Header layer */
1245
+ /* -------------------------------------------------------------------------- */
1246
+
1247
+ /**
1248
+ * The strip above the plot: what the run is of, what it currently reads, and
1249
+ * what the colours mean.
1250
+ *
1251
+ * The value is not derived here. A readout that follows the finger belongs to
1252
+ * whoever owns the data — take it from `onActiveIndexChange` and pass the
1253
+ * formatted string down, so one header can show the closing balance when
1254
+ * nothing is pressed and a step's change when something is.
1255
+ */
1256
+ function WaterfallChartHeader({
1257
+ className,
1258
+ title,
1259
+ value,
1260
+ caption,
1261
+ labels,
1262
+ legend = false,
1263
+ children,
1264
+ ...props
1265
+ }) {
1266
+ const {
1267
+ steps,
1268
+ colors
1269
+ } = useChart('WaterfallChart.Header');
1270
+ const present = useMemo(() => {
1271
+ const order = ['rise', 'fall', 'total'];
1272
+ const seen = new Set(steps.map(step => step.kind));
1273
+ return order.filter(kind => seen.has(kind));
1274
+ }, [steps]);
1275
+ const names = {
1276
+ rise: labels?.rise ?? 'Increase',
1277
+ fall: labels?.fall ?? 'Decrease',
1278
+ total: labels?.total ?? 'Total'
1279
+ };
1280
+ const trailing = children ?? (legend && present.length ? /*#__PURE__*/_jsx(View, {
1281
+ className: "flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1",
1282
+ children: present.map(kind => /*#__PURE__*/_jsx(RoleSwatch, {
1283
+ color: colors[kind],
1284
+ label: names[kind]
1285
+ }, kind))
1286
+ }) : null);
1287
+ return /*#__PURE__*/_jsxs(View, {
1288
+ ...props,
1289
+ className: cn('flex-row items-start justify-between gap-3 pb-3', className),
1290
+ children: [/*#__PURE__*/_jsxs(View, {
1291
+ className: "flex-1 gap-0.5",
1292
+ children: [title ? /*#__PURE__*/_jsx(Text, {
1293
+ size: "xs",
1294
+ muted: true,
1295
+ children: title
1296
+ }) : null, value ? /*#__PURE__*/_jsx(Text, {
1297
+ size: "xl",
1298
+ weight: "bold",
1299
+ children: value
1300
+ }) : null, caption ? /*#__PURE__*/_jsx(Text, {
1301
+ size: "xs",
1302
+ muted: true,
1303
+ children: caption
1304
+ }) : null]
1305
+ }), trailing ? /*#__PURE__*/_jsx(View, {
1306
+ className: "shrink pt-1",
1307
+ children: trailing
1308
+ }) : null]
1309
+ });
1310
+ }
1311
+ WaterfallChartHeader.displayName = 'WaterfallChart.Header';
1312
+ WaterfallChartHeader.layer = 'header';
1313
+ export const WaterfallChart = Object.assign(WaterfallChartRoot, {
1314
+ Header: WaterfallChartHeader,
1315
+ Grid: WaterfallChartGrid,
1316
+ Connectors: WaterfallChartConnectors,
1317
+ Bars: WaterfallChartBars,
1318
+ Values: WaterfallChartValues,
1319
+ Skeleton: WaterfallChartSkeleton,
1320
+ XAxis: WaterfallChartXAxis,
1321
+ YAxis: WaterfallChartYAxis,
1322
+ Tooltip: WaterfallChartTooltip,
1323
+ Legend: WaterfallChartLegend
1324
+ });
1325
+ //# sourceMappingURL=index.js.map