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
package/src/index.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Public API — stable exports for `react-native-livechart` consumers.
3
+ *
4
+ * @remarks Conceptually inspired by [liveline](https://github.com/benjitaylor/liveline)
5
+ * (Benji Taylor) — live charts for React. This package is a separate React Native
6
+ * implementation on Skia and Reanimated, not a fork of the web library.
7
+ */
8
+
9
+ // ── Components ───────────────────────────────────────────────────────────────
10
+
11
+ import { LiveChart } from "./components/LiveChart";
12
+ import { LiveChartSeries } from "./components/LiveChartSeries";
13
+ import { LiveChartTransition } from "./components/LiveChartTransition";
14
+
15
+ export { LiveChart, LiveChartSeries, LiveChartTransition };
16
+ export type { LiveChartTransitionProps } from "./components/LiveChartTransition";
17
+
18
+ // ── Utilities ────────────────────────────────────────────────────────────────
19
+
20
+ export { formatTime, formatValue } from "./lib/format";
21
+ export { MONO_FONT_FAMILY } from "./lib/monoFontFamily";
22
+
23
+ // ── Optional hooks (degen / trade markers — see README) ───────────────────────
24
+
25
+ export { useDegen } from "./hooks/useDegen";
26
+ export { useTradeStream } from "./hooks/useTradeStream";
27
+
28
+ // ── Types ────────────────────────────────────────────────────────────────────
29
+
30
+ export type {
31
+ BadgeConfig,
32
+ BadgeVariant,
33
+ CandlePoint,
34
+ ChartInsets,
35
+ DegenOptions,
36
+ DegenShakePayload,
37
+ FontConfig,
38
+ FontWeight,
39
+ GradientConfig,
40
+ GridStyleConfig,
41
+ LeftEdgeFadeConfig,
42
+ LegendConfig,
43
+ LegendStyle,
44
+ LineConfig,
45
+ LiveChartCoreProps,
46
+ LiveChartPalette,
47
+ LiveChartPoint,
48
+ LiveChartProps,
49
+ LiveChartSeriesProps,
50
+ Marker,
51
+ MarkerHoverEvent,
52
+ MarkerKind,
53
+ Momentum,
54
+ MomentumConfig,
55
+ MultiSeriesDotConfig,
56
+ PulseConfig,
57
+ ReferenceLine,
58
+ ScrubConfig,
59
+ ScrubPoint,
60
+ ScrubPointCore,
61
+ ScrubPointMulti,
62
+ ScrubSeriesValue,
63
+ SeriesConfig,
64
+ ThemeMode,
65
+ TradeEvent,
66
+ ValueLineConfig,
67
+ XAxisConfig,
68
+ YAxisConfig,
69
+ } from "./types";
@@ -0,0 +1,36 @@
1
+ /** Adaptive value formatter with compact suffixes. Worklet-safe. */
2
+ export function formatValue(v: number): string {
3
+ "worklet";
4
+ const abs = Math.abs(v);
5
+ const sign = v < 0 ? "-" : "";
6
+ if (abs >= 1_000_000) {
7
+ const m = abs / 1_000_000;
8
+ return (
9
+ sign +
10
+ (m >= 100 ? m.toFixed(0) : m >= 10 ? m.toFixed(1) : m.toFixed(2)) +
11
+ "M"
12
+ );
13
+ }
14
+ if (abs >= 1_000) {
15
+ const k = abs / 1_000;
16
+ return (
17
+ sign +
18
+ (k >= 100 ? k.toFixed(0) : k >= 10 ? k.toFixed(1) : k.toFixed(2)) +
19
+ "K"
20
+ );
21
+ }
22
+ if (abs >= 100) return sign + abs.toFixed(1);
23
+ if (abs >= 1) return sign + abs.toFixed(2);
24
+ if (abs === 0) return "0";
25
+ return sign + abs.toPrecision(4);
26
+ }
27
+
28
+ /** HH:MM:SS time formatter. Worklet-safe. */
29
+ export function formatTime(t: number): string {
30
+ "worklet";
31
+ const d = new Date(t * 1000);
32
+ const hh = String(d.getHours()).padStart(2, "0");
33
+ const mm = String(d.getMinutes()).padStart(2, "0");
34
+ const ss = String(d.getSeconds()).padStart(2, "0");
35
+ return `${hh}:${mm}:${ss}`;
36
+ }
@@ -0,0 +1,10 @@
1
+ import type { SkFont } from "@shopify/react-native-skia";
2
+
3
+ /**
4
+ * Horizontal layout width for `text` via Skia `measureText` (replaces deprecated `getTextWidth`).
5
+ * Marked worklet for use inside Reanimated `useDerivedValue`.
6
+ */
7
+ export function measureFontTextWidth(font: SkFont, text: string): number {
8
+ "worklet";
9
+ return font.measureText(text).width;
10
+ }
@@ -0,0 +1,8 @@
1
+ import { Platform } from "react-native";
2
+
3
+ /**
4
+ * React Native `Text` / navigation title styles: iOS has no generic "monospace"
5
+ * family (unlike web/CSS). Menlo is the system monospace on Apple platforms.
6
+ */
7
+ export const MONO_FONT_FAMILY =
8
+ Platform.select({ ios: "Menlo", default: "monospace" }) ?? "monospace";
@@ -0,0 +1,31 @@
1
+ /** Parse hex color (#rgb or #rrggbb) to [r, g, b]. Worklet-safe (no regex). */
2
+ export function hexToRgb(hex: string): [number, number, number] {
3
+ "worklet";
4
+ // Fallback to neutral grey if hex is unparseable
5
+ let r = 128;
6
+ let g = 128;
7
+ let b = 128;
8
+ if (hex.length === 4) {
9
+ r = parseInt(hex[1] + hex[1], 16);
10
+ g = parseInt(hex[2] + hex[2], 16);
11
+ b = parseInt(hex[3] + hex[3], 16);
12
+ } else if (hex.length >= 7) {
13
+ r = parseInt(hex.slice(1, 3), 16);
14
+ g = parseInt(hex.slice(3, 5), 16);
15
+ b = parseInt(hex.slice(5, 7), 16);
16
+ }
17
+ return [r, g, b];
18
+ }
19
+
20
+ /** Interpolate between two RGB triples. Returns `rgb(r,g,b)` string. */
21
+ export function lerpColor(
22
+ a: [number, number, number],
23
+ b: [number, number, number],
24
+ t: number,
25
+ ): string {
26
+ "worklet";
27
+ const r = Math.round(a[0] + (b[0] - a[0]) * t);
28
+ const g = Math.round(a[1] + (b[1] - a[1]) * t);
29
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t);
30
+ return `rgb(${r},${g},${bl})`;
31
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Particle system for "degen" momentum bursts.
3
+ *
4
+ * Particles live in a packed `Float64Array` ring buffer. Each slot occupies
5
+ * `DEGEN_STRIDE` (7) contiguous floats:
6
+ *
7
+ * | offset | field | description |
8
+ * | ------ | ---------- | ------------------------------------ |
9
+ * | 0 | x | current x position (px) |
10
+ * | 1 | y | current y position (px) |
11
+ * | 2 | vx | velocity x (px/s) |
12
+ * | 3 | vy | velocity y (px/s) |
13
+ * | 4 | t0 | spawn timestamp (seconds) |
14
+ * | 5 | active | 1 = alive, 0 = expired |
15
+ * | 6 | size | particle radius (px, pre-scaled) |
16
+ * | 7 | colorIndex | index into the renderer's color list |
17
+ *
18
+ * All functions are worklet-safe for UI-thread execution.
19
+ */
20
+ import { DEGEN_STRIDE } from "../constants";
21
+
22
+ /** Deterministic pseudo-random hash for reproducible particle variation. */
23
+ export function hash(a: number, b: number): number {
24
+ "worklet";
25
+ const x = Math.sin(a * 12.9898 + b * 78.233) * 43758.5453;
26
+ return x - Math.floor(x);
27
+ }
28
+
29
+ export interface SpawnParams {
30
+ /** Origin x — center of the burst (px). */
31
+ ox: number;
32
+ /** Origin y — center of the burst (px). */
33
+ oy: number;
34
+ /** Scale multiplier applied to speed and size. */
35
+ sc: number;
36
+ /** Number of particles to spawn in this burst. */
37
+ burst: number;
38
+ /** Central emission angle (radians). */
39
+ baseAngle: number;
40
+ /** Angular spread around `baseAngle` (radians). */
41
+ spread: number;
42
+ /** Horizontal position jitter (±half, px). */
43
+ jx: number;
44
+ /** Vertical position jitter (±half, px). */
45
+ jy: number;
46
+ /** Minimum initial speed (px/s, before `sc`). */
47
+ sMin: number;
48
+ /** Maximum initial speed (px/s, before `sc`). */
49
+ sMax: number;
50
+ /** Minimum particle radius (px, before `sc`). */
51
+ szMin: number;
52
+ /** Maximum particle radius (px, before `sc`). */
53
+ szMax: number;
54
+ /** Current timestamp (seconds). */
55
+ now: number;
56
+ /** Ring-buffer rotation index for slot allocation. */
57
+ baseRot: number;
58
+ /** Total number of slots in the ring buffer. */
59
+ slots: number;
60
+ /**
61
+ * Color list index written to every particle in this burst. Use a fixed value
62
+ * (e.g. a series index) to color the whole burst uniformly, or `-1` to cycle
63
+ * the renderer's color list per particle (the particle's loop index is stored).
64
+ * Defaults to `-1`.
65
+ */
66
+ colorIndex?: number;
67
+ }
68
+
69
+ /** Write `p.burst` particles into the ring buffer starting at `p.baseRot`. Returns the next rotation index. */
70
+ export function spawnBurst(buf: Float64Array, p: SpawnParams): number {
71
+ "worklet";
72
+ const stride = DEGEN_STRIDE;
73
+ const rot = p.baseRot;
74
+ const colorIndex = p.colorIndex ?? -1;
75
+ for (let k = 0; k < p.burst; k++) {
76
+ const slot = (rot + k) % p.slots;
77
+ const b = slot * stride;
78
+ const angle = p.baseAngle + (hash(k, p.now * 1000) - 0.5) * p.spread;
79
+ const speed =
80
+ (p.sMin + hash(k + 100, p.now * 1000) * (p.sMax - p.sMin)) * p.sc;
81
+ buf[b + 0] = p.ox + (hash(k + 200, p.now * 1000) - 0.5) * p.jx;
82
+ buf[b + 1] = p.oy + (hash(k + 300, p.now * 1000) - 0.5) * p.jy;
83
+ buf[b + 2] = Math.cos(angle) * speed;
84
+ buf[b + 3] = Math.sin(angle) * speed;
85
+ buf[b + 4] = p.now;
86
+ buf[b + 5] = 1;
87
+ buf[b + 6] =
88
+ (p.szMin + hash(k + 400, p.now * 1000) * (p.szMax - p.szMin)) * p.sc;
89
+ // Fixed index colors the whole burst; -1 cycles per particle via k.
90
+ buf[b + 7] = colorIndex < 0 ? k : colorIndex;
91
+ }
92
+ return (rot + p.burst) % p.slots;
93
+ }
94
+
95
+ export interface ShakeResult {
96
+ x: number;
97
+ y: number;
98
+ active: boolean;
99
+ }
100
+
101
+ /** Compute screen-shake displacement with exponential decay envelope. */
102
+ export function computeShake(
103
+ elapsed: number,
104
+ dur: number,
105
+ sc: number,
106
+ sint: number,
107
+ ): ShakeResult {
108
+ "worklet";
109
+ if (elapsed >= dur) return { x: 0, y: 0, active: false };
110
+ const amp = 10 * sc * sint;
111
+ const tail = 1 - elapsed / dur;
112
+ const env = Math.exp(-elapsed * 10) * tail;
113
+ return {
114
+ x: Math.sin(elapsed * 88) * amp * env,
115
+ y: Math.cos(elapsed * 71) * amp * env * 0.92,
116
+ active: true,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Advance all particles by `dtSec`: apply velocity, drag, and expire particles older than `burstDur`.
122
+ * Returns the number of particles still alive after the tick, so callers can skip per-frame
123
+ * repaint work (bumping `packRevision`) when the field is empty.
124
+ */
125
+ export function tickParticles(
126
+ buf: Float64Array,
127
+ slots: number,
128
+ now: number,
129
+ burstDur: number,
130
+ drag: number,
131
+ dtSec: number,
132
+ ): number {
133
+ "worklet";
134
+ const stride = DEGEN_STRIDE;
135
+ let active = 0;
136
+ for (let i = 0; i < slots; i++) {
137
+ const b = i * stride;
138
+ if (buf[b + 5] < 0.5) continue;
139
+ const t0 = buf[b + 4];
140
+ if (now - t0 > burstDur) {
141
+ buf[b + 5] = 0;
142
+ continue;
143
+ }
144
+ buf[b + 0] += buf[b + 2] * dtSec;
145
+ buf[b + 1] += buf[b + 3] * dtSec;
146
+ buf[b + 2] *= drag;
147
+ buf[b + 3] *= drag;
148
+ active++;
149
+ }
150
+ return active;
151
+ }
@@ -0,0 +1,32 @@
1
+ import type { LiveChartPoint } from "../types";
2
+
3
+ /**
4
+ * Binary search to find interpolated value at a given time.
5
+ * Returns null if time is outside data range.
6
+ */
7
+ export function interpolateAtTime(
8
+ points: LiveChartPoint[],
9
+ time: number,
10
+ ): number | null {
11
+ "worklet";
12
+ if (points.length === 0) return null;
13
+ if (time <= points[0].time) return points[0].value;
14
+ if (time >= points[points.length - 1].time)
15
+ return points[points.length - 1].value;
16
+
17
+ let lo = 0;
18
+ let hi = points.length - 1;
19
+ while (hi - lo > 1) {
20
+ const mid = (lo + hi) >> 1;
21
+ if (points[mid].time <= time) lo = mid;
22
+ else hi = mid;
23
+ }
24
+
25
+ const p1 = points[lo];
26
+ const p2 = points[hi];
27
+ const dt = p2.time - p1.time;
28
+ /* istanbul ignore next -- unreachable with sorted keys: duplicate times collapse to the last index with time <= t, so p1/p2 always have distinct times unless clamps fire first */
29
+ if (dt === 0) return p1.value;
30
+ const t = (time - p1.time) / dt;
31
+ return p1.value + (p2.value - p1.value) * t;
32
+ }
@@ -0,0 +1,17 @@
1
+ /** Pick a nice time interval in seconds for time axis labels. */
2
+ export function niceTimeInterval(windowSecs: number): number {
3
+ "worklet";
4
+ if (windowSecs <= 15) return 2;
5
+ if (windowSecs <= 30) return 5;
6
+ if (windowSecs <= 60) return 10;
7
+ if (windowSecs <= 120) return 15;
8
+ if (windowSecs <= 300) return 30;
9
+ if (windowSecs <= 600) return 60;
10
+ if (windowSecs <= 1800) return 300;
11
+ if (windowSecs <= 3600) return 600;
12
+ if (windowSecs <= 14400) return 1800;
13
+ if (windowSecs <= 43200) return 3600;
14
+ if (windowSecs <= 86400) return 7200;
15
+ if (windowSecs <= 604800) return 86400;
16
+ return 604800;
17
+ }
@@ -0,0 +1,17 @@
1
+ import { MS_PER_FRAME_60FPS } from "../constants";
2
+
3
+ /**
4
+ * Frame-rate-independent exponential lerp.
5
+ * `speed` is the fraction approached per frame at 60fps.
6
+ * At lower frame rates, dt is larger so we approach more per frame.
7
+ */
8
+ export function lerp(
9
+ current: number,
10
+ target: number,
11
+ speed: number,
12
+ dt = MS_PER_FRAME_60FPS,
13
+ ): number {
14
+ "worklet";
15
+ const factor = 1 - Math.pow(1 - speed, dt / MS_PER_FRAME_60FPS);
16
+ return current + (target - current) * factor;
17
+ }
@@ -0,0 +1,175 @@
1
+ import type { LiveChartPoint, Marker, SeriesConfig } from "../types";
2
+ import { splineValueAtTime } from "./spline";
3
+
4
+ /** Screen-space position for one marker, index-aligned with the marker array. */
5
+ export interface ProjectedMarker {
6
+ x: number;
7
+ y: number;
8
+ visible: boolean;
9
+ }
10
+
11
+ export interface ProjectMarkersOpts {
12
+ canvasWidth: number;
13
+ canvasHeight: number;
14
+ padTop: number;
15
+ padBottom: number;
16
+ padLeft: number;
17
+ padRight: number;
18
+ timestamp: number;
19
+ displayWindow: number;
20
+ displayMin: number;
21
+ displayMax: number;
22
+ /** Multi-series data, used to anchor markers by `seriesId`. */
23
+ series?: SeriesConfig[];
24
+ /** Single-series line data; anchors markers that omit `value` (and `seriesId`). */
25
+ lineData?: LiveChartPoint[];
26
+ }
27
+
28
+ /** How far off-chart (px) a marker may sit before it is culled. */
29
+ const CULL_MARGIN = 24;
30
+
31
+ /**
32
+ * Project one marker (given its primitive anchor fields) into `target`,
33
+ * without allocation. Primitives only, so per-glyph worklet closures don't
34
+ * capture the full `Marker` (which may carry non-serializable `data`/`image`).
35
+ */
36
+ function projectInto(
37
+ time: number,
38
+ value: number | undefined,
39
+ seriesId: string | undefined,
40
+ opts: ProjectMarkersOpts,
41
+ target: ProjectedMarker,
42
+ ): void {
43
+ "worklet";
44
+ const w = opts.canvasWidth;
45
+ const h = opts.canvasHeight;
46
+ const chartLeft = opts.padLeft;
47
+ const chartRight = w - opts.padRight;
48
+ const chartW = chartRight - chartLeft;
49
+ const chartTop = opts.padTop;
50
+ const chartBottom = h - opts.padBottom;
51
+ const chartH = chartBottom - chartTop;
52
+ const valRange = opts.displayMax - opts.displayMin;
53
+ if (
54
+ w === 0 ||
55
+ h === 0 ||
56
+ chartW <= 0 ||
57
+ opts.displayWindow <= 0 ||
58
+ valRange <= 0
59
+ ) {
60
+ target.visible = false;
61
+ return;
62
+ }
63
+ let v: number | null = null;
64
+ if (value !== undefined) {
65
+ v = value;
66
+ } else if (seriesId !== undefined && opts.series) {
67
+ for (let j = 0; j < opts.series.length; j++) {
68
+ if (opts.series[j].id === seriesId) {
69
+ // Evaluate the rendered spline (not the linear chord) so the glyph sits
70
+ // exactly on the curve between data points.
71
+ v = splineValueAtTime(opts.series[j].data, time);
72
+ break;
73
+ }
74
+ }
75
+ } else if (opts.lineData) {
76
+ // Single-series anchor: omit `value` to pin the marker to the line at `time`
77
+ // (on the rendered spline curve, not the linear chord between points).
78
+ v = splineValueAtTime(opts.lineData, time);
79
+ }
80
+ if (v === null) {
81
+ target.visible = false;
82
+ return;
83
+ }
84
+ const winStart = opts.timestamp - opts.displayWindow;
85
+ const x = chartLeft + ((time - winStart) / opts.displayWindow) * chartW;
86
+ const y = chartTop + ((opts.displayMax - v) / valRange) * chartH;
87
+ target.x = x;
88
+ target.y = y;
89
+ target.visible =
90
+ x >= chartLeft - CULL_MARGIN &&
91
+ x <= chartRight + CULL_MARGIN &&
92
+ y >= chartTop - CULL_MARGIN &&
93
+ y <= chartBottom + CULL_MARGIN;
94
+ }
95
+
96
+ /**
97
+ * Project markers to screen positions, mutating `out` in place (reused buffer).
98
+ * y comes from `value` (absolute) or, failing that, from interpolating the
99
+ * `seriesId` series' data at the marker's time. Returns `out`.
100
+ */
101
+ export function projectMarkers(
102
+ markers: Marker[],
103
+ out: ProjectedMarker[],
104
+ opts: ProjectMarkersOpts,
105
+ ): ProjectedMarker[] {
106
+ "worklet";
107
+ out.length = markers.length;
108
+ for (let i = 0; i < markers.length; i++) {
109
+ let cur = out[i];
110
+ if (!cur) {
111
+ cur = { x: 0, y: 0, visible: false };
112
+ out[i] = cur;
113
+ }
114
+ const m = markers[i];
115
+ projectInto(m.time, m.value, m.seriesId, opts, cur);
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /**
121
+ * Project a single marker by its anchor fields, returning a fresh
122
+ * `ProjectedMarker`. Used per-glyph at render so each glyph owns its position
123
+ * (no shared index buffer → no flicker when the marker array reorders).
124
+ */
125
+ export function projectPoint(
126
+ time: number,
127
+ value: number | undefined,
128
+ seriesId: string | undefined,
129
+ opts: ProjectMarkersOpts,
130
+ ): ProjectedMarker {
131
+ "worklet";
132
+ const target: ProjectedMarker = { x: 0, y: 0, visible: false };
133
+ projectInto(time, value, seriesId, opts, target);
134
+ return target;
135
+ }
136
+
137
+ /** Stable signature over marker id / kind / color / icon / size / image-presence
138
+ * (snapshot sync, not per-tick). */
139
+ export function markersSignature(markers: Marker[]): string {
140
+ "worklet";
141
+ let out = "";
142
+ for (let i = 0; i < markers.length; i++) {
143
+ if (i > 0) out += "\x1e";
144
+ const m = markers[i];
145
+ out += `${m.id}\x1f${m.kind}\x1f${m.color ?? ""}\x1f${m.icon ?? ""}\x1f${m.size ?? ""}\x1f${m.image ? 1 : 0}`;
146
+ }
147
+ return out;
148
+ }
149
+
150
+ /**
151
+ * Index of the nearest visible marker within `radius` px of `(x, y)`, or `-1`.
152
+ * Ties resolve to the later index (drawn on top).
153
+ */
154
+ export function nearestMarkerIndex(
155
+ positions: ProjectedMarker[],
156
+ x: number,
157
+ y: number,
158
+ radius: number,
159
+ ): number {
160
+ "worklet";
161
+ let best = -1;
162
+ let bestD = radius * radius;
163
+ for (let i = 0; i < positions.length; i++) {
164
+ const p = positions[i];
165
+ if (!p.visible) continue;
166
+ const dx = p.x - x;
167
+ const dy = p.y - y;
168
+ const d = dx * dx + dy * dy;
169
+ if (d <= bestD) {
170
+ bestD = d;
171
+ best = i;
172
+ }
173
+ }
174
+ return best;
175
+ }
@@ -0,0 +1,38 @@
1
+ import type { LiveChartPoint, Momentum } from "../types";
2
+
3
+ /**
4
+ * Auto-detect momentum from recent data points.
5
+ * Uses the full lookback window for threshold calculation
6
+ * but only the last 5 points for active velocity.
7
+ */
8
+ export function detectMomentum(
9
+ points: LiveChartPoint[],
10
+ lookback = 20,
11
+ threshold = 0.12,
12
+ ): Momentum {
13
+ "worklet";
14
+ if (points.length < 5) return "flat";
15
+
16
+ const start = Math.max(0, points.length - lookback);
17
+
18
+ let min = Infinity;
19
+ let max = -Infinity;
20
+ for (let i = start; i < points.length; i++) {
21
+ const v = points[i].value;
22
+ if (v < min) min = v;
23
+ if (v > max) max = v;
24
+ }
25
+ const range = max - min;
26
+ if (range === 0) return "flat";
27
+
28
+ const tailStart = Math.max(start, points.length - 5);
29
+ const first = points[tailStart].value;
30
+ const last = points[points.length - 1].value;
31
+ const delta = last - first;
32
+
33
+ const absThreshold = range * threshold;
34
+
35
+ if (delta > absThreshold) return "up";
36
+ if (delta < -absThreshold) return "down";
37
+ return "flat";
38
+ }
@@ -0,0 +1,42 @@
1
+ import type { CandlePoint } from "../types";
2
+
3
+ /**
4
+ * Find the candle whose time bucket contains `time`.
5
+ * Each candle spans [candle.time, candle.time + candleWidthSecs).
6
+ * Includes the live candle if it overlaps. Returns null if no match.
7
+ */
8
+ export function pickCandleAtTime(
9
+ candles: CandlePoint[],
10
+ liveCandle: CandlePoint | null,
11
+ time: number,
12
+ candleWidthSecs: number,
13
+ ): CandlePoint | null {
14
+ "worklet";
15
+ if (
16
+ liveCandle &&
17
+ time >= liveCandle.time &&
18
+ time < liveCandle.time + candleWidthSecs
19
+ ) {
20
+ return liveCandle;
21
+ }
22
+
23
+ if (candles.length === 0) return null;
24
+
25
+ // Binary search: find the last candle with time <= scrub time
26
+ let lo = 0;
27
+ let hi = candles.length - 1;
28
+ while (lo <= hi) {
29
+ const mid = (lo + hi) >> 1;
30
+ if (candles[mid].time <= time) lo = mid + 1;
31
+ else hi = mid - 1;
32
+ }
33
+ // hi is now the index of the last candle with time <= time
34
+ if (hi < 0) return null;
35
+
36
+ const c = candles[hi];
37
+ if (time >= c.time && time < c.time + candleWidthSecs) {
38
+ return c;
39
+ }
40
+
41
+ return null;
42
+ }
@@ -0,0 +1,54 @@
1
+ import type { LiveChartPoint } from "../types";
2
+
3
+ /**
4
+ * Compute visible Y range from data points + current value.
5
+ * Returns { min, max } with margin applied.
6
+ */
7
+ export function computeRange(
8
+ visible: LiveChartPoint[],
9
+ currentValue: number,
10
+ referenceValue?: number,
11
+ exaggerate?: boolean,
12
+ nonNegative?: boolean,
13
+ maxValue?: number,
14
+ ): { min: number; max: number } {
15
+ "worklet";
16
+ let targetMin = Infinity;
17
+ let targetMax = -Infinity;
18
+
19
+ for (let i = 0; i < visible.length; i++) {
20
+ const v = visible[i].value;
21
+ if (v < targetMin) targetMin = v;
22
+ if (v > targetMax) targetMax = v;
23
+ }
24
+
25
+ if (currentValue < targetMin) targetMin = currentValue;
26
+ if (currentValue > targetMax) targetMax = currentValue;
27
+
28
+ if (referenceValue !== undefined) {
29
+ if (referenceValue < targetMin) targetMin = referenceValue;
30
+ if (referenceValue > targetMax) targetMax = referenceValue;
31
+ }
32
+
33
+ const rawRange = targetMax - targetMin;
34
+ // marginFactor: breathing room above/below the data (tight for exaggerate mode)
35
+ const marginFactor = exaggerate ? 0.01 : 0.12;
36
+ // minRange: prevents the chart from collapsing to a flat line on tiny movements
37
+ const minRange =
38
+ rawRange * (exaggerate ? 0.02 : 0.1) || (exaggerate ? 0.04 : 0.4);
39
+
40
+ if (rawRange < minRange) {
41
+ const mid = (targetMin + targetMax) / 2;
42
+ targetMin = mid - minRange / 2;
43
+ targetMax = mid + minRange / 2;
44
+ } else {
45
+ const margin = rawRange * marginFactor;
46
+ targetMin -= margin;
47
+ targetMax += margin;
48
+ }
49
+
50
+ if (nonNegative && targetMin < 0) targetMin = 0;
51
+ if (maxValue !== undefined && targetMax > maxValue) targetMax = maxValue;
52
+
53
+ return { min: targetMin, max: targetMax };
54
+ }