react-native-vroom-chart 0.15.0 → 0.17.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 (53) hide show
  1. package/cpp/VroomChartHostObject.cpp +252 -1
  2. package/cpp/_core_include/vroom/vroom_chart.h +178 -4
  3. package/cpp/_core_src/atr.cpp +67 -0
  4. package/cpp/_core_src/atr.h +44 -0
  5. package/cpp/_core_src/atr_pane.cpp +162 -0
  6. package/cpp/_core_src/atr_pane.h +48 -0
  7. package/cpp/_core_src/candles.cpp +15 -14
  8. package/cpp/_core_src/chart.cpp +758 -200
  9. package/cpp/_core_src/chart.h +221 -3
  10. package/cpp/_core_src/chart_facade.cpp +236 -23
  11. package/cpp/_core_src/color_lerp.h +28 -0
  12. package/cpp/_core_src/fair_value_gaps.cpp +76 -0
  13. package/cpp/_core_src/fair_value_gaps.h +54 -0
  14. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  15. package/cpp/_core_src/fvg_overlay.h +43 -0
  16. package/cpp/_core_src/ichimoku.cpp +65 -0
  17. package/cpp/_core_src/ichimoku.h +45 -0
  18. package/cpp/_core_src/labels.cpp +3 -0
  19. package/cpp/_core_src/line_morph.h +159 -0
  20. package/cpp/_core_src/loading_line.cpp +183 -0
  21. package/cpp/_core_src/loading_line.h +38 -0
  22. package/cpp/_core_src/loading_wave.h +140 -0
  23. package/cpp/_core_src/ma_overlay.cpp +273 -45
  24. package/cpp/_core_src/ma_overlay.h +64 -2
  25. package/cpp/_core_src/macd.cpp +24 -1
  26. package/cpp/_core_src/macd.h +16 -0
  27. package/cpp/_core_src/macd_pane.cpp +71 -62
  28. package/cpp/_core_src/macd_pane.h +13 -1
  29. package/cpp/_core_src/pane_series.h +169 -0
  30. package/cpp/_core_src/price_indicator.cpp +21 -7
  31. package/cpp/_core_src/price_indicator.h +11 -1
  32. package/cpp/_core_src/price_indicator_anim.h +81 -0
  33. package/cpp/_core_src/rsi.cpp +4 -0
  34. package/cpp/_core_src/rsi.h +7 -0
  35. package/cpp/_core_src/rsi_pane.cpp +85 -29
  36. package/cpp/_core_src/rsi_pane.h +11 -1
  37. package/cpp/_core_src/theme.cpp +5 -0
  38. package/cpp/_core_src/tip_geometry.h +55 -0
  39. package/cpp/_core_src/viewport.h +68 -0
  40. package/lib/index.d.mts +281 -3
  41. package/lib/index.d.ts +281 -3
  42. package/lib/index.js +292 -14
  43. package/lib/index.js.map +1 -1
  44. package/lib/index.mjs +292 -14
  45. package/lib/index.mjs.map +1 -1
  46. package/package.json +1 -1
  47. package/src/VroomChart.tsx +33 -3
  48. package/src/dataTransitions.ts +47 -0
  49. package/src/index.ts +5 -0
  50. package/src/jsi.d.ts +139 -1
  51. package/src/theme.ts +1 -0
  52. package/src/types.ts +5 -0
  53. package/src/useChartCore.ts +402 -8
package/lib/index.mjs CHANGED
@@ -71,6 +71,13 @@ function classifyTransition(prev, next, seriesKeyChanged) {
71
71
  const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
72
72
  return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
73
73
  }
74
+ function classifyStream(prev, next) {
75
+ if (prev.length === 0 || next.length === 0) return "tick";
76
+ return next[next.length - 1].timeMs > prev[prev.length - 1].timeMs ? "append" : "tick";
77
+ }
78
+ function isPinnedToLatest(window, lastMs, stepMs) {
79
+ return window.endMs >= lastMs + stepMs;
80
+ }
74
81
  function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
75
82
  const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
