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.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,
@@ -335,6 +426,29 @@ function priceLinesToSpec(cfg) {
335
426
  };
336
427
  }
337
428
  var EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
429
+ var DEFAULT_FOOTPRINT_HOVER_BOOST = 1.25;
430
+ var FOOTPRINT_BUY = 0;
431
+ var FOOTPRINT_SELL = 1;
432
+ var UNBUCKETABLE_MS = -864e13;
433
+ function footprintsToSpec(cfg) {
434
+ return {
435
+ // Index alignment is load-bearing: the core reports hits as indices into this
436
+ // array and the gesture layer maps them straight back to the consumer's
437
+ // `footprints`. So a non-finite time — which can't be bucketed and would
438
+ // reach the native side as a garbage int64 — is neutralized *in place* rather
439
+ // than filtered out, which would shift every index after it onto the wrong
440
+ // trade.
441
+ prints: cfg.prints.map((f) => ({
442
+ timeMs: Number.isFinite(f.timeMs) ? f.timeMs : UNBUCKETABLE_MS,
443
+ side: f.side === "sell" ? FOOTPRINT_SELL : FOOTPRINT_BUY
444
+ })),
445
+ radiusPx: cfg.style?.radius ?? 0,
446
+ gapPx: cfg.style?.gap ?? 0,
447
+ marginPx: cfg.style?.margin ?? 0,
448
+ hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_FOOTPRINT_HOVER_BOOST
449
+ };
450
+ }
451
+ var EMPTY_FOOTPRINTS = footprintsToSpec({ prints: [] });
338
452
  var installed = false;
339
453
  function ensureInstalled() {
340
454
  if (installed) return;
@@ -345,23 +459,37 @@ function ensureInstalled() {
345
459
  }
346
460
  installed = true;
347
461
  }
348
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
462
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition) {
349
463
  const handleRef = useRef(null);
350
464
  const defaultWidthAppliedRef = useRef(false);
351
465
  const volumeCollapseRef = useRef(null);
352
466
  const prevDataRef = useRef(null);
353
467
  const intervalMorphRaf = useRef(null);
468
+ const streamRaf = useRef(null);
469
+ const streamWindowRef = useRef(null);
470
+ const streamMorphRef = useRef(false);
354
471
  const [picture, setPicture] = useState(null);
355
472
  if (!handleRef.current && size.width > 0 && size.height > 0) {
356
473
  ensureInstalled();
357
474
  handleRef.current = globalThis.VroomChartJSI.create();
358
475
  }
359
- 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
+ });
360
484
  animRef.current = {
361
485
  ms: Math.max(0, transition?.transitionMs ?? 300),
362
486
  easing: transition?.transitionEasing,
363
487
  reduceMotion: transition?.reduceMotion ?? false,
364
- 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)
365
493
  };
366
494
  const onFrameRef = useRef(transition?.onFrame);
367
495
  onFrameRef.current = transition?.onFrame;
@@ -385,12 +513,70 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
385
513
  };
386
514
  intervalMorphRaf.current = requestAnimationFrame(step);
387
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
+ );
388
570
  useEffect(() => {
389
571
  return () => {
390
572
  if (intervalMorphRaf.current != null) {
391
573
  cancelAnimationFrame(intervalMorphRaf.current);
392
574
  intervalMorphRaf.current = null;
393
575
  }
576
+ if (streamRaf.current != null) {
577
+ cancelAnimationFrame(streamRaf.current);
578
+ streamRaf.current = null;
579
+ }
394
580
  };
395
581
  }, []);
396
582
  const explicit = visibleRange != null;
@@ -399,15 +585,20 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
399
585
  const themeKey = theme ? JSON.stringify(theme) : "";
400
586
  const rsiKey = rsi ? JSON.stringify(rsi) : "";
401
587
  const macdKey = macd ? JSON.stringify(macd) : "";
588
+ const atrKey = atr ? JSON.stringify(atr) : "";
402
589
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
403
590
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
404
591
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
592
+ const ichimokuKey = ichimoku ? JSON.stringify(ichimoku) : "";
593
+ const fvgKey = fairValueGaps ? JSON.stringify(fairValueGaps) : "";
405
594
  const volumeKey = volume ? JSON.stringify(volume) : "";
