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.
Files changed (111) hide show
  1. package/dist/components/AxisLabelOverlay.d.ts +5 -1
  2. package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
  3. package/dist/components/CrosshairOverlay.d.ts +4 -2
  4. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  5. package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
  6. package/dist/components/CustomThresholdBadgeOverlay.d.ts +18 -0
  7. package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
  8. package/dist/components/CustomTooltipOverlay.d.ts +4 -2
  9. package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
  10. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  11. package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
  12. package/dist/components/ExtremaConnectorOverlay.d.ts.map +1 -1
  13. package/dist/components/LiveChart.d.ts.map +1 -1
  14. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  15. package/dist/components/MarkerOverlay.d.ts +4 -2
  16. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  17. package/dist/components/ReferenceLineOverlay.d.ts +10 -5
  18. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  19. package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
  20. package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
  21. package/dist/core/chartDataPresence.d.ts +8 -0
  22. package/dist/core/chartDataPresence.d.ts.map +1 -0
  23. package/dist/core/liveChartEngineTick.d.ts +6 -1
  24. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  25. package/dist/core/resolveConfig.d.ts +32 -1
  26. package/dist/core/resolveConfig.d.ts.map +1 -1
  27. package/dist/core/useLiveChartEngine.d.ts +9 -1
  28. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  29. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  30. package/dist/draw/candle.d.ts +3 -1
  31. package/dist/draw/candle.d.ts.map +1 -1
  32. package/dist/draw/candleGap.d.ts +14 -0
  33. package/dist/draw/candleGap.d.ts.map +1 -0
  34. package/dist/draw/lineGap.d.ts +25 -0
  35. package/dist/draw/lineGap.d.ts.map +1 -0
  36. package/dist/draw/markerAtlas.d.ts +3 -2
  37. package/dist/draw/markerAtlas.d.ts.map +1 -1
  38. package/dist/hooks/crosshairShared.d.ts +7 -3
  39. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  40. package/dist/hooks/useCandleGapPaths.d.ts +12 -0
  41. package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
  42. package/dist/hooks/useChartPaths.d.ts +4 -1
  43. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  44. package/dist/hooks/useChartReveal.d.ts +1 -1
  45. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  46. package/dist/hooks/useCrosshair.d.ts +9 -5
  47. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  48. package/dist/hooks/useDegen.d.ts.map +1 -1
  49. package/dist/hooks/useLineGapPaths.d.ts +10 -0
  50. package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
  51. package/dist/hooks/useLiveChartHasData.d.ts +2 -2
  52. package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
  53. package/dist/hooks/useReferenceLine.d.ts +4 -3
  54. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  55. package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
  56. package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
  57. package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
  58. package/dist/hooks/useThreshold.d.ts +13 -13
  59. package/dist/hooks/useThreshold.d.ts.map +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/math/candleGaps.d.ts +18 -0
  63. package/dist/math/candleGaps.d.ts.map +1 -0
  64. package/dist/math/referenceLineSeries.d.ts +12 -0
  65. package/dist/math/referenceLineSeries.d.ts.map +1 -0
  66. package/dist/math/referenceLines.d.ts +5 -5
  67. package/dist/math/referenceLines.d.ts.map +1 -1
  68. package/dist/math/spline.d.ts +5 -1
  69. package/dist/math/spline.d.ts.map +1 -1
  70. package/dist/types.d.ts +191 -17
  71. package/dist/types.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/components/AxisLabelOverlay.tsx +162 -81
  74. package/src/components/CrosshairOverlay.tsx +394 -147
  75. package/src/components/CustomMarkerOverlay.tsx +9 -6
  76. package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
  77. package/src/components/CustomTooltipOverlay.tsx +7 -3
  78. package/src/components/DegenParticlesOverlay.tsx +34 -23
  79. package/src/components/ExtremaConnectorOverlay.tsx +43 -3
  80. package/src/components/LiveChart.tsx +1918 -974
  81. package/src/components/LiveChartSeries.tsx +445 -296
  82. package/src/components/MarkerOverlay.tsx +215 -156
  83. package/src/components/ReferenceLineOverlay.tsx +113 -55
  84. package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
  85. package/src/core/chartDataPresence.ts +24 -0
  86. package/src/core/liveChartEngineTick.ts +34 -1
  87. package/src/core/resolveConfig.ts +186 -0
  88. package/src/core/useLiveChartEngine.ts +36 -1
  89. package/src/core/useLiveChartSeriesEngine.ts +2 -0
  90. package/src/draw/candle.ts +1 -1
  91. package/src/draw/candleGap.ts +132 -0
  92. package/src/draw/lineGap.ts +180 -0
  93. package/src/draw/markerAtlas.ts +8 -3
  94. package/src/hooks/crosshairShared.ts +92 -11
  95. package/src/hooks/useCandleGapPaths.ts +88 -0
  96. package/src/hooks/useChartPaths.ts +81 -22
  97. package/src/hooks/useChartReveal.ts +1 -1
  98. package/src/hooks/useCrosshair.ts +178 -69
  99. package/src/hooks/useDegen.ts +82 -53
  100. package/src/hooks/useLineGapPaths.ts +76 -0
  101. package/src/hooks/useLiveChartHasData.ts +8 -4
  102. package/src/hooks/useReferenceLine.ts +6 -5
  103. package/src/hooks/useReferenceLineSeries.ts +101 -0
  104. package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
  105. package/src/hooks/useThreshold.ts +31 -22
  106. package/src/index.ts +15 -0
  107. package/src/math/candleGaps.ts +120 -0
  108. package/src/math/referenceLineSeries.ts +83 -0
  109. package/src/math/referenceLines.ts +34 -6
  110. package/src/math/spline.ts +15 -5
  111. 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
