react-native-livechart 4.19.0 → 4.21.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/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/LiveChart.d.ts +2 -2
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts +2 -2
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +9 -4
- 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/liveChartEngineTick.d.ts +6 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +34 -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/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/grid.d.ts +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/lineGap.d.ts +25 -0
- package/dist/draw/lineGap.d.ts.map +1 -0
- 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/useCrosshair.d.ts +9 -5
- package/dist/hooks/useCrosshair.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/usePinchZoom.d.ts +7 -0
- package/dist/hooks/usePinchZoom.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/useThreshold.d.ts +13 -13
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/hooks/useYAxis.d.ts +1 -1
- package/dist/hooks/useYAxis.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 +212 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
- package/src/components/CustomTooltipOverlay.tsx +7 -3
- package/src/components/LiveChart.tsx +353 -56
- package/src/components/LiveChartSeries.tsx +28 -6
- package/src/components/ReferenceLineOverlay.tsx +38 -7
- package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
- package/src/core/liveChartEngineTick.ts +34 -1
- package/src/core/resolveConfig.ts +197 -1
- package/src/core/useLiveChartEngine.ts +37 -5
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- package/src/draw/grid.ts +34 -4
- package/src/draw/lineGap.ts +180 -0
- package/src/hooks/crosshairShared.ts +92 -11
- package/src/hooks/useCandleGapPaths.ts +88 -0
- package/src/hooks/useChartPaths.ts +81 -22
- package/src/hooks/useCrosshair.ts +126 -40
- package/src/hooks/useLineGapPaths.ts +76 -0
- package/src/hooks/usePinchZoom.ts +21 -0
- package/src/hooks/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useThreshold.ts +31 -22
- package/src/hooks/useYAxis.ts +2 -0
- package/src/index.ts +16 -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 +224 -14
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
import type { ResolvedCandleGapsConfig } from "../core/resolveConfig";
|
|
3
|
+
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
4
|
+
import { buildCandleGapGeometry } from "../draw/candleGap";
|
|
5
|
+
import type { ChartPadding } from "../draw/line";
|
|
6
|
+
import type { CandleMetrics, CandlePoint } from "../types";
|
|
7
|
+
import { usePathBuilder } from "./usePathBuilder";
|
|
8
|
+
|
|
9
|
+
/** Batched neutral previous-close paths for each semantic candle-gap kind. */
|
|
10
|
+
export function useCandleGapPaths(
|
|
11
|
+
engine: SingleEngineState,
|
|
12
|
+
padding: ChartPadding,
|
|
13
|
+
candles: SharedValue<CandlePoint[]>,
|
|
14
|
+
liveCandle: SharedValue<CandlePoint | null>,
|
|
15
|
+
displayCandleWidth: SharedValue<number>,
|
|
16
|
+
config: ResolvedCandleGapsConfig,
|
|
17
|
+
metrics: CandleMetrics,
|
|
18
|
+
) {
|
|
19
|
+
const noTradesBuilder = usePathBuilder();
|
|
20
|
+
const unavailableBuilder = usePathBuilder();
|
|
21
|
+
const unknownBuilder = usePathBuilder();
|
|
22
|
+
|
|
23
|
+
/* istanbul ignore next -- worklet */
|
|
24
|
+
const geometry = useDerivedValue(() =>
|
|
25
|
+
buildCandleGapGeometry(
|
|
26
|
+
candles.get(),
|
|
27
|
+
liveCandle.get(),
|
|
28
|
+
config.gaps,
|
|
29
|
+
padding,
|
|
30
|
+
engine.canvasWidth.get(),
|
|
31
|
+
engine.canvasHeight.get(),
|
|
32
|
+
engine.timestamp.get() - engine.displayWindow.get(),
|
|
33
|
+
engine.displayWindow.get(),
|
|
34
|
+
engine.displayMin.get(),
|
|
35
|
+
engine.displayMax.get(),
|
|
36
|
+
displayCandleWidth.get(),
|
|
37
|
+
config.styles["no-trades"].bridge !== null,
|
|
38
|
+
config.styles.unavailable.bridge !== null,
|
|
39
|
+
config.styles.unknown.bridge !== null,
|
|
40
|
+
metrics,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/* istanbul ignore next -- worklet */
|
|
45
|
+
const noTradesPath = useDerivedValue(() => {
|
|
46
|
+
const b = noTradesBuilder.get();
|
|
47
|
+
const marks = geometry.get().marks;
|
|
48
|
+
for (let i = 0; i < marks.length; i++) {
|
|
49
|
+
const mark = marks[i];
|
|
50
|
+
if (mark.kind !== "no-trades") continue;
|
|
51
|
+
b.moveTo(mark.x1, mark.y);
|
|
52
|
+
b.lineTo(mark.x2, mark.y);
|
|
53
|
+
}
|
|
54
|
+
return b.detach();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/* istanbul ignore next -- worklet */
|
|
58
|
+
const unavailablePath = useDerivedValue(() => {
|
|
59
|
+
const b = unavailableBuilder.get();
|
|
60
|
+
const marks = geometry.get().marks;
|
|
61
|
+
for (let i = 0; i < marks.length; i++) {
|
|
62
|
+
const mark = marks[i];
|
|
63
|
+
if (mark.kind !== "unavailable") continue;
|
|
64
|
+
b.moveTo(mark.x1, mark.y);
|
|
65
|
+
b.lineTo(mark.x2, mark.y);
|
|
66
|
+
}
|
|
67
|
+
return b.detach();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/* istanbul ignore next -- worklet */
|
|
71
|
+
const unknownPath = useDerivedValue(() => {
|
|
72
|
+
const b = unknownBuilder.get();
|
|
73
|
+
const marks = geometry.get().marks;
|
|
74
|
+
for (let i = 0; i < marks.length; i++) {
|
|
75
|
+
const mark = marks[i];
|
|
76
|
+
if (mark.kind !== "unknown") continue;
|
|
77
|
+
b.moveTo(mark.x1, mark.y);
|
|
78
|
+
b.lineTo(mark.x2, mark.y);
|
|
79
|
+
}
|
|
80
|
+
return b.detach();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
noTradesPath,
|
|
85
|
+
unavailablePath,
|
|
86
|
+
unknownPath,
|
|
87
|
+
} as const;
|
|
88
|
+
}
|
|
@@ -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
|
|
|
@@ -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,27 @@ 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;
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
/**
|
|
@@ -114,7 +126,7 @@ export function useCrosshair(
|
|
|
114
126
|
font: SkFont,
|
|
115
127
|
enabled: boolean,
|
|
116
128
|
onScrub?: (point: ScrubPoint | null) => void,
|
|
117
|
-
|
|
129
|
+
chartOpts?: CrosshairChartOpts,
|
|
118
130
|
/** Press-and-hold delay (ms) before scrubbing activates. 0 = immediate. */
|
|
119
131
|
panGestureDelay = 0,
|
|
120
132
|
onGestureStart?: () => void,
|
|
@@ -209,10 +221,14 @@ export function useCrosshair(
|
|
|
209
221
|
const actionShowText = scrubAction?.text ?? true;
|
|
210
222
|
const hasTimeBadge = scrubAction?.timeBadge ?? false;
|
|
211
223
|
|
|
212
|
-
const isCandleMode =
|
|
213
|
-
const candlesSV =
|
|
214
|
-
const liveCandleSV =
|
|
215
|
-
const candleWidthSecs =
|
|
224
|
+
const isCandleMode = chartOpts?.mode === "candle";
|
|
225
|
+
const candlesSV = chartOpts?.candles;
|
|
226
|
+
const liveCandleSV = chartOpts?.liveCandle;
|
|
227
|
+
const candleWidthSecs = chartOpts?.candleWidthSecs ?? 60;
|
|
228
|
+
const chartGaps = chartOpts?.gaps ?? [];
|
|
229
|
+
const bridgeNoTrades = chartOpts?.bridgeNoTrades ?? false;
|
|
230
|
+
const bridgeUnavailable = chartOpts?.bridgeUnavailable ?? false;
|
|
231
|
+
const bridgeUnknown = chartOpts?.bridgeUnknown ?? false;
|
|
216
232
|
|
|
217
233
|
// Monospace advance width, measured once per render (not per scrub frame) so
|
|
218
234
|
// the tooltip layout worklet can size text by character count instead of a
|
|
@@ -220,6 +236,7 @@ export function useCrosshair(
|
|
|
220
236
|
const monoCharWidth = font.measureText("0").width;
|
|
221
237
|
const scrubTime = useSharedValue(-1);
|
|
222
238
|
const scrubCandle = useSharedValue<CandlePoint | null>(null);
|
|
239
|
+
const scrubGap = useSharedValue<CandleGap | null>(null);
|
|
223
240
|
const scrubValue = useSharedValue<number | null>(null);
|
|
224
241
|
const crosshairOpacity = useSharedValue(0);
|
|
225
242
|
const scrubDotY = useSharedValue(-1);
|
|
@@ -252,12 +269,42 @@ export function useCrosshair(
|
|
|
252
269
|
candleWidthSecs,
|
|
253
270
|
)
|
|
254
271
|
: null;
|
|
272
|
+
const lineData = engine.data.get();
|
|
273
|
+
const candidateGap =
|
|
274
|
+
active && time >= 0 && (!isCandleMode || !candle)
|
|
275
|
+
? pickCandleGapAtTime(chartGaps, time)
|
|
276
|
+
: null;
|
|
277
|
+
const gap =
|
|
278
|
+
candidateGap &&
|
|
279
|
+
!isCandleMode &&
|
|
280
|
+
lineGapContainsData(lineData, candidateGap)
|
|
281
|
+
? null
|
|
282
|
+
: candidateGap;
|
|
283
|
+
const gapBridged = gap
|
|
284
|
+
? gap.kind === "no-trades"
|
|
285
|
+
? bridgeNoTrades
|
|
286
|
+
: gap.kind === "unavailable"
|
|
287
|
+
? bridgeUnavailable
|
|
288
|
+
: bridgeUnknown
|
|
289
|
+
: false;
|
|
290
|
+
const gapValue =
|
|
291
|
+
gap && gapBridged
|
|
292
|
+
? isCandleMode && candlesSV
|
|
293
|
+
? previousCandleCloseAtTime(
|
|
294
|
+
candlesSV.get(),
|
|
295
|
+
liveCandleSV?.get() ?? null,
|
|
296
|
+
time,
|
|
297
|
+
)
|
|
298
|
+
: previousLineValueAtTime(lineData, gap.from)
|
|
299
|
+
: null;
|
|
255
300
|
const value =
|
|
256
301
|
!active || time < 0
|
|
257
302
|
? null
|
|
258
303
|
: isCandleMode
|
|
259
|
-
? (candle?.close ??
|
|
260
|
-
:
|
|
304
|
+
? (candle?.close ?? gapValue)
|
|
305
|
+
: gap
|
|
306
|
+
? gapValue
|
|
307
|
+
: interpolateAtTime(lineData, time);
|
|
261
308
|
const dotY = computeScrubDotY(
|
|
262
309
|
value,
|
|
263
310
|
engine.displayMin.get(),
|
|
@@ -278,28 +325,49 @@ export function useCrosshair(
|
|
|
278
325
|
formatTime,
|
|
279
326
|
font,
|
|
280
327
|
monoCharWidth,
|
|
328
|
+
gap,
|
|
329
|
+
gapValue,
|
|
281
330
|
)
|
|
282
|
-
:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
331
|
+
: gap
|
|
332
|
+
? computeLineGapTooltipLayout(
|
|
333
|
+
active,
|
|
334
|
+
x,
|
|
335
|
+
gap,
|
|
336
|
+
time,
|
|
337
|
+
padding,
|
|
338
|
+
canvasWidth,
|
|
339
|
+
formatTime,
|
|
340
|
+
font,
|
|
341
|
+
monoCharWidth,
|
|
342
|
+
tooltipPlacement,
|
|
343
|
+
tooltipShowValue,
|
|
344
|
+
tooltipShowTime,
|
|
345
|
+
canvasHeight,
|
|
346
|
+
tooltipMargin,
|
|
347
|
+
dotY,
|
|
348
|
+
)
|
|
349
|
+
: deriveCrosshairTooltipSingle(
|
|
350
|
+
active,
|
|
351
|
+
x,
|
|
352
|
+
time,
|
|
353
|
+
value,
|
|
354
|
+
padding,
|
|
355
|
+
canvasWidth,
|
|
356
|
+
formatValue,
|
|
357
|
+
formatTime,
|
|
358
|
+
font,
|
|
359
|
+
monoCharWidth,
|
|
360
|
+
tooltipPlacement,
|
|
361
|
+
tooltipShowValue,
|
|
362
|
+
tooltipShowTime,
|
|
363
|
+
canvasHeight,
|
|
364
|
+
tooltipMargin,
|
|
365
|
+
dotY,
|
|
366
|
+
);
|
|
300
367
|
return {
|
|
301
368
|
time,
|
|
302
369
|
candle,
|
|
370
|
+
gap,
|
|
303
371
|
value,
|
|
304
372
|
opacity: computeCrosshairOpacity(active, x, canvasWidth, padding.right),
|
|
305
373
|
dotY,
|
|
@@ -310,6 +378,7 @@ export function useCrosshair(
|
|
|
310
378
|
"worklet";
|
|
311
379
|
scrubTime.set(snapshot.time);
|
|
312
380
|
scrubCandle.set(snapshot.candle);
|
|
381
|
+
scrubGap.set(snapshot.gap);
|
|
313
382
|
scrubValue.set(snapshot.value);
|
|
314
383
|
crosshairOpacity.set(snapshot.opacity);
|
|
315
384
|
scrubDotY.set(snapshot.dotY);
|
|
@@ -440,11 +509,15 @@ export function useCrosshair(
|
|
|
440
509
|
time: number,
|
|
441
510
|
value: number,
|
|
442
511
|
candleJson: string | null,
|
|
512
|
+
gapJson: string | null,
|
|
443
513
|
) {
|
|
444
514
|
const candle: CandlePoint | undefined = candleJson
|
|
445
515
|
? (JSON.parse(candleJson) as CandlePoint)
|
|
446
516
|
: undefined;
|
|
447
|
-
|
|
517
|
+
const gap: CandleGap | undefined = gapJson
|
|
518
|
+
? (JSON.parse(gapJson) as CandleGap)
|
|
519
|
+
: undefined;
|
|
520
|
+
onScrub?.({ time, value, x, y, candle, gap });
|
|
448
521
|
}
|
|
449
522
|
|
|
450
523
|
/* istanbul ignore next */
|
|
@@ -490,7 +563,9 @@ export function useCrosshair(
|
|
|
490
563
|
const c = scrubCandle.get();
|
|
491
564
|
if (c) candleJson = JSON.stringify(c);
|
|
492
565
|
}
|
|
493
|
-
|
|
566
|
+
const gap = scrubGap.get();
|
|
567
|
+
const gapJson = gap ? JSON.stringify(gap) : null;
|
|
568
|
+
return JSON.stringify([time, val, x, dotY, candleJson, gapJson]);
|
|
494
569
|
},
|
|
495
570
|
(curr, prev) => {
|
|
496
571
|
"worklet";
|
|
@@ -509,8 +584,17 @@ export function useCrosshair(
|
|
|
509
584
|
number,
|
|
510
585
|
number,
|
|
511
586
|
string | null,
|
|
587
|
+
string | null,
|
|
512
588
|
];
|
|
513
|
-
scheduleOnRN(
|
|
589
|
+
scheduleOnRN(
|
|
590
|
+
handleScrub,
|
|
591
|
+
row[2],
|
|
592
|
+
row[3],
|
|
593
|
+
row[0],
|
|
594
|
+
row[1],
|
|
595
|
+
row[4],
|
|
596
|
+
row[5],
|
|
597
|
+
);
|
|
514
598
|
},
|
|
515
599
|
);
|
|
516
600
|
|
|
@@ -543,6 +627,7 @@ export function useCrosshair(
|
|
|
543
627
|
engine.canvasWidth.get(),
|
|
544
628
|
engine.timestamp.get(),
|
|
545
629
|
engine.displayWindow.get(),
|
|
630
|
+
chartGaps,
|
|
546
631
|
);
|
|
547
632
|
};
|
|
548
633
|
|
|
@@ -824,6 +909,7 @@ export function useCrosshair(
|
|
|
824
909
|
scrubTime,
|
|
825
910
|
scrubValue,
|
|
826
911
|
scrubCandle,
|
|
912
|
+
scrubGap,
|
|
827
913
|
crosshairOpacity,
|
|
828
914
|
tooltipLayout,
|
|
829
915
|
scrubDotY,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useDerivedValue } from "react-native-reanimated";
|
|
2
|
+
import type { ResolvedCandleGapsConfig } from "../core/resolveConfig";
|
|
3
|
+
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
4
|
+
import type { ChartPadding } from "../draw/line";
|
|
5
|
+
import { buildLineGapGeometry } from "../draw/lineGap";
|
|
6
|
+
import { usePathBuilder } from "./usePathBuilder";
|
|
7
|
+
|
|
8
|
+
/** Batched flat previous-value bridge paths for each semantic line-gap kind. */
|
|
9
|
+
export function useLineGapPaths(
|
|
10
|
+
engine: SingleEngineState,
|
|
11
|
+
padding: ChartPadding,
|
|
12
|
+
config: ResolvedCandleGapsConfig,
|
|
13
|
+
) {
|
|
14
|
+
const noTradesBuilder = usePathBuilder();
|
|
15
|
+
const unavailableBuilder = usePathBuilder();
|
|
16
|
+
const unknownBuilder = usePathBuilder();
|
|
17
|
+
|
|
18
|
+
/* istanbul ignore next -- worklet */
|
|
19
|
+
const geometry = useDerivedValue(() =>
|
|
20
|
+
buildLineGapGeometry(
|
|
21
|
+
engine.data.get(),
|
|
22
|
+
config.gaps,
|
|
23
|
+
padding,
|
|
24
|
+
engine.canvasWidth.get(),
|
|
25
|
+
engine.canvasHeight.get(),
|
|
26
|
+
engine.timestamp.get() - engine.displayWindow.get(),
|
|
27
|
+
engine.displayWindow.get(),
|
|
28
|
+
engine.displayMin.get(),
|
|
29
|
+
engine.displayMax.get(),
|
|
30
|
+
config.styles["no-trades"].bridge !== null,
|
|
31
|
+
config.styles.unavailable.bridge !== null,
|
|
32
|
+
config.styles.unknown.bridge !== null,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/* istanbul ignore next -- worklet */
|
|
37
|
+
const noTradesPath = useDerivedValue(() => {
|
|
38
|
+
const b = noTradesBuilder.get();
|
|
39
|
+
const bridges = geometry.get().bridges;
|
|
40
|
+
for (let i = 0; i < bridges.length; i++) {
|
|
41
|
+
const bridge = bridges[i];
|
|
42
|
+
if (bridge.kind !== "no-trades") continue;
|
|
43
|
+
b.moveTo(bridge.x1, bridge.y);
|
|
44
|
+
b.lineTo(bridge.x2, bridge.y);
|
|
45
|
+
}
|
|
46
|
+
return b.detach();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/* istanbul ignore next -- worklet */
|
|
50
|
+
const unavailablePath = useDerivedValue(() => {
|
|
51
|
+
const b = unavailableBuilder.get();
|
|
52
|
+
const bridges = geometry.get().bridges;
|
|
53
|
+
for (let i = 0; i < bridges.length; i++) {
|
|
54
|
+
const bridge = bridges[i];
|
|
55
|
+
if (bridge.kind !== "unavailable") continue;
|
|
56
|
+
b.moveTo(bridge.x1, bridge.y);
|
|
57
|
+
b.lineTo(bridge.x2, bridge.y);
|
|
58
|
+
}
|
|
59
|
+
return b.detach();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/* istanbul ignore next -- worklet */
|
|
63
|
+
const unknownPath = useDerivedValue(() => {
|
|
64
|
+
const b = unknownBuilder.get();
|
|
65
|
+
const bridges = geometry.get().bridges;
|
|
66
|
+
for (let i = 0; i < bridges.length; i++) {
|
|
67
|
+
const bridge = bridges[i];
|
|
68
|
+
if (bridge.kind !== "unknown") continue;
|
|
69
|
+
b.moveTo(bridge.x1, bridge.y);
|
|
70
|
+
b.lineTo(bridge.x2, bridge.y);
|
|
71
|
+
}
|
|
72
|
+
return b.detach();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return { noTradesPath, unavailablePath, unknownPath } as const;
|
|
76
|
+
}
|
|
@@ -26,6 +26,27 @@ export interface PinchZoomEngineRefs {
|
|
|
26
26
|
canvasWidth: SharedValue<number>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** The pinch state cleared by the public chart handle's `resetZoom()` method. */
|
|
30
|
+
export type PinchZoomResetRefs = Pick<
|
|
31
|
+
PinchZoomEngineRefs,
|
|
32
|
+
"viewWindow" | "viewEnd"
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clear the focal-anchored pinch overrides so the engine follows its configured
|
|
37
|
+
* `timeWindow` and live edge again. Safe to call when the chart is already reset.
|
|
38
|
+
*/
|
|
39
|
+
export function resetPinchZoom({
|
|
40
|
+
viewWindow,
|
|
41
|
+
viewEnd,
|
|
42
|
+
}: PinchZoomResetRefs): void {
|
|
43
|
+
"worklet";
|
|
44
|
+
cancelAnimation(viewWindow);
|
|
45
|
+
cancelAnimation(viewEnd);
|
|
46
|
+
viewWindow.set(null);
|
|
47
|
+
viewEnd.set(null);
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
export interface UsePinchZoomOptions {
|
|
30
51
|
engine: PinchZoomEngineRefs;
|
|
31
52
|
padding: ChartPadding;
|
|
@@ -279,9 +279,10 @@ export function computeReferenceBadgeRect(
|
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
281
|
* Derives screen-space layout for a single reference line or band. Supports all
|
|
282
|
-
* three `ReferenceLine` forms (horizontal line, horizontal value
|
|
283
|
-
* time band) plus the pill badge for a Form-A value (in-range tag
|
|
284
|
-
* chevron pin).
|
|
282
|
+
* three scalar/band `ReferenceLine` forms (horizontal line, horizontal value
|
|
283
|
+
* band, vertical time band) plus the pill badge for a Form-A value (in-range tag
|
|
284
|
+
* + off-screen chevron pin). The series form is handled by
|
|
285
|
+
* `ReferenceLineSeriesOverlay`.
|
|
285
286
|
*/
|
|
286
287
|
export function useReferenceLine(
|
|
287
288
|
engine: ChartEngineLayout,
|
|
@@ -344,7 +345,7 @@ export function useReferenceLine(
|
|
|
344
345
|
const fm = font.getMetrics();
|
|
345
346
|
const baselineOffset = (fm.ascent + fm.descent) / 2;
|
|
346
347
|
|
|
347
|
-
// ── Form
|
|
348
|
+
// ── Form D — vertical time band (independent of the value range) ─────────
|
|
348
349
|
if (form === "time-band") {
|
|
349
350
|
if (line.from === undefined || line.to === undefined) return INVISIBLE;
|
|
350
351
|
const now = engine.timestamp.value;
|
|
@@ -392,7 +393,7 @@ export function useReferenceLine(
|
|
|
392
393
|
if (valRange <= 0) return INVISIBLE;
|
|
393
394
|
const toY = (v: number) => chartTop + ((dMax - v) / valRange) * chartH;
|
|
394
395
|
|
|
395
|
-
// ── Form
|
|
396
|
+
// ── Form C — horizontal value band ───────────────────────────────────────
|
|
396
397
|
if (form === "value-band") {
|
|
397
398
|
if (line.valueFrom === undefined || line.valueTo === undefined) {
|
|
398
399
|
return INVISIBLE;
|