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,821 @@
1
+ import { useLayoutEffect, useState } from "react";
2
+ import { View } from "react-native";
3
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
+ import { useSharedValue } from "react-native-reanimated";
5
+
6
+ /**
7
+ * Single-series live chart. UX and prop vocabulary parallel Benji Taylor’s
8
+ * `liveline` for React; implemented here with Skia, Reanimated, and Gesture Handler.
9
+ *
10
+ * @see https://github.com/benjitaylor/liveline
11
+ */
12
+ import {
13
+ Canvas,
14
+ Group,
15
+ LinearGradient,
16
+ Path,
17
+ vec,
18
+ } from "@shopify/react-native-skia";
19
+
20
+ import { DEFAULT_ACCENT_COLOR } from "../constants";
21
+ import {
22
+ resolveBadge,
23
+ resolveDegen,
24
+ resolveGradient,
25
+ resolveGridStyle,
26
+ resolveLeftEdgeFade,
27
+ resolvePulse,
28
+ resolveScrub,
29
+ resolveTradeStream,
30
+ resolveValueLine,
31
+ resolveXAxis,
32
+ resolveYAxis,
33
+ } from "../core/resolveConfig";
34
+ import { useLiveChartEngine } from "../core/useLiveChartEngine";
35
+ import { resolveChartLayout } from "../hooks/resolveChartLayout";
36
+ import { useBadge } from "../hooks/useBadge";
37
+ import { useCandlePaths } from "../hooks/useCandlePaths";
38
+ import { useCanvasLayout } from "../hooks/useCanvasLayout";
39
+ import { useChartColors } from "../hooks/useChartColors";
40
+ import { useChartPaths } from "../hooks/useChartPaths";
41
+ import { useChartReveal } from "../hooks/useChartReveal";
42
+ import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
43
+ import { useCrosshair } from "../hooks/useCrosshair";
44
+ import { useDegen } from "../hooks/useDegen";
45
+ import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
46
+ import { useLiveDot } from "../hooks/useLiveDot";
47
+ import { useMarkers } from "../hooks/useMarkers";
48
+ import { useModeBlend } from "../hooks/useModeBlend";
49
+ import { useMomentum } from "../hooks/useMomentum";
50
+ import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
51
+ import { useTradeStream } from "../hooks/useTradeStream";
52
+ import { useXAxis } from "../hooks/useXAxis";
53
+ import { useYAxis } from "../hooks/useYAxis";
54
+ import {
55
+ formatTime as defaultFormatTime,
56
+ formatValue as defaultFormatValue,
57
+ } from "../lib/format";
58
+ import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
59
+ import { collectReferenceValues } from "../math/referenceLines";
60
+ import {
61
+ applyPaletteOverride,
62
+ leftEdgeFadeColorsFromBgRgb,
63
+ resolveTheme,
64
+ } from "../theme";
65
+ import type { LiveChartProps, Marker, TradeEvent } from "../types";
66
+ import { BadgeOverlay } from "./BadgeOverlay";
67
+ import { CrosshairOverlay } from "./CrosshairOverlay";
68
+ import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
69
+ import { DotOverlay } from "./DotOverlay";
70
+ import { LeftEdgeFade } from "./LeftEdgeFade";
71
+ import { LoadingOverlay } from "./LoadingOverlay";
72
+ import { MarkerOverlay } from "./MarkerOverlay";
73
+ import { MultiSeriesTooltipStack } from "./MultiSeriesTooltipStack";
74
+ import { ValueTextOverlay } from "./ValueTextOverlay";
75
+ import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
76
+ import { TradeStreamOverlay } from "./TradeStreamOverlay";
77
+ import { ValueLineOverlay } from "./ValueLineOverlay";
78
+ import { XAxisOverlay } from "./XAxisOverlay";
79
+ import { YAxisOverlay } from "./YAxisOverlay";
80
+
81
+ /**
82
+ * Resolves props → configs → theme/layout → engine → per-frame derived values and
83
+ * overlay hooks, returning a single render model. All the chart's wiring lives
84
+ * here so the rendered pieces (`ChartStack`, `ChartScrubLayer`, `LiveChart`) stay
85
+ * small and presentational.
86
+ */
87
+ function useLiveChartController({
88
+ // ── Data ────────────────────────────────────────────────────────────────
89
+ data,
90
+ value,
91
+
92
+ // ── Appearance ──────────────────────────────────────────────────────────
93
+ theme = "dark",
94
+ accentColor = DEFAULT_ACCENT_COLOR,
95
+ gradient = true,
96
+ line: lineProp,
97
+ font: fontProp,
98
+ insets,
99
+ style,
100
+
101
+ // ── Candlestick ─────────────────────────────────────────────────────────
102
+ mode = "line",
103
+ candles,
104
+ candleWidth = 60,
105
+ liveCandle,
106
+
107
+ // ── Behaviour ───────────────────────────────────────────────────────────
108
+ timeWindow = 30,
109
+ paused = false,
110
+ loading = false,
111
+ smoothing = 0.08,
112
+ exaggerate = false,
113
+ nonNegative = false,
114
+ maxValue,
115
+ windowBuffer = 0,
116
+ nowOverride,
117
+ accessibilityLabel,
118
+ accessibilityRole = "image",
119
+ emptyText = "No data",
120
+ formatValue = defaultFormatValue,
121
+ formatTime = defaultFormatTime,
122
+
123
+ // ── Overlays ────────────────────────────────────────────────────────────
124
+ yAxis = true,
125
+ xAxis = true,
126
+ badge = true,
127
+ momentum = true,
128
+ pulse = true,
129
+ valueLine = true,
130
+ showValue = false,
131
+ valueMomentumColor = false,
132
+ referenceLines,
133
+ gridStyle,
134
+ palette: paletteOverride,
135
+ scrub = true,
136
+ tradeStream,
137
+ degen,
138
+ markers,
139
+ onMarkerHover,
140
+ markerHitRadius = 16,
141
+ leftEdgeFade = true,
142
+
143
+ // ── Callbacks ───────────────────────────────────────────────────────────
144
+ onScrub,
145
+ onDegenShake,
146
+ }: LiveChartProps) {
147
+ const emptyTradeStream = useSharedValue<TradeEvent[]>([]);
148
+ const tradeStreamSV = tradeStream ?? emptyTradeStream;
149
+ const emptyMarkers = useSharedValue<Marker[]>([]);
150
+ const markersSV = markers ?? emptyMarkers;
151
+ const isCandle = mode === "candle";
152
+
153
+ // ── Resolve feature configs ────────────────────────────────────────────
154
+ const yAxisCfg = resolveYAxis(yAxis);
155
+ const xAxisCfg = resolveXAxis(xAxis);
156
+ const badgeCfg = resolveBadge(badge);
157
+ const scrubCfg = resolveScrub(scrub);
158
+ const gradientCfg = isCandle ? null : resolveGradient(gradient);
159
+ const valueLineCfg = resolveValueLine(valueLine);
160
+ const pulseCfg = resolvePulse(pulse);
161
+ const gridStyleCfg = resolveGridStyle(gridStyle);
162
+ const degenCfg = resolveDegen(degen);
163
+ const tradeStreamResolved = resolveTradeStream(tradeStream);
164
+
165
+ const allRefLines = referenceLines ?? [];
166
+ const refValues = collectReferenceValues(allRefLines);
167
+
168
+ const badgeUsesRightGutter =
169
+ badgeCfg !== null && (badgeCfg.position ?? "right") === "right";
170
+
171
+ // ── Theme, font and layout ─────────────────────────────────────────────
172
+ const palette = applyPaletteOverride(
173
+ resolveTheme(accentColor, theme),
174
+ paletteOverride,
175
+ );
176
+
177
+ const leftEdgeFadeCfg = resolveLeftEdgeFade(
178
+ leftEdgeFade,
179
+ leftEdgeFadeColorsFromBgRgb(palette.bgRgb),
180
+ );
181
+
182
+ const skiaFont = useChartSkiaFont(
183
+ fontProp,
184
+ MONO_FONT_FAMILY,
185
+ palette.labelFontSize,
186
+ );
187
+
188
+ // Larger font for the optional live-value text overlay (showValue).
189
+ const valueFont = useChartSkiaFont(
190
+ fontProp,
191
+ MONO_FONT_FAMILY,
192
+ palette.valueFontSize * 2,
193
+ );
194
+
195
+ const pulseConfig = pulseCfg
196
+ ? {
197
+ maxRadius: pulseCfg.maxRadius,
198
+ strokeWidth: pulseCfg.strokeWidth,
199
+ }
200
+ : null;
201
+
202
+ // Snapshot the live value off the render path to size the right gutter to the
203
+ // value label. Reading a SharedValue during render trips Reanimated's
204
+ // strict-mode warning, and the gutter only needs a representative magnitude —
205
+ // so read it in a layout effect (re-measures before paint, no visible reflow)
206
+ // once on mount, re-synced if the `value` SharedValue identity changes.
207
+ // react-doctor's "derive during render" fix is exactly what Reanimated forbids
208
+ // here, so suppress its effect-read rules at this seed.
209
+ const [valueLayoutSample, setValueLayoutSample] = useState<
210
+ number | undefined
211
+ >(undefined);
212
+ // react-doctor-disable-next-line react-doctor/no-derived-state-effect -- Reanimated: must read the SharedValue off the render path
213
+ useLayoutEffect(() => {
214
+ // react-doctor-disable-next-line react-hooks-js/set-state-in-effect -- Reanimated: seeding from a SharedValue off render is the warning-free path
215
+ setValueLayoutSample(value.get());
216
+ }, [value]);
217
+
218
+ const { strokeWidth, padding: effectivePadding } = resolveChartLayout({
219
+ palette,
220
+ lineWidthOverride: lineProp?.width,
221
+ insetsOverride: insets,
222
+ yAxis: yAxisCfg !== null,
223
+ badge: badgeCfg !== null,
224
+ badgeUsesRightGutter,
225
+ badgeShowTail: badgeCfg?.tail ?? true,
226
+ xAxis: xAxisCfg !== null,
227
+ font: skiaFont,
228
+ formatValue,
229
+ currentValue: valueLayoutSample,
230
+ pulse: pulseConfig,
231
+ });
232
+
233
+ // ── Reveal state ────────────────────────────────────────────
234
+ // ≥2 line points or ≥2 committed candles; morphT=1 only when !loading && hasData.
235
+ const { hasData } = useLiveChartHasData({
236
+ isCandle,
237
+ data,
238
+ candles,
239
+ });
240
+
241
+ const reveal = useChartReveal(loading, hasData);
242
+
243
+ // After data clears, keep last snapshot until morphT finishes dropping (web parity).
244
+ const { lineEngineData, candlesEngine, liveEngine } =
245
+ useSingleChartReverseMorphInputs({
246
+ isCandle,
247
+ data,
248
+ candles,
249
+ liveCandle,
250
+ hasData,
251
+ morphT: reveal.morphT,
252
+ });
253
+
254
+ // ── Engine ─────────────────────────────────────────────────────────────
255
+ // Line mode: tick + paths use `lineEngineData` (stash when reversing). Candle mode:
256
+ // parent `data` stays tick/line-morph input; OHLC uses candlesEngine + liveEngine.
257
+ const engine = useLiveChartEngine({
258
+ data: isCandle ? data : lineEngineData,
259
+ value,
260
+ timeWindow,
261
+ paused,
262
+ smoothing,
263
+ exaggerate,
264
+ referenceValues: refValues,
265
+ nonNegative,
266
+ maxValue,
267
+ windowBuffer,
268
+ nowOverride,
269
+ mode,
270
+ candles: isCandle ? candlesEngine : candles,
271
+ liveCandle: isCandle ? liveEngine : liveCandle,
272
+ });
273
+
274
+ // ── Mode crossfade (line ↔ candle) ──────────────────────────────────
275
+ const { lineGroupOpacity, candleGroupOpacity } = useModeBlend(
276
+ isCandle,
277
+ reveal.lineOpacity,
278
+ );
279
+
280
+ // ── Per-frame derived values ───────────────────────────────────────────
281
+ const { layoutHeight, onLayout } = useCanvasLayout(engine);
282
+
283
+ const { linePath, fillPath } = useChartPaths(
284
+ engine,
285
+ effectivePadding,
286
+ reveal.morphT,
287
+ );
288
+ const { upBodiesPath, downBodiesPath, upWicksPath, downWicksPath } =
289
+ useCandlePaths(
290
+ engine,
291
+ effectivePadding,
292
+ // Match engine: stashed candles while reverse-morphing in candle mode.
293
+ isCandle ? candlesEngine : candles,
294
+ isCandle ? liveEngine : liveCandle,
295
+ candleWidth,
296
+ isCandle,
297
+ );
298
+ const { dotX, dotY } = useLiveDot(engine, effectivePadding);
299
+
300
+ const momentumSV = useMomentum(engine, momentum);
301
+
302
+ const tradeMarkers = useTradeStream(
303
+ engine,
304
+ tradeStreamSV,
305
+ effectivePadding,
306
+ tradeStreamResolved !== null,
307
+ );
308
+
309
+ const {
310
+ pack: degenPack,
311
+ packRevision: degenPackRevision,
312
+ shakeTransform: degenShakeTransform,
313
+ } = useDegen(engine, dotX, dotY, momentumSV, degenCfg, onDegenShake);
314
+
315
+ // ── Overlay hooks ─────────────────────────────────────────────────────
316
+ const { yAxisEntries } = useYAxis(
317
+ engine,
318
+ effectivePadding,
319
+ formatValue,
320
+ skiaFont,
321
+ yAxisCfg?.minGap ?? 36,
322
+ );
323
+
324
+ const { xAxisEntries } = useXAxis(
325
+ engine,
326
+ effectivePadding,
327
+ formatTime,
328
+ skiaFont,
329
+ );
330
+
331
+ const badgeData = useBadge(
332
+ engine,
333
+ effectivePadding,
334
+ palette,
335
+ formatValue,
336
+ skiaFont,
337
+ badgeCfg?.variant ?? "default",
338
+ badgeCfg?.tail ?? true,
339
+ momentumSV,
340
+ badgeCfg?.position ?? "right",
341
+ badgeCfg?.background,
342
+ );
343
+
344
+ // Scrub/crosshair must see the same stash-backed candles as the engine.
345
+ const candleOpts = isCandle
346
+ ? {
347
+ mode,
348
+ candles: candlesEngine,
349
+ liveCandle: liveEngine,
350
+ candleWidthSecs: candleWidth,
351
+ }
352
+ : undefined;
353
+
354
+ const crosshair = useCrosshair(
355
+ engine,
356
+ effectivePadding,
357
+ palette,
358
+ formatValue,
359
+ formatTime,
360
+ skiaFont,
361
+ scrubCfg !== null,
362
+ onScrub,
363
+ candleOpts,
364
+ );
365
+
366
+ const markersActive = markers != null;
367
+ // `projected` is used internally by the hit-test gesture; the overlay
368
+ // self-projects, so we only need the gesture here.
369
+ const { tapGesture: markerTapGesture } = useMarkers(
370
+ engine,
371
+ effectivePadding,
372
+ markersSV,
373
+ markersActive,
374
+ markerHitRadius,
375
+ onMarkerHover,
376
+ undefined, // seriesSV — single-series has none
377
+ engine.data, // anchor value-less markers to the line
378
+ );
379
+
380
+ const rootGesture = markersActive
381
+ ? Gesture.Race(crosshair.gesture, markerTapGesture)
382
+ : crosshair.gesture;
383
+
384
+ // ── Derived render values ──────────────────────────────────────────────
385
+ const {
386
+ backgroundColor,
387
+ gradientEnd,
388
+ gradientTopColor,
389
+ gradientBottomColor,
390
+ } = useChartColors(
391
+ palette,
392
+ gradientCfg,
393
+ accentColor,
394
+ layoutHeight,
395
+ effectivePadding,
396
+ );
397
+
398
+ return {
399
+ // passthrough props the render needs
400
+ style,
401
+ accessibilityLabel,
402
+ accessibilityRole,
403
+ emptyText,
404
+ showValue,
405
+ valueMomentumColor,
406
+ lineProp,
407
+ formatValue,
408
+ isCandle,
409
+ // configs
410
+ yAxisCfg,
411
+ xAxisCfg,
412
+ badgeCfg,
413
+ scrubCfg,
414
+ gradientCfg,
415
+ valueLineCfg,
416
+ pulseCfg,
417
+ gridStyleCfg,
418
+ degenCfg,
419
+ tradeStreamResolved,
420
+ leftEdgeFadeCfg,
421
+ allRefLines,
422
+ badgeUsesRightGutter,
423
+ // theme / layout / fonts
424
+ palette,
425
+ skiaFont,
426
+ valueFont,
427
+ strokeWidth,
428
+ effectivePadding,
429
+ // engine + reveal
430
+ engine,
431
+ reveal,
432
+ // derived render values
433
+ backgroundColor,
434
+ gradientEnd,
435
+ gradientTopColor,
436
+ gradientBottomColor,
437
+ lineGroupOpacity,
438
+ candleGroupOpacity,
439
+ onLayout,
440
+ linePath,
441
+ fillPath,
442
+ upBodiesPath,
443
+ downBodiesPath,
444
+ upWicksPath,
445
+ downWicksPath,
446
+ dotX,
447
+ dotY,
448
+ momentumSV,
449
+ tradeMarkers,
450
+ degenPack,
451
+ degenPackRevision,
452
+ degenShakeTransform,
453
+ yAxisEntries,
454
+ xAxisEntries,
455
+ badgeData,
456
+ crosshair,
457
+ rootGesture,
458
+ markersActive,
459
+ markersSV,
460
+ };
461
+ }
462
+
463
+ type LiveChartModel = ReturnType<typeof useLiveChartController>;
464
+
465
+ /** Main shaken chart stack: grid, fill, line/candles, axes, badge, dot, degen,
466
+ * markers, and the loading/empty art. The live value text is a separate layer
467
+ * (ChartValueOverlay) so it sits above the left-edge fade. */
468
+ function ChartStack({ model }: { model: LiveChartModel }) {
469
+ const {
470
+ degenShakeTransform,
471
+ yAxisCfg,
472
+ reveal,
473
+ yAxisEntries,
474
+ engine,
475
+ effectivePadding,
476
+ palette,
477
+ skiaFont,
478
+ badgeUsesRightGutter,
479
+ badgeCfg,
480
+ gridStyleCfg,
481
+ gradientCfg,
482
+ fillPath,
483
+ gradientEnd,
484
+ gradientTopColor,
485
+ gradientBottomColor,
486
+ valueLineCfg,
487
+ dotY,
488
+ allRefLines,
489
+ formatValue,
490
+ lineGroupOpacity,
491
+ linePath,
492
+ strokeWidth,
493
+ lineProp,
494
+ candleGroupOpacity,
495
+ upWicksPath,
496
+ downWicksPath,
497
+ upBodiesPath,
498
+ downBodiesPath,
499
+ xAxisCfg,
500
+ xAxisEntries,
501
+ badgeData,
502
+ dotX,
503
+ pulseCfg,
504
+ degenCfg,
505
+ degenPack,
506
+ degenPackRevision,
507
+ markersActive,
508
+ markersSV,
509
+ emptyText,
510
+ } = model;
511
+
512
+ return (
513
+ <Group transform={degenShakeTransform}>
514
+ {/* Y-axis grid */}
515
+ {yAxisCfg && (
516
+ <Group opacity={reveal.yAxisOpacity}>
517
+ <YAxisOverlay
518
+ entries={yAxisEntries}
519
+ engine={engine}
520
+ padding={effectivePadding}
521
+ palette={palette}
522
+ font={skiaFont}
523
+ badge={badgeUsesRightGutter}
524
+ badgeTail={badgeCfg?.tail ?? true}
525
+ gridStyle={gridStyleCfg}
526
+ />
527
+ </Group>
528
+ )}
529
+
530
+ {/* Area gradient fill */}
531
+ {gradientCfg && (
532
+ <Group opacity={reveal.fillOpacity}>
533
+ <Path path={fillPath} style="fill">
534
+ <LinearGradient
535
+ start={vec(0, effectivePadding.top)}
536
+ end={vec(0, gradientEnd)}
537
+ colors={[gradientTopColor, gradientBottomColor]}
538
+ />
539
+ </Path>
540
+ </Group>
541
+ )}
542
+
543
+ {/* Value line + reference line (behind chart line) */}
544
+ {valueLineCfg && (
545
+ <Group opacity={reveal.lineOpacity}>
546
+ <ValueLineOverlay
547
+ dotY={dotY}
548
+ engine={engine}
549
+ padding={effectivePadding}
550
+ strokeWidth={valueLineCfg.strokeWidth}
551
+ intervals={valueLineCfg.intervals}
552
+ color={valueLineCfg.color ?? palette.dashLine}
553
+ />
554
+ </Group>
555
+ )}
556
+
557
+ {allRefLines.map((rl) => (
558
+ <ReferenceLineOverlay
559
+ key={`${rl.value ?? ""}:${rl.valueFrom ?? ""}:${rl.valueTo ?? ""}:${rl.from ?? ""}:${rl.to ?? ""}:${rl.label ?? ""}`}
560
+ engine={engine}
561
+ padding={effectivePadding}
562
+ line={rl}
563
+ palette={palette}
564
+ formatValue={formatValue}
565
+ font={skiaFont}
566
+ />
567
+ ))}
568
+
569
+ {/* Chart line (fades out in candle mode) */}
570
+ <Group opacity={lineGroupOpacity}>
571
+ <Path
572
+ path={linePath}
573
+ style="stroke"
574
+ strokeWidth={strokeWidth}
575
+ color={lineProp?.color ?? palette.line}
576
+ strokeCap="round"
577
+ strokeJoin="round"
578
+ />
579
+ </Group>
580
+
581
+ {/* Candle bodies/wicks (fades in in candle mode) */}
582
+ <Group opacity={candleGroupOpacity}>
583
+ <Path
584
+ path={upWicksPath}
585
+ style="stroke"
586
+ strokeWidth={1}
587
+ color={palette.wickUp}
588
+ />
589
+ <Path
590
+ path={downWicksPath}
591
+ style="stroke"
592
+ strokeWidth={1}
593
+ color={palette.wickDown}
594
+ />
595
+ <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
596
+ <Path
597
+ path={downBodiesPath}
598
+ style="fill"
599
+ color={palette.candleDown}
600
+ />
601
+ </Group>
602
+
603
+ {/* X-axis time labels */}
604
+ {xAxisCfg && (
605
+ <XAxisOverlay
606
+ entries={xAxisEntries}
607
+ engine={engine}
608
+ padding={effectivePadding}
609
+ palette={palette}
610
+ font={skiaFont}
611
+ />
612
+ )}
613
+
614
+ {/* Badge and live dot */}
615
+ {badgeCfg && (
616
+ <Group opacity={reveal.badgeOpacity}>
617
+ <BadgeOverlay badge={badgeData} font={skiaFont} />
618
+ </Group>
619
+ )}
620
+
621
+ <Group opacity={reveal.dotOpacity}>
622
+ <DotOverlay
623
+ dotX={dotX}
624
+ dotY={dotY}
625
+ palette={palette}
626
+ engine={engine}
627
+ pulse={pulseCfg}
628
+ />
629
+ </Group>
630
+
631
+ {degenCfg && (
632
+ <Group opacity={reveal.dotOpacity}>
633
+ <DegenParticlesOverlay
634
+ pack={degenPack}
635
+ packRevision={degenPackRevision}
636
+ engine={engine}
637
+ palette={palette}
638
+ particleSlotCount={degenCfg.particleSlotCount}
639
+ particleBurstDurationSec={degenCfg.particleBurstDurationSec}
640
+ particleOpacity={degenCfg.particleOpacity}
641
+ colors={degenCfg.colors}
642
+ />
643
+ </Group>
644
+ )}
645
+
646
+ {markersActive && (
647
+ <Group opacity={reveal.dotOpacity}>
648
+ <MarkerOverlay
649
+ markers={markersSV}
650
+ engine={engine}
651
+ padding={effectivePadding}
652
+ palette={palette}
653
+ font={skiaFont}
654
+ lineData={engine.data}
655
+ />
656
+ </Group>
657
+ )}
658
+
659
+ {/* Loading / empty state — before left-edge fade so the squiggle/empty art fades like the chart */}
660
+ <LoadingOverlay
661
+ engine={engine}
662
+ padding={effectivePadding}
663
+ palette={palette}
664
+ font={skiaFont}
665
+ morphT={reveal.morphT}
666
+ isLoading={reveal.isLoading}
667
+ isEmpty={reveal.isEmpty}
668
+ emptyText={emptyText}
669
+ strokeWidth={strokeWidth}
670
+ badge={badgeCfg !== null}
671
+ badgeTail={badgeCfg?.tail ?? true}
672
+ />
673
+ </Group>
674
+ );
675
+ }
676
+
677
+ /** Trade-tape labels and the scrub crosshair/tooltip (drawn in canvas space, on
678
+ * top of the shaken stack). */
679
+ function ChartScrubLayer({ model }: { model: LiveChartModel }) {
680
+ const {
681
+ tradeStreamResolved,
682
+ scrubCfg,
683
+ degenShakeTransform,
684
+ tradeMarkers,
685
+ palette,
686
+ effectivePadding,
687
+ skiaFont,
688
+ reveal,
689
+ crosshair,
690
+ isCandle,
691
+ } = model;
692
+
693
+ if (!tradeStreamResolved && !scrubCfg) return null;
694
+
695
+ return (
696
+ <Group transform={degenShakeTransform}>
697
+ {tradeStreamResolved && (
698
+ <TradeStreamOverlay
699
+ markers={tradeMarkers}
700
+ palette={palette}
701
+ padding={effectivePadding}
702
+ font={skiaFont}
703
+ opacity={reveal.dotOpacity}
704
+ labelOffsetX={tradeStreamResolved.labelOffsetX}
705
+ />
706
+ )}
707
+
708
+ {scrubCfg && (
709
+ <CrosshairOverlay
710
+ scrubX={crosshair.scrubX}
711
+ crosshairOpacity={crosshair.crosshairOpacity}
712
+ tooltipLayout={crosshair.tooltipLayout}
713
+ engine={model.engine}
714
+ padding={effectivePadding}
715
+ palette={palette}
716
+ font={skiaFont}
717
+ showTooltip={scrubCfg.tooltip}
718
+ dimOpacity={scrubCfg.dimOpacity}
719
+ crosshairLineColor={scrubCfg.crosshairLineColor}
720
+ crosshairDimColor={scrubCfg.crosshairDimColor}
721
+ tooltipBackground={scrubCfg.tooltipBackground}
722
+ tooltipColor={scrubCfg.tooltipColor}
723
+ tooltipBorderColor={scrubCfg.tooltipBorderColor}
724
+ >
725
+ {/* Candle charts render a multi-line OHLC tooltip; the line
726
+ chart falls back to CrosshairOverlay's default value/time
727
+ body. Composed as children rather than a JSX-valued prop. */}
728
+ {isCandle ? (
729
+ <MultiSeriesTooltipStack
730
+ tooltipLayout={crosshair.tooltipLayout}
731
+ font={skiaFont}
732
+ palette={palette}
733
+ />
734
+ ) : null}
735
+ </CrosshairOverlay>
736
+ )}
737
+ </Group>
738
+ );
739
+ }
740
+
741
+ /** Live-value text drawn as its own canvas layer, above both the area gradient
742
+ * and the left-edge fade, so the large number stays crisp at the left edge
743
+ * instead of being washed out by the fade's `dstOut` blend. */
744
+ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
745
+ const {
746
+ showValue,
747
+ degenShakeTransform,
748
+ engine,
749
+ effectivePadding,
750
+ palette,
751
+ valueFont,
752
+ formatValue,
753
+ momentumSV,
754
+ valueMomentumColor,
755
+ reveal,
756
+ } = model;
757
+
758
+ if (!showValue) return null;
759
+
760
+ return (
761
+ <Group transform={degenShakeTransform}>
762
+ <Group opacity={reveal.lineOpacity}>
763
+ <ValueTextOverlay
764
+ engine={engine}
765
+ padding={effectivePadding}
766
+ palette={palette}
767
+ font={valueFont}
768
+ formatValue={formatValue}
769
+ momentum={momentumSV}
770
+ momentumColor={valueMomentumColor}
771
+ />
772
+ </Group>
773
+ </Group>
774
+ );
775
+ }
776
+
777
+ export function LiveChart(props: LiveChartProps) {
778
+ const model = useLiveChartController(props);
779
+ const {
780
+ rootGesture,
781
+ backgroundColor,
782
+ style,
783
+ onLayout,
784
+ accessibilityLabel,
785
+ accessibilityRole,
786
+ leftEdgeFadeCfg,
787
+ effectivePadding,
788
+ engine,
789
+ } = model;
790
+
791
+ return (
792
+ <GestureDetector gesture={rootGesture}>
793
+ <View
794
+ style={[{ flex: 1, backgroundColor }, style]}
795
+ onLayout={onLayout}
796
+ accessible={accessibilityLabel != null}
797
+ accessibilityLabel={accessibilityLabel}
798
+ accessibilityRole={accessibilityRole}
799
+ >
800
+ <Canvas style={{ flex: 1 }}>
801
+ {/* Shaken chart stack — left-edge fade is a sibling below so dstOut runs in canvas space */}
802
+ <ChartStack model={model} />
803
+
804
+ {leftEdgeFadeCfg && (
805
+ <LeftEdgeFade
806
+ paddingLeft={effectivePadding.left}
807
+ fadeWidth={leftEdgeFadeCfg.width}
808
+ startColor={leftEdgeFadeCfg.startColor}
809
+ endColor={leftEdgeFadeCfg.endColor}
810
+ engine={engine}
811
+ />
812
+ )}
813
+
814
+ <ChartValueOverlay model={model} />
815
+
816
+ <ChartScrubLayer model={model} />
817
+ </Canvas>
818
+ </View>
819
+ </GestureDetector>
820
+ );
821
+ }