react-native-livechart 1.0.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 (243) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +198 -0
  3. package/dist/components/AnimatedLabel.d.ts +21 -0
  4. package/dist/components/AnimatedLabel.d.ts.map +1 -0
  5. package/dist/components/BadgeOverlay.d.ts +16 -0
  6. package/dist/components/BadgeOverlay.d.ts.map +1 -0
  7. package/dist/components/CrosshairLine.d.ts +20 -0
  8. package/dist/components/CrosshairLine.d.ts.map +1 -0
  9. package/dist/components/CrosshairOverlay.d.ts +29 -0
  10. package/dist/components/CrosshairOverlay.d.ts.map +1 -0
  11. package/dist/components/DegenParticlesOverlay.d.ts +16 -0
  12. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -0
  13. package/dist/components/DotOverlay.d.ts +17 -0
  14. package/dist/components/DotOverlay.d.ts.map +1 -0
  15. package/dist/components/LeftEdgeFade.d.ts +9 -0
  16. package/dist/components/LeftEdgeFade.d.ts.map +1 -0
  17. package/dist/components/LiveChart.d.ts +3 -0
  18. package/dist/components/LiveChart.d.ts.map +1 -0
  19. package/dist/components/LiveChartSeries.d.ts +3 -0
  20. package/dist/components/LiveChartSeries.d.ts.map +1 -0
  21. package/dist/components/LiveChartTransition.d.ts +28 -0
  22. package/dist/components/LiveChartTransition.d.ts.map +1 -0
  23. package/dist/components/LoadingOverlay.d.ts +24 -0
  24. package/dist/components/LoadingOverlay.d.ts.map +1 -0
  25. package/dist/components/MarkerOverlay.d.ts +25 -0
  26. package/dist/components/MarkerOverlay.d.ts.map +1 -0
  27. package/dist/components/MultiSeriesDots.d.ts +11 -0
  28. package/dist/components/MultiSeriesDots.d.ts.map +1 -0
  29. package/dist/components/MultiSeriesStroke.d.ts +14 -0
  30. package/dist/components/MultiSeriesStroke.d.ts.map +1 -0
  31. package/dist/components/MultiSeriesTooltipStack.d.ts +10 -0
  32. package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -0
  33. package/dist/components/MultiSeriesValueLabels.d.ts +11 -0
  34. package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -0
  35. package/dist/components/MultiSeriesValueLines.d.ts +10 -0
  36. package/dist/components/MultiSeriesValueLines.d.ts.map +1 -0
  37. package/dist/components/ReferenceLineOverlay.d.ts +19 -0
  38. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -0
  39. package/dist/components/SeriesToggleChips.d.ts +15 -0
  40. package/dist/components/SeriesToggleChips.d.ts.map +1 -0
  41. package/dist/components/TradeStreamOverlay.d.ts +14 -0
  42. package/dist/components/TradeStreamOverlay.d.ts.map +1 -0
  43. package/dist/components/ValueLineOverlay.d.ts +16 -0
  44. package/dist/components/ValueLineOverlay.d.ts.map +1 -0
  45. package/dist/components/ValueTextOverlay.d.ts +20 -0
  46. package/dist/components/ValueTextOverlay.d.ts.map +1 -0
  47. package/dist/components/XAxisOverlay.d.ts +14 -0
  48. package/dist/components/XAxisOverlay.d.ts.map +1 -0
  49. package/dist/components/YAxisOverlay.d.ts +23 -0
  50. package/dist/components/YAxisOverlay.d.ts.map +1 -0
  51. package/dist/components/seriesMetaSig.d.ts +4 -0
  52. package/dist/components/seriesMetaSig.d.ts.map +1 -0
  53. package/dist/constants.d.ts +38 -0
  54. package/dist/constants.d.ts.map +1 -0
  55. package/dist/core/liveChartEngineTick.d.ts +45 -0
  56. package/dist/core/liveChartEngineTick.d.ts.map +1 -0
  57. package/dist/core/liveChartSeriesEngineTick.d.ts +38 -0
  58. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -0
  59. package/dist/core/multiSeriesLayout.d.ts +22 -0
  60. package/dist/core/multiSeriesLayout.d.ts.map +1 -0
  61. package/dist/core/resolveConfig.d.ts +188 -0
  62. package/dist/core/resolveConfig.d.ts.map +1 -0
  63. package/dist/core/useLiveChartEngine.d.ts +79 -0
  64. package/dist/core/useLiveChartEngine.d.ts.map +1 -0
  65. package/dist/core/useLiveChartSeriesEngine.d.ts +68 -0
  66. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -0
  67. package/dist/draw/candle.d.ts +25 -0
  68. package/dist/draw/candle.d.ts.map +1 -0
  69. package/dist/draw/grid.d.ts +24 -0
  70. package/dist/draw/grid.d.ts.map +1 -0
  71. package/dist/draw/line.d.ts +86 -0
  72. package/dist/draw/line.d.ts.map +1 -0
  73. package/dist/draw/trade.d.ts +38 -0
  74. package/dist/draw/trade.d.ts.map +1 -0
  75. package/dist/hooks/crosshairSeries.d.ts +13 -0
  76. package/dist/hooks/crosshairSeries.d.ts.map +1 -0
  77. package/dist/hooks/crosshairShared.d.ts +72 -0
  78. package/dist/hooks/crosshairShared.d.ts.map +1 -0
  79. package/dist/hooks/index.d.ts +24 -0
  80. package/dist/hooks/index.d.ts.map +1 -0
  81. package/dist/hooks/resolveChartLayout.d.ts +42 -0
  82. package/dist/hooks/resolveChartLayout.d.ts.map +1 -0
  83. package/dist/hooks/useBadge.d.ts +14 -0
  84. package/dist/hooks/useBadge.d.ts.map +1 -0
  85. package/dist/hooks/useCandlePaths.d.ts +19 -0
  86. package/dist/hooks/useCandlePaths.d.ts.map +1 -0
  87. package/dist/hooks/useCanvasLayout.d.ts +12 -0
  88. package/dist/hooks/useCanvasLayout.d.ts.map +1 -0
  89. package/dist/hooks/useChartColors.d.ts +20 -0
  90. package/dist/hooks/useChartColors.d.ts.map +1 -0
  91. package/dist/hooks/useChartPaths.d.ts +19 -0
  92. package/dist/hooks/useChartPaths.d.ts.map +1 -0
  93. package/dist/hooks/useChartReveal.d.ts +30 -0
  94. package/dist/hooks/useChartReveal.d.ts.map +1 -0
  95. package/dist/hooks/useChartSkiaFont.d.ts +8 -0
  96. package/dist/hooks/useChartSkiaFont.d.ts.map +1 -0
  97. package/dist/hooks/useCrosshair.d.ts +20 -0
  98. package/dist/hooks/useCrosshair.d.ts.map +1 -0
  99. package/dist/hooks/useCrosshairSeries.d.ts +10 -0
  100. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -0
  101. package/dist/hooks/useDegen.d.ts +17 -0
  102. package/dist/hooks/useDegen.d.ts.map +1 -0
  103. package/dist/hooks/useLiveChartHasData.d.ts +17 -0
  104. package/dist/hooks/useLiveChartHasData.d.ts.map +1 -0
  105. package/dist/hooks/useLiveDot.d.ts +12 -0
  106. package/dist/hooks/useLiveDot.d.ts.map +1 -0
  107. package/dist/hooks/useMarkers.d.ts +15 -0
  108. package/dist/hooks/useMarkers.d.ts.map +1 -0
  109. package/dist/hooks/useModeBlend.d.ts +18 -0
  110. package/dist/hooks/useModeBlend.d.ts.map +1 -0
  111. package/dist/hooks/useMomentum.d.ts +16 -0
  112. package/dist/hooks/useMomentum.d.ts.map +1 -0
  113. package/dist/hooks/useMultiSeriesDegen.d.ts +25 -0
  114. package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -0
  115. package/dist/hooks/useMultiSeriesLinePaths.d.ts +15 -0
  116. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -0
  117. package/dist/hooks/useReferenceLine.d.ts +32 -0
  118. package/dist/hooks/useReferenceLine.d.ts.map +1 -0
  119. package/dist/hooks/useReverseMorphEngineInputs.d.ts +24 -0
  120. package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -0
  121. package/dist/hooks/useTradeStream.d.ts +12 -0
  122. package/dist/hooks/useTradeStream.d.ts.map +1 -0
  123. package/dist/hooks/useXAxis.d.ts +32 -0
  124. package/dist/hooks/useXAxis.d.ts.map +1 -0
  125. package/dist/hooks/useYAxis.d.ts +13 -0
  126. package/dist/hooks/useYAxis.d.ts.map +1 -0
  127. package/dist/index.d.ts +18 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/lib/format.d.ts +5 -0
  130. package/dist/lib/format.d.ts.map +1 -0
  131. package/dist/lib/measureFontTextWidth.d.ts +7 -0
  132. package/dist/lib/measureFontTextWidth.d.ts.map +1 -0
  133. package/dist/lib/monoFontFamily.d.ts +6 -0
  134. package/dist/lib/monoFontFamily.d.ts.map +1 -0
  135. package/dist/math/color.d.ts +5 -0
  136. package/dist/math/color.d.ts.map +1 -0
  137. package/dist/math/degenTick.d.ts +57 -0
  138. package/dist/math/degenTick.d.ts.map +1 -0
  139. package/dist/math/interpolate.d.ts +7 -0
  140. package/dist/math/interpolate.d.ts.map +1 -0
  141. package/dist/math/intervals.d.ts +3 -0
  142. package/dist/math/intervals.d.ts.map +1 -0
  143. package/dist/math/lerp.d.ts +7 -0
  144. package/dist/math/lerp.d.ts.map +1 -0
  145. package/dist/math/markers.d.ts +44 -0
  146. package/dist/math/markers.d.ts.map +1 -0
  147. package/dist/math/momentum.d.ts +8 -0
  148. package/dist/math/momentum.d.ts.map +1 -0
  149. package/dist/math/pickCandle.d.ts +8 -0
  150. package/dist/math/pickCandle.d.ts.map +1 -0
  151. package/dist/math/range.d.ts +10 -0
  152. package/dist/math/range.d.ts.map +1 -0
  153. package/dist/math/referenceLines.d.ts +20 -0
  154. package/dist/math/referenceLines.d.ts.map +1 -0
  155. package/dist/math/spline.d.ts +45 -0
  156. package/dist/math/spline.d.ts.map +1 -0
  157. package/dist/math/squiggly.d.ts +36 -0
  158. package/dist/math/squiggly.d.ts.map +1 -0
  159. package/dist/theme.d.ts +30 -0
  160. package/dist/theme.d.ts.map +1 -0
  161. package/dist/types.d.ts +720 -0
  162. package/dist/types.d.ts.map +1 -0
  163. package/package.json +72 -0
  164. package/src/components/AnimatedLabel.tsx +41 -0
  165. package/src/components/BadgeOverlay.tsx +39 -0
  166. package/src/components/CrosshairLine.tsx +90 -0
  167. package/src/components/CrosshairOverlay.tsx +170 -0
  168. package/src/components/DegenParticlesOverlay.tsx +141 -0
  169. package/src/components/DotOverlay.tsx +69 -0
  170. package/src/components/LeftEdgeFade.tsx +36 -0
  171. package/src/components/LiveChart.tsx +821 -0
  172. package/src/components/LiveChartSeries.tsx +607 -0
  173. package/src/components/LiveChartTransition.tsx +135 -0
  174. package/src/components/LoadingOverlay.tsx +301 -0
  175. package/src/components/MarkerOverlay.tsx +331 -0
  176. package/src/components/MultiSeriesDots.tsx +117 -0
  177. package/src/components/MultiSeriesStroke.tsx +79 -0
  178. package/src/components/MultiSeriesTooltipStack.tsx +78 -0
  179. package/src/components/MultiSeriesValueLabels.tsx +103 -0
  180. package/src/components/MultiSeriesValueLines.tsx +115 -0
  181. package/src/components/ReferenceLineOverlay.tsx +317 -0
  182. package/src/components/SeriesToggleChips.tsx +201 -0
  183. package/src/components/TradeStreamOverlay.tsx +117 -0
  184. package/src/components/ValueLineOverlay.tsx +63 -0
  185. package/src/components/ValueTextOverlay.tsx +43 -0
  186. package/src/components/XAxisOverlay.tsx +109 -0
  187. package/src/components/YAxisOverlay.tsx +135 -0
  188. package/src/components/seriesMetaSig.ts +13 -0
  189. package/src/constants.ts +44 -0
  190. package/src/core/liveChartEngineTick.ts +185 -0
  191. package/src/core/liveChartSeriesEngineTick.ts +163 -0
  192. package/src/core/multiSeriesLayout.ts +80 -0
  193. package/src/core/resolveConfig.ts +538 -0
  194. package/src/core/useLiveChartEngine.ts +207 -0
  195. package/src/core/useLiveChartSeriesEngine.ts +215 -0
  196. package/src/draw/candle.ts +210 -0
  197. package/src/draw/grid.ts +174 -0
  198. package/src/draw/line.ts +257 -0
  199. package/src/draw/trade.ts +202 -0
  200. package/src/hooks/crosshairSeries.ts +78 -0
  201. package/src/hooks/crosshairShared.ts +296 -0
  202. package/src/hooks/index.ts +26 -0
  203. package/src/hooks/resolveChartLayout.ts +144 -0
  204. package/src/hooks/useBadge.ts +192 -0
  205. package/src/hooks/useCandlePaths.ts +165 -0
  206. package/src/hooks/useCanvasLayout.ts +22 -0
  207. package/src/hooks/useChartColors.ts +48 -0
  208. package/src/hooks/useChartPaths.ts +121 -0
  209. package/src/hooks/useChartReveal.ts +123 -0
  210. package/src/hooks/useChartSkiaFont.ts +34 -0
  211. package/src/hooks/useCrosshair.ts +250 -0
  212. package/src/hooks/useCrosshairSeries.ts +163 -0
  213. package/src/hooks/useDegen.ts +262 -0
  214. package/src/hooks/useLiveChartHasData.ts +31 -0
  215. package/src/hooks/useLiveDot.ts +31 -0
  216. package/src/hooks/useMarkers.ts +95 -0
  217. package/src/hooks/useModeBlend.ts +52 -0
  218. package/src/hooks/useMomentum.ts +37 -0
  219. package/src/hooks/useMultiSeriesDegen.ts +260 -0
  220. package/src/hooks/useMultiSeriesLinePaths.ts +76 -0
  221. package/src/hooks/useReferenceLine.ts +212 -0
  222. package/src/hooks/useReverseMorphEngineInputs.ts +154 -0
  223. package/src/hooks/useTradeStream.ts +64 -0
  224. package/src/hooks/useXAxis.ts +200 -0
  225. package/src/hooks/useYAxis.ts +48 -0
  226. package/src/index.ts +69 -0
  227. package/src/lib/format.ts +36 -0
  228. package/src/lib/measureFontTextWidth.ts +10 -0
  229. package/src/lib/monoFontFamily.ts +8 -0
  230. package/src/math/color.ts +31 -0
  231. package/src/math/degenTick.ts +151 -0
  232. package/src/math/interpolate.ts +32 -0
  233. package/src/math/intervals.ts +17 -0
  234. package/src/math/lerp.ts +17 -0
  235. package/src/math/markers.ts +175 -0
  236. package/src/math/momentum.ts +38 -0
  237. package/src/math/pickCandle.ts +42 -0
  238. package/src/math/range.ts +54 -0
  239. package/src/math/referenceLines.ts +56 -0
  240. package/src/math/spline.ts +183 -0
  241. package/src/math/squiggly.ts +108 -0
  242. package/src/theme.ts +231 -0
  243. package/src/types.ts +789 -0
