react-native-vroom-chart 0.16.0 → 0.17.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 +81 -0
- package/cpp/_core_include/vroom/vroom_chart.h +41 -0
- package/cpp/_core_src/candles.cpp +15 -14
- package/cpp/_core_src/chart.cpp +297 -28
- package/cpp/_core_src/chart.h +71 -2
- package/cpp/_core_src/chart_facade.cpp +27 -0
- package/cpp/_core_src/color_lerp.h +28 -0
- package/cpp/_core_src/loading_line.cpp +183 -0
- package/cpp/_core_src/loading_line.h +38 -0
- package/cpp/_core_src/loading_wave.h +140 -0
- package/cpp/_core_src/ma_overlay.cpp +15 -10
- package/cpp/_core_src/ma_overlay.h +7 -0
- package/cpp/_core_src/price_indicator.cpp +21 -7
- package/cpp/_core_src/price_indicator.h +11 -1
- package/cpp/_core_src/price_indicator_anim.h +81 -0
- package/cpp/_core_src/theme.cpp +5 -0
- package/cpp/_core_src/tip_geometry.h +55 -0
- package/cpp/_core_src/viewport.h +33 -0
- package/lib/index.d.mts +30 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +71 -11
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +71 -11
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +12 -0
- package/src/jsi.d.ts +42 -0
- package/src/theme.ts +1 -0
- package/src/useChartCore.ts +119 -4
package/package.json
CHANGED
package/src/VroomChart.tsx
CHANGED
|
@@ -59,6 +59,7 @@ function isSkImage(frame: ChartFrame): frame is SkImage {
|
|
|
59
59
|
export function VroomChart(props: VroomChartProps) {
|
|
60
60
|
const {
|
|
61
61
|
candles,
|
|
62
|
+
loading,
|
|
62
63
|
seriesKey,
|
|
63
64
|
width: widthProp,
|
|
64
65
|
height: heightProp,
|
|
@@ -204,8 +205,15 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
204
205
|
reduceMotion,
|
|
205
206
|
onFrame,
|
|
206
207
|
},
|
|
208
|
+
loading,
|
|
207
209
|
);
|
|
208
210
|
|
|
211
|
+
// Same condition useChartCore draws the line on: a refresh that still has
|
|
212
|
+
// data keeps the chart interactive. Every gesture below is gated on this —
|
|
213
|
+
// there's nothing to pan, zoom or inspect while the line is up, and a
|
|
214
|
+
// crosshair reading prices off a placeholder walk would be actively wrong.
|
|
215
|
+
const showLoadingLine = loading === true && candles.length === 0;
|
|
216
|
+
|
|
209
217
|
// When the crosshair is showing, pan moves it (instead of scrolling) and
|
|
210
218
|
// pinch is disabled. A ref (not state) so gesture callbacks read it
|
|
211
219
|
// synchronously without re-subscribing. Tap dismisses it.
|
|
@@ -600,6 +608,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
600
608
|
};
|
|
601
609
|
|
|
602
610
|
const pan = Gesture.Pan()
|
|
611
|
+
.enabled(!showLoadingLine)
|
|
603
612
|
.runOnJS(true)
|
|
604
613
|
.maxPointers(1) // don't fight Pinch's two-finger gesture
|
|
605
614
|
.onStart((e) => {
|
|
@@ -745,6 +754,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
745
754
|
enableY: false,
|
|
746
755
|
});
|
|
747
756
|
const pinch = Gesture.Pinch()
|
|
757
|
+
.enabled(!showLoadingLine)
|
|
748
758
|
.runOnJS(true)
|
|
749
759
|
.onTouchesDown((e) => {
|
|
750
760
|
if (e.numberOfTouches < 2) return;
|
|
@@ -795,6 +805,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
795
805
|
// activates `pan` (it needs movement first), so the chart won't scroll under
|
|
796
806
|
// the hold. The dot/horizontal line are lifted above the fingertip.
|
|
797
807
|
const longPress = Gesture.LongPress()
|
|
808
|
+
.enabled(!showLoadingLine)
|
|
798
809
|
.runOnJS(true)
|
|
799
810
|
.onStart((e) => {
|
|
800
811
|
if (!handle) return;
|
|
@@ -826,6 +837,7 @@ export function VroomChart(props: VroomChartProps) {
|
|
|
826
837
|
// badge, and otherwise dismisses the crosshair while it's up. Any other tap is a
|
|
827
838
|
// no-op, so it never interferes with normal pan/pinch.
|
|
828
839
|
const tap = Gesture.Tap()
|
|
840
|
+
.enabled(!showLoadingLine)
|
|
829
841
|
.runOnJS(true)
|
|
830
842
|
.onStart((e) => {
|
|
831
843
|
if (!handle) return;
|
package/src/jsi.d.ts
CHANGED
|
@@ -92,6 +92,48 @@ export interface ChartHandle {
|
|
|
92
92
|
* the capture. Driven per-frame by the host animation loop.
|
|
93
93
|
*/
|
|
94
94
|
setIntervalMorph(t: number): void;
|
|
95
|
+
/**
|
|
96
|
+
* Shows or hides the loading line: a single stroke drawn across the plot in a
|
|
97
|
+
* slow travelling sine, for a chart that is laid out but has no data yet.
|
|
98
|
+
* Suppresses the axis text, price badge, crosshair and indicator panes while
|
|
99
|
+
* it's up.
|
|
100
|
+
*
|
|
101
|
+
* `on` must mean "loading *and* holding no data" — the core takes it at face
|
|
102
|
+
* value rather than checking its candle buffer, since a host mid-asset-switch
|
|
103
|
+
* can still be holding the previous asset's bars. Pass `animate: false` for
|
|
104
|
+
* reduced motion: the line draws still and the chart may go idle.
|
|
105
|
+
*
|
|
106
|
+
* Passing `false` abandons any hand-off in flight, which is what a failed or
|
|
107
|
+
* superseded fetch wants. To hand off to data instead, use the two stages
|
|
108
|
+
* below.
|
|
109
|
+
*/
|
|
110
|
+
setLoading(on: boolean, animate?: boolean): void;
|
|
111
|
+
/**
|
|
112
|
+
* Hand-off stage one: reshapes the loading line into the series. Freezes the
|
|
113
|
+
* sine where it is and aims each vertex at the vertical centre of the candle
|
|
114
|
+
* that will occupy its column, so the line resolves into the silhouette of
|
|
115
|
+
* the data.
|
|
116
|
+
*
|
|
117
|
+
* Call *after* setCandles — it reads them to know where to aim — then drive
|
|
118
|
+
* {@link setLoadingMorph} from 0 to 1. The axes and the candles stay
|
|
119
|
+
* suppressed throughout; stage two brings them in.
|
|
120
|
+
*/
|
|
121
|
+
beginLoadingMorph(): void;
|
|
122
|
+
/**
|
|
123
|
+
* Advances stage one. `t` (clamped to 0..1) is the eased progress: 0 renders
|
|
124
|
+
* the frozen sine, 1 the polyline through the candle centres.
|
|
125
|
+
*/
|
|
126
|
+
setLoadingMorph(t: number): void;
|
|
127
|
+
/**
|
|
128
|
+
* Hand-off stage two: the candles take over. Captures each one collapsed onto
|
|
129
|
+
* its own vertical centre at zero alpha and leaves the loading state, so the
|
|
130
|
+
* bars grow outward from the line and their color fades up as it fades out.
|
|
131
|
+
*
|
|
132
|
+
* Call once stage one has reached 1, then drive {@link setIntervalMorph} from
|
|
133
|
+
* 0 to 1 — the line's fade-out rides that same clock, so the two finish
|
|
134
|
+
* together.
|
|
135
|
+
*/
|
|
136
|
+
beginLoadingReveal(): void;
|
|
95
137
|
/** Shifts the visible range by `dx`/`dy` pixels and returns a fresh picture. */
|
|
96
138
|
pan(dx: number, dy: number): ChartFrame | null;
|
|
97
139
|
/**
|
package/src/theme.ts
CHANGED
|
@@ -20,6 +20,7 @@ export const COLOR_KEYS: Partial<Record<keyof VroomTheme, number>> = {
|
|
|
20
20
|
accentBull: 14, // VROOM_COLOR_ACCENT_BULL
|
|
21
21
|
accentBear: 15, // VROOM_COLOR_ACCENT_BEAR
|
|
22
22
|
lineColor: 16, // VROOM_COLOR_LINE
|
|
23
|
+
skeleton: 17, // VROOM_COLOR_SKELETON
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
// Maps each numeric VroomTheme field to its VroomFloatKey index.
|
package/src/useChartCore.ts
CHANGED
|
@@ -425,6 +425,9 @@ export function useChartCore(
|
|
|
425
425
|
priceLines?: PriceLinesProp,
|
|
426
426
|
footprints?: FootprintsProp,
|
|
427
427
|
transition?: TransitionOptions,
|
|
428
|
+
// Trails the config params because it's data state, not configuration: it
|
|
429
|
+
// pairs with `candles` above (see showLoadingLine below).
|
|
430
|
+
loading?: boolean,
|
|
428
431
|
): ChartCoreState {
|
|
429
432
|
const handleRef = useRef<ChartHandle | null>(null);
|
|
430
433
|
// Push setDefaultCandleWidth only once (first load): setCandles re-runs on
|
|
@@ -486,22 +489,39 @@ export function useChartCore(
|
|
|
486
489
|
onFrameRef.current = transition?.onFrame;
|
|
487
490
|
const seriesKey = transition?.seriesKey;
|
|
488
491
|
|
|
492
|
+
// Set only while the loading hand-off is in its *first* stage, which is the
|
|
493
|
+
// one an interruption can't simply land: stage one leaves `loading` set in
|
|
494
|
+
// the core, and only stage two releases it.
|
|
495
|
+
const loadingHandoffRef = useRef(false);
|
|
496
|
+
|
|
489
497
|
// Stop an in-flight interval morph and land the core on the new candles.
|
|
490
498
|
const endIntervalMorph = useCallback(() => {
|
|
491
499
|
if (intervalMorphRaf.current != null) {
|
|
492
500
|
cancelAnimationFrame(intervalMorphRaf.current);
|
|
493
501
|
intervalMorphRaf.current = null;
|
|
494
502
|
}
|
|
495
|
-
handleRef.current
|
|
503
|
+
const h = handleRef.current;
|
|
504
|
+
// Walk a half-finished hand-off through the rest of its stages rather than
|
|
505
|
+
// just stopping the clock, or the core would be left drawing the loading
|
|
506
|
+
// line over the data it was supposed to hand off to.
|
|
507
|
+
if (loadingHandoffRef.current) {
|
|
508
|
+
loadingHandoffRef.current = false;
|
|
509
|
+
h?.setLoadingMorph(1);
|
|
510
|
+
h?.beginLoadingReveal();
|
|
511
|
+
}
|
|
512
|
+
h?.setIntervalMorph(1);
|
|
496
513
|
}, []);
|
|
497
514
|
|
|
498
515
|
// Runs the interval morph clock. The core holds the pre-swap geometry (see
|
|
499
516
|
// beginIntervalMorph) and reshapes each candle slot toward its new counterpart.
|
|
500
|
-
|
|
517
|
+
// `durationMs` overrides transitionMs for the loading hand-off, which splits
|
|
518
|
+
// it across two stages.
|
|
519
|
+
const startIntervalMorph = useCallback((h: ChartHandle, durationMs?: number) => {
|
|
501
520
|
const { ms, easing } = animRef.current;
|
|
521
|
+
const dur = durationMs ?? ms;
|
|
502
522
|
const start = performance.now();
|
|
503
523
|
const step = (now: number) => {
|
|
504
|
-
const p = Math.min(1, (now - start) /
|
|
524
|
+
const p = Math.min(1, (now - start) / dur);
|
|
505
525
|
h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
|
|
506
526
|
const pic = h.render();
|
|
507
527
|
if (pic) onFrameRef.current?.(pic);
|
|
@@ -510,6 +530,44 @@ export function useChartCore(
|
|
|
510
530
|
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
511
531
|
}, []);
|
|
512
532
|
|
|
533
|
+
// Hands the loading line over to the data that just landed, in two stages:
|
|
534
|
+
// the line reshapes into the series' silhouette, then the candles grow out of
|
|
535
|
+
// it while it fades.
|
|
536
|
+
//
|
|
537
|
+
// Sequential rather than overlapped — the shape has to read as the data
|
|
538
|
+
// before the bars start emerging from it — so the two split transitionMs and
|
|
539
|
+
// the whole hand-off costs what any other transition costs.
|
|
540
|
+
const startLoadingHandoff = useCallback(
|
|
541
|
+
(h: ChartHandle) => {
|
|
542
|
+
const { ms, easing } = animRef.current;
|
|
543
|
+
const half = ms / 2;
|
|
544
|
+
loadingHandoffRef.current = true;
|
|
545
|
+
h.beginLoadingMorph();
|
|
546
|
+
const start = performance.now();
|
|
547
|
+
const step = (now: number) => {
|
|
548
|
+
const p = Math.min(1, (now - start) / half);
|
|
549
|
+
h.setLoadingMorph(p < 1 ? ease(easing, p) : 1);
|
|
550
|
+
const pic = h.render();
|
|
551
|
+
if (pic) onFrameRef.current?.(pic);
|
|
552
|
+
if (p < 1) {
|
|
553
|
+
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
intervalMorphRaf.current = null;
|
|
557
|
+
// Past here an interruption is an ordinary interval morph again: the
|
|
558
|
+
// core has left the loading state, so landing the clock is enough.
|
|
559
|
+
loadingHandoffRef.current = false;
|
|
560
|
+
// Stage two rides the interval-morph clock, which also carries the
|
|
561
|
+
// line's fade-out — so the bars' growth and the line's exit finish
|
|
562
|
+
// together instead of one outlasting the other.
|
|
563
|
+
h.beginLoadingReveal();
|
|
564
|
+
startIntervalMorph(h, half);
|
|
565
|
+
};
|
|
566
|
+
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
567
|
+
},
|
|
568
|
+
[startIntervalMorph],
|
|
569
|
+
);
|
|
570
|
+
|
|
513
571
|
// Stops an in-flight stream animation and puts the chart somewhere coherent.
|
|
514
572
|
//
|
|
515
573
|
// A pending window shift always lands on its target: abandoned mid-slide it
|
|
@@ -606,6 +664,17 @@ export function useChartCore(
|
|
|
606
664
|
const startMs = visibleRange?.startMs ?? 0;
|
|
607
665
|
const endMs = visibleRange?.endMs ?? 0;
|
|
608
666
|
|
|
667
|
+
// The core trusts `setLoading` outright, so the "and no data yet" half of the
|
|
668
|
+
// condition is decided here. Both halves matter: without `loading` a chart
|
|
669
|
+
// that legitimately has no bars would wave a placeholder forever, and without
|
|
670
|
+
// the emptiness check a background refresh of a loaded series would blank the
|
|
671
|
+
// chart the user is already reading.
|
|
672
|
+
const showLoadingLine = loading === true && candles.length === 0;
|
|
673
|
+
// Tracks whether the *core* is currently showing the line, which is what
|
|
674
|
+
// decides if the next data push is a hand-off. Distinct from `showLoadingLine`:
|
|
675
|
+
// that is this render's intent, this is what's on screen.
|
|
676
|
+
const lineUpRef = useRef(false);
|
|
677
|
+
|
|
609
678
|
// Stable deps so inline `theme={{...}}` / `rsi={{...}}` literals don't re-run
|
|
610
679
|
// the effect every render — only when the actual values change.
|
|
611
680
|
const themeKey = theme ? JSON.stringify(theme) : '';
|
|
@@ -645,6 +714,31 @@ export function useChartCore(
|
|
|
645
714
|
// the viewport: a stream leaves it alone, a timeframe switch re-anchors and
|
|
646
715
|
// morphs into it, a different asset resets it.
|
|
647
716
|
let morphing = false;
|
|
717
|
+
|
|
718
|
+
if (showLoadingLine) {
|
|
719
|
+
// Clear the core's buffer, which the `candles.length > 0` gate below
|
|
720
|
+
// otherwise never does: pushing an empty array is treated as "hold the
|
|
721
|
+
// last frame" everywhere else, so a chart switching assets would still be
|
|
722
|
+
// holding the previous one's bars underneath the line — and would
|
|
723
|
+
// classify the incoming series as a timeframe switch rather than a fresh
|
|
724
|
+
// load. Scoped to the loading case so that hold-the-last-frame behavior
|
|
725
|
+
// is untouched for every other empty push.
|
|
726
|
+
if (!lineUpRef.current) {
|
|
727
|
+
endIntervalMorph();
|
|
728
|
+
settleStream();
|
|
729
|
+
h.setCandles(packCandles([]));
|
|
730
|
+
prevDataRef.current = null;
|
|
731
|
+
}
|
|
732
|
+
h.setLoading(true, !animRef.current.reduceMotion);
|
|
733
|
+
lineUpRef.current = true;
|
|
734
|
+
} else if (lineUpRef.current && candles.length === 0) {
|
|
735
|
+
// Loading resolved to nothing — an empty result, or an error the consumer
|
|
736
|
+
// handled. There's no geometry to morph into, so drop the line rather
|
|
737
|
+
// than leaving it waving at data that isn't coming.
|
|
738
|
+
h.setLoading(false, true);
|
|
739
|
+
lineUpRef.current = false;
|
|
740
|
+
}
|
|
741
|
+
|
|
648
742
|
if (candles.length > 0) {
|
|
649
743
|
const prev = prevDataRef.current;
|
|
650
744
|
const freshHandle = prev == null || prev.handle !== h;
|
|
@@ -667,6 +761,8 @@ export function useChartCore(
|
|
|
667
761
|
} | null = null;
|
|
668
762
|
// The pre-swap candle envelope, used to scale-lock the y-axis below.
|
|
669
763
|
let prevEnvelope: { low: number; high: number } | null = null;
|
|
764
|
+
// Set when this push is the loading line's hand-off to real data.
|
|
765
|
+
let handOff = false;
|
|
670
766
|
// Set for an animated live update: whether the last bar reshapes, and
|
|
671
767
|
// the window an appended bar should pull the view to.
|
|
672
768
|
let stream: { morph: boolean; window: VisibleRange | null } | null = null;
|
|
@@ -747,6 +843,22 @@ export function useChartCore(
|
|
|
747
843
|
endIntervalMorph();
|
|
748
844
|
}
|
|
749
845
|
|
|
846
|
+
// The loading line's data has landed, so it hands over to the series
|
|
847
|
+
// instead of the chart cutting to it. Always classified 'initial' (the
|
|
848
|
+
// loading branch above cleared prevDataRef), so this runs after that
|
|
849
|
+
// branch's endIntervalMorph.
|
|
850
|
+
if (lineUpRef.current) {
|
|
851
|
+
lineUpRef.current = false;
|
|
852
|
+
handOff =
|
|
853
|
+
animRef.current.ms > 0 &&
|
|
854
|
+
!animRef.current.reduceMotion &&
|
|
855
|
+
onFrameRef.current != null;
|
|
856
|
+
// Snap path only. The hand-off itself starts after setCandles and the
|
|
857
|
+
// framing below: both its stages aim at where the candles will
|
|
858
|
+
// actually sit, so neither can be set up until they're there.
|
|
859
|
+
if (!handOff) h.setLoading(false, true);
|
|
860
|
+
}
|
|
861
|
+
|
|
750
862
|
h.setCandles(packCandles(candles));
|
|
751
863
|
|
|
752
864
|
if (transitionKind === 'timeframe') {
|
|
@@ -778,6 +890,9 @@ export function useChartCore(
|
|
|
778
890
|
} else if (transitionKind === 'reset') {
|
|
779
891
|
h.resetView();
|
|
780
892
|
}
|
|
893
|
+
// After setCandles and the framing above, so the line aims at — and the
|
|
894
|
+
// candles grow from — the geometry each bar will actually occupy.
|
|
895
|
+
if (handOff) startLoadingHandoff(h);
|
|
781
896
|
prevDataRef.current = { handle: h, candles, seriesKey };
|
|
782
897
|
}
|
|
783
898
|
}
|
|
@@ -827,7 +942,7 @@ export function useChartCore(
|
|
|
827
942
|
// fairValueGaps/volume/priceLines/footprints are represented by their *Key
|
|
828
943
|
// deps.
|
|
829
944
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
830
|
-
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph, startStreamAnim, settleStream]);
|
|
945
|
+
}, [candles, showLoadingLine, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, startLoadingHandoff, endIntervalMorph, startStreamAnim, settleStream]);
|
|
831
946
|
|
|
832
947
|
return { handle: handleRef.current, picture, volumeCollapseRef };
|
|
833
948
|
}
|