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/cpp/VroomChartHostObject.cpp +133 -8
- package/cpp/_core_include/vroom/vroom_chart.h +90 -7
- package/cpp/_core_src/candles.h +7 -5
- package/cpp/_core_src/chart.cpp +287 -152
- package/cpp/_core_src/chart.h +29 -3
- package/cpp/_core_src/chart_facade.cpp +113 -1
- package/cpp/_core_src/footprints.cpp +226 -0
- package/cpp/_core_src/footprints.h +45 -0
- package/cpp/_core_src/footprints_layout.cpp +140 -0
- package/cpp/_core_src/footprints_layout.h +94 -0
- package/cpp/_core_src/tip_pulse.h +4 -4
- package/cpp/_core_src/viewport.h +4 -0
- package/lib/index.d.mts +186 -6
- package/lib/index.d.ts +186 -6
- package/lib/index.js +89 -7
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +89 -7
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +89 -5
- package/src/index.ts +7 -0
- package/src/jsi.d.ts +45 -8
- package/src/types.ts +7 -0
- package/src/useChartCore.ts +56 -5
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
|
-
|
|
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);
|