react-native-vroom-chart 0.14.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 (47) hide show
  1. package/cpp/VroomChartHostObject.cpp +289 -2
  2. package/cpp/_core_include/vroom/vroom_chart.h +221 -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 +477 -172
  8. package/cpp/_core_src/chart.h +173 -1
  9. package/cpp/_core_src/chart_facade.cpp +317 -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/footprints.cpp +226 -0
  13. package/cpp/_core_src/footprints.h +45 -0
  14. package/cpp/_core_src/footprints_layout.cpp +140 -0
  15. package/cpp/_core_src/footprints_layout.h +94 -0
  16. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  17. package/cpp/_core_src/fvg_overlay.h +43 -0
  18. package/cpp/_core_src/ichimoku.cpp +65 -0
  19. package/cpp/_core_src/ichimoku.h +45 -0
  20. package/cpp/_core_src/labels.cpp +3 -0
  21. package/cpp/_core_src/line_morph.h +159 -0
  22. package/cpp/_core_src/ma_overlay.cpp +259 -36
  23. package/cpp/_core_src/ma_overlay.h +57 -2
  24. package/cpp/_core_src/macd.cpp +24 -1
  25. package/cpp/_core_src/macd.h +16 -0
  26. package/cpp/_core_src/macd_pane.cpp +71 -62
  27. package/cpp/_core_src/macd_pane.h +13 -1
  28. package/cpp/_core_src/pane_series.h +169 -0
  29. package/cpp/_core_src/rsi.cpp +4 -0
  30. package/cpp/_core_src/rsi.h +7 -0
  31. package/cpp/_core_src/rsi_pane.cpp +85 -29
  32. package/cpp/_core_src/rsi_pane.h +11 -1
  33. package/cpp/_core_src/tip_pulse.h +4 -4
  34. package/cpp/_core_src/viewport.h +35 -0
  35. package/lib/index.d.mts +385 -3
  36. package/lib/index.d.ts +385 -3
  37. package/lib/index.js +305 -7
  38. package/lib/index.js.map +1 -1
  39. package/lib/index.mjs +305 -7
  40. package/lib/index.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/src/VroomChart.tsx +108 -7
  43. package/src/dataTransitions.ts +47 -0
  44. package/src/index.ts +10 -0
  45. package/src/jsi.d.ts +136 -1
  46. package/src/types.ts +10 -0
  47. package/src/useChartCore.ts +330 -5
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;
@@ -244,6 +251,7 @@ var MA_SOURCES = [
244
251
  "hlc3",
245
252
  "ohlc4"
246
253
  ];
254
+ var ATR_SMOOTHINGS = ["rma", "sma", "ema"];
247
255
  var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
248
256
  function overlayToNumeric(o) {
249
257
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
@@ -270,7 +278,8 @@ function rsiToSpec(cfg) {
270
278
  maColor: inheritColor(cfg?.maColor),
271
279
  maWidth: cfg?.maWidth ?? -1,
272
280
  bandColor: inheritColor(cfg?.bandColor),
273
- bandsVisible: cfg?.bandsVisible ?? true
281
+ bandsVisible: cfg?.bandsVisible ?? true,
282
+ extremeFill: cfg?.extremeFill ?? true
274
283
  };
275
284
  }
276
285
  function vwapToSpec(cfg) {
@@ -301,6 +310,79 @@ function bollingerToSpec(cfg) {
301
310
  fillOpacity: cfg?.fillOpacity ?? 0.1
302
311
  };
303
312
  }