76
83
  const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
@@ -151,8 +158,10 @@ var COLOR_KEYS = {
151
158
  // VROOM_COLOR_ACCENT_BULL
152
159
  accentBear: 15,
153
160
  // VROOM_COLOR_ACCENT_BEAR
154
- lineColor: 16
161
+ lineColor: 16,
155
162
  // VROOM_COLOR_LINE
163
+ skeleton: 17
164
+ // VROOM_COLOR_SKELETON
156
165
  };
157
166
  var FLOAT_KEYS = {
158
167
  wickWidth: 1,
@@ -216,6 +225,7 @@ var MA_SOURCES = [
216
225
  "hlc3",
217
226
  "ohlc4"
218
227
  ];
228
+ var ATR_SMOOTHINGS = ["rma", "sma", "ema"];
219
229
  var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
220
230
  function overlayToNumeric(o) {
221
231
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
@@ -242,7 +252,8 @@ function rsiToSpec(cfg) {
242
252
  maColor: inheritColor(cfg?.maColor),
243
253
  maWidth: cfg?.maWidth ?? -1,
244
254
  bandColor: inheritColor(cfg?.bandColor),
245
- bandsVisible: cfg?.bandsVisible ?? true
255
+ bandsVisible: cfg?.bandsVisible ?? true,
256
+ extremeFill: cfg?.extremeFill ?? true
246
257
  };
247
258
  }
248
259
  function vwapToSpec(cfg) {
@@ -273,6 +284,79 @@ function bollingerToSpec(cfg) {
273
284
  fillOpacity: cfg?.fillOpacity ?? 0.1
274
285
  };
275
286
  }
287
+ var DEFAULT_ICH_GREEN = 4280723098;
288
+ var DEFAULT_ICH_RED = 4293874512;
289
+ var DEFAULT_ICH_BLUE = 4280902399;
290
+ var DEFAULT_ICH_ORANGE = 4294929664;
291
+ var DEFAULT_ICH_TEAL = 4278238420;
292
+ function ichimokuToSpec(cfg) {
293
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
294
+ return {
295
+ enabled: cfg?.enabled ?? false,
296
+ tenkanPeriod: cfg?.tenkanPeriod ?? 9,
297
+ kijunPeriod: cfg?.kijunPeriod ?? 26,
298
+ senkouBPeriod: cfg?.senkouBPeriod ?? 52,
299
+ displacement: cfg?.displacement ?? 26,
300
+ tenkanColor: color(cfg?.tenkanColor, DEFAULT_ICH_BLUE),
301
+ tenkanWidth: cfg?.tenkanWidth ?? 1,
302
+ tenkanEnabled: cfg?.tenkanVisible ?? true,
303
+ kijunColor: color(cfg?.kijunColor, DEFAULT_ICH_RED),
304
+ kijunWidth: cfg?.kijunWidth ?? 1,
305
+ kijunEnabled: cfg?.kijunVisible ?? true,
306
+ senkouAColor: color(cfg?.senkouAColor, DEFAULT_ICH_GREEN),
307
+ senkouAWidth: cfg?.senkouAWidth ?? 1,
308
+ senkouAEnabled: cfg?.senkouAVisible ?? true,
309
+ senkouBColor: color(cfg?.senkouBColor, DEFAULT_ICH_ORANGE),
310
+ senkouBWidth: cfg?.senkouBWidth ?? 1,
311
+ senkouBEnabled: cfg?.senkouBVisible ?? true,
312
+ chikouColor: color(cfg?.chikouColor, DEFAULT_ICH_TEAL),
313
+ chikouWidth: cfg?.chikouWidth ?? 1,
314
+ chikouEnabled: cfg?.chikouVisible ?? true,
315
+ cloudEnabled: cfg?.cloudVisible ?? true,
316
+ bullishCloudColor: color(cfg?.bullishCloudColor, DEFAULT_ICH_GREEN),
317
+ bearishCloudColor: color(cfg?.bearishCloudColor, DEFAULT_ICH_RED),
318
+ cloudOpacity: cfg?.cloudOpacity ?? 0.15
319
+ };
320
+ }
321
+ var DEFAULT_FVG_GREEN = 4280723098;
322
+ var DEFAULT_FVG_RED = 4293874512;
323
+ var FVG_FILL_TYPES = ["close", "wick"];
324
+ var FVG_BORDER_STYLES = ["solid", "dotted", "dashed"];
325
+ function fvgToSpec(cfg) {
326
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
327
+ const bullish = color(cfg?.bullishColor, DEFAULT_FVG_GREEN);
328
+ const bearish = color(cfg?.bearishColor, DEFAULT_FVG_RED);
329
+ return {
330
+ enabled: cfg?.enabled ?? false,
331
+ maxBarsBack: cfg?.maxBarsBack ?? 300,
332
+ waitForClose: cfg?.waitForClose ?? false,
333
+ fillType: Math.max(0, FVG_FILL_TYPES.indexOf(cfg?.fillType ?? "close")),
334
+ deleteAfterFill: cfg?.deleteAfterFill ?? true,
335
+ extendBoxes: cfg?.extendBoxes ?? false,
336
+ boxLength: cfg?.boxLength ?? 20,
337
+ bullishColor: bullish,
338
+ bearishColor: bearish,
339
+ opacity: cfg?.opacity ?? 0.15,
340
+ borderEnabled: cfg?.borderVisible ?? true,
341
+ borderStyle: Math.max(
342
+ 0,
343
+ FVG_BORDER_STYLES.indexOf(cfg?.borderStyle ?? "solid")
344
+ ),
345
+ borderWidth: cfg?.borderWidth ?? 1,
346
+ bullishBorderColor: color(cfg?.bullishBorderColor, bullish),
347
+ bearishBorderColor: color(cfg?.bearishBorderColor, bearish),
348
+ labelsEnabled: cfg?.showLabels ?? true,
349
+ label: cfg?.label ?? "FVG",
350
+ labelDistance: cfg?.labelDistance ?? 10,
351
+ // Alpha 0 is the core's "inherit the border color" sentinel.
352
+ labelColor: color(cfg?.labelColor, 0),
353
+ labelFontSize: cfg?.labelFontSize ?? 0,
354
+ showInverse: cfg?.showInverse ?? false,
355
+ inverseBullishColor: color(cfg?.inverseBullishColor, bullish),
356
+ inverseBearishColor: color(cfg?.inverseBearishColor, bearish),
357
+ inverseLabel: cfg?.inverseLabel ?? "iFVG"
358
+ };
359
+ }
276
360
  function macdToSpec(cfg) {
277
361
  const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
278
362
  return {
@@ -298,6 +382,15 @@ function macdToSpec(cfg) {
298
382
  zeroVisible: cfg?.zeroLineVisible ?? true
299
383
  };
300
384
  }
385
+ function atrToSpec(cfg) {
386
+ return {
387
+ enabled: cfg?.enabled ?? false,
388
+ period: cfg?.period ?? 14,
389
+ smoothing: Math.max(0, ATR_SMOOTHINGS.indexOf(cfg?.smoothing ?? "rma")),
390
+ lineColor: inheritColor(cfg?.lineColor),
391
+ lineWidth: cfg?.lineWidth ?? -1
392
+ };
393
+ }
301
394
  function volumeToSpec(cfg) {
302
395
  return {
303
396
  enabled: cfg?.enabled ?? true,
@@ -368,39 +461,61 @@ function ensureInstalled() {
368
461
  }
369
462
  installed = true;
370
463
  }
371
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, footprints, transition) {
464
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition, loading) {
372
465
  const handleRef = useRef(null);
373
466
  const defaultWidthAppliedRef = useRef(false);
374
467
  const volumeCollapseRef = useRef(null);
375
468
  const prevDataRef = useRef(null);
376
469
  const intervalMorphRaf = useRef(null);
470
+ const streamRaf = useRef(null);
471
+ const streamWindowRef = useRef(null);
472
+ const streamMorphRef = useRef(false);
377
473
  const [picture, setPicture] = useState(null);
378
474
  if (!handleRef.current && size.width > 0 && size.height > 0) {
379
475
  ensureInstalled();
380
476
  handleRef.current = globalThis.VroomChartJSI.create();
381
477
  }
382
- const animRef = useRef({ ms: 300, easing: void 0, reduceMotion: false, interval: "transform" });
478
+ const animRef = useRef({
479
+ ms: 300,
480
+ easing: void 0,
481
+ reduceMotion: false,
482
+ interval: "transform",
483
+ stream: "none",
484
+ streamMs: 150
485
+ });
383
486
  animRef.current = {
384
487
  ms: Math.max(0, transition?.transitionMs ?? 300),
385
488
  easing: transition?.transitionEasing,
386
489
  reduceMotion: transition?.reduceMotion ?? false,
387
- interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
490
+ interval: transition?.intervalTransition === "fade" ? "fade" : "transform",
491
+ stream: transition?.streamTransition === "transform" ? "transform" : "none",
492
+ // Shorter than transitionMs by default: ticks can land faster than a 300ms
493
+ // curve, and every one that does interrupts the last.
494
+ streamMs: Math.max(0, transition?.streamTransitionMs ?? 150)
388
495
  };
389
496
  const onFrameRef = useRef(transition?.onFrame);
390
497
  onFrameRef.current = transition?.onFrame;
391
498
  const seriesKey = transition?.seriesKey;
499
+ const loadingHandoffRef = useRef(false);
392
500
  const endIntervalMorph = useCallback(() => {
393
501
  if (intervalMorphRaf.current != null) {
394
502
  cancelAnimationFrame(intervalMorphRaf.current);
395
503
  intervalMorphRaf.current = null;
396
504
  }
397
- handleRef.current?.setIntervalMorph(1);
505
+ const h = handleRef.current;
506
+ if (loadingHandoffRef.current) {
507
+ loadingHandoffRef.current = false;
508
+ h?.setLoadingMorph(1);
509
+ h?.beginLoadingReveal();
510
+ }
511
+ h?.setIntervalMorph(1);
398
512
  }, []);
399
- const startIntervalMorph = useCallback((h) => {
513
+ const startIntervalMorph = useCallback((h, durationMs) => {
400
514
  const { ms, easing } = animRef.current;
515
+ const dur = durationMs ?? ms;
401
516
  const start = performance.now();
402
517
  const step = (now) => {
403
- const p = Math.min(1, (now - start) / ms);
518
+ const p = Math.min(1, (now - start) / dur);
404
519
  h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
405
520
  const pic = h.render();
406
521
  if (pic) onFrameRef.current?.(pic);
@@ -408,23 +523,111 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
408
523
  };
409
524
  intervalMorphRaf.current = requestAnimationFrame(step);
410
525
  }, []);
526
+ const startLoadingHandoff = useCallback(
527
+ (h) => {
528
+ const { ms, easing } = animRef.current;
529
+ const half = ms / 2;
530
+ loadingHandoffRef.current = true;
531
+ h.beginLoadingMorph();
532
+ const start = performance.now();
533
+ const step = (now) => {
534
+ const p = Math.min(1, (now - start) / half);
535
+ h.setLoadingMorph(p < 1 ? ease(easing, p) : 1);
536
+ const pic = h.render();
537
+ if (pic) onFrameRef.current?.(pic);
538
+ if (p < 1) {
539
+ intervalMorphRaf.current = requestAnimationFrame(step);
540
+ return;
541
+ }
542
+ intervalMorphRaf.current = null;
543
+ loadingHandoffRef.current = false;
544
+ h.beginLoadingReveal();
545
+ startIntervalMorph(h, half);
546
+ };
547
+ intervalMorphRaf.current = requestAnimationFrame(step);
548
+ },
549
+ [startIntervalMorph]
550
+ );
551
+ const settleStream = useCallback((keepMorph = false) => {
552
+ if (streamRaf.current != null) {
553
+ cancelAnimationFrame(streamRaf.current);
554
+ streamRaf.current = null;
555
+ }
556
+ const h = handleRef.current;
557
+ const target = streamWindowRef.current;
558
+ streamWindowRef.current = null;
559
+ if (target) h?.setVisibleRange(target.startMs, target.endMs);
560
+ if (streamMorphRef.current && !keepMorph) {
561
+ streamMorphRef.current = false;
562
+ h?.setIntervalMorph(1);
563
+ }
564
+ }, []);
565
+ const startStreamAnim = useCallback(
566
+ (h, morphing, window) => {
567
+ const { streamMs, easing } = animRef.current;
568
+ let from = window ? h.getVisibleRange() : null;
569
+ let applied = null;
570
+ streamWindowRef.current = window;
571
+ streamMorphRef.current = morphing;
572
+ const start = performance.now();
573
+ const step = (now) => {
574
+ if (from && applied) {
575
+ const now_w = h.getVisibleRange();
576
+ if (now_w.startMs !== applied.startMs || now_w.endMs !== applied.endMs) {
577
+ from = null;
578
+ streamWindowRef.current = null;
579
+ }
580
+ }
581
+ const p = Math.min(1, (now - start) / streamMs);
582
+ const e = p < 1 ? ease(easing, p) : 1;
583
+ if (morphing) h.setIntervalMorph(e);
584
+ if (from && window) {
585
+ applied = {
586
+ startMs: Math.round(from.startMs + (window.startMs - from.startMs) * e),
587
+ endMs: Math.round(from.endMs + (window.endMs - from.endMs) * e)
588
+ };
589
+ h.setVisibleRange(applied.startMs, applied.endMs);
590
+ }
591
+ const pic = h.render();
592
+ if (pic) onFrameRef.current?.(pic);
593
+ if (p < 1) {
594
+ streamRaf.current = requestAnimationFrame(step);
595
+ } else {
596
+ streamRaf.current = null;
597
+ streamWindowRef.current = null;
598
+ streamMorphRef.current = false;
599
+ }
600
+ };
601
+ streamRaf.current = requestAnimationFrame(step);
602
+ },
603
+ []
604
+ );
411
605
  useEffect(() => {
412
606
  return () => {
413
607
  if (intervalMorphRaf.current != null) {
414
608
  cancelAnimationFrame(intervalMorphRaf.current);
415
609
  intervalMorphRaf.current = null;
416
610
  }
611
+ if (streamRaf.current != null) {
612
+ cancelAnimationFrame(streamRaf.current);
613
+ streamRaf.current = null;
614
+ }
417
615
  };
418
616
  }, []);
419
617
  const explicit = visibleRange != null;
420
618
  const startMs = visibleRange?.startMs ?? 0;
421
619
  const endMs = visibleRange?.endMs ?? 0;
620
+ const showLoadingLine = loading === true && candles.length === 0;
621
+ const lineUpRef = useRef(false);
422
622
  const themeKey = theme ? JSON.stringify(theme) : "";
423
623
  const rsiKey = rsi ? JSON.stringify(rsi) : "";
424
624
  const macdKey = macd ? JSON.stringify(macd) : "";
625
+ const atrKey = atr ? JSON.stringify(atr) : "";
425
626
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
426
627
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
427
628
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
629
+ const ichimokuKey = ichimoku ? JSON.stringify(ichimoku) : "";
630
+ const fvgKey = fairValueGaps ? JSON.stringify(fairValueGaps) : "";
428
631
  const volumeKey = volume ? JSON.stringify(volume) : "";
429
632
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
430
633
  const footprintsKey = footprints ? JSON.stringify(footprints) : "";
@@ -432,11 +635,25 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
432
635
  const h = handleRef.current;
433
636
  if (!h) return;
434
637
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
638
+ h.setIchimoku(ichimokuToSpec(ichimoku));
435
639
  if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
436
640
  h.setDefaultCandleWidth(defaultCandleWidth);
437
641
  defaultWidthAppliedRef.current = true;
438
642
  }
439
643
  let morphing = false;
644
+ if (showLoadingLine) {
645
+ if (!lineUpRef.current) {
646
+ endIntervalMorph();
647
+ settleStream();
648
+ h.setCandles(packCandles([]));
649
+ prevDataRef.current = null;
650
+ }
651
+ h.setLoading(true, !animRef.current.reduceMotion);
652
+ lineUpRef.current = true;
653
+ } else if (lineUpRef.current && candles.length === 0) {
654
+ h.setLoading(false, true);
655
+ lineUpRef.current = false;
656
+ }
440
657
  if (candles.length > 0) {
441
658
  const prev = prevDataRef.current;
442
659
  const freshHandle = prev == null || prev.handle !== h;
@@ -444,6 +661,37 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
444
661
  const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
445
662
  let tfArgs = null;
446
663
  let prevEnvelope = null;
664
+ let handOff = false;
665
+ let stream = null;
666
+ if (transitionKind === "stream" && prev != null && !explicit) {
667
+ const { stream: mode, streamMs, reduceMotion } = animRef.current;
668
+ const stepMs = inferStepMs(candles);
669
+ if (mode === "transform" && streamMs > 0 && stepMs != null && !reduceMotion && onFrameRef.current != null) {
670
+ const lastMs = candles[candles.length - 1].timeMs;
671
+ const prevLastMs = prev.candles[prev.candles.length - 1].timeMs;
672
+ if (classifyStream(prev.candles, candles) === "append") {
673
+ const w = h.getVisibleRange();
674
+ const prevStepMs = inferStepMs(prev.candles) ?? stepMs;
675
+ if (isPinnedToLatest(w, prevLastMs, prevStepMs)) {
676
+ const by = lastMs - prevLastMs;
677
+ stream = {
678
+ morph: false,
679
+ window: { startMs: w.startMs + by, endMs: w.endMs + by }
680
+ };
681
+ }
682
+ } else {
683
+ stream = { morph: true, window: null };
684
+ }
685
+ }
686
+ if (stream?.morph) {
687
+ settleStream(true);
688
+ h.beginStreamMorph();
689
+ } else {
690
+ settleStream();
691
+ }
692
+ } else if (transitionKind === "stream") {
693
+ settleStream();
694
+ }
447
695
  if (transitionKind === "timeframe" && prev != null) {
448
696
  const oldWindow = h.getVisibleRange();
449
697
  const oldStepMs = inferStepMs(prev.candles);
@@ -463,6 +711,11 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
463
711
  } else if (transitionKind === "initial" || transitionKind === "reset") {
464
712
  endIntervalMorph();
465
713
  }
714
+ if (lineUpRef.current) {
715
+ lineUpRef.current = false;
716
+ handOff = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
717
+ if (!handOff) h.setLoading(false, true);
718
+ }
466
719
  h.setCandles(packCandles(candles));
467
720
  if (transitionKind === "timeframe") {
468
721
  const newStepMs = inferStepMs(candles);
@@ -479,9 +732,12 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
479
732
  if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
480
733
  else h.resetPriceScale();
481
734
  if (morphing) startIntervalMorph(h);
735
+ } else if (stream) {
736
+ startStreamAnim(h, stream.morph, stream.window);
482
737
  } else if (transitionKind === "reset") {
483
738
  h.resetView();
484
739
  }
740
+ if (handOff) startLoadingHandoff(h);
485
741
  prevDataRef.current = { handle: h, candles, seriesKey };
486
742
  }
487
743
  }
@@ -496,9 +752,11 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
496
752
  }
497
753
  h.setRSI(rsiToSpec(rsi));
498
754
  h.setMACD(macdToSpec(macd));
755
+ h.setATR(atrToSpec(atr));
499
756
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
500
757
  h.setVWAP(vwapToSpec(vwap));
501
758
  h.setBollinger(bollingerToSpec(bollingerBands));
759
+ h.setFairValueGaps(fvgToSpec(fairValueGaps));
502
760
  h.setVolume(volumeToSpec(volume));
503
761
  const collapse = volumeCollapseRef.current;
504
762
  if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
@@ -509,7 +767,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
509
767
  footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
510
768
  );
511
769
  if (!morphing) setPicture(h.render());
512
- }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph]);
770
+ }, [candles, showLoadingLine, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, startLoadingHandoff, endIntervalMorph, startStreamAnim, settleStream]);
513
771
  return { handle: handleRef.current, picture, volumeCollapseRef };
