react-native-vroom-chart 0.7.0 → 0.9.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.
package/lib/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/VroomChart.tsx
2
- import React, { useEffect as useEffect2, useRef as useRef2, useCallback, useState as useState2, useMemo } from "react";
2
+ import React, { useEffect as useEffect2, useRef as useRef2, useCallback as useCallback2, useState as useState2, useMemo } from "react";
3
3
  import { View } from "react-native";
4
4
  import { Canvas, Picture, Skia } from "@shopify/react-native-skia";
5
5
  import {
@@ -7,15 +7,95 @@ import {
7
7
  GestureDetector,
8
8
  GestureHandlerRootView
9
9
  } from "react-native-gesture-handler";
10
- import { useSharedValue } from "react-native-reanimated";
10
+ import { useReducedMotion, useSharedValue } from "react-native-reanimated";
11
11
 
12
12
  // src/useChartCore.ts
13
- import { useEffect, useRef, useState } from "react";
13
+ import { useCallback, useEffect, useRef, useState } from "react";
14
14
 
15
15
  // src/NativeVroomChart.ts
16
16
  import { TurboModuleRegistry } from "react-native";
17
17
  var NativeVroomChart_default = TurboModuleRegistry.getEnforcing("VroomChartModule");
18
18
 
19
+ // src/dataTransitions.ts
20
+ var STEP_TOLERANCE = 0.01;
21
+ var MAX_SAME_ASSET_CLOSE_RATIO = 1.25;
22
+ var MAX_END_DRIFT_STEPS = 3;
23
+ var MAX_STREAM_ADVANCE_STEPS = 5;
24
+ function inferStepMs(candles) {
25
+ if (candles.length < 2) return null;
26
+ const k = Math.min(candles.length - 1, 8);
27
+ const diffs = [];
28
+ for (let i = 0; i < k; i++) diffs.push(candles[i + 1].timeMs - candles[i].timeMs);
29
+ diffs.sort((a, b) => a - b);
30
+ const median = diffs[Math.floor(diffs.length / 2)];
31
+ return median > 0 ? median : null;
32
+ }
33
+ function indexByTime(candles, t) {
34
+ let lo = 0;
35
+ let hi = candles.length - 1;
36
+ while (lo <= hi) {
37
+ const mid = lo + hi >>> 1;
38
+ const v = candles[mid].timeMs;
39
+ if (v === t) return mid;
40
+ if (v < t) lo = mid + 1;
41
+ else hi = mid - 1;
42
+ }
43
+ return -1;
44
+ }
45
+ function classifyTransition(prev, next, seriesKeyChanged) {
46
+ if (!prev || prev.length === 0) return "initial";
47
+ if (next.length === 0) return "stream";
48
+ if (seriesKeyChanged) return "reset";
49
+ const prevStep = inferStepMs(prev);
50
+ const nextStep = inferStepMs(next);
51
+ if (prevStep == null || nextStep == null) return "reset";
52
+ const prevLast = prev[prev.length - 1];
53
+ const nextLast = next[next.length - 1];
54
+ if (Math.abs(nextStep - prevStep) <= prevStep * STEP_TOLERANCE) {
55
+ const idx = indexByTime(next, prevLast.timeMs);
56
+ const aligned = idx >= 0;
57
+ const sharedBarRatio = aligned && next[idx].close > 0 && prevLast.close > 0 ? Math.max(next[idx].close / prevLast.close, prevLast.close / next[idx].close) : Infinity;
58
+ const advanced = nextLast.timeMs >= prevLast.timeMs && nextLast.timeMs - prevLast.timeMs <= MAX_STREAM_ADVANCE_STEPS * nextStep;
59
+ return sharedBarRatio <= MAX_SAME_ASSET_CLOSE_RATIO && advanced ? "stream" : "reset";
60
+ }
61
+ const closeRatio = prevLast.close > 0 && nextLast.close > 0 ? Math.max(nextLast.close / prevLast.close, prevLast.close / nextLast.close) : Infinity;
62
+ const prevEnd = prevLast.timeMs + prevStep;
63
+ const nextEnd = nextLast.timeMs + nextStep;
64
+ const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
65
+ return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
66
+ }
67
+ function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
68
+ const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
69
+ const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
70
+ const offsetSlots = Math.min(Math.max(offsetRaw, 0), slots * 0.75);
71
+ const endMs = Math.round(newLastMs + offsetSlots * newStepMs);
72
+ return { startMs: Math.round(endMs - slots * newStepMs), endMs };
73
+ }
74
+
75
+ // src/easing.ts
76
+ function ease(kind, p) {
77
+ switch (kind) {
78
+ case "linear":
79
+ return p;
80
+ case "ease-in":
81
+ return p * p;
82
+ case "ease-out":
83
+ return p * (2 - p);
84
+ default:
85
+ return p * p * (3 - 2 * p);
86
+ }
87
+ }
88
+ var EASINGS = [
89
+ "linear",
90
+ "ease-in",
91
+ "ease-out",
92
+ "ease-in-out"
93
+ ];
94
+ function easingIndex(kind) {
95
+ const i = kind ? EASINGS.indexOf(kind) : -1;
96
+ return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
97
+ }
98
+
19
99
  // src/packCandles.ts
