react-native-livechart 4.9.1 → 4.10.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/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +9 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +2 -0
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +2 -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/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useXAxis.d.ts.map +1 -1
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairOverlay.tsx +6 -1
- package/src/components/LiveChart.tsx +505 -318
- package/src/components/MarkerOverlay.tsx +11 -16
- package/src/components/YAxisOverlay.tsx +43 -1
- package/src/draw/markerAtlas.ts +22 -2
- package/src/hooks/crosshairShared.ts +19 -18
- package/src/hooks/delayedPanGuard.ts +80 -0
- package/src/hooks/useCrosshair.ts +232 -182
- package/src/hooks/useCrosshairSeries.ts +38 -0
- package/src/hooks/useXAxis.ts +21 -5
- package/src/types.ts +7 -2
|
@@ -3,7 +3,6 @@ import { Platform } from "react-native";
|
|
|
3
3
|
import { Gesture } from "react-native-gesture-handler";
|
|
4
4
|
import {
|
|
5
5
|
useAnimatedReaction,
|
|
6
|
-
useDerivedValue,
|
|
7
6
|
useSharedValue,
|
|
8
7
|
type SharedValue,
|
|
9
8
|
} from "react-native-reanimated";
|
|
@@ -32,10 +31,18 @@ import {
|
|
|
32
31
|
deriveCrosshairTooltipSingle,
|
|
33
32
|
HIDDEN_ACTION_BADGE,
|
|
34
33
|
HIDDEN_TIME_BADGE,
|
|
34
|
+
HIDDEN_TOOLTIP,
|
|
35
35
|
pointInRect,
|
|
36
36
|
snapPrice,
|
|
37
37
|
type CrosshairState,
|
|
38
38
|
} from "./crosshairShared";
|
|
39
|
+
import {
|
|
40
|
+
delayedPanTouchCancelled,
|
|
41
|
+
delayedPanTouchDown,
|
|
42
|
+
delayedPanTouchUp,
|
|
43
|
+
resetDelayedPanGuard,
|
|
44
|
+
shouldStartDelayedPan,
|
|
45
|
+
} from "./delayedPanGuard";
|
|
39
46
|
|
|
40
47
|
const ACTION_HIT_SLOP = 6;
|
|
41
48
|
const RETICLE_HIT = 14;
|
|
@@ -150,6 +157,10 @@ export function useCrosshair(
|
|
|
150
157
|
// Tracks whether the active scrub phase actually began, so a tap that never
|
|
151
158
|
// activates doesn't emit a spurious onGestureEnd.
|
|
152
159
|
const gestureStarted = useSharedValue(false);
|
|
160
|
+
// Guard state for RNGH's delayed-pan post-lift activation on iOS. The shared
|
|
161
|
+
// lifecycle helpers are also used by LiveChartSeries and unit-tested directly.
|
|
162
|
+
const fingerDown = useSharedValue(false);
|
|
163
|
+
const panActivated = useSharedValue(false);
|
|
153
164
|
// Where the crosshair line should start (canvas Y) so it stops at a top-pinned
|
|
154
165
|
// custom tooltip instead of running through it. -1 = no top tooltip → the line
|
|
155
166
|
// starts at padding.top. Written by CustomTooltipOverlay, read by CrosshairOverlay.
|
|
@@ -172,203 +183,216 @@ export function useCrosshair(
|
|
|
172
183
|
const actionShowText = scrubAction?.text ?? true;
|
|
173
184
|
const hasTimeBadge = scrubAction?.timeBadge ?? false;
|
|
174
185
|
|
|
175
|
-
const scrubTime = useDerivedValue(() =>
|
|
176
|
-
computeScrubTime(
|
|
177
|
-
scrubActive.get(),
|
|
178
|
-
scrubX.get(),
|
|
179
|
-
padding,
|
|
180
|
-
engine.canvasWidth.get(),
|
|
181
|
-
engine.timestamp.get(),
|
|
182
|
-
engine.displayWindow.get(),
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
|
|
186
186
|
const isCandleMode = candleOpts?.mode === "candle";
|
|
187
187
|
const candlesSV = candleOpts?.candles;
|
|
188
188
|
const liveCandleSV = candleOpts?.liveCandle;
|
|
189
189
|
const candleWidthSecs = candleOpts?.candleWidthSecs ?? 60;
|
|
190
190
|
|
|
191
|
-
/* istanbul ignore next -- worklet */
|
|
192
|
-
const scrubCandle = useDerivedValue(() => {
|
|
193
|
-
if (
|
|
194
|
-
!isCandleMode ||
|
|
195
|
-
!candlesSV ||
|
|
196
|
-
!scrubActive.get() ||
|
|
197
|
-
scrubTime.get() < 0
|
|
198
|
-
)
|
|
199
|
-
return null;
|
|
200
|
-
return pickCandleAtTime(
|
|
201
|
-
candlesSV.get(),
|
|
202
|
-
liveCandleSV?.get() ?? null,
|
|
203
|
-
scrubTime.get(),
|
|
204
|
-
candleWidthSecs,
|
|
205
|
-
);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
/* istanbul ignore next -- worklet */
|
|
209
|
-
const scrubValue = useDerivedValue(() => {
|
|
210
|
-
if (!scrubActive.get() || scrubTime.get() < 0) return null;
|
|
211
|
-
if (isCandleMode) {
|
|
212
|
-
return scrubCandle.get()?.close ?? null;
|
|
213
|
-
}
|
|
214
|
-
return interpolateAtTime(engine.data.get(), scrubTime.get());
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const crosshairOpacity = useDerivedValue(() =>
|
|
218
|
-
computeCrosshairOpacity(
|
|
219
|
-
scrubActive.get(),
|
|
220
|
-
scrubX.get(),
|
|
221
|
-
engine.canvasWidth.get(),
|
|
222
|
-
padding.right,
|
|
223
|
-
),
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
// Y pixel of the scrub intersection — used by the selection dot. -1 when
|
|
227
|
-
// there's no value to mark.
|
|
228
|
-
const scrubDotY = useDerivedValue(() =>
|
|
229
|
-
computeScrubDotY(
|
|
230
|
-
scrubValue.get(),
|
|
231
|
-
engine.displayMin.get(),
|
|
232
|
-
engine.displayMax.get(),
|
|
233
|
-
engine.canvasHeight.get(),
|
|
234
|
-
padding.top,
|
|
235
|
-
padding.bottom,
|
|
236
|
-
),
|
|
237
|
-
);
|
|
238
|
-
|
|
239
191
|
// Monospace advance width, measured once per render (not per scrub frame) so
|
|
240
192
|
// the tooltip layout worklet can size text by character count instead of a
|
|
241
193
|
// per-frame Skia measureText.
|
|
242
194
|
const monoCharWidth = font.measureText("0").width;
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
195
|
+
const scrubTime = useSharedValue(-1);
|
|
196
|
+
const scrubCandle = useSharedValue<CandlePoint | null>(null);
|
|
197
|
+
const scrubValue = useSharedValue<number | null>(null);
|
|
198
|
+
const crosshairOpacity = useSharedValue(0);
|
|
199
|
+
const scrubDotY = useSharedValue(-1);
|
|
200
|
+
const tooltipLayout = useSharedValue(HIDDEN_TOOLTIP);
|
|
201
|
+
|
|
202
|
+
// These six outputs traverse the same scrub + engine inputs. One reaction
|
|
203
|
+
// computes and publishes the snapshot atomically instead of compiling six
|
|
204
|
+
// independent derived-value mappers with chained dependencies.
|
|
205
|
+
useAnimatedReaction(
|
|
206
|
+
() => {
|
|
207
|
+
"worklet";
|
|
208
|
+
const active = scrubActive.get();
|
|
209
|
+
const x = scrubX.get();
|
|
210
|
+
const canvasWidth = engine.canvasWidth.get();
|
|
211
|
+
const canvasHeight = engine.canvasHeight.get();
|
|
212
|
+
const time = computeScrubTime(
|
|
213
|
+
active,
|
|
214
|
+
x,
|
|
251
215
|
padding,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
font,
|
|
256
|
-
monoCharWidth,
|
|
216
|
+
canvasWidth,
|
|
217
|
+
engine.timestamp.get(),
|
|
218
|
+
engine.displayWindow.get(),
|
|
257
219
|
);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
220
|
+
const candle =
|
|
221
|
+
isCandleMode && candlesSV && active && time >= 0
|
|
222
|
+
? pickCandleAtTime(
|
|
223
|
+
candlesSV.get(),
|
|
224
|
+
liveCandleSV?.get() ?? null,
|
|
225
|
+
time,
|
|
226
|
+
candleWidthSecs,
|
|
227
|
+
)
|
|
228
|
+
: null;
|
|
229
|
+
const value =
|
|
230
|
+
!active || time < 0
|
|
231
|
+
? null
|
|
232
|
+
: isCandleMode
|
|
233
|
+
? (candle?.close ?? null)
|
|
234
|
+
: interpolateAtTime(engine.data.get(), time);
|
|
235
|
+
const dotY = computeScrubDotY(
|
|
236
|
+
value,
|
|
237
|
+
engine.displayMin.get(),
|
|
238
|
+
engine.displayMax.get(),
|
|
239
|
+
canvasHeight,
|
|
240
|
+
padding.top,
|
|
241
|
+
padding.bottom,
|
|
242
|
+
);
|
|
243
|
+
const layout = isCandleMode
|
|
244
|
+
? computeCandleTooltipLayout(
|
|
245
|
+
active,
|
|
246
|
+
x,
|
|
247
|
+
candle,
|
|
248
|
+
time,
|
|
249
|
+
padding,
|
|
250
|
+
canvasWidth,
|
|
251
|
+
formatValue,
|
|
252
|
+
formatTime,
|
|
253
|
+
font,
|
|
254
|
+
monoCharWidth,
|
|
255
|
+
)
|
|
256
|
+
: deriveCrosshairTooltipSingle(
|
|
257
|
+
active,
|
|
258
|
+
x,
|
|
259
|
+
time,
|
|
260
|
+
value,
|
|
261
|
+
padding,
|
|
262
|
+
canvasWidth,
|
|
263
|
+
formatValue,
|
|
264
|
+
formatTime,
|
|
265
|
+
font,
|
|
266
|
+
monoCharWidth,
|
|
267
|
+
tooltipPlacement,
|
|
268
|
+
tooltipShowValue,
|
|
269
|
+
tooltipShowTime,
|
|
270
|
+
canvasHeight,
|
|
271
|
+
tooltipMargin,
|
|
272
|
+
dotY,
|
|
273
|
+
);
|
|
274
|
+
return {
|
|
275
|
+
time,
|
|
276
|
+
candle,
|
|
277
|
+
value,
|
|
278
|
+
opacity: computeCrosshairOpacity(active, x, canvasWidth, padding.right),
|
|
279
|
+
dotY,
|
|
280
|
+
layout,
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
(snapshot) => {
|
|
284
|
+
"worklet";
|
|
285
|
+
scrubTime.set(snapshot.time);
|
|
286
|
+
scrubCandle.set(snapshot.candle);
|
|
287
|
+
scrubValue.set(snapshot.value);
|
|
288
|
+
crosshairOpacity.set(snapshot.opacity);
|
|
289
|
+
scrubDotY.set(snapshot.dotY);
|
|
290
|
+
tooltipLayout.set(snapshot.layout);
|
|
291
|
+
},
|
|
292
|
+
);
|
|
278
293
|
|
|
279
294
|
// ── Scrub-action lock derivations ──────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// Screen Y of the locked level, DERIVED from the frozen price each frame so the
|
|
292
|
-
// line + badge track the chosen price as displayMin/Max move (rather than the
|
|
293
|
-
// price drifting under a pixel-fixed reticle). Pins to the plot edge when the
|
|
294
|
-
// price scrolls out of the visible range; -1 until placed / laid out.
|
|
295
|
+
const lockPrice = useSharedValue<number | null>(null);
|
|
296
|
+
const lockY = useSharedValue(-1);
|
|
297
|
+
const lockTime = useSharedValue(-1);
|
|
298
|
+
const lockCandle = useSharedValue<CandlePoint | null>(null);
|
|
299
|
+
const actionBadge = useSharedValue(HIDDEN_ACTION_BADGE);
|
|
300
|
+
const timeBadge = useSharedValue(HIDDEN_TIME_BADGE);
|
|
301
|
+
|
|
302
|
+
// The lock price, projected coordinates, candle and both badges are one
|
|
303
|
+
// coherent reticle snapshot. Publishing them from one reaction replaces six
|
|
304
|
+
// chained derived-value mappers and prevents intermediate mixed-frame state.
|
|
295
305
|
/* istanbul ignore next -- worklet (locked branch only runs on the UI thread) */
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
engine.
|
|
303
|
-
engine.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
306
|
+
useAnimatedReaction(
|
|
307
|
+
() => {
|
|
308
|
+
"worklet";
|
|
309
|
+
const active = lockActive.get();
|
|
310
|
+
const x = lockX.get();
|
|
311
|
+
const rawPrice = lockPriceValue.get();
|
|
312
|
+
const canvasWidth = engine.canvasWidth.get();
|
|
313
|
+
const canvasHeight = engine.canvasHeight.get();
|
|
314
|
+
const price =
|
|
315
|
+
active && rawPrice !== null
|
|
316
|
+
? snapPrice(rawPrice, snapIncrement)
|
|
317
|
+
: null;
|
|
318
|
+
let y = -1;
|
|
319
|
+
if (
|
|
320
|
+
rawPrice !== null &&
|
|
321
|
+
canvasHeight - padding.top - padding.bottom > 0
|
|
322
|
+
) {
|
|
323
|
+
y = clampPlotY(
|
|
324
|
+
computeScrubDotY(
|
|
325
|
+
rawPrice,
|
|
326
|
+
engine.displayMin.get(),
|
|
327
|
+
engine.displayMax.get(),
|
|
328
|
+
canvasHeight,
|
|
329
|
+
padding.top,
|
|
330
|
+
padding.bottom,
|
|
331
|
+
),
|
|
332
|
+
padding.top,
|
|
333
|
+
canvasHeight,
|
|
334
|
+
padding.bottom,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const time = computeScrubTime(
|
|
338
|
+
active,
|
|
339
|
+
x,
|
|
340
|
+
padding,
|
|
341
|
+
canvasWidth,
|
|
342
|
+
engine.timestamp.get(),
|
|
343
|
+
engine.displayWindow.get(),
|
|
344
|
+
);
|
|
345
|
+
const candle =
|
|
346
|
+
isCandleMode && candlesSV && active && time >= 0
|
|
347
|
+
? pickCandleAtTime(
|
|
348
|
+
candlesSV.get(),
|
|
349
|
+
liveCandleSV?.get() ?? null,
|
|
350
|
+
time,
|
|
351
|
+
candleWidthSecs,
|
|
352
|
+
)
|
|
353
|
+
: null;
|
|
354
|
+
const badge =
|
|
355
|
+
price === null
|
|
356
|
+
? HIDDEN_ACTION_BADGE
|
|
357
|
+
: computeActionBadgeLayout(
|
|
358
|
+
active,
|
|
359
|
+
y,
|
|
360
|
+
actionShowText ? formatValue(price) : "",
|
|
361
|
+
actionIcon,
|
|
362
|
+
canvasWidth,
|
|
363
|
+
canvasWidth - padding.right,
|
|
364
|
+
font,
|
|
365
|
+
badgeMetrics.marginEdge,
|
|
366
|
+
badgeMetrics.padX,
|
|
367
|
+
badgeMetrics.padY,
|
|
368
|
+
);
|
|
369
|
+
const timeLayout =
|
|
370
|
+
!hasTimeBadge || !active || time < 0
|
|
371
|
+
? HIDDEN_TIME_BADGE
|
|
372
|
+
: computeTimeBadgeLayout(
|
|
373
|
+
active,
|
|
374
|
+
x,
|
|
375
|
+
formatTime(time),
|
|
376
|
+
canvasWidth,
|
|
377
|
+
canvasHeight - padding.bottom + X_AXIS_LABEL_OFFSET_Y,
|
|
378
|
+
font,
|
|
379
|
+
badgeMetrics.padX,
|
|
380
|
+
badgeMetrics.padY,
|
|
381
|
+
badgeMetrics.marginEdge,
|
|
382
|
+
);
|
|
383
|
+
return { price, y, time, candle, badge, timeLayout };
|
|
384
|
+
},
|
|
385
|
+
(snapshot) => {
|
|
386
|
+
"worklet";
|
|
387
|
+
lockPrice.set(snapshot.price);
|
|
388
|
+
lockY.set(snapshot.y);
|
|
389
|
+
lockTime.set(snapshot.time);
|
|
390
|
+
lockCandle.set(snapshot.candle);
|
|
391
|
+
actionBadge.set(snapshot.badge);
|
|
392
|
+
timeBadge.set(snapshot.timeLayout);
|
|
393
|
+
},
|
|
320
394
|
);
|
|
321
395
|
|
|
322
|
-
/* istanbul ignore next -- worklet */
|
|
323
|
-
const lockCandle = useDerivedValue(() => {
|
|
324
|
-
if (!isCandleMode || !candlesSV || !lockActive.get() || lockTime.get() < 0)
|
|
325
|
-
return null;
|
|
326
|
-
return pickCandleAtTime(
|
|
327
|
-
candlesSV.get(),
|
|
328
|
-
liveCandleSV?.get() ?? null,
|
|
329
|
-
lockTime.get(),
|
|
330
|
-
candleWidthSecs,
|
|
331
|
-
);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// Right-gutter action-badge layout — also the tap hit rect. Hidden when not locked.
|
|
335
|
-
/* istanbul ignore next -- worklet (locked branch only runs on the UI thread) */
|
|
336
|
-
const actionBadge = useDerivedValue(() => {
|
|
337
|
-
const price = lockPrice.get();
|
|
338
|
-
if (price === null) return HIDDEN_ACTION_BADGE;
|
|
339
|
-
return computeActionBadgeLayout(
|
|
340
|
-
lockActive.get(),
|
|
341
|
-
lockY.get(),
|
|
342
|
-
actionShowText ? formatValue(price) : "",
|
|
343
|
-
actionIcon,
|
|
344
|
-
engine.canvasWidth.get(),
|
|
345
|
-
engine.canvasWidth.get() - padding.right,
|
|
346
|
-
font,
|
|
347
|
-
badgeMetrics.marginEdge,
|
|
348
|
-
badgeMetrics.padX,
|
|
349
|
-
badgeMetrics.padY,
|
|
350
|
-
);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// X-axis time badge at the reticle (opt-in). Hidden unless enabled + locked.
|
|
354
|
-
/* istanbul ignore next -- worklet (locked branch only runs on the UI thread) */
|
|
355
|
-
const timeBadge = useDerivedValue(() => {
|
|
356
|
-
if (!hasTimeBadge || !lockActive.get()) return HIDDEN_TIME_BADGE;
|
|
357
|
-
const t = lockTime.get();
|
|
358
|
-
if (t < 0) return HIDDEN_TIME_BADGE;
|
|
359
|
-
return computeTimeBadgeLayout(
|
|
360
|
-
lockActive.get(),
|
|
361
|
-
lockX.get(),
|
|
362
|
-
formatTime(t),
|
|
363
|
-
engine.canvasWidth.get(),
|
|
364
|
-
engine.canvasHeight.get() - padding.bottom + X_AXIS_LABEL_OFFSET_Y,
|
|
365
|
-
font,
|
|
366
|
-
badgeMetrics.padX,
|
|
367
|
-
badgeMetrics.padY,
|
|
368
|
-
badgeMetrics.marginEdge,
|
|
369
|
-
);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
396
|
/* istanbul ignore next -- invoked only via scheduleOnRN from UI-thread gesture */
|
|
373
397
|
function handleScrubAction(
|
|
374
398
|
price: number,
|
|
@@ -485,6 +509,30 @@ export function useCrosshair(
|
|
|
485
509
|
.activateAfterLongPress(longPressMs)
|
|
486
510
|
.maxPointers(1)
|
|
487
511
|
.shouldCancelWhenOutside(false)
|
|
512
|
+
.onTouchesDown(
|
|
513
|
+
/* istanbul ignore next */ () => {
|
|
514
|
+
"worklet";
|
|
515
|
+
delayedPanTouchDown(longPressMs, fingerDown);
|
|
516
|
+
},
|
|
517
|
+
)
|
|
518
|
+
.onTouchesUp(
|
|
519
|
+
/* istanbul ignore next */ (e, manager) => {
|
|
520
|
+
"worklet";
|
|
521
|
+
delayedPanTouchUp(
|
|
522
|
+
longPressMs,
|
|
523
|
+
e,
|
|
524
|
+
manager,
|
|
525
|
+
fingerDown,
|
|
526
|
+
panActivated,
|
|
527
|
+
);
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
.onTouchesCancelled(
|
|
531
|
+
/* istanbul ignore next */ () => {
|
|
532
|
+
"worklet";
|
|
533
|
+
delayedPanTouchCancelled(longPressMs, fingerDown);
|
|
534
|
+
},
|
|
535
|
+
)
|
|
488
536
|
// Start scrubbing on ACTIVE (onStart), not on touch-down (onBegin):
|
|
489
537
|
// `activateAfterLongPress` only delays activation, so onBegin still fires
|
|
490
538
|
// immediately — using it would scrub instantly and ignore panGestureDelay,
|
|
@@ -492,6 +540,7 @@ export function useCrosshair(
|
|
|
492
540
|
.onStart(
|
|
493
541
|
/* istanbul ignore next */ (e) => {
|
|
494
542
|
"worklet";
|
|
543
|
+
if (!shouldStartDelayedPan(longPressMs, fingerDown, panActivated)) return;
|
|
495
544
|
if (!enabled) return;
|
|
496
545
|
// Scrub-action: once a reticle is placed, drag adjusts it (2D — the Y is
|
|
497
546
|
// the price). The ephemeral live-scrub never engages, so scrubActive
|
|
@@ -556,6 +605,7 @@ export function useCrosshair(
|
|
|
556
605
|
.onFinalize(
|
|
557
606
|
/* istanbul ignore next */ () => {
|
|
558
607
|
"worklet";
|
|
608
|
+
resetDelayedPanGuard(fingerDown, panActivated);
|
|
559
609
|
// A lock-adjust drag leaves the reticle in place (scrubActive was never
|
|
560
610
|
// set); a live-scrub clears its crosshair. Always clear scrubActive so a
|
|
561
611
|
// stray scrub can never linger behind a placed reticle.
|
|
@@ -20,6 +20,13 @@ import {
|
|
|
20
20
|
HIDDEN_TOOLTIP,
|
|
21
21
|
type CrosshairState,
|
|
22
22
|
} from "./crosshairShared";
|
|
23
|
+
import {
|
|
24
|
+
delayedPanTouchCancelled,
|
|
25
|
+
delayedPanTouchDown,
|
|
26
|
+
delayedPanTouchUp,
|
|
27
|
+
resetDelayedPanGuard,
|
|
28
|
+
shouldStartDelayedPan,
|
|
29
|
+
} from "./delayedPanGuard";
|
|
23
30
|
|
|
24
31
|
/**
|
|
25
32
|
* LiveChartSeries crosshair + scrub. No tooltip — data is delivered via
|
|
@@ -40,6 +47,10 @@ export function useCrosshairSeries(
|
|
|
40
47
|
// Tracks whether the active scrub phase actually began, so a tap that never
|
|
41
48
|
// activates doesn't emit a spurious onGestureEnd.
|
|
42
49
|
const gestureStarted = useSharedValue(false);
|
|
50
|
+
// Mirrors LiveChart's guard against RNGH's delayed-pan timer activating after
|
|
51
|
+
// the final pointer has already lifted on iOS.
|
|
52
|
+
const fingerDown = useSharedValue(false);
|
|
53
|
+
const panActivated = useSharedValue(false);
|
|
43
54
|
|
|
44
55
|
const scrubTime = useDerivedValue(() =>
|
|
45
56
|
computeScrubTime(
|
|
@@ -158,6 +169,30 @@ export function useCrosshairSeries(
|
|
|
158
169
|
.activateAfterLongPress(panGestureDelay)
|
|
159
170
|
.maxPointers(1)
|
|
160
171
|
.shouldCancelWhenOutside(false)
|
|
172
|
+
.onTouchesDown(
|
|
173
|
+
/* istanbul ignore next */ () => {
|
|
174
|
+
"worklet";
|
|
175
|
+
delayedPanTouchDown(panGestureDelay, fingerDown);
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
.onTouchesUp(
|
|
179
|
+
/* istanbul ignore next */ (e, manager) => {
|
|
180
|
+
"worklet";
|
|
181
|
+
delayedPanTouchUp(
|
|
182
|
+
panGestureDelay,
|
|
183
|
+
e,
|
|
184
|
+
manager,
|
|
185
|
+
fingerDown,
|
|
186
|
+
panActivated,
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
.onTouchesCancelled(
|
|
191
|
+
/* istanbul ignore next */ () => {
|
|
192
|
+
"worklet";
|
|
193
|
+
delayedPanTouchCancelled(panGestureDelay, fingerDown);
|
|
194
|
+
},
|
|
195
|
+
)
|
|
161
196
|
// Start scrubbing on ACTIVE (onStart), not on touch-down (onBegin):
|
|
162
197
|
// `activateAfterLongPress` only delays activation, so onBegin still fires
|
|
163
198
|
// immediately — using it would scrub instantly and ignore panGestureDelay,
|
|
@@ -165,6 +200,8 @@ export function useCrosshairSeries(
|
|
|
165
200
|
.onStart(
|
|
166
201
|
/* istanbul ignore next */ (e) => {
|
|
167
202
|
"worklet";
|
|
203
|
+
if (!shouldStartDelayedPan(panGestureDelay, fingerDown, panActivated))
|
|
204
|
+
return;
|
|
168
205
|
if (!enabled) return;
|
|
169
206
|
scrubX.set(e.x);
|
|
170
207
|
scrubActive.set(true);
|
|
@@ -182,6 +219,7 @@ export function useCrosshairSeries(
|
|
|
182
219
|
.onFinalize(
|
|
183
220
|
/* istanbul ignore next */ () => {
|
|
184
221
|
"worklet";
|
|
222
|
+
resetDelayedPanGuard(fingerDown, panActivated);
|
|
185
223
|
scrubActive.set(false);
|
|
186
224
|
if (gestureStarted.get()) {
|
|
187
225
|
gestureStarted.set(false);
|
package/src/hooks/useXAxis.ts
CHANGED
|
@@ -129,15 +129,28 @@ export function useXAxis(
|
|
|
129
129
|
targetKeys.push(Math.round(t * 100));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
// SharedValue payloads may be frozen after crossing the JS/UI boundary.
|
|
133
|
+
// Work on a fresh cache (including fresh entries) so adding/removing keys
|
|
134
|
+
// and updating alpha never mutates the serialized value returned by get().
|
|
135
|
+
const previousAlphas = labelAlphas.get();
|
|
136
|
+
const alphas: Record<number, { alpha: number; text: string }> = {};
|
|
137
|
+
const previousKeys = Object.keys(previousAlphas);
|
|
138
|
+
for (let i = 0; i < previousKeys.length; i++) {
|
|
139
|
+
const key = Number(previousKeys[i]);
|
|
140
|
+
const previous = previousAlphas[key];
|
|
141
|
+
alphas[key] = { alpha: previous.alpha, text: previous.text };
|
|
142
|
+
}
|
|
143
|
+
let cacheChanged = false;
|
|
133
144
|
|
|
134
145
|
// Create labels for new keys only. A key encodes its time, so the formatted
|
|
135
|
-
// text never changes — re-formatting
|
|
136
|
-
//
|
|
146
|
+
// text never changes — re-formatting every frame would churn strings for
|
|
147
|
+
// the GC. The cache itself is copied below to keep SharedValue payloads
|
|
148
|
+
// immutable after they cross the JS/UI boundary.
|
|
137
149
|
for (let i = 0; i < targetKeys.length; i++) {
|
|
138
150
|
const key = targetKeys[i];
|
|
139
151
|
if (!alphas[key]) {
|
|
140
152
|
alphas[key] = { alpha: 0, text: formatTime(key / 100) };
|
|
153
|
+
cacheChanged = true;
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
156
|
|
|
@@ -160,13 +173,16 @@ export function useXAxis(
|
|
|
160
173
|
|
|
161
174
|
if (next < 0.01 && target === 0) {
|
|
162
175
|
delete alphas[key];
|
|
176
|
+
cacheChanged = true;
|
|
163
177
|
} else {
|
|
164
|
-
//
|
|
178
|
+
// The entry is already a private copy, so updating alpha cannot mutate
|
|
179
|
+
// the serialized cache held by the SharedValue.
|
|
180
|
+
if (label.alpha !== next) cacheChanged = true;
|
|
165
181
|
label.alpha = next;
|
|
166
182
|
}
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
labelAlphas.set(alphas);
|
|
185
|
+
if (cacheChanged) labelAlphas.set(alphas);
|
|
170
186
|
|
|
171
187
|
// Collect visible labels
|
|
172
188
|
const raw: XAxisEntry[] = [];
|
package/src/types.ts
CHANGED
|
@@ -1236,10 +1236,15 @@ export interface MarkerClusterConfig {
|
|
|
1236
1236
|
* A dedicated badge drawn for a collapsed marker cluster — your own Skia design,
|
|
1237
1237
|
* independent of the member markers. Pass it as
|
|
1238
1238
|
* {@link MarkerClusterConfig.groupBadge}. Glyph precedence mirrors a single marker:
|
|
1239
|
-
* `image` → `icon` (`pill`) →
|
|
1240
|
-
*
|
|
1239
|
+
* `image` → `icon` (`pill`) → the built-in count badge. An object containing only
|
|
1240
|
+
* {@link letterSpacing} customizes that built-in count badge.
|
|
1241
1241
|
*/
|
|
1242
1242
|
export interface MarkerGroupBadge {
|
|
1243
|
+
/**
|
|
1244
|
+
* Extra spacing in px between digits in the built-in count badge. Also applies
|
|
1245
|
+
* to the optional corner count on a custom group badge. Default `0`.
|
|
1246
|
+
*/
|
|
1247
|
+
letterSpacing?: number;
|
|
1243
1248
|
/** Custom Skia image for the collapsed group (e.g. from `useImage`). Takes
|
|
1244
1249
|
* precedence over {@link icon}. */
|
|
1245
1250
|
image?: SkImage;
|