406
595
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
596
+ const footprintsKey = footprints ? JSON.stringify(footprints) : "";
407
597
  useEffect(() => {
408
598
  const h = handleRef.current;
409
599
  if (!h) return;
410
600
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
601
+ h.setIchimoku(ichimokuToSpec(ichimoku));
411
602
  if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
412
603
  h.setDefaultCandleWidth(defaultCandleWidth);
413
604
  defaultWidthAppliedRef.current = true;
@@ -420,6 +611,36 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
420
611
  const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
421
612
  let tfArgs = null;
422
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
+ }
423
644
  if (transitionKind === "timeframe" && prev != null) {
424
645
  const oldWindow = h.getVisibleRange();
425
646
  const oldStepMs = inferStepMs(prev.candles);
@@ -455,6 +676,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
455
676
  if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
456
677
  else h.resetPriceScale();
457
678
  if (morphing) startIntervalMorph(h);
679
+ } else if (stream) {
680
+ startStreamAnim(h, stream.morph, stream.window);
458
681
  } else if (transitionKind === "reset") {
459
682
  h.resetView();
460
683
  }
@@ -472,21 +695,27 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
472
695
  }
473
696
  h.setRSI(rsiToSpec(rsi));
474
697
  h.setMACD(macdToSpec(macd));
698
+ h.setATR(atrToSpec(atr));
475
699
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
476
700
  h.setVWAP(vwapToSpec(vwap));
477
701
  h.setBollinger(bollingerToSpec(bollingerBands));
702
+ h.setFairValueGaps(fvgToSpec(fairValueGaps));
478
703
  h.setVolume(volumeToSpec(volume));
479
704
  const collapse = volumeCollapseRef.current;
480
705
  if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
481
706
  h.setPriceLines(
482
707
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
483
708
  );
709
+ h.setFootprints(
710
+ footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
711
+ );
484
712
  if (!morphing) setPicture(h.render());
485
- }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, 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]);
486
714
  return { handle: handleRef.current, picture, volumeCollapseRef };
487
715
  }
488
716
 
489
717
  // src/VroomChart.tsx
718
+ var FOOTPRINT_SELL2 = 1;
490
719
  function isSkImage(frame) {
491
720
  return typeof frame.getImageInfo === "function";
492
721
  }
@@ -503,12 +732,17 @@ function VroomChart(props) {
503
732
  transitionMs,
504
733
  transitionEasing,
505
734
  intervalTransition,
735
+ streamTransition,
736
+ streamTransitionMs,
506
737
  theme,
507
738
  rsi,
508
739
  macd,
740
+ atr,
509
741
  movingAverages,
510
742
  vwap,
511
743
  bollingerBands,
744
+ ichimoku,
745
+ fairValueGaps,
512
746
  volume,
513
747
  crosshairOffset = 40,
514
748
  onCrosshair,
@@ -517,7 +751,10 @@ function VroomChart(props) {
517
751
  priceLinesStyle,
518
752
  onPriceLineDrag,
519
753
  onPriceLineDragEnd,
520
- onPriceLineClose
754
+ onPriceLineClose,
755
+ footprints,
756
+ footprintsStyle,
757
+ onFootprint
521
758
  } = props;
522
759
  const [measured, setMeasured] = useState2({ width: 0, height: 0 });
523
760
  const width = widthProp ?? measured.width;
@@ -537,6 +774,10 @@ function VroomChart(props) {
537
774
  } : void 0,
538
775
  [priceLines, priceLinesStyle, onPriceLineClose]
539
776
  );