20
100
  var BYTES_PER_CANDLE = 48;
21
101
  function packCandles(candles) {
@@ -74,12 +154,18 @@ var FLOAT_KEYS = {
74
154
  // VROOM_FLOAT_VOLUME_RADIUS_PX
75
155
  lineWidth: 11,
76
156
  // VROOM_FLOAT_LINE_WIDTH_PX
77
- lineGradientOpacity: 12
157
+ lineGradientOpacity: 12,
78
158
  // VROOM_FLOAT_LINE_GRADIENT_OPACITY
159
+ lineTension: 13
160
+ // VROOM_FLOAT_LINE_TENSION
79
161
  };
162
+ var FLOAT_LINE_TIP_PULSE = 15;
80
163
  var BOOL_KEYS = {
81
- wickRoundCap: 9
164
+ wickRoundCap: 9,
82
165
  // VROOM_FLOAT_WICK_ROUND_CAP
166
+ lineTipDot: 14,
167
+ // VROOM_FLOAT_LINE_TIP_DOT
168
+ lineTipPulse: FLOAT_LINE_TIP_PULSE
83
169
  };
84
170
  function parseColor(value) {
85
171
  if (typeof value === "number") {
@@ -250,15 +336,53 @@ function ensureInstalled() {
250
336
  }
251
337
  installed = true;
252
338
  }
253
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines) {
339
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
254
340
  const handleRef = useRef(null);
255
341
  const defaultWidthAppliedRef = useRef(false);
256
342
  const volumeCollapseRef = useRef(null);
343
+ const prevDataRef = useRef(null);
344
+ const intervalMorphRaf = useRef(null);
257
345
  const [picture, setPicture] = useState(null);
258
346
  if (!handleRef.current && size.width > 0 && size.height > 0) {
259
347
  ensureInstalled();
260
348
  handleRef.current = globalThis.VroomChartJSI.create();
261
349
  }
350
+ const animRef = useRef({ ms: 300, easing: void 0, reduceMotion: false });
351
+ animRef.current = {
352
+ ms: Math.max(0, transition?.transitionMs ?? 300),
353
+ easing: transition?.transitionEasing,
354
+ reduceMotion: transition?.reduceMotion ?? false
355
+ };
356
+ const onFrameRef = useRef(transition?.onFrame);
357
+ onFrameRef.current = transition?.onFrame;
358
+ const seriesKey = transition?.seriesKey;
359
+ const endIntervalMorph = useCallback(() => {
360
+ if (intervalMorphRaf.current != null) {
361
+ cancelAnimationFrame(intervalMorphRaf.current);
362
+ intervalMorphRaf.current = null;
363
+ }
364
+ handleRef.current?.setIntervalMorph(1);
365
+ }, []);
366
+ const startIntervalMorph = useCallback((h) => {
367
+ const { ms, easing } = animRef.current;
368
+ const start = performance.now();
369
+ const step = (now) => {
370
+ const p = Math.min(1, (now - start) / ms);
371
+ h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
372
+ const pic = h.render();
373
+ if (pic) onFrameRef.current?.(pic);
374
+ intervalMorphRaf.current = p < 1 ? requestAnimationFrame(step) : null;
375
+ };
376
+ intervalMorphRaf.current = requestAnimationFrame(step);
377
+ }, []);
378
+ useEffect(() => {
379
+ return () => {
380
+ if (intervalMorphRaf.current != null) {
381
+ cancelAnimationFrame(intervalMorphRaf.current);
382
+ intervalMorphRaf.current = null;
383
+ }
384
+ };
385
+ }, []);
262
386
  const explicit = visibleRange != null;
263
387
  const startMs = visibleRange?.startMs ?? 0;
264
388
  const endMs = visibleRange?.endMs ?? 0;
@@ -278,8 +402,54 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
278
402
  h.setDefaultCandleWidth(defaultCandleWidth);
279
403
  defaultWidthAppliedRef.current = true;
280
404
  }
405
+ let morphing = false;
281
406
  if (candles.length > 0) {
282
- h.setCandles(packCandles(candles));
407
+ const prev = prevDataRef.current;
408
+ const freshHandle = prev == null || prev.handle !== h;
409
+ if (freshHandle || prev.candles !== candles || prev.seriesKey !== seriesKey) {
410
+ const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
411
+ let tfArgs = null;
412
+ let prevEnvelope = null;
413
+ if (transitionKind === "timeframe" && prev != null) {
414
+ const oldWindow = h.getVisibleRange();
415
+ const oldStepMs = inferStepMs(prev.candles);
416
+ if (oldWindow.endMs > oldWindow.startMs && oldStepMs != null) {
417
+ tfArgs = {
418
+ oldWindow,
419
+ oldStepMs,
420
+ oldLastMs: prev.candles[prev.candles.length - 1].timeMs
421
+ };
422
+ }
423
+ prevEnvelope = h.getVisiblePriceEnvelope();
424
+ morphing = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
425
+ if (morphing) {
426
+ endIntervalMorph();
427
+ h.beginIntervalMorph();
428
+ }
429
+ } else if (transitionKind === "initial" || transitionKind === "reset") {
430
+ endIntervalMorph();
431
+ }
432
+ h.setCandles(packCandles(candles));
433
+ if (transitionKind === "timeframe") {
434
+ const newStepMs = inferStepMs(candles);
435
+ if (tfArgs && newStepMs != null) {
436
+ const w = timeframeWindow(
437
+ tfArgs.oldWindow,
438
+ tfArgs.oldStepMs,
439
+ tfArgs.oldLastMs,
440
+ newStepMs,
441
+ candles[candles.length - 1].timeMs
442
+ );
443
+ h.setVisibleRange(w.startMs, w.endMs);
444
+ }
445
+ if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
446
+ else h.resetPriceScale();
447
+ if (morphing) startIntervalMorph(h);
448
+ } else if (transitionKind === "reset") {
449
+ h.resetView();
450
+ }
451
+ prevDataRef.current = { handle: h, candles, seriesKey };
452
+ }
283
453
  }
284
454
  if (explicit) {
285
455
  h.setVisibleRange(startMs, endMs);
@@ -287,6 +457,9 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
287
457
  if (theme) {
288
458
  applyTheme(h, theme);
289
459
  }
460
+ if (animRef.current.reduceMotion) {
461
+ h.setFloat(FLOAT_LINE_TIP_PULSE, 0);
462
+ }
290
463
  h.setRSI(rsiToSpec(rsi));
291
464
  h.setMACD(macdToSpec(macd));
292
465
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
@@ -298,39 +471,16 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
298
471
  h.setPriceLines(
299
472
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
300
473
  );
301
- setPicture(h.render());
302
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey]);
474
+ if (!morphing) setPicture(h.render());
475
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
303
476
  return { handle: handleRef.current, picture, volumeCollapseRef };
