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
@@ -18,6 +18,7 @@ import Animated, {
18
18
  withDelay,
19
19
  withSequence,
20
20
  withTiming,
21
+ type SharedValue,
21
22
  } from "react-native-reanimated";
22
23
  import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
23
24
 
@@ -45,6 +46,7 @@ import {
45
46
  resolveAreaDots,
46
47
  resolveAxisLabel,
47
48
  resolveBadge,
49
+ resolveCandleGaps,
48
50
  resolveDegen,
49
51
  resolveDot,
50
52
  resolveGradient,
@@ -70,7 +72,10 @@ import {
70
72
  resolveXAxis,
71
73
  resolveYAxis,
72
74
  } from "../core/resolveConfig";
73
- import type { ResolvedThresholdConfig } from "../core/resolveConfig";
75
+ import type {
76
+ ResolvedCandleGapsConfig,
77
+ ResolvedThresholdConfig,
78
+ } from "../core/resolveConfig";
74
79
  import {
75
80
  liveIndicatorScrollOpacity,
76
81
  resolveHideLiveOnScrollBack,
@@ -85,6 +90,7 @@ import {
85
90
  import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
86
91
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
87
92
  import { useBadge } from "../hooks/useBadge";
93
+ import { useCandleGapPaths } from "../hooks/useCandleGapPaths";
88
94
  import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
89
95
  import { useCanvasLayout } from "../hooks/useCanvasLayout";
90
96
  import { useChartColors } from "../hooks/useChartColors";
@@ -96,6 +102,7 @@ import { useCrosshair } from "../hooks/useCrosshair";
96
102
  import { useDegen } from "../hooks/useDegen";
97
103
  import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
98
104
  import { useLiveDot } from "../hooks/useLiveDot";
105
+ import { useLineGapPaths } from "../hooks/useLineGapPaths";
99
106
  import { useMarkers } from "../hooks/useMarkers";
100
107
  import { useReferenceDrag } from "../hooks/useReferenceDrag";
101
108
  import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
@@ -122,6 +129,10 @@ import {
122
129
  formatValue as defaultFormatValue,
123
130
  } from "../lib/format";
124
131
  import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
132
+ import {
133
+ candleGapBucketStartAtTime,
134
+ candleGapDefaultLabel,
135
+ } from "../math/candleGaps";
125
136
  import { computeScrubDotY } from "../hooks/crosshairShared";
126
137
  import {
127
138
  groupReferenceLines,
@@ -147,7 +158,9 @@ import type {
147
158
  LiveChartPoint,
148
159
  LiveChartProps,
149
160
  Marker,
161
+ ReferenceLine,
150
162
  } from "../types";
163
+ import { CustomThresholdBadgeOverlay } from "./CustomThresholdBadgeOverlay";
151
164
  import {
152
165
  ThresholdBadgeOverlay,
153
166
  ThresholdLineOverlay,
@@ -285,6 +298,8 @@ function useLiveChartController({
285
298
  candles,
286
299
  candleWidth = 60,
287
300
  liveCandle,
301
+ candleGaps,
302
+ lineGaps,
288
303
  volume,
289
304
 
290
305
  // ── Behaviour ───────────────────────────────────────────────────────────
@@ -342,6 +357,7 @@ function useLiveChartController({
342
357
  renderMarker,
343
358
  renderTooltip,
344
359
  renderOverlay,
360
+ renderThresholdBadge,
345
361
  renderReferenceLine,
346
362
  renderOffAxisReferenceLine,
347
363
  referenceLineGrouping,
@@ -380,6 +396,9 @@ function useLiveChartController({
380
396
  // Volume bars sit below the candles — a candle-mode-only feature (inert in
381
397
  // line mode, like the candle paths themselves).
382
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;
383
402
  const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
384
403
  const gradientCfg = isCandle ? null : resolveGradient(gradient);
385
404
  // Dot-lattice area fill (clipped to the under-line region). Inert in candle
@@ -414,7 +433,35 @@ function useLiveChartController({
414
433
  const tradeStreamResolved = resolveTradeStream(tradeStream);
415
434
  const metricsCfg = resolveMetrics(metrics);
416
435
 
417
- 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];
418
465
  const refValues = collectReferenceValues(allRefLines);
419
466
 
420
467
  // Per-line live value overrides + drag flags for draggable lines and the custom
@@ -451,17 +498,23 @@ function useLiveChartController({
451
498
 
452
499
  // Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
453
500
  // (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
454
- const refLineCustom = customReferenceLineFlags(
455
- allRefLines,
456
- renderReferenceLine,
457
- );
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
+ ];
458
508
  // An off-axis renderer replaces only the edge-pinned tag. A full custom tag
459
509
  // takes precedence for the same line to avoid mounting two native tags.
460
- const refLineOffAxisCustom = customReferenceLineFlags(
461
- allRefLines,
462
- renderOffAxisReferenceLine,
463
- "off-axis",
464
- ).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]);
465
518
  const refLineKeys = referenceLineReactKeys(allRefLines);
466
519
  // RN custom tags report their measured widths here so the Skia connector can
467
520
  // start after the native badge instead of the hidden built-in pill.
@@ -747,6 +800,13 @@ function useLiveChartController({
747
800
  mode,
748
801
  candles: isCandle ? candlesEngine : candles,
749
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),
750
810
  });
751
811
 
752
812
  // Mirror the UI-thread scroll state to React so the floating y-axis can keep
@@ -848,6 +908,7 @@ function useLiveChartController({
848
908
  thresholdValue,
849
909
  thresholdSeriesSV,
850
910
  thresholdCfg?.extendToNow ?? true,
911
+ thresholdCfg?.line?.labelAnchor ?? "last",
851
912
  );
852
913
  const thresholdStopColors = thresholdCfg
853
914
  ? thresholdStops(thresholdCfg, palette)
@@ -908,23 +969,38 @@ function useLiveChartController({
908
969
  thresholdSeriesGeom.clipRightX,
909
970
  );
910
971
 
911
- // Marker line + badge sources: the series anchors at the value-at-now (flat-
912
- // extended past its last point); the constant case at the single benchmark Y.
913
- // The badge gets its own visibility — it's pinned at the value-at-now Y, which
914
- // 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.
915
976
  const thresholdMarkerLineY = thresholdIsSeries
916
- ? thresholdSeriesGeom.currentLineY
977
+ ? thresholdSeriesGeom.badgeLineY
917
978
  : thresholdGeom.lineY;
918
979
  const thresholdMarkerVisible = thresholdIsSeries
919
980
  ? thresholdSeriesGeom.visible
920
981
  : thresholdGeom.visible;
921
982
  const thresholdBadgeVisible = thresholdIsSeries
922
- ? thresholdSeriesGeom.currentVisible
983
+ ? thresholdSeriesGeom.badgeVisible
923
984
  : thresholdGeom.visible;
924
985
  const thresholdMarkerValue =
925
986
  thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
926
- ? (thresholdCfg.value ?? thresholdSeriesGeom.currentValue)
927
- : 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;
928
1004
  const thresholdSeriesPts = thresholdIsSeries
929
1005
  ? thresholdSeriesGeom.screenPts
930
1006
  : undefined;
@@ -955,6 +1031,7 @@ function useLiveChartController({
955
1031
  ? thresholdSeriesGeom.samples
956
1032
  : undefined,
957
1033
  lineProp?.simplify,
1034
+ lineGapsCfg?.gaps,
958
1035
  );
959
1036
 
960
1037
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -1003,14 +1080,28 @@ function useLiveChartController({
1003
1080
 
1004
1081
  // ── Overlay hooks ─────────────────────────────────────────────────────
1005
1082
  // Scrub/crosshair must see the same stash-backed candles as the engine.
1006
- const candleOpts = isCandle
1083
+ const crosshairChartOpts = isCandle
1007
1084
  ? {
1008
1085
  mode,
1009
1086
  candles: candlesEngine,
1010
1087
  liveCandle: liveEngine,
1011
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),
1012
1095
  }
1013
- : 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;
1014
1105
 
1015
1106
  const markersActive = markers != null;
1016
1107
  const markerClusterCfg = resolveMarkerCluster(markerCluster);
@@ -1111,7 +1202,7 @@ function useLiveChartController({
1111
1202
  // yet becomes scrubbable on touch. `static` only kills the continuous loop.
1112
1203
  scrubCfg !== null || scrubActionCfg !== null,
1113
1204
  onScrub,
1114
- candleOpts,
1205
+ crosshairChartOpts,
1115
1206
  scrubHoldMs,
1116
1207
  onGestureStart,
1117
1208
  onGestureEnd,
@@ -1469,6 +1560,7 @@ function useLiveChartController({
1469
1560
  thresholdMarkerVisible,
1470
1561
  thresholdBadgeVisible,
1471
1562
  thresholdMarkerValue,
1563
+ thresholdCustomBadge,
1472
1564
  thresholdSeriesPts,
1473
1565
  badgeUsesRightGutter,
1474
1566
  // theme / layout / fonts
@@ -1510,6 +1602,8 @@ function useLiveChartController({
1510
1602
  thresholdFillPath,
1511
1603
  lineIsLinear,
1512
1604
  volumeCfg,
1605
+ candleGapsCfg,
1606
+ lineGapsCfg,
1513
1607
  // Volume bars: active flag, fade-in opacity, and resolved colors (default to
1514
1608
  // the candle palette). The reserved band height is read by the x-axis.
1515
1609
  volumeActive: volumeCfg !== null,
@@ -1802,6 +1896,124 @@ function ChartFillLayer({
1802
1896
  }
1803
1897
 
1804
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
+ }
1805
2017
 
1806
2018
  /** One batched candle pass: two body paths and two wick paths. */
1807
2019
  function CandlePathBatch({
@@ -1858,6 +2070,7 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1858
2070
  volumeOpacity,
1859
2071
  volumeUpColor,
1860
2072
  volumeDownColor,
2073
+ candleGapsCfg,
1861
2074
  scrubCfg,
1862
2075
  crosshair,
1863
2076
  } = model;
@@ -1875,24 +2088,42 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1875
2088
  const focusOtherCandles = scrubCfg?.dimTarget === "otherCandles";
1876
2089
  const candleDimOpacity = scrubCfg?.dimOpacity ?? 1;
1877
2090
  const candleDimFadeMs = scrubCfg?.dimFadeMs ?? 0;
1878
- // Destructure the two SharedValues before entering worklets. Closing over the
2091
+ // Destructure SharedValues before entering worklets. Closing over the
1879
2092
  // whole crosshair object would also serialize its RNGH gesture instances.
1880
2093
  const scrubActive = crosshair.scrubActive;
1881
2094
  // ChartCandleLayer is single-series only; useCrosshair always supplies this
1882
2095
  // value (it is optional on the shared state type only for LiveChartSeries).
1883
2096
  const scrubCandle = crosshair.scrubCandle!;
1884
- // 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-
1885
2114
  // strength pass can cover the base batch until the release fade completes.
1886
- const lastScrubCandle = useSharedValue<CandlePoint | null>(null);
2115
+ const lastFocusBucket = useSharedValue<{ time: number } | null>(null);
1887
2116
  useAnimatedReaction(
1888
- () => (focusOtherCandles ? scrubCandle.get() : null),
1889
- (candle) => {
1890
- if (candle) lastScrubCandle.set(candle);
2117
+ () => currentFocusBucketTime.get(),
2118
+ (time) => {
2119
+ if (time !== null) lastFocusBucket.set({ time });
1891
2120
  },
1892
2121
  );
1893
2122
  const inactiveCandleOpacity = useDerivedValue(() => {
1894
2123
  const target =
1895
- focusOtherCandles && scrubActive.get() && scrubCandle.get()
2124
+ focusOtherCandles &&
2125
+ scrubActive.get() &&
2126
+ currentFocusBucketTime.get() !== null
1896
2127
  ? candleDimOpacity
1897
2128
  : 1;
1898
2129
  return candleDimFadeMs > 0
@@ -1901,33 +2132,36 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1901
2132
  }, [
1902
2133
  focusOtherCandles,
1903
2134
  scrubActive,
1904
- scrubCandle,
2135
+ currentFocusBucketTime,
1905
2136
  candleDimOpacity,
1906
2137
  candleDimFadeMs,
1907
2138
  ]);
1908
2139
  const focusedCandleOpacity = useDerivedValue(() => {
1909
- const currentCandle = scrubCandle.get();
1910
- const focusCandle = currentCandle ?? lastScrubCandle.get();
1911
- 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;
1912
2144
  return computeCandleFocusPassOpacity(
1913
2145
  scrubActive.get(),
1914
- currentCandle !== null,
2146
+ currentTime !== null,
1915
2147
  inactiveCandleOpacity.get(),
1916
2148
  );
1917
2149
  }, [
1918
2150
  focusOtherCandles,
1919
2151
  scrubActive,
1920
- scrubCandle,
1921
- lastScrubCandle,
2152
+ currentFocusBucketTime,
2153
+ lastFocusBucket,
1922
2154
  inactiveCandleOpacity,
1923
2155
  ]);
1924
2156
  const focusedCandleClip = useDerivedValue(() => {
1925
2157
  if (!focusOtherCandles) {
1926
2158
  return HIDDEN_CANDLE_FOCUS_CLIP;
1927
2159
  }
1928
- const focusCandle = scrubCandle.get() ?? lastScrubCandle.get();
2160
+ const currentTime = currentFocusBucketTime.get();
2161
+ const focusBucket =
2162
+ currentTime === null ? lastFocusBucket.get() : { time: currentTime };
1929
2163
  return computeCandleFocusClip(
1930
- focusCandle,
2164
+ focusBucket,
1931
2165
  effectivePadding,
1932
2166
  engine.canvasWidth.get(),
1933
2167
  engine.canvasHeight.get(),
@@ -1937,8 +2171,8 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1937
2171
  );
1938
2172
  }, [
1939
2173
  focusOtherCandles,
1940
- scrubCandle,
1941
- lastScrubCandle,
2174
+ currentFocusBucketTime,
2175
+ lastFocusBucket,
1942
2176
  effectivePadding,
1943
2177
  engine.canvasWidth,
1944
2178
  engine.canvasHeight,
@@ -1950,6 +2184,16 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1950
2184
  return (
1951
2185
  <Group opacity={seriesOpacity}>
1952
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
+ )}
1953
2197
  {focusOtherCandles ? (
1954
2198
  <>
1955
2199
  <Group opacity={inactiveCandleOpacity}>
@@ -2032,11 +2276,13 @@ function ChartStack({
2032
2276
  thresholdMarkerVisible,
2033
2277
  thresholdBadgeVisible,
2034
2278
  thresholdMarkerValue,
2279
+ thresholdCustomBadge,
2035
2280
  thresholdSeriesPts,
2036
2281
  formatValue,
2037
2282
  lineGroupOpacity,
2038
2283
  seriesOpacity,
2039
2284
  linePath,
2285
+ lineGapsCfg,
2040
2286
  lineIsLinear,
2041
2287
  strokeWidth,
2042
2288
  lineProp,
@@ -2181,6 +2427,7 @@ function ChartStack({
2181
2427
  />
2182
2428
  ) : null}
2183
2429
  </Path>
2430
+ {lineGapsCfg && <ChartLineGapLayer model={model} />}
2184
2431
  </Group>
2185
2432
  </Group>
2186
2433
 
@@ -2256,7 +2503,7 @@ function ChartStack({
2256
2503
 
2257
2504
  {/* Threshold label badge — on top of the line/dot/markers so it's never
2258
2505
  painted over (the dashed marker line itself stays behind the line, above). */}
2259
- {thresholdCfg?.line && (
2506
+ {thresholdCfg?.line && thresholdCustomBadge == null && (
2260
2507
  <ThresholdBadgeOverlay
2261
2508
  engine={engine}
2262
2509
  padding={effectivePadding}
@@ -2310,15 +2557,27 @@ function ChartTradeStreamLayer({
2310
2557
  tradeStream,
2311
2558
  tradeStreamResolved,
2312
2559
  effectivePadding,
2560
+ volumeBandHeight,
2313
2561
  palette,
2314
2562
  skiaFont,
2315
2563
  reveal,
2316
2564
  isStatic,
2317
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;
2318
2577
  const tradeMarkers = useTradeStream(
2319
2578
  engine,
2320
2579
  tradeStream!,
2321
- effectivePadding,
2580
+ tradeStreamPadding,
2322
2581
  !isStatic,
2323
2582
  !isStatic,
2324
2583
  );
@@ -2327,7 +2586,7 @@ function ChartTradeStreamLayer({
2327
2586
  <TradeStreamOverlay
2328
2587
  markers={tradeMarkers}
2329
2588
  palette={palette}
2330
- padding={effectivePadding}
2589
+ padding={tradeStreamPadding}
2331
2590
  font={skiaFont}
2332
2591
  opacity={reveal.dotOpacity}
2333
2592
  labelOffsetX={tradeStreamResolved!.labelOffsetX}
@@ -2765,6 +3024,10 @@ function ChartView({
2765
3024
  formatValue,
2766
3025
  topLabelCfg,
2767
3026
  bottomLabelCfg,
3027
+ thresholdCfg,
3028
+ thresholdMarkerLineY,
3029
+ thresholdBadgeVisible,
3030
+ thresholdCustomBadge,
2768
3031
  markersActive,
2769
3032
  renderMarker,
2770
3033
  renderTooltip,
@@ -2878,6 +3141,19 @@ function ChartView({
2878
3141
  />
2879
3142
  )}
2880
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
+
2881
3157
  {/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
2882
3158
  pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
2883
3159
  a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
@@ -2900,6 +3176,7 @@ function ChartView({
2900
3176
  scrubTime={crosshair.scrubTime}
2901
3177
  scrubActive={crosshair.scrubActive}
2902
3178
  scrubCandle={crosshair.scrubCandle}
3179
+ scrubGap={crosshair.scrubGap}
2903
3180
  tooltipLayout={crosshair.tooltipLayout}
2904
3181
  engine={engine}
2905
3182
  padding={effectivePadding}