react-native-livechart 4.24.1 → 4.25.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 (89) hide show
  1. package/dist/components/DegenParticlesOverlay.d.ts +2 -2
  2. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  3. package/dist/components/LiveChart.d.ts.map +1 -1
  4. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  5. package/dist/components/LoadingOverlay.d.ts +5 -1
  6. package/dist/components/LoadingOverlay.d.ts.map +1 -1
  7. package/dist/components/YAxisOverlay.d.ts +3 -1
  8. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  9. package/dist/constants.d.ts +3 -3
  10. package/dist/core/historyRangeCache.d.ts +25 -0
  11. package/dist/core/historyRangeCache.d.ts.map +1 -0
  12. package/dist/core/liveChartEngineTick.d.ts +4 -1
  13. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  14. package/dist/core/resolveConfig.d.ts +1 -1
  15. package/dist/core/useHistoryRevision.d.ts +5 -0
  16. package/dist/core/useHistoryRevision.d.ts.map +1 -0
  17. package/dist/core/useLiveChartEngine.d.ts +17 -1
  18. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  19. package/dist/core/useLiveChartSeriesEngine.d.ts +2 -0
  20. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  21. package/dist/draw/candleBodyPath.d.ts +10 -0
  22. package/dist/draw/candleBodyPath.d.ts.map +1 -0
  23. package/dist/draw/lineFillPaths.d.ts +10 -0
  24. package/dist/draw/lineFillPaths.d.ts.map +1 -0
  25. package/dist/draw/volume.d.ts +4 -1
  26. package/dist/draw/volume.d.ts.map +1 -1
  27. package/dist/draw/volumeRange.d.ts +12 -0
  28. package/dist/draw/volumeRange.d.ts.map +1 -0
  29. package/dist/hooks/useCandlePaths.d.ts +1 -1
  30. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  31. package/dist/hooks/useChartPaths.d.ts +1 -1
  32. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  33. package/dist/hooks/useDegen.d.ts +1 -0
  34. package/dist/hooks/useDegen.d.ts.map +1 -1
  35. package/dist/hooks/useDemandFrameLoop.d.ts +11 -0
  36. package/dist/hooks/useDemandFrameLoop.d.ts.map +1 -0
  37. package/dist/hooks/useLoadingLook.d.ts +12 -0
  38. package/dist/hooks/useLoadingLook.d.ts.map +1 -0
  39. package/dist/hooks/useMarkers.d.ts +3 -1
  40. package/dist/hooks/useMarkers.d.ts.map +1 -1
  41. package/dist/hooks/useMultiSeriesDegen.d.ts +1 -0
  42. package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -1
  43. package/dist/hooks/usePanScroll.d.ts +2 -0
  44. package/dist/hooks/usePanScroll.d.ts.map +1 -1
  45. package/dist/hooks/usePinchZoom.d.ts +2 -0
  46. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  47. package/dist/hooks/useVolumeRange.d.ts +14 -0
  48. package/dist/hooks/useVolumeRange.d.ts.map +1 -0
  49. package/dist/hooks/useXAxis.d.ts +7 -0
  50. package/dist/hooks/useXAxis.d.ts.map +1 -1
  51. package/dist/hooks/useYAxis.d.ts.map +1 -1
  52. package/dist/math/spline.d.ts +3 -1
  53. package/dist/math/spline.d.ts.map +1 -1
  54. package/dist/math/squiggly.d.ts +8 -0
  55. package/dist/math/squiggly.d.ts.map +1 -1
  56. package/dist/types.d.ts +11 -2
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/components/DegenParticlesOverlay.tsx +5 -5
  60. package/src/components/LiveChart.tsx +34 -4
  61. package/src/components/LiveChartSeries.tsx +14 -6
  62. package/src/components/LoadingOverlay.tsx +53 -12
  63. package/src/components/YAxisOverlay.tsx +38 -2
  64. package/src/constants.ts +3 -3
  65. package/src/core/historyRangeCache.ts +109 -0
  66. package/src/core/liveChartEngineTick.ts +24 -57
  67. package/src/core/resolveConfig.ts +1 -1
  68. package/src/core/useHistoryRevision.ts +36 -0
  69. package/src/core/useLiveChartEngine.ts +151 -64
  70. package/src/core/useLiveChartSeriesEngine.ts +16 -1
  71. package/src/draw/candleBodyPath.ts +38 -0
  72. package/src/draw/lineFillPaths.ts +63 -0
  73. package/src/draw/volume.ts +21 -23
  74. package/src/draw/volumeRange.ts +51 -0
  75. package/src/hooks/useCandlePaths.ts +68 -94
  76. package/src/hooks/useChartPaths.ts +87 -111
  77. package/src/hooks/useDegen.ts +10 -6
  78. package/src/hooks/useDemandFrameLoop.ts +82 -0
  79. package/src/hooks/useLoadingLook.ts +30 -0
  80. package/src/hooks/useMarkers.ts +18 -4
  81. package/src/hooks/useMultiSeriesDegen.ts +7 -3
  82. package/src/hooks/usePanScroll.ts +5 -0
  83. package/src/hooks/usePinchZoom.ts +5 -0
  84. package/src/hooks/useVolumeRange.ts +40 -0
  85. package/src/hooks/useXAxis.ts +27 -3
  86. package/src/hooks/useYAxis.ts +18 -2
  87. package/src/math/spline.ts +16 -8
  88. package/src/math/squiggly.ts +12 -0
  89. package/src/types.ts +11 -2
