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.js CHANGED
@@ -363,6 +363,29 @@ function priceLinesToSpec(cfg) {
363
363
  };
364
364
  }
365
365
  var EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
366
+ var DEFAULT_FOOTPRINT_HOVER_BOOST = 1.25;
367
+ var FOOTPRINT_BUY = 0;
368
+ var FOOTPRINT_SELL = 1;
369
+ var UNBUCKETABLE_MS = -864e13;
370
+ function footprintsToSpec(cfg) {
371
+ return {
372
+ // Index alignment is load-bearing: the core reports hits as indices into this
373
+ // array and the gesture layer maps them straight back to the consumer's
374
+ // `footprints`. So a non-finite time — which can't be bucketed and would
375
+ // reach the native side as a garbage int64 — is neutralized *in place* rather
376
+ // than filtered out, which would shift every index after it onto the wrong
377
+ // trade.
378
+ prints: cfg.prints.map((f) => ({
379
+ timeMs: Number.isFinite(f.timeMs) ? f.timeMs : UNBUCKETABLE_MS,
380
+ side: f.side === "sell" ? FOOTPRINT_SELL : FOOTPRINT_BUY
381
+ })),
382
+ radiusPx: cfg.style?.radius ?? 0,
383
+ gapPx: cfg.style?.gap ?? 0,
384
+ marginPx: cfg.style?.margin ?? 0,
385
+ hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_FOOTPRINT_HOVER_BOOST
386
+ };
387
+ }
388
+ var EMPTY_FOOTPRINTS = footprintsToSpec({ prints: [] });
366
389
  var installed = false;
367
390
  function ensureInstalled() {
368
391
  if (installed) return;
@@ -373,7 +396,7 @@ function ensureInstalled() {
373
396
  }
374
397
  installed = true;
375
398
  }
376
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
399
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, footprints, transition) {
377
400
  const handleRef = (0, import_react.useRef)(null);
378
401
  const defaultWidthAppliedRef = (0, import_react.useRef)(false);
379
402
  const volumeCollapseRef = (0, import_react.useRef)(null);
@@ -384,11 +407,12 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
384
407
  ensureInstalled();
385
408
  handleRef.current = globalThis.VroomChartJSI.create();
386
409
  }
387
- const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false });
410
+ const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false, interval: "transform" });
388
411
  animRef.current = {
389
412
  ms: Math.max(0, transition?.transitionMs ?? 300),
390
413
  easing: transition?.transitionEasing,
391
- reduceMotion: transition?.reduceMotion ?? false
414
+ reduceMotion: transition?.reduceMotion ?? false,
415
+ interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
392
416
  };
393
417
  const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
394
418
  onFrameRef.current = transition?.onFrame;
@@ -431,6 +455,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
431
455
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
432
456
  const volumeKey = volume ? JSON.stringify(volume) : "";
433
457
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
458
+ const footprintsKey = footprints ? JSON.stringify(footprints) : "";
434
459
  (0, import_react.useEffect)(() => {
435
460
  const h = handleRef.current;
436
461
  if (!h) return;
@@ -461,7 +486,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
461
486
  morphing = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
462
487
  if (morphing) {
463
488
  endIntervalMorph();
464
- h.beginIntervalMorph();
489
+ h.beginIntervalMorph(animRef.current.interval);
465
490
  }
466
491
  } else if (transitionKind === "initial" || transitionKind === "reset") {
467
492
  endIntervalMorph();
@@ -508,12 +533,16 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
508
533
  h.setPriceLines(
509
534
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
510
535
  );
536
+ h.setFootprints(
537
+ footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
538
+ );
511
539
  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, startIntervalMorph, endIntervalMorph]);
540
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph]);
513
541
  return { handle: handleRef.current, picture, volumeCollapseRef };
514
542
  }
515
543
 
516
544
  // src/VroomChart.tsx
545
+ var FOOTPRINT_SELL2 = 1;
517
546
  function isSkImage(frame) {
518
547
  return typeof frame.getImageInfo === "function";
519
548
  }
@@ -529,6 +558,7 @@ function VroomChart(props) {
529
558
  chartType,
530
559
  transitionMs,
531
560
  transitionEasing,
561
+ intervalTransition,
532
562
  theme,
533
563
  rsi,
534
564
  macd,
@@ -543,7 +573,10 @@ function VroomChart(props) {
543
573
  priceLinesStyle,
544
574
  onPriceLineDrag,
545
575
  onPriceLineDragEnd,
546
- onPriceLineClose
576
+ onPriceLineClose,
577
+ footprints,
578
+ footprintsStyle,
579
+ onFootprint
547
580
  } = props;
548
581
  const [measured, setMeasured] = (0, import_react2.useState)({ width: 0, height: 0 });
549
582
  const width = widthProp ?? measured.width;
@@ -563,6 +596,10 @@ function VroomChart(props) {
563
596
  } : void 0,
564
597
  [priceLines, priceLinesStyle, onPriceLineClose]
565
598
  );
