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.js CHANGED
@@ -99,6 +99,13 @@ function classifyTransition(prev, next, seriesKeyChanged) {
99
99
  const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
100
100
  return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
101
101
  }
102
+ function classifyStream(prev, next) {
103
+ if (prev.length === 0 || next.length === 0) return "tick";
104
+ return next[next.length - 1].timeMs > prev[prev.length - 1].timeMs ? "append" : "tick";
105
+ }
106
+ function isPinnedToLatest(window, lastMs, stepMs) {
107
+ return window.endMs >= lastMs + stepMs;
108
+ }
102
109
  function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
103
110
  const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
104
111
  const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
@@ -179,8 +186,10 @@ var COLOR_KEYS = {
179
186
  // VROOM_COLOR_ACCENT_BULL
180
187
  accentBear: 15,
181
188
  // VROOM_COLOR_ACCENT_BEAR
182
- lineColor: 16
189
+ lineColor: 16,
183
190
  // VROOM_COLOR_LINE
191
+ skeleton: 17
192
+ // VROOM_COLOR_SKELETON
184
193
  };
185
194
  var FLOAT_KEYS = {
186
195
  wickWidth: 1,
@@ -244,6 +253,7 @@ var MA_SOURCES = [
244
253
  "hlc3",
245
254
  "ohlc4"
246
255
  ];
256
+ var ATR_SMOOTHINGS = ["rma", "sma", "ema"];
247
257
  var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
248
258
  function overlayToNumeric(o) {
249
259
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
@@ -270,7 +280,8 @@ function rsiToSpec(cfg) {
270
280
  maColor: inheritColor(cfg?.maColor),
271
281
  maWidth: cfg?.maWidth ?? -1,
272
282
  bandColor: inheritColor(cfg?.bandColor),
273
- bandsVisible: cfg?.bandsVisible ?? true
283
+ bandsVisible: cfg?.bandsVisible ?? true,
284
+ extremeFill: cfg?.extremeFill ?? true
274
285
  };
275
286
  }
276
287
  function vwapToSpec(cfg) {
@@ -301,6 +312,79 @@ function bollingerToSpec(cfg) {
301
312
  fillOpacity: cfg?.fillOpacity ?? 0.1
302
313
  };
303
314
  }
315
+ var DEFAULT_ICH_GREEN = 4280723098;
316
+ var DEFAULT_ICH_RED = 4293874512;
317
+ var DEFAULT_ICH_BLUE = 4280902399;
318
+ var DEFAULT_ICH_ORANGE = 4294929664;
319
+ var DEFAULT_ICH_TEAL = 4278238420;
320
+ function ichimokuToSpec(cfg) {
321
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
322
+ return {
323
+ enabled: cfg?.enabled ?? false,
324
+ tenkanPeriod: cfg?.tenkanPeriod ?? 9,
325
+ kijunPeriod: cfg?.kijunPeriod ?? 26,
326
+ senkouBPeriod: cfg?.senkouBPeriod ?? 52,
327
+ displacement: cfg?.displacement ?? 26,
328
+ tenkanColor: color(cfg?.tenkanColor, DEFAULT_ICH_BLUE),
329
+ tenkanWidth: cfg?.tenkanWidth ?? 1,
330
+ tenkanEnabled: cfg?.tenkanVisible ?? true,
331
+ kijunColor: color(cfg?.kijunColor, DEFAULT_ICH_RED),
332
+ kijunWidth: cfg?.kijunWidth ?? 1,
333
+ kijunEnabled: cfg?.kijunVisible ?? true,
334
+ senkouAColor: color(cfg?.senkouAColor, DEFAULT_ICH_GREEN),
335
+ senkouAWidth: cfg?.senkouAWidth ?? 1,
336
+ senkouAEnabled: cfg?.senkouAVisible ?? true,
337
+ senkouBColor: color(cfg?.senkouBColor, DEFAULT_ICH_ORANGE),
338
+ senkouBWidth: cfg?.senkouBWidth ?? 1,
339
+ senkouBEnabled: cfg?.senkouBVisible ?? true,
340
+ chikouColor: color(cfg?.chikouColor, DEFAULT_ICH_TEAL),
341
+ chikouWidth: cfg?.chikouWidth ?? 1,
342
+ chikouEnabled: cfg?.chikouVisible ?? true,
343
+ cloudEnabled: cfg?.cloudVisible ?? true,
344
+ bullishCloudColor: color(cfg?.bullishCloudColor, DEFAULT_ICH_GREEN),
345
+ bearishCloudColor: color(cfg?.bearishCloudColor, DEFAULT_ICH_RED),
346
+ cloudOpacity: cfg?.cloudOpacity ?? 0.15
347
+ };
348
+ }
349
+ var DEFAULT_FVG_GREEN = 4280723098;
350
+ var DEFAULT_FVG_RED = 4293874512;
351
+ var FVG_FILL_TYPES = ["close", "wick"];
352
+ var FVG_BORDER_STYLES = ["solid", "dotted", "dashed"];
353
+ function fvgToSpec(cfg) {
354
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
355
+ const bullish = color(cfg?.bullishColor, DEFAULT_FVG_GREEN);
356
+ const bearish = color(cfg?.bearishColor, DEFAULT_FVG_RED);
357
+ return {
358
+ enabled: cfg?.enabled ?? false,
359
+ maxBarsBack: cfg?.maxBarsBack ?? 300,
360
+ waitForClose: cfg?.waitForClose ?? false,
361
+ fillType: Math.max(0, FVG_FILL_TYPES.indexOf(cfg?.fillType ?? "close")),
362
+ deleteAfterFill: cfg?.deleteAfterFill ?? true,
363
+ extendBoxes: cfg?.extendBoxes ?? false,
364
+ boxLength: cfg?.boxLength ?? 20,
365
+ bullishColor: bullish,
366
+ bearishColor: bearish,
367
+ opacity: cfg?.opacity ?? 0.15,
368
+ borderEnabled: cfg?.borderVisible ?? true,
369
+ borderStyle: Math.max(
370
+ 0,
371
+ FVG_BORDER_STYLES.indexOf(cfg?.borderStyle ?? "solid")
372
+ ),
373
+ borderWidth: cfg?.borderWidth ?? 1,
374
+ bullishBorderColor: color(cfg?.bullishBorderColor, bullish),
375
+ bearishBorderColor: color(cfg?.bearishBorderColor, bearish),
376
+ labelsEnabled: cfg?.showLabels ?? true,
377
+ label: cfg?.label ?? "FVG",
378
+ labelDistance: cfg?.labelDistance ?? 10,
379
+ // Alpha 0 is the core's "inherit the border color" sentinel.
380
+ labelColor: color(cfg?.labelColor, 0),
381
+ labelFontSize: cfg?.labelFontSize ?? 0,
382
+ showInverse: cfg?.showInverse ?? false,
383
+ inverseBullishColor: color(cfg?.inverseBullishColor, bullish),
384
+ inverseBearishColor: color(cfg?.inverseBearishColor, bearish),
385
+ inverseLabel: cfg?.inverseLabel ?? "iFVG"
386
+ };
387
+ }
304
388
  function macdToSpec(cfg) {
305
389
  const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
306
390
  return {
@@ -326,6 +410,15 @@ function macdToSpec(cfg) {
326
410
  zeroVisible: cfg?.zeroLineVisible ?? true
327
411
  };
328
412
  }
413
+ function atrToSpec(cfg) {
414
+ return {
415
+ enabled: cfg?.enabled ?? false,
416
+ period: cfg?.period ?? 14,
417
+ smoothing: Math.max(0, ATR_SMOOTHINGS.indexOf(cfg?.smoothing ?? "rma")),
418
+ lineColor: inheritColor(cfg?.lineColor),
419
+ lineWidth: cfg?.lineWidth ?? -1
420
+ };
421
+ }
329
422
  function volumeToSpec(cfg) {
330
423
  return {
331
424
  enabled: cfg?.enabled ?? true,
@@ -396,39 +489,61 @@ function ensureInstalled() {
396
489
  }
397
490
  installed = true;
398
491
  }
399
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, footprints, transition) {
492
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition, loading) {
400
493
  const handleRef = (0, import_react.useRef)(null);
401
494
  const defaultWidthAppliedRef = (0, import_react.useRef)(false);
402
495
  const volumeCollapseRef = (0, import_react.useRef)(null);
403
496
  const prevDataRef = (0, import_react.useRef)(null);
404
497
  const intervalMorphRaf = (0, import_react.useRef)(null);
498
+ const streamRaf = (0, import_react.useRef)(null);
499
+ const streamWindowRef = (0, import_react.useRef)(null);
500
+ const streamMorphRef = (0, import_react.useRef)(false);
405
501
  const [picture, setPicture] = (0, import_react.useState)(null);
406
502
  if (!handleRef.current && size.width > 0 && size.height > 0) {
407
503
  ensureInstalled();
408
504
  handleRef.current = globalThis.VroomChartJSI.create();
409
505
  }
410
- const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false, interval: "transform" });
506
+ const animRef = (0, import_react.useRef)({
507
+ ms: 300,
508
+ easing: void 0,
509
+ reduceMotion: false,
510
+ interval: "transform",
511
+ stream: "none",
512
+ streamMs: 150
513
+ });
411
514
  animRef.current = {
412
515
  ms: Math.max(0, transition?.transitionMs ?? 300),
413
516
  easing: transition?.transitionEasing,
414
517
  reduceMotion: transition?.reduceMotion ?? false,
415
- interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
518
+ interval: transition?.intervalTransition === "fade" ? "fade" : "transform",
519
+ stream: transition?.streamTransition === "transform" ? "transform" : "none",
520
+ // Shorter than transitionMs by default: ticks can land faster than a 300ms
521
+ // curve, and every one that does interrupts the last.
522
+ streamMs: Math.max(0, transition?.streamTransitionMs ?? 150)
416
523
  };
417
524
  const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
418
525
  onFrameRef.current = transition?.onFrame;
419
526
  const seriesKey = transition?.seriesKey;
527
+ const loadingHandoffRef = (0, import_react.useRef)(false);
420
528
  const endIntervalMorph = (0, import_react.useCallback)(() => {
421
529
  if (intervalMorphRaf.current != null) {
422
530
  cancelAnimationFrame(intervalMorphRaf.current);
423
531
  intervalMorphRaf.current = null;
424
532
  }
425
- handleRef.current?.setIntervalMorph(1);
533
+ const h = handleRef.current;
534
+ if (loadingHandoffRef.current) {
535
+ loadingHandoffRef.current = false;
536
+ h?.setLoadingMorph(1);
537
+ h?.beginLoadingReveal();
538
+ }
539
+ h?.setIntervalMorph(1);
426
540
  }, []);
427
- const startIntervalMorph = (0, import_react.useCallback)((h) => {
541
+ const startIntervalMorph = (0, import_react.useCallback)((h, durationMs) => {
428
542
  const { ms, easing } = animRef.current;
543
+ const dur = durationMs ?? ms;
429
544
  const start = performance.now();
430
545
  const step = (now) => {
431
- const p = Math.min(1, (now - start) / ms);
546
+ const p = Math.min(1, (now - start) / dur);
432
547
  h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
433
548
  const pic = h.render();
434
549
  if (pic) onFrameRef.current?.(pic);
@@ -436,23 +551,111 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
436
551
  };
437
552
  intervalMorphRaf.current = requestAnimationFrame(step);
438
553
  }, []);
554
+ const startLoadingHandoff = (0, import_react.useCallback)(
555
+ (h) => {
556
+ const { ms, easing } = animRef.current;
557
+ const half = ms / 2;
558
+ loadingHandoffRef.current = true;
559
+ h.beginLoadingMorph();
560
+ const start = performance.now();
561
+ const step = (now) => {
562
+ const p = Math.min(1, (now - start) / half);
563
+ h.setLoadingMorph(p < 1 ? ease(easing, p) : 1);
564
+ const pic = h.render();
565
+ if (pic) onFrameRef.current?.(pic);
566
+ if (p < 1) {
567
+ intervalMorphRaf.current = requestAnimationFrame(step);
568
+ return;
569
+ }
570
+ intervalMorphRaf.current = null;
571
+ loadingHandoffRef.current = false;
572
+ h.beginLoadingReveal();
573
+ startIntervalMorph(h, half);
574
+ };
575
+ intervalMorphRaf.current = requestAnimationFrame(step);
576
+ },
577
+ [startIntervalMorph]
578
+ );
579
+ const settleStream = (0, import_react.useCallback)((keepMorph = false) => {
580
+ if (streamRaf.current != null) {
581
+ cancelAnimationFrame(streamRaf.current);
582
+ streamRaf.current = null;
583
+ }
584
+ const h = handleRef.current;
585
+ const target = streamWindowRef.current;
586
+ streamWindowRef.current = null;
587
+ if (target) h?.setVisibleRange(target.startMs, target.endMs);
588
+ if (streamMorphRef.current && !keepMorph) {
589
+ streamMorphRef.current = false;
590
+ h?.setIntervalMorph(1);
591
+ }
592
+ }, []);
593
+ const startStreamAnim = (0, import_react.useCallback)(
594
+ (h, morphing, window) => {
595
+ const { streamMs, easing } = animRef.current;
596
+ let from = window ? h.getVisibleRange() : null;
597
+ let applied = null;
598
+ streamWindowRef.current = window;
599
+ streamMorphRef.current = morphing;
600
+ const start = performance.now();
601
+ const step = (now) => {
602
+ if (from && applied) {
603
+ const now_w = h.getVisibleRange();
604
+ if (now_w.startMs !== applied.startMs || now_w.endMs !== applied.endMs) {
605
+ from = null;
606
+ streamWindowRef.current = null;
607
+ }
608
+ }
609
+ const p = Math.min(1, (now - start) / streamMs);
610
+ const e = p < 1 ? ease(easing, p) : 1;
611
+ if (morphing) h.setIntervalMorph(e);
612
+ if (from && window) {
613
+ applied = {
614
+ startMs: Math.round(from.startMs + (window.startMs - from.startMs) * e),
615
+ endMs: Math.round(from.endMs + (window.endMs - from.endMs) * e)
616
+ };
617
+ h.setVisibleRange(applied.startMs, applied.endMs);
618
+ }
619
+ const pic = h.render();
620
+ if (pic) onFrameRef.current?.(pic);
621
+ if (p < 1) {
622
+ streamRaf.current = requestAnimationFrame(step);
623
+ } else {
624
+ streamRaf.current = null;
625
+ streamWindowRef.current = null;
626
+ streamMorphRef.current = false;
627
+ }
628
+ };
629
+ streamRaf.current = requestAnimationFrame(step);
630
+ },
631
+ []
632
+ );
439
633
  (0, import_react.useEffect)(() => {
440
634
  return () => {
441
635
  if (intervalMorphRaf.current != null) {
442
636
  cancelAnimationFrame(intervalMorphRaf.current);
443
637
  intervalMorphRaf.current = null;
444
638
  }
639
+ if (streamRaf.current != null) {
640
+ cancelAnimationFrame(streamRaf.current);
641
+ streamRaf.current = null;
642
+ }
445
643
  };
446
644
  }, []);
447
645
  const explicit = visibleRange != null;
448
646
  const startMs = visibleRange?.startMs ?? 0;
449
647
  const endMs = visibleRange?.endMs ?? 0;
648
+ const showLoadingLine = loading === true && candles.length === 0;
649
+ const lineUpRef = (0, import_react.useRef)(false);
450
650
  const themeKey = theme ? JSON.stringify(theme) : "";
451
651
  const rsiKey = rsi ? JSON.stringify(rsi) : "";
452
652
  const macdKey = macd ? JSON.stringify(macd) : "";
653
+ const atrKey = atr ? JSON.stringify(atr) : "";
453
654
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
454
655
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
455
656
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
657
+ const ichimokuKey = ichimoku ? JSON.stringify(ichimoku) : "";
658
+ const fvgKey = fairValueGaps ? JSON.stringify(fairValueGaps) : "";
456
659
  const volumeKey = volume ? JSON.stringify(volume) : "";
457
660
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
458
661
  const footprintsKey = footprints ? JSON.stringify(footprints) : "";
@@ -460,11 +663,25 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
460
663
  const h = handleRef.current;
461
664
  if (!h) return;
462
665
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
666
+ h.setIchimoku(ichimokuToSpec(ichimoku));
463
667
  if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
464
668
  h.setDefaultCandleWidth(defaultCandleWidth);
465
669
  defaultWidthAppliedRef.current = true;
466
670
  }
467
671
  let morphing = false;
672
+ if (showLoadingLine) {
673
+ if (!lineUpRef.current) {
674
+ endIntervalMorph();
675
+ settleStream();
676
+ h.setCandles(packCandles([]));
677
+ prevDataRef.current = null;
678
+ }
679
+ h.setLoading(true, !animRef.current.reduceMotion);
680
+ lineUpRef.current = true;
681
+ } else if (lineUpRef.current && candles.length === 0) {
682
+ h.setLoading(false, true);
683
+ lineUpRef.current = false;
684
+ }
468
685
  if (candles.length > 0) {
469
686
  const prev = prevDataRef.current;
470
687
  const freshHandle = prev == null || prev.handle !== h;
@@ -472,6 +689,37 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
472
689
  const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
473
690
  let tfArgs = null;
474
691
  let prevEnvelope = null;
692
+ let handOff = false;
693
+ let stream = null;
694
+ if (transitionKind === "stream" && prev != null && !explicit) {
695
+ const { stream: mode, streamMs, reduceMotion } = animRef.current;
696
+ const stepMs = inferStepMs(candles);
697
+ if (mode === "transform" && streamMs > 0 && stepMs != null && !reduceMotion && onFrameRef.current != null) {
698
+ const lastMs = candles[candles.length - 1].timeMs;
699
+ const prevLastMs = prev.candles[prev.candles.length - 1].timeMs;
700
+ if (classifyStream(prev.candles, candles) === "append") {
701
+ const w = h.getVisibleRange();
702
+ const prevStepMs = inferStepMs(prev.candles) ?? stepMs;
703
+ if (isPinnedToLatest(w, prevLastMs, prevStepMs)) {
704
+ const by = lastMs - prevLastMs;
705
+ stream = {
706
+ morph: false,
707
+ window: { startMs: w.startMs + by, endMs: w.endMs + by }
708
+ };
709
+ }
710
+ } else {
711
+ stream = { morph: true, window: null };
712
+ }
713
+ }
714
+ if (stream?.morph) {
715
+ settleStream(true);
716
+ h.beginStreamMorph();
717
+ } else {
718
+ settleStream();
719
+ }
720
+ } else if (transitionKind === "stream") {
721
+ settleStream();
722
+ }
475
723
  if (transitionKind === "timeframe" && prev != null) {
476
724
  const oldWindow = h.getVisibleRange();
477
725
  const oldStepMs = inferStepMs(prev.candles);
@@ -491,6 +739,11 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
491
739
  } else if (transitionKind === "initial" || transitionKind === "reset") {
492
740
  endIntervalMorph();
493
741
  }
742
+ if (lineUpRef.current) {
743
+ lineUpRef.current = false;
744
+ handOff = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
745
+ if (!handOff) h.setLoading(false, true);
746
+ }
494
747
  h.setCandles(packCandles(candles));
495
748
  if (transitionKind === "timeframe") {
496
749
  const newStepMs = inferStepMs(candles);
@@ -507,9 +760,12 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
507
760
  if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
508
761
  else h.resetPriceScale();
509
762
  if (morphing) startIntervalMorph(h);
763
+ } else if (stream) {
764
+ startStreamAnim(h, stream.morph, stream.window);
510
765
  } else if (transitionKind === "reset") {
511
766
  h.resetView();
512
767
  }
768
+ if (handOff) startLoadingHandoff(h);
513
769
  prevDataRef.current = { handle: h, candles, seriesKey };
514
770
  }
515
771
  }
@@ -524,9 +780,11 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
524
780
  }
