react-native-vroom-chart 0.15.0 → 0.16.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 (42) hide show
  1. package/cpp/VroomChartHostObject.cpp +171 -1
  2. package/cpp/_core_include/vroom/vroom_chart.h +137 -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/chart.cpp +461 -172
  8. package/cpp/_core_src/chart.h +150 -1
  9. package/cpp/_core_src/chart_facade.cpp +209 -23
  10. package/cpp/_core_src/fair_value_gaps.cpp +76 -0
  11. package/cpp/_core_src/fair_value_gaps.h +54 -0
  12. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  13. package/cpp/_core_src/fvg_overlay.h +43 -0
  14. package/cpp/_core_src/ichimoku.cpp +65 -0
  15. package/cpp/_core_src/ichimoku.h +45 -0
  16. package/cpp/_core_src/labels.cpp +3 -0
  17. package/cpp/_core_src/line_morph.h +159 -0
  18. package/cpp/_core_src/ma_overlay.cpp +259 -36
  19. package/cpp/_core_src/ma_overlay.h +57 -2
  20. package/cpp/_core_src/macd.cpp +24 -1
  21. package/cpp/_core_src/macd.h +16 -0
  22. package/cpp/_core_src/macd_pane.cpp +71 -62
  23. package/cpp/_core_src/macd_pane.h +13 -1
  24. package/cpp/_core_src/pane_series.h +169 -0
  25. package/cpp/_core_src/rsi.cpp +4 -0
  26. package/cpp/_core_src/rsi.h +7 -0
  27. package/cpp/_core_src/rsi_pane.cpp +85 -29
  28. package/cpp/_core_src/rsi_pane.h +11 -1
  29. package/cpp/_core_src/viewport.h +35 -0
  30. package/lib/index.d.mts +251 -3
  31. package/lib/index.d.ts +251 -3
  32. package/lib/index.js +224 -6
  33. package/lib/index.js.map +1 -1
  34. package/lib/index.mjs +224 -6
  35. package/lib/index.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/src/VroomChart.tsx +21 -3
  38. package/src/dataTransitions.ts +47 -0
  39. package/src/index.ts +5 -0
  40. package/src/jsi.d.ts +97 -1
  41. package/src/types.ts +5 -0
  42. package/src/useChartCore.ts +284 -5
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;
@@ -216,6 +223,7 @@ var MA_SOURCES = [
216
223
  "hlc3",
217
224
  "ohlc4"
218
225
  ];
226
+ var ATR_SMOOTHINGS = ["rma", "sma", "ema"];
219
227
  var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
220
228
  function overlayToNumeric(o) {
221
229
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
@@ -242,7 +250,8 @@ function rsiToSpec(cfg) {
242
250
  maColor: inheritColor(cfg?.maColor),
243
251
  maWidth: cfg?.maWidth ?? -1,
244
252
  bandColor: inheritColor(cfg?.bandColor),
245
- bandsVisible: cfg?.bandsVisible ?? true
253
+ bandsVisible: cfg?.bandsVisible ?? true,
254
+ extremeFill: cfg?.extremeFill ?? true
246
255
  };
247
256
  }
248
257
  function vwapToSpec(cfg) {
@@ -273,6 +282,79 @@ function bollingerToSpec(cfg) {
273
282
  fillOpacity: cfg?.fillOpacity ?? 0.1
274
283
  };
275
284
  }
