react-native-livechart 4.20.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.
Files changed (75) hide show
  1. package/dist/components/CustomThresholdBadgeOverlay.d.ts +18 -0
  2. package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
  3. package/dist/components/CustomTooltipOverlay.d.ts +4 -2
  4. package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
  5. package/dist/components/LiveChart.d.ts.map +1 -1
  6. package/dist/components/ReferenceLineOverlay.d.ts +9 -4
  7. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  8. package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
  9. package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
  10. package/dist/core/liveChartEngineTick.d.ts +6 -1
  11. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  12. package/dist/core/resolveConfig.d.ts +32 -1
  13. package/dist/core/resolveConfig.d.ts.map +1 -1
  14. package/dist/core/useLiveChartEngine.d.ts +9 -1
  15. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  16. package/dist/draw/candle.d.ts +3 -1
  17. package/dist/draw/candle.d.ts.map +1 -1
  18. package/dist/draw/candleGap.d.ts +14 -0
  19. package/dist/draw/candleGap.d.ts.map +1 -0
  20. package/dist/draw/lineGap.d.ts +25 -0
  21. package/dist/draw/lineGap.d.ts.map +1 -0
  22. package/dist/hooks/crosshairShared.d.ts +7 -3
  23. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  24. package/dist/hooks/useCandleGapPaths.d.ts +12 -0
  25. package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
  26. package/dist/hooks/useChartPaths.d.ts +4 -1
  27. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  28. package/dist/hooks/useCrosshair.d.ts +9 -5
  29. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  30. package/dist/hooks/useLineGapPaths.d.ts +10 -0
  31. package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
  32. package/dist/hooks/useReferenceLine.d.ts +4 -3
  33. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  34. package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
  35. package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
  36. package/dist/hooks/useThreshold.d.ts +13 -13
  37. package/dist/hooks/useThreshold.d.ts.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/math/candleGaps.d.ts +18 -0
  41. package/dist/math/candleGaps.d.ts.map +1 -0
  42. package/dist/math/referenceLineSeries.d.ts +12 -0
  43. package/dist/math/referenceLineSeries.d.ts.map +1 -0
  44. package/dist/math/referenceLines.d.ts +5 -5
  45. package/dist/math/referenceLines.d.ts.map +1 -1
  46. package/dist/math/spline.d.ts +5 -1
  47. package/dist/math/spline.d.ts.map +1 -1
  48. package/dist/types.d.ts +188 -14
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
  52. package/src/components/CustomTooltipOverlay.tsx +7 -3
  53. package/src/components/LiveChart.tsx +320 -43
  54. package/src/components/ReferenceLineOverlay.tsx +38 -7
  55. package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
  56. package/src/core/liveChartEngineTick.ts +34 -1
  57. package/src/core/resolveConfig.ts +186 -0
  58. package/src/core/useLiveChartEngine.ts +37 -5
  59. package/src/draw/candle.ts +1 -1
  60. package/src/draw/candleGap.ts +132 -0
  61. package/src/draw/lineGap.ts +180 -0
  62. package/src/hooks/crosshairShared.ts +92 -11
  63. package/src/hooks/useCandleGapPaths.ts +88 -0
  64. package/src/hooks/useChartPaths.ts +81 -22
  65. package/src/hooks/useCrosshair.ts +126 -40
  66. package/src/hooks/useLineGapPaths.ts +76 -0
  67. package/src/hooks/useReferenceLine.ts +6 -5
  68. package/src/hooks/useReferenceLineSeries.ts +101 -0
  69. package/src/hooks/useThreshold.ts +31 -22
  70. package/src/index.ts +15 -0
  71. package/src/math/candleGaps.ts +120 -0
  72. package/src/math/referenceLineSeries.ts +83 -0
  73. package/src/math/referenceLines.ts +34 -6
  74. package/src/math/spline.ts +15 -5
  75. package/src/types.ts +199 -14
@@ -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
 