525
781
  h.setRSI(rsiToSpec(rsi));
526
782
  h.setMACD(macdToSpec(macd));
783
+ h.setATR(atrToSpec(atr));
527
784
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
528
785
  h.setVWAP(vwapToSpec(vwap));
529
786
  h.setBollinger(bollingerToSpec(bollingerBands));
787
+ h.setFairValueGaps(fvgToSpec(fairValueGaps));
530
788
  h.setVolume(volumeToSpec(volume));
531
789
  const collapse = volumeCollapseRef.current;
532
790
  if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
@@ -537,7 +795,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
537
795
  footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
538
796
  );
539
797
  if (!morphing) setPicture(h.render());
540
- }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph]);
798
+ }, [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]);
541
799
  return { handle: handleRef.current, picture, volumeCollapseRef };
542
800
  }
543
801
 
@@ -549,6 +807,7 @@ function isSkImage(frame) {
549
807
  function VroomChart(props) {
550
808
  const {
551
809
  candles,
810
+ loading,
552
811
  seriesKey,
553
812
  width: widthProp,
554
813
  height: heightProp,
@@ -559,12 +818,17 @@ function VroomChart(props) {
559
818
  transitionMs,
560
819
  transitionEasing,
561
820
  intervalTransition,
821
+ streamTransition,
822
+ streamTransitionMs,
562
823
  theme,
563
824
  rsi,
564
825
  macd,
826
+ atr,
565
827
  movingAverages,
566
828
  vwap,
567
829
  bollingerBands,
830
+ ichimoku,
831
+ fairValueGaps,
568
832
  volume,
569
833
  crosshairOffset = 40,
570
834
  onCrosshair,
@@ -643,14 +907,28 @@ function VroomChart(props) {
643
907
  theme,
644
908
  rsi,
645
909
  macd,
910
+ atr,
646
911
  movingAverages,
647
912
  vwap,
648
913
  bollingerBands,
914
+ ichimoku,
915
+ fairValueGaps,
649
916
  volume,
650
917
  priceLinesProp,
651
918
  footprintsProp,
652
- { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
919
+ {
920
+ seriesKey,
921
+ transitionMs,
922
+ transitionEasing,
923
+ intervalTransition,
924
+ streamTransition,
925
+ streamTransitionMs,
926
+ reduceMotion,
927
+ onFrame
928
+ },
929
+ loading
653
930
  );
931
+ const showLoadingLine = loading === true && candles.length === 0;
654
932
  const crosshairActive = (0, import_react2.useRef)(false);
655
933
  const footprintActive = (0, import_react2.useRef)(false);
656
934
  const lastCrosshairTime = (0, import_react2.useRef)(null);
@@ -930,7 +1208,7 @@ function VroomChart(props) {
930
1208
  pane: null
931
1209
  });
932
1210
  };
933
- const pan = import_react_native_gesture_handler.Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
1211
+ const pan = import_react_native_gesture_handler.Gesture.Pan().enabled(!showLoadingLine).runOnJS(true).maxPointers(1).onStart((e) => {
934
1212
  cancelDecay();
935
1213
  panMode.current = hitAxis(e.x, e.y);
936
1214
  priceDrag.current = null;
@@ -1025,7 +1303,7 @@ function VroomChart(props) {
1025
1303
  enableX: false,
1026
1304
  enableY: false
1027
1305
  });
1028
- const pinch = import_react_native_gesture_handler.Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
1306
+ const pinch = import_react_native_gesture_handler.Gesture.Pinch().enabled(!showLoadingLine).runOnJS(true).onTouchesDown((e) => {
1029
1307
  if (e.numberOfTouches < 2) return;
1030
1308
  dismissFootprint();
1031
1309
  const [a, b] = e.allTouches;
@@ -1063,7 +1341,7 @@ function VroomChart(props) {
1063
1341
  if (next) applyFrame(next);
1064
1342
  maybeStartAnim();
1065
1343
  });
1066
- const longPress = import_react_native_gesture_handler.Gesture.LongPress().runOnJS(true).onStart((e) => {
1344
+ const longPress = import_react_native_gesture_handler.Gesture.LongPress().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
1067
1345
  if (!handle) return;
1068
1346
  if (hitAxis(e.x, e.y) !== "chart") return;
1069
1347
  if (hitPriceLine(e.x, e.y)) return;
@@ -1083,7 +1361,7 @@ function VroomChart(props) {
1083
1361
  reason: "show"
1084
1362
  });
1085
1363
  });
1086
- const tap = import_react_native_gesture_handler.Gesture.Tap().runOnJS(true).onStart((e) => {
1364
+ const tap = import_react_native_gesture_handler.Gesture.Tap().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
1087
1365
  if (!handle) return;
1088
1366
  const prints = footprints ?? [];
1089
1367
  const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;