313
+ var DEFAULT_ICH_GREEN = 4280723098;
314
+ var DEFAULT_ICH_RED = 4293874512;
315
+ var DEFAULT_ICH_BLUE = 4280902399;
316
+ var DEFAULT_ICH_ORANGE = 4294929664;
317
+ var DEFAULT_ICH_TEAL = 4278238420;
318
+ function ichimokuToSpec(cfg) {
319
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
320
+ return {
321
+ enabled: cfg?.enabled ?? false,
322
+ tenkanPeriod: cfg?.tenkanPeriod ?? 9,
323
+ kijunPeriod: cfg?.kijunPeriod ?? 26,
324
+ senkouBPeriod: cfg?.senkouBPeriod ?? 52,
325
+ displacement: cfg?.displacement ?? 26,
326
+ tenkanColor: color(cfg?.tenkanColor, DEFAULT_ICH_BLUE),
327
+ tenkanWidth: cfg?.tenkanWidth ?? 1,
328
+ tenkanEnabled: cfg?.tenkanVisible ?? true,
329
+ kijunColor: color(cfg?.kijunColor, DEFAULT_ICH_RED),
330
+ kijunWidth: cfg?.kijunWidth ?? 1,
331
+ kijunEnabled: cfg?.kijunVisible ?? true,
332
+ senkouAColor: color(cfg?.senkouAColor, DEFAULT_ICH_GREEN),
333
+ senkouAWidth: cfg?.senkouAWidth ?? 1,
334
+ senkouAEnabled: cfg?.senkouAVisible ?? true,
335
+ senkouBColor: color(cfg?.senkouBColor, DEFAULT_ICH_ORANGE),
336
+ senkouBWidth: cfg?.senkouBWidth ?? 1,
337
+ senkouBEnabled: cfg?.senkouBVisible ?? true,
338
+ chikouColor: color(cfg?.chikouColor, DEFAULT_ICH_TEAL),
339
+ chikouWidth: cfg?.chikouWidth ?? 1,
340
+ chikouEnabled: cfg?.chikouVisible ?? true,
341
+ cloudEnabled: cfg?.cloudVisible ?? true,
342
+ bullishCloudColor: color(cfg?.bullishCloudColor, DEFAULT_ICH_GREEN),
343
+ bearishCloudColor: color(cfg?.bearishCloudColor, DEFAULT_ICH_RED),
344
+ cloudOpacity: cfg?.cloudOpacity ?? 0.15
345
+ };
346
+ }
347
+ var DEFAULT_FVG_GREEN = 4280723098;
348
+ var DEFAULT_FVG_RED = 4293874512;
349
+ var FVG_FILL_TYPES = ["close", "wick"];
350
+ var FVG_BORDER_STYLES = ["solid", "dotted", "dashed"];
351
+ function fvgToSpec(cfg) {
352
+ const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
353
+ const bullish = color(cfg?.bullishColor, DEFAULT_FVG_GREEN);
354
+ const bearish = color(cfg?.bearishColor, DEFAULT_FVG_RED);
355
+ return {
356
+ enabled: cfg?.enabled ?? false,
357
+ maxBarsBack: cfg?.maxBarsBack ?? 300,
358
+ waitForClose: cfg?.waitForClose ?? false,
359
+ fillType: Math.max(0, FVG_FILL_TYPES.indexOf(cfg?.fillType ?? "close")),
360
+ deleteAfterFill: cfg?.deleteAfterFill ?? true,
361
+ extendBoxes: cfg?.extendBoxes ?? false,
362
+ boxLength: cfg?.boxLength ?? 20,
363
+ bullishColor: bullish,
364
+ bearishColor: bearish,
365
+ opacity: cfg?.opacity ?? 0.15,
366
+ borderEnabled: cfg?.borderVisible ?? true,
367
+ borderStyle: Math.max(
368
+ 0,
369
+ FVG_BORDER_STYLES.indexOf(cfg?.borderStyle ?? "solid")
370
+ ),
371
+ borderWidth: cfg?.borderWidth ?? 1,
372
+ bullishBorderColor: color(cfg?.bullishBorderColor, bullish),
373
+ bearishBorderColor: color(cfg?.bearishBorderColor, bearish),
374
+ labelsEnabled: cfg?.showLabels ?? true,
375
+ label: cfg?.label ?? "FVG",
376
+ labelDistance: cfg?.labelDistance ?? 10,
377
+ // Alpha 0 is the core's "inherit the border color" sentinel.
378
+ labelColor: color(cfg?.labelColor, 0),
379
+ labelFontSize: cfg?.labelFontSize ?? 0,
380
+ showInverse: cfg?.showInverse ?? false,
381
+ inverseBullishColor: color(cfg?.inverseBullishColor, bullish),
382
+ inverseBearishColor: color(cfg?.inverseBearishColor, bearish),
383
+ inverseLabel: cfg?.inverseLabel ?? "iFVG"
384
+ };
385
+ }
304
386
  function macdToSpec(cfg) {
305
387
  const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
306
388
  return {
@@ -326,6 +408,15 @@ function macdToSpec(cfg) {
326
408
  zeroVisible: cfg?.zeroLineVisible ?? true
327
409
  };
328
410
  }
411
+ function atrToSpec(cfg) {
412
+ return {
413
+ enabled: cfg?.enabled ?? false,
414
+ period: cfg?.period ?? 14,
415
+ smoothing: Math.max(0, ATR_SMOOTHINGS.indexOf(cfg?.smoothing ?? "rma")),
416
+ lineColor: inheritColor(cfg?.lineColor),
417
+ lineWidth: cfg?.lineWidth ?? -1
418
+ };
419
+ }
329
420
  function volumeToSpec(cfg) {
330
421
  return {
331
422
  enabled: cfg?.enabled ?? true,
@@ -363,6 +454,29 @@ function priceLinesToSpec(cfg) {
363
454
  };
364
455
  }
365
456
  var EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
457
+ var DEFAULT_FOOTPRINT_HOVER_BOOST = 1.25;
458
+ var FOOTPRINT_BUY = 0;
459
+ var FOOTPRINT_SELL = 1;
460
+ var UNBUCKETABLE_MS = -864e13;
461
+ function footprintsToSpec(cfg) {
462
+ return {
463
+ // Index alignment is load-bearing: the core reports hits as indices into this
464
+ // array and the gesture layer maps them straight back to the consumer's
465
+ // `footprints`. So a non-finite time — which can't be bucketed and would
466
+ // reach the native side as a garbage int64 — is neutralized *in place* rather
467
+ // than filtered out, which would shift every index after it onto the wrong
468
+ // trade.
469
+ prints: cfg.prints.map((f) => ({
470
+ timeMs: Number.isFinite(f.timeMs) ? f.timeMs : UNBUCKETABLE_MS,
471
+ side: f.side === "sell" ? FOOTPRINT_SELL : FOOTPRINT_BUY
472
+ })),
473
+ radiusPx: cfg.style?.radius ?? 0,
474
+ gapPx: cfg.style?.gap ?? 0,
475
+ marginPx: cfg.style?.margin ?? 0,
476
+ hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_FOOTPRINT_HOVER_BOOST
477
+ };
478
+ }
479
+ var EMPTY_FOOTPRINTS = footprintsToSpec({ prints: [] });
366
480
  var installed = false;
367
481
  function ensureInstalled() {
368
482
  if (installed) return;
@@ -373,23 +487,37 @@ function ensureInstalled() {
373
487
  }
374
488
  installed = true;
375
489
  }
376
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
490
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition) {
377
491
  const handleRef = (0, import_react.useRef)(null);
378
492
  const defaultWidthAppliedRef = (0, import_react.useRef)(false);
379
493
  const volumeCollapseRef = (0, import_react.useRef)(null);
380
494
  const prevDataRef = (0, import_react.useRef)(null);
381
495
  const intervalMorphRaf = (0, import_react.useRef)(null);
496
+ const streamRaf = (0, import_react.useRef)(null);
497
+ const streamWindowRef = (0, import_react.useRef)(null);
498
+ const streamMorphRef = (0, import_react.useRef)(false);
382
499
  const [picture, setPicture] = (0, import_react.useState)(null);
383
500
  if (!handleRef.current && size.width > 0 && size.height > 0) {
384
501
  ensureInstalled();
385
502
  handleRef.current = globalThis.VroomChartJSI.create();
386
503
  }
387
- const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false, interval: "transform" });
504
+ const animRef = (0, import_react.useRef)({
505
+ ms: 300,
506
+ easing: void 0,
507
+ reduceMotion: false,
508
+ interval: "transform",
509
+ stream: "none",
510
+ streamMs: 150
511
+ });
388
512
  animRef.current = {
389
513
  ms: Math.max(0, transition?.transitionMs ?? 300),
390
514
  easing: transition?.transitionEasing,
391
515
  reduceMotion: transition?.reduceMotion ?? false,
392
- interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
516
+ interval: transition?.intervalTransition === "fade" ? "fade" : "transform",
517
+ stream: transition?.streamTransition === "transform" ? "transform" : "none",
518
+ // Shorter than transitionMs by default: ticks can land faster than a 300ms
519
+ // curve, and every one that does interrupts the last.
520
+ streamMs: Math.max(0, transition?.streamTransitionMs ?? 150)
393
521
  };
