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.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);
@@ -404,6 +427,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
404
427
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
405
428
  const volumeKey = volume ? JSON.stringify(volume) : "";
406
429
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
430
+ const footprintsKey = footprints ? JSON.stringify(footprints) : "";
407
431
  useEffect(() => {
408
432
  const h = handleRef.current;
409
433
  if (!h) return;
@@ -481,12 +505,16 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
481
505
  h.setPriceLines(
482
506
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
483
507
  );
508
+ h.setFootprints(
509
+ footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
510
+ );
484
511
  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]);
512
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph]);
486
513
  return { handle: handleRef.current, picture, volumeCollapseRef };
487
514
  }
488
515
 
489
516
  // src/VroomChart.tsx
517
+ var FOOTPRINT_SELL2 = 1;
490
518
  function isSkImage(frame) {
491
519
  return typeof frame.getImageInfo === "function";
492
520
  }
@@ -517,7 +545,10 @@ function VroomChart(props) {
517
545
  priceLinesStyle,
518
546
  onPriceLineDrag,
519
547
  onPriceLineDragEnd,
520
- onPriceLineClose
548
+ onPriceLineClose,
549
+ footprints,
550
+ footprintsStyle,
551
+ onFootprint
521
552
  } = props;
522
553
  const [measured, setMeasured] = useState2({ width: 0, height: 0 });
523
554
  const width = widthProp ?? measured.width;
@@ -537,6 +568,10 @@ function VroomChart(props) {
537
568
  } : void 0,
538
569
  [priceLines, priceLinesStyle, onPriceLineClose]
539
570
  );
571
+ const footprintsProp = useMemo(
572
+ () => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
573
+ [footprints, footprintsStyle]
574
+ );
540
575
  const emptyPicture = useMemo(() => {
541
576
  const rec = Skia.PictureRecorder();
542
577
  rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
@@ -585,9 +620,11 @@ function VroomChart(props) {
585
620
  bollingerBands,
586
621
  volume,
587
622
  priceLinesProp,
623
+ footprintsProp,
588
624
  { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
589
625
  );
590
626
  const crosshairActive = useRef2(false);
627
+ const footprintActive = useRef2(false);
591
628
  const lastCrosshairTime = useRef2(null);
592
629
  const decayRaf = useRef2(null);
593
630
  const cancelDecay = useCallback2(() => {
@@ -847,6 +884,24 @@ function VroomChart(props) {
847
884
  null
848
885
  );
849
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
+ };
850
905
  const pan = Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
851
906
  cancelDecay();
852
907
  panMode.current = hitAxis(e.x, e.y);
@@ -861,6 +916,7 @@ function VroomChart(props) {
861
916
  if (p) applyFrame(p);
862
917
  }
863
918
  }
919
+ if (panMode.current !== "price-line") dismissFootprint();
864
920
  }).onChange((e) => {
865
921
  if (!handle) return;
866
922
  let next = null;
@@ -943,6 +999,7 @@ function VroomChart(props) {
943
999
  });
944
1000
  const pinch = Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
945
1001
  if (e.numberOfTouches < 2) return;
1002
+ dismissFootprint();
946
1003
  const [a, b] = e.allTouches;
947
1004
  const spanX = Math.abs(a.x - b.x);
948
1005
  const spanY = Math.abs(a.y - b.y);
@@ -983,6 +1040,7 @@ function VroomChart(props) {
983
1040
  if (hitAxis(e.x, e.y) !== "chart") return;
984
1041
  if (hitPriceLine(e.x, e.y)) return;
985
1042
  cancelDecay();
1043
+ dismissFootprint(false);
986
1044
  crosshairActive.current = true;
987
1045
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
988
1046
  if (ch) applyFrame(ch);
@@ -999,6 +1057,28 @@ function VroomChart(props) {
999
1057
  });
1000
1058
  const tap = Gesture.Tap().runOnJS(true).onStart((e) => {
1001
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();
1002
1082
  const pl = hitPriceLine(e.x, e.y);
1003
1083
  if (pl && pl.part === 1) {
1004
1084
  onPriceLineClose?.(pl.line.id);