285
+ var DEFAULT_ICH_GREEN = 4280723098;
286
+ var DEFAULT_ICH_RED = 4293874512;
287
+ var DEFAULT_ICH_BLUE = 4280902399;
288
+ var DEFAULT_ICH_ORANGE = 4294929664;
289
+ var DEFAULT_ICH_TEAL = 4278238420;
290
+ function ichimokuToSpec(cfg) {
291
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
292
+ return {
293
+ enabled: cfg?.enabled ?? false,
294
+ tenkanPeriod: cfg?.tenkanPeriod ?? 9,
295
+ kijunPeriod: cfg?.kijunPeriod ?? 26,
296
+ senkouBPeriod: cfg?.senkouBPeriod ?? 52,
297
+ displacement: cfg?.displacement ?? 26,
298
+ tenkanColor: color(cfg?.tenkanColor, DEFAULT_ICH_BLUE),
299
+ tenkanWidth: cfg?.tenkanWidth ?? 1,
300
+ tenkanEnabled: cfg?.tenkanVisible ?? true,
301
+ kijunColor: color(cfg?.kijunColor, DEFAULT_ICH_RED),
302
+ kijunWidth: cfg?.kijunWidth ?? 1,
303
+ kijunEnabled: cfg?.kijunVisible ?? true,
304
+ senkouAColor: color(cfg?.senkouAColor, DEFAULT_ICH_GREEN),
305
+ senkouAWidth: cfg?.senkouAWidth ?? 1,
306
+ senkouAEnabled: cfg?.senkouAVisible ?? true,
307
+ senkouBColor: color(cfg?.senkouBColor, DEFAULT_ICH_ORANGE),
308
+ senkouBWidth: cfg?.senkouBWidth ?? 1,
309
+ senkouBEnabled: cfg?.senkouBVisible ?? true,
310
+ chikouColor: color(cfg?.chikouColor, DEFAULT_ICH_TEAL),
311
+ chikouWidth: cfg?.chikouWidth ?? 1,
312
+ chikouEnabled: cfg?.chikouVisible ?? true,
313
+ cloudEnabled: cfg?.cloudVisible ?? true,
314
+ bullishCloudColor: color(cfg?.bullishCloudColor, DEFAULT_ICH_GREEN),
315
+ bearishCloudColor: color(cfg?.bearishCloudColor, DEFAULT_ICH_RED),
316
+ cloudOpacity: cfg?.cloudOpacity ?? 0.15
317
+ };
318
+ }
319
+ var DEFAULT_FVG_GREEN = 4280723098;
320
+ var DEFAULT_FVG_RED = 4293874512;
321
+ var FVG_FILL_TYPES = ["close", "wick"];
322
+ var FVG_BORDER_STYLES = ["solid", "dotted", "dashed"];
323
+ function fvgToSpec(cfg) {
324
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
325
+ const bullish = color(cfg?.bullishColor, DEFAULT_FVG_GREEN);
326
+ const bearish = color(cfg?.bearishColor, DEFAULT_FVG_RED);
327
+ return {
328
+ enabled: cfg?.enabled ?? false,
329
+ maxBarsBack: cfg?.maxBarsBack ?? 300,
330
+ waitForClose: cfg?.waitForClose ?? false,
331
+ fillType: Math.max(0, FVG_FILL_TYPES.indexOf(cfg?.fillType ?? "close")),
332
+ deleteAfterFill: cfg?.deleteAfterFill ?? true,
333
+ extendBoxes: cfg?.extendBoxes ?? false,
334
+ boxLength: cfg?.boxLength ?? 20,
335
+ bullishColor: bullish,
336
+ bearishColor: bearish,
337
+ opacity: cfg?.opacity ?? 0.15,
338
+ borderEnabled: cfg?.borderVisible ?? true,
339
+ borderStyle: Math.max(
340
+ 0,
341
+ FVG_BORDER_STYLES.indexOf(cfg?.borderStyle ?? "solid")
342
+ ),
343
+ borderWidth: cfg?.borderWidth ?? 1,
344
+ bullishBorderColor: color(cfg?.bullishBorderColor, bullish),
345
+ bearishBorderColor: color(cfg?.bearishBorderColor, bearish),
346
+ labelsEnabled: cfg?.showLabels ?? true,
347
+ label: cfg?.label ?? "FVG",
348
+ labelDistance: cfg?.labelDistance ?? 10,
349
+ // Alpha 0 is the core's "inherit the border color" sentinel.
350
+ labelColor: color(cfg?.labelColor, 0),
351
+ labelFontSize: cfg?.labelFontSize ?? 0,
352
+ showInverse: cfg?.showInverse ?? false,
353
+ inverseBullishColor: color(cfg?.inverseBullishColor, bullish),
354
+ inverseBearishColor: color(cfg?.inverseBearishColor, bearish),
355
+ inverseLabel: cfg?.inverseLabel ?? "iFVG"
356
+ };
357
+ }
276
358
  function macdToSpec(cfg) {
277
359
  const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
278
360
  return {
@@ -298,6 +380,15 @@ function macdToSpec(cfg) {
298
380
  zeroVisible: cfg?.zeroLineVisible ?? true
299
381
  };
300
382
  }
383
+ function atrToSpec(cfg) {
384
+ return {
385
+ enabled: cfg?.enabled ?? false,
386
+ period: cfg?.period ?? 14,
387
+ smoothing: Math.max(0, ATR_SMOOTHINGS.indexOf(cfg?.smoothing ?? "rma")),
388
+ lineColor: inheritColor(cfg?.lineColor),
389
+ lineWidth: cfg?.lineWidth ?? -1
390
+ };
391
+ }
301
392
  function volumeToSpec(cfg) {
302
393
  return {
303
394
  enabled: cfg?.enabled ?? true,
@@ -368,23 +459,37 @@ function ensureInstalled() {
368
459
  }
369
460
  installed = true;
370
461
  }
371
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, footprints, transition) {
462
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition) {
372
463
  const handleRef = useRef(null);
373
464
  const defaultWidthAppliedRef = useRef(false);
374
465
  const volumeCollapseRef = useRef(null);
375
466
  const prevDataRef = useRef(null);
376
467
  const intervalMorphRaf = useRef(null);
468
+ const streamRaf = useRef(null);
469
+ const streamWindowRef = useRef(null);
470
+ const streamMorphRef = useRef(false);
377
471
  const [picture, setPicture] = useState(null);
378
472
  if (!handleRef.current && size.width > 0 && size.height > 0) {
379
473
  ensureInstalled();
380
474
  handleRef.current = globalThis.VroomChartJSI.create();
381
475
  }
382
- const animRef = useRef({ ms: 300, easing: void 0, reduceMotion: false, interval: "transform" });
476
+ const animRef = useRef({
477
+ ms: 300,
478
+ easing: void 0,
479
+ reduceMotion: false,
480
+ interval: "transform",
481
+ stream: "none",
482
+ streamMs: 150
483
+ });
383
484
  animRef.current = {
384
485
  ms: Math.max(0, transition?.transitionMs ?? 300),
385
486
  easing: transition?.transitionEasing,
386
487
  reduceMotion: transition?.reduceMotion ?? false,
387
- interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
488
+ interval: transition?.intervalTransition === "fade" ? "fade" : "transform",
489
+ stream: transition?.streamTransition === "transform" ? "transform" : "none",
490
+ // Shorter than transitionMs by default: ticks can land faster than a 300ms
491
+ // curve, and every one that does interrupts the last.
492
+ streamMs: Math.max(0, transition?.streamTransitionMs ?? 150)
388
493
  };
389
494
  const onFrameRef = useRef(transition?.onFrame);
390
495
  onFrameRef.current = transition?.onFrame;
@@ -408,12 +513,70 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
408
513
  };
409
514
  intervalMorphRaf.current = requestAnimationFrame(step);
410
515
  }, []);