394
522
  const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
395
523
  onFrameRef.current = transition?.onFrame;
@@ -413,12 +541,70 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
413
541
  };
414
542
  intervalMorphRaf.current = requestAnimationFrame(step);
415
543
  }, []);
544
+ const settleStream = (0, import_react.useCallback)((keepMorph = false) => {
545
+ if (streamRaf.current != null) {
546
+ cancelAnimationFrame(streamRaf.current);
547
+ streamRaf.current = null;
548
+ }
549
+ const h = handleRef.current;
550
+ const target = streamWindowRef.current;
551
+ streamWindowRef.current = null;
552
+ if (target) h?.setVisibleRange(target.startMs, target.endMs);
553
+ if (streamMorphRef.current && !keepMorph) {
554
+ streamMorphRef.current = false;
555
+ h?.setIntervalMorph(1);
556
+ }
557
+ }, []);
558
+ const startStreamAnim = (0, import_react.useCallback)(
559
+ (h, morphing, window) => {
560
+ const { streamMs, easing } = animRef.current;
561
+ let from = window ? h.getVisibleRange() : null;
562
+ let applied = null;
563
+ streamWindowRef.current = window;
564
+ streamMorphRef.current = morphing;
565
+ const start = performance.now();
566
+ const step = (now) => {
567
+ if (from && applied) {
568
+ const now_w = h.getVisibleRange();
569
+ if (now_w.startMs !== applied.startMs || now_w.endMs !== applied.endMs) {
570
+ from = null;
571
+ streamWindowRef.current = null;
572
+ }
573
+ }
574
+ const p = Math.min(1, (now - start) / streamMs);
575
+ const e = p < 1 ? ease(easing, p) : 1;
576
+ if (morphing) h.setIntervalMorph(e);
577
+ if (from && window) {
578
+ applied = {
579
+ startMs: Math.round(from.startMs + (window.startMs - from.startMs) * e),
580
+ endMs: Math.round(from.endMs + (window.endMs - from.endMs) * e)
581
+ };
582
+ h.setVisibleRange(applied.startMs, applied.endMs);
583
+ }
584
+ const pic = h.render();
585
+ if (pic) onFrameRef.current?.(pic);
586
+ if (p < 1) {
587
+ streamRaf.current = requestAnimationFrame(step);
588
+ } else {
589
+ streamRaf.current = null;
590
+ streamWindowRef.current = null;
591
+ streamMorphRef.current = false;
592
+ }
593
+ };
594
+ streamRaf.current = requestAnimationFrame(step);
595
+ },
596
+ []
597
+ );
416
598
  (0, import_react.useEffect)(() => {
417
599
  return () => {
418
600
  if (intervalMorphRaf.current != null) {
419
601
  cancelAnimationFrame(intervalMorphRaf.current);
420
602
  intervalMorphRaf.current = null;
421
603
  }
604
+ if (streamRaf.current != null) {
605
+ cancelAnimationFrame(streamRaf.current);
606
+ streamRaf.current = null;
607
+ }
422
608
  };
423
609
  }, []);