@@ -4,7 +4,13 @@ import {
4
4
  Path,
5
5
  type SkFont,
6
6
  } from "@shopify/react-native-skia";
7
- import { useDerivedValue, type SharedValue } from "react-native-reanimated";
7
+ import { useState } from "react";
8
+ import {
9
+ useAnimatedReaction,
10
+ useDerivedValue,
11
+ type SharedValue,
12
+ } from "react-native-reanimated";
13
+ import { scheduleOnRN } from "react-native-worklets";
8
14
  import { BADGE_METRICS_DEFAULTS, MAX_Y_LABELS } from "../constants";
9
15
  import type { ResolvedGridStyleConfig } from "../core/resolveConfig";
10
16
  import {
@@ -63,6 +69,7 @@ export function YAxisOverlay({
63
69
  side = "right",
64
70
  labelRightMargin,
65
71
  gridEndGap = 0,
72
+ minGap = 36,
66
73
  }: {
67
74
  entries: SharedValue<YAxisEntry[]>;
68
75
  engine: ChartEngineLayout;
@@ -104,6 +111,8 @@ export function YAxisOverlay({
104
111
  labelRightMargin?: number;
105
112
  /** Gap between the grid-line end and the shared label column. */
106
113
  gridEndGap?: number;
114
+ /** The grid's `minGap`: bounds how many labelled lines the plot can hold. */
115
+ minGap?: number;
107
116
  }) {
108
117
  const gridColor = gridStyle?.color ?? palette.gridLine;
109
118
  const gridWidth = gridStyle?.strokeWidth ?? 1;
@@ -190,6 +199,33 @@ export function YAxisOverlay({
190
199
  return result;
191
200
  });
192
201
 
202
+ // The label pool starts at MAX_Y_LABELS and grows to the number of labels the
203
+ // grid produces. A tall plot (or a small `minGap`) carries more labelled grid
204
+ // lines than a fixed pool held, and the extra lines were drawn without labels —
205
+ // which ones depended on key order. It grows no further than a settled grid
206
+ // can need: labelled lines stay at least half a `minGap` apart (the grid's
207
+ // hysteresis floor). Lines still fading out after a zoom can briefly outnumber
208
+ // that; for those frames some lines go unlabelled, as before. The pool never
209
+ // shrinks: a chart that fits keeps its cost, and zooming back and forth
210
+ // doesn't remount labels.
211
+ const [labelSlots, setLabelSlots] = useState(MAX_Y_LABELS);
212
+ const growLabelSlots = (slots: number) =>
213
+ setLabelSlots((current) => Math.max(current, slots));
214
+ useAnimatedReaction(
215
+ () => {
216
+ const plotHeight =
217
+ engine.canvasHeight.get() - padding.top - padding.bottom;
218
+ const settledMax = Math.floor(plotHeight / (minGap / 2)) + 1;
219
+ return Math.min(labelEntries.get().length, settledMax);
220
+ },
221
+ // Runs on every pass of the mapper: ask once per new size.
222
+ (slots, previous) => {
223
+ if (slots > labelSlots && slots !== previous) {
224
+ scheduleOnRN(growLabelSlots, slots);
225
+ }
226
+ },
227
+ );
228
+
193
229
  return (
194
230
  <Group>
195
231
  {variant !== "labels" && (
@@ -207,7 +243,7 @@ export function YAxisOverlay({
207
243
  </Group>
208
244
  )}
209
245
  {variant !== "grid" &&
210
- Array.from({ length: MAX_Y_LABELS }, (_, i) => (
246
+ Array.from({ length: labelSlots }, (_, i) => (
211
247
  <AnimatedLabel
212
248
  key={i}
213
249
  entries={labelEntries}
package/src/constants.ts CHANGED
@@ -10,9 +10,9 @@ import type {
10
10
  export const MS_PER_FRAME_60FPS = 16.67;
11
11
 
12
12
  /**
13
- * Size of the pre-allocated Y-axis label pool — the most price labels the axis
14
- * ever renders at once. Caps both the dynamic (nice-interval) grid and the
15
- * fixed-`count` mode (see {@link YAxisConfig.count}).
13
+ * Initial size of the Y-axis label pool. The pool grows when the dynamic
14
+ * (nice-interval) grid produces more labels (a tall plot or a small `minGap`);
15
+ * the fixed-`count` mode stays capped at this (see {@link YAxisConfig.count}).
16
16
  */
17
17
  export const MAX_Y_LABELS = 15;
18
18
 
@@ -0,0 +1,109 @@
1
+ import type { CandlePoint, LiveChartPoint } from "../types";
2
+
3
+ /** A fresh wrapper per SharedValue notification, including same-array modify(). */
4
+ export interface HistoryRevision<T> {
5
+ data: T[];
6
+ }
7
+
8
+ /** Per-engine, one-entry cache. Only committed history belongs here. */
9
+ export interface HistoryRangeCache {
10
+ revision: object | undefined;
11
+ data: LiveChartPoint[] | CandlePoint[] | undefined;
12
+ candle: boolean;
13
+ start: number;
14
+ end: number;
15
+ min: number;
16
+ max: number;
17
+ minTime: number;
18
+ maxTime: number;
19
+ }
20
+
21
+ export function makeHistoryRangeCache(): HistoryRangeCache {
22
+ "worklet";
23
+ return {
24
+ revision: undefined,
25
+ data: undefined,
26
+ candle: false,
27
+ start: -1,
28
+ end: -1,
29
+ min: Infinity,
30
+ max: -Infinity,
31
+ minTime: 0,
32
+ maxTime: 0,
33
+ };
34
+ }
35
+
36
+ /** Inclusive time bounds; the returned end index is exclusive. */
37
+ function timeBound(
38
+ data: { time: number }[],
39
+ time: number,
40
+ upper: boolean,
41
+ ): number {
42
+ "worklet";
43
+ let lo = 0;
44
+ let hi = data.length;
45
+ while (lo < hi) {
46
+ const mid = (lo + hi) >>> 1;
47
+ if (upper ? data[mid].time <= time : data[mid].time < time) lo = mid + 1;
48
+ else hi = mid;
49
+ }
50
+ return lo;
51
+ }
52
+
53
+ /**
54
+ * Reuse only when a trusted revision AND both visible index bounds match.
55
+ * Without a revision, always scan: identity/length cannot detect in-place edits.
56
+ * The caller owns the cache; live candles, references and animation stay outside.
57
+ */
58
+ export function historyRange(
59
+ data: LiveChartPoint[] | CandlePoint[],
60
+ candle: boolean,
61
+ from: number,
62
+ to: number,
63
+ revision: object | undefined,
64
+ cache: HistoryRangeCache,
65
+ ): HistoryRangeCache {
66
+ "worklet";
67
+ const start = timeBound(data, from, false);
68
+ const end = to === Infinity ? data.length : timeBound(data, to, true);
69
+ if (
70
+ revision !== undefined &&
71
+ cache.revision === revision &&
72
+ cache.data === data &&
73
+ cache.candle === candle &&
74
+ cache.start === start &&
75
+ cache.end === end
76
+ )
77
+ return cache;
78
+
79
+ cache.revision = revision;
80
+ cache.data = data;
81
+ cache.candle = candle;
82
+ cache.start = start;
83
+ cache.end = end;
84
+ let min = Infinity;
85
+ let max = -Infinity;
86
+ let minTime = 0;
87
+ let maxTime = 0;
88
+ for (let i = start; i < end; i++) {
89
+ const point = data[i];
90
+ const low = candle
91
+ ? (point as CandlePoint).low
92
+ : (point as LiveChartPoint).value;
93
+ const high = candle ? (point as CandlePoint).high : low;
94
+ // Strict comparisons preserve the first occurrence when extrema tie.
95
+ if (low < min) {
96
+ min = low;
97
+ minTime = point.time;
98
+ }
99
+ if (high > max) {
100
+ max = high;
101
+ maxTime = point.time;
102
+ }
103
+ }
104
+ cache.min = min;
105
+ cache.max = max;
106
+ cache.minTime = minTime;
107
+ cache.maxTime = maxTime;
108
+ return cache;
109
+ }
@@ -1,5 +1,11 @@
1
1
  import type { CandleGap, CandlePoint, LiveChartPoint } from "../types";
2
2
 
3
+ import {
4
+ historyRange,
5
+ makeHistoryRangeCache,
6
+ type HistoryRangeCache,
7
+ } from "./historyRangeCache";
8
+
3
9
  import { MOTION_METRICS_DEFAULTS } from "../constants";
4
10
  import { lerp } from "../math/lerp";
5
11
  import { thresholdRangeMinMax } from "../math/threshold";
@@ -79,6 +85,8 @@ export interface EngineTickInput {
79
85
  yRangeScale?: number;
80
86
  targetValue: number;
81
87
  points: LiveChartPoint[];
88
+ /** Internal notification revision for the selected history; omit to disable reuse. */
89
+ historyRevision?: object;
82
90
  /** Seconds since Unix epoch; defaults to `Date.now() / 1000` */
83
91
  nowSeconds?: number;
84
92
  /** Override the engine's "now" (unix seconds) — e.g. fill historical data edge-to-edge. */
@@ -187,6 +195,7 @@ export function advanceTimestampByPixel(
187
195
  export function tickLiveChartEngineFrame(
188
196
  state: EngineTickMutable,
189
197
  input: EngineTickInput,
198
+ historyCache?: HistoryRangeCache,
190
199
  ): void {
191
200
  "worklet";
192
201
  const baseNow = input.nowOverride ?? input.nowSeconds ?? Date.now() / 1000;
@@ -301,39 +310,22 @@ export function tickLiveChartEngineFrame(
301
310
 
302
311
  const winStart = state.timestamp - state.displayWindow;
303
312
 
304
- let tMin = Infinity;
305
- let tMax = -Infinity;
306
- // Time of the running min / max — captured alongside the value so an extrema
307
- // label can be pinned at the point's x. Holds the data extrema only (folded
308
- // below with the live value / references for the Y range, but snapshotted
309
- // before that into `extrema*` so the label tracks the real high/low).
310
- let minTime = 0;
311
- let maxTime = 0;
313
+ const candleMode = input.mode === "candle";
314
+ const history = historyRange(
315
+ candleMode ? (input.candles ?? []) : input.points,
316
+ candleMode,
317
+ winStart,
318
+ // Line feeds may lead the local clock. Preserve their inclusive live tail.
319
+ candleMode || scrolledBack ? state.timestamp : Infinity,
320
+ input.historyRevision,
321
+ historyCache ?? makeHistoryRangeCache(),
322
+ );
323
+ let tMin = history.min;
324
+ let tMax = history.max;
325
+ let minTime = history.minTime;
326
+ let maxTime = history.maxTime;
312
327
 
313
- if (input.mode === "candle") {
314
- const candles = input.candles;
315
- if (candles && candles.length > 0) {
316
- let lo = 0;
317
- let hi = candles.length;
318
- while (lo < hi) {
319
- const mid = (lo + hi) >> 1;
320
- if (candles[mid].time < winStart) lo = mid + 1;
321
- else hi = mid;
322
- }
323
- for (let i = lo; i < candles.length; i++) {
324
- if (candles[i].time > state.timestamp) break;
325
- /* istanbul ignore next -- trivial min/max */
326
- if (candles[i].low < tMin) {
327
- tMin = candles[i].low;
328
- minTime = candles[i].time;
329
- }
330
- /* istanbul ignore next -- trivial min/max */
331
- if (candles[i].high > tMax) {
332
- tMax = candles[i].high;
333
- maxTime = candles[i].time;
334
- }
335
- }
336
- }
328
+ if (candleMode) {
337
329
  const lc = input.liveCandle;
338
330
  // Fold the in-progress candle in only while its bucket is visible — when
339
331
  // scrolled back past it, the live candle must not stretch the Y range.
@@ -349,31 +341,6 @@ export function tickLiveChartEngineFrame(
349
341
  maxTime = lc.time;
350
342
  }
351
343
  }
352
- } else {
353
- const points = input.points;
354
- let lo = 0;
355
- let hi = points.length;
356
- while (lo < hi) {
357
- const mid = (lo + hi) >> 1;
358
- if (points[mid].time < winStart) lo = mid + 1;
359
- else hi = mid;
360
- }
361
- for (let i = lo; i < points.length; i++) {
362
- // While scrolled back, stop at the frozen right edge (mirrors the candle
363
- // scan) so newer points don't inflate the visible Y range. Following
364
- // live, keep the tail inclusive — feed timestamps can run slightly ahead
365
- // of the local clock and must not flicker out of the range.
366
- if (scrolledBack && points[i].time > state.timestamp) break;
367
- const v = points[i].value;
368
- if (v < tMin) {
369
- tMin = v;
370
- minTime = points[i].time;
371
- }
372
- if (v > tMax) {
373
- tMax = v;
374
- maxTime = points[i].time;
375
- }
376
- }
377
344
  }
378
345
 
379
346
  // Snapshot the raw data extrema (value + time) before the live value /
@@ -545,7 +545,7 @@ const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
545
545
  * Resolves `yAxis` prop to a fully-typed config or null (disabled).
546
546
  * `true` → defaults, object → merged with defaults, falsy → null.
547
547
  * `count` is normalized to a non-negative integer (the grid math clamps the
548
- * upper bound to the label pool size).
548
+ * upper bound to `MAX_Y_LABELS`).
549
549
  */
550
550
  export function resolveYAxis(
551
551
  prop: boolean | YAxisConfig | undefined,
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useSharedValue, type SharedValue } from "react-native-reanimated";
3
+ import { scheduleOnUI } from "react-native-worklets";
4
+ import type { HistoryRevision } from "./historyRangeCache";
5
+
6
+ // Reanimated's mapper IDs are positive. Separate IDs also let multiple charts
7
+ // subscribe to the same feed without replacing each other's listeners.
8
+ let nextHistoryListenerId = -1;
9
+
10
+ /** Invalidate synchronously: a frame can run before derived mappers flush. */
11
+ export function useHistoryRevision<T>(
12
+ data: SharedValue<T[]> | undefined,
13
+ source: SharedValue<T[]> | undefined = data,
14
+ ): SharedValue<HistoryRevision<T> | undefined> {
15
+ const revision = useSharedValue<HistoryRevision<T> | undefined>(undefined);
16
+ const [listenerId] = useState(() => nextHistoryListenerId--);
17
+ useEffect(() => {
18
+ scheduleOnUI(() => {
19
+ "worklet";
20
+ const refresh = () => {
21
+ "worklet";
22
+ revision.set({ data: data?.get() ?? [] });
23
+ };
24
+ data?.addListener(listenerId, refresh);
25
+ if (source !== data) source?.addListener(listenerId, refresh);
26
+ refresh();
27
+ });
28
+ return () =>
29
+ scheduleOnUI(() => {
30
+ "worklet";
31
+ data?.removeListener(listenerId);
32
+ if (source !== data) source?.removeListener(listenerId);
33
+ });
34
+ }, [data, source, revision, listenerId]);
35
+ return revision;
36
+ }