react-native-vroom-chart 0.14.0 → 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);
@@ -432,6 +455,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
432
455
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
433
456
  const volumeKey = volume ? JSON.stringify(volume) : "";
434
457
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
458
+ const footprintsKey = footprints ? JSON.stringify(footprints) : "";
435
459
  (0, import_react.useEffect)(() => {
436
460
  const h = handleRef.current;
437
461
  if (!h) return;
@@ -509,12 +533,16 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
509
533
  h.setPriceLines(
510
534
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
511
535
  );
536
+ h.setFootprints(
537
+ footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
538
+ );
512
539
  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]);
540
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph]);
514
541
  return { handle: handleRef.current, picture, volumeCollapseRef };
515
542
  }
516
543
 
517
544
  // src/VroomChart.tsx
545
+ var FOOTPRINT_SELL2 = 1;
518
546
  function isSkImage(frame) {
519
547
  return typeof frame.getImageInfo === "function";
520
548
  }
@@ -545,7 +573,10 @@ function VroomChart(props) {
545
573
  priceLinesStyle,
546
574
  onPriceLineDrag,
547
575
  onPriceLineDragEnd,
548
- onPriceLineClose
576
+ onPriceLineClose,
577
+ footprints,
578
+ footprintsStyle,
579
+ onFootprint
549
580
  } = props;
550
581
  const [measured, setMeasured] = (0, import_react2.useState)({ width: 0, height: 0 });
551
582
  const width = widthProp ?? measured.width;
@@ -565,6 +596,10 @@ function VroomChart(props) {
565
596
  } : void 0,
566
597
  [priceLines, priceLinesStyle, onPriceLineClose]
567
598
  );
599
+ const footprintsProp = (0, import_react2.useMemo)(
600
+ () => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
601
+ [footprints, footprintsStyle]
602
+ );
568
603
  const emptyPicture = (0, import_react2.useMemo)(() => {
569
604
  const rec = import_react_native_skia.Skia.PictureRecorder();
570
605
  rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
@@ -613,9 +648,11 @@ function VroomChart(props) {
613
648
  bollingerBands,
614
649
  volume,
615
650
  priceLinesProp,
651
+ footprintsProp,
616
652
  { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
617
653
  );
618
654
  const crosshairActive = (0, import_react2.useRef)(false);
655
+ const footprintActive = (0, import_react2.useRef)(false);
619
656
  const lastCrosshairTime = (0, import_react2.useRef)(null);
620
657
  const decayRaf = (0, import_react2.useRef)(null);
621
658
  const cancelDecay = (0, import_react2.useCallback)(() => {
@@ -875,6 +912,24 @@ function VroomChart(props) {
875
912
  null
876
913
  );
877
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
+ };
878
933
  const pan = import_react_native_gesture_handler.Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
879
934
  cancelDecay();
880
935
  panMode.current = hitAxis(e.x, e.y);
@@ -889,6 +944,7 @@ function VroomChart(props) {
889
944
  if (p) applyFrame(p);
890
945
  }
891
946
  }
947
+ if (panMode.current !== "price-line") dismissFootprint();
892
948
  }).onChange((e) => {
893
949
  if (!handle) return;
894
950
  let next = null;
@@ -971,6 +1027,7 @@ function VroomChart(props) {
971
1027
  });
972
1028
  const pinch = import_react_native_gesture_handler.Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
973
1029
  if (e.numberOfTouches < 2) return;
1030
+ dismissFootprint();
974
1031
  const [a, b] = e.allTouches;
975
1032
  const spanX = Math.abs(a.x - b.x);
976
1033
  const spanY = Math.abs(a.y - b.y);
@@ -1011,6 +1068,7 @@ function VroomChart(props) {
1011
1068
  if (hitAxis(e.x, e.y) !== "chart") return;
1012
1069
  if (hitPriceLine(e.x, e.y)) return;
1013
1070
  cancelDecay();
1071
+ dismissFootprint(false);
1014
1072
  crosshairActive.current = true;
1015
1073
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
1016
1074
  if (ch) applyFrame(ch);
@@ -1027,6 +1085,28 @@ function VroomChart(props) {
1027
1085
  });
1028
1086
  const tap = import_react_native_gesture_handler.Gesture.Tap().runOnJS(true).onStart((e) => {
1029
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();
1030
1110
  const pl = hitPriceLine(e.x, e.y);
1031
1111
  if (pl && pl.part === 1) {
1032
1112
  onPriceLineClose?.(pl.line.id);