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,331 @@
1
+ import {
2
+ Circle,
3
+ Group,
4
+ Image as SkiaImage,
5
+ Path,
6
+ Skia,
7
+ Text as SkiaText,
8
+ type SkFont,
9
+ type SkPath,
10
+ } from "@shopify/react-native-skia";
11
+ import { useRef, useState } from "react";
12
+ import {
13
+ useAnimatedReaction,
14
+ useDerivedValue,
15
+ type SharedValue,
16
+ } from "react-native-reanimated";
17
+ import { scheduleOnRN } from "react-native-worklets";
18
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
19
+ import type { ChartPadding } from "../draw/line";
20
+ import { markersSignature, projectPoint } from "../math/markers";
21
+ import type {
22
+ LiveChartPalette,
23
+ LiveChartPoint,
24
+ Marker,
25
+ MarkerKind,
26
+ SeriesConfig,
27
+ } from "../types";
28
+
29
+ /** Default glyph color per kind when `marker.color` is unset. */
30
+ function defaultMarkerColor(kind: MarkerKind, palette: LiveChartPalette): string {
31
+ switch (kind) {
32
+ case "trade":
33
+ return palette.line;
34
+ case "boost":
35
+ return palette.refLine;
36
+ case "graduation":
37
+ return palette.dotUp;
38
+ case "winner":
39
+ return palette.dotUp;
40
+ case "clawback":
41
+ return palette.refLabel;
42
+ }
43
+ }
44
+
45
+ const OFF = -9999;
46
+ const DEFAULT_ICON_SIZE = 16;
47
+ /** Circular badge: padding around the icon glyph, background-colored ring width,
48
+ * and the icon color drawn on the fill. */
49
+ const PILL_PAD = 2;
50
+ const PILL_BORDER = 2;
51
+ const PILL_TEXT_COLOR = "#ffffff";
52
+
53
+ function MarkerGlyph({
54
+ marker,
55
+ color,
56
+ bgColor,
57
+ engine,
58
+ padding,
59
+ seriesSV,
60
+ lineDataSV,
61
+ font,
62
+ axisY,
63
+ }: {
64
+ marker: Marker;
65
+ color: string;
66
+ /** Chart background color, drawn as a ring behind a `pill` badge. */
67
+ bgColor?: string;
68
+ engine: ChartEngineLayout;
69
+ padding: ChartPadding;
70
+ seriesSV?: SharedValue<SeriesConfig[]>;
71
+ lineDataSV?: SharedValue<LiveChartPoint[]>;
72
+ font: SkFont;
73
+ axisY: SharedValue<number>;
74
+ }) {
75
+ // Capture only primitive anchor fields — never the full marker (which may
76
+ // carry non-serializable `data` / `image`) — in the worklet closure.
77
+ const { kind, icon, image, pill } = marker;
78
+ const time = marker.time;
79
+ const value = marker.value;
80
+ const seriesId = marker.seriesId;
81
+ const size = marker.size ?? DEFAULT_ICON_SIZE;
82
+
83
+ // Each glyph projects its OWN marker — no shared index buffer, so reordering
84
+ // the marker array can't make a glyph flash another marker's position.
85
+ const layout = useDerivedValue(() =>
86
+ projectPoint(time, value, seriesId, {
87
+ canvasWidth: engine.canvasWidth.get(),
88
+ canvasHeight: engine.canvasHeight.get(),
89
+ padTop: padding.top,
90
+ padBottom: padding.bottom,
91
+ padLeft: padding.left,
92
+ padRight: padding.right,
93
+ timestamp: engine.timestamp.get(),
94
+ displayWindow: engine.displayWindow.get(),
95
+ displayMin: engine.displayMin.get(),
96
+ displayMax: engine.displayMax.get(),
97
+ series: seriesSV?.get(),
98
+ lineData: lineDataSV?.get(),
99
+ }),
100
+ );
101
+
102
+ const cx = useDerivedValue(() =>
103
+ layout.get().visible ? layout.get().x : OFF,
104
+ );
105
+ const cy = useDerivedValue(() =>
106
+ layout.get().visible ? layout.get().y : OFF,
107
+ );
108
+ const opacity = useDerivedValue(() => (layout.get().visible ? 1 : 0));
109
+
110
+ const cacheRef = useRef<{ a: SkPath; b: SkPath; tick: boolean } | null>(null);
111
+ if (cacheRef.current === null) {
112
+ cacheRef.current = { a: Skia.Path.Make(), b: Skia.Path.Make(), tick: false };
113
+ }
114
+
115
+ const glyphPath = useDerivedValue(() => {
116
+ const cache = cacheRef.current!;
117
+ cache.tick = !cache.tick;
118
+ const p: SkPath = cache.tick ? cache.a : cache.b;
119
+ p.reset();
120
+ const l = layout.get();
121
+ if (!l.visible) return p;
122
+ const x = l.x;
123
+ const y = l.y;
124
+ if (kind === "winner") {
125
+ const outer = 7;
126
+ const inner = 3;
127
+ for (let k = 0; k < 10; k++) {
128
+ const ang = -Math.PI / 2 + (k * Math.PI) / 5;
129
+ const rad = k % 2 === 0 ? outer : inner;
130
+ const px = x + rad * Math.cos(ang);
131
+ const py = y + rad * Math.sin(ang);
132
+ if (k === 0) p.moveTo(px, py);
133
+ else p.lineTo(px, py);
134
+ }
135
+ p.close();
136
+ } else if (kind === "boost") {
137
+ const L = 6;
138
+ for (let k = 0; k < 4; k++) {
139
+ const ang = (k * Math.PI) / 4;
140
+ const dx = L * Math.cos(ang);
141
+ const dy = L * Math.sin(ang);
142
+ p.moveTo(x - dx, y - dy);
143
+ p.lineTo(x + dx, y + dy);
144
+ }
145
+ } else if (kind === "graduation") {
146
+ p.moveTo(x, axisY.get());
147
+ p.lineTo(x, y);
148
+ p.moveTo(x, y - 7);
149
+ p.lineTo(x + 8, y - 4);
150
+ p.lineTo(x, y - 1);
151
+ p.close();
152
+ } else if (kind === "clawback") {
153
+ const s = 5;
154
+ const ay = axisY.get() - s;
155
+ p.moveTo(x - s, ay);
156
+ p.lineTo(x + s, ay);
157
+ p.lineTo(x + s, ay + s * 2);
158
+ p.lineTo(x - s, ay + s * 2);
159
+ p.close();
160
+ }
161
+ return p;
162
+ });
163
+
164
+ // Image icon centered on the marker.
165
+ const imgX = useDerivedValue(() =>
166
+ layout.get().visible ? layout.get().x - size / 2 : OFF,
167
+ );
168
+ const imgY = useDerivedValue(() =>
169
+ layout.get().visible ? layout.get().y - size / 2 : OFF,
170
+ );
171
+
172
+ // Text / emoji icon centering — width + baseline shift depend only on the
173
+ // (stable) font and icon, so measure once instead of every frame. Skia
174
+ // `measureText`/`getMetrics` both allocate, so hoisting them out of the
175
+ // per-frame worklet removes one measure + one metrics call per glyph/frame.
176
+ // Center the icon glyph on the marker using its measured bounds — tighter and
177
+ // more accurate than font ascent/descent, so `+` / `−` etc. sit centered both
178
+ // horizontally and vertically in the badge. Stable per render, not per frame.
179
+ const iconBounds = font.measureText(icon ?? "");
180
+ const iconDX = iconBounds.x + iconBounds.width / 2;
181
+ const iconDY = iconBounds.y + iconBounds.height / 2;
182
+
183
+ const iconX = useDerivedValue(() =>
184
+ layout.get().visible ? layout.get().x - iconDX : OFF,
185
+ );
186
+ const iconY = useDerivedValue(() =>
187
+ layout.get().visible ? layout.get().y - iconDY : OFF,
188
+ );
189
+
190
+ // Circular badge radius — fits the glyph's larger dimension + padding.
191
+ const pillR = Math.max(iconBounds.width, iconBounds.height) / 2 + PILL_PAD;
192
+
193
+ if (image) {
194
+ return (
195
+ <Group opacity={opacity}>
196
+ <SkiaImage
197
+ image={image}
198
+ x={imgX}
199
+ y={imgY}
200
+ width={size}
201
+ height={size}
202
+ fit="contain"
203
+ />
204
+ </Group>
205
+ );
206
+ }
207
+
208
+ if (icon) {
209
+ if (pill) {
210
+ // Filled circular badge in the marker color with the icon in white — e.g.
211
+ // a green `+` buy / red `−` sell tag. A background-colored ring underneath
212
+ // separates it from the line passing behind.
213
+ return (
214
+ <Group opacity={opacity}>
215
+ {bgColor !== undefined && (
216
+ <Circle cx={cx} cy={cy} r={pillR + PILL_BORDER} color={bgColor} />
217
+ )}
218
+ <Circle cx={cx} cy={cy} r={pillR} color={color} />
219
+ <SkiaText
220
+ x={iconX}
221
+ y={iconY}
222
+ text={icon}
223
+ font={font}
224
+ color={PILL_TEXT_COLOR}
225
+ />
226
+ </Group>
227
+ );
228
+ }
229
+ return (
230
+ <Group opacity={opacity}>
231
+ <SkiaText x={iconX} y={iconY} text={icon} font={font} color={color} />
232
+ </Group>
233
+ );
234
+ }
235
+
236
+ if (kind === "trade") {
237
+ return (
238
+ <Group opacity={opacity}>
239
+ <Circle cx={cx} cy={cy} r={5} color={color} style="stroke" strokeWidth={2} />
240
+ <Circle cx={cx} cy={cy} r={2} color={color} />
241
+ </Group>
242
+ );
243
+ }
244
+
245
+ const fill = kind === "winner" || kind === "clawback";
246
+ return (
247
+ <Group opacity={opacity}>
248
+ <Path
249
+ path={glyphPath}
250
+ color={color}
251
+ style={fill ? "fill" : "stroke"}
252
+ strokeWidth={1.5}
253
+ strokeCap="round"
254
+ strokeJoin="round"
255
+ />
256
+ </Group>
257
+ );
258
+ }
259
+
260
+ /**
261
+ * Renders the `markers` SharedValue as canvas glyphs. Marker metadata is
262
+ * mirrored into React state when it changes; each glyph projects its own
263
+ * position every frame (keyed by id), so adding/removing markers never makes a
264
+ * surviving glyph flash another marker's position.
265
+ *
266
+ * Glyph precedence per marker: `image` → `icon` (text) → built-in `kind` shape.
267
+ */
268
+ export function MarkerOverlay({
269
+ markers,
270
+ engine,
271
+ padding,
272
+ palette,
273
+ font,
274
+ series,
275
+ lineData,
276
+ }: {
277
+ markers: SharedValue<Marker[]>;
278
+ engine: ChartEngineLayout;
279
+ padding: ChartPadding;
280
+ palette: LiveChartPalette;
281
+ font: SkFont;
282
+ /** Multi-series data, used to anchor markers by `seriesId`. */
283
+ series?: SharedValue<SeriesConfig[]>;
284
+ /** Single-series line data; anchors markers that omit `value`. */
285
+ lineData?: SharedValue<LiveChartPoint[]>;
286
+ }) {
287
+ // Seed from the current markers at mount; the reaction below keeps it in sync.
288
+ const [snapshot, setSnapshot] = useState<Marker[]>(() => markers.get().slice());
289
+
290
+ // Read the `markers` prop from closure rather than a SharedValue passed
291
+ // through `scheduleOnRN`: the handle serialized across the worklet→JS
292
+ // boundary exposes the native `.value` accessor but NOT the `.get()` method,
293
+ // so calling `.get()` on it throws ("sv.get is not a function").
294
+ const pull = () => {
295
+ setSnapshot(markers.get().slice());
296
+ };
297
+
298
+ useAnimatedReaction(
299
+ () => markersSignature(markers.get()),
300
+ /* istanbul ignore next -- scheduleOnRN from UI-thread reaction */
301
+ (sig, prev) => {
302
+ if (sig !== prev) scheduleOnRN(pull);
303
+ },
304
+ [markers, pull],
305
+ );
306
+
307
+ const axisY = useDerivedValue(
308
+ () => engine.canvasHeight.get() - padding.bottom,
309
+ );
310
+
311
+ const bgColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
312
+
313
+ return (
314
+ <Group>
315
+ {snapshot.map((m) => (
316
+ <MarkerGlyph
317
+ key={m.id}
318
+ marker={m}
319
+ color={m.color ?? defaultMarkerColor(m.kind, palette)}
320
+ bgColor={bgColor}
321
+ engine={engine}
322
+ padding={padding}
323
+ seriesSV={series}
324
+ lineDataSV={lineData}
325
+ font={font}
326
+ axisY={axisY}
327
+ />
328
+ ))}
329
+ </Group>
330
+ );
331
+ }
@@ -0,0 +1,117 @@
1
+ import { Circle, Group } from "@shopify/react-native-skia";
2
+
3
+ import { useDerivedValue } from "react-native-reanimated";
4
+ import { MAX_MULTI_SERIES } from "../constants";
5
+ import type { ChartPadding } from "../draw/line";
6
+ import type { ResolvedPulseConfig } from "../core/resolveConfig";
7
+ import type { MultiEngineState } from "../core/useLiveChartEngine";
8
+
9
+ const MIN_PULSE_RADIUS = 6;
10
+ const PULSE_STAGGER_MS = 200;
11
+
12
+ function SeriesDotAtIndex({
13
+ index,
14
+ engine,
15
+ padding,
16
+ color,
17
+ radius,
18
+ pulse,
19
+ }: {
20
+ index: number;
21
+ engine: MultiEngineState;
22
+ padding: ChartPadding;
23
+ color: string;
24
+ radius: number;
25
+ pulse: ResolvedPulseConfig | null;
26
+ }) {
27
+ const dotX = useDerivedValue(() => {
28
+ const w = engine.canvasWidth.value;
29
+ if (w === 0) return -100;
30
+ return w - padding.right;
31
+ });
32
+
33
+ const dotY = useDerivedValue(() => {
34
+ const h = engine.canvasHeight.value;
35
+ if (h === 0) return -100;
36
+ const s = engine.series.value;
37
+ const displays = engine.displaySeriesValues.value;
38
+ if (index >= s.length) return -100;
39
+ const chartH = h - padding.top - padding.bottom;
40
+ const dMin = engine.displayMin.value;
41
+ const dMax = engine.displayMax.value;
42
+ const valRange = dMax - dMin;
43
+ const v = displays[index] ?? s[index].value;
44
+ if (valRange === 0) return padding.top + chartH / 2;
45
+ return padding.top + ((dMax - v) / valRange) * chartH;
46
+ });
47
+
48
+ const opacity = useDerivedValue(() => {
49
+ const s = engine.series.value;
50
+ const op = engine.seriesOpacities.value;
51
+ if (index >= s.length || index >= op.length) return 0;
52
+ return op[index] ?? 0;
53
+ });
54
+
55
+ const pulseRadius = useDerivedValue(() => {
56
+ if (!pulse) return 0;
57
+ const nowMs = engine.timestamp.value * 1000 + index * PULSE_STAGGER_MS;
58
+ const t = (nowMs % pulse.interval) / pulse.duration;
59
+ if (t >= 1) return 0;
60
+ return MIN_PULSE_RADIUS + t * (pulse.maxRadius - MIN_PULSE_RADIUS);
61
+ });
62
+
63
+ const pulseOpacity = useDerivedValue(() => {
64
+ if (!pulse) return 0;
65
+ const nowMs = engine.timestamp.value * 1000 + index * PULSE_STAGGER_MS;
66
+ const t = (nowMs % pulse.interval) / pulse.duration;
67
+ if (t >= 1) return 0;
68
+ return pulse.opacity * (1 - t);
69
+ });
70
+
71
+ return (
72
+ <Group opacity={opacity}>
73
+ {pulse && (
74
+ <Circle
75
+ cx={dotX}
76
+ cy={dotY}
77
+ r={pulseRadius}
78
+ color={color}
79
+ style="stroke"
80
+ strokeWidth={pulse.strokeWidth}
81
+ opacity={pulseOpacity}
82
+ />
83
+ )}
84
+ <Circle cx={dotX} cy={dotY} r={radius} color={color} />
85
+ </Group>
86
+ );
87
+ }
88
+
89
+ export function MultiSeriesDots({
90
+ engine,
91
+ padding,
92
+ colors,
93
+ radius,
94
+ pulse,
95
+ }: {
96
+ engine: MultiEngineState;
97
+ padding: ChartPadding;
98
+ colors: string[];
99
+ radius: number;
100
+ pulse: ResolvedPulseConfig | null;
101
+ }) {
102
+ return (
103
+ <Group>
104
+ {Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
105
+ <SeriesDotAtIndex
106
+ key={i}
107
+ index={i}
108
+ engine={engine}
109
+ padding={padding}
110
+ color={colors[i] ?? "#ffffff"}
111
+ radius={radius}
112
+ pulse={pulse}
113
+ />
114
+ ))}
115
+ </Group>
116
+ );
117
+ }
@@ -0,0 +1,79 @@
1
+ import { Blur, Group, Path, Skia } from "@shopify/react-native-skia";
2
+ import { DashPathEffect } from "@shopify/react-native-skia";
3
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
4
+ import type { SeriesLineStyle } from "../core/multiSeriesLayout";
5
+ import { SERIES_COLORS } from "../theme";
6
+ import type { SeriesConfig } from "../types";
7
+
8
+ export function MultiSeriesStroke({
9
+ index,
10
+ paths,
11
+ opacities,
12
+ series,
13
+ strokeWidth,
14
+ lineStyle,
15
+ }: {
16
+ index: number;
17
+ paths: SharedValue<ReturnType<typeof Skia.Path.Make>[]>;
18
+ opacities: SharedValue<number[]>;
19
+ series: SharedValue<SeriesConfig[]>;
20
+ strokeWidth: number;
21
+ /** Per-series static stroke style (dash / width / glow). */
22
+ lineStyle?: SeriesLineStyle;
23
+ }) {
24
+ const path = useDerivedValue(
25
+ /* istanbul ignore next -- Skia path derived on UI thread */
26
+ () => paths.value[index] ?? Skia.Path.Make(),
27
+ );
28
+ const opacity = useDerivedValue(
29
+ /* istanbul ignore next -- Reanimated derived on UI thread */
30
+ () => {
31
+ const o = opacities.value;
32
+ if (index >= o.length) return 0;
33
+ return o[index] ?? 0;
34
+ },
35
+ );
36
+ const color = useDerivedValue(
37
+ /* istanbul ignore next -- Reanimated derived on UI thread */
38
+ () => {
39
+ const s = series.value;
40
+ if (index >= s.length) return "#ffffff";
41
+ return (s[index].color ??
42
+ SERIES_COLORS[index % SERIES_COLORS.length]) as string;
43
+ },
44
+ );
45
+
46
+ const sw = lineStyle?.strokeWidth ?? strokeWidth;
47
+ const dashed = lineStyle?.dashed ?? false;
48
+ const intervals = lineStyle?.intervals ?? [6, 4];
49
+ const glow = lineStyle?.glow ?? false;
50
+
51
+ return (
52
+ <Group opacity={opacity}>
53
+ {glow && (
54
+ <Group opacity={0.35}>
55
+ <Path
56
+ path={path}
57
+ style="stroke"
58
+ strokeWidth={sw * 2.5}
59
+ color={color}
60
+ strokeCap="round"
61
+ strokeJoin="round"
62
+ >
63
+ <Blur blur={4} />
64
+ </Path>
65
+ </Group>
66
+ )}
67
+ <Path
68
+ path={path}
69
+ style="stroke"
70
+ strokeWidth={sw}
71
+ color={color}
72
+ strokeCap="round"
73
+ strokeJoin="round"
74
+ >
75
+ {dashed && <DashPathEffect intervals={intervals} />}
76
+ </Path>
77
+ </Group>
78
+ );
79
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ Group,
3
+ Text as SkiaText,
4
+ type SkFont,
5
+ } from "@shopify/react-native-skia";
6
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
7
+ import { MAX_MULTI_SERIES } from "../constants";
8
+ import type { TooltipLayout } from "../hooks/crosshairShared";
9
+ import type { LiveChartPalette } from "../types";
10
+
11
+ const TOOLTIP_STACK_SLOTS = MAX_MULTI_SERIES + 1;
12
+
13
+ function TooltipStackLine({
14
+ index,
15
+ tooltipLayout,
16
+ font,
17
+ palette,
18
+ }: {
19
+ index: number;
20
+ tooltipLayout: SharedValue<TooltipLayout>;
21
+ font: SkFont;
22
+ palette: LiveChartPalette;
23
+ }) {
24
+ const opacity = useDerivedValue(() => {
25
+ const sl = tooltipLayout.value.stackedLines;
26
+ if (!sl || index >= sl.length) return 0;
27
+ return 1;
28
+ });
29
+ const text = useDerivedValue(() => {
30
+ const sl = tooltipLayout.value.stackedLines;
31
+ if (!sl || index >= sl.length) return "";
32
+ return sl[index].text;
33
+ });
34
+ const x = useDerivedValue(() => {
35
+ const sl = tooltipLayout.value.stackedLines;
36
+ if (!sl || index >= sl.length) return -400;
37
+ return sl[index].textX;
38
+ });
39
+ const y = useDerivedValue(() => {
40
+ const sl = tooltipLayout.value.stackedLines;
41
+ if (!sl || index >= sl.length) return 0;
42
+ return sl[index].baselineY;
43
+ });
44
+ const color = useDerivedValue(() => {
45
+ const sl = tooltipLayout.value.stackedLines;
46
+ if (!sl || index >= sl.length) return palette.tooltipText;
47
+ return sl[index].dim ? palette.gridLabel : palette.tooltipText;
48
+ });
49
+ return (
50
+ <Group opacity={opacity}>
51
+ <SkiaText x={x} y={y} text={text} font={font} color={color} />
52
+ </Group>
53
+ );
54
+ }
55
+
56
+ export function MultiSeriesTooltipStack({
57
+ tooltipLayout,
58
+ font,
59
+ palette,
60
+ }: {
61
+ tooltipLayout: SharedValue<TooltipLayout>;
62
+ font: SkFont;
63
+ palette: LiveChartPalette;
64
+ }) {
65
+ return (
66
+ <Group>
67
+ {Array.from({ length: TOOLTIP_STACK_SLOTS }, (_, i) => (
68
+ <TooltipStackLine
69
+ key={i}
70
+ index={i}
71
+ tooltipLayout={tooltipLayout}
72
+ font={font}
73
+ palette={palette}
74
+ />
75
+ ))}
76
+ </Group>
77
+ );
78
+ }
@@ -0,0 +1,103 @@
1
+ import {
2
+ Group,
3
+ Text as SkiaText,
4
+ type SkFont,
5
+ } from "@shopify/react-native-skia";
6
+ import { useDerivedValue } from "react-native-reanimated";
7
+
8
+ import { MAX_MULTI_SERIES } from "../constants";
9
+ import type { ChartPadding } from "../draw/line";
10
+ import type { MultiEngineState } from "../core/useLiveChartEngine";
11
+
12
+ const LABEL_GAP = 8;
13
+
14
+ function SeriesValueLabelAtIndex({
15
+ index,
16
+ engine,
17
+ padding,
18
+ color,
19
+ font,
20
+ dotRadius,
21
+ }: {
22
+ index: number;
23
+ engine: MultiEngineState;
24
+ padding: ChartPadding;
25
+ color: string;
26
+ font: SkFont;
27
+ dotRadius: number;
28
+ }) {
29
+ const labelX = useDerivedValue(() => {
30
+ const w = engine.canvasWidth.value;
31
+ if (w === 0) return -200;
32
+ return w - padding.right + dotRadius + LABEL_GAP;
33
+ });
34
+
35
+ const labelY = useDerivedValue(() => {
36
+ const h = engine.canvasHeight.value;
37
+ if (h === 0) return -200;
38
+ const s = engine.series.value;
39
+ const displays = engine.displaySeriesValues.value;
40
+ if (index >= s.length) return -200;
41
+ const chartH = h - padding.top - padding.bottom;
42
+ const dMin = engine.displayMin.value;
43
+ const dMax = engine.displayMax.value;
44
+ const valRange = dMax - dMin;
45
+ const v = displays[index] ?? s[index].value;
46
+ const y =
47
+ valRange === 0
48
+ ? padding.top + chartH / 2
49
+ : padding.top + ((dMax - v) / valRange) * chartH;
50
+
51
+ const fm = font.getMetrics();
52
+ return y - (fm.ascent + fm.descent) / 2;
53
+ });
54
+
55
+ const text = useDerivedValue(() => {
56
+ const s = engine.series.value;
57
+ if (index >= s.length) return "";
58
+ return s[index].label ?? s[index].id;
59
+ });
60
+
61
+ const opacity = useDerivedValue(() => {
62
+ const s = engine.series.value;
63
+ const op = engine.seriesOpacities.value;
64
+ if (index >= s.length || index >= op.length) return 0;
65
+ return op[index] ?? 0;
66
+ });
67
+
68
+ return (
69
+ <Group opacity={opacity}>
70
+ <SkiaText x={labelX} y={labelY} text={text} font={font} color={color} />
71
+ </Group>
72
+ );
73
+ }
74
+
75
+ export function MultiSeriesValueLabels({
76
+ engine,
77
+ padding,
78
+ colors,
79
+ font,
80
+ dotRadius,
81
+ }: {
82
+ engine: MultiEngineState;
83
+ padding: ChartPadding;
84
+ colors: string[];
85
+ font: SkFont;
86
+ dotRadius: number;
87
+ }) {
88
+ return (
89
+ <Group>
90
+ {Array.from({ length: MAX_MULTI_SERIES }, (_, i) => (
91
+ <SeriesValueLabelAtIndex
92
+ key={i}
93
+ index={i}
94
+ engine={engine}
95
+ padding={padding}
96
+ color={colors[i] ?? "#ffffff"}
97
+ font={font}
98
+ dotRadius={dotRadius}
99
+ />
100
+ ))}
101
+ </Group>
102
+ );
103
+ }