react-native-livechart 4.9.2 → 4.11.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/README.md +24 -0
- package/dist/components/CrosshairLine.d.ts +3 -1
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +3 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +4 -2
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/LeftEdgeFade.d.ts +3 -1
- package/dist/components/LeftEdgeFade.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/LoadingOverlay.d.ts +3 -1
- package/dist/components/LoadingOverlay.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +3 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLabels.d.ts +3 -1
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -1
- package/dist/components/MultiSeriesValueLines.d.ts +3 -1
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -1
- package/dist/components/ThresholdSplitShader.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +9 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/components/thresholdSplitShaderSource.d.ts +13 -0
- package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +10 -5
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +2 -0
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/particleAtlas.d.ts +3 -2
- package/dist/draw/particleAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +6 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/delayedPanGuard.d.ts +31 -0
- package/dist/hooks/delayedPanGuard.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +4 -4
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/squiggly.d.ts +6 -3
- package/dist/math/squiggly.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +2 -2
- package/dist/types.d.ts +35 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +17 -0
- package/src/components/CrosshairOverlay.tsx +54 -36
- package/src/components/CustomReferenceLineOverlay.tsx +16 -7
- package/src/components/DegenParticlesOverlay.tsx +40 -5
- package/src/components/LeftEdgeFade.tsx +12 -6
- package/src/components/LiveChart.tsx +34 -16
- package/src/components/LiveChartSeries.tsx +94 -21
- package/src/components/LoadingOverlay.tsx +26 -12
- package/src/components/MarkerOverlay.tsx +31 -19
- package/src/components/MultiSeriesDots.tsx +4 -2
- package/src/components/MultiSeriesTooltipStack.tsx +4 -2
- package/src/components/MultiSeriesValueLabels.tsx +4 -2
- package/src/components/MultiSeriesValueLines.tsx +4 -2
- package/src/components/ThresholdSplitShader.tsx +5 -37
- package/src/components/YAxisOverlay.tsx +43 -1
- package/src/components/thresholdSplitShaderSource.ts +70 -0
- package/src/core/useLiveChartEngine.ts +110 -42
- package/src/core/useLiveChartSeriesEngine.ts +137 -70
- package/src/draw/markerAtlas.ts +22 -2
- package/src/draw/particleAtlas.ts +21 -10
- package/src/hooks/crosshairShared.ts +24 -18
- package/src/hooks/delayedPanGuard.ts +80 -0
- package/src/hooks/useChartPaths.ts +11 -0
- package/src/hooks/useCrosshair.ts +53 -10
- package/src/hooks/useCrosshairSeries.ts +49 -6
- package/src/hooks/useMultiSeriesLinePaths.ts +15 -9
- package/src/index.ts +1 -0
- package/src/math/squiggly.ts +28 -11
- package/src/math/threshold.ts +2 -2
- package/src/types.ts +40 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
2
|
+
|
|
3
|
+
type BooleanSharedValue = Pick<SharedValue<boolean>, "get" | "set">;
|
|
4
|
+
|
|
5
|
+
type TouchCountEvent = {
|
|
6
|
+
numberOfTouches: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type GestureStateManager = {
|
|
10
|
+
fail: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Mark the first pointer of a delayed pan as down. Zero-delay pans are inert. */
|
|
14
|
+
export function delayedPanTouchDown(
|
|
15
|
+
delayMs: number,
|
|
16
|
+
fingerDown: BooleanSharedValue,
|
|
17
|
+
): void {
|
|
18
|
+
"worklet";
|
|
19
|
+
if (delayMs > 0) fingerDown.set(true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fail a delayed pan that is still pending when its final pointer lifts.
|
|
24
|
+
*
|
|
25
|
+
* RNGH 2.x on iOS can leave `activateAfterLongPress`'s timer armed after a
|
|
26
|
+
* stationary touch ends. Failing the pending recognizer forces the reset that
|
|
27
|
+
* cancels that timer. An already-active pan is left to finish normally.
|
|
28
|
+
*/
|
|
29
|
+
export function delayedPanTouchUp(
|
|
30
|
+
delayMs: number,
|
|
31
|
+
event: TouchCountEvent,
|
|
32
|
+
manager: GestureStateManager,
|
|
33
|
+
fingerDown: BooleanSharedValue,
|
|
34
|
+
panActivated: BooleanSharedValue,
|
|
35
|
+
): void {
|
|
36
|
+
"worklet";
|
|
37
|
+
if (delayMs <= 0 || event.numberOfTouches > 0) return;
|
|
38
|
+
fingerDown.set(false);
|
|
39
|
+
if (!panActivated.get()) manager.fail();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Clear pointer state when the native touch stream is cancelled. */
|
|
43
|
+
export function delayedPanTouchCancelled(
|
|
44
|
+
delayMs: number,
|
|
45
|
+
fingerDown: BooleanSharedValue,
|
|
46
|
+
): void {
|
|
47
|
+
"worklet";
|
|
48
|
+
if (delayMs > 0) fingerDown.set(false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Record a real delayed-pan activation, or reject an activation whose timer
|
|
53
|
+
* raced the final pointer-up. The rejected path deliberately keeps
|
|
54
|
+
* `panActivated` false so a missing native FINALIZE cannot poison the next
|
|
55
|
+
* interaction.
|
|
56
|
+
*/
|
|
57
|
+
export function shouldStartDelayedPan(
|
|
58
|
+
delayMs: number,
|
|
59
|
+
fingerDown: BooleanSharedValue,
|
|
60
|
+
panActivated: BooleanSharedValue,
|
|
61
|
+
): boolean {
|
|
62
|
+
"worklet";
|
|
63
|
+
if (delayMs <= 0) return true;
|
|
64
|
+
if (!fingerDown.get()) {
|
|
65
|
+
panActivated.set(false);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
panActivated.set(true);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Reset the delayed-pan lifecycle after native finalization. */
|
|
73
|
+
export function resetDelayedPanGuard(
|
|
74
|
+
fingerDown: BooleanSharedValue,
|
|
75
|
+
panActivated: BooleanSharedValue,
|
|
76
|
+
): void {
|
|
77
|
+
"worklet";
|
|
78
|
+
fingerDown.set(false);
|
|
79
|
+
panActivated.set(false);
|
|
80
|
+
}
|
|
@@ -55,6 +55,10 @@ export function useChartPaths(
|
|
|
55
55
|
ptsA: number[];
|
|
56
56
|
ptsB: number[];
|
|
57
57
|
ptsTick: boolean;
|
|
58
|
+
squigglePts: number[];
|
|
59
|
+
morphA: number[];
|
|
60
|
+
morphB: number[];
|
|
61
|
+
morphTick: boolean;
|
|
58
62
|
scratch: ReturnType<typeof makeSplineScratch>;
|
|
59
63
|
} | null>(null);
|
|
60
64
|
if (cacheRef.current === null) {
|
|
@@ -63,6 +67,10 @@ export function useChartPaths(
|
|
|
63
67
|
ptsA: [] as number[],
|
|
64
68
|
ptsB: [] as number[],
|
|
65
69
|
ptsTick: false,
|
|
70
|
+
squigglePts: [] as number[],
|
|
71
|
+
morphA: [] as number[],
|
|
72
|
+
morphB: [] as number[],
|
|
73
|
+
morphTick: false,
|
|
66
74
|
scratch: makeSplineScratch(),
|
|
67
75
|
};
|
|
68
76
|
}
|
|
@@ -102,15 +110,18 @@ export function useChartPaths(
|
|
|
102
110
|
centerY,
|
|
103
111
|
squiggleAmplitude,
|
|
104
112
|
squiggleSpeed,
|
|
113
|
+
cache.squigglePts,
|
|
105
114
|
);
|
|
106
115
|
|
|
107
116
|
// Blend center-out: centre of chart reveals first, edges last
|
|
117
|
+
cache.morphTick = !cache.morphTick;
|
|
108
118
|
return blendPtsY(
|
|
109
119
|
squigglyPts,
|
|
110
120
|
realPts,
|
|
111
121
|
t,
|
|
112
122
|
padding,
|
|
113
123
|
engine.canvasWidth.get(),
|
|
124
|
+
cache.morphTick ? cache.morphA : cache.morphB,
|
|
114
125
|
);
|
|
115
126
|
});
|
|
116
127
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type SkFont } from "@shopify/react-native-skia";
|
|
2
|
-
import { Platform } from "react-native";
|
|
3
2
|
import { Gesture } from "react-native-gesture-handler";
|
|
4
3
|
import {
|
|
5
4
|
useAnimatedReaction,
|
|
@@ -33,9 +32,18 @@ import {
|
|
|
33
32
|
HIDDEN_TIME_BADGE,
|
|
34
33
|
HIDDEN_TOOLTIP,
|
|
35
34
|
pointInRect,
|
|
35
|
+
SCRUB_ACTIVATE_X_PX,
|
|
36
|
+
SCRUB_FAIL_Y_PX,
|
|
36
37
|
snapPrice,
|
|
37
38
|
type CrosshairState,
|
|
38
39
|
} from "./crosshairShared";
|
|
40
|
+
import {
|
|
41
|
+
delayedPanTouchCancelled,
|
|
42
|
+
delayedPanTouchDown,
|
|
43
|
+
delayedPanTouchUp,
|
|
44
|
+
resetDelayedPanGuard,
|
|
45
|
+
shouldStartDelayedPan,
|
|
46
|
+
} from "./delayedPanGuard";
|
|
39
47
|
|
|
40
48
|
const ACTION_HIT_SLOP = 6;
|
|
41
49
|
const RETICLE_HIT = 14;
|
|
@@ -150,6 +158,10 @@ export function useCrosshair(
|
|
|
150
158
|
// Tracks whether the active scrub phase actually began, so a tap that never
|
|
151
159
|
// activates doesn't emit a spurious onGestureEnd.
|
|
152
160
|
const gestureStarted = useSharedValue(false);
|
|
161
|
+
// Guard state for RNGH's delayed-pan post-lift activation on iOS. The shared
|
|
162
|
+
// lifecycle helpers are also used by LiveChartSeries and unit-tested directly.
|
|
163
|
+
const fingerDown = useSharedValue(false);
|
|
164
|
+
const panActivated = useSharedValue(false);
|
|
153
165
|
// Where the crosshair line should start (canvas Y) so it stops at a top-pinned
|
|
154
166
|
// custom tooltip instead of running through it. -1 = no top tooltip → the line
|
|
155
167
|
// starts at padding.top. Written by CustomTooltipOverlay, read by CrosshairOverlay.
|
|
@@ -490,14 +502,32 @@ export function useCrosshair(
|
|
|
490
502
|
: panGestureDelay;
|
|
491
503
|
|
|
492
504
|
let gesture = Gesture.Pan()
|
|
493
|
-
// Scrub-action: minDistance 0 — the press-hold above is the sole activator
|
|
494
|
-
// (movement during the hold fails the pan), so the crosshair never appears
|
|
495
|
-
// from a tap's travel. The Tap (maxDistance SCRUB_ACTION_TAP_SLOP) owns quick
|
|
496
|
-
// gestures; only a held press becomes a scrub/adjust drag.
|
|
497
|
-
.minDistance(!hasScrubAction && Platform.OS === "android" ? 10 : 0)
|
|
498
|
-
.activateAfterLongPress(longPressMs)
|
|
499
505
|
.maxPointers(1)
|
|
500
506
|
.shouldCancelWhenOutside(false)
|
|
507
|
+
.onTouchesDown(
|
|
508
|
+
/* istanbul ignore next */ () => {
|
|
509
|
+
"worklet";
|
|
510
|
+
delayedPanTouchDown(longPressMs, fingerDown);
|
|
511
|
+
},
|
|
512
|
+
)
|
|
513
|
+
.onTouchesUp(
|
|
514
|
+
/* istanbul ignore next */ (e, manager) => {
|
|
515
|
+
"worklet";
|
|
516
|
+
delayedPanTouchUp(
|
|
517
|
+
longPressMs,
|
|
518
|
+
e,
|
|
519
|
+
manager,
|
|
520
|
+
fingerDown,
|
|
521
|
+
panActivated,
|
|
522
|
+
);
|
|
523
|
+
},
|
|
524
|
+
)
|
|
525
|
+
.onTouchesCancelled(
|
|
526
|
+
/* istanbul ignore next */ () => {
|
|
527
|
+
"worklet";
|
|
528
|
+
delayedPanTouchCancelled(longPressMs, fingerDown);
|
|
529
|
+
},
|
|
530
|
+
)
|
|
501
531
|
// Start scrubbing on ACTIVE (onStart), not on touch-down (onBegin):
|
|
502
532
|
// `activateAfterLongPress` only delays activation, so onBegin still fires
|
|
503
533
|
// immediately — using it would scrub instantly and ignore panGestureDelay,
|
|
@@ -505,6 +535,7 @@ export function useCrosshair(
|
|
|
505
535
|
.onStart(
|
|
506
536
|
/* istanbul ignore next */ (e) => {
|
|
507
537
|
"worklet";
|
|
538
|
+
if (!shouldStartDelayedPan(longPressMs, fingerDown, panActivated)) return;
|
|
508
539
|
if (!enabled) return;
|
|
509
540
|
// Scrub-action: once a reticle is placed, drag adjusts it (2D — the Y is
|
|
510
541
|
// the price). The ephemeral live-scrub never engages, so scrubActive
|
|
@@ -569,6 +600,7 @@ export function useCrosshair(
|
|
|
569
600
|
.onFinalize(
|
|
570
601
|
/* istanbul ignore next */ () => {
|
|
571
602
|
"worklet";
|
|
603
|
+
resetDelayedPanGuard(fingerDown, panActivated);
|
|
572
604
|
// A lock-adjust drag leaves the reticle in place (scrubActive was never
|
|
573
605
|
// set); a live-scrub clears its crosshair. Always clear scrubActive so a
|
|
574
606
|
// stray scrub can never linger behind a placed reticle.
|
|
@@ -583,11 +615,22 @@ export function useCrosshair(
|
|
|
583
615
|
},
|
|
584
616
|
);
|
|
585
617
|
|
|
586
|
-
|
|
587
|
-
|
|
618
|
+
// Passing zero-valued activation modifiers lets RNGH's iOS pan recognizer
|
|
619
|
+
// activate before the axis constraints classify the drag. Only configure a
|
|
620
|
+
// hold when one is requested, and reserve minDistance(0) for scrub-action's
|
|
621
|
+
// deliberate press-hold interaction.
|
|
622
|
+
if (longPressMs > 0) {
|
|
623
|
+
gesture = gesture.activateAfterLongPress(longPressMs);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (hasScrubAction) {
|
|
627
|
+
gesture = gesture.minDistance(0);
|
|
628
|
+
} else {
|
|
588
629
|
// Lock mode needs free vertical drag (Y = price), so the failOffsetY clamp —
|
|
589
630
|
// which would kill a vertical adjust — is applied only outside scrub-action.
|
|
590
|
-
gesture = gesture
|
|
631
|
+
gesture = gesture
|
|
632
|
+
.activeOffsetX([-SCRUB_ACTIVATE_X_PX, SCRUB_ACTIVATE_X_PX])
|
|
633
|
+
.failOffsetY([-SCRUB_FAIL_Y_PX, SCRUB_FAIL_Y_PX]);
|
|
591
634
|
}
|
|
592
635
|
|
|
593
636
|
// Axis-drag time-scroll: carve the bottom "time ruler" band out of the scrub's
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Platform } from "react-native";
|
|
2
1
|
import { Gesture } from "react-native-gesture-handler";
|
|
3
2
|
import {
|
|
4
3
|
useAnimatedReaction,
|
|
@@ -18,8 +17,17 @@ import {
|
|
|
18
17
|
computeScrubDotY,
|
|
19
18
|
computeScrubTime,
|
|
20
19
|
HIDDEN_TOOLTIP,
|
|
20
|
+
SCRUB_ACTIVATE_X_PX,
|
|
21
|
+
SCRUB_FAIL_Y_PX,
|
|
21
22
|
type CrosshairState,
|
|
22
23
|
} from "./crosshairShared";
|
|
24
|
+
import {
|
|
25
|
+
delayedPanTouchCancelled,
|
|
26
|
+
delayedPanTouchDown,
|
|
27
|
+
delayedPanTouchUp,
|
|
28
|
+
resetDelayedPanGuard,
|
|
29
|
+
shouldStartDelayedPan,
|
|
30
|
+
} from "./delayedPanGuard";
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
33
|
* LiveChartSeries crosshair + scrub. No tooltip — data is delivered via
|
|
@@ -40,6 +48,10 @@ export function useCrosshairSeries(
|
|
|
40
48
|
// Tracks whether the active scrub phase actually began, so a tap that never
|
|
41
49
|
// activates doesn't emit a spurious onGestureEnd.
|
|
42
50
|
const gestureStarted = useSharedValue(false);
|
|
51
|
+
// Mirrors LiveChart's guard against RNGH's delayed-pan timer activating after
|
|
52
|
+
// the final pointer has already lifted on iOS.
|
|
53
|
+
const fingerDown = useSharedValue(false);
|
|
54
|
+
const panActivated = useSharedValue(false);
|
|
43
55
|
|
|
44
56
|
const scrubTime = useDerivedValue(() =>
|
|
45
57
|
computeScrubTime(
|
|
@@ -154,10 +166,32 @@ export function useCrosshairSeries(
|
|
|
154
166
|
);
|
|
155
167
|
|
|
156
168
|
let gesture = Gesture.Pan()
|
|
157
|
-
.minDistance(Platform.OS === "android" ? 10 : 0)
|
|
158
|
-
.activateAfterLongPress(panGestureDelay)
|
|
159
169
|
.maxPointers(1)
|
|
160
170
|
.shouldCancelWhenOutside(false)
|
|
171
|
+
.onTouchesDown(
|
|
172
|
+
/* istanbul ignore next */ () => {
|
|
173
|
+
"worklet";
|
|
174
|
+
delayedPanTouchDown(panGestureDelay, fingerDown);
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
.onTouchesUp(
|
|
178
|
+
/* istanbul ignore next */ (e, manager) => {
|
|
179
|
+
"worklet";
|
|
180
|
+
delayedPanTouchUp(
|
|
181
|
+
panGestureDelay,
|
|
182
|
+
e,
|
|
183
|
+
manager,
|
|
184
|
+
fingerDown,
|
|
185
|
+
panActivated,
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
.onTouchesCancelled(
|
|
190
|
+
/* istanbul ignore next */ () => {
|
|
191
|
+
"worklet";
|
|
192
|
+
delayedPanTouchCancelled(panGestureDelay, fingerDown);
|
|
193
|
+
},
|
|
194
|
+
)
|
|
161
195
|
// Start scrubbing on ACTIVE (onStart), not on touch-down (onBegin):
|
|
162
196
|
// `activateAfterLongPress` only delays activation, so onBegin still fires
|
|
163
197
|
// immediately — using it would scrub instantly and ignore panGestureDelay,
|
|
@@ -165,6 +199,8 @@ export function useCrosshairSeries(
|
|
|
165
199
|
.onStart(
|
|
166
200
|
/* istanbul ignore next */ (e) => {
|
|
167
201
|
"worklet";
|
|
202
|
+
if (!shouldStartDelayedPan(panGestureDelay, fingerDown, panActivated))
|
|
203
|
+
return;
|
|
168
204
|
if (!enabled) return;
|
|
169
205
|
scrubX.set(e.x);
|
|
170
206
|
scrubActive.set(true);
|
|
@@ -182,6 +218,7 @@ export function useCrosshairSeries(
|
|
|
182
218
|
.onFinalize(
|
|
183
219
|
/* istanbul ignore next */ () => {
|
|
184
220
|
"worklet";
|
|
221
|
+
resetDelayedPanGuard(fingerDown, panActivated);
|
|
185
222
|
scrubActive.set(false);
|
|
186
223
|
if (gestureStarted.get()) {
|
|
187
224
|
gestureStarted.set(false);
|
|
@@ -190,11 +227,17 @@ export function useCrosshairSeries(
|
|
|
190
227
|
},
|
|
191
228
|
);
|
|
192
229
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
230
|
+
// Omit zero-valued activation modifiers so RNGH can classify direction before
|
|
231
|
+
// the pan activates on iOS. A vertical drag then remains available to a parent
|
|
232
|
+
// ScrollView/list, while a clear horizontal drag starts scrubbing.
|
|
233
|
+
if (panGestureDelay > 0) {
|
|
234
|
+
gesture = gesture.activateAfterLongPress(panGestureDelay);
|
|
196
235
|
}
|
|
197
236
|
|
|
237
|
+
gesture = gesture
|
|
238
|
+
.activeOffsetX([-SCRUB_ACTIVATE_X_PX, SCRUB_ACTIVATE_X_PX])
|
|
239
|
+
.failOffsetY([-SCRUB_FAIL_Y_PX, SCRUB_FAIL_Y_PX]);
|
|
240
|
+
|
|
198
241
|
return {
|
|
199
242
|
scrubX,
|
|
200
243
|
scrubActive,
|
|
@@ -8,30 +8,37 @@ import { drawSpline, makeSplineScratch } from "../math/spline";
|
|
|
8
8
|
import { usePathBuilders } from "./usePathBuilder";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* One derived shared value holding
|
|
12
|
-
*
|
|
11
|
+
* One derived shared value holding only the active series' Skia paths, capped at
|
|
12
|
+
* `MAX_MULTI_SERIES`.
|
|
13
13
|
*
|
|
14
14
|
* Each visible series' path is built into a per-slot `Skia.PathBuilder` (reused
|
|
15
15
|
* across frames via a SharedValue) and finalized with `detach()` — a fresh
|
|
16
16
|
* immutable `SkPath` per frame, so `MultiSeriesStroke`'s per-slot
|
|
17
17
|
* `useDerivedValue(() => paths.value[index])` repaints without the two-SkPath
|
|
18
|
-
* ping-pong. Only
|
|
18
|
+
* ping-pong. Only active series allocate and publish a path.
|
|
19
19
|
*/
|
|
20
20
|
export function useMultiSeriesLinePaths(
|
|
21
21
|
engine: MultiEngineState,
|
|
22
22
|
padding: ChartPadding,
|
|
23
|
+
activeSeriesCount = MAX_MULTI_SERIES,
|
|
23
24
|
): SharedValue<SkPath[]> {
|
|
24
25
|
const builders = usePathBuilders(MAX_MULTI_SERIES);
|
|
25
26
|
|
|
26
27
|
const poolRef = useRef<{
|
|
27
28
|
empty: SkPath;
|
|
28
29
|
ptsBuf: number[];
|
|
30
|
+
pathsA: SkPath[];
|
|
31
|
+
pathsB: SkPath[];
|
|
32
|
+
pathsTick: boolean;
|
|
29
33
|
scratch: ReturnType<typeof makeSplineScratch>;
|
|
30
34
|
} | null>(null);
|
|
31
35
|
if (poolRef.current === null) {
|
|
32
36
|
poolRef.current = {
|
|
33
37
|
empty: Skia.Path.Make(),
|
|
34
38
|
ptsBuf: [] as number[],
|
|
39
|
+
pathsA: [] as SkPath[],
|
|
40
|
+
pathsB: [] as SkPath[],
|
|
41
|
+
pathsTick: false,
|
|
35
42
|
scratch: makeSplineScratch(),
|
|
36
43
|
};
|
|
37
44
|
}
|
|
@@ -41,12 +48,11 @@ export function useMultiSeriesLinePaths(
|
|
|
41
48
|
const slots = builders.value;
|
|
42
49
|
const s = engine.series.get();
|
|
43
50
|
const displays = engine.displaySeriesValues.get();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
51
|
+
pool.pathsTick = !pool.pathsTick;
|
|
52
|
+
const out = pool.pathsTick ? pool.pathsA : pool.pathsB;
|
|
53
|
+
out.length = 0;
|
|
54
|
+
const count = Math.min(activeSeriesCount, s.length, MAX_MULTI_SERIES);
|
|
55
|
+
for (let i = 0; i < count; i++) {
|
|
50
56
|
const pts = buildLinePoints(
|
|
51
57
|
s[i].data,
|
|
52
58
|
displays[i] ?? s[i].value,
|
package/src/index.ts
CHANGED
package/src/math/squiggly.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function squigglyYAt(
|
|
|
31
31
|
* Build a flat [x0,y0,x1,y1,...] point array for a standalone squiggly line
|
|
32
32
|
* spanning the full chart area (used in loading / empty state).
|
|
33
33
|
* Points are spaced ~4px apart for smooth curves.
|
|
34
|
+
* Pass `out` to reuse a caller-owned frame scratch.
|
|
34
35
|
*/
|
|
35
36
|
export function buildSquigglyPts(
|
|
36
37
|
canvasWidth: number,
|
|
@@ -39,17 +40,22 @@ export function buildSquigglyPts(
|
|
|
39
40
|
t: number,
|
|
40
41
|
base = 14,
|
|
41
42
|
speed = 1,
|
|
43
|
+
out?: number[],
|
|
42
44
|
): number[] {
|
|
43
45
|
"worklet";
|
|
46
|
+
const pts = out ?? [];
|
|
44
47
|
const leftEdge = padding.left;
|
|
45
48
|
const rightEdge = canvasWidth - padding.right;
|
|
46
49
|
const chartW = rightEdge - leftEdge;
|
|
47
|
-
if (chartW <= 0 || canvasHeight <= 0)
|
|
50
|
+
if (chartW <= 0 || canvasHeight <= 0) {
|
|
51
|
+
pts.length = 0;
|
|
52
|
+
return pts;
|
|
53
|
+
}
|
|
48
54
|
|
|
49
55
|
const centerY = (canvasHeight - padding.bottom + padding.top) / 2;
|
|
50
56
|
const step = 4;
|
|
51
57
|
const count = Math.ceil(chartW / step) + 1;
|
|
52
|
-
|
|
58
|
+
pts.length = count * 2;
|
|
53
59
|
for (let i = 0; i < count; i++) {
|
|
54
60
|
const x = leftEdge + Math.min(i * step, chartW);
|
|
55
61
|
pts[i * 2] = x;
|
|
@@ -61,6 +67,7 @@ export function buildSquigglyPts(
|
|
|
61
67
|
/**
|
|
62
68
|
* Given a real flat-pts array (from buildLinePoints), replace each Y with the
|
|
63
69
|
* squiggly Y at the same X. Used during the morph reveal.
|
|
70
|
+
* Pass `out` to reuse a caller-owned frame scratch.
|
|
64
71
|
*/
|
|
65
72
|
export function squigglifyPts(
|
|
66
73
|
flatPts: number[],
|
|
@@ -68,15 +75,17 @@ export function squigglifyPts(
|
|
|
68
75
|
centerY: number,
|
|
69
76
|
base = 14,
|
|
70
77
|
speed = 1,
|
|
78
|
+
out?: number[],
|
|
71
79
|
): number[] {
|
|
72
80
|
"worklet";
|
|
73
81
|
const n = flatPts.length;
|
|
74
|
-
const out
|
|
82
|
+
const target = out ?? [];
|
|
83
|
+
target.length = n;
|
|
75
84
|
for (let i = 0; i < n; i += 2) {
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
target[i] = flatPts[i];
|
|
86
|
+
target[i + 1] = squigglyYAt(flatPts[i], centerY, t, base, speed);
|
|
78
87
|
}
|
|
79
|
-
return
|
|
88
|
+
return target;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
/**
|
|
@@ -97,6 +106,7 @@ export function smoothstep(t: number): number {
|
|
|
97
106
|
* distFromCentre = |x - chartCentreX| / (chartW / 2) // 0=centre, 1=edge
|
|
98
107
|
* weight = smoothstep((morphT - distFromCentre * 0.5) / 0.5)
|
|
99
108
|
* blendedY = fromY + (toY - fromY) * weight
|
|
109
|
+
* Pass `out` to reuse a caller-owned frame scratch.
|
|
100
110
|
*/
|
|
101
111
|
export function blendPtsY(
|
|
102
112
|
from: number[],
|
|
@@ -104,19 +114,26 @@ export function blendPtsY(
|
|
|
104
114
|
morphT: number,
|
|
105
115
|
padding: ChartPadding,
|
|
106
116
|
canvasWidth: number,
|
|
117
|
+
out?: number[],
|
|
107
118
|
): number[] {
|
|
108
119
|
"worklet";
|
|
109
120
|
const n = from.length;
|
|
110
|
-
if (n === 0) return to;
|
|
121
|
+
if (n === 0 && !out) return to;
|
|
111
122
|
const chartCentreX = (padding.left + canvasWidth - padding.right) / 2;
|
|
112
123
|
const halfChartW = (canvasWidth - padding.left - padding.right) / 2;
|
|
113
|
-
const out
|
|
124
|
+
const target = out ?? [];
|
|
125
|
+
const outputLength = n === 0 ? to.length : n;
|
|
126
|
+
target.length = outputLength;
|
|
127
|
+
if (n === 0) {
|
|
128
|
+
for (let i = 0; i < to.length; i++) target[i] = to[i];
|
|
129
|
+
return target;
|
|
130
|
+
}
|
|
114
131
|
for (let i = 0; i < n; i += 2) {
|
|
115
|
-
|
|
132
|
+
target[i] = to[i]; // X always from real pts
|
|
116
133
|
const distFromCentre =
|
|
117
134
|
halfChartW > 0 ? Math.abs(from[i] - chartCentreX) / halfChartW : 0;
|
|
118
135
|
const weight = smoothstep((morphT - distFromCentre * 0.5) / 0.5);
|
|
119
|
-
|
|
136
|
+
target[i + 1] = from[i + 1] + (to[i + 1] - from[i + 1]) * weight;
|
|
120
137
|
}
|
|
121
|
-
return
|
|
138
|
+
return target;
|
|
122
139
|
}
|
package/src/math/threshold.ts
CHANGED
|
@@ -12,8 +12,8 @@ import type { LiveChartPoint } from "../types";
|
|
|
12
12
|
* projected to this many evenly-spaced pixel-Y values across the plot, which the
|
|
13
13
|
* shader linearly interpolates between. ~one sample per 6px on a phone plot —
|
|
14
14
|
* fine enough that the line's crossing point is coloured accurately. The shader
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* selects between them through a balanced constant-index branch tree, so 64
|
|
16
|
+
* samples require at most six comparisons per covered fragment.
|
|
17
17
|
*/
|
|
18
18
|
export const THRESHOLD_SAMPLE_COUNT = 64;
|
|
19
19
|
|
package/src/types.ts
CHANGED
|
@@ -44,6 +44,10 @@ export type FontWeight =
|
|
|
44
44
|
/** Color scheme for the chart background, grid, and derived palette colors. */
|
|
45
45
|
export type ThemeMode = "light" | "dark";
|
|
46
46
|
|
|
47
|
+
/** Skia canvas composition mode. Android uses TextureView for `"transparent"`
|
|
48
|
+
* and an opaque SurfaceView for `"opaque"`. */
|
|
49
|
+
export type CanvasMode = "transparent" | "opaque";
|
|
50
|
+
|
|
47
51
|
/**
|
|
48
52
|
* Badge pill style.
|
|
49
53
|
* - `"default"` — accent-colored background with white text.
|
|
@@ -249,8 +253,9 @@ export interface ReferenceLineBadgeConfig extends BadgeStyleConfig {
|
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
/**
|
|
252
|
-
* Context passed to a custom {@link LiveChartProps.renderReferenceLine}
|
|
253
|
-
*
|
|
256
|
+
* Context passed to a custom {@link LiveChartProps.renderReferenceLine} or
|
|
257
|
+
* {@link LiveChartSeriesProps.renderReferenceLine}. The chart floats the element
|
|
258
|
+
* you return over the canvas and pins it to the line's value on
|
|
254
259
|
* the UI thread (vertically centered on the line, horizontally at the badge /
|
|
255
260
|
* label position) — so it tracks the rescaling axis and any drag smoothly without
|
|
256
261
|
* JS re-renders, just like {@link TooltipRenderProps}. Replaces the built-in pill
|
|
@@ -1122,7 +1127,8 @@ export interface TradeEvent {
|
|
|
1122
1127
|
}
|
|
1123
1128
|
|
|
1124
1129
|
/** Built-in marker glyph kinds drawn into the chart canvas. */
|
|
1125
|
-
export type MarkerKind =
|
|
1130
|
+
export type MarkerKind =
|
|
1131
|
+
"trade" | "boost" | "graduation" | "winner" | "clawback";
|
|
1126
1132
|
|
|
1127
1133
|
/**
|
|
1128
1134
|
* A marker rendered into the chart at `(time, y)`. Exactly one of `seriesId`
|
|
@@ -1236,10 +1242,15 @@ export interface MarkerClusterConfig {
|
|
|
1236
1242
|
* A dedicated badge drawn for a collapsed marker cluster — your own Skia design,
|
|
1237
1243
|
* independent of the member markers. Pass it as
|
|
1238
1244
|
* {@link MarkerClusterConfig.groupBadge}. Glyph precedence mirrors a single marker:
|
|
1239
|
-
* `image` → `icon` (`pill`) →
|
|
1240
|
-
*
|
|
1245
|
+
* `image` → `icon` (`pill`) → the built-in count badge. An object containing only
|
|
1246
|
+
* {@link letterSpacing} customizes that built-in count badge.
|
|
1241
1247
|
*/
|
|
1242
1248
|
export interface MarkerGroupBadge {
|
|
1249
|
+
/**
|
|
1250
|
+
* Extra spacing in px between digits in the built-in count badge. Also applies
|
|
1251
|
+
* to the optional corner count on a custom group badge. Default `0`.
|
|
1252
|
+
*/
|
|
1253
|
+
letterSpacing?: number;
|
|
1243
1254
|
/** Custom Skia image for the collapsed group (e.g. from `useImage`). Takes
|
|
1244
1255
|
* precedence over {@link icon}. */
|
|
1245
1256
|
image?: SkImage;
|
|
@@ -1743,6 +1754,18 @@ export interface LiveChartCoreProps {
|
|
|
1743
1754
|
insets?: ChartInsets;
|
|
1744
1755
|
/** Container View style. */
|
|
1745
1756
|
style?: ViewStyle;
|
|
1757
|
+
/**
|
|
1758
|
+
* Canvas composition mode. `"transparent"` keeps Skia's default compositing
|
|
1759
|
+
* path (TextureView on Android) and can reveal content behind the chart.
|
|
1760
|
+
* `"opaque"` makes the canvas own and paint its palette background; on Android
|
|
1761
|
+
* this selects SurfaceView and may reduce RenderThread work, but it cannot be
|
|
1762
|
+
* used when the chart must reveal content behind it. Default `"transparent"`.
|
|
1763
|
+
*
|
|
1764
|
+
* @experimental Profile on representative physical Android hardware before
|
|
1765
|
+
* enabling broadly; SurfaceView ordering, clipping, transforms, screenshots,
|
|
1766
|
+
* and parent scrolling should be validated in the consuming layout.
|
|
1767
|
+
*/
|
|
1768
|
+
canvasMode?: CanvasMode;
|
|
1746
1769
|
/** Visible time window in seconds. Default `30`. */
|
|
1747
1770
|
timeWindow?: number;
|
|
1748
1771
|
/** Freeze chart scrolling. Resume catches up to real time. Default `false`. */
|
|
@@ -2172,6 +2195,18 @@ export interface LiveChartProps extends LiveChartCoreProps {
|
|
|
2172
2195
|
export interface LiveChartSeriesProps extends LiveChartCoreProps {
|
|
2173
2196
|
/** Array of series definitions. Must be a SharedValue for UI-thread reads. */
|
|
2174
2197
|
series: SharedValue<SeriesConfig[]>;
|
|
2198
|
+
/**
|
|
2199
|
+
* Render a Form-A reference line's tag as a custom **React Native** element
|
|
2200
|
+
* instead of the built-in Skia pill / gutter label. The chart pins the returned
|
|
2201
|
+
* element to the line's live Y position on the UI thread (see
|
|
2202
|
+
* {@link ReferenceLineRenderProps}), including off-axis edge pinning. Return
|
|
2203
|
+
* `null`/`undefined` to keep that line's built-in tag. The line stroke always
|
|
2204
|
+
* remains visible, and `badge.position` / `labelPosition` controls the custom
|
|
2205
|
+
* tag's left, center, or right anchor.
|
|
2206
|
+
*/
|
|
2207
|
+
renderReferenceLine?: (
|
|
2208
|
+
ctx: ReferenceLineRenderProps,
|
|
2209
|
+
) => ReactElement | null | undefined;
|
|
2175
2210
|
/** Called when a series toggle chip is tapped. */
|
|
2176
2211
|
onSeriesToggle?: (id: string, visible: boolean) => void;
|
|
2177
2212
|
/**
|