304
477
  }
305
478
 
306
- // src/easing.ts
307
- function ease(kind, p) {
308
- switch (kind) {
309
- case "linear":
310
- return p;
311
- case "ease-in":
312
- return p * p;
313
- case "ease-out":
314
- return p * (2 - p);
315
- default:
316
- return p * p * (3 - 2 * p);
317
- }
318
- }
319
- var EASINGS = [
320
- "linear",
321
- "ease-in",
322
- "ease-out",
323
- "ease-in-out"
324
- ];
325
- function easingIndex(kind) {
326
- const i = kind ? EASINGS.indexOf(kind) : -1;
327
- return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
328
- }
329
-
330
479
  // src/VroomChart.tsx
331
480
  function VroomChart(props) {
332
481
  const {
333
482
  candles,
483
+ seriesKey,
334
484
  width: widthProp,
335
485
  height: heightProp,
336
486
  style,
@@ -358,7 +508,7 @@ function VroomChart(props) {
358
508
  const [measured, setMeasured] = useState2({ width: 0, height: 0 });
359
509
  const width = widthProp ?? measured.width;
360
510
  const height = heightProp ?? measured.height;
361
- const onLayout = useCallback((e) => {
511
+ const onLayout = useCallback2((e) => {
362
512
  const w = Math.round(e.nativeEvent.layout.width);
363
513
  const h = Math.round(e.nativeEvent.layout.height);
364
514
  setMeasured(
@@ -373,6 +523,19 @@ function VroomChart(props) {
373
523
  } : void 0,
374
524
  [priceLines, priceLinesStyle, onPriceLineClose]
375
525
  );
526
+ const emptyPicture = useMemo(() => {
527
+ const rec = Skia.PictureRecorder();
528
+ rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
529
+ return rec.finishRecordingAsPicture();
530
+ }, []);
531
+ const pictureSV = useSharedValue(emptyPicture);
532
+ const reduceMotion = useReducedMotion();
533
+ const onFrame = useCallback2(
534
+ (p) => {
535
+ pictureSV.value = p;
536
+ },
537
+ [pictureSV]
538
+ );
376
539
  const { handle, picture, volumeCollapseRef } = useChartCore(
377
540
  candles,
378
541
  { width, height },
@@ -386,21 +549,13 @@ function VroomChart(props) {
386
549
  vwap,
387
550
  bollingerBands,
388
551
  volume,
389
- priceLinesProp
552
+ priceLinesProp,
553
+ { seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame }
390
554
  );
391
- const emptyPicture = useMemo(() => {
392
- const rec = Skia.PictureRecorder();
393
- rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
394
- return rec.finishRecordingAsPicture();
395
- }, []);
396
- const pictureSV = useSharedValue(emptyPicture);
397
555
  const crosshairActive = useRef2(false);
398
556
  const lastCrosshairTime = useRef2(null);
399
- useEffect2(() => {
400
- if (picture) pictureSV.value = picture;
401
- }, [picture, pictureSV]);
402
557
  const decayRaf = useRef2(null);
403
- const cancelDecay = useCallback(() => {
558
+ const cancelDecay = useCallback2(() => {
404
559
  if (decayRaf.current != null) {
405
560
  cancelAnimationFrame(decayRaf.current);
406
561
  decayRaf.current = null;
@@ -408,7 +563,7 @@ function VroomChart(props) {
408
563
  }, []);
409
564
  useEffect2(() => cancelDecay, [cancelDecay]);
410
565
  const animRaf = useRef2(null);
411
- const animTick = useCallback(() => {
566
+ const animTick = useCallback2(() => {
412
567
  animRaf.current = null;
413
568
  if (!handle) return;
414
569
  const next = handle.render();
@@ -417,7 +572,7 @@ function VroomChart(props) {
417
572
  animRaf.current = requestAnimationFrame(animTick);
418
573
  }
419
574
  }, [handle, pictureSV]);
420
- const maybeStartAnim = useCallback(() => {
575
+ const maybeStartAnim = useCallback2(() => {
421
576
  if (animRaf.current != null) return;
422
577
  if (!handle?.isAnimating()) return;
423
578
  animRaf.current = requestAnimationFrame(animTick);
@@ -430,6 +585,10 @@ function VroomChart(props) {
430
585
  }
431
586
  };
432
587
  }, []);
588
+ useEffect2(() => {
589
+ if (picture) pictureSV.value = picture;
590
+ maybeStartAnim();
591
+ }, [picture, pictureSV, maybeStartAnim]);
433
592
  const morphRaf = useRef2(null);
434
593
  const morphFade = useRef2(null);
435
594
  const morphHandle = useRef2(null);
@@ -466,7 +625,7 @@ function VroomChart(props) {
466
625
  const prog = Math.min(1, (now - startTs) / dur);
467
626
  const fade = from + (target - from) * ease(easingRef.current, prog);
468
627
  morphFade.current = fade;
469
- handle.setMorph(fade, fade);
628
+ handle.setMorph(reduceMotion ? 0 : fade, fade);
470
629
  const p = handle.render();
471
630
  if (p) pictureSV.value = p;
472
631
  if (prog < 1) {
@@ -486,7 +645,7 @@ function VroomChart(props) {
486
645
  morphRaf.current = null;
487
646
  }
488
647
  };
489
- }, [handle, chartType, transitionMs, pictureSV]);
648
+ }, [handle, chartType, transitionMs, reduceMotion, pictureSV]);
490
649
  const volumeRaf = useRef2(null);
491
650
  const volumeHandle = useRef2(null);
492
651
  useEffect2(() => {
@@ -504,7 +663,7 @@ function VroomChart(props) {
504
663
  volumeRaf.current = null;
505
664
  }
506
665
  const dur = Math.max(0, transitionMs ?? 300);
507
- if (dur === 0) {
666
+ if (dur === 0 || reduceMotion) {
508
667
  volumeCollapseRef.current = { t: target, easing };
509
668
  handle.setVolumeCollapse(target, easing);
510
669
  const p = handle.render();
@@ -531,8 +690,8 @@ function VroomChart(props) {
531
690
  volumeRaf.current = null;
532
691
  }
533
692
  };
534
- }, [handle, volume?.enabled, transitionMs, pictureSV, volumeCollapseRef]);
535
- const hitAxis = useCallback(
693
+ }, [handle, volume?.enabled, transitionMs, reduceMotion, pictureSV, volumeCollapseRef]);
694
+ const hitAxis = useCallback2(
536
695
  (x, y) => {
537
696
  if (!handle) return "chart";
538
697
  const { yAxisWidth, xAxisHeight, indicatorHeight } = handle.getAxisMetrics();
@@ -545,7 +704,7 @@ function VroomChart(props) {
545
704
  },
546
705
  [handle, width, height]
547
706
  );
548
- const hitPriceLine = useCallback(
707
+ const hitPriceLine = useCallback2(
549
708
  (x, y) => {
550
709
  if (!handle || !priceLines?.length) return null;
551
710
  const hit = handle.hitTestPriceLine(x, y);
@@ -742,6 +901,9 @@ function VroomChart(props) {
742
901
  );
743
902
  }
744
903
  export {
745
- VroomChart
904
+ VroomChart,
905
+ classifyTransition,
906
+ inferStepMs,
907
+ timeframeWindow
746
908
  };
747
909
  //# sourceMappingURL=index.mjs.map