react-native-vroom-chart 0.13.1 → 0.15.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.
package/lib/index.mjs CHANGED
@@ -335,6 +335,29 @@ function priceLinesToSpec(cfg) {
335
335
  };
336
336
  }
337
337
  var EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
338
+ var DEFAULT_FOOTPRINT_HOVER_BOOST = 1.25;
339
+ var FOOTPRINT_BUY = 0;
340
+ var FOOTPRINT_SELL = 1;
341
+ var UNBUCKETABLE_MS = -864e13;
342
+ function footprintsToSpec(cfg) {
343
+ return {
344
+ // Index alignment is load-bearing: the core reports hits as indices into this
345
+ // array and the gesture layer maps them straight back to the consumer's
346
+ // `footprints`. So a non-finite time — which can't be bucketed and would
347
+ // reach the native side as a garbage int64 — is neutralized *in place* rather
348
+ // than filtered out, which would shift every index after it onto the wrong
349
+ // trade.
350
+ prints: cfg.prints.map((f) => ({
351
+ timeMs: Number.isFinite(f.timeMs) ? f.timeMs : UNBUCKETABLE_MS,
352
+ side: f.side === "sell" ? FOOTPRINT_SELL : FOOTPRINT_BUY
353
+ })),
354
+ radiusPx: cfg.style?.radius ?? 0,
355
+ gapPx: cfg.style?.gap ?? 0,
356
+ marginPx: cfg.style?.margin ?? 0,
357
+ hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_FOOTPRINT_HOVER_BOOST
358
+ };
359
+ }
360
+ var EMPTY_FOOTPRINTS = footprintsToSpec({ prints: [] });
338
361
  var installed = false;
339
362
  function ensureInstalled() {
340
363
  if (installed) return;
@@ -345,7 +368,7 @@ function ensureInstalled() {
345
368
  }
346
369
  installed = true;
347
370
  }
348
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
371
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, footprints, transition) {
349
372
  const handleRef = useRef(null);
350
373
  const defaultWidthAppliedRef = useRef(false);
351
374
  const volumeCollapseRef = useRef(null);
@@ -356,11 +379,12 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
356
379
  ensureInstalled();
357
380
  handleRef.current = globalThis.VroomChartJSI.create();
358
381
  }
359
- const animRef = useRef({ ms: 300, easing: void 0, reduceMotion: false });
382
+ const animRef = useRef({ ms: 300, easing: void 0, reduceMotion: false, interval: "transform" });
360
383
  animRef.current = {
361
384
  ms: Math.max(0, transition?.transitionMs ?? 300),
362
385
  easing: transition?.transitionEasing,
363
- reduceMotion: transition?.reduceMotion ?? false
386
+ reduceMotion: transition?.reduceMotion ?? false,
387
+ interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
364
388
  };
365
389
  const onFrameRef = useRef(transition?.onFrame);
366
390
  onFrameRef.current = transition?.onFrame;
@@ -403,6 +427,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
403
427
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
404
428
  const volumeKey = volume ? JSON.stringify(volume) : "";
405
429
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
430
+ const footprintsKey = footprints ? JSON.stringify(footprints) : "";
406
431
  useEffect(() => {
407
432
  const h = handleRef.current;
408
433
  if (!h) return;
@@ -433,7 +458,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
433
458
  morphing = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
434
459
  if (morphing) {
435
460
  endIntervalMorph();
436
- h.beginIntervalMorph();
461
+ h.beginIntervalMorph(animRef.current.interval);
437
462
  }
438
463
  } else if (transitionKind === "initial" || transitionKind === "reset") {
439
464
  endIntervalMorph();
@@ -480,12 +505,16 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
480
505
  h.setPriceLines(
481
506
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
482
507
  );
508
+ h.setFootprints(
509
+ footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
510
+ );
483
511
  if (!morphing) setPicture(h.render());
484
- }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
512
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph]);
485
513
  return { handle: handleRef.current, picture, volumeCollapseRef };
486
514
  }
487
515
 
488
516
  // src/VroomChart.tsx
517
+ var FOOTPRINT_SELL2 = 1;
489
518
  function isSkImage(frame) {
490
519
  return typeof frame.getImageInfo === "function";
491
520
  }
@@ -501,6 +530,7 @@ function VroomChart(props) {
501
530
  chartType,
502
531
  transitionMs,
503
532
  transitionEasing,
533
+ intervalTransition,
504
534
  theme,
505
535
  rsi,
506
536
  macd,
@@ -515,7 +545,10 @@ function VroomChart(props) {
515
545
  priceLinesStyle,
516
546
  onPriceLineDrag,
517
547
  onPriceLineDragEnd,
518
- onPriceLineClose
548
+ onPriceLineClose,
549
+ footprints,
550
+ footprintsStyle,
551
+ onFootprint
519
552
  } = props;
520
553
  const [measured, setMeasured] = useState2({ width: 0, height: 0 });
521
554
  const width = widthProp ?? measured.width;
@@ -535,6 +568,10 @@ function VroomChart(props) {
535
568
  } : void 0,
536
569
  [priceLines, priceLinesStyle, onPriceLineClose]
537
570
  );
