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.
Files changed (88) 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 +2 -2
  6. package/dist/components/LiveChart.d.ts.map +1 -1
  7. package/dist/components/LiveChartSeries.d.ts +2 -2
  8. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  9. package/dist/components/ReferenceLineOverlay.d.ts +9 -4
  10. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  11. package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
  12. package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
  13. package/dist/core/liveChartEngineTick.d.ts +6 -1
  14. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  15. package/dist/core/resolveConfig.d.ts +34 -1
  16. package/dist/core/resolveConfig.d.ts.map +1 -1
  17. package/dist/core/useLiveChartEngine.d.ts +9 -1
  18. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  19. package/dist/draw/candle.d.ts +3 -1
  20. package/dist/draw/candle.d.ts.map +1 -1
  21. package/dist/draw/candleGap.d.ts +14 -0
  22. package/dist/draw/candleGap.d.ts.map +1 -0
  23. package/dist/draw/grid.d.ts +1 -1
  24. package/dist/draw/grid.d.ts.map +1 -1
  25. package/dist/draw/lineGap.d.ts +25 -0
  26. package/dist/draw/lineGap.d.ts.map +1 -0
  27. package/dist/hooks/crosshairShared.d.ts +7 -3
  28. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  29. package/dist/hooks/useCandleGapPaths.d.ts +12 -0
  30. package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
  31. package/dist/hooks/useChartPaths.d.ts +4 -1
  32. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  33. package/dist/hooks/useCrosshair.d.ts +9 -5
  34. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  35. package/dist/hooks/useLineGapPaths.d.ts +10 -0
  36. package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
  37. package/dist/hooks/usePinchZoom.d.ts +7 -0
  38. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  39. package/dist/hooks/useReferenceLine.d.ts +4 -3
  40. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  41. package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
  42. package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
  43. package/dist/hooks/useThreshold.d.ts +13 -13
  44. package/dist/hooks/useThreshold.d.ts.map +1 -1
  45. package/dist/hooks/useYAxis.d.ts +1 -1
  46. package/dist/hooks/useYAxis.d.ts.map +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/math/candleGaps.d.ts +18 -0
  50. package/dist/math/candleGaps.d.ts.map +1 -0
  51. package/dist/math/referenceLineSeries.d.ts +12 -0
  52. package/dist/math/referenceLineSeries.d.ts.map +1 -0
  53. package/dist/math/referenceLines.d.ts +5 -5
  54. package/dist/math/referenceLines.d.ts.map +1 -1
  55. package/dist/math/spline.d.ts +5 -1
  56. package/dist/math/spline.d.ts.map +1 -1
  57. package/dist/types.d.ts +212 -14
  58. package/dist/types.d.ts.map +1 -1
  59. package/package.json +1 -1
  60. package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
  61. package/src/components/CustomTooltipOverlay.tsx +7 -3
  62. package/src/components/LiveChart.tsx +353 -56
  63. package/src/components/LiveChartSeries.tsx +28 -6
  64. package/src/components/ReferenceLineOverlay.tsx +38 -7
  65. package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
  66. package/src/core/liveChartEngineTick.ts +34 -1
  67. package/src/core/resolveConfig.ts +197 -1
  68. package/src/core/useLiveChartEngine.ts +37 -5
  69. package/src/draw/candle.ts +1 -1
  70. package/src/draw/candleGap.ts +132 -0
  71. package/src/draw/grid.ts +34 -4
  72. package/src/draw/lineGap.ts +180 -0
  73. package/src/hooks/crosshairShared.ts +92 -11
  74. package/src/hooks/useCandleGapPaths.ts +88 -0
  75. package/src/hooks/useChartPaths.ts +81 -22
  76. package/src/hooks/useCrosshair.ts +126 -40
  77. package/src/hooks/useLineGapPaths.ts +76 -0
  78. package/src/hooks/usePinchZoom.ts +21 -0
  79. package/src/hooks/useReferenceLine.ts +6 -5
  80. package/src/hooks/useReferenceLineSeries.ts +101 -0
  81. package/src/hooks/useThreshold.ts +31 -22
  82. package/src/hooks/useYAxis.ts +2 -0
  83. package/src/index.ts +16 -0
  84. package/src/math/candleGaps.ts +120 -0
  85. package/src/math/referenceLineSeries.ts +83 -0
  86. package/src/math/referenceLines.ts +34 -6
  87. package/src/math/spline.ts +15 -5
  88. package/src/types.ts +224 -14
@@ -1,4 +1,12 @@
1
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ forwardRef,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
2
10
  import { StyleSheet, View } from "react-native";