424
610
  const explicit = visibleRange != null;
@@ -427,15 +613,20 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
427
613
  const themeKey = theme ? JSON.stringify(theme) : "";
428
614
  const rsiKey = rsi ? JSON.stringify(rsi) : "";
429
615
  const macdKey = macd ? JSON.stringify(macd) : "";
616
+ const atrKey = atr ? JSON.stringify(atr) : "";
430
617
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
431
618
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
432
619
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
620
+ const ichimokuKey = ichimoku ? JSON.stringify(ichimoku) : "";
621
+ const fvgKey = fairValueGaps ? JSON.stringify(fairValueGaps) : "";
433
622
  const volumeKey = volume ? JSON.stringify(volume) : "";
434
623
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
624
+ const footprintsKey = footprints ? JSON.stringify(footprints) : "";
435
625
  (0, import_react.useEffect)(() => {
436
626
  const h = handleRef.current;
437
627
  if (!h) return;
438
628
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
629
+ h.setIchimoku(ichimokuToSpec(ichimoku));
439
630
  if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
440
631
  h.setDefaultCandleWidth(defaultCandleWidth);
441
632
  defaultWidthAppliedRef.current = true;
@@ -448,6 +639,36 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
448
639
  const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
449
640
  let tfArgs = null;
450
641
  let prevEnvelope = null;
642
+ let stream = null;
643
+ if (transitionKind === "stream" && prev != null && !explicit) {
644
+ const { stream: mode, streamMs, reduceMotion } = animRef.current;
645
+ const stepMs = inferStepMs(candles);
646
+ if (mode === "transform" && streamMs > 0 && stepMs != null && !reduceMotion && onFrameRef.current != null) {
647
+ const lastMs = candles[candles.length - 1].timeMs;
648
+ const prevLastMs = prev.candles[prev.candles.length - 1].timeMs;
649
+ if (classifyStream(prev.candles, candles) === "append") {
650
+ const w = h.getVisibleRange();
651
+ const prevStepMs = inferStepMs(prev.candles) ?? stepMs;
652
+ if (isPinnedToLatest(w, prevLastMs, prevStepMs)) {
653
+ const by = lastMs - prevLastMs;
654
+ stream = {
655
+ morph: false,
656
+ window: { startMs: w.startMs + by, endMs: w.endMs + by }
657
+ };
658
+ }
659
+ } else {
660
+ stream = { morph: true, window: null };
661
+ }
662
+ }
663
+ if (stream?.morph) {
664
+ settleStream(true);
665
+ h.beginStreamMorph();
666
+ } else {
667
+ settleStream();
668
+ }
669
+ } else if (transitionKind === "stream") {
670
+ settleStream();
671
+ }
451
672
  if (transitionKind === "timeframe" && prev != null) {
452
673
  const oldWindow = h.getVisibleRange();
453
674
  const oldStepMs = inferStepMs(prev.candles);
@@ -483,6 +704,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
483
704
  if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
484
705
  else h.resetPriceScale();
485
706
  if (morphing) startIntervalMorph(h);
707
+ } else if (stream) {
708
+ startStreamAnim(h, stream.morph, stream.window);
486
709
  } else if (transitionKind === "reset") {
487
710
  h.resetView();
488
711
  }
@@ -500,21 +723,27 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
500
723
  }
501
724
  h.setRSI(rsiToSpec(rsi));
502
725
  h.setMACD(macdToSpec(macd));
726
+ h.setATR(atrToSpec(atr));
503
727
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
504
728
  h.setVWAP(vwapToSpec(vwap));
505
729
  h.setBollinger(bollingerToSpec(bollingerBands));
730
+ h.setFairValueGaps(fvgToSpec(fairValueGaps));
506
731
  h.setVolume(volumeToSpec(volume));
507
732
  const collapse = volumeCollapseRef.current;
508
733
  if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
509
734
  h.setPriceLines(
510
735
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
511
736
  );
737
+ h.setFootprints(
738
+ footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
739
+ );
512
740
  if (!morphing) setPicture(h.render());
513
- }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
741
+ }, [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]);
514
742
  return { handle: handleRef.current, picture, volumeCollapseRef };
