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,135 @@
1
+ import {
2
+ isValidElement,
3
+ type ReactElement,
4
+ type ReactNode,
5
+ useEffect,
6
+ useReducer,
7
+ useRef,
8
+ } from "react";
9
+ import { StyleSheet, View, type ViewStyle } from "react-native";
10
+ import Animated, {
11
+ useAnimatedStyle,
12
+ useSharedValue,
13
+ withTiming,
14
+ } from "react-native-reanimated";
15
+
16
+ export interface LiveChartTransitionProps {
17
+ /** Key of the active child to show. Must match a child's `key`. */
18
+ active: string;
19
+ /** Chart elements, each with a unique `key`. */
20
+ children: ReactNode;
21
+ /** Cross-fade duration in ms. Default `300`. */
22
+ duration?: number;
23
+ /**
24
+ * Keep every child mounted for the lifetime of the transition (only the
25
+ * opacity animates). Each chart's engine then settles its y-range once, up
26
+ * front, so switching is a pure cross-fade with no reveal/range re-animation.
27
+ * Costs more (all engines run continuously). Default `false` (mount the
28
+ * active child + unmount the outgoing one after the fade).
29
+ */
30
+ keepMounted?: boolean;
31
+ /** Container style. */
32
+ style?: ViewStyle;
33
+ }
34
+
35
+ function FadeLayer({
36
+ visible,
37
+ duration,
38
+ children,
39
+ }: {
40
+ visible: boolean;
41
+ duration: number;
42
+ children: ReactNode;
43
+ }) {
44
+ const opacity = useSharedValue(visible ? 1 : 0);
45
+ useEffect(() => {
46
+ opacity.set(withTiming(visible ? 1 : 0, { duration }));
47
+ }, [visible, duration, opacity]);
48
+ const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.get() }));
49
+ return (
50
+ <Animated.View
51
+ pointerEvents={visible ? "auto" : "none"}
52
+ style={[StyleSheet.absoluteFill, animatedStyle]}
53
+ >
54
+ {children}
55
+ </Animated.View>
56
+ );
57
+ }
58
+
59
+ type MountedAction =
60
+ | { type: "mount"; key: string }
61
+ | { type: "unmount"; key: string };
62
+
63
+ /** Tracks which child keys are currently mounted during a cross-fade. */
64
+ function mountedReducer(
65
+ mounted: Set<string>,
66
+ action: MountedAction,
67
+ ): Set<string> {
68
+ switch (action.type) {
69
+ case "mount":
70
+ if (mounted.has(action.key)) return mounted;
71
+ return new Set(mounted).add(action.key);
72
+ case "unmount": {
73
+ if (!mounted.has(action.key)) return mounted;
74
+ const next = new Set(mounted);
75
+ next.delete(action.key);
76
+ return next;
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Cross-fades between chart instances by `key` (e.g. line ↔ candle). Mounts the
83
+ * active child plus any outgoing child still fading out; unmounts the outgoing
84
+ * child once the fade completes. RN/Reanimated analogue of liveline's
85
+ * `LivelineTransition`.
86
+ */
87
+ export function LiveChartTransition({
88
+ active,
89
+ children,
90
+ duration = 300,
91
+ keepMounted = false,
92
+ style,
93
+ }: LiveChartTransitionProps) {
94
+ // Use the raw children (not Children.toArray, which rewrites keys to ".$key").
95
+ const childArray = (
96
+ Array.isArray(children) ? children : [children]
97
+ ).filter(isValidElement) as ReactElement[];
98
+
99
+ const [mounted, dispatch] = useReducer(
100
+ mountedReducer,
101
+ active,
102
+ (initial) => new Set([initial]),
103
+ );
104
+ const prevRef = useRef(active);
105
+
106
+ useEffect(() => {
107
+ if (keepMounted) return;
108
+ if (active === prevRef.current) return;
109
+ const outgoing = prevRef.current;
110
+ prevRef.current = active;
111
+ dispatch({ type: "mount", key: active });
112
+ const timer = setTimeout(() => {
113
+ dispatch({ type: "unmount", key: outgoing });
114
+ }, duration + 50);
115
+ return () => clearTimeout(timer);
116
+ }, [active, duration, keepMounted]);
117
+
118
+ return (
119
+ <View style={[styles.root, style]}>
120
+ {childArray.map((child) => {
121
+ const key = String(child.key ?? "");
122
+ if (!keepMounted && !mounted.has(key)) return null;
123
+ return (
124
+ <FadeLayer key={key} visible={key === active} duration={duration}>
125
+ {child}
126
+ </FadeLayer>
127
+ );
128
+ })}
129
+ </View>
130
+ );
131
+ }
132
+
133
+ const styles = StyleSheet.create({
134
+ root: { flex: 1, position: "relative" },
135
+ });
@@ -0,0 +1,301 @@
1
+ import {
2
+ Group,
3
+ LinearGradient,
4
+ Path,
5
+ Rect,
6
+ RoundedRect,
7
+ Skia,
8
+ Text as SkiaText,
9
+ vec,
10
+ type SkFont,
11
+ type SkPath,
12
+ } from "@shopify/react-native-skia";
13
+ import { useRef } from "react";
14
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
15
+ import {
16
+ BADGE_DOT_GAP,
17
+ EMPTY_GAP_FADE_WIDTH,
18
+ EMPTY_STATE_LABEL_ALPHA,
19
+ EMPTY_TEXT_GAP_PAD,
20
+ } from "../constants";
21
+ import {
22
+ badgeTailAndCap,
23
+ gutterCenteredTextLeftX,
24
+ pillTextLeftX,
25
+ type ChartPadding,
26
+ } from "../draw/line";
27
+ import { drawSpline } from "../math/spline";
28
+ import { buildSquigglyPts } from "../math/squiggly";
29
+ import type { LiveChartPalette } from "../types";
30
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
31
+
32
+ const PLACEHOLDER_LABEL_COUNT = 4;
33
+ const RECT_W = 16;
34
+ const RECT_H = 4;
35
+ const RECT_R = 4;
36
+ const RECT_SPACING = 32;
37
+
38
+ function buildSplineInto(path: ReturnType<typeof Skia.Path.Make>, pts: number[]) {
39
+ "worklet";
40
+ path.reset();
41
+ const n = pts.length >> 1;
42
+ if (n < 2) return path;
43
+ path.moveTo(pts[0], pts[1]);
44
+ drawSpline(path, pts);
45
+ return path;
46
+ }
47
+
48
+ export function LoadingOverlay({
49
+ engine,
50
+ padding,
51
+ palette,
52
+ font,
53
+ morphT,
54
+ isLoading,
55
+ isEmpty,
56
+ emptyText,
57
+ strokeWidth,
58
+ badge = false,
59
+ badgeTail = true,
60
+ }: {
61
+ engine: ChartEngineLayout;
62
+ padding: ChartPadding;
63
+ palette: LiveChartPalette;
64
+ font: SkFont;
65
+ morphT: SharedValue<number>;
66
+ isLoading: SharedValue<boolean>;
67
+ /** Derived or shared; only `.value` is read on the UI thread. */
68
+ isEmpty: SharedValue<boolean> | { value: boolean };
69
+ emptyText: string;
70
+ strokeWidth: number;
71
+ /** Mirror the badge prop so labels align with GridOverlay's label positions. */
72
+ badge?: boolean;
73
+ /** Whether the badge tail spike is shown; affects the left inset used for skeleton alignment. */
74
+ badgeTail?: boolean;
75
+ }) {
76
+ // Same left-inset formula as GridOverlay (only used when badge=true)
77
+ const leftInset = BADGE_DOT_GAP + badgeTailAndCap(font.getSize(), badgeTail);
78
+
79
+ // Ping-pong persistent paths — avoid allocating a JSI-backed SkPath per frame
80
+ // while the squiggly loading/empty-state animation is running.
81
+ const squigglyCacheRef = useRef<{
82
+ a: SkPath;
83
+ b: SkPath;
84
+ tick: boolean;
85
+ } | null>(null);
86
+ if (squigglyCacheRef.current === null) {
87
+ squigglyCacheRef.current = {
88
+ a: Skia.Path.Make(),
89
+ b: Skia.Path.Make(),
90
+ tick: false,
91
+ };
92
+ }
93
+
94
+ // Squiggly path — animated each frame via timestamp
95
+ const squigglyPath = useDerivedValue(() => {
96
+ const squigglyCache = squigglyCacheRef.current!;
97
+ squigglyCache.tick = !squigglyCache.tick;
98
+ const path = squigglyCache.tick ? squigglyCache.a : squigglyCache.b;
99
+ if (!isLoading.get() && !isEmpty.value && morphT.get() >= 1) {
100
+ path.reset();
101
+ return path;
102
+ }
103
+ const pts = buildSquigglyPts(
104
+ engine.canvasWidth.get(),
105
+ engine.canvasHeight.get(),
106
+ padding,
107
+ engine.timestamp.get(),
108
+ );
109
+ return buildSplineInto(path, pts);
110
+ });
111
+
112
+ // Fades out quickly as the reveal animation begins (first 33% of morphT)
113
+ const groupOpacity = useDerivedValue(() => {
114
+ if (!isLoading.get() && !isEmpty.value) {
115
+ return Math.max(0, 1 - morphT.get() * 3);
116
+ }
117
+ return 1;
118
+ });
119
+
120
+ // Placeholder Y-axis rects: skeleton pills centred in the right gutter
121
+ const labelLayout = useDerivedValue(() => {
122
+ const w = engine.canvasWidth.get();
123
+ const h = engine.canvasHeight.get();
124
+ if (w === 0 || h === 0) {
125
+ return { x: -200, ys: [0, 0, 0, 0] as [number, number, number, number] };
126
+ }
127
+ const chartH = h - padding.top - padding.bottom;
128
+ // Mirror GridOverlay: pillTextLeftX when badge is on, gutterCenteredTextLeftX otherwise
129
+ const x = badge
130
+ ? pillTextLeftX(w, padding.right, leftInset, RECT_W)
131
+ : gutterCenteredTextLeftX(w, padding.right, RECT_W);
132
+ // Centre the group of rects around the chart's vertical midpoint
133
+ const groupH = (PLACEHOLDER_LABEL_COUNT - 1) * RECT_SPACING;
134
+ const groupStartCY = padding.top + chartH / 2 - groupH / 2;
135
+ const ys: [number, number, number, number] = [0, 0, 0, 0];
136
+ for (let i = 0; i < PLACEHOLDER_LABEL_COUNT; i++) {
137
+ const rowCenterY = groupStartCY + i * RECT_SPACING;
138
+ ys[i] = rowCenterY - RECT_H / 2; // top of rect so center lands on rowCenterY
139
+ }
140
+ return { x, ys };
141
+ });
142
+
143
+ // X and Y positions for the placeholder rects
144
+ const lx = useDerivedValue(() => labelLayout.get().x);
145
+ const ly0 = useDerivedValue(() => labelLayout.get().ys[0]);
146
+ const ly1 = useDerivedValue(() => labelLayout.get().ys[1]);
147
+ const ly2 = useDerivedValue(() => labelLayout.get().ys[2]);
148
+ const ly3 = useDerivedValue(() => labelLayout.get().ys[3]);
149
+
150
+ // Empty-state text + gradient gap
151
+ const emptyGapLayout = useDerivedValue(() => {
152
+ const w = engine.canvasWidth.get();
153
+ const h = engine.canvasHeight.get();
154
+ if (w === 0 || h === 0) {
155
+ return {
156
+ gapLeft: 0,
157
+ gapRight: 0,
158
+ centerY: 0,
159
+ eraseH: 0,
160
+ fadeW: EMPTY_GAP_FADE_WIDTH,
161
+ textX: 0,
162
+ textY: 0,
163
+ textW: 0,
164
+ showGap: 0,
165
+ };
166
+ }
167
+ const chartH = h - padding.top - padding.bottom;
168
+ const centerY = padding.top + chartH / 2;
169
+ const chartCentreX = (padding.left + w - padding.right) / 2;
170
+ const textW = font.measureText(emptyText).width;
171
+ const gapHalf = textW / 2 + EMPTY_TEXT_GAP_PAD;
172
+ const fadeW = EMPTY_GAP_FADE_WIDTH;
173
+ const gapLeft = chartCentreX - gapHalf - fadeW;
174
+ const gapRight = chartCentreX + gapHalf + fadeW;
175
+ const eraseH = Math.max(56, chartH * 0.28) + strokeWidth;
176
+ const fm = font.getMetrics();
177
+ const textX = chartCentreX - textW / 2;
178
+ const textY = centerY - (fm.ascent + fm.descent) / 2;
179
+ return {
180
+ gapLeft,
181
+ gapRight,
182
+ centerY,
183
+ eraseH,
184
+ fadeW,
185
+ textX,
186
+ textY,
187
+ textW,
188
+ showGap: isEmpty.value ? 1 : 0,
189
+ };
190
+ });
191
+
192
+ const gapLeft = useDerivedValue(() => emptyGapLayout.get().gapLeft);
193
+ const gapTop = useDerivedValue(
194
+ () => emptyGapLayout.get().centerY - emptyGapLayout.get().eraseH / 2,
195
+ );
196
+ const gapWidth = useDerivedValue(
197
+ () => emptyGapLayout.get().gapRight - emptyGapLayout.get().gapLeft,
198
+ );
199
+ const gapHeight = useDerivedValue(() => emptyGapLayout.get().eraseH);
200
+ const emptyTextX = useDerivedValue(() => emptyGapLayout.get().textX);
201
+ const emptyTextY = useDerivedValue(() => emptyGapLayout.get().textY);
202
+ const showGapGroup = useDerivedValue(() => emptyGapLayout.get().showGap);
203
+
204
+ const gapGradientPositions = useDerivedValue(() => {
205
+ const L = emptyGapLayout.get().gapRight - emptyGapLayout.get().gapLeft;
206
+ const fw = emptyGapLayout.get().fadeW;
207
+ if (L <= 0) return [0, 0, 1, 1] as number[];
208
+ const t1 = Math.min(fw / L, 0.49);
209
+ const t2 = Math.max(1 - fw / L, 0.51);
210
+ return [0, t1, t2, 1];
211
+ });
212
+
213
+ const gapGradEnd = useDerivedValue(() =>
214
+ vec(
215
+ Math.max(1, emptyGapLayout.get().gapRight - emptyGapLayout.get().gapLeft),
216
+ 0,
217
+ ),
218
+ );
219
+
220
+ const emptyLabelText = useDerivedValue(() =>
221
+ isEmpty.value ? emptyText : "",
222
+ );
223
+ const emptyLabelOpacity = useDerivedValue(() =>
224
+ isEmpty.value ? EMPTY_STATE_LABEL_ALPHA : 0,
225
+ );
226
+
227
+ return (
228
+ <Group opacity={groupOpacity}>
229
+ {/* Squiggly loading line */}
230
+ <Path
231
+ path={squigglyPath}
232
+ style="stroke"
233
+ strokeWidth={strokeWidth}
234
+ color={palette.gridLine}
235
+ strokeCap="round"
236
+ strokeJoin="round"
237
+ />
238
+
239
+ {/* Erase a soft horizontal band through the squiggle for the label (dstOut) */}
240
+ <Group opacity={showGapGroup} blendMode="dstOut">
241
+ <Rect x={gapLeft} y={gapTop} width={gapWidth} height={gapHeight}>
242
+ <LinearGradient
243
+ start={vec(0, 0)}
244
+ end={gapGradEnd}
245
+ colors={[
246
+ "rgba(0,0,0,0)",
247
+ "rgba(0,0,0,1)",
248
+ "rgba(0,0,0,1)",
249
+ "rgba(0,0,0,0)",
250
+ ]}
251
+ positions={gapGradientPositions}
252
+ />
253
+ </Rect>
254
+ </Group>
255
+
256
+ {/* Skeleton Y-axis label placeholders */}
257
+ <RoundedRect
258
+ x={lx}
259
+ y={ly0}
260
+ width={RECT_W}
261
+ height={RECT_H}
262
+ r={RECT_R}
263
+ color={palette.gridLine}
264
+ />
265
+ <RoundedRect
266
+ x={lx}
267
+ y={ly1}
268
+ width={RECT_W}
269
+ height={RECT_H}
270
+ r={RECT_R}
271
+ color={palette.gridLine}
272
+ />
273
+ <RoundedRect
274
+ x={lx}
275
+ y={ly2}
276
+ width={RECT_W}
277
+ height={RECT_H}
278
+ r={RECT_R}
279
+ color={palette.gridLine}
280
+ />
281
+ <RoundedRect
282
+ x={lx}
283
+ y={ly3}
284
+ width={RECT_W}
285
+ height={RECT_H}
286
+ r={RECT_R}
287
+ color={palette.gridLine}
288
+ />
289
+
290
+ {/* Empty state label */}
291
+ <SkiaText
292
+ x={emptyTextX}
293
+ y={emptyTextY}
294
+ text={emptyLabelText}
295
+ font={font}
296
+ color={palette.gridLabel}
297
+ opacity={emptyLabelOpacity}
298
+ />
299
+ </Group>
300
+ );
301
+ }