@@ -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
- 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;
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
- candleOpts?: CrosshairCandleOpts,
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 = candleOpts?.mode === "candle";
213
- const candlesSV = candleOpts?.candles;
214
- const liveCandleSV = candleOpts?.liveCandle;
215
- const candleWidthSecs = candleOpts?.candleWidthSecs ?? 60;
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 ?? null)
260
- : interpolateAtTime(engine.data.get(), time);
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
- : 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
- );
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
- onScrub?.({ time, value, x, y, candle });
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
- return JSON.stringify([time, val, x, dotY, candleJson]);
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(handleScrub, row[2], row[3], row[0], row[1], row[4]);
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
+ }
@@ -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 band, vertical
283
- * time band) plus the pill badge for a Form-A value (in-range tag + off-screen
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 C — vertical time band (independent of the value range) ─────────
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 B — horizontal value band ───────────────────────────────────────
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;
@@ -0,0 +1,101 @@
1
+ import { useRef } from "react";
2
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
3
+
4
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
5
+ import type { ChartPadding } from "../draw/line";
6
+ import { interpolateAtTime } from "../math/interpolate";
7
+ import { buildReferenceLineSeriesPoints } from "../math/referenceLineSeries";
8
+ import {
9
+ thresholdLineY,
10
+ thresholdSeriesVisible,
11
+ thresholdVisible,
12
+ } from "../math/threshold";
13
+ import type { LiveChartPoint } from "../types";
14
+
15
+ export interface ReferenceLineSeriesGeometry {
16
+ /** Visible series polyline as `[x, y, …]` screen coordinates. */
17
+ screenPts: SharedValue<number[]>;
18
+ /** Whether any series segment crosses the visible plot. */
19
+ visible: SharedValue<boolean>;
20
+ /** Series value at the chart's current right edge. */
21
+ currentValue: SharedValue<number>;
22
+ /** Canvas Y coordinate of {@link currentValue}. */
23
+ currentY: SharedValue<number>;
24
+ /** Whether the current-value label belongs inside the visible plot. */
25
+ currentVisible: SharedValue<boolean>;
26
+ }
27
+
28
+ /**
29
+ * UI-thread geometry for a static historical reference-line series. Point
30
+ * buffers ping-pong so Reanimated observes a changed array reference each frame
31
+ * without allocating new intermediate arrays.
32
+ */
33
+ export function useReferenceLineSeries(
34
+ engine: ChartEngineLayout,
35
+ padding: ChartPadding,
36
+ points: LiveChartPoint[],
37
+ extendToNow: boolean,
38
+ ): ReferenceLineSeriesGeometry {
39
+ const cacheRef = useRef<{
40
+ a: number[];
41
+ b: number[];
42
+ tick: boolean;
43
+ } | null>(null);
44
+ if (cacheRef.current === null) {
45
+ cacheRef.current = { a: [], b: [], tick: false };
46
+ }
47
+
48
+ const screenPts = useDerivedValue(() => {
49
+ const cache = cacheRef.current!;
50
+ cache.tick = !cache.tick;
51
+ return buildReferenceLineSeriesPoints(
52
+ points,
53
+ engine.timestamp.get(),
54
+ engine.displayWindow.get(),
55
+ engine.displayMin.get(),
56
+ engine.displayMax.get(),
57
+ engine.canvasWidth.get(),
58
+ engine.canvasHeight.get(),
59
+ padding,
60
+ extendToNow,
61
+ cache.tick ? cache.a : cache.b,
62
+ );
63
+ });
64
+
65
+ const visible = useDerivedValue(() =>
66
+ thresholdSeriesVisible(
67
+ screenPts.get(),
68
+ engine.canvasHeight.get(),
69
+ padding.top,
70
+ padding.bottom,
71
+ ),
72
+ );
73
+
74
+ const currentValue = useDerivedValue(
75
+ () => interpolateAtTime(points, engine.timestamp.get()) ?? NaN,
76
+ );
77
+ const currentY = useDerivedValue(() =>
78
+ thresholdLineY(
79
+ currentValue.get(),
80
+ engine.displayMin.get(),
81
+ engine.displayMax.get(),
82
+ engine.canvasHeight.get(),
83
+ padding.top,
84
+ padding.bottom,
85
+ ),
86
+ );
87
+ const currentVisible = useDerivedValue(() => {
88
+ if (points.length === 0) return false;
89
+ if (!extendToNow && points[points.length - 1].time < engine.timestamp.get()) {
90
+ return false;
91
+ }
92
+ return thresholdVisible(
93
+ currentY.get(),
94
+ engine.canvasHeight.get(),
95
+ padding.top,
96
+ padding.bottom,
97
+ );
98
+ });
99
+
100
+ return { screenPts, visible, currentValue, currentY, currentVisible };
101
+ }