react-native-vroom-chart 0.13.0 → 0.14.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/android/CMakeLists.txt +8 -0
- package/android/README.md +11 -10
- package/cpp/VroomChartHostObject.cpp +155 -54
- package/cpp/VroomChartHostObject.h +1 -1
- package/cpp/VroomJsiInstaller.cpp +1 -1
- package/cpp/_core_include/vroom/vroom_chart.h +6 -7
- package/cpp/_core_src/candles.h +7 -5
- package/cpp/_core_src/chart.cpp +271 -152
- package/cpp/_core_src/chart.h +6 -3
- package/cpp/_core_src/chart_facade.cpp +5 -1
- package/cpp/_core_src/viewport.h +4 -0
- package/lib/index.d.mts +52 -6
- package/lib/index.d.ts +52 -6
- package/lib/index.js +68 -36
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +77 -38
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +94 -44
- package/src/index.ts +2 -0
- package/src/jsi.d.ts +18 -17
- package/src/types.ts +2 -0
- package/src/useChartCore.ts +11 -7
package/src/VroomChart.tsx
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
// VroomChart — Phase 3.
|
|
2
2
|
//
|
|
3
|
-
// Owns
|
|
4
|
-
// - useChartCore's "initial"
|
|
5
|
-
// - Pan gesture callbacks that call handle.pan(dx, dy) → fresh
|
|
3
|
+
// Owns SharedValues driven by:
|
|
4
|
+
// - useChartCore's "initial" frame (when data/size/range change), AND
|
|
5
|
+
// - Pan gesture callbacks that call handle.pan(dx, dy) → a fresh frame.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// iOS wraps an SkPicture in-process. Android rasterizes to an SkImage (the
|
|
8
|
+
// two Skia copies can't share a picture pointer) so pan/zoom don't serialize
|
|
9
|
+
// the scene — and the system typeface — on every frame.
|
|
10
|
+
//
|
|
11
|
+
// Reanimated 4 + RN-Skia 2 propagate SharedValue changes to <Picture>/<Image>
|
|
12
|
+
// without a React re-render, so gesture-driven redraws are cheap.
|
|
9
13
|
//
|
|
10
14
|
// Gestures run on the JS thread for now (`runOnJS(true)`) — installing the
|
|
11
15
|
// JSI bindings on the worklet runtime is a later perf optimization.
|
|
12
16
|
|
|
13
17
|
import React, { useEffect, useRef, useCallback, useState, useMemo } from 'react';
|
|
14
|
-
import { View, type LayoutChangeEvent } from 'react-native';
|
|
15
|
-
import {
|
|
18
|
+
import { PixelRatio, View, type LayoutChangeEvent } from 'react-native';
|
|
19
|
+
import {
|
|
20
|
+
AlphaType,
|
|
21
|
+
Canvas,
|
|
22
|
+
ColorType,
|
|
23
|
+
Image,
|
|
24
|
+
Picture,
|
|
25
|
+
Skia,
|
|
26
|
+
type SkImage,
|
|
27
|
+
type SkPicture,
|
|
28
|
+
} from '@shopify/react-native-skia';
|
|
16
29
|
import {
|
|
17
30
|
Gesture,
|
|
18
31
|
GestureDetector,
|
|
@@ -22,9 +35,14 @@ import { useReducedMotion, useSharedValue } from 'react-native-reanimated';
|
|
|
22
35
|
|
|
23
36
|
import { useChartCore } from './useChartCore';
|
|
24
37
|
import { ease, easingIndex } from './easing';
|
|
38
|
+
import type { ChartFrame } from './jsi.d';
|
|
25
39
|
import type { VroomChartProps } from './types';
|
|
26
40
|
import './jsi.d';
|
|
27
41
|
|
|
42
|
+
function isSkImage(frame: ChartFrame): frame is SkImage {
|
|
43
|
+
return typeof (frame as SkImage).getImageInfo === 'function';
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
/**
|
|
29
47
|
* Skia-rendered candlestick chart. Pass OHLCV `candles` and size it via `style`
|
|
30
48
|
* (it fills its parent by default). Pan to scroll, pinch to zoom, drag the
|
|
@@ -46,6 +64,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
46
64
|
chartType,
|
|
47
65
|
transitionMs,
|
|
48
66
|
transitionEasing,
|
|
67
|
+
intervalTransition,
|
|
49
68
|
theme,
|
|
50
69
|
rsi,
|
|
51
70
|
macd,
|
|
@@ -92,16 +111,39 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
92
111
|
[priceLines, priceLinesStyle, onPriceLineClose],
|
|
93
112
|
);
|
|
94
113
|
|
|
95
|
-
// RN-Skia's recorder reads
|
|
96
|
-
// behind JS-thread writes. If it ever reads null it throws ("Invalid
|
|
97
|
-
// value for SkTextBlob received" — RN-Skia's mislabeled SkPicture
|
|
98
|
-
// we seed
|
|
114
|
+
// RN-Skia's recorder reads these SharedValues on the UI/render runtime, a
|
|
115
|
+
// beat behind JS-thread writes. If it ever reads null it throws ("Invalid
|
|
116
|
+
// prop value for SkTextBlob received" — RN-Skia's mislabeled SkPicture
|
|
117
|
+
// error), so we seed them and *never* assign null. Android writes the image
|
|
118
|
+
// SV (raster path); iOS writes the picture SV. The unused layer stays a
|
|
119
|
+
// transparent 1×1 so it doesn't cover the other.
|
|
99
120
|
const emptyPicture = useMemo(() => {
|
|
100
121
|
const rec = Skia.PictureRecorder();
|
|
101
122
|
rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
|
|
102
123
|
return rec.finishRecordingAsPicture();
|
|
103
124
|
}, []);
|
|
125
|
+
const emptyImage = useMemo(() => {
|
|
126
|
+
const data = Skia.Data.fromBytes(new Uint8Array(4));
|
|
127
|
+
return Skia.Image.MakeImage(
|
|
128
|
+
{
|
|
129
|
+
width: 1,
|
|
130
|
+
height: 1,
|
|
131
|
+
colorType: ColorType.RGBA_8888,
|
|
132
|
+
alphaType: AlphaType.Premul,
|
|
133
|
+
},
|
|
134
|
+
data,
|
|
135
|
+
4,
|
|
136
|
+
)!;
|
|
137
|
+
}, []);
|
|
104
138
|
const pictureSV = useSharedValue<SkPicture>(emptyPicture);
|
|
139
|
+
const imageSV = useSharedValue<SkImage>(emptyImage);
|
|
140
|
+
const applyFrame = useCallback(
|
|
141
|
+
(frame: ChartFrame) => {
|
|
142
|
+
if (isSkImage(frame)) imageSV.value = frame;
|
|
143
|
+
else pictureSV.value = frame;
|
|
144
|
+
},
|
|
145
|
+
[imageSV, pictureSV],
|
|
146
|
+
);
|
|
105
147
|
|
|
106
148
|
// An OS reduced-motion preference snaps every transition, the way
|
|
107
149
|
// prefers-reduced-motion does on web.
|
|
@@ -111,15 +153,15 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
111
153
|
// capture) but repaints every frame, so it writes straight into the SV rather
|
|
112
154
|
// than through React state — the same bypass the gesture handlers use.
|
|
113
155
|
const onFrame = useCallback(
|
|
114
|
-
(p:
|
|
115
|
-
|
|
156
|
+
(p: ChartFrame) => {
|
|
157
|
+
applyFrame(p);
|
|
116
158
|
},
|
|
117
|
-
[
|
|
159
|
+
[applyFrame],
|
|
118
160
|
);
|
|
119
161
|
|
|
120
162
|
const { handle, picture, volumeCollapseRef } = useChartCore(
|
|
121
163
|
candles,
|
|
122
|
-
{ width, height },
|
|
164
|
+
{ width, height, pxRatio: PixelRatio.get() },
|
|
123
165
|
visibleRange,
|
|
124
166
|
defaultCandleWidth,
|
|
125
167
|
chartType,
|
|
@@ -131,7 +173,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
131
173
|
bollingerBands,
|
|
132
174
|
volume,
|
|
133
175
|
priceLinesProp,
|
|
134
|
-
{ seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame },
|
|
176
|
+
{ seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame },
|
|
135
177
|
);
|
|
136
178
|
|
|
137
179
|
// When the crosshair is showing, pan moves it (instead of scrolling) and
|
|
@@ -166,11 +208,11 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
166
208
|
animRaf.current = null;
|
|
167
209
|
if (!handle) return;
|
|
168
210
|
const next = handle.render();
|
|
169
|
-
if (next)
|
|
211
|
+
if (next) applyFrame(next);
|
|
170
212
|
if (handle.isAnimating()) {
|
|
171
213
|
animRaf.current = requestAnimationFrame(animTick);
|
|
172
214
|
}
|
|
173
|
-
}, [handle,
|
|
215
|
+
}, [handle, applyFrame]);
|
|
174
216
|
const maybeStartAnim = useCallback(() => {
|
|
175
217
|
if (animRaf.current != null) return;
|
|
176
218
|
if (!handle?.isAnimating()) return;
|
|
@@ -194,9 +236,9 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
194
236
|
// pulse on needs — otherwise the ring wouldn't move until you touched the
|
|
195
237
|
// chart.
|
|
196
238
|
useEffect(() => {
|
|
197
|
-
if (picture)
|
|
239
|
+
if (picture) applyFrame(picture);
|
|
198
240
|
maybeStartAnim();
|
|
199
|
-
}, [picture,
|
|
241
|
+
}, [picture, applyFrame, maybeStartAnim]);
|
|
200
242
|
|
|
201
243
|
// Candle↔line morph. When `chartType` changes we drive the core per-frame with
|
|
202
244
|
// a (collapse, fade) blend and push a fresh picture into the SV each frame — the
|
|
@@ -225,7 +267,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
225
267
|
morphFade.current = target;
|
|
226
268
|
handle.setChartType(target);
|
|
227
269
|
const p = handle.render();
|
|
228
|
-
if (p)
|
|
270
|
+
if (p) applyFrame(p);
|
|
229
271
|
maybeStartAnim();
|
|
230
272
|
return undefined;
|
|
231
273
|
}
|
|
@@ -243,7 +285,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
243
285
|
morphFade.current = target;
|
|
244
286
|
handle.setChartType(target);
|
|
245
287
|
const p = handle.render();
|
|
246
|
-
if (p)
|
|
288
|
+
if (p) applyFrame(p);
|
|
247
289
|
maybeStartAnim();
|
|
248
290
|
return undefined;
|
|
249
291
|
}
|
|
@@ -258,7 +300,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
258
300
|
// Reduced motion still crossfades, but skips the vertical collapse.
|
|
259
301
|
handle.setMorph(reduceMotion ? 0 : fade, fade);
|
|
260
302
|
const p = handle.render();
|
|
261
|
-
if (p)
|
|
303
|
+
if (p) applyFrame(p);
|
|
262
304
|
if (prog < 1) {
|
|
263
305
|
morphRaf.current = requestAnimationFrame(step);
|
|
264
306
|
} else {
|
|
@@ -266,7 +308,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
266
308
|
morphFade.current = target;
|
|
267
309
|
handle.setChartType(target); // lock the exact endpoint
|
|
268
310
|
const q = handle.render();
|
|
269
|
-
if (q)
|
|
311
|
+
if (q) applyFrame(q);
|
|
270
312
|
maybeStartAnim();
|
|
271
313
|
}
|
|
272
314
|
};
|
|
@@ -279,9 +321,9 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
279
321
|
}
|
|
280
322
|
};
|
|
281
323
|
// maybeStartAnim is memoized on [handle, animTick] and animTick on
|
|
282
|
-
// [handle,
|
|
324
|
+
// [handle, applyFrame], both already deps here — so it adds no new restarts
|
|
283
325
|
// of this clock.
|
|
284
|
-
}, [handle, chartType, transitionMs, reduceMotion,
|
|
326
|
+
}, [handle, chartType, transitionMs, reduceMotion, applyFrame, maybeStartAnim]);
|
|
285
327
|
|
|
286
328
|
// Volume-bar collapse. The core staggers the bars itself — tallest falling
|
|
287
329
|
// first, all landing together — so unlike the loop above this one hands it
|
|
@@ -321,7 +363,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
321
363
|
volumeCollapseRef.current = { t: target, easing };
|
|
322
364
|
handle.setVolumeCollapse(target, easing);
|
|
323
365
|
const p = handle.render();
|
|
324
|
-
if (p)
|
|
366
|
+
if (p) applyFrame(p);
|
|
325
367
|
maybeStartAnim();
|
|
326
368
|
return undefined;
|
|
327
369
|
}
|
|
@@ -338,7 +380,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
338
380
|
volumeCollapseRef.current = { t, easing: kind };
|
|
339
381
|
handle.setVolumeCollapse(t, kind);
|
|
340
382
|
const p = handle.render();
|
|
341
|
-
if (p)
|
|
383
|
+
if (p) applyFrame(p);
|
|
342
384
|
if (prog < 1) {
|
|
343
385
|
volumeRaf.current = requestAnimationFrame(step);
|
|
344
386
|
} else {
|
|
@@ -359,7 +401,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
359
401
|
volume?.enabled,
|
|
360
402
|
transitionMs,
|
|
361
403
|
reduceMotion,
|
|
362
|
-
|
|
404
|
+
applyFrame,
|
|
363
405
|
volumeCollapseRef,
|
|
364
406
|
maybeStartAnim,
|
|
365
407
|
]);
|
|
@@ -386,7 +428,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
386
428
|
axisCollapse.current = { y: targetY, x: targetX };
|
|
387
429
|
handle.setAxisCollapse(targetY, targetX);
|
|
388
430
|
const p = handle.render();
|
|
389
|
-
if (p)
|
|
431
|
+
if (p) applyFrame(p);
|
|
390
432
|
maybeStartAnim();
|
|
391
433
|
return undefined;
|
|
392
434
|
}
|
|
@@ -408,7 +450,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
408
450
|
axisCollapse.current = { y: targetY, x: targetX };
|
|
409
451
|
handle.setAxisCollapse(targetY, targetX);
|
|
410
452
|
const p = handle.render();
|
|
411
|
-
if (p)
|
|
453
|
+
if (p) applyFrame(p);
|
|
412
454
|
maybeStartAnim();
|
|
413
455
|
return undefined;
|
|
414
456
|
}
|
|
@@ -425,7 +467,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
425
467
|
axisCollapse.current = { y, x };
|
|
426
468
|
handle.setAxisCollapse(y, x);
|
|
427
469
|
const p = handle.render();
|
|
428
|
-
if (p)
|
|
470
|
+
if (p) applyFrame(p);
|
|
429
471
|
if (prog < 1) {
|
|
430
472
|
axisRaf.current = requestAnimationFrame(step);
|
|
431
473
|
} else {
|
|
@@ -447,7 +489,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
447
489
|
showXAxis,
|
|
448
490
|
transitionMs,
|
|
449
491
|
reduceMotion,
|
|
450
|
-
|
|
492
|
+
applyFrame,
|
|
451
493
|
maybeStartAnim,
|
|
452
494
|
]);
|
|
453
495
|
|
|
@@ -516,7 +558,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
516
558
|
priceDrag.current = { index: pl.index, id: pl.line.id, price: pl.line.price };
|
|
517
559
|
handle.setPriceLineDrag(pl.index, pl.line.price);
|
|
518
560
|
const p = handle.render();
|
|
519
|
-
if (p)
|
|
561
|
+
if (p) applyFrame(p);
|
|
520
562
|
}
|
|
521
563
|
}
|
|
522
564
|
})
|
|
@@ -546,7 +588,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
546
588
|
// scrolling. Vertical line tracks the finger x; the dot/horizontal line
|
|
547
589
|
// stay lifted `crosshairOffset` px above the fingertip.
|
|
548
590
|
const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
|
|
549
|
-
if (ch)
|
|
591
|
+
if (ch) applyFrame(ch);
|
|
550
592
|
// The line follows the finger every frame (above), but only notify the
|
|
551
593
|
// host when the snapped slot actually changes. The slot has a timeMs
|
|
552
594
|
// even in the empty space ahead of the last candle, where candle=null.
|
|
@@ -564,7 +606,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
564
606
|
// (axes follow). Diagonal works naturally.
|
|
565
607
|
next = handle.translate(e.changeX, e.changeY);
|
|
566
608
|
}
|
|
567
|
-
if (next)
|
|
609
|
+
if (next) applyFrame(next);
|
|
568
610
|
maybeStartAnim();
|
|
569
611
|
})
|
|
570
612
|
.onEnd((e) => {
|
|
@@ -577,7 +619,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
577
619
|
priceDrag.current = null;
|
|
578
620
|
handle.setPriceLineDrag(-1, 0);
|
|
579
621
|
const p = handle.render();
|
|
580
|
-
if (p)
|
|
622
|
+
if (p) applyFrame(p);
|
|
581
623
|
if (g) onPriceLineDragEnd?.(g.id, g.price);
|
|
582
624
|
return;
|
|
583
625
|
}
|
|
@@ -606,7 +648,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
606
648
|
velocity *= Math.pow(0.5, dt / HALF_LIFE_S);
|
|
607
649
|
const dx = velocity * dt;
|
|
608
650
|
const next = handle.pan(dx, 0);
|
|
609
|
-
if (next)
|
|
651
|
+
if (next) applyFrame(next);
|
|
610
652
|
maybeStartAnim();
|
|
611
653
|
|
|
612
654
|
if (Math.abs(velocity) > MIN_STOP) {
|
|
@@ -680,7 +722,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
680
722
|
if (frameX === 1 && frameY === 1) return;
|
|
681
723
|
|
|
682
724
|
const next = handle.zoom(frameX, frameY, focalX, focalY);
|
|
683
|
-
if (next)
|
|
725
|
+
if (next) applyFrame(next);
|
|
684
726
|
maybeStartAnim();
|
|
685
727
|
});
|
|
686
728
|
|
|
@@ -699,7 +741,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
699
741
|
cancelDecay();
|
|
700
742
|
crosshairActive.current = true;
|
|
701
743
|
const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
|
|
702
|
-
if (ch)
|
|
744
|
+
if (ch) applyFrame(ch);
|
|
703
745
|
const info = handle.getCrosshairInfo();
|
|
704
746
|
lastCrosshairTime.current = info?.timeMs ?? null;
|
|
705
747
|
onCrosshair?.({
|
|
@@ -729,7 +771,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
729
771
|
if (hitAxis(e.x, e.y) !== 'chart') return;
|
|
730
772
|
crosshairActive.current = false;
|
|
731
773
|
const ch = handle.clearCrosshair();
|
|
732
|
-
if (ch)
|
|
774
|
+
if (ch) applyFrame(ch);
|
|
733
775
|
lastCrosshairTime.current = null;
|
|
734
776
|
onCrosshair?.({ active: false, candle: null, timeMs: null, price: null, reason: 'hide' });
|
|
735
777
|
});
|
|
@@ -749,9 +791,17 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
749
791
|
<View style={{ flex: 1 }}>
|
|
750
792
|
<Canvas style={{ flex: 1 }}>
|
|
751
793
|
{width > 0 && height > 0 ? (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
794
|
+
<>
|
|
795
|
+
<Picture picture={pictureSV} />
|
|
796
|
+
<Image
|
|
797
|
+
image={imageSV}
|
|
798
|
+
x={0}
|
|
799
|
+
y={0}
|
|
800
|
+
width={width}
|
|
801
|
+
height={height}
|
|
802
|
+
fit="fill"
|
|
803
|
+
/>
|
|
804
|
+
</>
|
|
755
805
|
) : null}
|
|
756
806
|
</Canvas>
|
|
757
807
|
</View>
|
package/src/index.ts
CHANGED
package/src/jsi.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Ambient declaration for the JSI HostObject installed by VroomChartModule.
|
|
2
2
|
|
|
3
|
-
import type { SkPicture } from '@shopify/react-native-skia';
|
|
3
|
+
import type { SkImage, SkPicture } from '@shopify/react-native-skia';
|
|
4
|
+
import type { IntervalTransition } from '@vroomchart/types';
|
|
5
|
+
|
|
6
|
+
/** Frame the JSI handle returns: an SkPicture on iOS, an SkImage on Android. */
|
|
7
|
+
export type ChartFrame = SkPicture | SkImage;
|
|
4
8
|
|
|
5
9
|
export interface ChartHandle {
|
|
6
10
|
setCandles(buffer: ArrayBuffer): void;
|
|
@@ -62,15 +66,12 @@ export interface ChartHandle {
|
|
|
62
66
|
*/
|
|
63
67
|
preservePriceEnvelope(prevLow: number, prevHigh: number): void;
|
|
64
68
|
/**
|
|
65
|
-
* Capture the visible candle geometry so the next data swap can animate
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* Candles are paired by *slot* — position counting back from the right edge of
|
|
70
|
-
* the visible window, which a timeframe switch preserves. Call before
|
|
71
|
-
* 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.
|
|
72
73
|
*/
|
|
73
|
-
beginIntervalMorph(): void;
|
|
74
|
+
beginIntervalMorph(mode?: IntervalTransition): void;
|
|
74
75
|
/**
|
|
75
76
|
* Advance the interval morph started by beginIntervalMorph. `t` (clamped to
|
|
76
77
|
* 0..1) is the eased progress: 0 renders the captured geometry pixel-
|
|
@@ -79,30 +80,30 @@ export interface ChartHandle {
|
|
|
79
80
|
*/
|
|
80
81
|
setIntervalMorph(t: number): void;
|
|
81
82
|
/** Shifts the visible range by `dx`/`dy` pixels and returns a fresh picture. */
|
|
82
|
-
pan(dx: number, dy: number):
|
|
83
|
+
pan(dx: number, dy: number): ChartFrame | null;
|
|
83
84
|
/**
|
|
84
85
|
* Two-finger translation: shifts the time window AND the price bounds
|
|
85
86
|
* without rescaling. dy > 0 (drag down) moves content down.
|
|
86
87
|
*/
|
|
87
|
-
translate(dx: number, dy: number):
|
|
88
|
+
translate(dx: number, dy: number): ChartFrame | null;
|
|
88
89
|
/**
|
|
89
90
|
* Directional zoom by per-axis multiplicative factors around focus point
|
|
90
91
|
* (`fx`, `fy`) in pixels. `scaleX` resizes the time window (>1 = wider
|
|
91
92
|
* candles); `scaleY` resizes the price range (>1 = taller candles). Pass 1
|
|
92
93
|
* for an axis to leave it untouched.
|
|
93
94
|
*/
|
|
94
|
-
zoom(scaleX: number, scaleY: number, fx: number, fy: number):
|
|
95
|
+
zoom(scaleX: number, scaleY: number, fx: number, fy: number): ChartFrame | null;
|
|
95
96
|
/**
|
|
96
97
|
* Drag-on-y-axis price scaling. `dy > 0` widens the price range
|
|
97
98
|
* (candles shrink). Pivots around the price-range center.
|
|
98
99
|
*/
|
|
99
|
-
scalePriceAxis(dy: number):
|
|
100
|
+
scalePriceAxis(dy: number): ChartFrame | null;
|
|
100
101
|
/**
|
|
101
102
|
* Drag-on-x-axis time scaling. `dx > 0` widens the time window
|
|
102
103
|
* (candles thin). Pivots around the right edge so the most recent
|
|
103
104
|
* visible candle stays in place.
|
|
104
105
|
*/
|
|
105
|
-
scaleTimeAxis(dx: number):
|
|
106
|
+
scaleTimeAxis(dx: number): ChartFrame | null;
|
|
106
107
|
/**
|
|
107
108
|
* Current axis dimensions in pixels for hit testing in JS gestures.
|
|
108
109
|
* `indicatorHeight` is the below-chart indicator pane height (0 when none).
|
|
@@ -117,9 +118,9 @@ export interface ChartHandle {
|
|
|
117
118
|
* `y` should already be lifted above the touch point so the dot/horizontal
|
|
118
119
|
* line aren't hidden under the thumb.
|
|
119
120
|
*/
|
|
120
|
-
setCrosshair(x: number, y: number):
|
|
121
|
+
setCrosshair(x: number, y: number): ChartFrame | null;
|
|
121
122
|
/** Hides the crosshair and returns a fresh picture. */
|
|
122
|
-
clearCrosshair():
|
|
123
|
+
clearCrosshair(): ChartFrame | null;
|
|
123
124
|
/**
|
|
124
125
|
* OHLCV of the candle the crosshair currently snaps to, or null when the
|
|
125
126
|
* crosshair is inactive / there are no visible candles. Cheap to poll at
|
|
@@ -326,7 +327,7 @@ export interface ChartHandle {
|
|
|
326
327
|
setPriceLineDrag(index: number, price: number): void;
|
|
327
328
|
/** True while any axis-label fade is still in progress. Drives a RAF loop. */
|
|
328
329
|
isAnimating(): boolean;
|
|
329
|
-
render():
|
|
330
|
+
render(): ChartFrame | null;
|
|
330
331
|
}
|
|
331
332
|
|
|
332
333
|
export interface VroomChartJSI {
|
package/src/types.ts
CHANGED
package/src/useChartCore.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
|
-
import type { SkPicture } from '@shopify/react-native-skia';
|
|
4
3
|
|
|
5
4
|
import NativeVroomChart from './NativeVroomChart';
|
|
6
5
|
import type { DataTransition } from './dataTransitions';
|
|
7
6
|
import { classifyTransition, inferStepMs, timeframeWindow } from './dataTransitions';
|
|
8
7
|
import { ease } from './easing';
|
|
9
|
-
import type { ChartHandle } from './jsi.d';
|
|
8
|
+
import type { ChartFrame, ChartHandle } from './jsi.d';
|
|
10
9
|
import { packCandles } from './packCandles';
|
|
11
10
|
import { applyTheme, parseColor, FLOAT_LINE_TIP_PULSE } from './theme';
|
|
12
11
|
import type {
|
|
@@ -19,6 +18,7 @@ import type {
|
|
|
19
18
|
PriceLinesStyle,
|
|
20
19
|
RSIConfig,
|
|
21
20
|
TransitionEasing,
|
|
21
|
+
IntervalTransition,
|
|
22
22
|
VisibleRange,
|
|
23
23
|
VolumeConfig,
|
|
24
24
|
VroomTheme,
|
|
@@ -230,16 +230,18 @@ export type TransitionOptions = {
|
|
|
230
230
|
transitionMs?: number;
|
|
231
231
|
/** Curve applied to the morph's progress. Default 'ease-in-out'. */
|
|
232
232
|
transitionEasing?: TransitionEasing;
|
|
233
|
+
/** `'transform'` (default) slot-lerps; `'fade'` fades out then in. */
|
|
234
|
+
intervalTransition?: IntervalTransition;
|
|
233
235
|
/** OS reduced-motion preference: skips the capture and snaps. */
|
|
234
236
|
reduceMotion?: boolean;
|
|
235
237
|
/** Receives every morph frame. Without one, data swaps snap. */
|
|
236
|
-
onFrame?: (picture:
|
|
238
|
+
onFrame?: (picture: ChartFrame) => void;
|
|
237
239
|
};
|
|
238
240
|
|
|
239
241
|
export type ChartCoreState = {
|
|
240
242
|
handle: ChartHandle | null;
|
|
241
243
|
/** Picture freshly rendered after the latest data/size/range push. */
|
|
242
|
-
picture:
|
|
244
|
+
picture: ChartFrame | null;
|
|
243
245
|
/**
|
|
244
246
|
* The last volume collapse handed to the core, or null before the first push.
|
|
245
247
|
* VroomChart's animation loop owns this — it lives here only so the data effect
|
|
@@ -282,7 +284,7 @@ export function useChartCore(
|
|
|
282
284
|
seriesKey?: string;
|
|
283
285
|
} | null>(null);
|
|
284
286
|
const intervalMorphRaf = useRef<number | null>(null);
|
|
285
|
-
const [picture, setPicture] = useState<
|
|
287
|
+
const [picture, setPicture] = useState<ChartFrame | null>(null);
|
|
286
288
|
|
|
287
289
|
if (!handleRef.current && size.width > 0 && size.height > 0) {
|
|
288
290
|
ensureInstalled();
|
|
@@ -296,11 +298,13 @@ export function useChartCore(
|
|
|
296
298
|
ms: number;
|
|
297
299
|
easing: TransitionEasing | undefined;
|
|
298
300
|
reduceMotion: boolean;
|
|
299
|
-
|
|
301
|
+
interval: IntervalTransition;
|
|
302
|
+
}>({ ms: 300, easing: undefined, reduceMotion: false, interval: 'transform' });
|
|
300
303
|
animRef.current = {
|
|
301
304
|
ms: Math.max(0, transition?.transitionMs ?? 300),
|
|
302
305
|
easing: transition?.transitionEasing,
|
|
303
306
|
reduceMotion: transition?.reduceMotion ?? false,
|
|
307
|
+
interval: transition?.intervalTransition === 'fade' ? 'fade' : 'transform',
|
|
304
308
|
};
|
|
305
309
|
const onFrameRef = useRef(transition?.onFrame);
|
|
306
310
|
onFrameRef.current = transition?.onFrame;
|
|
@@ -420,7 +424,7 @@ export function useChartCore(
|
|
|
420
424
|
onFrameRef.current != null;
|
|
421
425
|
if (morphing) {
|
|
422
426
|
endIntervalMorph();
|
|
423
|
-
h.beginIntervalMorph();
|
|
427
|
+
h.beginIntervalMorph(animRef.current.interval);
|
|
424
428
|
}
|
|
425
429
|
} else if (transitionKind === 'initial' || transitionKind === 'reset') {
|
|
426
430
|
// Wholesale reframing — the slot pairing no longer holds, so land any
|