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,212 @@
1
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
2
+
3
+ import type { SkFont } from "@shopify/react-native-skia";
4
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
5
+ import type { ChartPadding } from "../draw/line";
6
+ import { measureFontTextWidth } from "../lib/measureFontTextWidth";
7
+ import {
8
+ classifyReferenceEdge,
9
+ referenceLineForm,
10
+ } from "../math/referenceLines";
11
+ import type { ReferenceLine } from "../types";
12
+
13
+ /** Screen-space geometry for one reference line / band, recomputed each frame. */
14
+ export interface ReferenceLineLayout {
15
+ visible: boolean;
16
+ /** Line y, or band top-edge y. */
17
+ y: number;
18
+ /** Band bottom-edge y (value/time band); equals `y` for a plain line. */
19
+ yBottom: number;
20
+ /** Left x extent. */
21
+ x1: number;
22
+ /** Right x extent. */
23
+ x2: number;
24
+ /** Resolved label text (with appended value when `showValue`). */
25
+ label: string;
26
+ labelX: number;
27
+ labelY: number;
28
+ /** True when a Form-A line is off-screen and its off-axis badge is showing. */
29
+ offAxis: boolean;
30
+ /** Off-axis chevron points up (target above range) vs down (below). */
31
+ chevronUp: boolean;
32
+ }
33
+
34
+ const INVISIBLE: ReferenceLineLayout = {
35
+ visible: false,
36
+ y: -1,
37
+ yBottom: -1,
38
+ x1: 0,
39
+ x2: 0,
40
+ label: "",
41
+ labelX: 0,
42
+ labelY: -1,
43
+ offAxis: false,
44
+ chevronUp: false,
45
+ };
46
+
47
+ /** Off-axis indicator inset from the nearest plot edge, in px. */
48
+ const OFF_AXIS_EDGE_INSET = 12;
49
+
50
+ /**
51
+ * Derives screen-space layout for a single reference line or band. Supports all
52
+ * three `ReferenceLine` forms (horizontal line, horizontal value band, vertical
53
+ * time band) plus the off-axis badge for an off-screen Form-A value.
54
+ */
55
+ export function useReferenceLine(
56
+ engine: ChartEngineLayout,
57
+ padding: ChartPadding,
58
+ line: ReferenceLine | undefined,
59
+ formatValue: (v: number) => string,
60
+ font: SkFont,
61
+ ): SharedValue<ReferenceLineLayout> {
62
+ const form = line ? referenceLineForm(line) : "none";
63
+
64
+ return useDerivedValue<ReferenceLineLayout>(() => {
65
+ if (!line || form === "none") return INVISIBLE;
66
+
67
+ const w = engine.canvasWidth.value;
68
+ const h = engine.canvasHeight.value;
69
+ if (w === 0 || h === 0) return INVISIBLE;
70
+
71
+ const chartTop = padding.top;
72
+ const chartBottom = h - padding.bottom;
73
+ const chartH = chartBottom - chartTop;
74
+ const x1 = padding.left;
75
+ const x2 = w - padding.right;
76
+
77
+ const fm = font.getMetrics();
78
+ const baselineOffset = (fm.ascent + fm.descent) / 2;
79
+
80
+ // ── Form C — vertical time band (independent of the value range) ─────────
81
+ if (form === "time-band") {
82
+ if (line.from === undefined || line.to === undefined) return INVISIBLE;
83
+ const now = engine.timestamp.value;
84
+ const win = engine.displayWindow.value;
85
+ const winStart = now - win;
86
+ const chartW = x2 - x1;
87
+ if (win <= 0 || chartW <= 0) return INVISIBLE;
88
+
89
+ let bx1 = x1 + ((line.from - winStart) / win) * chartW;
90
+ let bx2 = x1 + ((line.to - winStart) / win) * chartW;
91
+ if (bx2 < bx1) {
92
+ const t = bx1;
93
+ bx1 = bx2;
94
+ bx2 = t;
95
+ }
96
+ if (bx2 < x1 || bx1 > x2) return INVISIBLE;
97
+ if (bx1 < x1) bx1 = x1;
98
+ if (bx2 > x2) bx2 = x2;
99
+
100
+ const label = line.label ?? "";
101
+ const labelX =
102
+ line.labelPosition === "right"
103
+ ? bx2 - 4 - measureFontTextWidth(font, label)
104
+ : bx1 + 4;
105
+ return {
106
+ visible: true,
107
+ y: chartTop,
108
+ yBottom: chartBottom,
109
+ x1: bx1,
110
+ x2: bx2,
111
+ label,
112
+ labelX,
113
+ labelY: chartTop - fm.ascent + 2,
114
+ offAxis: false,
115
+ chevronUp: false,
116
+ };
117
+ }
118
+
119
+ // Forms A / B project values onto y, so they need a non-degenerate range.
120
+ const dMin = engine.displayMin.value;
121
+ const dMax = engine.displayMax.value;
122
+ const valRange = dMax - dMin;
123
+ if (valRange <= 0) return INVISIBLE;
124
+ const toY = (v: number) => chartTop + ((dMax - v) / valRange) * chartH;
125
+
126
+ // ── Form B — horizontal value band ───────────────────────────────────────
127
+ if (form === "value-band") {
128
+ if (line.valueFrom === undefined || line.valueTo === undefined) {
129
+ return INVISIBLE;
130
+ }
131
+ let yTop = toY(line.valueTo);
132
+ let yBot = toY(line.valueFrom);
133
+ if (yBot < yTop) {
134
+ const t = yTop;
135
+ yTop = yBot;
136
+ yBot = t;
137
+ }
138
+ if (yBot < chartTop || yTop > chartBottom) return INVISIBLE;
139
+ if (yTop < chartTop) yTop = chartTop;
140
+ if (yBot > chartBottom) yBot = chartBottom;
141
+
142
+ const label = line.label ?? "";
143
+ const labelX =
144
+ line.labelPosition === "right"
145
+ ? x2 - 4 - measureFontTextWidth(font, label)
146
+ : x1 + 4;
147
+ return {
148
+ visible: true,
149
+ y: yTop,
150
+ yBottom: yBot,
151
+ x1,
152
+ x2,
153
+ label,
154
+ labelX,
155
+ labelY: yTop - fm.ascent + 2,
156
+ offAxis: false,
157
+ chevronUp: false,
158
+ };
159
+ }
160
+
161
+ // ── Form A — horizontal line (with optional off-axis badge) ──────────────
162
+ if (line.value === undefined) return INVISIBLE;
163
+ const v = line.value;
164
+ const edge = classifyReferenceEdge(v, dMin, dMax);
165
+
166
+ if (edge !== "in") {
167
+ if (!line.offAxisBadge) return INVISIBLE; // legacy: cull off-screen line
168
+ const above = edge === "above";
169
+ const clampedY = above
170
+ ? chartTop + OFF_AXIS_EDGE_INSET
171
+ : chartBottom - OFF_AXIS_EDGE_INSET;
172
+ const word = line.offAxisBadgeLabel ?? line.label;
173
+ const text = word ? `${word}: ${formatValue(v)}` : formatValue(v);
174
+ return {
175
+ visible: true,
176
+ y: clampedY,
177
+ yBottom: clampedY,
178
+ x1,
179
+ x2,
180
+ label: text,
181
+ labelX: x1 + 16,
182
+ labelY: clampedY - baselineOffset,
183
+ offAxis: true,
184
+ chevronUp: above,
185
+ };
186
+ }
187
+
188
+ const y = toY(v);
189
+ let label = line.label ?? formatValue(v);
190
+ if (line.showValue && line.label) label = `${line.label} ${formatValue(v)}`;
191
+
192
+ const pos = line.labelPosition ?? "right";
193
+ let labelX: number;
194
+ if (pos === "left") labelX = x1 + 4;
195
+ else if (pos === "center")
196
+ labelX = (x1 + x2) / 2 - measureFontTextWidth(font, label) / 2;
197
+ else labelX = x2 + 4; // "right" — legacy gutter position
198
+
199
+ return {
200
+ visible: true,
201
+ y,
202
+ yBottom: y,
203
+ x1,
204
+ x2,
205
+ label,
206
+ labelX,
207
+ labelY: y - baselineOffset,
208
+ offAxis: false,
209
+ chevronUp: false,
210
+ };
211
+ });
212
+ }
@@ -0,0 +1,154 @@
1
+ import {
2
+ useAnimatedReaction,
3
+ useSharedValue,
4
+ type SharedValue,
5
+ } from "react-native-reanimated";
6
+ import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
7
+
8
+ const STASH_MORPH_EPS = 0.01;
9
+
10
+ /**
11
+ * While `morphT` animates down after data clears, keep feeding the engine the
12
+ * last stashed line/candle snapshot.
13
+ */
14
+ export function useSingleChartReverseMorphInputs({
15
+ isCandle,
16
+ data,
17
+ candles,
18
+ liveCandle,
19
+ hasData,
20
+ morphT,
21
+ }: {
22
+ isCandle: boolean;
23
+ data: SharedValue<LiveChartPoint[]>;
24
+ candles: SharedValue<CandlePoint[]> | undefined;
25
+ liveCandle: SharedValue<CandlePoint | null> | undefined;
26
+ hasData: SharedValue<boolean>;
27
+ morphT: SharedValue<number>;
28
+ }): {
29
+ lineEngineData: SharedValue<LiveChartPoint[]>;
30
+ candlesEngine: SharedValue<CandlePoint[]>;
31
+ liveEngine: SharedValue<CandlePoint | null>;
32
+ } {
33
+ const lineStash = useSharedValue<LiveChartPoint[]>([]);
34
+ const candleStash = useSharedValue<CandlePoint[]>([]);
35
+ const lineEngineData = useSharedValue<LiveChartPoint[]>([]);
36
+ const candlesEngine = useSharedValue<CandlePoint[]>([]);
37
+ const liveEngine = useSharedValue<CandlePoint | null>(null);
38
+
39
+ useAnimatedReaction(
40
+ () => ({
41
+ candle: isCandle,
42
+ has: hasData.get(),
43
+ m: morphT.get(),
44
+ d: data.get(),
45
+ cLen: candles?.get().length ?? 0,
46
+ c: candles?.get(),
47
+ lc: liveCandle?.get() ?? null,
48
+ }),
49
+ (curr) => {
50
+ "worklet";
51
+ if (!curr.candle) {
52
+ if (curr.has && curr.d.length >= 2) {
53
+ lineStash.set(curr.d.slice());
54
+ lineEngineData.set(curr.d);
55
+ } else if (
56
+ !curr.has &&
57
+ curr.m > STASH_MORPH_EPS &&
58
+ lineStash.get().length >= 2
59
+ ) {
60
+ lineEngineData.set(lineStash.get());
61
+ } else {
62
+ lineEngineData.set(curr.d);
63
+ }
64
+ return;
65
+ }
66
+
67
+ const cArr = curr.c;
68
+ if (curr.has && curr.cLen >= 2 && cArr) {
69
+ candleStash.set(cArr.slice());
70
+ candlesEngine.set(cArr);
71
+ liveEngine.set(curr.lc);
72
+ } else if (
73
+ !curr.has &&
74
+ curr.m > STASH_MORPH_EPS &&
75
+ candleStash.get().length >= 2
76
+ ) {
77
+ candlesEngine.set(candleStash.get());
78
+ liveEngine.set(null);
79
+ } else {
80
+ candlesEngine.set(cArr ?? []);
81
+ liveEngine.set(curr.lc);
82
+ }
83
+ },
84
+ [isCandle, data, candles, liveCandle, hasData, morphT],
85
+ );
86
+
87
+ return { lineEngineData, candlesEngine, liveEngine };
88
+ }
89
+
90
+ function snapshotSeriesWorklet(s: SeriesConfig[]): SeriesConfig[] {
91
+ "worklet";
92
+ const out: SeriesConfig[] = [];
93
+ for (let i = 0; i < s.length; i++) {
94
+ const x = s[i];
95
+ out.push({ ...x, data: x.data.slice() });
96
+ }
97
+ return out;
98
+ }
99
+
100
+ export function useMultiSeriesReverseMorphInputs({
101
+ series,
102
+ hasData,
103
+ morphT,
104
+ }: {
105
+ series: SharedValue<SeriesConfig[]>;
106
+ hasData: SharedValue<boolean>;
107
+ morphT: SharedValue<number>;
108
+ }): SharedValue<SeriesConfig[]> {
109
+ const seriesStash = useSharedValue<SeriesConfig[]>([]);
110
+ const effectiveSeries = useSharedValue<SeriesConfig[]>([]);
111
+
112
+ useAnimatedReaction(
113
+ () => ({
114
+ has: hasData.get(),
115
+ m: morphT.get(),
116
+ live: series.get(),
117
+ }),
118
+ (curr) => {
119
+ "worklet";
120
+ const live = curr.live;
121
+
122
+ let anyReady = false;
123
+ for (let i = 0; i < live.length; i++) {
124
+ if (live[i].data.length >= 2) {
125
+ anyReady = true;
126
+ break;
127
+ }
128
+ }
129
+
130
+ if (curr.has && anyReady) {
131
+ seriesStash.set(snapshotSeriesWorklet(live));
132
+ effectiveSeries.set(live);
133
+ } else {
134
+ let stashReady = false;
135
+ const stash = seriesStash.get();
136
+ for (let i = 0; i < stash.length; i++) {
137
+ if (stash[i].data.length >= 2) {
138
+ stashReady = true;
139
+ break;
140
+ }
141
+ }
142
+
143
+ if (!curr.has && curr.m > STASH_MORPH_EPS && stashReady) {
144
+ effectiveSeries.set(seriesStash.get());
145
+ } else {
146
+ effectiveSeries.set(live);
147
+ }
148
+ }
149
+ },
150
+ [series, hasData, morphT],
151
+ );
152
+
153
+ return effectiveSeries;
154
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ useFrameCallback,
3
+ useSharedValue,
4
+ type SharedValue,
5
+ } from "react-native-reanimated";
6
+ import { MS_PER_FRAME_60FPS } from "../constants";
7
+ import type { SingleEngineState } from "../core/useLiveChartEngine";
8
+ import type { ChartPadding } from "../draw/line";
9
+ import {
10
+ createTradeStreamState,
11
+ projectLabels,
12
+ tickTradeStream,
13
+ type TradeMarker,
14
+ type TradeStreamState,
15
+ } from "../draw/trade";
16
+ import type { TradeEvent } from "../types";
17
+
18
+ /**
19
+ * Process live trade events into projected on-chart markers.
20
+ * Runs a frame callback that ticks the trade-stream state and produces
21
+ * `TradeMarker[]` positioned within the chart area.
22
+ */
23
+ export function useTradeStream(
24
+ engine: SingleEngineState,
25
+ tradeStream: SharedValue<TradeEvent[]>,
26
+ padding: ChartPadding,
27
+ active: boolean,
28
+ ): SharedValue<TradeMarker[]> {
29
+ const markers = useSharedValue<TradeMarker[]>([]);
30
+ const state = useSharedValue<TradeStreamState>(createTradeStreamState());
31
+
32
+ useFrameCallback(
33
+ /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (
34
+ frameInfo,
35
+ ) => {
36
+ "worklet";
37
+ if (!active) {
38
+ if (markers.get().length > 0) markers.set([]);
39
+ return;
40
+ }
41
+
42
+ const dt = frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS;
43
+ const h = engine.canvasHeight.get();
44
+ const chartTop = padding.top;
45
+ const LABEL_CLEARANCE = 6;
46
+ const chartBottom = h - padding.bottom - LABEL_CLEARANCE;
47
+
48
+ if (chartBottom <= chartTop) return;
49
+
50
+ const s = state.get();
51
+ tickTradeStream(s, tradeStream.get(), dt, chartTop, chartBottom);
52
+
53
+ // Idle tape: nothing to draw and nothing already drawn. Skip the projection
54
+ // and SharedValue write so the TapeLabel derived values don't re-run and we
55
+ // don't allocate a fresh marker array every frame.
56
+ if (s.labels.length === 0 && markers.get().length === 0) return;
57
+
58
+ const chartH = chartBottom - chartTop;
59
+ markers.set(projectLabels(s, chartTop, chartH));
60
+ },
61
+ );
62
+
63
+ return markers;
64
+ }
@@ -0,0 +1,200 @@
1
+ import { useEffect } from "react";
2
+ import { useDerivedValue, useSharedValue } from "react-native-reanimated";
3
+
4
+ import type { SkFont } from "@shopify/react-native-skia";
5
+ import { MS_PER_FRAME_60FPS } from "../constants";
6
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
7
+ import type { ChartPadding } from "../draw/line";
8
+ import { measureFontTextWidth } from "../lib/measureFontTextWidth";
9
+ import { niceTimeInterval } from "../math/intervals";
10
+ import { lerp } from "../math/lerp";
11
+
12
+ export interface XAxisEntry {
13
+ x: number;
14
+ label: string;
15
+ alpha: number;
16
+ }
17
+
18
+ const FADE = 0.08;
19
+
20
+ /**
21
+ * Build a fresh label cache that re-formats every tick's `text` with
22
+ * `formatTime`, preserving each entry's `alpha`. The cache (see {@link useXAxis})
23
+ * formats each tick once for allocation reasons; this refreshes it when the
24
+ * `formatTime` prop changes. Returns a NEW object (and new entry objects) rather
25
+ * than mutating `cache` in place — the cache is also read inside the derived
26
+ * value worklet, and mutating an already-serialized object from the JS thread
27
+ * is unsafe under Reanimated's worklet model. Replacing the SharedValue with a
28
+ * fresh object propagates cleanly to the UI thread.
29
+ */
30
+ export function reformatXAxisLabels(
31
+ cache: Record<number, { alpha: number; text: string }>,
32
+ formatTime: (t: number) => string,
33
+ ): Record<number, { alpha: number; text: string }> {
34
+ const next: Record<number, { alpha: number; text: string }> = {};
35
+ for (const key in cache) {
36
+ next[key] = { alpha: cache[key].alpha, text: formatTime(Number(key) / 100) };
37
+ }
38
+ return next;
39
+ }
40
+
41
+ function xAxisKeyIsTarget(key: number, targetKeys: number[]): boolean {
42
+ "worklet";
43
+ for (let i = 0; i < targetKeys.length; i++) {
44
+ if (targetKeys[i] === key) return true;
45
+ }
46
+ return false;
47
+ }
48
+
49
+ /** Insertion sort — worklet-safe, no callback functions. */
50
+ export function insertionSortByX(arr: XAxisEntry[]) {
51
+ "worklet";
52
+ for (let i = 1; i < arr.length; i++) {
53
+ const item = arr[i];
54
+ let j = i - 1;
55
+ while (j >= 0 && arr[j].x > item.x) {
56
+ /* istanbul ignore next -- worklet instrumentation under-counts swaps; insertionSort.test.ts covers behaviour */
57
+ arr[j + 1] = arr[j];
58
+ /* istanbul ignore next */
59
+ j--;
60
+ }
61
+ arr[j + 1] = item;
62
+ }
63
+ }
64
+
65
+ export function useXAxis(
66
+ engine: ChartEngineLayout,
67
+ padding: ChartPadding,
68
+ formatTime: (t: number) => string,
69
+ font: SkFont,
70
+ ) {
71
+ const labelAlphas = useSharedValue<
72
+ Record<number, { alpha: number; text: string }>
73
+ >({});
74
+
75
+ // A tick's key encodes its time, so its label is formatted once (below) and
76
+ // never re-formatted per frame — an allocation optimization. That assumes
77
+ // `formatTime` is stable; if the consumer swaps it at runtime, already-cached
78
+ // labels would otherwise keep their old text until they scroll off-window.
79
+ // Rebuild the cache with the new formatter (alphas preserved, so no fade
80
+ // restart) whenever the formatter identity changes.
81
+ useEffect(() => {
82
+ labelAlphas.set(reformatXAxisLabels(labelAlphas.get(), formatTime));
83
+ }, [formatTime, labelAlphas]);
84
+
85
+ const xAxisEntries = useDerivedValue(() => {
86
+ const w = engine.canvasWidth.get();
87
+ const h = engine.canvasHeight.get();
88
+ if (w === 0 || h === 0) return [] as XAxisEntry[];
89
+
90
+ const now = engine.timestamp.get();
91
+ const windowSecs = engine.displayWindow.get();
92
+ const winStart = now - windowSecs;
93
+
94
+ const chartLeft = padding.left;
95
+ const chartRight = w - padding.right;
96
+ const chartW = chartRight - chartLeft;
97
+ /* istanbul ignore next -- defensive; Reanimated test mock rarely executes this guard with non-zero w/h */
98
+ if (chartW <= 0) return [] as XAxisEntry[];
99
+
100
+ const fadeZone = 50;
101
+
102
+ // Pick interval from window duration
103
+ const targetPxPerSec = chartW / windowSecs;
104
+ let interval = niceTimeInterval(windowSecs);
105
+ while (interval * targetPxPerSec < 60 && interval < windowSecs) {
106
+ interval *= 2;
107
+ }
108
+
109
+ // Generate target label keys (plain array, no Set — worklet-safe)
110
+ const firstTime = Math.ceil((winStart - interval) / interval) * interval;
111
+ const targetKeys: number[] = [];
112
+ for (
113
+ let t = firstTime;
114
+ t <= now + interval && targetKeys.length < 30;
115
+ t += interval
116
+ ) {
117
+ targetKeys.push(Math.round(t * 100));
118
+ }
119
+
120
+ const alphas = labelAlphas.get();
121
+
122
+ // Create labels for new keys only. A key encodes its time, so the formatted
123
+ // text never changes — re-formatting (and re-allocating the entry) every
124
+ // frame just churns strings/objects for the GC.
125
+ for (let i = 0; i < targetKeys.length; i++) {
126
+ const key = targetKeys[i];
127
+ if (!alphas[key]) {
128
+ alphas[key] = { alpha: 0, text: formatTime(key / 100) };
129
+ }
130
+ }
131
+
132
+ // Update alphas
133
+ const allKeys = Object.keys(alphas);
134
+ for (let i = 0; i < allKeys.length; i++) {
135
+ const key = Number(allKeys[i]);
136
+ const label = alphas[key];
137
+ const x = chartLeft + ((key / 100 - winStart) / windowSecs) * chartW;
138
+
139
+ const fromLeft = x - chartLeft;
140
+ const fromRight = chartRight - x;
141
+ const fromEdge = Math.min(fromLeft, fromRight);
142
+ const edgeAlpha =
143
+ fromEdge >= fadeZone ? 1 : fromEdge <= 0 ? 0 : fromEdge / fadeZone;
144
+
145
+ const target = xAxisKeyIsTarget(key, targetKeys) ? edgeAlpha : 0;
146
+ let next = lerp(label.alpha, target, FADE, MS_PER_FRAME_60FPS);
147
+ if (Math.abs(next - target) < 0.02) next = target;
148
+
149
+ if (next < 0.01 && target === 0) {
150
+ delete alphas[key];
151
+ } else {
152
+ // Mutate in place — text is stable, so no need to reallocate the entry.
153
+ label.alpha = next;
154
+ }
155
+ }
156
+
157
+ labelAlphas.set(alphas);
158
+
159
+ // Collect visible labels
160
+ const raw: XAxisEntry[] = [];
161
+ const visibleKeys = Object.keys(alphas);
162
+ for (let i = 0; i < visibleKeys.length; i++) {
163
+ const key = Number(visibleKeys[i]);
164
+ const label = alphas[key];
165
+ /* istanbul ignore next -- alpha band skip is timing-sensitive under RTL + Reanimated mocks */
166
+ if (label.alpha < 0.02) continue;
167
+ const x = chartLeft + ((key / 100 - winStart) / windowSecs) * chartW;
168
+ /* istanbul ignore next -- horizontal clip; x stays in-band for consistent time keys */
169
+ if (x < chartLeft - 20 || x > chartRight) continue;
170
+ raw.push({ x, label: label.text, alpha: label.alpha });
171
+ }
172
+
173
+ // Worklet-safe sort (no callback)
174
+ insertionSortByX(raw);
175
+
176
+ // Overlap resolution: keep higher-alpha label when two collide
177
+ const drawn: XAxisEntry[] = [];
178
+ for (let i = 0; i < raw.length; i++) {
179
+ const entry = raw[i];
180
+ const textW = measureFontTextWidth(font, entry.label);
181
+ const left = entry.x - textW / 2;
182
+ if (drawn.length > 0) {
183
+ const prev = drawn[drawn.length - 1];
184
+ const prevW = measureFontTextWidth(font, prev.label);
185
+ const prevRight = prev.x + prevW / 2;
186
+ if (left < prevRight + 8) {
187
+ if (entry.alpha > prev.alpha) {
188
+ drawn[drawn.length - 1] = entry;
189
+ }
190
+ continue;
191
+ }
192
+ }
193
+ drawn.push(entry);
194
+ }
195
+
196
+ return drawn;
197
+ });
198
+
199
+ return { xAxisEntries, font };
200
+ }
@@ -0,0 +1,48 @@
1
+ import { useDerivedValue, useSharedValue } from "react-native-reanimated";
2
+
3
+ import type { SkFont } from "@shopify/react-native-skia";
4
+ import { MS_PER_FRAME_60FPS } from "../constants";
5
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
6
+ import { computeGridEntries } from "../draw/grid";
7
+ import type { ChartPadding } from "../draw/line";
8
+
9
+ /**
10
+ * Compute Y-axis grid entries (values + labels) with animated fade-in/out.
11
+ * Uses `computeGridEntries` to pick nice intervals and track label alpha for
12
+ * smooth transitions when the value range changes.
13
+ */
14
+ export function useYAxis(
15
+ engine: ChartEngineLayout,
16
+ padding: ChartPadding,
17
+ formatValue: (v: number) => string,
18
+ font: SkFont,
19
+ minGap = 36,
20
+ ) {
21
+ const prevInterval = useSharedValue(0);
22
+ const labelAlphas = useSharedValue<Record<number, number>>({});
23
+
24
+ const yAxisEntries = useDerivedValue(() => {
25
+ const dt = MS_PER_FRAME_60FPS;
26
+
27
+ const alphas = labelAlphas.get();
28
+ const result = computeGridEntries(
29
+ engine.displayMin.get(),
30
+ engine.displayMax.get(),
31
+ engine.canvasHeight.get(),
32
+ padding.top,
33
+ padding.bottom,
34
+ prevInterval.get(),
35
+ alphas,
36
+ formatValue,
37
+ dt,
38
+ minGap,
39
+ );
40
+
41
+ prevInterval.set(result.interval);
42
+ labelAlphas.set(alphas);
43
+
44
+ return result.entries;
45
+ });
46
+
47
+ return { yAxisEntries, font };
48
+ }