react-native-livechart 3.0.0 → 3.1.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 (63) hide show
  1. package/README.md +9 -3
  2. package/dist/components/AxisLabelOverlay.d.ts +29 -0
  3. package/dist/components/AxisLabelOverlay.d.ts.map +1 -0
  4. package/dist/components/CrosshairLine.d.ts +12 -1
  5. package/dist/components/CrosshairLine.d.ts.map +1 -1
  6. package/dist/components/CrosshairOverlay.d.ts +15 -1
  7. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  8. package/dist/components/DegenParticlesOverlay.d.ts +17 -0
  9. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  10. package/dist/components/LiveChart.d.ts.map +1 -1
  11. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  12. package/dist/components/SelectionDot.d.ts +32 -0
  13. package/dist/components/SelectionDot.d.ts.map +1 -0
  14. package/dist/core/resolveConfig.d.ts +46 -1
  15. package/dist/core/resolveConfig.d.ts.map +1 -1
  16. package/dist/core/useLiveChartEngine.d.ts +6 -0
  17. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  18. package/dist/draw/particleAtlas.d.ts +39 -0
  19. package/dist/draw/particleAtlas.d.ts.map +1 -0
  20. package/dist/hooks/crosshairShared.d.ts +10 -0
  21. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  22. package/dist/hooks/useCandlePaths.d.ts +3 -1
  23. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  24. package/dist/hooks/useChartColors.d.ts +5 -0
  25. package/dist/hooks/useChartColors.d.ts.map +1 -1
  26. package/dist/hooks/useChartReveal.d.ts +3 -1
  27. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  28. package/dist/hooks/useCrosshair.d.ts +1 -1
  29. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  30. package/dist/hooks/useCrosshairSeries.d.ts +1 -1
  31. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
  32. package/dist/hooks/useDegen.d.ts +3 -1
  33. package/dist/hooks/useDegen.d.ts.map +1 -1
  34. package/dist/hooks/useMarkers.d.ts +3 -1
  35. package/dist/hooks/useMarkers.d.ts.map +1 -1
  36. package/dist/hooks/useTradeStream.d.ts +3 -1
  37. package/dist/hooks/useTradeStream.d.ts.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/types.d.ts +97 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/components/AxisLabelOverlay.tsx +133 -0
  44. package/src/components/CrosshairLine.tsx +27 -0
  45. package/src/components/CrosshairOverlay.tsx +50 -18
  46. package/src/components/DegenParticlesOverlay.tsx +93 -109
  47. package/src/components/LiveChart.tsx +66 -10
  48. package/src/components/LiveChartSeries.tsx +40 -0
  49. package/src/components/SelectionDot.tsx +92 -0
  50. package/src/core/resolveConfig.ts +98 -0
  51. package/src/core/useLiveChartEngine.ts +78 -27
  52. package/src/draw/particleAtlas.ts +95 -0
  53. package/src/hooks/crosshairShared.ts +26 -0
  54. package/src/hooks/useCandlePaths.ts +3 -1
  55. package/src/hooks/useChartColors.ts +19 -0
  56. package/src/hooks/useChartReveal.ts +7 -4
  57. package/src/hooks/useCrosshair.ts +46 -7
  58. package/src/hooks/useCrosshairSeries.ts +47 -8
  59. package/src/hooks/useDegen.ts +6 -0
  60. package/src/hooks/useMarkers.ts +3 -0
  61. package/src/hooks/useTradeStream.ts +3 -0
  62. package/src/index.ts +4 -0
  63. package/src/types.ts +101 -0
@@ -5,6 +5,7 @@ import {
5
5
  useDerivedValue,
6
6
  useSharedValue,
7
7
  } from "react-native-reanimated";
8
+ import { scheduleOnRN } from "react-native-worklets";
8
9
  import type { MultiEngineState } from "../core/useLiveChartEngine";
9
10
  import { type ChartPadding } from "../draw/line";
10
11
  import type { ScrubPointMulti } from "../types";