514
772
  }
515
773
 
@@ -521,6 +779,7 @@ function isSkImage(frame) {
521
779
  function VroomChart(props) {
522
780
  const {
523
781
  candles,
782
+ loading,
524
783
  seriesKey,
525
784
  width: widthProp,
526
785
  height: heightProp,
@@ -531,12 +790,17 @@ function VroomChart(props) {
531
790
  transitionMs,
532
791
  transitionEasing,
533
792
  intervalTransition,
793
+ streamTransition,
794
+ streamTransitionMs,
534
795
  theme,
535
796
  rsi,
536
797
  macd,
798
+ atr,
537
799
  movingAverages,
538
800
  vwap,
539
801
  bollingerBands,
802
+ ichimoku,
803
+ fairValueGaps,
540
804
  volume,
541
805
  crosshairOffset = 40,
542
806
  onCrosshair,
@@ -615,14 +879,28 @@ function VroomChart(props) {
615
879
  theme,
616
880
  rsi,
617
881
  macd,
882
+ atr,
618
883
  movingAverages,
619
884
  vwap,
620
885
  bollingerBands,
886
+ ichimoku,
887
+ fairValueGaps,
621
888
  volume,
622
889
  priceLinesProp,
623
890
  footprintsProp,
624
- { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
891
+ {
892
+ seriesKey,
893
+ transitionMs,
894
+ transitionEasing,
895
+ intervalTransition,
896
+ streamTransition,
897
+ streamTransitionMs,
898
+ reduceMotion,
899
+ onFrame
900
+ },
901
+ loading
625
902
  );
903
+ const showLoadingLine = loading === true && candles.length === 0;
626
904
  const crosshairActive = useRef2(false);
627
905
  const footprintActive = useRef2(false);
628
906
  const lastCrosshairTime = useRef2(null);
@@ -902,7 +1180,7 @@ function VroomChart(props) {
902
1180
  pane: null
903
1181
  });
904
1182
  };
905
- const pan = Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
1183
+ const pan = Gesture.Pan().enabled(!showLoadingLine).runOnJS(true).maxPointers(1).onStart((e) => {
906
1184
  cancelDecay();
907
1185
  panMode.current = hitAxis(e.x, e.y);
908
1186
  priceDrag.current = null;
@@ -997,7 +1275,7 @@ function VroomChart(props) {
997
1275
  enableX: false,
998
1276
  enableY: false
999
1277
  });
1000
- const pinch = Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
1278
+ const pinch = Gesture.Pinch().enabled(!showLoadingLine).runOnJS(true).onTouchesDown((e) => {
1001
1279
  if (e.numberOfTouches < 2) return;
1002
1280
  dismissFootprint();
1003
1281
  const [a, b] = e.allTouches;
@@ -1035,7 +1313,7 @@ function VroomChart(props) {
1035
1313
  if (next) applyFrame(next);
1036
1314
  maybeStartAnim();
1037
1315
  });
1038
- const longPress = Gesture.LongPress().runOnJS(true).onStart((e) => {
1316
+ const longPress = Gesture.LongPress().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
1039
1317
  if (!handle) return;
1040
1318
  if (hitAxis(e.x, e.y) !== "chart") return;
1041
1319
  if (hitPriceLine(e.x, e.y)) return;
@@ -1055,7 +1333,7 @@ function VroomChart(props) {
1055
1333
  reason: "show"
1056
1334
  });
1057
1335
  });
1058
- const tap = Gesture.Tap().runOnJS(true).onStart((e) => {
1336
+ const tap = Gesture.Tap().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
1059
1337
  if (!handle) return;
1060
1338
  const prints = footprints ?? [];
1061
1339
  const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;