3
11
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
12
  import Animated, {
@@ -10,8 +18,9 @@ import Animated, {
10
18
  withDelay,
11
19
  withSequence,
12
20
  withTiming,
21
+ type SharedValue,
13
22
  } from "react-native-reanimated";
14
- import { scheduleOnRN } from "react-native-worklets";
23
+ import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
15
24
 
16
25
  /**
17
26
  * Single-series live chart. UX and prop vocabulary parallel Benji Taylor’s
@@ -37,6 +46,7 @@ import {
37
46
  resolveAreaDots,
38
47
  resolveAxisLabel,
39
48
  resolveBadge,
49
+ resolveCandleGaps,
40
50
  resolveDegen,
41
51
  resolveDot,
42
52
  resolveGradient,
@@ -62,7 +72,10 @@ import {
62
72
  resolveXAxis,
63
73
  resolveYAxis,
64
74
  } from "../core/resolveConfig";
65
- import type { ResolvedThresholdConfig } from "../core/resolveConfig";
75
+ import type {
76
+ ResolvedCandleGapsConfig,
77
+ ResolvedThresholdConfig,
78
+ } from "../core/resolveConfig";
66
79
  import {
67
80
  liveIndicatorScrollOpacity,
68
81
  resolveHideLiveOnScrollBack,
@@ -77,6 +90,7 @@ import {
77
90
  import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
78
91
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
79
92
  import { useBadge } from "../hooks/useBadge";
93
+ import { useCandleGapPaths } from "../hooks/useCandleGapPaths";
80
94
  import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
81
95
  import { useCanvasLayout } from "../hooks/useCanvasLayout";
82
96
  import { useChartColors } from "../hooks/useChartColors";
@@ -88,13 +102,14 @@ import { useCrosshair } from "../hooks/useCrosshair";
88
102
  import { useDegen } from "../hooks/useDegen";
89
103
  import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
90
104
  import { useLiveDot } from "../hooks/useLiveDot";
105
+ import { useLineGapPaths } from "../hooks/useLineGapPaths";
91
106
  import { useMarkers } from "../hooks/useMarkers";
92
107
  import { useReferenceDrag } from "../hooks/useReferenceDrag";
93
108
  import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
94
109
  import { useModeBlend } from "../hooks/useModeBlend";
95
110
  import { resolveMomentumProp, useMomentum } from "../hooks/useMomentum";
96
111
  import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
97
- import { usePinchZoom } from "../hooks/usePinchZoom";
112
+ import { resetPinchZoom, usePinchZoom } from "../hooks/usePinchZoom";
98
113
  import {
99
114
  SERIES_INDICATOR_FADE_MS,
100
115
  useSeriesIndicatorOpacity,
@@ -114,6 +129,10 @@ import {
114
129
  formatValue as defaultFormatValue,
115
130
  } from "../lib/format";
116
131
  import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
132
+ import {
133
+ candleGapBucketStartAtTime,
134
+ candleGapDefaultLabel,
135
+ } from "../math/candleGaps";
117
136
  import { computeScrubDotY } from "../hooks/crosshairShared";
118
137
  import {
119
138
  groupReferenceLines,
@@ -134,11 +153,14 @@ import {
134
153
  } from "../theme";
135
154
  import type {
136
155
  CandlePoint,
156
+ LiveChartHandle,
137
157
  LiveChartPalette,
138
158
  LiveChartPoint,
139
159
  LiveChartProps,
140
160
  Marker,
161
+ ReferenceLine,
141
162
  } from "../types";
163
+ import { CustomThresholdBadgeOverlay } from "./CustomThresholdBadgeOverlay";
142
164
  import {
143
165
  ThresholdBadgeOverlay,
144
166
  ThresholdLineOverlay,
@@ -276,6 +298,8 @@ function useLiveChartController({
276
298
  candles,
277
299
  candleWidth = 60,
278
300
  liveCandle,
301
+ candleGaps,
302
+ lineGaps,
279
303
  volume,
280
304
 
281
305
  // ── Behaviour ───────────────────────────────────────────────────────────
@@ -333,6 +357,7 @@ function useLiveChartController({
333
357
  renderMarker,
334
358
  renderTooltip,
335
359
  renderOverlay,
360
+ renderThresholdBadge,
336
361
  renderReferenceLine,
337
362
  renderOffAxisReferenceLine,
338
363
  referenceLineGrouping,
@@ -371,6 +396,9 @@ function useLiveChartController({
371
396
  // Volume bars sit below the candles — a candle-mode-only feature (inert in
372
397
  // line mode, like the candle paths themselves).
373
398
  const volumeCfg = isCandle ? resolveVolume(volume) : null;
399
+ const chartGapsCfg = resolveCandleGaps(isCandle ? candleGaps : lineGaps);
400
+ const candleGapsCfg = isCandle ? chartGapsCfg : null;
401
+ const lineGapsCfg = isCandle ? null : chartGapsCfg;
374
402
  const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
375
403
  const gradientCfg = isCandle ? null : resolveGradient(gradient);
376
404
  // Dot-lattice area fill (clipped to the under-line region). Inert in candle
@@ -405,7 +433,35 @@ function useLiveChartController({
405
433
  const tradeStreamResolved = resolveTradeStream(tradeStream);
406
434
  const metricsCfg = resolveMetrics(metrics);
407
435
 
408
- const allRefLines = referenceLines ?? [];
436
+ const chartGapBands: ReferenceLine[] =
437
+ chartGapsCfg?.gaps.flatMap((gap) => {
438
+ const gapStyle = chartGapsCfg.styles[gap.kind];
439
+ const band = gapStyle.band;
440
+ if (band === null) return [];
441
+ const label = gapStyle.label;
442
+ return [
443
+ {
444
+ id: `chart-gap:${gap.kind}:${gap.from}:${gap.to}`,
445
+ from: gap.from,
446
+ to: gap.to,
447
+ label:
448
+ label === null
449
+ ? undefined
450
+ : (gap.label ?? candleGapDefaultLabel(gap.kind)),
451
+ color: band.borderColor,
452
+ fillColor: band.fillColor,
453
+ fillOpacity: band.fillOpacity,
454
+ strokeOpacity: band.borderOpacity,
455
+ strokeWidth: band.borderWidth > 0 ? band.borderWidth : undefined,
456
+ intervals: band.intervals,
457
+ labelColor: label?.color,
458
+ labelPosition: label?.position,
459
+ },
460
+ ];
461
+ }) ?? [];
462
+ // Gap bands append after consumer reference lines so public line indices stay
463
+ // stable for callbacks. Time bands contribute no Y values or press targets.
464
+ const allRefLines = [...(referenceLines ?? []), ...chartGapBands];
409
465
  const refValues = collectReferenceValues(allRefLines);
410
466
 
411
467
  // Per-line live value overrides + drag flags for draggable lines and the custom
@@ -442,17 +498,23 @@ function useLiveChartController({
442
498
 
443
499
  // Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
444
500
  // (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
445
- const refLineCustom = customReferenceLineFlags(
446
- allRefLines,
447
- renderReferenceLine,
448
- );
501
+ // Semantic gap bands are library-owned annotations: do not leak them through
502
+ // the consumer's reference-line renderer or change its callback cardinality.
503
+ const consumerRefLines = referenceLines ?? [];
504
+ const refLineCustom = [
505
+ ...customReferenceLineFlags(consumerRefLines, renderReferenceLine),
506
+ ...chartGapBands.map(() => false),
507
+ ];
449
508
  // An off-axis renderer replaces only the edge-pinned tag. A full custom tag
450
509
  // takes precedence for the same line to avoid mounting two native tags.
451
- const refLineOffAxisCustom = customReferenceLineFlags(
452
- allRefLines,
453
- renderOffAxisReferenceLine,
454
- "off-axis",
455
- ).map((custom, index) => custom && !refLineCustom[index]);
510
+ const refLineOffAxisCustom = [
511
+ ...customReferenceLineFlags(
512
+ consumerRefLines,
513
+ renderOffAxisReferenceLine,
514
+ "off-axis",
515
+ ),
516
+ ...chartGapBands.map(() => false),
517
+ ].map((custom, index) => custom && !refLineCustom[index]);
456
518
  const refLineKeys = referenceLineReactKeys(allRefLines);
457
519
  // RN custom tags report their measured widths here so the Skia connector can
458
520
  // start after the native badge instead of the hidden built-in pill.
@@ -738,6 +800,13 @@ function useLiveChartController({
738
800
  mode,
739
801
  candles: isCandle ? candlesEngine : candles,
740
802
  liveCandle: isCandle ? liveEngine : liveCandle,
803
+ candleGaps: candleGapsCfg?.gaps,
804
+ candleGapBridgeNoTrades:
805
+ Boolean(candleGapsCfg?.styles["no-trades"].bridge),
806
+ candleGapBridgeUnavailable:
807
+ Boolean(candleGapsCfg?.styles.unavailable.bridge),
808
+ candleGapBridgeUnknown:
809
+ Boolean(candleGapsCfg?.styles.unknown.bridge),
741
810
  });
742
811
 
743
812
  // Mirror the UI-thread scroll state to React so the floating y-axis can keep
@@ -839,6 +908,7 @@ function useLiveChartController({
839
908
  thresholdValue,
840
909
  thresholdSeriesSV,
841
910
  thresholdCfg?.extendToNow ?? true,
911
+ thresholdCfg?.line?.labelAnchor ?? "last",
842
912
  );
843
913
  const thresholdStopColors = thresholdCfg
844
914
  ? thresholdStops(thresholdCfg, palette)
@@ -899,23 +969,38 @@ function useLiveChartController({
899
969
  thresholdSeriesGeom.clipRightX,
900
970
  );
901
971
 
902
- // Marker line + badge sources: the series anchors at the value-at-now (flat-
903
- // extended past its last point); the constant case at the single benchmark Y.
904
- // The badge gets its own visibility — it's pinned at the value-at-now Y, which
905
- // can be off-plot while older polyline segments are still visible.
972
+ // Marker line + badge sources: a series badge independently selects its first
973
+ // or last visible endpoint; the constant case stays at the single benchmark Y.
974
+ // The badge gets its own visibility because its selected endpoint can be
975
+ // off-plot while older polyline segments are still visible.
906
976
  const thresholdMarkerLineY = thresholdIsSeries
907
- ? thresholdSeriesGeom.currentLineY
977
+ ? thresholdSeriesGeom.badgeLineY
908
978
  : thresholdGeom.lineY;
909
979
  const thresholdMarkerVisible = thresholdIsSeries
910
980
  ? thresholdSeriesGeom.visible
911
981
  : thresholdGeom.visible;
912
982
  const thresholdBadgeVisible = thresholdIsSeries
913
- ? thresholdSeriesGeom.currentVisible
983
+ ? thresholdSeriesGeom.badgeVisible
914
984
  : thresholdGeom.visible;
915
985
  const thresholdMarkerValue =
916
986
  thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
917
- ? (thresholdCfg.value ?? thresholdSeriesGeom.currentValue)
918
- : thresholdSeriesGeom.currentValue;
987
+ ? (thresholdCfg.value ?? thresholdSeriesGeom.badgeValue)
988
+ : thresholdSeriesGeom.badgeValue;
989
+ const hasCustomThresholdBadge =
990
+ thresholdCfg?.line != null && renderThresholdBadge != null;
991
+ const thresholdMarkerValueStr = useDerivedValue(() =>
992
+ hasCustomThresholdBadge ? formatValue(thresholdMarkerValue.get()) : "",
993
+ );
994
+ const thresholdCustomBadge =
995
+ thresholdCfg?.line && renderThresholdBadge
996
+ ? renderThresholdBadge({
997
+ line: thresholdCfg.line,
998
+ value: thresholdMarkerValue,
999
+ valueStr: thresholdMarkerValueStr,
1000
+ y: thresholdMarkerLineY,
1001
+ visible: thresholdBadgeVisible,
1002
+ })
1003
+ : null;
919
1004
  const thresholdSeriesPts = thresholdIsSeries
920
1005
  ? thresholdSeriesGeom.screenPts
921
1006
  : undefined;
@@ -946,6 +1031,7 @@ function useLiveChartController({
946
1031
  ? thresholdSeriesGeom.samples
947
1032
  : undefined,
948
1033
  lineProp?.simplify,
1034
+ lineGapsCfg?.gaps,
949
1035
  );
950
1036
 
951
1037
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -994,14 +1080,28 @@ function useLiveChartController({
994
1080
 
995
1081
  // ── Overlay hooks ─────────────────────────────────────────────────────
996
1082
  // Scrub/crosshair must see the same stash-backed candles as the engine.
997
- const candleOpts = isCandle
1083
+ const crosshairChartOpts = isCandle
998
1084
  ? {
999
1085
  mode,
1000
1086
  candles: candlesEngine,
1001
1087
  liveCandle: liveEngine,
1002
1088
  candleWidthSecs: candleWidth,
1089
+ gaps: candleGapsCfg?.gaps,
1090
+ bridgeNoTrades:
1091
+ Boolean(candleGapsCfg?.styles["no-trades"].bridge),
1092
+ bridgeUnavailable:
1093
+ Boolean(candleGapsCfg?.styles.unavailable.bridge),
1094
+ bridgeUnknown: Boolean(candleGapsCfg?.styles.unknown.bridge),
1003
1095
  }
1004
- : undefined;
1096
+ : lineGapsCfg
1097
+ ? {
1098
+ mode,
1099
+ gaps: lineGapsCfg.gaps,
1100
+ bridgeNoTrades: Boolean(lineGapsCfg.styles["no-trades"].bridge),
1101
+ bridgeUnavailable: Boolean(lineGapsCfg.styles.unavailable.bridge),
1102
+ bridgeUnknown: Boolean(lineGapsCfg.styles.unknown.bridge),
1103
+ }
1104
+ : undefined;
1005
1105
 
1006
1106
  const markersActive = markers != null;
1007
1107
  const markerClusterCfg = resolveMarkerCluster(markerCluster);
@@ -1102,7 +1202,7 @@ function useLiveChartController({
1102
1202
  // yet becomes scrubbable on touch. `static` only kills the continuous loop.
1103
1203
  scrubCfg !== null || scrubActionCfg !== null,
1104
1204
  onScrub,
1105
- candleOpts,
1205
+ crosshairChartOpts,
1106
1206
  scrubHoldMs,
1107
1207
  onGestureStart,
1108
1208
  onGestureEnd,
@@ -1460,6 +1560,7 @@ function useLiveChartController({
1460
1560
  thresholdMarkerVisible,
1461
1561
  thresholdBadgeVisible,
1462
1562
  thresholdMarkerValue,
1563
+ thresholdCustomBadge,
1463
1564
  thresholdSeriesPts,
1464
1565
  badgeUsesRightGutter,
1465
1566
  // theme / layout / fonts
@@ -1501,6 +1602,8 @@ function useLiveChartController({
1501
1602
  thresholdFillPath,
1502
1603
  lineIsLinear,
1503
1604
  volumeCfg,
1605
+ candleGapsCfg,
1606
+ lineGapsCfg,
1504
1607
  // Volume bars: active flag, fade-in opacity, and resolved colors (default to
1505
1608
  // the candle palette). The reserved band height is read by the x-axis.
1506
1609
  volumeActive: volumeCfg !== null,
@@ -1586,6 +1689,7 @@ function ChartWithYAxis({ model }: { model: LiveChartModel }) {
1586
1689
  yAxisCfg?.minGap ?? 36,
1587
1690
  metricsCfg.grid,
1588
1691
  yAxisCfg?.count ?? 0,
1692
+ yAxisCfg?.intervalScale ?? 1,
1589
1693
  );
1590
1694
  if (model.degenCfg) {
1591
1695
  return <ChartWithDegen model={model} yAxisEntries={yAxisEntries} />;
@@ -1792,6 +1896,124 @@ function ChartFillLayer({
1792
1896
  }
1793
1897
 
1794
1898
  type CandlePaths = ReturnType<typeof useCandlePaths>;
1899
+ type CandleGapPaths = ReturnType<typeof useCandleGapPaths>;
1900
+ type LineGapPaths = ReturnType<typeof useLineGapPaths>;
1901
+
1902
+ function GapBridgePathBatch({
1903
+ paths,
1904
+ config,
1905
+ palette,
1906
+ }: {
1907
+ paths: CandleGapPaths | LineGapPaths;
1908
+ config: ResolvedCandleGapsConfig;
1909
+ palette: LiveChartPalette;
1910
+ }) {
1911
+ const noTrades = config.styles["no-trades"].bridge;
1912
+ const unavailable = config.styles.unavailable.bridge;
1913
+ const unknown = config.styles.unknown.bridge;
1914
+ return (
1915
+ <>
1916
+ {noTrades !== null && (
1917
+ <Group opacity={noTrades.opacity}>
1918
+ <Path
1919
+ path={paths.noTradesPath}
1920
+ style="stroke"
1921
+ strokeWidth={noTrades.strokeWidth}
1922
+ strokeCap={noTrades.strokeCap}
1923
+ color={noTrades.color ?? palette.refLine}
1924
+ />
1925
+ </Group>
1926
+ )}
1927
+ {unavailable !== null && (
1928
+ <Group opacity={unavailable.opacity}>
1929
+ <Path
1930
+ path={paths.unavailablePath}
1931
+ style="stroke"
1932
+ strokeWidth={unavailable.strokeWidth}
1933
+ strokeCap={unavailable.strokeCap}
1934
+ color={unavailable.color ?? palette.refLine}
1935
+ />
1936
+ </Group>
1937
+ )}
1938
+ {unknown !== null && (
1939
+ <Group opacity={unknown.opacity}>
1940
+ <Path
1941
+ path={paths.unknownPath}
1942
+ style="stroke"
1943
+ strokeWidth={unknown.strokeWidth}
1944
+ strokeCap={unknown.strokeCap}
1945
+ color={unknown.color ?? palette.refLine}
1946
+ />
1947
+ </Group>
1948
+ )}
1949
+ </>
1950
+ );
1951
+ }
1952
+
1953
+ function ChartCandleGapLayer({
1954
+ model,
1955
+ config,
1956
+ focusOtherCandles,
1957
+ inactiveOpacity,
1958
+ focusedOpacity,
1959
+ focusedClip,
1960
+ }: {
1961
+ model: LiveChartModel;
1962
+ config: ResolvedCandleGapsConfig;
1963
+ focusOtherCandles: boolean;
1964
+ inactiveOpacity: SharedValue<number>;
1965
+ focusedOpacity: SharedValue<number>;
1966
+ focusedClip: SharedValue<{
1967
+ x: number;
1968
+ y: number;
1969
+ width: number;
1970
+ height: number;
1971
+ }>;
1972
+ }) {
1973
+ const paths = useCandleGapPaths(
1974
+ model.engine,
1975
+ model.effectivePadding,
1976
+ model.candlesEngine,
1977
+ model.liveEngine,
1978
+ model.displayCandleWidth,
1979
+ config,
1980
+ model.metricsCfg.candle,
1981
+ );
1982
+ const batch = (
1983
+ <GapBridgePathBatch
1984
+ paths={paths}
1985
+ config={config}
1986
+ palette={model.palette}
1987
+ />
1988
+ );
1989
+ return focusOtherCandles ? (
1990
+ <>
1991
+ <Group opacity={inactiveOpacity}>{batch}</Group>
1992
+ <Group opacity={focusedOpacity} clip={focusedClip}>
1993
+ {batch}
1994
+ </Group>
1995
+ </>
1996
+ ) : (
1997
+ batch
1998
+ );
1999
+ }
2000
+
2001
+ /** Line-mode bridge paths live in a child so candle charts register no worklets. */
2002
+ function ChartLineGapLayer({ model }: { model: LiveChartModel }) {
2003
+ const config = model.lineGapsCfg!;
2004
+ const paths = useLineGapPaths(
2005
+ model.engine,
2006
+ model.effectivePadding,
2007
+ config,
2008
+ );
2009
+ return (
2010
+ <GapBridgePathBatch
2011
+ paths={paths}
2012
+ config={config}
2013
+ palette={model.palette}
2014
+ />
2015
+ );
2016
+ }
1795
2017
 
1796
2018
  /** One batched candle pass: two body paths and two wick paths. */
1797
2019
  function CandlePathBatch({
@@ -1848,6 +2070,7 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1848
2070
  volumeOpacity,
1849
2071
  volumeUpColor,
1850
2072
  volumeDownColor,
2073
+ candleGapsCfg,
1851
2074
  scrubCfg,
1852
2075
  crosshair,
1853
2076
  } = model;
@@ -1865,24 +2088,42 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1865
2088
  const focusOtherCandles = scrubCfg?.dimTarget === "otherCandles";
1866
2089
  const candleDimOpacity = scrubCfg?.dimOpacity ?? 1;
1867
2090
  const candleDimFadeMs = scrubCfg?.dimFadeMs ?? 0;
1868
- // Destructure the two SharedValues before entering worklets. Closing over the
2091
+ // Destructure SharedValues before entering worklets. Closing over the
1869
2092
  // whole crosshair object would also serialize its RNGH gesture instances.
1870
2093
  const scrubActive = crosshair.scrubActive;
1871
2094
  // ChartCandleLayer is single-series only; useCrosshair always supplies this
1872
2095
  // value (it is optional on the shared state type only for LiveChartSeries).
1873
2096
  const scrubCandle = crosshair.scrubCandle!;
1874
- // Keep the last selection after scrubCandle clears so its clipped full-
2097
+ const scrubGap = crosshair.scrubGap!;
2098
+ const scrubTime = crosshair.scrubTime;
2099
+ const currentFocusBucketTime = useDerivedValue(() => {
2100
+ if (!focusOtherCandles) return null;
2101
+ const candle = scrubCandle.get();
2102
+ if (candle) return candle.time;
2103
+ const gap = scrubGap.get();
2104
+ if (!gap) return null;
2105
+ return candleGapBucketStartAtTime(
2106
+ gap,
2107
+ scrubTime.get(),
2108
+ candlesEngine.get(),
2109
+ liveEngine.get(),
2110
+ displayCandleWidth.get(),
2111
+ );
2112
+ });
2113
+ // Keep the last selection after the current target clears so its clipped full-
1875
2114
  // strength pass can cover the base batch until the release fade completes.
1876
- const lastScrubCandle = useSharedValue<CandlePoint | null>(null);
2115
+ const lastFocusBucket = useSharedValue<{ time: number } | null>(null);
1877
2116
  useAnimatedReaction(
1878
- () => (focusOtherCandles ? scrubCandle.get() : null),
1879
- (candle) => {
1880
- if (candle) lastScrubCandle.set(candle);
2117
+ () => currentFocusBucketTime.get(),
2118
+ (time) => {
2119
+ if (time !== null) lastFocusBucket.set({ time });
1881
2120
  },
1882
2121
  );
1883
2122
  const inactiveCandleOpacity = useDerivedValue(() => {
1884
2123
  const target =
1885
- focusOtherCandles && scrubActive.get() && scrubCandle.get()
2124
+ focusOtherCandles &&
2125
+ scrubActive.get() &&
2126
+ currentFocusBucketTime.get() !== null
1886
2127
  ? candleDimOpacity
1887
2128
  : 1;
1888
2129
  return candleDimFadeMs > 0
@@ -1891,33 +2132,36 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1891
2132
  }, [
1892
2133
  focusOtherCandles,
1893
2134
  scrubActive,
1894
- scrubCandle,
2135
+ currentFocusBucketTime,
1895
2136
  candleDimOpacity,
1896
2137
  candleDimFadeMs,
1897
2138
  ]);
1898
2139
  const focusedCandleOpacity = useDerivedValue(() => {
1899
- const currentCandle = scrubCandle.get();
1900
- const focusCandle = currentCandle ?? lastScrubCandle.get();
1901
- if (!focusOtherCandles || !focusCandle) return 0;
2140
+ const currentTime = currentFocusBucketTime.get();
2141
+ const focusBucket =
2142
+ currentTime === null ? lastFocusBucket.get() : { time: currentTime };
2143
+ if (!focusOtherCandles || !focusBucket) return 0;
1902
2144
  return computeCandleFocusPassOpacity(
1903
2145
  scrubActive.get(),
1904
- currentCandle !== null,
2146
+ currentTime !== null,
1905
2147
  inactiveCandleOpacity.get(),
1906
2148
  );
1907
2149
  }, [
1908
2150
  focusOtherCandles,
1909
2151
  scrubActive,
1910
- scrubCandle,
1911
- lastScrubCandle,
2152
+ currentFocusBucketTime,
2153
+ lastFocusBucket,
1912
2154
  inactiveCandleOpacity,
1913
2155
  ]);
1914
2156
  const focusedCandleClip = useDerivedValue(() => {
1915
2157
  if (!focusOtherCandles) {
1916
2158
  return HIDDEN_CANDLE_FOCUS_CLIP;
1917
2159
  }
1918
- const focusCandle = scrubCandle.get() ?? lastScrubCandle.get();
2160
+ const currentTime = currentFocusBucketTime.get();
2161
+ const focusBucket =
2162
+ currentTime === null ? lastFocusBucket.get() : { time: currentTime };
1919
2163
  return computeCandleFocusClip(
1920
- focusCandle,
2164
+ focusBucket,
1921
2165
  effectivePadding,
1922
2166
  engine.canvasWidth.get(),
1923
2167
  engine.canvasHeight.get(),
@@ -1927,8 +2171,8 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1927
2171
  );
1928
2172
  }, [
1929
2173
  focusOtherCandles,
1930
- scrubCandle,
1931
- lastScrubCandle,
2174
+ currentFocusBucketTime,
2175
+ lastFocusBucket,
1932
2176
  effectivePadding,
1933
2177
  engine.canvasWidth,
1934
2178
  engine.canvasHeight,
@@ -1940,6 +2184,16 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1940
2184
  return (
1941
2185
  <Group opacity={seriesOpacity}>
1942
2186
  <Group opacity={candleGroupOpacity}>
2187
+ {candleGapsCfg && (
2188
+ <ChartCandleGapLayer
2189
+ model={model}
2190
+ config={candleGapsCfg}
2191
+ focusOtherCandles={focusOtherCandles}
2192
+ inactiveOpacity={inactiveCandleOpacity}
2193
+ focusedOpacity={focusedCandleOpacity}
2194
+ focusedClip={focusedCandleClip}
2195
+ />
2196
+ )}
1943
2197
  {focusOtherCandles ? (
1944
2198
  <>
1945
2199
  <Group opacity={inactiveCandleOpacity}>
@@ -2022,11 +2276,13 @@ function ChartStack({
2022
2276
  thresholdMarkerVisible,
2023
2277
  thresholdBadgeVisible,
2024
2278
  thresholdMarkerValue,
2279
+ thresholdCustomBadge,
2025
2280
  thresholdSeriesPts,
2026
2281
  formatValue,
2027
2282
  lineGroupOpacity,
2028
2283
  seriesOpacity,
2029
2284
  linePath,
2285
+ lineGapsCfg,
2030
2286
  lineIsLinear,
2031
2287
  strokeWidth,
2032
2288
  lineProp,
@@ -2171,6 +2427,7 @@ function ChartStack({
2171
2427
  />
2172
2428
  ) : null}
2173
2429
  </Path>
2430
+ {lineGapsCfg && <ChartLineGapLayer model={model} />}
2174
2431
  </Group>
2175
2432
  </Group>
2176
2433
 
@@ -2246,7 +2503,7 @@ function ChartStack({
2246
2503
 
2247
2504
  {/* Threshold label badge — on top of the line/dot/markers so it's never
2248
2505
  painted over (the dashed marker line itself stays behind the line, above). */}
2249
- {thresholdCfg?.line && (
2506
+ {thresholdCfg?.line && thresholdCustomBadge == null && (
2250
2507
  <ThresholdBadgeOverlay
2251
2508
  engine={engine}
2252
2509
  padding={effectivePadding}
@@ -2300,15 +2557,27 @@ function ChartTradeStreamLayer({
2300
2557
  tradeStream,
2301
2558
  tradeStreamResolved,
2302
2559
  effectivePadding,
2560
+ volumeBandHeight,
2303
2561
  palette,
2304
2562
  skiaFont,
2305
2563
  reveal,
2306
2564
  isStatic,
2307
2565
  } = model;
2566
+ // `effectivePadding.bottom` includes the volume reservation so candle prices
2567
+ // stop above the bars. The trade tape should still enter at the chart's true
2568
+ // lower edge (the volume baseline / x-axis), so remove that reservation for
2569
+ // this overlay's coordinate space only.
2570
+ const tradeStreamPadding =
2571
+ volumeBandHeight > 0
2572
+ ? {
2573
+ ...effectivePadding,
2574
+ bottom: effectivePadding.bottom - volumeBandHeight,
2575
+ }
2576
+ : effectivePadding;
2308
2577
  const tradeMarkers = useTradeStream(
2309
2578
  engine,
2310
2579
  tradeStream!,
2311
- effectivePadding,
2580
+ tradeStreamPadding,
2312
2581
  !isStatic,
2313
2582
  !isStatic,
2314
2583
  );
@@ -2317,7 +2586,7 @@ function ChartTradeStreamLayer({
2317
2586
  <TradeStreamOverlay
2318
2587
  markers={tradeMarkers}
2319
2588
  palette={palette}
2320
- padding={effectivePadding}
2589
+ padding={tradeStreamPadding}
2321
2590
  font={skiaFont}
2322
2591
  opacity={reveal.dotOpacity}
2323
2592
  labelOffsetX={tradeStreamResolved!.labelOffsetX}
@@ -2755,6 +3024,10 @@ function ChartView({
2755
3024
  formatValue,
2756
3025
  topLabelCfg,
2757
3026
  bottomLabelCfg,
3027
+ thresholdCfg,
3028
+ thresholdMarkerLineY,
3029
+ thresholdBadgeVisible,
3030
+ thresholdCustomBadge,
2758
3031
  markersActive,
2759
3032
  renderMarker,
2760
3033
  renderTooltip,
@@ -2868,6 +3141,19 @@ function ChartView({
2868
3141
  />
2869
3142
  )}
2870
3143
 
3144
+ {/* Custom threshold badge — a native view positioned from the same live
3145
+ value/Y SharedValues as the built-in Skia badge. */}
3146
+ {thresholdCustomBadge && thresholdCfg?.line && (
3147
+ <CustomThresholdBadgeOverlay
3148
+ element={thresholdCustomBadge}
3149
+ engine={engine}
3150
+ padding={effectivePadding}
3151
+ y={thresholdMarkerLineY}
3152
+ visible={thresholdBadgeVisible}
3153
+ position={thresholdCfg.line.labelPosition}
3154
+ />
3155
+ )}
3156
+
2871
3157
  {/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
2872
3158
  pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
2873
3159
  a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
@@ -2890,6 +3176,7 @@ function ChartView({
2890
3176
  scrubTime={crosshair.scrubTime}
2891
3177
  scrubActive={crosshair.scrubActive}
2892
3178
  scrubCandle={crosshair.scrubCandle}
3179
+ scrubGap={crosshair.scrubGap}
2893
3180
  tooltipLayout={crosshair.tooltipLayout}
2894
3181
  engine={engine}
2895
3182
  padding={effectivePadding}
@@ -2911,13 +3198,23 @@ function ChartView({
2911
3198
  );
2912
3199
  }
2913
3200
 
2914
- export function LiveChart(props: LiveChartProps) {
2915
- const model = useLiveChartController(props);
2916
- if (model.yAxisCfg) {
2917
- return <ChartWithYAxis model={model} />;
2918
- }
2919
- if (model.degenCfg) {
2920
- return <ChartWithDegen model={model} yAxisEntries={null} />;
2921
- }
2922
- return <ChartView model={model} yAxisEntries={null} degen={null} />;
2923
- }
3201
+ export const LiveChart = forwardRef<LiveChartHandle, LiveChartProps>(
3202
+ function LiveChart(props, ref) {
3203
+ const model = useLiveChartController(props);
3204
+ const { viewEnd, viewWindow } = model.engine;
3205
+ useImperativeHandle(
3206
+ ref,
3207
+ () => ({
3208
+ resetZoom: () => scheduleOnUI(resetPinchZoom, { viewEnd, viewWindow }),
3209
+ }),
3210
+ [viewEnd, viewWindow],
3211
+ );
3212
+ if (model.yAxisCfg) {
3213
+ return <ChartWithYAxis model={model} />;
3214
+ }
3215
+ if (model.degenCfg) {
3216
+ return <ChartWithDegen model={model} yAxisEntries={null} />;
3217
+ }
3218
+ return <ChartView model={model} yAxisEntries={null} degen={null} />;
3219
+ },
3220
+ );