516
+ const settleStream = useCallback((keepMorph = false) => {
517
+ if (streamRaf.current != null) {
518
+ cancelAnimationFrame(streamRaf.current);
519
+ streamRaf.current = null;
520
+ }
521
+ const h = handleRef.current;
522
+ const target = streamWindowRef.current;
523
+ streamWindowRef.current = null;
524
+ if (target) h?.setVisibleRange(target.startMs, target.endMs);
525
+ if (streamMorphRef.current && !keepMorph) {
526
+ streamMorphRef.current = false;
527
+ h?.setIntervalMorph(1);
528
+ }
529
+ }, []);
530
+ const startStreamAnim = useCallback(
531
+ (h, morphing, window) => {
532
+ const { streamMs, easing } = animRef.current;
533
+ let from = window ? h.getVisibleRange() : null;
534
+ let applied = null;
535
+ streamWindowRef.current = window;
536
+ streamMorphRef.current = morphing;
537
+ const start = performance.now();
538
+ const step = (now) => {
539
+ if (from && applied) {
540
+ const now_w = h.getVisibleRange();
541
+ if (now_w.startMs !== applied.startMs || now_w.endMs !== applied.endMs) {
542
+ from = null;
543
+ streamWindowRef.current = null;
544
+ }
545
+ }
546
+ const p = Math.min(1, (now - start) / streamMs);
547
+ const e = p < 1 ? ease(easing, p) : 1;
548
+ if (morphing) h.setIntervalMorph(e);
549
+ if (from && window) {
550
+ applied = {
551
+ startMs: Math.round(from.startMs + (window.startMs - from.startMs) * e),
552
+ endMs: Math.round(from.endMs + (window.endMs - from.endMs) * e)
553
+ };
554
+ h.setVisibleRange(applied.startMs, applied.endMs);
555
+ }
556
+ const pic = h.render();
557
+ if (pic) onFrameRef.current?.(pic);
558
+ if (p < 1) {
559
+ streamRaf.current = requestAnimationFrame(step);
560
+ } else {
561
+ streamRaf.current = null;
562
+ streamWindowRef.current = null;
563
+ streamMorphRef.current = false;
564
+ }
565
+ };
566
+ streamRaf.current = requestAnimationFrame(step);
567
+ },
568
+ []
569
+ );
411
570
  useEffect(() => {
412
571
  return () => {
413
572
  if (intervalMorphRaf.current != null) {
414
573
  cancelAnimationFrame(intervalMorphRaf.current);
415
574
  intervalMorphRaf.current = null;
416
575
  }
576
+ if (streamRaf.current != null) {
577
+ cancelAnimationFrame(streamRaf.current);
578
+ streamRaf.current = null;
579
+ }
417
580
  };
418
581
  }, []);