571
+ const footprintsProp = useMemo(
572
+ () => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
573
+ [footprints, footprintsStyle]
574
+ );
538
575
  const emptyPicture = useMemo(() => {
539
576
  const rec = Skia.PictureRecorder();
540
577
  rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
@@ -583,9 +620,11 @@ function VroomChart(props) {
583
620
  bollingerBands,
584
621
  volume,
585
622
  priceLinesProp,
586
- { seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame }
623
+ footprintsProp,
624
+ { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
587
625
  );
588
626
  const crosshairActive = useRef2(false);
627
+ const footprintActive = useRef2(false);
589
628
  const lastCrosshairTime = useRef2(null);
590
629
  const decayRaf = useRef2(null);
591
630
  const cancelDecay = useCallback2(() => {
@@ -845,6 +884,24 @@ function VroomChart(props) {
845
884
  null
846
885
  );
847
886
  const panMode = useRef2("chart");
887
+ const dismissFootprint = (redraw = true) => {
888
+ if (!handle || !footprintActive.current) return;
889
+ footprintActive.current = false;
890
+ handle.setFootprintHover(0, -1);
891
+ if (redraw) {
892
+ const frame = handle.render();
893
+ if (frame) applyFrame(frame);
894
+ }
895
+ onFootprint?.({
896
+ active: false,
897
+ reason: "hide",
898
+ side: null,
899
+ timeMs: null,
900
+ footprints: [],
901
+ badge: null,
902
+ pane: null
903
+ });
904
+ };
848
905
  const pan = Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
849
906
  cancelDecay();
850
907
  panMode.current = hitAxis(e.x, e.y);
@@ -859,6 +916,7 @@ function VroomChart(props) {
859
916
  if (p) applyFrame(p);
860
917
  }
861
918
  }
919
+ if (panMode.current !== "price-line") dismissFootprint();
862
920
  }).onChange((e) => {
863
921
  if (!handle) return;
864
922
  let next = null;
@@ -941,6 +999,7 @@ function VroomChart(props) {
941
999
  });
942
1000
  const pinch = Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
943
1001
  if (e.numberOfTouches < 2) return;
1002
+ dismissFootprint();
944
1003
  const [a, b] = e.allTouches;
945
1004
  const spanX = Math.abs(a.x - b.x);
946
1005
  const spanY = Math.abs(a.y - b.y);
@@ -981,6 +1040,7 @@ function VroomChart(props) {
981
1040
  if (hitAxis(e.x, e.y) !== "chart") return;
982
1041
  if (hitPriceLine(e.x, e.y)) return;
983
1042
  cancelDecay();
1043
+ dismissFootprint(false);
984
1044
  crosshairActive.current = true;
985
1045
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
986
1046
  if (ch) applyFrame(ch);
@@ -997,6 +1057,28 @@ function VroomChart(props) {
997
1057
  });
998
1058
  const tap = Gesture.Tap().runOnJS(true).onStart((e) => {
999
1059
  if (!handle) return;
1060
+ const prints = footprints ?? [];
1061
+ const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
1062
+ if (fp) {
1063
+ handle.setFootprintHover(fp.candleTimeMs, fp.side);
1064
+ const frame = handle.render();
1065
+ if (frame) applyFrame(frame);
1066
+ const wasActive = footprintActive.current;
1067
+ footprintActive.current = true;
1068
+ onFootprint?.({
1069
+ active: true,
1070
+ reason: wasActive ? "move" : "show",
1071
+ side: fp.side === FOOTPRINT_SELL2 ? "sell" : "buy",
1072
+ timeMs: fp.candleTimeMs,
1073
+ // The core reports indices into the array we last pushed, which is this
1074
+ // same prop — so this rejoins each badge to the consumer's own objects.
1075
+ footprints: fp.indices.map((i) => prints[i]).filter((f) => f != null),
1076
+ badge: { x: fp.x, y: fp.y, radius: fp.radius },
1077
+ pane: fp.pane
1078
+ });
1079
+ return;
1080
+ }
1081
+ dismissFootprint();
1000
1082
  const pl = hitPriceLine(e.x, e.y);
1001
1083
  if (pl && pl.part === 1) {
1002
1084
  onPriceLineClose?.(pl.line.id);