- b.moveTo(pts[0], pts[1]);
179
- drawSpline(b, pts, cache.scratch, linear);
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
- b.moveTo(pts[0], pts[1]);
190
- drawSpline(b, pts, cache.scratch, linear);
226
+ const ranges = segmentRanges.get();
227
+ if (ranges.length === 0) return cache.emptyPath;
191
228
  const bottom = engine.canvasHeight.get() - padding.bottom;
192
- b.lineTo(pts[(n - 1) * 2], bottom);
193
- b.lineTo(pts[0], bottom);
194
- b.close();
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
- b.lineTo(rightX, sampleThresholdYAt(tsamples, x0, x1, rightX));
235
- for (let i = count - 1; i >= 0; i--) {
236
- const sx = x0 + step * i;
237
- if (sx > leftX && sx < rightX) b.lineTo(sx, tsamples[i]);
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
- b.moveTo(pts[0], pts[1]);
249
- drawSpline(b, pts, cache.scratch, linear);
250
- b.lineTo(pts[(n - 1) * 2], yT);
251
- b.lineTo(pts[0], yT);
252
- b.close();
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 fewer than two samples. */
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
- export {
88
- computeCandleTooltipLayout,
89
- computeCrosshairOpacity,
90
- computeScrubTime,
91
- computeTooltipLayout,
92
- computeTooltipLayoutMulti,
93
- deriveCrosshairTooltipSingle,
94
- deriveScrubValueSingle,
95
- HIDDEN_TOOLTIP
96
- } from "./crosshairShared";
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 CrosshairCandleOpts {
106
+ export interface CrosshairChartOpts {
99
107
  mode: "line" | "candle";
100
108
  candles?: SharedValue<CandlePoint[]>;
101
109
  liveCandle?: SharedValue<CandlePoint | null>;
102
- candleWidthSecs: number;
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
- candleOpts?: CrosshairCandleOpts,
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 hasScrubAction = scrubAction != null;
203
- const clampPlainScrubToPlot = clampToPlot && !hasScrubAction;
204
- const hasOnScrubAction = onScrubAction != null;
205
- const dismissOnTapOutside = scrubAction?.dismissOnTapOutside ?? false;
206
- const dismissOnAction = scrubAction?.dismissOnAction ?? false;
207
- const snapIncrement = scrubAction?.snap;
208
- const actionIcon = scrubAction?.icon ?? "+";
209
- const actionShowText = scrubAction?.text ?? true;
210
- const hasTimeBadge = scrubAction?.timeBadge ?? false;
211
-
212
- const isCandleMode = candleOpts?.mode === "candle";
213
- const candlesSV = candleOpts?.candles;
214
- const liveCandleSV = candleOpts?.liveCandle;
215
- const candleWidthSecs = candleOpts?.candleWidthSecs ?? 60;
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 ?? null)
260
- : interpolateAtTime(engine.data.get(), time);
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
- : deriveCrosshairTooltipSingle(
283
- active,
284
- x,
285
- time,
286
- value,
287
- padding,
288
- canvasWidth,
289
- formatValue,
290
- formatTime,
291
- font,
292
- monoCharWidth,
293
- tooltipPlacement,
294
- tooltipShowValue,
295
- tooltipShowTime,
296
- canvasHeight,
297
- tooltipMargin,
298
- dotY,
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
- onScrub?.({ time, value, x, y, candle });
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
- return JSON.stringify([time, val, x, dotY, candleJson]);
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(e.x, padding.left, engine.canvasWidth.get(), padding.right),
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(e.x, padding.left, engine.canvasWidth.get(), padding.right),
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
- padding,
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,