@@ -0,0 +1,250 @@
1
+ import { type SkFont } from "@shopify/react-native-skia";
2
+ import { Platform } from "react-native";
3
+ import { Gesture } from "react-native-gesture-handler";
4
+ import {
5
+ useAnimatedReaction,
6
+ useDerivedValue,
7
+ useSharedValue,
8
+ type SharedValue,
9
+ } from "react-native-reanimated";
10
+ import { scheduleOnRN } from "react-native-worklets";
11
+ import type { SingleEngineState } from "../core/useLiveChartEngine";
12
+ import { type ChartPadding } from "../draw/line";
13
+ import { interpolateAtTime } from "../math/interpolate";
14
+ import { pickCandleAtTime } from "../math/pickCandle";
15
+ import type { CandlePoint, LiveChartPalette, ScrubPoint } from "../types";
16
+ import {
17
+ computeCandleTooltipLayout,
18
+ computeCrosshairOpacity,
19
+ computeScrubTime,
20
+ deriveCrosshairTooltipSingle,
21
+ type CrosshairState,
22
+ } from "./crosshairShared";
23
+
24
+ export type { CrosshairState, TooltipLayout } from "./crosshairShared";
25
+ export type { ScrubPoint };
26
+
27
+ export {
28
+ computeCandleTooltipLayout,
29
+ computeCrosshairOpacity,
30
+ computeScrubTime,
31
+ computeTooltipLayout,
32
+ computeTooltipLayoutMulti,
33
+ deriveCrosshairTooltipSingle,
34
+ deriveScrubValueSingle,
35
+ HIDDEN_TOOLTIP
36
+ } from "./crosshairShared";
37
+
38
+ export interface CrosshairCandleOpts {
39
+ mode: "line" | "candle";
40
+ candles?: SharedValue<CandlePoint[]>;
41
+ liveCandle?: SharedValue<CandlePoint | null>;
42
+ candleWidthSecs: number;
43
+ }
44
+
45
+ /**
46
+ * Single-series crosshair + scrub. Use `useCrosshairSeries` for `LiveChartSeries`.
47
+ */
48
+ export function useCrosshair(
49
+ engine: SingleEngineState,
50
+ padding: ChartPadding,
51
+ _palette: LiveChartPalette,
52
+ formatValue: (v: number) => string,
53
+ formatTime: (t: number) => string,
54
+ font: SkFont,
55
+ enabled: boolean,
56
+ onScrub?: (point: ScrubPoint | null) => void,
57
+ candleOpts?: CrosshairCandleOpts,
58
+ ): CrosshairState {
59
+ const scrubX = useSharedValue(-1);
60
+ const scrubActive = useSharedValue(false);
61
+
62
+ const scrubTime = useDerivedValue(() =>
63
+ computeScrubTime(
64
+ scrubActive.get(),
65
+ scrubX.get(),
66
+ padding,
67
+ engine.canvasWidth.get(),
68
+ engine.timestamp.get(),
69
+ engine.displayWindow.get(),
70
+ ),
71
+ );
72
+
73
+ const isCandleMode = candleOpts?.mode === "candle";
74
+ const candlesSV = candleOpts?.candles;
75
+ const liveCandleSV = candleOpts?.liveCandle;
76
+ const candleWidthSecs = candleOpts?.candleWidthSecs ?? 60;
77
+
78
+ /* istanbul ignore next -- worklet */
79
+ const scrubCandle = useDerivedValue(() => {
80
+ if (
81
+ !isCandleMode ||
82
+ !candlesSV ||
83
+ !scrubActive.get() ||
84
+ scrubTime.get() < 0
85
+ )
86
+ return null;
87
+ return pickCandleAtTime(
88
+ candlesSV.get(),
89
+ liveCandleSV?.get() ?? null,
90
+ scrubTime.get(),
91
+ candleWidthSecs,
92
+ );
93
+ });
94
+
95
+ /* istanbul ignore next -- worklet */
96
+ const scrubValue = useDerivedValue(() => {
97
+ if (!scrubActive.get() || scrubTime.get() < 0) return null;
98
+ if (isCandleMode) {
99
+ return scrubCandle.get()?.close ?? null;
100
+ }
101
+ return interpolateAtTime(engine.data.get(), scrubTime.get());
102
+ });
103
+
104
+ const crosshairOpacity = useDerivedValue(() =>
105
+ computeCrosshairOpacity(
106
+ scrubActive.get(),
107
+ scrubX.get(),
108
+ engine.canvasWidth.get(),
109
+ padding.right,
110
+ ),
111
+ );
112
+
113
+ const tooltipLayout = useDerivedValue(() => {
114
+ if (isCandleMode) {
115
+ return computeCandleTooltipLayout(
116
+ scrubActive.get(),
117
+ scrubX.get(),
118
+ scrubCandle.get(),
119
+ scrubTime.get(),
120
+ padding,
121
+ engine.canvasWidth.get(),
122
+ formatValue,
123
+ formatTime,
124
+ font,
125
+ );
126
+ }
127
+ return deriveCrosshairTooltipSingle(
128
+ scrubActive.get(),
129
+ scrubX.get(),
130
+ scrubTime.get(),
131
+ scrubValue.get(),
132
+ padding,
133
+ engine.canvasWidth.get(),
134
+ formatValue,
135
+ formatTime,
136
+ font,
137
+ );
138
+ });
139
+
140
+ /* istanbul ignore next -- invoked only via scheduleOnRN from UI-thread gesture */
141
+ function handleScrub(
142
+ x: number,
143
+ y: number,
144
+ time: number,
145
+ value: number,
146
+ candleJson: string | null,
147
+ ) {
148
+ const candle: CandlePoint | undefined = candleJson
149
+ ? (JSON.parse(candleJson) as CandlePoint)
150
+ : undefined;
151
+ onScrub?.({ time, value, x, y, candle });
152
+ }
153
+
154
+ /* istanbul ignore next */
155
+ function handleScrubEnd() {
156
+ onScrub?.(null);
157
+ }
158
+
159
+ const hasOnScrub = onScrub != null;
160
+
161
+ useAnimatedReaction(
162
+ () => {
163
+ "worklet";
164
+ if (!hasOnScrub) return "__idle__";
165
+ if (!scrubActive.get()) return "__inactive__";
166
+ const time = scrubTime.get();
167
+ const val = scrubValue.get();
168
+ const x = scrubX.get();
169
+ if (val === null || time < 0) return "__pending__";
170
+ const chartW = engine.canvasWidth.get() - padding.left - padding.right;
171
+ if (chartW <= 0) return "__pending__";
172
+ const h = engine.canvasHeight.get();
173
+ const chartH = h - padding.top - padding.bottom;
174
+ const valRange = engine.displayMax.get() - engine.displayMin.get();
175
+ const dotY =
176
+ valRange === 0
177
+ ? padding.top + chartH / 2
178
+ : padding.top + ((engine.displayMax.get() - val) / valRange) * chartH;
179
+ let candleJson: string | null = null;
180
+ if (isCandleMode) {
181
+ const c = scrubCandle.get();
182
+ if (c) candleJson = JSON.stringify(c);
183
+ }
184
+ return JSON.stringify([time, val, x, dotY, candleJson]);
185
+ },
186
+ (curr, prev) => {
187
+ "worklet";
188
+ if (!hasOnScrub) return;
189
+ if (
190
+ curr === "__idle__" ||
191
+ curr === "__inactive__" ||
192
+ curr === "__pending__"
193
+ ) {
194
+ return;
195
+ }
196
+ if (curr === prev) return;
197
+ const row = JSON.parse(curr) as [
198
+ number,
199
+ number,
200
+ number,
201
+ number,
202
+ string | null,
203
+ ];
204
+ scheduleOnRN(handleScrub, row[2], row[3], row[0], row[1], row[4]);
205
+ },
206
+ );
207
+
208
+ let gesture = Gesture.Pan()
209
+ .minDistance(Platform.OS === "android" ? 10 : 0)
210
+ .activateAfterLongPress(0)
211
+ .maxPointers(1)
212
+ .shouldCancelWhenOutside(false)
213
+ .onBegin(
214
+ /* istanbul ignore next */ (e) => {
215
+ "worklet";
216
+ if (!enabled) return;
217
+ scrubX.set(e.x);
218
+ scrubActive.set(true);
219
+ },
220
+ )
221
+ .onUpdate(
222
+ /* istanbul ignore next */ (e) => {
223
+ "worklet";
224
+ if (!enabled) return;
225
+ scrubX.set(e.x);
226
+ },
227
+ )
228
+ .onFinalize(
229
+ /* istanbul ignore next */ () => {
230
+ "worklet";
231
+ scrubActive.set(false);
232
+ if (hasOnScrub) scheduleOnRN(handleScrubEnd);
233
+ },
234
+ );
235
+
236
+ /* istanbul ignore next -- Android-only gesture axis config */
237
+ if (Platform.OS === "android") {
238
+ gesture = gesture.activeOffsetX([-25, 25]).failOffsetY([-25, 25]);
239
+ }
240
+
241
+ return {
242
+ scrubX,
243
+ scrubActive,
244
+ scrubTime,
245
+ scrubValue,
246
+ crosshairOpacity,
247
+ tooltipLayout,
248
+ gesture,
249
+ };
250
+ }
@@ -0,0 +1,163 @@
1
+ import { Platform } from "react-native";
2
+ import { Gesture } from "react-native-gesture-handler";
3
+ import {
4
+ useAnimatedReaction,
5
+ useDerivedValue,
6
+ useSharedValue,
7
+ } from "react-native-reanimated";
8
+ import type { MultiEngineState } from "../core/useLiveChartEngine";
9
+ import { type ChartPadding } from "../draw/line";
10
+ import type { ScrubPointMulti } from "../types";
11
+ import {
12
+ deriveScrubValueSeries,
13
+ interpolateSeriesAtTime,
14
+ } from "./crosshairSeries";
15
+ import {
16
+ computeCrosshairOpacity,
17
+ computeScrubTime,
18
+ HIDDEN_TOOLTIP,
19
+ type CrosshairState,
20
+ } from "./crosshairShared";
21
+
22
+ /**
23
+ * LiveChartSeries crosshair + scrub. No tooltip — data is delivered via
24
+ * `onScrub` worklet callback on the UI thread.
25
+ */
26
+ export function useCrosshairSeries(
27
+ engine: MultiEngineState,
28
+ padding: ChartPadding,
29
+ enabled: boolean,
30
+ onScrub?: (point: ScrubPointMulti | null) => void,
31
+ ): CrosshairState {
32
+ const scrubX = useSharedValue(-1);
33
+ const scrubActive = useSharedValue(false);
34
+
35
+ const scrubTime = useDerivedValue(() =>
36
+ computeScrubTime(
37
+ scrubActive.get(),
38
+ scrubX.get(),
39
+ padding,
40
+ engine.canvasWidth.get(),
41
+ engine.timestamp.get(),
42
+ engine.displayWindow.get(),
43
+ ),
44
+ );
45
+
46
+ const scrubValue = useDerivedValue(() =>
47
+ deriveScrubValueSeries(
48
+ scrubActive.get(),
49
+ scrubTime.get(),
50
+ engine.series.get(),
51
+ ),
52
+ );
53
+
54
+ const crosshairOpacity = useDerivedValue(() =>
55
+ computeCrosshairOpacity(
56
+ scrubActive.get(),
57
+ scrubX.get(),
58
+ engine.canvasWidth.get(),
59
+ padding.right,
60
+ ),
61
+ );
62
+
63
+ const tooltipLayout = useSharedValue(HIDDEN_TOOLTIP);
64
+
65
+ const hasOnScrub = onScrub != null;
66
+
67
+ useAnimatedReaction(
68
+ () => {
69
+ "worklet";
70
+ if (!hasOnScrub) return "__idle__";
71
+ if (!scrubActive.get()) return "__inactive__";
72
+ const time = scrubTime.get();
73
+ const x = scrubX.get();
74
+ const chartW = engine.canvasWidth.get() - padding.left - padding.right;
75
+ if (chartW <= 0) return "__pending__";
76
+ const r = interpolateSeriesAtTime(engine.series.get(), time);
77
+ if (r.primary === null) return "__pending__";
78
+ const h = engine.canvasHeight.get();
79
+ const chartH = h - padding.top - padding.bottom;
80
+ const valRange = engine.displayMax.get() - engine.displayMin.get();
81
+ const dotY =
82
+ valRange === 0
83
+ ? padding.top + chartH / 2
84
+ : padding.top +
85
+ ((engine.displayMax.get() - r.primary) / valRange) * chartH;
86
+ return JSON.stringify({
87
+ time,
88
+ x,
89
+ y: dotY,
90
+ primary: r.primary,
91
+ series: r.seriesValues,
92
+ });
93
+ },
94
+ (curr, prev) => {
95
+ "worklet";
96
+ if (!hasOnScrub) return;
97
+ if (curr === "__inactive__") {
98
+ onScrub!(null);
99
+ return;
100
+ }
101
+ if (curr === "__idle__" || curr === "__pending__") {
102
+ return;
103
+ }
104
+ if (curr === prev) return;
105
+ const p = JSON.parse(curr) as {
106
+ time: number;
107
+ x: number;
108
+ y: number;
109
+ primary: number;
110
+ series: ScrubPointMulti["seriesValues"];
111
+ };
112
+ onScrub!({
113
+ time: p.time,
114
+ value: p.primary,
115
+ x: p.x,
116
+ y: p.y,
117
+ seriesValues: p.series,
118
+ });
119
+ },
120
+ );
121
+
122
+ let gesture = Gesture.Pan()
123
+ .minDistance(Platform.OS === "android" ? 10 : 0)
124
+ .activateAfterLongPress(0)
125
+ .maxPointers(1)
126
+ .shouldCancelWhenOutside(false)
127
+ .onBegin(
128
+ /* istanbul ignore next */ (e) => {
129
+ "worklet";
130
+ if (!enabled) return;
131
+ scrubX.set(e.x);
132
+ scrubActive.set(true);
133
+ },
134
+ )
135
+ .onUpdate(
136
+ /* istanbul ignore next */ (e) => {
137
+ "worklet";
138
+ if (!enabled) return;
139
+ scrubX.set(e.x);
140
+ },
141
+ )
142
+ .onFinalize(
143
+ /* istanbul ignore next */ () => {
144
+ "worklet";
145
+ scrubActive.set(false);
146
+ },
147
+ );
148
+
149
+ /* istanbul ignore next */
150
+ if (Platform.OS === "android") {
151
+ gesture = gesture.activeOffsetX([-25, 25]).failOffsetY([-25, 25]);
152
+ }
153
+
154
+ return {
155
+ scrubX,
156
+ scrubActive,
157
+ scrubTime,
158
+ scrubValue,
159
+ crosshairOpacity,
160
+ tooltipLayout,
161
+ gesture,
162
+ };
163
+ }
@@ -0,0 +1,262 @@
1
+ import { useEffect, useRef } from "react";
2
+ import {
3
+ useDerivedValue,
4
+ useFrameCallback,
5
+ useSharedValue,
6
+ type DerivedValue,
7
+ type SharedValue,
8
+ } from "react-native-reanimated";
9
+ import { runOnJS } from "react-native-worklets";
10
+ import { DEGEN_STRIDE } from "../constants";
11
+ import type { ResolvedDegenConfig } from "../core/resolveConfig";
12
+ import type { SingleEngineState } from "../core/useLiveChartEngine";
13
+ import { computeShake, spawnBurst, tickParticles } from "../math/degenTick";
14
+ import type { DegenShakePayload, Momentum } from "../types";
15
+
16
+ export function useDegen(
17
+ engine: SingleEngineState,
18
+ dotX: SharedValue<number>,
19
+ dotY: SharedValue<number>,
20
+ momentumSV: SharedValue<Momentum>,
21
+ cfg: ResolvedDegenConfig | null,
22
+ onShake?: (payload: DegenShakePayload) => void,
23
+ ): {
24
+ pack: SharedValue<Float64Array<ArrayBuffer>>;
25
+ packRevision: SharedValue<number>;
26
+ shakeTransform: DerivedValue<
27
+ [{ translateX: number }, { translateY: number }]
28
+ >;
29
+ } {
30
+ const MAX_SLOTS = 80;
31
+ const pack = useSharedValue(new Float64Array(MAX_SLOTS * DEGEN_STRIDE));
32
+ const packRevision = useSharedValue(0);
33
+ const prevM = useSharedValue<Momentum>("flat");
34
+ const writeRot = useSharedValue(0);
35
+ const enabledSV = useSharedValue(0);
36
+ const scaleSV = useSharedValue(1);
37
+ const downSV = useSharedValue(0);
38
+ const shakeEnabledSV = useSharedValue(0);
39
+ const shakeIntensitySV = useSharedValue(1);
40
+ const shakeDurationSecSV = useSharedValue(0.45);
41
+ const slotCountSV = useSharedValue(60);
42
+ const burstParticleCountSV = useSharedValue(20);
43
+ const particleBurstDurationSecSV = useSharedValue(1.0);
44
+ const dragSV = useSharedValue(0.95);
45
+ const sizeMinSV = useSharedValue(1);
46
+ const sizeMaxSV = useSharedValue(2.2);
47
+ const spreadAngleSV = useSharedValue(Math.PI * 1.2);
48
+ const jitterXSV = useSharedValue(24);
49
+ const jitterYSV = useSharedValue(8);
50
+ const speedMinSV = useSharedValue(60);
51
+ const speedMaxSV = useSharedValue(160);
52
+ const shakeX = useSharedValue(0);
53
+ const shakeY = useSharedValue(0);
54
+ const shakeStart = useSharedValue(0);
55
+ const prevTimestamp = useSharedValue(0);
56
+ const prevActiveCount = useSharedValue(0);
57
+ const hasOnShakeListenerSV = useSharedValue(0);
58
+
59
+ const onShakeRef = useRef(onShake);
60
+ useEffect(() => {
61
+ onShakeRef.current = onShake;
62
+ });
63
+
64
+ const emitShake = (direction: "up" | "down") => {
65
+ onShakeRef.current?.({ direction });
66
+ };
67
+
68
+ const degenOff = cfg === null;
69
+ const resolvedScale = cfg?.scale ?? 1;
70
+ const resolvedDown = cfg?.downMomentum ?? false;
71
+ const resolvedShake = cfg?.shake ?? true;
72
+ const resolvedShakeIntensity = cfg?.shakeIntensity ?? 1;
73
+ const resolvedShakeDurationSec = cfg?.shakeDurationSec ?? 0.45;
74
+ const resolvedSlotCount = cfg?.particleSlotCount ?? 60;
75
+ const resolvedBurstParticleCount = cfg?.burstParticleCount ?? 20;
76
+ const resolvedParticleBurstDurationSec = cfg?.particleBurstDurationSec ?? 1.0;
77
+ const resolvedDrag = cfg?.drag ?? 0.95;
78
+ const resolvedSizeMin = cfg?.particleSizeMin ?? 1;
79
+ const resolvedSizeMax = cfg?.particleSizeMax ?? 2.2;
80
+ const resolvedSpreadAngle = cfg?.spreadAngle ?? Math.PI * 1.2;
81
+ const resolvedJitterX = cfg?.positionJitterX ?? 24;
82
+ const resolvedJitterY = cfg?.positionJitterY ?? 8;
83
+ const resolvedSpeedMin = cfg?.speedMin ?? 60;
84
+ const resolvedSpeedMax = cfg?.speedMax ?? 160;
85
+
86
+ useEffect(() => {
87
+ // Mirror the resolved config into SharedValues for the UI-thread worklet.
88
+ // Everything is set unconditionally — no prop-derived `if` branch — so this
89
+ // is a plain external-store sync, not a disguised event handler. When degen
90
+ // is off, enabledSV is 0 and the frame worklet early-returns (zeroing the
91
+ // particle buffer and shake itself), so the remaining values are inert; the
92
+ // resolved* defaults already stand in for the null-config case.
93
+ enabledSV.set(degenOff ? 0 : 1);
94
+ shakeEnabledSV.set(!degenOff && resolvedShake ? 1 : 0);
95
+ hasOnShakeListenerSV.set(!degenOff && onShake != null ? 1 : 0);
96
+ scaleSV.set(resolvedScale);
97
+ downSV.set(resolvedDown ? 1 : 0);
98
+ shakeIntensitySV.set(resolvedShakeIntensity);
99
+ shakeDurationSecSV.set(resolvedShakeDurationSec);
100
+ slotCountSV.set(resolvedSlotCount);
101
+ burstParticleCountSV.set(resolvedBurstParticleCount);
102
+ particleBurstDurationSecSV.set(resolvedParticleBurstDurationSec);
103
+ dragSV.set(resolvedDrag);
104
+ sizeMinSV.set(resolvedSizeMin);
105
+ sizeMaxSV.set(resolvedSizeMax);
106
+ spreadAngleSV.set(resolvedSpreadAngle);
107
+ jitterXSV.set(resolvedJitterX);
108
+ jitterYSV.set(resolvedJitterY);
109
+ speedMinSV.set(resolvedSpeedMin);
110
+ speedMaxSV.set(resolvedSpeedMax);
111
+ }, [
112
+ degenOff,
113
+ onShake,
114
+ resolvedScale,
115
+ resolvedDown,
116
+ resolvedShake,
117
+ resolvedShakeIntensity,
118
+ resolvedShakeDurationSec,
119
+ resolvedSlotCount,
120
+ resolvedBurstParticleCount,
121
+ resolvedParticleBurstDurationSec,
122
+ resolvedDrag,
123
+ resolvedSizeMin,
124
+ resolvedSizeMax,
125
+ resolvedSpreadAngle,
126
+ resolvedJitterX,
127
+ resolvedJitterY,
128
+ resolvedSpeedMin,
129
+ resolvedSpeedMax,
130
+ // SharedValue refs are stable (useSharedValue), so listing them satisfies
131
+ // exhaustive-deps without ever re-running the effect.
132
+ enabledSV,
133
+ shakeEnabledSV,
134
+ hasOnShakeListenerSV,
135
+ shakeStart,
136
+ shakeX,
137
+ shakeY,
138
+ scaleSV,
139
+ downSV,
140
+ shakeIntensitySV,
141
+ shakeDurationSecSV,
142
+ slotCountSV,
143
+ burstParticleCountSV,
144
+ particleBurstDurationSecSV,
145
+ dragSV,
146
+ sizeMinSV,
147
+ sizeMaxSV,
148
+ spreadAngleSV,
149
+ jitterXSV,
150
+ jitterYSV,
151
+ speedMinSV,
152
+ speedMaxSV,
153
+ ]);
154
+
155
+ useFrameCallback(
156
+ /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
157
+ "worklet";
158
+ const now = engine.timestamp.get();
159
+ const buf = pack.get();
160
+ const slots = slotCountSV.get();
161
+
162
+ const dtSec = prevTimestamp.get() > 0 ? now - prevTimestamp.get() : 0.016;
163
+ prevTimestamp.set(now);
164
+
165
+ if (enabledSV.get() < 0.5) {
166
+ for (let i = 0; i < slots; i++) buf[i * DEGEN_STRIDE + 5] = 0;
167
+ prevM.set(momentumSV.get());
168
+ prevActiveCount.set(0);
169
+ shakeStart.set(0);
170
+ shakeX.set(0);
171
+ shakeY.set(0);
172
+ return;
173
+ }
174
+
175
+ const m = momentumSV.get();
176
+ const prev = prevM.get();
177
+
178
+ if (m !== prev && m !== "flat") {
179
+ const allowDown = downSV.get() > 0.5;
180
+ const ok = m === "up" || (m === "down" && allowDown);
181
+ if (ok) {
182
+ const cw = engine.canvasWidth.get();
183
+ const ch = engine.canvasHeight.get();
184
+ if (cw >= 1 && ch >= 1) {
185
+ const isUp = m === "up";
186
+ writeRot.set(
187
+ spawnBurst(buf, {
188
+ ox: dotX.get(),
189
+ oy: dotY.get(),
190
+ sc: scaleSV.get(),
191
+ burst: burstParticleCountSV.get(),
192
+ baseAngle: isUp ? -Math.PI / 2 : Math.PI / 2,
193
+ spread: spreadAngleSV.get(),
194
+ jx: jitterXSV.get(),
195
+ jy: jitterYSV.get(),
196
+ sMin: speedMinSV.get(),
197
+ sMax: speedMaxSV.get(),
198
+ szMin: sizeMinSV.get(),
199
+ szMax: sizeMaxSV.get(),
200
+ now,
201
+ baseRot: writeRot.get(),
202
+ slots,
203
+ colorIndex: -1, // cycle the color list per particle
204
+ }),
205
+ );
206
+ if (shakeEnabledSV.get() > 0.5) {
207
+ shakeStart.set(now);
208
+ if (hasOnShakeListenerSV.get() > 0.5) {
209
+ runOnJS(emitShake)(isUp ? "up" : "down");
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ prevM.set(m);
216
+
217
+ if (shakeStart.get() > 0 && shakeEnabledSV.get() > 0.5) {
218
+ const r = computeShake(
219
+ now - shakeStart.get(),
220
+ shakeDurationSecSV.get(),
221
+ scaleSV.get(),
222
+ shakeIntensitySV.get(),
223
+ );
224
+ shakeX.set(r.x);
225
+ shakeY.set(r.y);
226
+ if (!r.active) shakeStart.set(0);
227
+ } else if (shakeEnabledSV.get() < 0.5) {
228
+ shakeStart.set(0);
229
+ shakeX.set(0);
230
+ shakeY.set(0);
231
+ }
232
+
233
+ const activeCount = tickParticles(
234
+ buf,
235
+ slots,
236
+ now,
237
+ particleBurstDurationSecSV.get(),
238
+ dragSV.get(),
239
+ dtSec,
240
+ );
241
+
242
+ // Repaint only while particles are alive (or on the frame they all expire,
243
+ // so the overlay clears). When the field is empty the 4 derived values per
244
+ // slot stay subscribed to `packRevision` alone and freeze — no per-frame
245
+ // worklet churn for an idle particle system.
246
+ if (activeCount > 0 || prevActiveCount.get() > 0) {
247
+ packRevision.set(packRevision.get() + 1);
248
+ }
249
+ prevActiveCount.set(activeCount);
250
+ },
251
+ );
252
+
253
+ const shakeTransform = useDerivedValue(() => {
254
+ "worklet";
255
+ return [{ translateX: shakeX.get() }, { translateY: shakeY.get() }] as [
256
+ { translateX: number },
257
+ { translateY: number },
258
+ ];
259
+ });
260
+
261
+ return { pack, packRevision, shakeTransform };
262
+ }
@@ -0,0 +1,31 @@
1
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
2
+ import type { CandlePoint, LiveChartPoint } from "../types";
3
+
4
+ /**
5
+ * Data presence for {@link LiveChart}: line mode needs ≥2 points;
6
+ * candle mode needs ≥2 committed candles (`liveCandle` alone does not count).
7
+ *
8
+ * {@link useChartReveal} seeds its first-paint state off `hasData` directly (in a
9
+ * layout effect), so no JS-thread snapshot is read during render here.
10
+ */
11
+ export function useLiveChartHasData({
12
+ isCandle,
13
+ data,
14
+ candles,
15
+ }: {
16
+ isCandle: boolean;
17
+ data: SharedValue<LiveChartPoint[]>;
18
+ candles: SharedValue<CandlePoint[]> | undefined;
19
+ }): {
20
+ hasData: SharedValue<boolean>;
21
+ } {
22
+ const hasData = useDerivedValue(() => {
23
+ "worklet";
24
+ if (isCandle) {
25
+ return (candles?.value.length ?? 0) >= 2;
26
+ }
27
+ return data.value.length >= 2;
28
+ });
29
+
30
+ return { hasData };
31
+ }