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/src/VroomChart.tsx
CHANGED
|
@@ -36,9 +36,12 @@ import { useReducedMotion, useSharedValue } from 'react-native-reanimated';
|
|
|
36
36
|
import { useChartCore } from './useChartCore';
|
|
37
37
|
import { ease, easingIndex } from './easing';
|
|
38
38
|
import type { ChartFrame } from './jsi.d';
|
|
39
|
-
import type { VroomChartProps } from './types';
|
|
39
|
+
import type { Footprint, VroomChartProps } from './types';
|
|
40
40
|
import './jsi.d';
|
|
41
41
|
|
|
42
|
+
// Mirrors VroomFootprintSide in packages/core/include/vroom/vroom_chart.h.
|
|
43
|
+
const FOOTPRINT_SELL = 1;
|
|
44
|
+
|
|
42
45
|
function isSkImage(frame: ChartFrame): frame is SkImage {
|
|
43
46
|
return typeof (frame as SkImage).getImageInfo === 'function';
|
|
44
47
|
}
|
|
@@ -64,6 +67,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
64
67
|
chartType,
|
|
65
68
|
transitionMs,
|
|
66
69
|
transitionEasing,
|
|
70
|
+
intervalTransition,
|
|
67
71
|
theme,
|
|
68
72
|
rsi,
|
|
69
73
|
macd,
|
|
@@ -79,6 +83,9 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
79
83
|
onPriceLineDrag,
|
|
80
84
|
onPriceLineDragEnd,
|
|
81
85
|
onPriceLineClose,
|
|
86
|
+
footprints,
|
|
87
|
+
footprintsStyle,
|
|
88
|
+
onFootprint,
|
|
82
89
|
} = props;
|
|
83
90
|
|
|
84
91
|
// Fill the parent by default: measure via onLayout. Explicit width/height
|
|
@@ -110,6 +117,11 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
110
117
|
[priceLines, priceLinesStyle, onPriceLineClose],
|
|
111
118
|
);
|
|
112
119
|
|
|
120
|
+
const footprintsProp = useMemo(
|
|
121
|
+
() => (footprints ? { prints: footprints, style: footprintsStyle } : undefined),
|
|
122
|
+
[footprints, footprintsStyle],
|
|
123
|
+
);
|
|
124
|
+
|
|
113
125
|
// RN-Skia's recorder reads these SharedValues on the UI/render runtime, a
|
|
114
126
|
// beat behind JS-thread writes. If it ever reads null it throws ("Invalid
|
|
115
127
|
// prop value for SkTextBlob received" — RN-Skia's mislabeled SkPicture
|
|
@@ -172,7 +184,8 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
172
184
|
bollingerBands,
|
|
173
185
|
volume,
|
|
174
186
|
priceLinesProp,
|
|
175
|
-
|
|
187
|
+
footprintsProp,
|
|
188
|
+
{ seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame },
|
|
176
189
|
);
|
|
177
190
|
|
|
178
191
|
// When the crosshair is showing, pan moves it (instead of scrolling) and
|
|
@@ -180,6 +193,11 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
180
193
|
// synchronously without re-subscribing. Tap dismisses it.
|
|
181
194
|
const crosshairActive = useRef(false);
|
|
182
195
|
|
|
196
|
+
// Whether a footprint badge is currently open, so a tap that misses every badge
|
|
197
|
+
// knows whether it has a tooltip to dismiss. A ref for the same reason as
|
|
198
|
+
// crosshairActive: gesture callbacks read it synchronously.
|
|
199
|
+
const footprintActive = useRef(false);
|
|
200
|
+
|
|
183
201
|
// timeMs of the candle last reported through onCrosshair, so a drag fires a
|
|
184
202
|
// 'move' event only when it crosses into a *different* candle (one per
|
|
185
203
|
// candle, not per frame). Null while the crosshair is hidden.
|
|
@@ -538,6 +556,31 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
538
556
|
'chart' | 'price-axis' | 'time-axis' | 'indicator' | 'price-line'
|
|
539
557
|
>('chart');
|
|
540
558
|
|
|
559
|
+
// Closes an open footprint tooltip. Any viewport change slides the candles out
|
|
560
|
+
// from under it and the crosshair replaces it outright, so the host is told to
|
|
561
|
+
// take it down rather than left holding a position the badge has moved away
|
|
562
|
+
// from. `redraw` is false for callers that render a frame of their own right
|
|
563
|
+
// after — on Android that render rasterizes pixels, so the duplicate is worth
|
|
564
|
+
// skipping.
|
|
565
|
+
const dismissFootprint = (redraw = true) => {
|
|
566
|
+
if (!handle || !footprintActive.current) return;
|
|
567
|
+
footprintActive.current = false;
|
|
568
|
+
handle.setFootprintHover(0, -1);
|
|
569
|
+
if (redraw) {
|
|
570
|
+
const frame = handle.render();
|
|
571
|
+
if (frame) applyFrame(frame);
|
|
572
|
+
}
|
|
573
|
+
onFootprint?.({
|
|
574
|
+
active: false,
|
|
575
|
+
reason: 'hide',
|
|
576
|
+
side: null,
|
|
577
|
+
timeMs: null,
|
|
578
|
+
footprints: [],
|
|
579
|
+
badge: null,
|
|
580
|
+
pane: null,
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
|
|
541
584
|
const pan = Gesture.Pan()
|
|
542
585
|
.runOnJS(true)
|
|
543
586
|
.maxPointers(1) // don't fight Pinch's two-finger gesture
|
|
@@ -560,6 +603,10 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
560
603
|
if (p) applyFrame(p);
|
|
561
604
|
}
|
|
562
605
|
}
|
|
606
|
+
// Every mode but a price-line drag moves the viewport, and this one call
|
|
607
|
+
// covers the momentum fling too — decay only ever starts from a pan that
|
|
608
|
+
// already began here.
|
|
609
|
+
if (panMode.current !== 'price-line') dismissFootprint();
|
|
563
610
|
})
|
|
564
611
|
.onChange((e) => {
|
|
565
612
|
if (!handle) return;
|
|
@@ -683,6 +730,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
683
730
|
.runOnJS(true)
|
|
684
731
|
.onTouchesDown((e) => {
|
|
685
732
|
if (e.numberOfTouches < 2) return;
|
|
733
|
+
dismissFootprint();
|
|
686
734
|
const [a, b] = e.allTouches;
|
|
687
735
|
const spanX = Math.abs(a.x - b.x);
|
|
688
736
|
const spanY = Math.abs(a.y - b.y);
|
|
@@ -738,6 +786,10 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
738
786
|
// close button — so it must not raise the crosshair over the top.
|
|
739
787
|
if (hitPriceLine(e.x, e.y)) return;
|
|
740
788
|
cancelDecay();
|
|
789
|
+
// The crosshair takes the pane over, so it can't share it with a tooltip.
|
|
790
|
+
// No redraw: setCrosshair below returns a frame that already has the badge
|
|
791
|
+
// un-highlighted.
|
|
792
|
+
dismissFootprint(false);
|
|
741
793
|
crosshairActive.current = true;
|
|
742
794
|
const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
|
|
743
795
|
if (ch) applyFrame(ch);
|
|
@@ -752,13 +804,45 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
752
804
|
});
|
|
753
805
|
});
|
|
754
806
|
|
|
755
|
-
// A tap activates a price line's close button,
|
|
756
|
-
// crosshair while it's up. Any other tap is a
|
|
757
|
-
// with normal pan/pinch.
|
|
807
|
+
// A tap activates a price line's close button, selects or dismisses a footprint
|
|
808
|
+
// badge, and otherwise dismisses the crosshair while it's up. Any other tap is a
|
|
809
|
+
// no-op, so it never interferes with normal pan/pinch.
|
|
758
810
|
const tap = Gesture.Tap()
|
|
759
811
|
.runOnJS(true)
|
|
760
812
|
.onStart((e) => {
|
|
761
813
|
if (!handle) return;
|
|
814
|
+
|
|
815
|
+
// Badges get first refusal: one is a ~9px circle, while a price line's grab
|
|
816
|
+
// band spans the pane and would otherwise swallow any badge it crosses.
|
|
817
|
+
// Touch has no hover, so a tap is what opens a footprint here, and the next
|
|
818
|
+
// tap anywhere closes it.
|
|
819
|
+
const prints = footprints ?? [];
|
|
820
|
+
const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
|
|
821
|
+
if (fp) {
|
|
822
|
+
handle.setFootprintHover(fp.candleTimeMs, fp.side);
|
|
823
|
+
const frame = handle.render();
|
|
824
|
+
if (frame) applyFrame(frame);
|
|
825
|
+
const wasActive = footprintActive.current;
|
|
826
|
+
footprintActive.current = true;
|
|
827
|
+
onFootprint?.({
|
|
828
|
+
active: true,
|
|
829
|
+
reason: wasActive ? 'move' : 'show',
|
|
830
|
+
side: fp.side === FOOTPRINT_SELL ? 'sell' : 'buy',
|
|
831
|
+
timeMs: fp.candleTimeMs,
|
|
832
|
+
// The core reports indices into the array we last pushed, which is this
|
|
833
|
+
// same prop — so this rejoins each badge to the consumer's own objects.
|
|
834
|
+
footprints: fp.indices
|
|
835
|
+
.map((i) => prints[i])
|
|
836
|
+
.filter((f): f is Footprint => f != null),
|
|
837
|
+
badge: { x: fp.x, y: fp.y, radius: fp.radius },
|
|
838
|
+
pane: fp.pane,
|
|
839
|
+
});
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
// A tap that missed every badge dismisses the open one, so the host tooltip
|
|
843
|
+
// goes away the same way the crosshair does.
|
|
844
|
+
dismissFootprint();
|
|
845
|
+
|
|
762
846
|
// The close button is a tap target whether or not the crosshair is up.
|
|
763
847
|
const pl = hitPriceLine(e.x, e.y);
|
|
764
848
|
if (pl && pl.part === 1) {
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,13 @@ export type {
|
|
|
22
22
|
VolumeConfig,
|
|
23
23
|
ChartType,
|
|
24
24
|
TransitionEasing,
|
|
25
|
+
IntervalTransition,
|
|
25
26
|
PriceLine,
|
|
26
27
|
PriceLinesStyle,
|
|
28
|
+
Footprint,
|
|
29
|
+
FootprintSide,
|
|
30
|
+
FootprintsStyle,
|
|
31
|
+
FootprintEvent,
|
|
32
|
+
PlotRect,
|
|
33
|
+
DefaultDrawingStyle,
|
|
27
34
|
} from './types';
|
package/src/jsi.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Ambient declaration for the JSI HostObject installed by VroomChartModule.
|
|
2
2
|
|
|
3
3
|
import type { SkImage, SkPicture } from '@shopify/react-native-skia';
|
|
4
|
+
import type { IntervalTransition } from '@vroomchart/types';
|
|
4
5
|
|
|
5
6
|
/** Frame the JSI handle returns: an SkPicture on iOS, an SkImage on Android. */
|
|
6
7
|
export type ChartFrame = SkPicture | SkImage;
|
|
@@ -65,15 +66,12 @@ export interface ChartHandle {
|
|
|
65
66
|
*/
|
|
66
67
|
preservePriceEnvelope(prevLow: number, prevHigh: number): void;
|
|
67
68
|
/**
|
|
68
|
-
* Capture the visible candle geometry so the next data swap can animate
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* Candles are paired by *slot* — position counting back from the right edge of
|
|
73
|
-
* the visible window, which a timeframe switch preserves. Call before
|
|
74
|
-
* setCandles, then drive setIntervalMorph from 0 to 1.
|
|
69
|
+
* Capture the visible candle geometry so the next data swap can animate.
|
|
70
|
+
* `'transform'` (default) lerps each slot into its counterpart; `'fade'`
|
|
71
|
+
* fades the capture out then the new scene in. Call before setCandles,
|
|
72
|
+
* then drive setIntervalMorph from 0 to 1.
|
|
75
73
|
*/
|
|
76
|
-
beginIntervalMorph(): void;
|
|
74
|
+
beginIntervalMorph(mode?: IntervalTransition): void;
|
|
77
75
|
/**
|
|
78
76
|
* Advance the interval morph started by beginIntervalMorph. `t` (clamped to
|
|
79
77
|
* 0..1) is the eased progress: 0 renders the captured geometry pixel-
|
|
@@ -327,6 +325,45 @@ export interface ChartHandle {
|
|
|
327
325
|
* committed price is untouched — restate setPriceLines to apply the move.
|
|
328
326
|
*/
|
|
329
327
|
setPriceLineDrag(index: number, price: number): void;
|
|
328
|
+
/**
|
|
329
|
+
* Replaces the full set of footprints (plus their shared style). `side` is
|
|
330
|
+
* 0=buy, 1=sell; `timeMs` is the raw execution time — the core buckets each
|
|
331
|
+
* trade onto whichever candle's window contains it and regroups whenever the
|
|
332
|
+
* candles change. Geometry fields at 0 take the core's defaults. Pass an empty
|
|
333
|
+
* `prints` array to clear.
|
|
334
|
+
*/
|
|
335
|
+
setFootprints(spec: {
|
|
336
|
+
prints: { timeMs: number; side: number }[];
|
|
337
|
+
radiusPx: number;
|
|
338
|
+
gapPx: number;
|
|
339
|
+
marginPx: number;
|
|
340
|
+
hoverBoost: number;
|
|
341
|
+
}): void;
|
|
342
|
+
/**
|
|
343
|
+
* Hit-tests pixel (x, y) against the footprint badges; null on a miss, nearest
|
|
344
|
+
* center wins when two overlap. `indices` addresses the array last passed to
|
|
345
|
+
* setFootprints and covers *both* sides of that candle, ascending by time, so
|
|
346
|
+
* one call is enough to fill a tooltip; `pane` is the plot rect, for deciding
|
|
347
|
+
* which side of the badge that tooltip fits on. Cheap to call at gesture rate.
|
|
348
|
+
*/
|
|
349
|
+
hitTestFootprint(
|
|
350
|
+
x: number,
|
|
351
|
+
y: number,
|
|
352
|
+
): {
|
|
353
|
+
side: number;
|
|
354
|
+
candleTimeMs: number;
|
|
355
|
+
x: number;
|
|
356
|
+
y: number;
|
|
357
|
+
radius: number;
|
|
358
|
+
pane: { left: number; top: number; right: number; bottom: number };
|
|
359
|
+
indices: number[];
|
|
360
|
+
} | null;
|
|
361
|
+
/**
|
|
362
|
+
* Marks a footprint badge as hovered so it renders highlighted; side -1 clears.
|
|
363
|
+
* The arguments match hitTestFootprint. Touch has no hover, so on RN this
|
|
364
|
+
* tracks the badge the user last tapped.
|
|
365
|
+
*/
|
|
366
|
+
setFootprintHover(candleTimeMs: number, side: number): void;
|
|
330
367
|
/** True while any axis-label fade is still in progress. Drives a RAF loop. */
|
|
331
368
|
isAnimating(): boolean;
|
|
332
369
|
render(): ChartFrame | null;
|
package/src/types.ts
CHANGED
|
@@ -20,8 +20,15 @@ export type {
|
|
|
20
20
|
MACDConfig,
|
|
21
21
|
ChartType,
|
|
22
22
|
TransitionEasing,
|
|
23
|
+
IntervalTransition,
|
|
23
24
|
PriceLine,
|
|
24
25
|
PriceLinesStyle,
|
|
26
|
+
Footprint,
|
|
27
|
+
FootprintSide,
|
|
28
|
+
FootprintsStyle,
|
|
29
|
+
FootprintEvent,
|
|
30
|
+
PlotRect,
|
|
31
|
+
DefaultDrawingStyle,
|
|
25
32
|
} from '@vroomchart/types';
|
|
26
33
|
|
|
27
34
|
/**
|
package/src/useChartCore.ts
CHANGED
|
@@ -16,8 +16,11 @@ import type {
|
|
|
16
16
|
MovingAverageOverlay,
|
|
17
17
|
PriceLine,
|
|
18
18
|
PriceLinesStyle,
|
|
19
|
+
Footprint,
|
|
20
|
+
FootprintsStyle,
|
|
19
21
|
RSIConfig,
|
|
20
22
|
TransitionEasing,
|
|
23
|
+
IntervalTransition,
|
|
21
24
|
VisibleRange,
|
|
22
25
|
VolumeConfig,
|
|
23
26
|
VroomTheme,
|
|
@@ -202,6 +205,45 @@ function priceLinesToSpec(cfg: PriceLinesProp) {
|
|
|
202
205
|
// requires them).
|
|
203
206
|
const EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
|
|
204
207
|
|
|
208
|
+
// Footprints share the price lines' hover weight so the two widgets light up
|
|
209
|
+
// alike. Zeroed geometry defers to the core's own defaults.
|
|
210
|
+
const DEFAULT_FOOTPRINT_HOVER_BOOST = 1.25;
|
|
211
|
+
|
|
212
|
+
// Mirrors VroomFootprintSide in packages/core/include/vroom/vroom_chart.h.
|
|
213
|
+
const FOOTPRINT_BUY = 0;
|
|
214
|
+
const FOOTPRINT_SELL = 1;
|
|
215
|
+
|
|
216
|
+
// A time no real series can contain (~273,000 BCE), still comfortably inside
|
|
217
|
+
// int64. Parks a malformed footprint where the core will never bucket it.
|
|
218
|
+
const UNBUCKETABLE_MS = -8.64e15;
|
|
219
|
+
|
|
220
|
+
/** The footprints + their shared style, as the chart's props express them. */
|
|
221
|
+
export type FootprintsProp = {
|
|
222
|
+
prints: Footprint[];
|
|
223
|
+
style?: FootprintsStyle;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function footprintsToSpec(cfg: FootprintsProp) {
|
|
227
|
+
return {
|
|
228
|
+
// Index alignment is load-bearing: the core reports hits as indices into this
|
|
229
|
+
// array and the gesture layer maps them straight back to the consumer's
|
|
230
|
+
// `footprints`. So a non-finite time — which can't be bucketed and would
|
|
231
|
+
// reach the native side as a garbage int64 — is neutralized *in place* rather
|
|
232
|
+
// than filtered out, which would shift every index after it onto the wrong
|
|
233
|
+
// trade.
|
|
234
|
+
prints: cfg.prints.map((f) => ({
|
|
235
|
+
timeMs: Number.isFinite(f.timeMs) ? f.timeMs : UNBUCKETABLE_MS,
|
|
236
|
+
side: f.side === 'sell' ? FOOTPRINT_SELL : FOOTPRINT_BUY,
|
|
237
|
+
})),
|
|
238
|
+
radiusPx: cfg.style?.radius ?? 0,
|
|
239
|
+
gapPx: cfg.style?.gap ?? 0,
|
|
240
|
+
marginPx: cfg.style?.margin ?? 0,
|
|
241
|
+
hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_FOOTPRINT_HOVER_BOOST,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const EMPTY_FOOTPRINTS = footprintsToSpec({ prints: [] });
|
|
246
|
+
|
|
205
247
|
let installed = false;
|
|
206
248
|
function ensureInstalled(): void {
|
|
207
249
|
if (installed) return;
|
|
@@ -229,6 +271,8 @@ export type TransitionOptions = {
|
|
|
229
271
|
transitionMs?: number;
|
|
230
272
|
/** Curve applied to the morph's progress. Default 'ease-in-out'. */
|
|
231
273
|
transitionEasing?: TransitionEasing;
|
|
274
|
+
/** `'transform'` (default) slot-lerps; `'fade'` fades out then in. */
|
|
275
|
+
intervalTransition?: IntervalTransition;
|
|
232
276
|
/** OS reduced-motion preference: skips the capture and snaps. */
|
|
233
277
|
reduceMotion?: boolean;
|
|
234
278
|
/** Receives every morph frame. Without one, data swaps snap. */
|
|
@@ -265,6 +309,7 @@ export function useChartCore(
|
|
|
265
309
|
bollingerBands?: BollingerBandsConfig,
|
|
266
310
|
volume?: VolumeConfig,
|
|
267
311
|
priceLines?: PriceLinesProp,
|
|
312
|
+
footprints?: FootprintsProp,
|
|
268
313
|
transition?: TransitionOptions,
|
|
269
314
|
): ChartCoreState {
|
|
270
315
|
const handleRef = useRef<ChartHandle | null>(null);
|
|
@@ -295,11 +340,13 @@ export function useChartCore(
|
|
|
295
340
|
ms: number;
|
|
296
341
|
easing: TransitionEasing | undefined;
|
|
297
342
|
reduceMotion: boolean;
|
|
298
|
-
|
|
343
|
+
interval: IntervalTransition;
|
|
344
|
+
}>({ ms: 300, easing: undefined, reduceMotion: false, interval: 'transform' });
|
|
299
345
|
animRef.current = {
|
|
300
346
|
ms: Math.max(0, transition?.transitionMs ?? 300),
|
|
301
347
|
easing: transition?.transitionEasing,
|
|
302
348
|
reduceMotion: transition?.reduceMotion ?? false,
|
|
349
|
+
interval: transition?.intervalTransition === 'fade' ? 'fade' : 'transform',
|
|
303
350
|
};
|
|
304
351
|
const onFrameRef = useRef(transition?.onFrame);
|
|
305
352
|
onFrameRef.current = transition?.onFrame;
|
|
@@ -356,6 +403,7 @@ export function useChartCore(
|
|
|
356
403
|
const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : '';
|
|
357
404
|
const volumeKey = volume ? JSON.stringify(volume) : '';
|
|
358
405
|
const priceLinesKey = priceLines ? JSON.stringify(priceLines) : '';
|
|
406
|
+
const footprintsKey = footprints ? JSON.stringify(footprints) : '';
|
|
359
407
|
|
|
360
408
|
useEffect(() => {
|
|
361
409
|
const h = handleRef.current;
|
|
@@ -419,7 +467,7 @@ export function useChartCore(
|
|
|
419
467
|
onFrameRef.current != null;
|
|
420
468
|
if (morphing) {
|
|
421
469
|
endIntervalMorph();
|
|
422
|
-
h.beginIntervalMorph();
|
|
470
|
+
h.beginIntervalMorph(animRef.current.interval);
|
|
423
471
|
}
|
|
424
472
|
} else if (transitionKind === 'initial' || transitionKind === 'reset') {
|
|
425
473
|
// Wholesale reframing — the slot pairing no longer holds, so land any
|
|
@@ -487,6 +535,9 @@ export function useChartCore(
|
|
|
487
535
|
h.setPriceLines(
|
|
488
536
|
priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES,
|
|
489
537
|
);
|
|
538
|
+
h.setFootprints(
|
|
539
|
+
footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS,
|
|
540
|
+
);
|
|
490
541
|
// TODO(rn-parity): mirror the web `liquidity` overlay here (setLiquidity +
|
|
491
542
|
// the VroomBand structs in the JSI handle) — web-only for now.
|
|
492
543
|
// A just-started morph is already pushing frames straight to the host sink;
|
|
@@ -494,10 +545,10 @@ export function useChartCore(
|
|
|
494
545
|
// frame 0 is pixel-identical to what's on screen, so there's nothing to show
|
|
495
546
|
// in the meantime anyway.
|
|
496
547
|
if (!morphing) setPicture(h.render());
|
|
497
|
-
// theme/rsi/macd/movingAverages/vwap/bollingerBands/volume/priceLines
|
|
498
|
-
// represented by their *Key deps.
|
|
548
|
+
// theme/rsi/macd/movingAverages/vwap/bollingerBands/volume/priceLines/
|
|
549
|
+
// footprints are represented by their *Key deps.
|
|
499
550
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
500
|
-
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
|
|
551
|
+
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph]);
|
|
501
552
|
|
|
502
553
|
return { handle: handleRef.current, picture, volumeCollapseRef };
|
|
503
554
|
}
|