515
743
  }
516
744
 
517
745
  // src/VroomChart.tsx
746
+ var FOOTPRINT_SELL2 = 1;
518
747
  function isSkImage(frame) {
519
748
  return typeof frame.getImageInfo === "function";
520
749
  }
@@ -531,12 +760,17 @@ function VroomChart(props) {
531
760
  transitionMs,
532
761
  transitionEasing,
533
762
  intervalTransition,
763
+ streamTransition,
764
+ streamTransitionMs,
534
765
  theme,
535
766
  rsi,
536
767
  macd,
768
+ atr,
537
769
  movingAverages,
538
770
  vwap,
539
771
  bollingerBands,
772
+ ichimoku,
773
+ fairValueGaps,
540
774
  volume,
541
775
  crosshairOffset = 40,
542
776
  onCrosshair,
@@ -545,7 +779,10 @@ function VroomChart(props) {
545
779
  priceLinesStyle,
546
780
  onPriceLineDrag,
547
781
  onPriceLineDragEnd,
548
- onPriceLineClose
782
+ onPriceLineClose,
783
+ footprints,
784
+ footprintsStyle,
785
+ onFootprint
549
786
  } = props;
550
787
  const [measured, setMeasured] = (0, import_react2.useState)({ width: 0, height: 0 });
551
788
  const width = widthProp ?? measured.width;
@@ -565,6 +802,10 @@ function VroomChart(props) {
565
802
  } : void 0,
566
803
  [priceLines, priceLinesStyle, onPriceLineClose]
567
804
  );