419
582
  const explicit = visibleRange != null;
@@ -422,9 +585,12 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
422
585
  const themeKey = theme ? JSON.stringify(theme) : "";
423
586
  const rsiKey = rsi ? JSON.stringify(rsi) : "";
424
587
  const macdKey = macd ? JSON.stringify(macd) : "";
588
+ const atrKey = atr ? JSON.stringify(atr) : "";
425
589
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
426
590
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
427
591
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
592
+ const ichimokuKey = ichimoku ? JSON.stringify(ichimoku) : "";
593
+ const fvgKey = fairValueGaps ? JSON.stringify(fairValueGaps) : "";
428
594
  const volumeKey = volume ? JSON.stringify(volume) : "";
429
595
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
430
596
  const footprintsKey = footprints ? JSON.stringify(footprints) : "";
@@ -432,6 +598,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
432
598
  const h = handleRef.current;
433
599
  if (!h) return;
434
600
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
601
+ h.setIchimoku(ichimokuToSpec(ichimoku));
435
602
  if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
436
603
  h.setDefaultCandleWidth(defaultCandleWidth);
437
604
  defaultWidthAppliedRef.current = true;
@@ -444,6 +611,36 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
444
611
  const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
445
612
  let tfArgs = null;
446
613
  let prevEnvelope = null;