599
+ const footprintsProp = (0, import_react2.useMemo)(
600
+ () => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
601
+ [footprints, footprintsStyle]
602
+ );
566
603
  const emptyPicture = (0, import_react2.useMemo)(() => {
567
604
  const rec = import_react_native_skia.Skia.PictureRecorder();
568
605
  rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
@@ -611,9 +648,11 @@ function VroomChart(props) {
611
648
  bollingerBands,
612
649
  volume,
613
650
  priceLinesProp,
614
- { seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame }
651
+ footprintsProp,
652
+ { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
615
653
  );
616
654
  const crosshairActive = (0, import_react2.useRef)(false);
655
+ const footprintActive = (0, import_react2.useRef)(false);
617
656
  const lastCrosshairTime = (0, import_react2.useRef)(null);
618
657
  const decayRaf = (0, import_react2.useRef)(null);
619
658
  const cancelDecay = (0, import_react2.useCallback)(() => {
@@ -873,6 +912,24 @@ function VroomChart(props) {
873
912
  null
874
913
  );
875
914
  const panMode = (0, import_react2.useRef)("chart");
915
+ const dismissFootprint = (redraw = true) => {
916
+ if (!handle || !footprintActive.current) return;
917
+ footprintActive.current = false;
918
+ handle.setFootprintHover(0, -1);
919
+ if (redraw) {
920
+ const frame = handle.render();
921
+ if (frame) applyFrame(frame);
922
+ }
923
+ onFootprint?.({
924
+ active: false,
925
+ reason: "hide",
926
+ side: null,
927
+ timeMs: null,
928
+ footprints: [],
929
+ badge: null,
930
+ pane: null
931
+ });
932
+ };
876
933
  const pan = import_react_native_gesture_handler.Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
877
934
  cancelDecay();
878
935
  panMode.current = hitAxis(e.x, e.y);
@@ -887,6 +944,7 @@ function VroomChart(props) {
887
944
  if (p) applyFrame(p);
888
945
  }
889
946
  }
947
+ if (panMode.current !== "price-line") dismissFootprint();
890
948
  }).onChange((e) => {
891
949
  if (!handle) return;
892
950
  let next = null;
@@ -969,6 +1027,7 @@ function VroomChart(props) {
969
1027
  });
970
1028
  const pinch = import_react_native_gesture_handler.Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
971
1029
  if (e.numberOfTouches < 2) return;
1030
+ dismissFootprint();
972
1031
  const [a, b] = e.allTouches;
973
1032
  const spanX = Math.abs(a.x - b.x);
974
1033
  const spanY = Math.abs(a.y - b.y);
@@ -1009,6 +1068,7 @@ function VroomChart(props) {
1009
1068
  if (hitAxis(e.x, e.y) !== "chart") return;
1010
1069
  if (hitPriceLine(e.x, e.y)) return;
1011
1070
  cancelDecay();
1071
+ dismissFootprint(false);
1012
1072
  crosshairActive.current = true;
1013
1073
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
1014
1074
  if (ch) applyFrame(ch);
@@ -1025,6 +1085,28 @@ function VroomChart(props) {
1025
1085
  });
1026
1086
  const tap = import_react_native_gesture_handler.Gesture.Tap().runOnJS(true).onStart((e) => {
1027
1087
  if (!handle) return;
1088
+ const prints = footprints ?? [];
1089
+ const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
1090
+ if (fp) {
1091
+ handle.setFootprintHover(fp.candleTimeMs, fp.side);
1092
+ const frame = handle.render();
1093
+ if (frame) applyFrame(frame);
1094
+ const wasActive = footprintActive.current;
1095
+ footprintActive.current = true;
1096
+ onFootprint?.({
1097
+ active: true,
1098
+ reason: wasActive ? "move" : "show",
1099
+ side: fp.side === FOOTPRINT_SELL2 ? "sell" : "buy",
1100
+ timeMs: fp.candleTimeMs,
1101
+ // The core reports indices into the array we last pushed, which is this
1102
+ // same prop — so this rejoins each badge to the consumer's own objects.
1103
+ footprints: fp.indices.map((i) => prints[i]).filter((f) => f != null),
1104
+ badge: { x: fp.x, y: fp.y, radius: fp.radius },
1105
+ pane: fp.pane
1106
+ });
1107
+ return;
1108
+ }
1109
+ dismissFootprint();
1028
1110
  const pl = hitPriceLine(e.x, e.y);
1029
1111
  if (pl && pl.part === 1) {
1030
1112
  onPriceLineClose?.(pl.line.id);