805
+ const footprintsProp = (0, import_react2.useMemo)(
806
+ () => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
807
+ [footprints, footprintsStyle]
808
+ );
568
809
  const emptyPicture = (0, import_react2.useMemo)(() => {
569
810
  const rec = import_react_native_skia.Skia.PictureRecorder();
570
811
  rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
@@ -608,14 +849,28 @@ function VroomChart(props) {
608
849
  theme,
609
850
  rsi,
610
851
  macd,
852
+ atr,
611
853
  movingAverages,
612
854
  vwap,
613
855
  bollingerBands,
856
+ ichimoku,
857
+ fairValueGaps,
614
858
  volume,
615
859
  priceLinesProp,
616
- { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
860
+ footprintsProp,
861
+ {
862
+ seriesKey,
863
+ transitionMs,
864
+ transitionEasing,
865
+ intervalTransition,
866
+ streamTransition,
867
+ streamTransitionMs,
868
+ reduceMotion,
869
+ onFrame
870
+ }
617
871
  );
618
872
  const crosshairActive = (0, import_react2.useRef)(false);
873
+ const footprintActive = (0, import_react2.useRef)(false);
619
874
  const lastCrosshairTime = (0, import_react2.useRef)(null);
620
875
  const decayRaf = (0, import_react2.useRef)(null);
621
876
  const cancelDecay = (0, import_react2.useCallback)(() => {
@@ -875,6 +1130,24 @@ function VroomChart(props) {
875
1130
  null
876
1131
  );
877
1132
  const panMode = (0, import_react2.useRef)("chart");
1133
+ const dismissFootprint = (redraw = true) => {
1134
+ if (!handle || !footprintActive.current) return;
1135
+ footprintActive.current = false;
1136
+ handle.setFootprintHover(0, -1);
1137
+ if (redraw) {
1138
+ const frame = handle.render();
1139
+ if (frame) applyFrame(frame);
1140
+ }
1141
+ onFootprint?.({
1142
+ active: false,
1143
+ reason: "hide",
1144
+ side: null,
1145
+ timeMs: null,
1146
+ footprints: [],
1147
+ badge: null,
1148
+ pane: null
1149
+ });
1150
+ };
878
1151
  const pan = import_react_native_gesture_handler.Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
879
1152
  cancelDecay();
880
1153
  panMode.current = hitAxis(e.x, e.y);
@@ -889,6 +1162,7 @@ function VroomChart(props) {
889
1162
  if (p) applyFrame(p);
890
1163
  }
891
1164
  }
1165
+ if (panMode.current !== "price-line") dismissFootprint();
892
1166
  }).onChange((e) => {
893
1167
  if (!handle) return;
894
1168
  let next = null;
@@ -971,6 +1245,7 @@ function VroomChart(props) {
971
1245
  });
972
1246
  const pinch = import_react_native_gesture_handler.Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
973
1247
  if (e.numberOfTouches < 2) return;
1248
+ dismissFootprint();
974
1249
  const [a, b] = e.allTouches;
975
1250
  const spanX = Math.abs(a.x - b.x);
976
1251
  const spanY = Math.abs(a.y - b.y);
@@ -1011,6 +1286,7 @@ function VroomChart(props) {
1011
1286
  if (hitAxis(e.x, e.y) !== "chart") return;
1012
1287
  if (hitPriceLine(e.x, e.y)) return;
1013
1288
  cancelDecay();
1289
+ dismissFootprint(false);
1014
1290
  crosshairActive.current = true;
1015
1291
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
1016
1292
  if (ch) applyFrame(ch);
@@ -1027,6 +1303,28 @@ function VroomChart(props) {
1027
1303
  });
1028
1304
  const tap = import_react_native_gesture_handler.Gesture.Tap().runOnJS(true).onStart((e) => {
1029
1305
  if (!handle) return;
1306
+ const prints = footprints ?? [];
1307
+ const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
1308
+ if (fp) {
1309
+ handle.setFootprintHover(fp.candleTimeMs, fp.side);
1310
+ const frame = handle.render();
1311
+ if (frame) applyFrame(frame);
1312
+ const wasActive = footprintActive.current;
1313
+ footprintActive.current = true;
1314
+ onFootprint?.({
1315
+ active: true,
1316
+ reason: wasActive ? "move" : "show",
1317
+ side: fp.side === FOOTPRINT_SELL2 ? "sell" : "buy",
1318
+ timeMs: fp.candleTimeMs,
1319
+ // The core reports indices into the array we last pushed, which is this
1320
+ // same prop — so this rejoins each badge to the consumer's own objects.
1321
+ footprints: fp.indices.map((i) => prints[i]).filter((f) => f != null),
1322
+ badge: { x: fp.x, y: fp.y, radius: fp.radius },
1323
+ pane: fp.pane
1324
+ });
1325
+ return;
1326
+ }
1327
+ dismissFootprint();
1030
1328
  const pl = hitPriceLine(e.x, e.y);
1031
1329
  if (pl && pl.part === 1) {
1032
1330
  onPriceLineClose?.(pl.line.id);