react-native-livechart 4.9.1 → 4.9.2
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.
|
@@ -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,6 +31,7 @@ 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,
|
|
@@ -172,203 +172,216 @@ export function useCrosshair(
|
|
|
172
172
|
const actionShowText = scrubAction?.text ?? true;
|
|
173
173
|
const hasTimeBadge = scrubAction?.timeBadge ?? false;
|
|
174
174
|
|
|
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
175
|
const isCandleMode = candleOpts?.mode === "candle";
|
|
187
176
|
const candlesSV = candleOpts?.candles;
|
|
188
177
|
const liveCandleSV = candleOpts?.liveCandle;
|
|
189
178
|
const candleWidthSecs = candleOpts?.candleWidthSecs ?? 60;
|
|
190
179
|
|
|
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
180
|
// Monospace advance width, measured once per render (not per scrub frame) so
|
|
240
181
|
// the tooltip layout worklet can size text by character count instead of a
|
|
241
182
|
// per-frame Skia measureText.
|
|
242
183
|
const monoCharWidth = font.measureText("0").width;
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
184
|
+
const scrubTime = useSharedValue(-1);
|
|
185
|
+
const scrubCandle = useSharedValue<CandlePoint | null>(null);
|
|
186
|
+
const scrubValue = useSharedValue<number | null>(null);
|
|
187
|
+
const crosshairOpacity = useSharedValue(0);
|
|
188
|
+
const scrubDotY = useSharedValue(-1);
|
|
189
|
+
const tooltipLayout = useSharedValue(HIDDEN_TOOLTIP);
|
|
190
|
+
|
|
191
|
+
// These six outputs traverse the same scrub + engine inputs. One reaction
|
|
192
|
+
// computes and publishes the snapshot atomically instead of compiling six
|
|
193
|
+
// independent derived-value mappers with chained dependencies.
|
|
194
|
+
useAnimatedReaction(
|
|
195
|
+
() => {
|
|
196
|
+
"worklet";
|
|
197
|
+
const active = scrubActive.get();
|
|
198
|
+
const x = scrubX.get();
|
|
199
|
+
const canvasWidth = engine.canvasWidth.get();
|
|
200
|
+
const canvasHeight = engine.canvasHeight.get();
|
|
201
|
+
const time = computeScrubTime(
|
|
202
|
+
active,
|
|
203
|
+
x,
|
|
251
204
|
padding,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
font,
|
|
256
|
-
monoCharWidth,
|
|
205
|
+
canvasWidth,
|
|
206
|
+
engine.timestamp.get(),
|
|
207
|
+
engine.displayWindow.get(),
|
|
257
208
|
);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
209
|
+
const candle =
|
|
210
|
+
isCandleMode && candlesSV && active && time >= 0
|
|
211
|
+
? pickCandleAtTime(
|
|
212
|
+
candlesSV.get(),
|
|
213
|
+
liveCandleSV?.get() ?? null,
|
|
214
|
+
time,
|
|
215
|
+
candleWidthSecs,
|
|
216
|
+
)
|
|
217
|
+
: null;
|
|
218
|
+
const value =
|
|
219
|
+
!active || time < 0
|
|
220
|
+
? null
|
|
221
|
+
: isCandleMode
|
|
222
|
+
? (candle?.close ?? null)
|
|
223
|
+
: interpolateAtTime(engine.data.get(), time);
|
|
224
|
+
const dotY = computeScrubDotY(
|
|
225
|
+
value,
|
|
226
|
+
engine.displayMin.get(),
|
|
227
|
+
engine.displayMax.get(),
|
|
228
|
+
canvasHeight,
|
|
229
|
+
padding.top,
|
|
230
|
+
padding.bottom,
|
|
231
|
+
);
|
|
232
|
+
const layout = isCandleMode
|
|
233
|
+
? computeCandleTooltipLayout(
|
|
234
|
+
active,
|
|
235
|
+
x,
|
|
236
|
+
candle,
|
|
237
|
+
time,
|
|
238
|
+
padding,
|
|
239
|
+
canvasWidth,
|
|
240
|
+
formatValue,
|
|
241
|
+
formatTime,
|
|
242
|
+
font,
|
|
243
|
+
monoCharWidth,
|
|
244
|
+
)
|
|
245
|
+
: deriveCrosshairTooltipSingle(
|
|
246
|
+
active,
|
|
247
|
+
x,
|
|
248
|
+
time,
|
|
249
|
+
value,
|
|
250
|
+
padding,
|
|
251
|
+
canvasWidth,
|
|
252
|
+
formatValue,
|
|
253
|
+
formatTime,
|
|
254
|
+
font,
|
|
255
|
+
monoCharWidth,
|
|
256
|
+
tooltipPlacement,
|
|
257
|
+
tooltipShowValue,
|
|
258
|
+
tooltipShowTime,
|
|
259
|
+
canvasHeight,
|
|
260
|
+
tooltipMargin,
|
|
261
|
+
dotY,
|
|
262
|
+
);
|
|
263
|
+
return {
|
|
264
|
+
time,
|
|
265
|
+
candle,
|
|
266
|
+
value,
|
|
267
|
+
opacity: computeCrosshairOpacity(active, x, canvasWidth, padding.right),
|
|
268
|
+
dotY,
|
|
269
|
+
layout,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
(snapshot) => {
|
|
273
|
+
"worklet";
|
|
274
|
+
scrubTime.set(snapshot.time);
|
|
275
|
+
scrubCandle.set(snapshot.candle);
|
|
276
|
+
scrubValue.set(snapshot.value);
|
|
277
|
+
crosshairOpacity.set(snapshot.opacity);
|
|
278
|
+
scrubDotY.set(snapshot.dotY);
|
|
279
|
+
tooltipLayout.set(snapshot.layout);
|
|
280
|
+
},
|
|
281
|
+
);
|
|
278
282
|
|
|
279
283
|
// ── 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.
|
|
284
|
+
const lockPrice = useSharedValue<number | null>(null);
|
|
285
|
+
const lockY = useSharedValue(-1);
|
|
286
|
+
const lockTime = useSharedValue(-1);
|
|
287
|
+
const lockCandle = useSharedValue<CandlePoint | null>(null);
|
|
288
|
+
const actionBadge = useSharedValue(HIDDEN_ACTION_BADGE);
|
|
289
|
+
const timeBadge = useSharedValue(HIDDEN_TIME_BADGE);
|
|
290
|
+
|
|
291
|
+
// The lock price, projected coordinates, candle and both badges are one
|
|
292
|
+
// coherent reticle snapshot. Publishing them from one reaction replaces six
|
|
293
|
+
// chained derived-value mappers and prevents intermediate mixed-frame state.
|
|
295
294
|
/* 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
|
-
|
|
295
|
+
useAnimatedReaction(
|
|
296
|
+
() => {
|
|
297
|
+
"worklet";
|
|
298
|
+
const active = lockActive.get();
|
|
299
|
+
const x = lockX.get();
|
|
300
|
+
const rawPrice = lockPriceValue.get();
|
|
301
|
+
const canvasWidth = engine.canvasWidth.get();
|
|
302
|
+
const canvasHeight = engine.canvasHeight.get();
|
|
303
|
+
const price =
|
|
304
|
+
active && rawPrice !== null
|
|
305
|
+
? snapPrice(rawPrice, snapIncrement)
|
|
306
|
+
: null;
|
|
307
|
+
let y = -1;
|
|
308
|
+
if (
|
|
309
|
+
rawPrice !== null &&
|
|
310
|
+
canvasHeight - padding.top - padding.bottom > 0
|
|
311
|
+
) {
|
|
312
|
+
y = clampPlotY(
|
|
313
|
+
computeScrubDotY(
|
|
314
|
+
rawPrice,
|
|
315
|
+
engine.displayMin.get(),
|
|
316
|
+
engine.displayMax.get(),
|
|
317
|
+
canvasHeight,
|
|
318
|
+
padding.top,
|
|
319
|
+
padding.bottom,
|
|
320
|
+
),
|
|
321
|
+
padding.top,
|
|
322
|
+
canvasHeight,
|
|
323
|
+
padding.bottom,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const time = computeScrubTime(
|
|
327
|
+
active,
|
|
328
|
+
x,
|
|
329
|
+
padding,
|
|
330
|
+
canvasWidth,
|
|
331
|
+
engine.timestamp.get(),
|
|
332
|
+
engine.displayWindow.get(),
|
|
333
|
+
);
|
|
334
|
+
const candle =
|
|
335
|
+
isCandleMode && candlesSV && active && time >= 0
|
|
336
|
+
? pickCandleAtTime(
|
|
337
|
+
candlesSV.get(),
|
|
338
|
+
liveCandleSV?.get() ?? null,
|
|
339
|
+
time,
|
|
340
|
+
candleWidthSecs,
|
|
341
|
+
)
|
|
342
|
+
: null;
|
|
343
|
+
const badge =
|
|
344
|
+
price === null
|
|
345
|
+
? HIDDEN_ACTION_BADGE
|
|
346
|
+
: computeActionBadgeLayout(
|
|
347
|
+
active,
|
|
348
|
+
y,
|
|
349
|
+
actionShowText ? formatValue(price) : "",
|
|
350
|
+
actionIcon,
|
|
351
|
+
canvasWidth,
|
|
352
|
+
canvasWidth - padding.right,
|
|
353
|
+
font,
|
|
354
|
+
badgeMetrics.marginEdge,
|
|
355
|
+
badgeMetrics.padX,
|
|
356
|
+
badgeMetrics.padY,
|
|
357
|
+
);
|
|
358
|
+
const timeLayout =
|
|
359
|
+
!hasTimeBadge || !active || time < 0
|
|
360
|
+
? HIDDEN_TIME_BADGE
|
|
361
|
+
: computeTimeBadgeLayout(
|
|
362
|
+
active,
|
|
363
|
+
x,
|
|
364
|
+
formatTime(time),
|
|
365
|
+
canvasWidth,
|
|
366
|
+
canvasHeight - padding.bottom + X_AXIS_LABEL_OFFSET_Y,
|
|
367
|
+
font,
|
|
368
|
+
badgeMetrics.padX,
|
|
369
|
+
badgeMetrics.padY,
|
|
370
|
+
badgeMetrics.marginEdge,
|
|
371
|
+
);
|
|
372
|
+
return { price, y, time, candle, badge, timeLayout };
|
|
373
|
+
},
|
|
374
|
+
(snapshot) => {
|
|
375
|
+
"worklet";
|
|
376
|
+
lockPrice.set(snapshot.price);
|
|
377
|
+
lockY.set(snapshot.y);
|
|
378
|
+
lockTime.set(snapshot.time);
|
|
379
|
+
lockCandle.set(snapshot.candle);
|
|
380
|
+
actionBadge.set(snapshot.badge);
|
|
381
|
+
timeBadge.set(snapshot.timeLayout);
|
|
382
|
+
},
|
|
320
383
|
);
|
|
321
384
|
|
|
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
385
|
/* istanbul ignore next -- invoked only via scheduleOnRN from UI-thread gesture */
|
|
373
386
|
function handleScrubAction(
|
|
374
387
|
price: number,
|
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[] = [];
|