react-native-livechart 4.22.2 → 4.24.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/CrosshairLine.d.ts +3 -2
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +3 -2
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- 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/SelectionDot.d.ts +6 -3
- package/dist/components/SelectionDot.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 +2 -1
- 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/useChartColors.d.ts +5 -3
- package/dist/hooks/useChartColors.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 +53 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +4 -2
- package/src/components/CrosshairOverlay.tsx +5 -3
- package/src/components/DotOverlay.tsx +36 -1
- package/src/components/LiveChart.tsx +39 -6
- package/src/components/LiveChartSeries.tsx +51 -23
- package/src/components/SelectionDot.tsx +7 -3
- package/src/components/YAxisOverlay.tsx +16 -10
- package/src/core/liveChartEngineTick.ts +99 -6
- package/src/core/resolveConfig.ts +3 -1
- package/src/core/useLiveChartEngine.ts +114 -41
- package/src/hooks/useBadge.ts +7 -2
- package/src/hooks/useCandlePaths.ts +10 -8
- package/src/hooks/useChartColors.ts +27 -13
- 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 +54 -6
|
@@ -18,7 +18,7 @@ export function DefaultSelectionDot({
|
|
|
18
18
|
x: SharedValue<number>;
|
|
19
19
|
y: SharedValue<number>;
|
|
20
20
|
opacity: SharedValue<number>;
|
|
21
|
-
color: string
|
|
21
|
+
color: string | SharedValue<string>;
|
|
22
22
|
size: number;
|
|
23
23
|
ring: ResolvedSelectionDotConfig["ring"];
|
|
24
24
|
}) {
|
|
@@ -56,13 +56,17 @@ export function SelectionDotSlot({
|
|
|
56
56
|
active,
|
|
57
57
|
opacity,
|
|
58
58
|
color,
|
|
59
|
+
plainColor,
|
|
59
60
|
}: {
|
|
60
61
|
config?: ResolvedSelectionDotConfig | null;
|
|
61
62
|
x: SharedValue<number>;
|
|
62
63
|
y?: SharedValue<number>;
|
|
63
64
|
active?: SharedValue<number> | SharedValue<boolean>;
|
|
64
65
|
opacity: SharedValue<number>;
|
|
65
|
-
color: string
|
|
66
|
+
color: string | SharedValue<string>;
|
|
67
|
+
/** Plain-string color for a custom `component`, whose `color` prop is a
|
|
68
|
+
* string, used when `color` is animated. */
|
|
69
|
+
plainColor: string;
|
|
66
70
|
}) {
|
|
67
71
|
if (!config || y === undefined || !active) return null;
|
|
68
72
|
const dotColor = config.color ?? color;
|
|
@@ -74,7 +78,7 @@ export function SelectionDotSlot({
|
|
|
74
78
|
y={y}
|
|
75
79
|
active={active as SharedValue<boolean>}
|
|
76
80
|
opacity={opacity}
|
|
77
|
-
color={dotColor}
|
|
81
|
+
color={typeof dotColor === "string" ? dotColor : plainColor}
|
|
78
82
|
size={config.size}
|
|
79
83
|
/>
|
|
80
84
|
);
|
|
@@ -60,6 +60,7 @@ export function YAxisOverlay({
|
|
|
60
60
|
gridStyle,
|
|
61
61
|
variant = "all",
|
|
62
62
|
float = false,
|
|
63
|
+
side = "right",
|
|
63
64
|
labelRightMargin,
|
|
64
65
|
gridEndGap = 0,
|
|
65
66
|
}: {
|
|
@@ -97,6 +98,8 @@ export function YAxisOverlay({
|
|
|
97
98
|
* plot) instead of centering them in a reserved gutter. See {@link YAxisConfig.float}.
|
|
98
99
|
*/
|
|
99
100
|
float?: boolean;
|
|
101
|
+
/** Render labels in the caller-reserved left gutter without moving the grid. */
|
|
102
|
+
side?: "left" | "right";
|
|
100
103
|
/** Fixed canvas-edge margin for a shared left-aligned label column. */
|
|
101
104
|
labelRightMargin?: number;
|
|
102
105
|
/** Gap between the grid-line end and the shared label column. */
|
|
@@ -109,7 +112,7 @@ export function YAxisOverlay({
|
|
|
109
112
|
const gridBuilder = usePathBuilder();
|
|
110
113
|
|
|
111
114
|
const rightAnchoredColumn = useDerivedValue(() => {
|
|
112
|
-
if (labelRightMargin === undefined) return null;
|
|
115
|
+
if (side === "left" || labelRightMargin === undefined) return null;
|
|
113
116
|
return rightAnchoredYAxisColumnLayout(
|
|
114
117
|
engine.canvasWidth.get(),
|
|
115
118
|
entries.get(),
|
|
@@ -156,20 +159,23 @@ export function YAxisOverlay({
|
|
|
156
159
|
const e = items[i];
|
|
157
160
|
const textW = measureFontTextWidth(font, e.label);
|
|
158
161
|
const x =
|
|
159
|
-
|
|
160
|
-
?
|
|
161
|
-
:
|
|
162
|
-
?
|
|
163
|
-
:
|
|
164
|
-
?
|
|
165
|
-
:
|
|
166
|
-
?
|
|
167
|
-
:
|
|
162
|
+
side === "left"
|
|
163
|
+
? (padding.left - textW) / 2
|
|
164
|
+
: columnX !== undefined
|
|
165
|
+
? columnX
|
|
166
|
+
: float
|
|
167
|
+
? gutterRightAlignedTextLeftX(w, textW, FLOAT_LABEL_RIGHT_MARGIN)
|
|
168
|
+
: badge
|
|
169
|
+
? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
|
|
170
|
+
: seriesLabelInset > 0
|
|
171
|
+
? gutterRightAlignedTextLeftX(w, textW)
|
|
172
|
+
: gutterCenteredTextLeftX(w, padding.right, textW);
|
|
168
173
|
result.push({
|
|
169
174
|
x,
|
|
170
175
|
y: e.y - baselineOffset,
|
|
171
176
|
label: e.label,
|
|
172
177
|
alpha:
|
|
178
|
+
side !== "left" &&
|
|
173
179
|
resolvedBadgeCenterY !== null &&
|
|
174
180
|
yAxisLabelIntersectsBadge(
|
|
175
181
|
e.y,
|
|
@@ -41,6 +41,10 @@ export interface EngineTickMutable {
|
|
|
41
41
|
extremaMaxTime: number;
|
|
42
42
|
/** Previous frame's yRangeScale — detects an in-flight scale drag. */
|
|
43
43
|
lastYRangeScale?: number;
|
|
44
|
+
/** Canvas width observed by the previous measured tick. */
|
|
45
|
+
lastCanvasWidth?: number;
|
|
46
|
+
/** Canvas height observed by the previous measured tick. */
|
|
47
|
+
lastCanvasHeight?: number;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export interface EngineTickInput {
|
|
@@ -142,6 +146,40 @@ export interface EngineTickInput {
|
|
|
142
146
|
candleGapBridgeUnknown?: boolean;
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
const SUBPIXEL_SETTLE_THRESHOLD = 0.5;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Lerps toward a target, then lands exactly once the remaining distance is
|
|
153
|
+
* smaller than half a rendered pixel.
|
|
154
|
+
*/
|
|
155
|
+
export function lerpAndSettle(
|
|
156
|
+
current: number,
|
|
157
|
+
target: number,
|
|
158
|
+
speed: number,
|
|
159
|
+
dt: number,
|
|
160
|
+
unitsPerPixel: number,
|
|
161
|
+
): number {
|
|
162
|
+
"worklet";
|
|
163
|
+
const next = lerp(current, target, speed, dt);
|
|
164
|
+
const epsilon = Math.abs(unitsPerPixel) * SUBPIXEL_SETTLE_THRESHOLD;
|
|
165
|
+
return epsilon > 0 && Math.abs(target - next) <= epsilon ? target : next;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Advances a live timestamp only after the chart has moved by half a pixel.
|
|
170
|
+
*/
|
|
171
|
+
export function advanceTimestampByPixel(
|
|
172
|
+
current: number,
|
|
173
|
+
target: number,
|
|
174
|
+
secondsPerPixel: number,
|
|
175
|
+
): number {
|
|
176
|
+
"worklet";
|
|
177
|
+
const step = Math.abs(secondsPerPixel) * SUBPIXEL_SETTLE_THRESHOLD;
|
|
178
|
+
if (!(step > 0) || !Number.isFinite(step) || target <= current) return target;
|
|
179
|
+
const elapsedSteps = Math.floor((target - current) / step);
|
|
180
|
+
return elapsedSteps > 0 ? current + elapsedSteps * step : current;
|
|
181
|
+
}
|
|
182
|
+
|
|
145
183
|
/**
|
|
146
184
|
* One frame of the live chart engine (mirrors `useLiveChartEngine` worklet body).
|
|
147
185
|
* Mutates `state` in place for testability and reuse from the hook.
|
|
@@ -153,7 +191,6 @@ export function tickLiveChartEngineFrame(
|
|
|
153
191
|
"worklet";
|
|
154
192
|
const baseNow = input.nowOverride ?? input.nowSeconds ?? Date.now() / 1000;
|
|
155
193
|
const liveEdge = baseNow + (input.windowBuffer ?? 0) * input.timeWindow;
|
|
156
|
-
state.liveEdge = liveEdge;
|
|
157
194
|
const viewEnd = input.viewEnd;
|
|
158
195
|
// First time of the visible series' data — the floor a frozen edge must stay
|
|
159
196
|
// at or above. `-Infinity` when there's no data to bound against, so the freeze
|
|
@@ -177,6 +214,17 @@ export function tickLiveChartEngineFrame(
|
|
|
177
214
|
viewEnd != null &&
|
|
178
215
|
viewEnd >= firstDataTime &&
|
|
179
216
|
(viewEnd < liveEdge || input.allowFutureViewEnd === true);
|
|
217
|
+
const canvasChanged =
|
|
218
|
+
state.lastCanvasWidth !== input.canvasWidth ||
|
|
219
|
+
state.lastCanvasHeight !== input.canvasHeight;
|
|
220
|
+
state.liveEdge =
|
|
221
|
+
!canvasChanged && input.canvasWidth > 0 && input.timeWindow > 0
|
|
222
|
+
? advanceTimestampByPixel(
|
|
223
|
+
state.liveEdge,
|
|
224
|
+
liveEdge,
|
|
225
|
+
input.timeWindow / input.canvasWidth,
|
|
226
|
+
)
|
|
227
|
+
: liveEdge;
|
|
180
228
|
if (scrolledBack) {
|
|
181
229
|
state.timestamp = viewEnd;
|
|
182
230
|
} else if (!input.paused) {
|
|
@@ -188,6 +236,16 @@ export function tickLiveChartEngineFrame(
|
|
|
188
236
|
if (returnT != null && returnT < 1 && input.returnFrom != null) {
|
|
189
237
|
state.timestamp =
|
|
190
238
|
input.returnFrom + (liveEdge - input.returnFrom) * returnT;
|
|
239
|
+
} else if (
|
|
240
|
+
!canvasChanged &&
|
|
241
|
+
input.canvasWidth > 0 &&
|
|
242
|
+
input.timeWindow > 0
|
|
243
|
+
) {
|
|
244
|
+
state.timestamp = advanceTimestampByPixel(
|
|
245
|
+
state.timestamp,
|
|
246
|
+
liveEdge,
|
|
247
|
+
input.timeWindow / input.canvasWidth,
|
|
248
|
+
);
|
|
191
249
|
} else {
|
|
192
250
|
state.timestamp = liveEdge;
|
|
193
251
|
}
|
|
@@ -195,6 +253,8 @@ export function tickLiveChartEngineFrame(
|
|
|
195
253
|
// else: paused with no active pan → leave the frozen timestamp untouched.
|
|
196
254
|
|
|
197
255
|
if (input.canvasWidth === 0 || input.canvasHeight === 0) return;
|
|
256
|
+
state.lastCanvasWidth = input.canvasWidth;
|
|
257
|
+
state.lastCanvasHeight = input.canvasHeight;
|
|
198
258
|
|
|
199
259
|
const speed = input.smoothing;
|
|
200
260
|
// One-shot settle (snapKey change): collapse this frame's easing so the
|
|
@@ -214,16 +274,30 @@ export function tickLiveChartEngineFrame(
|
|
|
214
274
|
(1 - gapRatio) *
|
|
215
275
|
(input.adaptiveSpeedBoost ?? MOTION_METRICS_DEFAULTS.adaptiveSpeedBoost);
|
|
216
276
|
|
|
277
|
+
const valueUnitsPerPixel =
|
|
278
|
+
(state.displayMax - state.displayMin) / input.canvasHeight;
|
|
217
279
|
state.displayValue = snap
|
|
218
280
|
? target
|
|
219
|
-
:
|
|
281
|
+
: lerpAndSettle(
|
|
282
|
+
state.displayValue,
|
|
283
|
+
target,
|
|
284
|
+
adaptiveSpeed,
|
|
285
|
+
input.dt,
|
|
286
|
+
valueUnitsPerPixel,
|
|
287
|
+
);
|
|
220
288
|
|
|
221
289
|
// Pinch-zoom: ease toward the zoom override when set, else the configured
|
|
222
290
|
// window. Mirrors the viewEnd freeze above (width vs. right edge).
|
|
223
291
|
const targetWindow = input.viewWindow ?? input.timeWindow;
|
|
224
292
|
state.displayWindow = snap
|
|
225
293
|
? targetWindow
|
|
226
|
-
:
|
|
294
|
+
: lerpAndSettle(
|
|
295
|
+
state.displayWindow,
|
|
296
|
+
targetWindow,
|
|
297
|
+
speed,
|
|
298
|
+
input.dt,
|
|
299
|
+
targetWindow / input.canvasWidth,
|
|
300
|
+
);
|
|
227
301
|
|
|
228
302
|
const winStart = state.timestamp - state.displayWindow;
|
|
229
303
|
|
|
@@ -437,16 +511,29 @@ export function tickLiveChartEngineFrame(
|
|
|
437
511
|
const maxV = input.maxValue;
|
|
438
512
|
if (maxV !== undefined && tMax > maxV) tMax = maxV;
|
|
439
513
|
|
|
514
|
+
const rangeUnitsPerPixel = (tMax - tMin) / input.canvasHeight;
|
|
440
515
|
if (snap || yScaleDragging || tMin < state.displayMin) {
|
|
441
516
|
state.displayMin = tMin;
|
|
442
517
|
} else {
|
|
443
|
-
state.displayMin =
|
|
518
|
+
state.displayMin = lerpAndSettle(
|
|
519
|
+
state.displayMin,
|
|
520
|
+
tMin,
|
|
521
|
+
speed,
|
|
522
|
+
input.dt,
|
|
523
|
+
rangeUnitsPerPixel,
|
|
524
|
+
);
|
|
444
525
|
}
|
|
445
526
|
|
|
446
527
|
if (snap || yScaleDragging || tMax > state.displayMax) {
|
|
447
528
|
state.displayMax = tMax;
|
|
448
529
|
} else {
|
|
449
|
-
state.displayMax =
|
|
530
|
+
state.displayMax = lerpAndSettle(
|
|
531
|
+
state.displayMax,
|
|
532
|
+
tMax,
|
|
533
|
+
speed,
|
|
534
|
+
input.dt,
|
|
535
|
+
rangeUnitsPerPixel,
|
|
536
|
+
);
|
|
450
537
|
}
|
|
451
538
|
}
|
|
452
539
|
|
|
@@ -485,6 +572,12 @@ export function tickLiveChartEngineFrame(
|
|
|
485
572
|
}
|
|
486
573
|
state.edgeValue = snap
|
|
487
574
|
? edgeTarget
|
|
488
|
-
:
|
|
575
|
+
: lerpAndSettle(
|
|
576
|
+
state.edgeValue,
|
|
577
|
+
edgeTarget,
|
|
578
|
+
speed,
|
|
579
|
+
input.dt,
|
|
580
|
+
valueUnitsPerPixel,
|
|
581
|
+
);
|
|
489
582
|
}
|
|
490
583
|
}
|
|
@@ -94,6 +94,7 @@ export interface ResolvedBadgeConfig {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export interface ResolvedYAxisConfig {
|
|
97
|
+
side: "left" | "right";
|
|
97
98
|
minGap: number;
|
|
98
99
|
/** Multiplier used to choose and align representable dynamic nice intervals. */
|
|
99
100
|
intervalScale: number;
|
|
@@ -295,7 +296,7 @@ export interface ResolvedGradientConfig {
|
|
|
295
296
|
/** undefined → use palette.fillBottom (transparent) at render time */
|
|
296
297
|
bottomOpacity: number | undefined;
|
|
297
298
|
/** Explicit color stops (top → bottom); overrides the opacity stops. */
|
|
298
|
-
colors: string[] | undefined;
|
|
299
|
+
colors: string[] | SharedValue<string[]> | undefined;
|
|
299
300
|
/** Stop positions (0..1) matching `colors` length. */
|
|
300
301
|
positions: number[] | undefined;
|
|
301
302
|
}
|
|
@@ -531,6 +532,7 @@ export function resolveBadge(
|
|
|
531
532
|
}
|
|
532
533
|
|
|
533
534
|
const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
|
|
535
|
+
side: "right",
|
|
534
536
|
minGap: 36,
|
|
535
537
|
intervalScale: 1,
|
|
536
538
|
count: 0,
|
|
@@ -20,6 +20,7 @@ import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
|
|
|
20
20
|
import type {
|
|
21
21
|
CandleGap,
|
|
22
22
|
CandlePoint,
|
|
23
|
+
LiveChartFrameStats,
|
|
23
24
|
LiveChartPoint,
|
|
24
25
|
SeriesConfig,
|
|
25
26
|
} from "../types";
|
|
@@ -93,6 +94,10 @@ export interface EngineConfig {
|
|
|
93
94
|
* {@link LiveChartProps.static}.
|
|
94
95
|
*/
|
|
95
96
|
static?: boolean;
|
|
97
|
+
/** Runtime gate for the frame loop. Gestures and configuration stay mounted. */
|
|
98
|
+
isFrameLoopActive?: SharedValue<boolean>;
|
|
99
|
+
/** Optional development-only frame publication counters. */
|
|
100
|
+
debugFrameStats?: SharedValue<LiveChartFrameStats>;
|
|
96
101
|
/**
|
|
97
102
|
* Opaque key that snaps the framing to its target in one frame whenever it
|
|
98
103
|
* changes (the next tick bypasses `smoothing` for the window, Y-range, and
|
|
@@ -282,6 +287,8 @@ export function makeEngineFrameScratch(): EngineFrameScratch {
|
|
|
282
287
|
extremaMaxValue: NaN,
|
|
283
288
|
extremaMinTime: NaN,
|
|
284
289
|
extremaMaxTime: NaN,
|
|
290
|
+
lastCanvasWidth: undefined,
|
|
291
|
+
lastCanvasHeight: undefined,
|
|
285
292
|
},
|
|
286
293
|
input: {
|
|
287
294
|
dt: MS_PER_FRAME_60FPS,
|
|
@@ -323,6 +330,8 @@ export function applyLiveChartEngineFrame(
|
|
|
323
330
|
extremaMaxValue: NaN,
|
|
324
331
|
extremaMinTime: NaN,
|
|
325
332
|
extremaMaxTime: NaN,
|
|
333
|
+
lastCanvasWidth: undefined,
|
|
334
|
+
lastCanvasHeight: undefined,
|
|
326
335
|
};
|
|
327
336
|
state.displayValue = sv.displayValue.value;
|
|
328
337
|
state.displayMin = sv.displayMin.value;
|
|
@@ -408,6 +417,40 @@ export function applyLiveChartEngineFrame(
|
|
|
408
417
|
export function useLiveChartEngine(
|
|
409
418
|
config: EngineConfig,
|
|
410
419
|
): SingleEngineState & ChartEngineScroll & ChartEngineEdge {
|
|
420
|
+
const {
|
|
421
|
+
data,
|
|
422
|
+
value,
|
|
423
|
+
timeWindow: configuredTimeWindow,
|
|
424
|
+
smoothing: configuredSmoothing,
|
|
425
|
+
adaptiveSpeedBoost,
|
|
426
|
+
exaggerate,
|
|
427
|
+
referenceValue: configuredReferenceValue,
|
|
428
|
+
referenceValues: configuredReferenceValues,
|
|
429
|
+
liveReferenceValues,
|
|
430
|
+
thresholdRangePoints: configuredThresholdRangePoints,
|
|
431
|
+
thresholdRangeExtendToNow: configuredThresholdRangeExtendToNow,
|
|
432
|
+
thresholdRangeExtendToStart: configuredThresholdRangeExtendToStart,
|
|
433
|
+
nonNegative,
|
|
434
|
+
maxValue,
|
|
435
|
+
yRangeScale,
|
|
436
|
+
nowOverride,
|
|
437
|
+
windowBuffer,
|
|
438
|
+
paused,
|
|
439
|
+
scrollEnabled,
|
|
440
|
+
allowFutureViewEnd,
|
|
441
|
+
returnToLiveMs,
|
|
442
|
+
static: isStatic,
|
|
443
|
+
isFrameLoopActive,
|
|
444
|
+
debugFrameStats,
|
|
445
|
+
snapKey,
|
|
446
|
+
mode,
|
|
447
|
+
candles,
|
|
448
|
+
liveCandle,
|
|
449
|
+
candleGaps,
|
|
450
|
+
candleGapBridgeNoTrades,
|
|
451
|
+
candleGapBridgeUnavailable,
|
|
452
|
+
candleGapBridgeUnknown,
|
|
453
|
+
} = config;
|
|
411
454
|
// Pinch-zoom window-width override (null = follow the configured window).
|
|
412
455
|
// Declared first so `timeWindow` below can fold it in. Defaults to null so
|
|
413
456
|
// charts without `zoom` behave exactly as before.
|
|
@@ -418,26 +461,28 @@ export function useLiveChartEngine(
|
|
|
418
461
|
// the new prop forever, and prop-driven window changes would silently no-op.
|
|
419
462
|
useEffect(() => {
|
|
420
463
|
viewWindow.set(null);
|
|
421
|
-
}, [
|
|
464
|
+
}, [configuredTimeWindow, viewWindow]);
|
|
422
465
|
|
|
423
466
|
// Low-frequency config → UI thread via useDerivedValue. `timeWindow` is the
|
|
424
467
|
// *effective* target window: the zoom override when set, else the prop. Both
|
|
425
468
|
// the tick's window lerp and the X-axis tick selection read it, so zoom flows
|
|
426
469
|
// downstream for free (mirrors how viewEnd drives `timestamp`).
|
|
427
|
-
const timeWindow = useDerivedValue(
|
|
470
|
+
const timeWindow = useDerivedValue(
|
|
471
|
+
() => viewWindow.value ?? configuredTimeWindow,
|
|
472
|
+
);
|
|
428
473
|
// Static charts snap to their target in one tick (smoothing=1), so the single
|
|
429
474
|
// settle reaction below produces the final state with no per-frame easing.
|
|
430
475
|
const smoothing = useDerivedValue(() =>
|
|
431
|
-
|
|
476
|
+
isStatic ? 1 : configuredSmoothing,
|
|
432
477
|
);
|
|
433
|
-
const adaptiveSpeedBoostSV = useDerivedValue(() =>
|
|
434
|
-
const exaggerateSV = useDerivedValue(() =>
|
|
435
|
-
const referenceValue = useDerivedValue(() =>
|
|
478
|
+
const adaptiveSpeedBoostSV = useDerivedValue(() => adaptiveSpeedBoost);
|
|
479
|
+
const exaggerateSV = useDerivedValue(() => exaggerate ?? false);
|
|
480
|
+
const referenceValue = useDerivedValue(() => configuredReferenceValue);
|
|
436
481
|
// Captured directly (not via `config.`) so Reanimated tracks the SharedValue and
|
|
437
482
|
// the merge re-runs each frame while a line is dragged; stable when idle.
|
|
438
|
-
const liveReferenceValuesSV =
|
|
483
|
+
const liveReferenceValuesSV = liveReferenceValues;
|
|
439
484
|
const referenceValues = useDerivedValue(() => {
|
|
440
|
-
const base =
|
|
485
|
+
const base = configuredReferenceValues;
|
|
441
486
|
const live = liveReferenceValuesSV?.value;
|
|
442
487
|
if (!live || live.length === 0) return base;
|
|
443
488
|
return base && base.length > 0 ? base.concat(live) : live;
|
|
@@ -446,56 +491,56 @@ export function useLiveChartEngine(
|
|
|
446
491
|
// directly (not via `config.`) like `liveReferenceValues`, so the live
|
|
447
492
|
// `threshold.series` SharedValue is tracked and the tick sees fresh points.
|
|
448
493
|
const thresholdRangeSourceSV =
|
|
449
|
-
|
|
450
|
-
!Array.isArray(
|
|
451
|
-
?
|
|
494
|
+
configuredThresholdRangePoints !== undefined &&
|
|
495
|
+
!Array.isArray(configuredThresholdRangePoints)
|
|
496
|
+
? configuredThresholdRangePoints
|
|
452
497
|
: null;
|
|
453
498
|
const thresholdRangePoints = useDerivedValue<LiveChartPoint[] | undefined>(
|
|
454
499
|
() =>
|
|
455
500
|
thresholdRangeSourceSV
|
|
456
501
|
? thresholdRangeSourceSV.value
|
|
457
|
-
: (
|
|
502
|
+
: (configuredThresholdRangePoints as LiveChartPoint[] | undefined),
|
|
458
503
|
);
|
|
459
504
|
const thresholdRangeExtendToNow = useDerivedValue(
|
|
460
|
-
() =>
|
|
505
|
+
() => configuredThresholdRangeExtendToNow ?? true,
|
|
461
506
|
);
|
|
462
507
|
const thresholdRangeExtendToStart = useDerivedValue(
|
|
463
|
-
() =>
|
|
508
|
+
() => configuredThresholdRangeExtendToStart ?? true,
|
|
464
509
|
);
|
|
465
|
-
const nonNegativeSV = useDerivedValue(() =>
|
|
466
|
-
const maxValueSV = useDerivedValue(() =>
|
|
467
|
-
const nowOverrideSV = useDerivedValue(() =>
|
|
468
|
-
const windowBufferSV = useDerivedValue(() =>
|
|
469
|
-
const pausedSV = useDerivedValue(() =>
|
|
510
|
+
const nonNegativeSV = useDerivedValue(() => nonNegative ?? false);
|
|
511
|
+
const maxValueSV = useDerivedValue(() => maxValue);
|
|
512
|
+
const nowOverrideSV = useDerivedValue(() => nowOverride);
|
|
513
|
+
const windowBufferSV = useDerivedValue(() => windowBuffer ?? 0);
|
|
514
|
+
const pausedSV = useDerivedValue(() => paused ?? false);
|
|
470
515
|
// Whether time-scroll is active. Drives the return-to-live reaction below
|
|
471
516
|
// (the tick no longer reads it — clearing `viewEnd` is what makes it follow).
|
|
472
517
|
// Defaults to enabled so a caller that omits it behaves as before.
|
|
473
|
-
const scrollEnabledSV = useDerivedValue(() =>
|
|
518
|
+
const scrollEnabledSV = useDerivedValue(() => scrollEnabled ?? true);
|
|
474
519
|
// Honor a viewEnd parked past the live edge (timeScroll.overscroll).
|
|
475
520
|
const allowFutureViewEndSV = useDerivedValue(
|
|
476
|
-
() =>
|
|
521
|
+
() => allowFutureViewEnd ?? false,
|
|
477
522
|
);
|
|
478
523
|
// Return-to-live glide duration (ms); 0 = instant snap. Read by the reaction.
|
|
479
524
|
const returnToLiveMsSV = useDerivedValue(
|
|
480
|
-
() =>
|
|
525
|
+
() => returnToLiveMs ?? RETURN_TO_LIVE_MS,
|
|
481
526
|
);
|
|
482
|
-
const modeSV = useDerivedValue(() =>
|
|
483
|
-
const candleGapsSV = useDerivedValue(() =>
|
|
527
|
+
const modeSV = useDerivedValue(() => mode ?? "line");
|
|
528
|
+
const candleGapsSV = useDerivedValue(() => candleGaps);
|
|
484
529
|
const candleGapBridgeNoTradesSV = useDerivedValue(
|
|
485
|
-
() =>
|
|
530
|
+
() => candleGapBridgeNoTrades ?? false,
|
|
486
531
|
);
|
|
487
532
|
const candleGapBridgeUnavailableSV = useDerivedValue(
|
|
488
|
-
() =>
|
|
533
|
+
() => candleGapBridgeUnavailable ?? false,
|
|
489
534
|
);
|
|
490
535
|
const candleGapBridgeUnknownSV = useDerivedValue(
|
|
491
|
-
() =>
|
|
536
|
+
() => candleGapBridgeUnknown ?? false,
|
|
492
537
|
);
|
|
493
538
|
|
|
494
539
|
// Animation state (mutated on UI thread each frame)
|
|
495
540
|
const displayValue = useSharedValue(0);
|
|
496
541
|
const displayMin = useSharedValue(0);
|
|
497
542
|
const displayMax = useSharedValue(1);
|
|
498
|
-
const displayWindow = useSharedValue(
|
|
543
|
+
const displayWindow = useSharedValue(configuredTimeWindow);
|
|
499
544
|
const canvasWidth = useSharedValue(0);
|
|
500
545
|
const canvasHeight = useSharedValue(0);
|
|
501
546
|
// Seed once; overwritten by the frame callback on the first tick.
|
|
@@ -525,8 +570,6 @@ export function useLiveChartEngine(
|
|
|
525
570
|
|
|
526
571
|
// High-frequency data reads directly from the caller's shared values —
|
|
527
572
|
// no useDerivedValue bridging, no closure serialization per tick.
|
|
528
|
-
const { data, value, candles, liveCandle } = config;
|
|
529
|
-
|
|
530
573
|
// One-shot "snap the framing" flag. The effect below flips it to `true` when
|
|
531
574
|
// `snapKey` changes; the next frame consumes it (bypassing `smoothing` for the
|
|
532
575
|
// window / range / value) and clears it, so a timeframe / dataset switch lands
|
|
@@ -534,20 +577,20 @@ export function useLiveChartEngine(
|
|
|
534
577
|
const snapSV = useSharedValue(false);
|
|
535
578
|
// Compare against the previous key (not a "first render" flag) so React 18
|
|
536
579
|
// StrictMode's double-invoked mount effect can't fire a spurious snap.
|
|
537
|
-
const lastSnapKey = useRef(
|
|
580
|
+
const lastSnapKey = useRef(snapKey);
|
|
538
581
|
// Layout effect, not passive: a passive effect runs after the commit has
|
|
539
582
|
// painted, so the frame between them draws the new data/candleWidth against
|
|
540
583
|
// the OLD framing (a one-frame squeeze on a timeframe switch). Flipping the
|
|
541
584
|
// flag before paint lets the first frame with the new props consume the snap.
|
|
542
585
|
useLayoutEffect(() => {
|
|
543
|
-
if (
|
|
544
|
-
lastSnapKey.current =
|
|
586
|
+
if (snapKey === lastSnapKey.current) return;
|
|
587
|
+
lastSnapKey.current = snapKey;
|
|
545
588
|
snapSV.set(true);
|
|
546
|
-
}, [
|
|
589
|
+
}, [snapKey, snapSV]);
|
|
547
590
|
|
|
548
591
|
// Static charts run zero per-frame loops. `isStaticSV` gates both the
|
|
549
592
|
// frame-callback autostart and the one-shot settle reaction below.
|
|
550
|
-
const isStaticSV = useDerivedValue(() =>
|
|
593
|
+
const isStaticSV = useDerivedValue(() => isStatic ?? false);
|
|
551
594
|
|
|
552
595
|
// Single refs object shared by the frame callback and the settle reaction so
|
|
553
596
|
// they can never drift out of sync. Keep its identity stable across unrelated
|
|
@@ -576,7 +619,7 @@ export function useLiveChartEngine(
|
|
|
576
619
|
thresholdRangeExtendToNow,
|
|
577
620
|
nonNegativeSV,
|
|
578
621
|
maxValueSV,
|
|
579
|
-
yRangeScaleSV:
|
|
622
|
+
yRangeScaleSV: yRangeScale,
|
|
580
623
|
nowOverrideSV,
|
|
581
624
|
windowBufferSV,
|
|
582
625
|
pausedSV,
|
|
@@ -610,7 +653,6 @@ export function useLiveChartEngine(
|
|
|
610
653
|
candles,
|
|
611
654
|
canvasHeight,
|
|
612
655
|
canvasWidth,
|
|
613
|
-
config.yRangeScale,
|
|
614
656
|
data,
|
|
615
657
|
displayMax,
|
|
616
658
|
displayMin,
|
|
@@ -638,12 +680,13 @@ export function useLiveChartEngine(
|
|
|
638
680
|
thresholdRangeExtendToNow,
|
|
639
681
|
thresholdRangeExtendToStart,
|
|
640
682
|
thresholdRangePoints,
|
|
641
|
-
timestamp,
|
|
642
683
|
timeWindow,
|
|
684
|
+
timestamp,
|
|
643
685
|
value,
|
|
644
686
|
viewEnd,
|
|
645
687
|
viewWindow,
|
|
646
688
|
windowBufferSV,
|
|
689
|
+
yRangeScale,
|
|
647
690
|
],
|
|
648
691
|
);
|
|
649
692
|
const frameScratchRef = useRef<EngineFrameScratch | null>(null);
|
|
@@ -657,13 +700,37 @@ export function useLiveChartEngine(
|
|
|
657
700
|
// loop is fully inert in static mode (the invariant that makes this worth it).
|
|
658
701
|
const engineFrameCallback = useFrameCallback((frameInfo) => {
|
|
659
702
|
"worklet";
|
|
703
|
+
if (isFrameLoopActive?.get() === false) return;
|
|
704
|
+
const beforeValue = debugFrameStats ? displayValue.get() : 0;
|
|
705
|
+
const beforeMin = debugFrameStats ? displayMin.get() : 0;
|
|
706
|
+
const beforeMax = debugFrameStats ? displayMax.get() : 0;
|
|
707
|
+
const beforeWindow = debugFrameStats ? displayWindow.get() : 0;
|
|
708
|
+
const beforeTimestamp = debugFrameStats ? timestamp.get() : 0;
|
|
709
|
+
const beforeLiveEdge = debugFrameStats ? liveEdge.get() : 0;
|
|
710
|
+
const beforeEdgeValue = debugFrameStats ? edgeValue.get() : 0;
|
|
660
711
|
applyLiveChartEngineFrame(frameInfo, frameRefs, frameScratchRef.current!);
|
|
661
|
-
|
|
712
|
+
if (debugFrameStats) {
|
|
713
|
+
const didPublish =
|
|
714
|
+
beforeValue !== displayValue.get() ||
|
|
715
|
+
beforeMin !== displayMin.get() ||
|
|
716
|
+
beforeMax !== displayMax.get() ||
|
|
717
|
+
beforeWindow !== displayWindow.get() ||
|
|
718
|
+
beforeTimestamp !== timestamp.get() ||
|
|
719
|
+
beforeLiveEdge !== liveEdge.get() ||
|
|
720
|
+
beforeEdgeValue !== edgeValue.get();
|
|
721
|
+
const previous = debugFrameStats.get();
|
|
722
|
+
debugFrameStats.set({
|
|
723
|
+
frames: previous.frames + 1,
|
|
724
|
+
published: previous.published + (didPublish ? 1 : 0),
|
|
725
|
+
skipped: previous.skipped + (didPublish ? 0 : 1),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}, !isStatic);
|
|
662
729
|
useEffect(() => {
|
|
663
730
|
// Reanimated only seeds `isActive` from `autostart` on mount. Keep the
|
|
664
731
|
// imperative state aligned when a chart switches between static and live.
|
|
665
|
-
engineFrameCallback.setActive(!
|
|
666
|
-
}, [
|
|
732
|
+
engineFrameCallback.setActive(!isStatic);
|
|
733
|
+
}, [isStatic, engineFrameCallback]);
|
|
667
734
|
|
|
668
735
|
// When time-scroll is disabled while scrolled back, return the window to the
|
|
669
736
|
// live edge. With a positive duration this glides: snapshot the frozen edge into
|
|
@@ -715,6 +782,12 @@ export function useLiveChartEngine(
|
|
|
715
782
|
canvasWidth.get(),
|
|
716
783
|
canvasHeight.get(),
|
|
717
784
|
timeWindow.get(),
|
|
785
|
+
// Range props can arrive after the data shared value. Re-settle when
|
|
786
|
+
// those bounds change too, rather than retaining the previous scale
|
|
787
|
+
// until another data/layout update (static charts have no frame loop).
|
|
788
|
+
maxValueSV.get(),
|
|
789
|
+
referenceValue.get(),
|
|
790
|
+
...(referenceValues.get() ?? []),
|
|
718
791
|
].join(",");
|
|
719
792
|
},
|
|
720
793
|
(curr, prev) => {
|
package/src/hooks/useBadge.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SkFont } from "@shopify/react-native-skia";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
import {
|
|
3
4
|
useDerivedValue,
|
|
4
5
|
useSharedValue,
|
|
@@ -9,7 +10,10 @@ import {
|
|
|
9
10
|
MOTION_METRICS_DEFAULTS,
|
|
10
11
|
MS_PER_FRAME_60FPS,
|
|
11
12
|
} from "../constants";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
measureFontTextWidth,
|
|
15
|
+
type TextWidthCache,
|
|
16
|
+
} from "../lib/measureFontTextWidth";
|
|
13
17
|
import type { ChartEngineWithLiveValue } from "../core/useLiveChartEngine";
|
|
14
18
|
import {
|
|
15
19
|
badgeTailAndCap,
|
|
@@ -64,6 +68,7 @@ export function useBadge(
|
|
|
64
68
|
const colorR = useSharedValue(0);
|
|
65
69
|
const colorG = useSharedValue(0);
|
|
66
70
|
const colorB = useSharedValue(0);
|
|
71
|
+
const textWidthCacheRef = useRef<TextWidthCache>({});
|
|
67
72
|
|
|
68
73
|
const upRgb = hexToRgb(palette.dotUp);
|
|
69
74
|
const downRgb = hexToRgb(palette.dotDown);
|
|
@@ -103,7 +108,7 @@ export function useBadge(
|
|
|
103
108
|
: padding.top + ((dMax - liveVal) / valRange) * chartH;
|
|
104
109
|
|
|
105
110
|
const text = formatValue(liveVal);
|
|
106
|
-
const textW = measureFontTextWidth(font, text);
|
|
111
|
+
const textW = measureFontTextWidth(font, text, textWidthCacheRef.current);
|
|
107
112
|
|
|
108
113
|
const pillH = font.getSize() + badgeMetrics.padY * 2;
|
|
109
114
|
// `midY` is the pill's vertical center (the tail anchors here); `capR` is the
|
|
@@ -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.
|