react-native-livechart 4.20.0 → 4.21.1
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 +18 -0
- package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +4 -2
- package/dist/components/CustomTooltipOverlay.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 +10 -5
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
- package/dist/core/chartDataPresence.d.ts +8 -0
- package/dist/core/chartDataPresence.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +6 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +32 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +3 -1
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/candleGap.d.ts +14 -0
- package/dist/draw/candleGap.d.ts.map +1 -0
- package/dist/draw/lineGap.d.ts +25 -0
- package/dist/draw/lineGap.d.ts.map +1 -0
- package/dist/draw/markerAtlas.d.ts +3 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +7 -3
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCandleGapPaths.d.ts +12 -0
- package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +4 -1
- package/dist/hooks/useChartPaths.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 +9 -5
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useLineGapPaths.d.ts +10 -0
- package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +2 -2
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +4 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +13 -13
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/candleGaps.d.ts +18 -0
- package/dist/math/candleGaps.d.ts.map +1 -0
- package/dist/math/referenceLineSeries.d.ts +12 -0
- package/dist/math/referenceLineSeries.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +5 -5
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/math/spline.d.ts +5 -1
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +191 -17
- 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 +74 -0
- package/src/components/CustomTooltipOverlay.tsx +7 -3
- package/src/components/DegenParticlesOverlay.tsx +34 -23
- package/src/components/ExtremaConnectorOverlay.tsx +43 -3
- package/src/components/LiveChart.tsx +1918 -974
- package/src/components/LiveChartSeries.tsx +445 -296
- package/src/components/MarkerOverlay.tsx +215 -156
- package/src/components/ReferenceLineOverlay.tsx +113 -55
- package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
- package/src/core/chartDataPresence.ts +24 -0
- package/src/core/liveChartEngineTick.ts +34 -1
- package/src/core/resolveConfig.ts +186 -0
- package/src/core/useLiveChartEngine.ts +36 -1
- package/src/core/useLiveChartSeriesEngine.ts +2 -0
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- package/src/draw/lineGap.ts +180 -0
- package/src/draw/markerAtlas.ts +8 -3
- package/src/hooks/crosshairShared.ts +92 -11
- package/src/hooks/useCandleGapPaths.ts +88 -0
- package/src/hooks/useChartPaths.ts +81 -22
- package/src/hooks/useChartReveal.ts +1 -1
- package/src/hooks/useCrosshair.ts +178 -69
- package/src/hooks/useDegen.ts +82 -53
- package/src/hooks/useLineGapPaths.ts +76 -0
- package/src/hooks/useLiveChartHasData.ts +8 -4
- package/src/hooks/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
- package/src/hooks/useThreshold.ts +31 -22
- package/src/index.ts +15 -0
- package/src/math/candleGaps.ts +120 -0
- package/src/math/referenceLineSeries.ts +83 -0
- package/src/math/referenceLines.ts +34 -6
- package/src/math/spline.ts +15 -5
- package/src/types.ts +202 -17
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
SingleEngineState,
|
|
8
8
|
} from "../core/useLiveChartEngine";
|
|
9
9
|
import { buildLinePoints, type ChartPadding } from "../draw/line";
|
|
10
|
+
import { buildLineGapSegmentRanges } from "../draw/lineGap";
|
|
10
11
|
import {
|
|
11
12
|
makeLineSimplifyScratch,
|
|
12
13
|
simplifyLinePoints,
|
|
@@ -15,6 +16,7 @@ import { drawSpline, makeSplineScratch } from "../math/spline";
|
|
|
15
16
|
import { sampleThresholdYAt, thresholdSampleSpanX } from "../math/threshold";
|
|
16
17
|
import { blendPtsY, squigglifyPts } from "../math/squiggly";
|
|
17
18
|
import { usePathBuilder } from "./usePathBuilder";
|
|
19
|
+
import type { CandleGap } from "../types";
|
|
18
20
|
|
|
19
21
|
/** Selects the synthetic right-edge line tip without coupling chart geometry
|
|
20
22
|
* to badge or live-indicator presentation options. */
|
|
@@ -58,6 +60,8 @@ export function useChartPaths(
|
|
|
58
60
|
thresholdSamples?: SharedValue<number[]>,
|
|
59
61
|
/** Screen-space path simplification tolerance in pixels. `0` disables it. */
|
|
60
62
|
simplifyTolerance = 0,
|
|
63
|
+
/** Explicit empty intervals that split line and fill geometry. */
|
|
64
|
+
lineGaps: CandleGap[] = [],
|
|
61
65
|
) {
|
|
62
66
|
const lineBuilder = usePathBuilder();
|
|
63
67
|
const fillBuilder = usePathBuilder();
|
|
@@ -68,6 +72,10 @@ export function useChartPaths(
|
|
|
68
72
|
ptsA: number[];
|
|
69
73
|
ptsB: number[];
|
|
70
74
|
rawPts: number[];
|
|
75
|
+
rangesA: number[];
|
|
76
|
+
rangesB: number[];
|
|
77
|
+
gapXs: number[];
|
|
78
|
+
rangesTick: boolean;
|
|
71
79
|
ptsTick: boolean;
|
|
72
80
|
squigglePts: number[];
|
|
73
81
|
morphA: number[];
|
|
@@ -82,6 +90,10 @@ export function useChartPaths(
|
|
|
82
90
|
ptsA: [] as number[],
|
|
83
91
|
ptsB: [] as number[],
|
|
84
92
|
rawPts: [] as number[],
|
|
93
|
+
rangesA: [] as number[],
|
|
94
|
+
rangesB: [] as number[],
|
|
95
|
+
gapXs: [] as number[],
|
|
96
|
+
rangesTick: false,
|
|
85
97
|
ptsTick: false,
|
|
86
98
|
squigglePts: [] as number[],
|
|
87
99
|
morphA: [] as number[],
|
|
@@ -169,14 +181,39 @@ export function useChartPaths(
|
|
|
169
181
|
);
|
|
170
182
|
});
|
|
171
183
|
|
|
184
|
+
const segmentRanges = useDerivedValue(() => {
|
|
185
|
+
const cache = cacheRef.current!;
|
|
186
|
+
cache.rangesTick = !cache.rangesTick;
|
|
187
|
+
const ranges = cache.rangesTick ? cache.rangesA : cache.rangesB;
|
|
188
|
+
const windowSecs = engine.displayWindow.get();
|
|
189
|
+
const canvasWidth = engine.canvasWidth.get();
|
|
190
|
+
return buildLineGapSegmentRanges(
|
|
191
|
+
flatPts.get(),
|
|
192
|
+
engine.data.get(),
|
|
193
|
+
lineGaps,
|
|
194
|
+
engine.timestamp.get() - windowSecs,
|
|
195
|
+
windowSecs,
|
|
196
|
+
padding.left,
|
|
197
|
+
canvasWidth - padding.left - padding.right,
|
|
198
|
+
ranges,
|
|
199
|
+
cache.gapXs,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
172
203
|
const linePath = useDerivedValue(() => {
|
|
173
204
|
const cache = cacheRef.current!;
|
|
174
205
|
const pts = flatPts.get();
|
|
175
206
|
const n = pts.length >> 1;
|
|
176
207
|
if (n < 2) return cache.emptyPath;
|
|
177
208
|
const b = lineBuilder.value;
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
const ranges = segmentRanges.get();
|
|
210
|
+
if (ranges.length === 0) return cache.emptyPath;
|
|
211
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
212
|
+
const start = ranges[i];
|
|
213
|
+
const end = ranges[i + 1];
|
|
214
|
+
b.moveTo(pts[start * 2], pts[start * 2 + 1]);
|
|
215
|
+
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
216
|
+
}
|
|
180
217
|
return b.detach();
|
|
181
218
|
});
|
|
182
219
|
|
|
@@ -186,12 +223,20 @@ export function useChartPaths(
|
|
|
186
223
|
const n = pts.length >> 1;
|
|
187
224
|
if (n < 2) return cache.emptyPath;
|
|
188
225
|
const b = fillBuilder.value;
|
|
189
|
-
|
|
190
|
-
|
|
226
|
+
const ranges = segmentRanges.get();
|
|
227
|
+
if (ranges.length === 0) return cache.emptyPath;
|
|
191
228
|
const bottom = engine.canvasHeight.get() - padding.bottom;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
229
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
230
|
+
const start = ranges[i];
|
|
231
|
+
const end = ranges[i + 1];
|
|
232
|
+
const first = start * 2;
|
|
233
|
+
const last = (end - 1) * 2;
|
|
234
|
+
b.moveTo(pts[first], pts[first + 1]);
|
|
235
|
+
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
236
|
+
b.lineTo(pts[last], bottom);
|
|
237
|
+
b.lineTo(pts[first], bottom);
|
|
238
|
+
b.close();
|
|
239
|
+
}
|
|
195
240
|
return b.detach();
|
|
196
241
|
});
|
|
197
242
|
|
|
@@ -207,19 +252,17 @@ export function useChartPaths(
|
|
|
207
252
|
const pts = flatPts.get();
|
|
208
253
|
const n = pts.length >> 1;
|
|
209
254
|
if (n < 2) return cache.emptyPath;
|
|
255
|
+
const ranges = segmentRanges.get();
|
|
256
|
+
if (ranges.length === 0) return cache.emptyPath;
|
|
210
257
|
|
|
211
258
|
const tsamples = thresholdSamples?.get();
|
|
212
259
|
if (tsamples && tsamples.length >= 2) {
|
|
213
260
|
const b = thresholdFillBuilder.value;
|
|
214
|
-
b.moveTo(pts[0], pts[1]);
|
|
215
|
-
drawSpline(b, pts, cache.scratch, linear);
|
|
216
261
|
// Band bottom = the SAMPLED threshold (identical to what the split shader
|
|
217
262
|
// reads), pinned to the LINE's x-range. Because the geometry and the shader
|
|
218
263
|
// use the same evenly-spaced, linearly-interpolated samples, a step riser
|
|
219
264
|
// ramps the same way in both — no green/red sliver bleeds through — and the
|
|
220
265
|
// x-range pin keeps the band closing with clean vertical sides (no wedge).
|
|
221
|
-
const leftX = pts[0];
|
|
222
|
-
const rightX = pts[(n - 1) * 2];
|
|
223
266
|
const count = tsamples.length;
|
|
224
267
|
// The samples live on the time-anchored, gliding grid — interpolate them
|
|
225
268
|
// across that span (same as the shader), not the static plot edges.
|
|
@@ -231,13 +274,23 @@ export function useChartPaths(
|
|
|
231
274
|
count,
|
|
232
275
|
);
|
|
233
276
|
const step = (x1 - x0) / (count - 1);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
277
|
+
for (let range = 0; range < ranges.length; range += 2) {
|
|
278
|
+
const start = ranges[range];
|
|
279
|
+
const end = ranges[range + 1];
|
|
280
|
+
const first = start * 2;
|
|
281
|
+
const last = (end - 1) * 2;
|
|
282
|
+
const leftX = pts[first];
|
|
283
|
+
const rightX = pts[last];
|
|
284
|
+
b.moveTo(leftX, pts[first + 1]);
|
|
285
|
+
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
286
|
+
b.lineTo(rightX, sampleThresholdYAt(tsamples, x0, x1, rightX));
|
|
287
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
288
|
+
const sx = x0 + step * i;
|
|
289
|
+
if (sx > leftX && sx < rightX) b.lineTo(sx, tsamples[i]);
|
|
290
|
+
}
|
|
291
|
+
b.lineTo(leftX, sampleThresholdYAt(tsamples, x0, x1, leftX));
|
|
292
|
+
b.close();
|
|
238
293
|
}
|
|
239
|
-
b.lineTo(leftX, sampleThresholdYAt(tsamples, x0, x1, leftX));
|
|
240
|
-
b.close();
|
|
241
294
|
return b.detach();
|
|
242
295
|
}
|
|
243
296
|
|
|
@@ -245,11 +298,17 @@ export function useChartPaths(
|
|
|
245
298
|
const yT = thresholdY.get();
|
|
246
299
|
if (!Number.isFinite(yT)) return cache.emptyPath;
|
|
247
300
|
const b = thresholdFillBuilder.value;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
301
|
+
for (let i = 0; i < ranges.length; i += 2) {
|
|
302
|
+
const start = ranges[i];
|
|
303
|
+
const end = ranges[i + 1];
|
|
304
|
+
const first = start * 2;
|
|
305
|
+
const last = (end - 1) * 2;
|
|
306
|
+
b.moveTo(pts[first], pts[first + 1]);
|
|
307
|
+
drawSpline(b, pts, cache.scratch, linear, start, end);
|
|
308
|
+
b.lineTo(pts[last], yT);
|
|
309
|
+
b.lineTo(pts[first], yT);
|
|
310
|
+
b.close();
|
|
311
|
+
}
|
|
253
312
|
return b.detach();
|
|
254
313
|
});
|
|
255
314
|
|
|
@@ -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>;
|
|
@@ -10,10 +10,16 @@ import { BADGE_METRICS_DEFAULTS, X_AXIS_LABEL_OFFSET_Y } from "../constants";
|
|
|
10
10
|
import type { ResolvedScrubActionConfig } from "../core/resolveConfig";
|
|
11
11
|
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
12
12
|
import { type ChartPadding } from "../draw/line";
|
|
13
|
+
import { lineGapContainsData, previousLineValueAtTime } from "../draw/lineGap";
|
|
13
14
|
import { interpolateAtTime } from "../math/interpolate";
|
|
15
|
+
import {
|
|
16
|
+
pickCandleGapAtTime,
|
|
17
|
+
previousCandleCloseAtTime,
|
|
18
|
+
} from "../math/candleGaps";
|
|
14
19
|
import { pickCandleAtTime } from "../math/pickCandle";
|
|
15
20
|
import type {
|
|
16
21
|
BadgeMetrics,
|
|
22
|
+
CandleGap,
|
|
17
23
|
CandlePoint,
|
|
18
24
|
LiveChartPalette,
|
|
19
25
|
ScrubActionPoint,
|
|
@@ -24,6 +30,7 @@ import {
|
|
|
24
30
|
computeActionBadgeLayout,
|
|
25
31
|
computeCandleTooltipLayout,
|
|
26
32
|
computeCrosshairOpacity,
|
|
33
|
+
computeLineGapTooltipLayout,
|
|
27
34
|
computeScrubDotY,
|
|
28
35
|
computeScrubTime,
|
|
29
36
|
computeTimeBadgeLayout,
|
|
@@ -84,22 +91,55 @@ function clampPlotY(
|
|
|
84
91
|
export type { CrosshairState, TooltipLayout } from "./crosshairShared";
|
|
85
92
|
export type { ScrubPoint };
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
export {
|
|
95
|
+
computeCandleTooltipLayout,
|
|
96
|
+
computeCrosshairOpacity,
|
|
97
|
+
computeLineGapTooltipLayout,
|
|
98
|
+
computeScrubTime,
|
|
99
|
+
computeTooltipLayout,
|
|
100
|
+
computeTooltipLayoutMulti,
|
|
101
|
+
deriveCrosshairTooltipSingle,
|
|
102
|
+
deriveScrubValueSingle,
|
|
103
|
+
HIDDEN_TOOLTIP,
|
|
104
|
+
} from "./crosshairShared";
|
|
97
105
|
|
|
98
|
-
export interface
|
|
106
|
+
export interface CrosshairChartOpts {
|
|
99
107
|
mode: "line" | "candle";
|
|
100
108
|
candles?: SharedValue<CandlePoint[]>;
|
|
101
109
|
liveCandle?: SharedValue<CandlePoint | null>;
|
|
102
|
-
candleWidthSecs
|
|
110
|
+
candleWidthSecs?: number;
|
|
111
|
+
gaps?: CandleGap[];
|
|
112
|
+
bridgeNoTrades?: boolean;
|
|
113
|
+
bridgeUnavailable?: boolean;
|
|
114
|
+
bridgeUnknown?: boolean;
|
|
115
|
+
}
|
|
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
|
+
};
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
/**
|
|
@@ -114,7 +154,7 @@ export function useCrosshair(
|
|
|
114
154
|
font: SkFont,
|
|
115
155
|
enabled: boolean,
|
|
116
156
|
onScrub?: (point: ScrubPoint | null) => void,
|
|
117
|
-
|
|
157
|
+
chartOpts?: CrosshairChartOpts,
|
|
118
158
|
/** Press-and-hold delay (ms) before scrubbing activates. 0 = immediate. */
|
|
119
159
|
panGestureDelay = 0,
|
|
120
160
|
onGestureStart?: () => void,
|
|
@@ -199,20 +239,30 @@ export function useCrosshair(
|
|
|
199
239
|
// screen Y is re-derived from it each frame (see `lockY` below) so the level
|
|
200
240
|
// tracks the price as the axis rescales instead of drifting under a fixed pixel.
|
|
201
241
|
const lockPriceValue = useSharedValue<number | null>(null);
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
);
|
|
216
266
|
|
|
217
267
|
// Monospace advance width, measured once per render (not per scrub frame) so
|
|
218
268
|
// the tooltip layout worklet can size text by character count instead of a
|
|
@@ -220,6 +270,7 @@ export function useCrosshair(
|
|
|
220
270
|
const monoCharWidth = font.measureText("0").width;
|
|
221
271
|
const scrubTime = useSharedValue(-1);
|
|
222
272
|
const scrubCandle = useSharedValue<CandlePoint | null>(null);
|
|
273
|
+
const scrubGap = useSharedValue<CandleGap | null>(null);
|
|
223
274
|
const scrubValue = useSharedValue<number | null>(null);
|
|
224
275
|
const crosshairOpacity = useSharedValue(0);
|
|
225
276
|
const scrubDotY = useSharedValue(-1);
|
|
@@ -252,12 +303,42 @@ export function useCrosshair(
|
|
|
252
303
|
candleWidthSecs,
|
|
253
304
|
)
|
|
254
305
|
: null;
|
|
306
|
+
const lineData = engine.data.get();
|
|
307
|
+
const candidateGap =
|
|
308
|
+
active && time >= 0 && (!isCandleMode || !candle)
|
|
309
|
+
? pickCandleGapAtTime(chartGaps, time)
|
|
310
|
+
: null;
|
|
311
|
+
const gap =
|
|
312
|
+
candidateGap &&
|
|
313
|
+
!isCandleMode &&
|
|
314
|
+
lineGapContainsData(lineData, candidateGap)
|
|
315
|
+
? null
|
|
316
|
+
: candidateGap;
|
|
317
|
+
const gapBridged = gap
|
|
318
|
+
? gap.kind === "no-trades"
|
|
319
|
+
? bridgeNoTrades
|
|
320
|
+
: gap.kind === "unavailable"
|
|
321
|
+
? bridgeUnavailable
|
|
322
|
+
: bridgeUnknown
|
|
323
|
+
: false;
|
|
324
|
+
const gapValue =
|
|
325
|
+
gap && gapBridged
|
|
326
|
+
? isCandleMode && candlesSV
|
|
327
|
+
? previousCandleCloseAtTime(
|
|
328
|
+
candlesSV.get(),
|
|
329
|
+
liveCandleSV?.get() ?? null,
|
|
330
|
+
time,
|
|
331
|
+
)
|
|
332
|
+
: previousLineValueAtTime(lineData, gap.from)
|
|
333
|
+
: null;
|
|
255
334
|
const value =
|
|
256
335
|
!active || time < 0
|
|
257
336
|
? null
|
|
258
337
|
: isCandleMode
|
|
259
|
-
? (candle?.close ??
|
|
260
|
-
:
|
|
338
|
+
? (candle?.close ?? gapValue)
|
|
339
|
+
: gap
|
|
340
|
+
? gapValue
|
|
341
|
+
: interpolateAtTime(lineData, time);
|
|
261
342
|
const dotY = computeScrubDotY(
|
|
262
343
|
value,
|
|
263
344
|
engine.displayMin.get(),
|
|
@@ -278,28 +359,49 @@ export function useCrosshair(
|
|
|
278
359
|
formatTime,
|
|
279
360
|
font,
|
|
280
361
|
monoCharWidth,
|
|
362
|
+
gap,
|
|
363
|
+
gapValue,
|
|
281
364
|
)
|
|
282
|
-
:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
365
|
+
: gap
|
|
366
|
+
? computeLineGapTooltipLayout(
|
|
367
|
+
active,
|
|
368
|
+
x,
|
|
369
|
+
gap,
|
|
370
|
+
time,
|
|
371
|
+
padding,
|
|
372
|
+
canvasWidth,
|
|
373
|
+
formatTime,
|
|
374
|
+
font,
|
|
375
|
+
monoCharWidth,
|
|
376
|
+
tooltipPlacement,
|
|
377
|
+
tooltipShowValue,
|
|
378
|
+
tooltipShowTime,
|
|
379
|
+
canvasHeight,
|
|
380
|
+
tooltipMargin,
|
|
381
|
+
dotY,
|
|
382
|
+
)
|
|
383
|
+
: deriveCrosshairTooltipSingle(
|
|
384
|
+
active,
|
|
385
|
+
x,
|
|
386
|
+
time,
|
|
387
|
+
value,
|
|
388
|
+
padding,
|
|
389
|
+
canvasWidth,
|
|
390
|
+
formatValue,
|
|
391
|
+
formatTime,
|
|
392
|
+
font,
|
|
393
|
+
monoCharWidth,
|
|
394
|
+
tooltipPlacement,
|
|
395
|
+
tooltipShowValue,
|
|
396
|
+
tooltipShowTime,
|
|
397
|
+
canvasHeight,
|
|
398
|
+
tooltipMargin,
|
|
399
|
+
dotY,
|
|
400
|
+
);
|
|
300
401
|
return {
|
|
301
402
|
time,
|
|
302
403
|
candle,
|
|
404
|
+
gap,
|
|
303
405
|
value,
|
|
304
406
|
opacity: computeCrosshairOpacity(active, x, canvasWidth, padding.right),
|
|
305
407
|
dotY,
|
|
@@ -310,6 +412,7 @@ export function useCrosshair(
|
|
|
310
412
|
"worklet";
|
|
311
413
|
scrubTime.set(snapshot.time);
|
|
312
414
|
scrubCandle.set(snapshot.candle);
|
|
415
|
+
scrubGap.set(snapshot.gap);
|
|
313
416
|
scrubValue.set(snapshot.value);
|
|
314
417
|
crosshairOpacity.set(snapshot.opacity);
|
|
315
418
|
scrubDotY.set(snapshot.dotY);
|
|
@@ -338,9 +441,7 @@ export function useCrosshair(
|
|
|
338
441
|
const canvasWidth = engine.canvasWidth.get();
|
|
339
442
|
const canvasHeight = engine.canvasHeight.get();
|
|
340
443
|
const price =
|
|
341
|
-
active && rawPrice !== null
|
|
342
|
-
? snapPrice(rawPrice, snapIncrement)
|
|
343
|
-
: null;
|
|
444
|
+
active && rawPrice !== null ? snapPrice(rawPrice, snapIncrement) : null;
|
|
344
445
|
let y = -1;
|
|
345
446
|
if (
|
|
346
447
|
rawPrice !== null &&
|
|
@@ -440,11 +541,15 @@ export function useCrosshair(
|
|
|
440
541
|
time: number,
|
|
441
542
|
value: number,
|
|
442
543
|
candleJson: string | null,
|
|
544
|
+
gapJson: string | null,
|
|
443
545
|
) {
|
|
444
546
|
const candle: CandlePoint | undefined = candleJson
|
|
445
547
|
? (JSON.parse(candleJson) as CandlePoint)
|
|
446
548
|
: undefined;
|
|
447
|
-
|
|
549
|
+
const gap: CandleGap | undefined = gapJson
|
|
550
|
+
? (JSON.parse(gapJson) as CandleGap)
|
|
551
|
+
: undefined;
|
|
552
|
+
onScrub?.({ time, value, x, y, candle, gap });
|
|
448
553
|
}
|
|
449
554
|
|
|
450
555
|
/* istanbul ignore next */
|
|
@@ -490,7 +595,9 @@ export function useCrosshair(
|
|
|
490
595
|
const c = scrubCandle.get();
|
|
491
596
|
if (c) candleJson = JSON.stringify(c);
|
|
492
597
|
}
|
|
493
|
-
|
|
598
|
+
const gap = scrubGap.get();
|
|
599
|
+
const gapJson = gap ? JSON.stringify(gap) : null;
|
|
600
|
+
return JSON.stringify([time, val, x, dotY, candleJson, gapJson]);
|
|
494
601
|
},
|
|
495
602
|
(curr, prev) => {
|
|
496
603
|
"worklet";
|
|
@@ -509,8 +616,9 @@ export function useCrosshair(
|
|
|
509
616
|
number,
|
|
510
617
|
number,
|
|
511
618
|
string | null,
|
|
619
|
+
string | null,
|
|
512
620
|
];
|
|
513
|
-
scheduleOnRN(handleScrub, row[2], row[3], row[0], row[1], row[4]);
|
|
621
|
+
scheduleOnRN(handleScrub, row[2], row[3], row[0], row[1], row[4], row[5]);
|
|
514
622
|
},
|
|
515
623
|
);
|
|
516
624
|
|
|
@@ -543,6 +651,7 @@ export function useCrosshair(
|
|
|
543
651
|
engine.canvasWidth.get(),
|
|
544
652
|
engine.timestamp.get(),
|
|
545
653
|
engine.displayWindow.get(),
|
|
654
|
+
chartGaps,
|
|
546
655
|
);
|
|
547
656
|
};
|
|
548
657
|
|
|
@@ -582,13 +691,7 @@ export function useCrosshair(
|
|
|
582
691
|
.onTouchesUp(
|
|
583
692
|
/* istanbul ignore next */ (e, manager) => {
|
|
584
693
|
"worklet";
|
|
585
|
-
delayedPanTouchUp(
|
|
586
|
-
longPressMs,
|
|
587
|
-
e,
|
|
588
|
-
manager,
|
|
589
|
-
fingerDown,
|
|
590
|
-
panActivated,
|
|
591
|
-
);
|
|
694
|
+
delayedPanTouchUp(longPressMs, e, manager, fingerDown, panActivated);
|
|
592
695
|
},
|
|
593
696
|
)
|
|
594
697
|
.onTouchesCancelled(
|
|
@@ -621,7 +724,12 @@ export function useCrosshair(
|
|
|
621
724
|
// stays false and the live crosshair hides itself.
|
|
622
725
|
if (hasScrubAction && lockActive.get()) {
|
|
623
726
|
lockX.set(
|
|
624
|
-
clampPlotX(
|
|
727
|
+
clampPlotX(
|
|
728
|
+
e.x,
|
|
729
|
+
padding.left,
|
|
730
|
+
engine.canvasWidth.get(),
|
|
731
|
+
padding.right,
|
|
732
|
+
),
|
|
625
733
|
);
|
|
626
734
|
// Freeze the chosen price (value at the pointer Y); the line's Y is
|
|
627
735
|
// re-derived from it so the level stays put in PRICE as the axis rescales.
|
|
@@ -662,7 +770,12 @@ export function useCrosshair(
|
|
|
662
770
|
if (!enabled) return;
|
|
663
771
|
if (hasScrubAction && lockActive.get()) {
|
|
664
772
|
lockX.set(
|
|
665
|
-
clampPlotX(
|
|
773
|
+
clampPlotX(
|
|
774
|
+
e.x,
|
|
775
|
+
padding.left,
|
|
776
|
+
engine.canvasWidth.get(),
|
|
777
|
+
padding.right,
|
|
778
|
+
),
|
|
666
779
|
);
|
|
667
780
|
// Freeze the chosen price (value at the pointer Y); the line's Y is
|
|
668
781
|
// re-derived from it so the level stays put in PRICE as the axis rescales.
|
|
@@ -810,13 +923,8 @@ export function useCrosshair(
|
|
|
810
923
|
: undefined;
|
|
811
924
|
// Keep the bottom axis scroll-only. Do not apply horizontal plot bounds here:
|
|
812
925
|
// the action badge is intentionally tappable in the right gutter.
|
|
813
|
-
const tapHitSlop = resolveScrubHitSlop(
|
|
814
|
-
|
|
815
|
-
false,
|
|
816
|
-
scrubBottomExclude,
|
|
817
|
-
);
|
|
818
|
-
if (tapGesture && tapHitSlop)
|
|
819
|
-
tapGesture = tapGesture.hitSlop(tapHitSlop);
|
|
926
|
+
const tapHitSlop = resolveScrubHitSlop(padding, false, scrubBottomExclude);
|
|
927
|
+
if (tapGesture && tapHitSlop) tapGesture = tapGesture.hitSlop(tapHitSlop);
|
|
820
928
|
|
|
821
929
|
return {
|
|
822
930
|
scrubX,
|
|
@@ -824,6 +932,7 @@ export function useCrosshair(
|
|
|
824
932
|
scrubTime,
|
|
825
933
|
scrubValue,
|
|
826
934
|
scrubCandle,
|
|
935
|
+
scrubGap,
|
|
827
936
|
crosshairOpacity,
|
|
828
937
|
tooltipLayout,
|
|
829
938
|
scrubDotY,
|