614
+ let stream = null;
615
+ if (transitionKind === "stream" && prev != null && !explicit) {
616
+ const { stream: mode, streamMs, reduceMotion } = animRef.current;
617
+ const stepMs = inferStepMs(candles);
618
+ if (mode === "transform" && streamMs > 0 && stepMs != null && !reduceMotion && onFrameRef.current != null) {
619
+ const lastMs = candles[candles.length - 1].timeMs;
620
+ const prevLastMs = prev.candles[prev.candles.length - 1].timeMs;
621
+ if (classifyStream(prev.candles, candles) === "append") {
622
+ const w = h.getVisibleRange();
623
+ const prevStepMs = inferStepMs(prev.candles) ?? stepMs;
624
+ if (isPinnedToLatest(w, prevLastMs, prevStepMs)) {
625
+ const by = lastMs - prevLastMs;
626
+ stream = {
627
+ morph: false,
628
+ window: { startMs: w.startMs + by, endMs: w.endMs + by }
629
+ };
630
+ }
631
+ } else {
632
+ stream = { morph: true, window: null };
633
+ }
634
+ }
635
+ if (stream?.morph) {
636
+ settleStream(true);
637
+ h.beginStreamMorph();
638
+ } else {
639
+ settleStream();
640
+ }
641
+ } else if (transitionKind === "stream") {
642
+ settleStream();
643
+ }
447
644
  if (transitionKind === "timeframe" && prev != null) {
448
645
  const oldWindow = h.getVisibleRange();
449
646
  const oldStepMs = inferStepMs(prev.candles);
@@ -479,6 +676,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
479
676
  if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
480
677
  else h.resetPriceScale();
481
678
  if (morphing) startIntervalMorph(h);
679
+ } else if (stream) {
680
+ startStreamAnim(h, stream.morph, stream.window);
482
681
  } else if (transitionKind === "reset") {
483
682
  h.resetView();
484
683
  }
@@ -496,9 +695,11 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
496
695
  }
497
696
  h.setRSI(rsiToSpec(rsi));
498
697
  h.setMACD(macdToSpec(macd));
698
+ h.setATR(atrToSpec(atr));
499
699
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
500
700
  h.setVWAP(vwapToSpec(vwap));
501
701
  h.setBollinger(bollingerToSpec(bollingerBands));
702
+ h.setFairValueGaps(fvgToSpec(fairValueGaps));
502
703
  h.setVolume(volumeToSpec(volume));
503
704
  const collapse = volumeCollapseRef.current;
504
705
  if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
@@ -509,7 +710,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
509
710
  footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
510
711
  );
511
712
  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]);
713
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph, startStreamAnim, settleStream]);
513
714
  return { handle: handleRef.current, picture, volumeCollapseRef };
514
715
  }
515
716
 
@@ -531,12 +732,17 @@ function VroomChart(props) {
531
732
  transitionMs,
532
733
  transitionEasing,
533
734
  intervalTransition,
735
+ streamTransition,
736
+ streamTransitionMs,
534
737
  theme,
535
738
  rsi,
536
739
  macd,
740
+ atr,
537
741
  movingAverages,
538
742
  vwap,
539
743
  bollingerBands,
744
+ ichimoku,
745
+ fairValueGaps,
540
746
  volume,
541
747
  crosshairOffset = 40,
542
748
  onCrosshair,
@@ -615,13 +821,25 @@ function VroomChart(props) {
615
821
  theme,
616
822
  rsi,
617
823
  macd,
824
+ atr,
618
825
  movingAverages,
619
826
  vwap,
620
827
  bollingerBands,
828
+ ichimoku,
829
+ fairValueGaps,
621
830
  volume,
622
831
  priceLinesProp,
623
832
  footprintsProp,
624
- { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
833
+ {
834
+ seriesKey,
835
+ transitionMs,
836
+ transitionEasing,
837
+ intervalTransition,
838
+ streamTransition,
839
+ streamTransitionMs,
840
+ reduceMotion,
841
+ onFrame
842
+ }
625
843
  );
626
844
  const crosshairActive = useRef2(false);
627
845
  const footprintActive = useRef2(false);