react-native-livechart 4.22.1 → 4.23.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/dist/components/DotOverlay.d.ts +8 -1
- package/dist/components/DotOverlay.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/YAxisOverlay.d.ts +3 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +13 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +1 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +5 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +3 -1
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +1 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +3 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useTradeStream.d.ts +3 -1
- package/dist/hooks/useTradeStream.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/measureFontTextWidth.d.ts +10 -1
- package/dist/lib/measureFontTextWidth.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +9 -0
- package/dist/math/referenceDrag.d.ts.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/DotOverlay.tsx +36 -1
- package/src/components/LiveChart.tsx +26 -3
- package/src/components/LiveChartSeries.tsx +51 -23
- package/src/components/YAxisOverlay.tsx +16 -10
- package/src/core/liveChartEngineTick.ts +99 -6
- package/src/core/resolveConfig.ts +2 -0
- package/src/core/useLiveChartEngine.ts +206 -85
- package/src/hooks/useBadge.ts +7 -2
- package/src/hooks/useCandlePaths.ts +10 -8
- package/src/hooks/useDegen.ts +2 -0
- package/src/hooks/useMarkers.ts +3 -0
- package/src/hooks/useReferenceDrag.ts +12 -7
- package/src/hooks/useReferenceLine.ts +30 -4
- package/src/hooks/useTradeStream.ts +3 -0
- package/src/index.ts +1 -0
- package/src/lib/measureFontTextWidth.ts +26 -2
- package/src/math/referenceDrag.ts +19 -0
- package/src/types.ts +30 -0
|
@@ -38,6 +38,8 @@ export function useCandleWidthLerp(
|
|
|
38
38
|
autostart: boolean,
|
|
39
39
|
/** Only advance the displayed width while candle mode is active. */
|
|
40
40
|
active: boolean,
|
|
41
|
+
/** Runtime gate shared with the chart engine. */
|
|
42
|
+
isFrameLoopActive?: SharedValue<boolean>,
|
|
41
43
|
): SharedValue<number> {
|
|
42
44
|
const targetCandleWidth = useDerivedValue(() => candleWidthSecs);
|
|
43
45
|
const displayCandleWidth = useSharedValue(candleWidthSecs);
|
|
@@ -51,16 +53,16 @@ export function useCandleWidthLerp(
|
|
|
51
53
|
|
|
52
54
|
const widthFrameCallback = useFrameCallback((frameInfo) => {
|
|
53
55
|
"worklet";
|
|
54
|
-
if (!active) return;
|
|
56
|
+
if (!active || isFrameLoopActive?.get() === false) return;
|
|
55
57
|
const dt = frameInfo.timeSincePreviousFrame ?? MS_PER_FRAME_60FPS;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
),
|
|
58
|
+
const target = targetCandleWidth.get();
|
|
59
|
+
const next = lerp(
|
|
60
|
+
displayCandleWidth.get(),
|
|
61
|
+
target,
|
|
62
|
+
widthLerpSpeed.get(),
|
|
63
|
+
dt,
|
|
63
64
|
);
|
|
65
|
+
displayCandleWidth.set(Math.abs(target - next) < 0.01 ? target : next);
|
|
64
66
|
}, autostart);
|
|
65
67
|
useEffect(() => {
|
|
66
68
|
// `autostart` is an initial seed in Reanimated, not a reactive switch.
|
package/src/hooks/useDegen.ts
CHANGED
|
@@ -46,6 +46,7 @@ export function useDegen(
|
|
|
46
46
|
momentumSV: SharedValue<Momentum>,
|
|
47
47
|
cfg: ResolvedDegenConfig | null,
|
|
48
48
|
onShake?: (payload: DegenShakePayload) => void,
|
|
49
|
+
isFrameLoopActive?: SharedValue<boolean>,
|
|
49
50
|
): {
|
|
50
51
|
pack: SharedValue<Float64Array<ArrayBuffer>>;
|
|
51
52
|
packRevision: SharedValue<number>;
|
|
@@ -186,6 +187,7 @@ export function useDegen(
|
|
|
186
187
|
useFrameCallback(
|
|
187
188
|
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
188
189
|
"worklet";
|
|
190
|
+
if (isFrameLoopActive?.get() === false) return;
|
|
189
191
|
const now = engine.timestamp.get();
|
|
190
192
|
const buf = pack.get();
|
|
191
193
|
const slots = slotCountSV.get();
|
package/src/hooks/useMarkers.ts
CHANGED
|
@@ -55,6 +55,8 @@ export function useMarkers(
|
|
|
55
55
|
lineLinear = false,
|
|
56
56
|
/** Collision config; default `"anchored"` (no stacking). */
|
|
57
57
|
cluster: ResolvedMarkerCluster = ANCHORED_CLUSTER,
|
|
58
|
+
/** Runtime gate shared with the chart engine. */
|
|
59
|
+
isFrameLoopActive?: SharedValue<boolean>,
|
|
58
60
|
): {
|
|
59
61
|
projected: SharedValue<ProjectedMarker[]>;
|
|
60
62
|
tapGesture: ReturnType<typeof Gesture.Tap>;
|
|
@@ -80,6 +82,7 @@ export function useMarkers(
|
|
|
80
82
|
const markerFrameCallback = useFrameCallback(
|
|
81
83
|
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
82
84
|
"worklet";
|
|
85
|
+
if (isFrameLoopActive?.get() === false) return;
|
|
83
86
|
const cache = cacheRef.current!;
|
|
84
87
|
if (!active) {
|
|
85
88
|
if (projected.get().length > 0) projected.set([]);
|
|
@@ -12,6 +12,7 @@ import type { ChartPadding } from "../draw/line";
|
|
|
12
12
|
import {
|
|
13
13
|
clampToBounds,
|
|
14
14
|
nearestDraggableIndex,
|
|
15
|
+
referenceDragOwnsTouch,
|
|
15
16
|
referenceValueOut,
|
|
16
17
|
resolveDragIntent,
|
|
17
18
|
} from "../math/referenceDrag";
|
|
@@ -257,17 +258,21 @@ export function useReferenceDrag(
|
|
|
257
258
|
activated.set(false);
|
|
258
259
|
};
|
|
259
260
|
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
261
|
+
// Hit-test shared with the scrub gesture: does this drag own the touch? The
|
|
262
|
+
// scrub's `onStart` consults it to bail, so it never drops a crosshair on a
|
|
263
|
+
// line even though it activates independently of this manual-activation pan
|
|
264
|
+
// (the `Exclusive` priority alone doesn't hold scrub back — not while this
|
|
265
|
+
// gesture is merely pressed, and not while it is dragging either). Once a line
|
|
266
|
+
// is grabbed the answer is yes regardless of position: the scrub asks with its
|
|
267
|
+
// touch-DOWN point, which a longer drag has carried the line away from, so the
|
|
268
|
+
// geometric test alone said "no line here" and a crosshair opened mid-drag.
|
|
269
|
+
// Otherwise it is the y-reach around the handles (x is unused — a Form-A line
|
|
270
|
+
// spans the full width).
|
|
266
271
|
/* istanbul ignore next -- worklet, runs on the UI thread */
|
|
267
272
|
const hitTest = (_x: number, y: number): boolean => {
|
|
268
273
|
"worklet";
|
|
269
274
|
if (!anyDraggable) return false;
|
|
270
|
-
return
|
|
275
|
+
return referenceDragOwnsTouch(dragIndex.get(), handleYs.get(), y, GRAB_SLOP);
|
|
271
276
|
};
|
|
272
277
|
|
|
273
278
|
const gesture = Gesture.Pan()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
|
|
3
4
|
import type { SkFont } from "@shopify/react-native-skia";
|
|
4
5
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
@@ -7,7 +8,10 @@ import {
|
|
|
7
8
|
type YAxisEntry,
|
|
8
9
|
} from "../draw/grid";
|
|
9
10
|
import type { ChartPadding } from "../draw/line";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
measureFontTextWidth,
|
|
13
|
+
type TextWidthCache,
|
|
14
|
+
} from "../lib/measureFontTextWidth";
|
|
11
15
|
import {
|
|
12
16
|
classifyReferenceEdge,
|
|
13
17
|
referenceLineForm,
|
|
@@ -120,6 +124,7 @@ function badgeGeometry(
|
|
|
120
124
|
x1: number,
|
|
121
125
|
x2: number,
|
|
122
126
|
font: SkFont,
|
|
127
|
+
textWidthCache?: TextWidthCache,
|
|
123
128
|
): BadgeGeometry {
|
|
124
129
|
"worklet";
|
|
125
130
|
// Measure the icon's visual bounds (not just width): `SkiaText` draws from the
|
|
@@ -129,7 +134,9 @@ function badgeGeometry(
|
|
|
129
134
|
// price-pill centering in computeActionBadgeLayout.
|
|
130
135
|
const iconBounds = icon ? font.measureText(icon) : null;
|
|
131
136
|
const iconW = iconBounds ? iconBounds.width : 0;
|
|
132
|
-
const textW = text
|
|
137
|
+
const textW = text
|
|
138
|
+
? measureFontTextWidth(font, text, textWidthCache)
|
|
139
|
+
: 0;
|
|
133
140
|
|
|
134
141
|
let contentW = 0;
|
|
135
142
|
let count = 0;
|
|
@@ -310,6 +317,7 @@ export function useReferenceLine(
|
|
|
310
317
|
const form = line ? referenceLineForm(line) : "none";
|
|
311
318
|
// Badge presentation depends only on the (stable) line props — resolve once.
|
|
312
319
|
const badge = line ? resolveReferenceBadge(line) : null;
|
|
320
|
+
const textWidthCacheRef = useRef<TextWidthCache>({});
|
|
313
321
|
|
|
314
322
|
return useDerivedValue<ReferenceLineLayout>(() => {
|
|
315
323
|
if (!line || form === "none") return INVISIBLE;
|
|
@@ -447,7 +455,16 @@ export function useReferenceLine(
|
|
|
447
455
|
? chartTop + OFF_AXIS_EDGE_INSET
|
|
448
456
|
: chartBottom - OFF_AXIS_EDGE_INSET;
|
|
449
457
|
const text = referenceBadgeText(line, badge, v, formatValue, true);
|
|
450
|
-
const g = badgeGeometry(
|
|
458
|
+
const g = badgeGeometry(
|
|
459
|
+
badge.position,
|
|
460
|
+
badge.icon,
|
|
461
|
+
text,
|
|
462
|
+
true,
|
|
463
|
+
x1,
|
|
464
|
+
x2,
|
|
465
|
+
font,
|
|
466
|
+
textWidthCacheRef.current,
|
|
467
|
+
);
|
|
451
468
|
return {
|
|
452
469
|
...INVISIBLE,
|
|
453
470
|
visible: true,
|
|
@@ -476,7 +493,16 @@ export function useReferenceLine(
|
|
|
476
493
|
// In-range pill badge (the `badge` config, not the legacy off-axis-only flag).
|
|
477
494
|
if (badge && badge.inRange) {
|
|
478
495
|
const text = referenceBadgeText(line, badge, v, formatValue, false);
|
|
479
|
-
const g = badgeGeometry(
|
|
496
|
+
const g = badgeGeometry(
|
|
497
|
+
badge.position,
|
|
498
|
+
badge.icon,
|
|
499
|
+
text,
|
|
500
|
+
false,
|
|
501
|
+
x1,
|
|
502
|
+
x2,
|
|
503
|
+
font,
|
|
504
|
+
textWidthCacheRef.current,
|
|
505
|
+
);
|
|
480
506
|
return {
|
|
481
507
|
...INVISIBLE,
|
|
482
508
|
visible: true,
|
|
@@ -28,6 +28,8 @@ export function useTradeStream(
|
|
|
28
28
|
active: boolean,
|
|
29
29
|
/** Static charts run no loops: register without starting. Default `true`. */
|
|
30
30
|
autostart = true,
|
|
31
|
+
/** Runtime gate shared with the chart engine. */
|
|
32
|
+
isFrameLoopActive?: SharedValue<boolean>,
|
|
31
33
|
): SharedValue<TradeMarker[]> {
|
|
32
34
|
const markers = useSharedValue<TradeMarker[]>([]);
|
|
33
35
|
const state = useSharedValue<TradeStreamState>(createTradeStreamState());
|
|
@@ -37,6 +39,7 @@ export function useTradeStream(
|
|
|
37
39
|
frameInfo,
|
|
38
40
|
) => {
|
|
39
41
|
"worklet";
|
|
42
|
+
if (isFrameLoopActive?.get() === false) return;
|
|
40
43
|
if (!active) {
|
|
41
44
|
if (markers.get().length > 0) markers.set([]);
|
|
42
45
|
return;
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import type { SkFont } from "@shopify/react-native-skia";
|
|
2
2
|
|
|
3
|
+
/** Caller-owned last-value cache for repeated UI-thread text measurement. */
|
|
4
|
+
export interface TextWidthCache {
|
|
5
|
+
/** Font used for the cached measurement. */
|
|
6
|
+
font?: SkFont;
|
|
7
|
+
/** Text used for the cached measurement. */
|
|
8
|
+
text?: string;
|
|
9
|
+
/** Cached width. */
|
|
10
|
+
width?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
/**
|
|
4
14
|
* Horizontal layout width for `text` via Skia `measureText` (replaces deprecated `getTextWidth`).
|
|
5
15
|
* Marked worklet for use inside Reanimated `useDerivedValue`.
|
|
6
16
|
*/
|
|
7
|
-
export function measureFontTextWidth(
|
|
17
|
+
export function measureFontTextWidth(
|
|
18
|
+
font: SkFont,
|
|
19
|
+
text: string,
|
|
20
|
+
cache?: TextWidthCache,
|
|
21
|
+
): number {
|
|
8
22
|
"worklet";
|
|
9
|
-
|
|
23
|
+
if (cache?.font === font && cache.text === text && cache.width !== undefined) {
|
|
24
|
+
return cache.width;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const width = font.measureText(text).width;
|
|
28
|
+
if (cache) {
|
|
29
|
+
cache.font = font;
|
|
30
|
+
cache.text = text;
|
|
31
|
+
cache.width = width;
|
|
32
|
+
}
|
|
33
|
+
return width;
|
|
10
34
|
}
|
|
@@ -43,6 +43,25 @@ export function nearestDraggableIndex(
|
|
|
43
43
|
return best;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Whether the reference-drag gesture owns the current touch — the answer the
|
|
48
|
+
* scrub's activation guard needs. A **grabbed** line (`dragIndex >= 0`, pressed
|
|
49
|
+
* or already dragging) owns the touch outright, wherever the asking gesture says
|
|
50
|
+
* the finger is: the scrub pan can activate mid-drag and asks with its
|
|
51
|
+
* touch-DOWN point, which a longer drag has carried the line away from. With no
|
|
52
|
+
* line grabbed, it is the geometric reach test around the handles.
|
|
53
|
+
*/
|
|
54
|
+
export function referenceDragOwnsTouch(
|
|
55
|
+
dragIndex: number,
|
|
56
|
+
handleYs: number[],
|
|
57
|
+
y: number,
|
|
58
|
+
slop: number,
|
|
59
|
+
): boolean {
|
|
60
|
+
"worklet";
|
|
61
|
+
if (dragIndex >= 0) return true;
|
|
62
|
+
return nearestDraggableIndex(handleYs, y, slop) >= 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
/**
|
|
47
66
|
* Resolve a grabbed line's drag intent from the touch travel (px) since the grab.
|
|
48
67
|
* Once a line is grabbed it **owns** the touch: any drag past `threshold` px in
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,16 @@ export interface LiveChartPoint {
|
|
|
16
16
|
value: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Development counters for continuous engine-frame state updates. */
|
|
20
|
+
export interface LiveChartFrameStats {
|
|
21
|
+
/** Frame callbacks that ran while the chart was live. */
|
|
22
|
+
frames: number;
|
|
23
|
+
/** Frames that changed at least one tracked engine value. */
|
|
24
|
+
published: number;
|
|
25
|
+
/** Frames whose tracked engine values were unchanged. */
|
|
26
|
+
skipped: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
/** Direction of recent price movement, used for dot/badge coloring and degen effects. */
|
|
20
30
|
export type Momentum = "up" | "down" | "flat";
|
|
21
31
|
|
|
@@ -707,6 +717,9 @@ export interface VolumeConfig {
|
|
|
707
717
|
|
|
708
718
|
/** Y-axis grid configuration. */
|
|
709
719
|
export interface YAxisConfig {
|
|
720
|
+
/** Price-label side. For left placement, reserve the label gutter with insets.left.
|
|
721
|
+
* Right-side float/labelRightMargin options do not affect left labels. Default "right". */
|
|
722
|
+
side?: "left" | "right";
|
|
710
723
|
/** Minimum pixel gap between grid lines. Default `36`. */
|
|
711
724
|
minGap?: number;
|
|
712
725
|
/**
|
|
@@ -2664,6 +2677,23 @@ export interface LiveChartProps extends LiveChartCoreProps {
|
|
|
2664
2677
|
* switching back to live restarts the suspended loops and catches up. Frame the data
|
|
2665
2678
|
* with `timeWindow` + `nowOverride` (see the historical-data-fill pattern). */
|
|
2666
2679
|
static?: boolean;
|
|
2680
|
+
/**
|
|
2681
|
+
* Runtime gate for continuous frame work. Set this SharedValue to `false`
|
|
2682
|
+
* while a parent scroll gesture needs the UI thread; data keeps updating and
|
|
2683
|
+
* the chart catches up when it becomes `true`. Unlike {@link static}, this
|
|
2684
|
+
* keeps pan, zoom, scrub, and draggable reference-line gestures mounted.
|
|
2685
|
+
*
|
|
2686
|
+
* @experimental
|
|
2687
|
+
*/
|
|
2688
|
+
isFrameLoopActive?: SharedValue<boolean>;
|
|
2689
|
+
/**
|
|
2690
|
+
* Optional development counter for engine frames that did or did not change
|
|
2691
|
+
* tracked engine values. It is not a count of Skia redraws. Supplying it adds
|
|
2692
|
+
* one SharedValue write per active engine frame.
|
|
2693
|
+
*
|
|
2694
|
+
* @experimental
|
|
2695
|
+
*/
|
|
2696
|
+
debugFrameStats?: SharedValue<LiveChartFrameStats>;
|
|
2667
2697
|
/**
|
|
2668
2698
|
* Render a custom overlay floated over the chart canvas, handed a price↔pixel /
|
|
2669
2699
|
* time↔pixel {@link ChartOverlayContext} so it can track the auto-rescaling axis
|