react-native-livechart 4.21.0 → 4.22.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/AxisLabelOverlay.d.ts +5 -1
- package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +4 -2
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomThresholdBadgeOverlay.d.ts +3 -1
- package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
- package/dist/components/ExtremaConnectorOverlay.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/MarkerOverlay.d.ts +4 -2
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdLineOverlay.d.ts +3 -1
- package/dist/components/ThresholdLineOverlay.d.ts.map +1 -1
- package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -1
- package/dist/core/chartDataPresence.d.ts +8 -0
- package/dist/core/chartDataPresence.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +3 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +2 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +4 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +3 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +1 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useLiveChartHasData.d.ts +2 -2
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -1
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +10 -5
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/math/referenceLineSeries.d.ts +1 -1
- package/dist/math/referenceLineSeries.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +4 -3
- package/dist/math/threshold.d.ts.map +1 -1
- package/dist/types.d.ts +23 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AxisLabelOverlay.tsx +162 -81
- package/src/components/CrosshairOverlay.tsx +394 -147
- package/src/components/CustomMarkerOverlay.tsx +9 -6
- package/src/components/CustomThresholdBadgeOverlay.tsx +11 -4
- package/src/components/DegenParticlesOverlay.tsx +34 -23
- package/src/components/ExtremaConnectorOverlay.tsx +43 -3
- package/src/components/LiveChart.tsx +1709 -1025
- package/src/components/LiveChartSeries.tsx +445 -296
- package/src/components/MarkerOverlay.tsx +215 -156
- package/src/components/ReferenceLineOverlay.tsx +77 -50
- package/src/components/ReferenceLineSeriesOverlay.tsx +2 -0
- package/src/components/ThresholdLineOverlay.tsx +16 -6
- package/src/components/thresholdSplitShaderSource.ts +4 -1
- package/src/core/chartDataPresence.ts +24 -0
- package/src/core/liveChartEngineTick.ts +4 -0
- package/src/core/resolveConfig.ts +3 -0
- package/src/core/useLiveChartEngine.ts +16 -3
- package/src/core/useLiveChartSeriesEngine.ts +2 -0
- package/src/draw/markerAtlas.ts +8 -3
- package/src/hooks/useChartReveal.ts +1 -1
- package/src/hooks/useCrosshair.ts +69 -46
- package/src/hooks/useDegen.ts +82 -53
- package/src/hooks/useLiveChartHasData.ts +8 -4
- package/src/hooks/useReferenceLineSeries.ts +5 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
- package/src/hooks/useThreshold.ts +41 -15
- package/src/math/referenceLineSeries.ts +9 -4
- package/src/math/threshold.ts +10 -3
- package/src/types.ts +23 -8
|
@@ -37,6 +37,9 @@ export function buildThresholdSplitShaderSource(sampleCount: number): string {
|
|
|
37
37
|
// and glides with the window so features move fluidly, frame to frame.
|
|
38
38
|
uniform float sampleLeft;
|
|
39
39
|
uniform float sampleRight;
|
|
40
|
+
// X where the threshold starts (extendToStart: false → the first point's
|
|
41
|
+
// pixel-X, else a large negative sentinel). Fragments left of it use restColor.
|
|
42
|
+
uniform float clipLeft;
|
|
40
43
|
// X where the threshold ends (extendToNow: false → the last point's pixel-X,
|
|
41
44
|
// else a huge sentinel). Fragments right of it paint restColor: the plain line
|
|
42
45
|
// color for the stroke, transparent for the band.
|
|
@@ -51,7 +54,7 @@ ${lookup}
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
half4 main(vec2 xy) {
|
|
54
|
-
if (xy.x > clipRight) {
|
|
57
|
+
if (xy.x < clipLeft || xy.x > clipRight) {
|
|
55
58
|
return half4(half3(restColor.rgb) * restColor.a, restColor.a);
|
|
56
59
|
}
|
|
57
60
|
float span = sampleRight - sampleLeft;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/** A single historical line sample is enough to anchor the live tip. */
|
|
4
|
+
export function hasLineChartData(points: LiveChartPoint[]): boolean {
|
|
5
|
+
"worklet";
|
|
6
|
+
return points.length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A single committed OHLC bucket is drawable as a candle. */
|
|
10
|
+
export function hasCandleChartData(
|
|
11
|
+
candles: CandlePoint[] | undefined,
|
|
12
|
+
): boolean {
|
|
13
|
+
"worklet";
|
|
14
|
+
return (candles?.length ?? 0) > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Multi-series charts are non-empty when any series has one sample. */
|
|
18
|
+
export function hasMultiSeriesChartData(series: SeriesConfig[]): boolean {
|
|
19
|
+
"worklet";
|
|
20
|
+
for (let i = 0; i < series.length; i++) {
|
|
21
|
+
if (series[i].data.length > 0) return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
@@ -61,6 +61,9 @@ export interface EngineTickInput {
|
|
|
61
61
|
* but windowed each tick.
|
|
62
62
|
*/
|
|
63
63
|
thresholdRangePoints?: LiveChartPoint[];
|
|
64
|
+
/** Whether {@link thresholdRangePoints} extends flat before its first point to
|
|
65
|
+
* the visible window start (`threshold.extendToStart`). Default `true`. */
|
|
66
|
+
thresholdRangeExtendToStart?: boolean;
|
|
64
67
|
/** Whether {@link thresholdRangePoints} extends flat past its last point to
|
|
65
68
|
* "now" (`threshold.extendToNow`). Default `true`. */
|
|
66
69
|
thresholdRangeExtendToNow?: boolean;
|
|
@@ -367,6 +370,7 @@ export function tickLiveChartEngineFrame(
|
|
|
367
370
|
thrPts,
|
|
368
371
|
state.timestamp,
|
|
369
372
|
state.displayWindow,
|
|
373
|
+
input.thresholdRangeExtendToStart ?? true,
|
|
370
374
|
input.thresholdRangeExtendToNow ?? true,
|
|
371
375
|
THRESHOLD_RANGE_SCRATCH,
|
|
372
376
|
);
|
|
@@ -447,6 +447,8 @@ export interface ResolvedThresholdConfig {
|
|
|
447
447
|
fillOpacity: number;
|
|
448
448
|
/** Fold the threshold into the Y-axis range fit. */
|
|
449
449
|
includeInRange: boolean;
|
|
450
|
+
/** Series forms: extend flat before the first point to the visible window's start. */
|
|
451
|
+
extendToStart: boolean;
|
|
450
452
|
/** Series forms: extend flat past the last point to "now". */
|
|
451
453
|
extendToNow: boolean;
|
|
452
454
|
line: ResolvedThresholdLineConfig | null;
|
|
@@ -495,6 +497,7 @@ export function resolveThreshold(
|
|
|
495
497
|
(typeof prop.fill === "object" ? prop.fill.opacity : undefined) ??
|
|
496
498
|
THRESHOLD_FILL_OPACITY_DEFAULT,
|
|
497
499
|
includeInRange: prop.includeInRange ?? false,
|
|
500
|
+
extendToStart: prop.extendToStart ?? true,
|
|
498
501
|
extendToNow: prop.extendToNow ?? true,
|
|
499
502
|
line: resolveThresholdLine(prop.line),
|
|
500
503
|
};
|
|
@@ -54,6 +54,9 @@ export interface EngineConfig {
|
|
|
54
54
|
* `LiveChartPoint[]` form.
|
|
55
55
|
*/
|
|
56
56
|
thresholdRangePoints?: SharedValue<LiveChartPoint[]> | LiveChartPoint[];
|
|
57
|
+
/** With {@link thresholdRangePoints}: whether the series extends flat before
|
|
58
|
+
* its first point to the visible window start. Default `true`. */
|
|
59
|
+
thresholdRangeExtendToStart?: boolean;
|
|
57
60
|
/** With {@link thresholdRangePoints}: whether the series extends flat past its
|
|
58
61
|
* last point to "now" (`threshold.extendToNow`). Default `true`. */
|
|
59
62
|
thresholdRangeExtendToNow?: boolean;
|
|
@@ -217,6 +220,7 @@ export interface EngineFrameRefs {
|
|
|
217
220
|
referenceValues?: SharedValue<number[] | undefined>;
|
|
218
221
|
/** Series threshold folded into the range fit (`threshold.includeInRange`). */
|
|
219
222
|
thresholdRangePoints?: SharedValue<LiveChartPoint[] | undefined>;
|
|
223
|
+
thresholdRangeExtendToStart?: SharedValue<boolean>;
|
|
220
224
|
thresholdRangeExtendToNow?: SharedValue<boolean>;
|
|
221
225
|
nonNegativeSV?: SharedValue<boolean>;
|
|
222
226
|
maxValueSV?: SharedValue<number | undefined>;
|
|
@@ -353,6 +357,8 @@ export function applyLiveChartEngineFrame(
|
|
|
353
357
|
input.referenceValue = sv.referenceValue.value;
|
|
354
358
|
input.referenceValues = sv.referenceValues?.value;
|
|
355
359
|
input.thresholdRangePoints = sv.thresholdRangePoints?.value;
|
|
360
|
+
input.thresholdRangeExtendToStart =
|
|
361
|
+
sv.thresholdRangeExtendToStart?.value ?? true;
|
|
356
362
|
input.thresholdRangeExtendToNow = sv.thresholdRangeExtendToNow?.value ?? true;
|
|
357
363
|
input.nonNegative = sv.nonNegativeSV?.value ?? false;
|
|
358
364
|
input.maxValue = sv.maxValueSV?.value;
|
|
@@ -453,6 +459,9 @@ export function useLiveChartEngine(
|
|
|
453
459
|
const thresholdRangeExtendToNow = useDerivedValue(
|
|
454
460
|
() => config.thresholdRangeExtendToNow ?? true,
|
|
455
461
|
);
|
|
462
|
+
const thresholdRangeExtendToStart = useDerivedValue(
|
|
463
|
+
() => config.thresholdRangeExtendToStart ?? true,
|
|
464
|
+
);
|
|
456
465
|
const nonNegativeSV = useDerivedValue(() => config.nonNegative ?? false);
|
|
457
466
|
const maxValueSV = useDerivedValue(() => config.maxValue);
|
|
458
467
|
const nowOverrideSV = useDerivedValue(() => config.nowOverride);
|
|
@@ -559,6 +568,7 @@ export function useLiveChartEngine(
|
|
|
559
568
|
referenceValue,
|
|
560
569
|
referenceValues,
|
|
561
570
|
thresholdRangePoints,
|
|
571
|
+
thresholdRangeExtendToStart,
|
|
562
572
|
thresholdRangeExtendToNow,
|
|
563
573
|
nonNegativeSV,
|
|
564
574
|
maxValueSV,
|
|
@@ -586,9 +596,12 @@ export function useLiveChartEngine(
|
|
|
586
596
|
extremaMinTime,
|
|
587
597
|
extremaMaxTime,
|
|
588
598
|
};
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
599
|
+
const frameScratchRef = useRef<EngineFrameScratch | null>(null);
|
|
600
|
+
if (frameScratchRef.current === null) {
|
|
601
|
+
// React permits this predictable lazy-ref initialization: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
|
|
602
|
+
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render -- false positive for React's documented lazy-ref exception
|
|
603
|
+
frameScratchRef.current = makeEngineFrameScratch();
|
|
604
|
+
}
|
|
592
605
|
|
|
593
606
|
// `autostart=false` registers the frame callback without running it — the live
|
|
594
607
|
// loop is fully inert in static mode (the invariant that makes this worth it).
|
|
@@ -356,6 +356,8 @@ export function useLiveChartSeriesEngine(
|
|
|
356
356
|
// Reused per-frame output buffers (ping-ponged) — see applyLiveChartSeriesEngineFrame.
|
|
357
357
|
const scratchRef = useRef<MultiSeriesEngineScratch | null>(null);
|
|
358
358
|
if (scratchRef.current === null) {
|
|
359
|
+
// React permits this predictable lazy-ref initialization: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
|
|
360
|
+
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render -- false positive for React's documented lazy-ref exception
|
|
359
361
|
scratchRef.current = makeMultiSeriesEngineScratch();
|
|
360
362
|
}
|
|
361
363
|
|
package/src/draw/markerAtlas.ts
CHANGED
|
@@ -17,6 +17,11 @@ import type {
|
|
|
17
17
|
MarkerKind,
|
|
18
18
|
} from "../types";
|
|
19
19
|
|
|
20
|
+
export type MarkerAtlasPalette = Pick<
|
|
21
|
+
LiveChartPalette,
|
|
22
|
+
"bgRgb" | "line" | "refLine" | "dotUp" | "refLabel"
|
|
23
|
+
>;
|
|
24
|
+
|
|
20
25
|
/** Default icon box (px) when `marker.size` is unset. */
|
|
21
26
|
export const DEFAULT_ICON_SIZE = 16;
|
|
22
27
|
/** Circular badge padding around the icon glyph + background ring width. */
|
|
@@ -78,7 +83,7 @@ export function groupCountTextWidth(
|
|
|
78
83
|
* per-frame overlay worklet can resolve a collapsed cluster's badge color. */
|
|
79
84
|
export function defaultMarkerColor(
|
|
80
85
|
kind: MarkerKind,
|
|
81
|
-
palette:
|
|
86
|
+
palette: MarkerAtlasPalette,
|
|
82
87
|
): string {
|
|
83
88
|
"worklet";
|
|
84
89
|
switch (kind) {
|
|
@@ -188,7 +193,7 @@ interface CellSpec {
|
|
|
188
193
|
* Geometry + draw routine for one marker appearance. Mirrors the per-glyph
|
|
189
194
|
* rendering the old `MarkerGlyph` did inline, so the atlas is pixel-equivalent.
|
|
190
195
|
*/
|
|
191
|
-
function cellSpec(m: Marker, palette:
|
|
196
|
+
function cellSpec(m: Marker, palette: MarkerAtlasPalette, font: SkFont): CellSpec {
|
|
192
197
|
const color = m.color ?? defaultMarkerColor(m.kind, palette);
|
|
193
198
|
const m2 = CELL_MARGIN * 2;
|
|
194
199
|
|
|
@@ -382,7 +387,7 @@ function groupBgCellSpec(
|
|
|
382
387
|
*/
|
|
383
388
|
export function buildMarkerAtlas(
|
|
384
389
|
markers: Marker[],
|
|
385
|
-
palette:
|
|
390
|
+
palette: MarkerAtlasPalette,
|
|
386
391
|
font: SkFont,
|
|
387
392
|
scale = 1,
|
|
388
393
|
/** Also bake count-badge cells (digits + per-color backgrounds) for
|
|
@@ -39,7 +39,7 @@ export interface ChartRevealState {
|
|
|
39
39
|
morphT: SharedValue<number>;
|
|
40
40
|
/** True while loading=true */
|
|
41
41
|
isLoading: SharedValue<boolean>;
|
|
42
|
-
/** True when not loading and
|
|
42
|
+
/** True when not loading and no samples are available. */
|
|
43
43
|
isEmpty: SharedValue<boolean>;
|
|
44
44
|
yAxisOpacity: SharedValue<number>;
|
|
45
45
|
fillOpacity: SharedValue<number>;
|
|
@@ -114,6 +114,34 @@ export interface CrosshairChartOpts {
|
|
|
114
114
|
bridgeUnknown?: boolean;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function resolveCrosshairSettings(
|
|
118
|
+
chartOpts: CrosshairChartOpts | undefined,
|
|
119
|
+
scrubAction: ResolvedScrubActionConfig | null | undefined,
|
|
120
|
+
onScrubAction: ((point: ScrubActionPoint) => void) | undefined,
|
|
121
|
+
clampToPlot: boolean,
|
|
122
|
+
) {
|
|
123
|
+
const hasScrubAction = scrubAction != null;
|
|
124
|
+
return {
|
|
125
|
+
hasScrubAction,
|
|
126
|
+
clampPlainScrubToPlot: clampToPlot && !hasScrubAction,
|
|
127
|
+
hasOnScrubAction: onScrubAction != null,
|
|
128
|
+
dismissOnTapOutside: scrubAction?.dismissOnTapOutside ?? false,
|
|
129
|
+
dismissOnAction: scrubAction?.dismissOnAction ?? false,
|
|
130
|
+
snapIncrement: scrubAction?.snap,
|
|
131
|
+
actionIcon: scrubAction?.icon ?? "+",
|
|
132
|
+
actionShowText: scrubAction?.text ?? true,
|
|
133
|
+
hasTimeBadge: scrubAction?.timeBadge ?? false,
|
|
134
|
+
isCandleMode: chartOpts?.mode === "candle",
|
|
135
|
+
candlesSV: chartOpts?.candles,
|
|
136
|
+
liveCandleSV: chartOpts?.liveCandle,
|
|
137
|
+
candleWidthSecs: chartOpts?.candleWidthSecs ?? 60,
|
|
138
|
+
chartGaps: chartOpts?.gaps ?? [],
|
|
139
|
+
bridgeNoTrades: chartOpts?.bridgeNoTrades ?? false,
|
|
140
|
+
bridgeUnavailable: chartOpts?.bridgeUnavailable ?? false,
|
|
141
|
+
bridgeUnknown: chartOpts?.bridgeUnknown ?? false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
117
145
|
/**
|
|
118
146
|
* Single-series crosshair + scrub. Use `useCrosshairSeries` for `LiveChartSeries`.
|
|
119
147
|
*/
|
|
@@ -211,24 +239,30 @@ export function useCrosshair(
|
|
|
211
239
|
// screen Y is re-derived from it each frame (see `lockY` below) so the level
|
|
212
240
|
// tracks the price as the axis rescales instead of drifting under a fixed pixel.
|
|
213
241
|
const lockPriceValue = useSharedValue<number | null>(null);
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
const {
|
|
243
|
+
hasScrubAction,
|
|
244
|
+
clampPlainScrubToPlot,
|
|
245
|
+
hasOnScrubAction,
|
|
246
|
+
dismissOnTapOutside,
|
|
247
|
+
dismissOnAction,
|
|
248
|
+
snapIncrement,
|
|
249
|
+
actionIcon,
|
|
250
|
+
actionShowText,
|
|
251
|
+
hasTimeBadge,
|
|
252
|
+
isCandleMode,
|
|
253
|
+
candlesSV,
|
|
254
|
+
liveCandleSV,
|
|
255
|
+
candleWidthSecs,
|
|
256
|
+
chartGaps,
|
|
257
|
+
bridgeNoTrades,
|
|
258
|
+
bridgeUnavailable,
|
|
259
|
+
bridgeUnknown,
|
|
260
|
+
} = resolveCrosshairSettings(
|
|
261
|
+
chartOpts,
|
|
262
|
+
scrubAction,
|
|
263
|
+
onScrubAction,
|
|
264
|
+
clampToPlot,
|
|
265
|
+
);
|
|
232
266
|
|
|
233
267
|
// Monospace advance width, measured once per render (not per scrub frame) so
|
|
234
268
|
// the tooltip layout worklet can size text by character count instead of a
|
|
@@ -407,9 +441,7 @@ export function useCrosshair(
|
|
|
407
441
|
const canvasWidth = engine.canvasWidth.get();
|
|
408
442
|
const canvasHeight = engine.canvasHeight.get();
|
|
409
443
|
const price =
|
|
410
|
-
active && rawPrice !== null
|
|
411
|
-
? snapPrice(rawPrice, snapIncrement)
|
|
412
|
-
: null;
|
|
444
|
+
active && rawPrice !== null ? snapPrice(rawPrice, snapIncrement) : null;
|
|
413
445
|
let y = -1;
|
|
414
446
|
if (
|
|
415
447
|
rawPrice !== null &&
|
|
@@ -586,15 +618,7 @@ export function useCrosshair(
|
|
|
586
618
|
string | null,
|
|
587
619
|
string | null,
|
|
588
620
|
];
|
|
589
|
-
scheduleOnRN(
|
|
590
|
-
handleScrub,
|
|
591
|
-
row[2],
|
|
592
|
-
row[3],
|
|
593
|
-
row[0],
|
|
594
|
-
row[1],
|
|
595
|
-
row[4],
|
|
596
|
-
row[5],
|
|
597
|
-
);
|
|
621
|
+
scheduleOnRN(handleScrub, row[2], row[3], row[0], row[1], row[4], row[5]);
|
|
598
622
|
},
|
|
599
623
|
);
|
|
600
624
|
|
|
@@ -667,13 +691,7 @@ export function useCrosshair(
|
|
|
667
691
|
.onTouchesUp(
|
|
668
692
|
/* istanbul ignore next */ (e, manager) => {
|
|
669
693
|
"worklet";
|
|
670
|
-
delayedPanTouchUp(
|
|
671
|
-
longPressMs,
|
|
672
|
-
e,
|
|
673
|
-
manager,
|
|
674
|
-
fingerDown,
|
|
675
|
-
panActivated,
|
|
676
|
-
);
|
|
694
|
+
delayedPanTouchUp(longPressMs, e, manager, fingerDown, panActivated);
|
|
677
695
|
},
|
|
678
696
|
)
|
|
679
697
|
.onTouchesCancelled(
|
|
@@ -706,7 +724,12 @@ export function useCrosshair(
|
|
|
706
724
|
// stays false and the live crosshair hides itself.
|
|
707
725
|
if (hasScrubAction && lockActive.get()) {
|
|
708
726
|
lockX.set(
|
|
709
|
-
clampPlotX(
|
|
727
|
+
clampPlotX(
|
|
728
|
+
e.x,
|
|
729
|
+
padding.left,
|
|
730
|
+
engine.canvasWidth.get(),
|
|
731
|
+
padding.right,
|
|
732
|
+
),
|
|
710
733
|
);
|
|
711
734
|
// Freeze the chosen price (value at the pointer Y); the line's Y is
|
|
712
735
|
// re-derived from it so the level stays put in PRICE as the axis rescales.
|
|
@@ -747,7 +770,12 @@ export function useCrosshair(
|
|
|
747
770
|
if (!enabled) return;
|
|
748
771
|
if (hasScrubAction && lockActive.get()) {
|
|
749
772
|
lockX.set(
|
|
750
|
-
clampPlotX(
|
|
773
|
+
clampPlotX(
|
|
774
|
+
e.x,
|
|
775
|
+
padding.left,
|
|
776
|
+
engine.canvasWidth.get(),
|
|
777
|
+
padding.right,
|
|
778
|
+
),
|
|
751
779
|
);
|
|
752
780
|
// Freeze the chosen price (value at the pointer Y); the line's Y is
|
|
753
781
|
// re-derived from it so the level stays put in PRICE as the axis rescales.
|
|
@@ -895,13 +923,8 @@ export function useCrosshair(
|
|
|
895
923
|
: undefined;
|
|
896
924
|
// Keep the bottom axis scroll-only. Do not apply horizontal plot bounds here:
|
|
897
925
|
// the action badge is intentionally tappable in the right gutter.
|
|
898
|
-
const tapHitSlop = resolveScrubHitSlop(
|
|
899
|
-
|
|
900
|
-
false,
|
|
901
|
-
scrubBottomExclude,
|
|
902
|
-
);
|
|
903
|
-
if (tapGesture && tapHitSlop)
|
|
904
|
-
tapGesture = tapGesture.hitSlop(tapHitSlop);
|
|
926
|
+
const tapHitSlop = resolveScrubHitSlop(padding, false, scrubBottomExclude);
|
|
927
|
+
if (tapGesture && tapHitSlop) tapGesture = tapGesture.hitSlop(tapHitSlop);
|
|
905
928
|
|
|
906
929
|
return {
|
|
907
930
|
scrubX,
|
package/src/hooks/useDegen.ts
CHANGED
|
@@ -13,6 +13,32 @@ import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
|
13
13
|
import { computeShake, spawnBurst, tickParticles } from "../math/degenTick";
|
|
14
14
|
import type { DegenShakePayload, Momentum } from "../types";
|
|
15
15
|
|
|
16
|
+
function resolveDegenSettings(
|
|
17
|
+
config: ResolvedDegenConfig | null,
|
|
18
|
+
hasShakeListener: boolean,
|
|
19
|
+
) {
|
|
20
|
+
return {
|
|
21
|
+
off: config === null,
|
|
22
|
+
scale: config?.scale ?? 1,
|
|
23
|
+
down: config?.downMomentum ?? false,
|
|
24
|
+
shake: config?.shake ?? true,
|
|
25
|
+
shakeIntensity: config?.shakeIntensity ?? 1,
|
|
26
|
+
shakeDurationSec: config?.shakeDurationSec ?? 0.45,
|
|
27
|
+
slotCount: config?.particleSlotCount ?? 60,
|
|
28
|
+
burstParticleCount: config?.burstParticleCount ?? 20,
|
|
29
|
+
particleBurstDurationSec: config?.particleBurstDurationSec ?? 1,
|
|
30
|
+
drag: config?.drag ?? 0.95,
|
|
31
|
+
sizeMin: config?.particleSizeMin ?? 1,
|
|
32
|
+
sizeMax: config?.particleSizeMax ?? 2.2,
|
|
33
|
+
spreadAngle: config?.spreadAngle ?? Math.PI * 1.2,
|
|
34
|
+
jitterX: config?.positionJitterX ?? 24,
|
|
35
|
+
jitterY: config?.positionJitterY ?? 8,
|
|
36
|
+
speedMin: config?.speedMin ?? 60,
|
|
37
|
+
speedMax: config?.speedMax ?? 160,
|
|
38
|
+
hasShakeListener,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
16
42
|
export function useDegen(
|
|
17
43
|
engine: SingleEngineState,
|
|
18
44
|
dotX: SharedValue<number>,
|
|
@@ -65,23 +91,26 @@ export function useDegen(
|
|
|
65
91
|
onShakeRef.current?.({ direction });
|
|
66
92
|
};
|
|
67
93
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
const {
|
|
95
|
+
off,
|
|
96
|
+
scale,
|
|
97
|
+
down,
|
|
98
|
+
shake,
|
|
99
|
+
shakeIntensity,
|
|
100
|
+
shakeDurationSec,
|
|
101
|
+
slotCount,
|
|
102
|
+
burstParticleCount,
|
|
103
|
+
particleBurstDurationSec,
|
|
104
|
+
drag,
|
|
105
|
+
sizeMin,
|
|
106
|
+
sizeMax,
|
|
107
|
+
spreadAngle,
|
|
108
|
+
jitterX,
|
|
109
|
+
jitterY,
|
|
110
|
+
speedMin,
|
|
111
|
+
speedMax,
|
|
112
|
+
hasShakeListener,
|
|
113
|
+
} = resolveDegenSettings(cfg, onShake != null);
|
|
85
114
|
|
|
86
115
|
useEffect(() => {
|
|
87
116
|
// Mirror the resolved config into SharedValues for the UI-thread worklet.
|
|
@@ -90,43 +119,43 @@ export function useDegen(
|
|
|
90
119
|
// is off, enabledSV is 0 and the frame worklet early-returns (zeroing the
|
|
91
120
|
// particle buffer and shake itself), so the remaining values are inert; the
|
|
92
121
|
// resolved* defaults already stand in for the null-config case.
|
|
93
|
-
enabledSV.set(
|
|
94
|
-
shakeEnabledSV.set(!
|
|
95
|
-
hasOnShakeListenerSV.set(!
|
|
96
|
-
scaleSV.set(
|
|
97
|
-
downSV.set(
|
|
98
|
-
shakeIntensitySV.set(
|
|
99
|
-
shakeDurationSecSV.set(
|
|
100
|
-
slotCountSV.set(
|
|
101
|
-
burstParticleCountSV.set(
|
|
102
|
-
particleBurstDurationSecSV.set(
|
|
103
|
-
dragSV.set(
|
|
104
|
-
sizeMinSV.set(
|
|
105
|
-
sizeMaxSV.set(
|
|
106
|
-
spreadAngleSV.set(
|
|
107
|
-
jitterXSV.set(
|
|
108
|
-
jitterYSV.set(
|
|
109
|
-
speedMinSV.set(
|
|
110
|
-
speedMaxSV.set(
|
|
122
|
+
enabledSV.set(off ? 0 : 1);
|
|
123
|
+
shakeEnabledSV.set(!off && shake ? 1 : 0);
|
|
124
|
+
hasOnShakeListenerSV.set(!off && hasShakeListener ? 1 : 0);
|
|
125
|
+
scaleSV.set(scale);
|
|
126
|
+
downSV.set(down ? 1 : 0);
|
|
127
|
+
shakeIntensitySV.set(shakeIntensity);
|
|
128
|
+
shakeDurationSecSV.set(shakeDurationSec);
|
|
129
|
+
slotCountSV.set(slotCount);
|
|
130
|
+
burstParticleCountSV.set(burstParticleCount);
|
|
131
|
+
particleBurstDurationSecSV.set(particleBurstDurationSec);
|
|
132
|
+
dragSV.set(drag);
|
|
133
|
+
sizeMinSV.set(sizeMin);
|
|
134
|
+
sizeMaxSV.set(sizeMax);
|
|
135
|
+
spreadAngleSV.set(spreadAngle);
|
|
136
|
+
jitterXSV.set(jitterX);
|
|
137
|
+
jitterYSV.set(jitterY);
|
|
138
|
+
speedMinSV.set(speedMin);
|
|
139
|
+
speedMaxSV.set(speedMax);
|
|
111
140
|
}, [
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
off,
|
|
142
|
+
scale,
|
|
143
|
+
down,
|
|
144
|
+
shake,
|
|
145
|
+
shakeIntensity,
|
|
146
|
+
shakeDurationSec,
|
|
147
|
+
slotCount,
|
|
148
|
+
burstParticleCount,
|
|
149
|
+
particleBurstDurationSec,
|
|
150
|
+
drag,
|
|
151
|
+
sizeMin,
|
|
152
|
+
sizeMax,
|
|
153
|
+
spreadAngle,
|
|
154
|
+
jitterX,
|
|
155
|
+
jitterY,
|
|
156
|
+
speedMin,
|
|
157
|
+
speedMax,
|
|
158
|
+
hasShakeListener,
|
|
130
159
|
// SharedValue refs are stable (useSharedValue), so listing them satisfies
|
|
131
160
|
// exhaustive-deps without ever re-running the effect.
|
|
132
161
|
enabledSV,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
import {
|
|
3
|
+
hasCandleChartData,
|
|
4
|
+
hasLineChartData,
|
|
5
|
+
} from "../core/chartDataPresence";
|
|
2
6
|
import type { CandlePoint, LiveChartPoint } from "../types";
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
|
-
* Data presence for {@link LiveChart}: line mode needs ≥
|
|
6
|
-
* candle mode needs ≥
|
|
9
|
+
* Data presence for {@link LiveChart}: line mode needs ≥1 point;
|
|
10
|
+
* candle mode needs ≥1 committed candle (`liveCandle` alone does not count).
|
|
7
11
|
*
|
|
8
12
|
* {@link useChartReveal} seeds its first-paint state off `hasData` directly (in a
|
|
9
13
|
* layout effect), so no JS-thread snapshot is read during render here.
|
|
@@ -22,9 +26,9 @@ export function useLiveChartHasData({
|
|
|
22
26
|
const hasData = useDerivedValue(() => {
|
|
23
27
|
"worklet";
|
|
24
28
|
if (isCandle) {
|
|
25
|
-
return (candles?.value
|
|
29
|
+
return hasCandleChartData(candles?.value);
|
|
26
30
|
}
|
|
27
|
-
return data.value
|
|
31
|
+
return hasLineChartData(data.value);
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
return { hasData };
|
|
@@ -34,6 +34,7 @@ export function useReferenceLineSeries(
|
|
|
34
34
|
engine: ChartEngineLayout,
|
|
35
35
|
padding: ChartPadding,
|
|
36
36
|
points: LiveChartPoint[],
|
|
37
|
+
extendToStart: boolean,
|
|
37
38
|
extendToNow: boolean,
|
|
38
39
|
): ReferenceLineSeriesGeometry {
|
|
39
40
|
const cacheRef = useRef<{
|
|
@@ -57,6 +58,7 @@ export function useReferenceLineSeries(
|
|
|
57
58
|
engine.canvasWidth.get(),
|
|
58
59
|
engine.canvasHeight.get(),
|
|
59
60
|
padding,
|
|
61
|
+
extendToStart,
|
|
60
62
|
extendToNow,
|
|
61
63
|
cache.tick ? cache.a : cache.b,
|
|
62
64
|
);
|
|
@@ -86,6 +88,9 @@ export function useReferenceLineSeries(
|
|
|
86
88
|
);
|
|
87
89
|
const currentVisible = useDerivedValue(() => {
|
|
88
90
|
if (points.length === 0) return false;
|
|
91
|
+
if (!extendToStart && points[0].time > engine.timestamp.get()) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
89
94
|
if (!extendToNow && points[points.length - 1].time < engine.timestamp.get()) {
|
|
90
95
|
return false;
|
|
91
96
|
}
|
|
@@ -3,6 +3,11 @@ import {
|
|
|
3
3
|
useSharedValue,
|
|
4
4
|
type SharedValue,
|
|
5
5
|
} from "react-native-reanimated";
|
|
6
|
+
import {
|
|
7
|
+
hasCandleChartData,
|
|
8
|
+
hasLineChartData,
|
|
9
|
+
hasMultiSeriesChartData,
|
|
10
|
+
} from "../core/chartDataPresence";
|
|
6
11
|
import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
|
|
7
12
|
|
|
8
13
|
const STASH_MORPH_EPS = 0.01;
|
|
@@ -42,20 +47,19 @@ export function useSingleChartReverseMorphInputs({
|
|
|
42
47
|
has: hasData.get(),
|
|
43
48
|
m: morphT.get(),
|
|
44
49
|
d: data.get(),
|
|
45
|
-
cLen: candles?.get().length ?? 0,
|
|
46
50
|
c: candles?.get(),
|
|
47
51
|
lc: liveCandle?.get() ?? null,
|
|
48
52
|
}),
|
|
49
53
|
(curr) => {
|
|
50
54
|
"worklet";
|
|
51
55
|
if (!curr.candle) {
|
|
52
|
-
if (curr.has && curr.d
|
|
56
|
+
if (curr.has && hasLineChartData(curr.d)) {
|
|
53
57
|
lineStash.set(curr.d.slice());
|
|
54
58
|
lineEngineData.set(curr.d);
|
|
55
59
|
} else if (
|
|
56
60
|
!curr.has &&
|
|
57
61
|
curr.m > STASH_MORPH_EPS &&
|
|
58
|
-
lineStash.get()
|
|
62
|
+
hasLineChartData(lineStash.get())
|
|
59
63
|
) {
|
|
60
64
|
lineEngineData.set(lineStash.get());
|
|
61
65
|
} else {
|
|
@@ -65,14 +69,14 @@ export function useSingleChartReverseMorphInputs({
|
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
const cArr = curr.c;
|
|
68
|
-
if (curr.has &&
|
|
72
|
+
if (curr.has && hasCandleChartData(cArr) && cArr) {
|
|
69
73
|
candleStash.set(cArr.slice());
|
|
70
74
|
candlesEngine.set(cArr);
|
|
71
75
|
liveEngine.set(curr.lc);
|
|
72
76
|
} else if (
|
|
73
77
|
!curr.has &&
|
|
74
78
|
curr.m > STASH_MORPH_EPS &&
|
|
75
|
-
candleStash.get()
|
|
79
|
+
hasCandleChartData(candleStash.get())
|
|
76
80
|
) {
|
|
77
81
|
candlesEngine.set(candleStash.get());
|
|
78
82
|
liveEngine.set(null);
|
|
@@ -118,27 +122,14 @@ export function useMultiSeriesReverseMorphInputs({
|
|
|
118
122
|
(curr) => {
|
|
119
123
|
"worklet";
|
|
120
124
|
const live = curr.live;
|
|
121
|
-
|
|
122
|
-
let anyReady = false;
|
|
123
|
-
for (let i = 0; i < live.length; i++) {
|
|
124
|
-
if (live[i].data.length >= 2) {
|
|
125
|
-
anyReady = true;
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
125
|
+
const anyReady = hasMultiSeriesChartData(live);
|
|
129
126
|
|
|
130
127
|
if (curr.has && anyReady) {
|
|
131
128
|
seriesStash.set(snapshotSeriesWorklet(live));
|
|
132
129
|
effectiveSeries.set(live);
|
|
133
130
|
} else {
|
|
134
|
-
let stashReady = false;
|
|
135
131
|
const stash = seriesStash.get();
|
|
136
|
-
|
|
137
|
-
if (stash[i].data.length >= 2) {
|
|
138
|
-
stashReady = true;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
132
|
+
const stashReady = hasMultiSeriesChartData(stash);
|
|
142
133
|
|
|
143
134
|
if (!curr.has && curr.m > STASH_MORPH_EPS && stashReady) {
|
|
144
135
|
effectiveSeries.set(seriesStash.get());
|