777
+ const footprintsProp = useMemo(
778
+ () => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
779
+ [footprints, footprintsStyle]
780
+ );
540
781
  const emptyPicture = useMemo(() => {
541
782
  const rec = Skia.PictureRecorder();
542
783
  rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
@@ -580,14 +821,28 @@ function VroomChart(props) {
580
821
  theme,
581
822
  rsi,
582
823
  macd,
824
+ atr,
583
825
  movingAverages,
584
826
  vwap,
585
827
  bollingerBands,
828
+ ichimoku,
829
+ fairValueGaps,
586
830
  volume,
587
831
  priceLinesProp,
588
- { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
832
+ footprintsProp,
833
+ {
834
+ seriesKey,
835
+ transitionMs,
836
+ transitionEasing,
837
+ intervalTransition,
838
+ streamTransition,
839
+ streamTransitionMs,
840
+ reduceMotion,
841
+ onFrame
842
+ }
589
843
  );
590
844
  const crosshairActive = useRef2(false);
845
+ const footprintActive = useRef2(false);
591
846
  const lastCrosshairTime = useRef2(null);
592
847
  const decayRaf = useRef2(null);
593
848
  const cancelDecay = useCallback2(() => {
@@ -847,6 +1102,24 @@ function VroomChart(props) {
847
1102
  null
848
1103
  );
849
1104
  const panMode = useRef2("chart");
1105
+ const dismissFootprint = (redraw = true) => {
1106
+ if (!handle || !footprintActive.current) return;
1107
+ footprintActive.current = false;
1108
+ handle.setFootprintHover(0, -1);
1109
+ if (redraw) {
1110
+ const frame = handle.render();
1111
+ if (frame) applyFrame(frame);
1112
+ }
1113
+ onFootprint?.({
1114
+ active: false,
1115
+ reason: "hide",
1116
+ side: null,
1117
+ timeMs: null,
1118
+ footprints: [],
1119
+ badge: null,
1120
+ pane: null
1121
+ });
1122
+ };
850
1123
  const pan = Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
851
1124
  cancelDecay();
852
1125
  panMode.current = hitAxis(e.x, e.y);
@@ -861,6 +1134,7 @@ function VroomChart(props) {
861
1134
  if (p) applyFrame(p);
862
1135
  }
863
1136
  }
1137
+ if (panMode.current !== "price-line") dismissFootprint();
864
1138
  }).onChange((e) => {
865
1139
  if (!handle) return;
866
1140
  let next = null;
@@ -943,6 +1217,7 @@ function VroomChart(props) {
943
1217
  });
944
1218
  const pinch = Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
945
1219
  if (e.numberOfTouches < 2) return;
1220
+ dismissFootprint();
946
1221
  const [a, b] = e.allTouches;
947
1222
  const spanX = Math.abs(a.x - b.x);
948
1223
  const spanY = Math.abs(a.y - b.y);
@@ -983,6 +1258,7 @@ function VroomChart(props) {
983
1258
  if (hitAxis(e.x, e.y) !== "chart") return;
984
1259
  if (hitPriceLine(e.x, e.y)) return;
985
1260
  cancelDecay();
1261
+ dismissFootprint(false);
986
1262
  crosshairActive.current = true;
987
1263
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
988
1264
  if (ch) applyFrame(ch);
@@ -999,6 +1275,28 @@ function VroomChart(props) {
999
1275
  });
1000
1276
  const tap = Gesture.Tap().runOnJS(true).onStart((e) => {
1001
1277
  if (!handle) return;
1278
+ const prints = footprints ?? [];
1279
+ const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
1280
+ if (fp) {
1281
+ handle.setFootprintHover(fp.candleTimeMs, fp.side);
1282
+ const frame = handle.render();
1283
+ if (frame) applyFrame(frame);
1284
+ const wasActive = footprintActive.current;
1285
+ footprintActive.current = true;
1286
+ onFootprint?.({
1287
+ active: true,
1288
+ reason: wasActive ? "move" : "show",
1289
+ side: fp.side === FOOTPRINT_SELL2 ? "sell" : "buy",
1290
+ timeMs: fp.candleTimeMs,
1291
+ // The core reports indices into the array we last pushed, which is this
1292
+ // same prop — so this rejoins each badge to the consumer's own objects.
1293
+ footprints: fp.indices.map((i) => prints[i]).filter((f) => f != null),
1294
+ badge: { x: fp.x, y: fp.y, radius: fp.radius },
1295
+ pane: fp.pane
1296
+ });
1297
+ return;
1298
+ }
1299
+ dismissFootprint();
1002
1300
  const pl = hitPriceLine(e.x, e.y);
1003
1301
  if (pl && pl.part === 1) {
1004
1302
  onPriceLineClose?.(pl.line.id);