@@ -14,6 +15,7 @@ import {
14
15
  } from "./crosshairSeries";
15
16
  import {
16
17
  computeCrosshairOpacity,
18
+ computeScrubDotY,
17
19
  computeScrubTime,
18
20
  HIDDEN_TOOLTIP,
19
21
  type CrosshairState,
@@ -30,9 +32,14 @@ export function useCrosshairSeries(
30
32
  onScrub?: (point: ScrubPointMulti | null) => void,
31
33
  /** Press-and-hold delay (ms) before scrubbing activates. 0 = immediate. */
32
34
  panGestureDelay = 0,
35
+ onGestureStart?: () => void,
36
+ onGestureEnd?: () => void,
33
37
  ): CrosshairState {
34
38
  const scrubX = useSharedValue(-1);
35
39
  const scrubActive = useSharedValue(false);
40
+ // Tracks whether the active scrub phase actually began, so a tap that never
41
+ // activates doesn't emit a spurious onGestureEnd.
42
+ const gestureStarted = useSharedValue(false);
36
43
 
37
44
  const scrubTime = useDerivedValue(() =>
38
45
  computeScrubTime(
@@ -62,9 +69,34 @@ export function useCrosshairSeries(
62
69
  ),
63
70
  );
64
71
 
72
+ // Y pixel of the scrub intersection (leading-series value) — used by the
73
+ // selection dot. -1 when there's no value to mark.
74
+ const scrubDotY = useDerivedValue(() =>
75
+ computeScrubDotY(
76
+ scrubValue.get(),
77
+ engine.displayMin.get(),
78
+ engine.displayMax.get(),
79
+ engine.canvasHeight.get(),
80
+ padding.top,
81
+ padding.bottom,
82
+ ),
83
+ );
84
+
65
85
  const tooltipLayout = useSharedValue(HIDDEN_TOOLTIP);
66
86
 
87
+ /* istanbul ignore next */
88
+ function handleGestureStart() {
89
+ onGestureStart?.();
90
+ }
91
+
92
+ /* istanbul ignore next */
93
+ function handleGestureEnd() {
94
+ onGestureEnd?.();
95
+ }
96
+
67
97
  const hasOnScrub = onScrub != null;
98
+ const hasOnGestureStart = onGestureStart != null;
99
+ const hasOnGestureEnd = onGestureEnd != null;
68
100
 
69
101
  useAnimatedReaction(
70
102
  () => {
@@ -77,14 +109,14 @@ export function useCrosshairSeries(
77
109
  if (chartW <= 0) return "__pending__";
78
110
  const r = interpolateSeriesAtTime(engine.series.get(), time);
79
111
  if (r.primary === null) return "__pending__";
80
- const h = engine.canvasHeight.get();
81
- const chartH = h - padding.top - padding.bottom;
82
- const valRange = engine.displayMax.get() - engine.displayMin.get();
83
- const dotY =
84
- valRange === 0
85
- ? padding.top + chartH / 2
86
- : padding.top +
87
- ((engine.displayMax.get() - r.primary) / valRange) * chartH;
112
+ const dotY = computeScrubDotY(
113
+ r.primary,
114
+ engine.displayMin.get(),
115
+ engine.displayMax.get(),
116
+ engine.canvasHeight.get(),
117
+ padding.top,
118
+ padding.bottom,
119
+ );
88
120
  return JSON.stringify({
89
121
  time,
90
122
  x,
@@ -136,6 +168,8 @@ export function useCrosshairSeries(
136
168
  if (!enabled) return;
137
169
  scrubX.set(e.x);
138
170
  scrubActive.set(true);
171
+ gestureStarted.set(true);
172
+ if (hasOnGestureStart) scheduleOnRN(handleGestureStart);
139
173
  },
140
174
  )
141
175
  .onUpdate(
@@ -149,6 +183,10 @@ export function useCrosshairSeries(
149
183
  /* istanbul ignore next */ () => {
150
184
  "worklet";
151
185
  scrubActive.set(false);
186
+ if (gestureStarted.get()) {
187
+ gestureStarted.set(false);
188
+ if (hasOnGestureEnd) scheduleOnRN(handleGestureEnd);
189
+ }
152
190
  },
153
191
  );
154
192
 
@@ -164,6 +202,7 @@ export function useCrosshairSeries(
164
202
  scrubValue,
165
203
  crosshairOpacity,
166
204
  tooltipLayout,
205
+ scrubDotY,
167
206
  gesture,
168
207
  };
169
208
  }
@@ -20,6 +20,8 @@ export function useDegen(
20
20
  momentumSV: SharedValue<Momentum>,
21
21
  cfg: ResolvedDegenConfig | null,
22
22
  onShake?: (payload: DegenShakePayload) => void,
23
+ /** Static charts run no loops: register the frame callback without starting it. */
24
+ isStatic = false,
23
25
  ): {
24
26
  pack: SharedValue<Float64Array<ArrayBuffer>>;
25
27
  packRevision: SharedValue<number>;
@@ -248,6 +250,10 @@ export function useDegen(
248
250
  }
249
251
  prevActiveCount.set(activeCount);
250
252
  },
253
+ // Static charts register the callback but never start it, so no degen frame
254
+ // loop runs. Live charts keep the default (autostart) loop unchanged. The
255
+ // controller also forces `cfg` to null in static, so degen is doubly off.
256
+ !isStatic,
251
257
  );
252
258
 
253
259
  const shakeTransform = useDerivedValue(() => {
@@ -30,6 +30,8 @@ export function useMarkers(
30
30
  onMarkerHover?: (event: MarkerHoverEvent | null) => void,
31
31
  seriesSV?: SharedValue<SeriesConfig[]>,
32
32
  lineData?: SharedValue<LiveChartPoint[]>,
33
+ /** Static charts run no loops: register without starting. Default `true`. */
34
+ autostart = true,
33
35
  ): { projected: SharedValue<ProjectedMarker[]>; tapGesture: ReturnType<typeof Gesture.Tap> } {
34
36
  const projected = useSharedValue<ProjectedMarker[]>([]);
35
37
  const cacheRef = useRef<{
@@ -73,6 +75,7 @@ export function useMarkers(
73
75
  });
74
76
  projected.set(buf);
75
77
  },
78
+ autostart,
76
79
  );
77
80
 
78
81
  const tapGesture = Gesture.Tap().onEnd(
@@ -25,6 +25,8 @@ export function useTradeStream(
25
25
  tradeStream: SharedValue<TradeEvent[]>,
26
26
  padding: ChartPadding,
27
27
  active: boolean,
28
+ /** Static charts run no loops: register without starting. Default `true`. */
29
+ autostart = true,
28
30
  ): SharedValue<TradeMarker[]> {
29
31
  const markers = useSharedValue<TradeMarker[]>([]);
30
32
  const state = useSharedValue<TradeStreamState>(createTradeStreamState());
@@ -58,6 +60,7 @@ export function useTradeStream(
58
60
  const chartH = chartBottom - chartTop;
59
61
  markers.set(projectLabels(s, chartTop, chartH));
60
62
  },
63
+ autostart,
61
64
  );
62
65
 
63
66
  return markers;
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export { useTradeStream } from "./hooks/useTradeStream";
28
28
  // ── Types ────────────────────────────────────────────────────────────────────
29
29
 
30
30
  export type {
31
+ AxisLabelConfig,
31
32
  BadgeConfig,
32
33
  BadgeMetrics,
33
34
  BadgeVariant,
@@ -67,6 +68,9 @@ export type {
67
68
  ScrubPointCore,
68
69
  ScrubPointMulti,
69
70
  ScrubSeriesValue,
71
+ SelectionDotConfig,
72
+ SelectionDotProps,
73
+ SelectionDotRingConfig,
70
74
  SeriesConfig,
71
75
  ThemeMode,
72
76
  TradeEvent,
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ComponentType, ReactElement } from "react";
1
2
  import type { ViewStyle } from "react-native";
2
3
  import type { SharedValue } from "react-native-reanimated";
3
4
 
@@ -150,6 +151,11 @@ export interface GradientConfig {
150
151
  topOpacity?: number;
151
152
  /** Opacity at the bottom of the gradient. Default `0`. */
152
153
  bottomOpacity?: number;
154
+ /** Explicit gradient color stops (top → bottom) for the area fill. Overrides
155
+ * topOpacity/bottomOpacity when provided. Must have at least 2 entries. */
156
+ colors?: string[];
157
+ /** Optional stop positions (0..1, ascending) matching `colors` length. */
158
+ positions?: number[];
153
159
  }
154
160
 
155
161
  /** Value badge pill configuration. */
@@ -170,6 +176,24 @@ export interface YAxisConfig {
170
176
  minGap?: number;
171
177
  }
172
178
 
179
+ /**
180
+ * Axis edge label — the value floated at the plot's top or bottom edge
181
+ * (Robinhood-style high/low). Pass `topLabel`/`bottomLabel` as `true` for the
182
+ * batteries-included value label (the chart's current top/bottom Y-axis bound,
183
+ * updated each frame), an object to configure it, or `{ render }` to float a
184
+ * fully custom element instead.
185
+ */
186
+ export interface AxisLabelConfig {
187
+ /** Formatter for the built-in value. Defaults to the chart's `formatValue`. */
188
+ format?: (v: number) => string;
189
+ /** Text color. Defaults to a muted label color (`palette.gridLabel`). */
190
+ color?: string;
191
+ /** Horizontal alignment within the plot width. Default `"right"`. */
192
+ position?: "left" | "right";
193
+ /** Full custom element, floated at the edge. Overrides the built-in value label. */
194
+ render?: () => ReactElement | null;
195
+ }
196
+
173
197
  /** X-axis (time) configuration. */
174
198
  export interface XAxisConfig {
175
199
  /** Minimum pixel gap between time labels. Default `60`. */
@@ -213,6 +237,55 @@ export interface ScrubConfig {
213
237
  panGestureDelay?: number;
214
238
  }
215
239
 
240
+ /**
241
+ * Props passed to a custom {@link SelectionDotConfig.component} — the dot drawn
242
+ * at the scrub intersection while scrubbing. All positional inputs are
243
+ * SharedValues so the dot animates on the UI thread without re-renders.
244
+ */
245
+ export interface SelectionDotProps {
246
+ /** Scrub X in canvas px. */
247
+ x: SharedValue<number>;
248
+ /** Scrub Y in canvas px (the line/value intersection). */
249
+ y: SharedValue<number>;
250
+ /** Whether scrubbing is active. */
251
+ active: SharedValue<boolean>;
252
+ /** Crosshair fade opacity (0..1), already ramped. */
253
+ opacity: SharedValue<number>;
254
+ /** Resolved dot color (accent/series color). */
255
+ color: string;
256
+ /** Suggested dot radius in px. */
257
+ size: number;
258
+ }
259
+
260
+ /** Outer ring drawn around the built-in selection dot (the subtle halo). */
261
+ export interface SelectionDotRingConfig {
262
+ /** Ring color. Defaults to the dot color. */
263
+ color?: string;
264
+ /** Ring thickness in pixels — how far the halo extends past the dot. Default `2`. */
265
+ width?: number;
266
+ }
267
+
268
+ /**
269
+ * Selection-dot styling — the dot drawn at the scrub intersection while
270
+ * scrubbing. Pass `selectionDot={false}` to hide it, an object to configure the
271
+ * built-in dot, or `{ component }` to supply a fully custom Skia dot.
272
+ */
273
+ export interface SelectionDotConfig {
274
+ /** Dot radius in px. Default `4`. */
275
+ size?: number;
276
+ /** Dot color. Defaults to the line / leading-series color. */
277
+ color?: string;
278
+ /**
279
+ * Outer ring around the dot. `true`/config = on, `false` = off. Default on.
280
+ */
281
+ ring?: boolean | SelectionDotRingConfig;
282
+ /**
283
+ * Fully custom Skia dot — receives the scrub position as SharedValues. When
284
+ * set, the `size` / `color` / `ring` knobs are ignored.
285
+ */
286
+ component?: ComponentType<SelectionDotProps>;
287
+ }
288
+
216
289
  /** Left-edge fade — soft erase so the chart blends into the left gutter (web liveline parity). */
217
290
  export interface LeftEdgeFadeConfig {
218
291
  /**
@@ -695,6 +768,20 @@ export interface LiveChartCoreProps {
695
768
  yAxis?: boolean | YAxisConfig;
696
769
  /** X-axis time labels. `true` = defaults, `false` = hidden, or pass `XAxisConfig`. Default `true`. */
697
770
  xAxis?: boolean | XAxisConfig;
771
+ /**
772
+ * Label floated at the top edge of the plot area. `true` = the built-in value
773
+ * label showing the chart's current TOP Y-axis bound (updated each frame),
774
+ * `false`/omitted = none, or pass `AxisLabelConfig` to configure it (or supply
775
+ * a custom `render`). Default off.
776
+ */
777
+ topLabel?: boolean | AxisLabelConfig;
778
+ /**
779
+ * Label floated at the bottom edge of the plot area. `true` = the built-in
780
+ * value label showing the chart's current BOTTOM Y-axis bound (updated each
781
+ * frame), `false`/omitted = none, or pass `AxisLabelConfig` (or a custom
782
+ * `render`). Default off.
783
+ */
784
+ bottomLabel?: boolean | AxisLabelConfig;
698
785
  /** Reference lines / bands drawn into the chart. Supports all three `ReferenceLine` forms. */
699
786
  referenceLines?: ReferenceLine[];
700
787
  /** Per-instance grid-line styling. Pass an object to override color / width / dash / opacity. */
@@ -737,6 +824,16 @@ export interface LiveChartCoreProps {
737
824
  accessibilityRole?: "image" | "none" | "adjustable" | "summary";
738
825
  /** Crosshair scrubbing on hover/drag. `true` = defaults, `false` = disabled, or pass `ScrubConfig`. Default `true`. */
739
826
  scrub?: boolean | ScrubConfig;
827
+ /**
828
+ * Selection dot drawn at the scrub intersection while scrubbing. `true`/omitted
829
+ * = built-in dot, `false` = hidden, or pass `SelectionDotConfig` (`size`,
830
+ * `color`, `ring`, or a custom `component`). Default `true`.
831
+ */
832
+ selectionDot?: boolean | SelectionDotConfig;
833
+ /** Called once when the user starts scrubbing/panning the chart. */
834
+ onGestureStart?: () => void;
835
+ /** Called once when the user stops scrubbing/panning the chart. */
836
+ onGestureEnd?: () => void;
740
837
  /**
741
838
  * Fade out chart content on the left (destination-out style). `true` = defaults, `false` = off,
742
839
  * or pass `LeftEdgeFadeConfig`. Default `true`.
@@ -796,6 +893,10 @@ export interface LiveChartProps extends LiveChartCoreProps {
796
893
  data: SharedValue<LiveChartPoint[]>;
797
894
  /** Latest live value for smooth interpolation between data updates. */
798
895
  value: SharedValue<number>;
896
+ /** Render once with no per-frame animation loop — for many small charts (sparklines)
897
+ * in a list. Pulse/scrub/degen and the entry animation are disabled. Frame the data
898
+ * with `timeWindow` + `nowOverride` (see the historical-data-fill pattern). */
899
+ static?: boolean;
799
900
  /** Called when the user scrubs the crosshair. `null` when scrub ends. */
800
901
  onScrub?: (point: ScrubPoint | null) => void;
801
902
  /**