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
@@ -13,6 +13,32 @@ import type { SingleEngineState } from "../core/useLiveChartEngine";
13
13
  import { computeShake, spawnBurst, tickParticles } from "../math/degenTick";
14
14
  import type { DegenShakePayload, Momentum } from "../types";
15
15
 
16
+ function resolveDegenSettings(
17
+ config: ResolvedDegenConfig | null,
18
+ hasShakeListener: boolean,
19
+ ) {
20
+ return {
21
+ off: config === null,
22
+ scale: config?.scale ?? 1,
23
+ down: config?.downMomentum ?? false,
24
+ shake: config?.shake ?? true,
25
+ shakeIntensity: config?.shakeIntensity ?? 1,
26
+ shakeDurationSec: config?.shakeDurationSec ?? 0.45,
27
+ slotCount: config?.particleSlotCount ?? 60,
28
+ burstParticleCount: config?.burstParticleCount ?? 20,
29
+ particleBurstDurationSec: config?.particleBurstDurationSec ?? 1,
30
+ drag: config?.drag ?? 0.95,
31
+ sizeMin: config?.particleSizeMin ?? 1,
32
+ sizeMax: config?.particleSizeMax ?? 2.2,
33
+ spreadAngle: config?.spreadAngle ?? Math.PI * 1.2,
34
+ jitterX: config?.positionJitterX ?? 24,
35
+ jitterY: config?.positionJitterY ?? 8,
36
+ speedMin: config?.speedMin ?? 60,
37
+ speedMax: config?.speedMax ?? 160,
38
+ hasShakeListener,
39
+ };
40
+ }
41
+
16
42
  export function useDegen(
17
43
  engine: SingleEngineState,
18
44
  dotX: SharedValue<number>,
@@ -65,23 +91,26 @@ export function useDegen(
65
91
  onShakeRef.current?.({ direction });
66
92
  };
67
93
 
68
- const degenOff = cfg === null;
69
- const resolvedScale = cfg?.scale ?? 1;
70
- const resolvedDown = cfg?.downMomentum ?? false;
71
- const resolvedShake = cfg?.shake ?? true;
72
- const resolvedShakeIntensity = cfg?.shakeIntensity ?? 1;
73
- const resolvedShakeDurationSec = cfg?.shakeDurationSec ?? 0.45;
74
- const resolvedSlotCount = cfg?.particleSlotCount ?? 60;
75
- const resolvedBurstParticleCount = cfg?.burstParticleCount ?? 20;
76
- const resolvedParticleBurstDurationSec = cfg?.particleBurstDurationSec ?? 1.0;
77
- const resolvedDrag = cfg?.drag ?? 0.95;
78
- const resolvedSizeMin = cfg?.particleSizeMin ?? 1;
79
- const resolvedSizeMax = cfg?.particleSizeMax ?? 2.2;
80
- const resolvedSpreadAngle = cfg?.spreadAngle ?? Math.PI * 1.2;
81
- const resolvedJitterX = cfg?.positionJitterX ?? 24;
82
- const resolvedJitterY = cfg?.positionJitterY ?? 8;
83
- const resolvedSpeedMin = cfg?.speedMin ?? 60;
84
- const resolvedSpeedMax = cfg?.speedMax ?? 160;
94
+ const {
95
+ off,
96
+ scale,
97
+ down,
98
+ shake,
99
+ shakeIntensity,
100
+ shakeDurationSec,
101
+ slotCount,
102
+ burstParticleCount,
103
+ particleBurstDurationSec,
104
+ drag,
105
+ sizeMin,
106
+ sizeMax,
107
+ spreadAngle,
108
+ jitterX,
109
+ jitterY,
110
+ speedMin,
111
+ speedMax,
112
+ hasShakeListener,
113
+ } = resolveDegenSettings(cfg, onShake != null);
85
114
 
86
115
  useEffect(() => {
87
116
  // Mirror the resolved config into SharedValues for the UI-thread worklet.
@@ -90,43 +119,43 @@ export function useDegen(
90
119
  // is off, enabledSV is 0 and the frame worklet early-returns (zeroing the
91
120
  // particle buffer and shake itself), so the remaining values are inert; the
92
121
  // resolved* defaults already stand in for the null-config case.
93
- enabledSV.set(degenOff ? 0 : 1);
94
- shakeEnabledSV.set(!degenOff && resolvedShake ? 1 : 0);
95
- hasOnShakeListenerSV.set(!degenOff && onShake != null ? 1 : 0);
96
- scaleSV.set(resolvedScale);
97
- downSV.set(resolvedDown ? 1 : 0);
98
- shakeIntensitySV.set(resolvedShakeIntensity);
99
- shakeDurationSecSV.set(resolvedShakeDurationSec);
100
- slotCountSV.set(resolvedSlotCount);
101
- burstParticleCountSV.set(resolvedBurstParticleCount);
102
- particleBurstDurationSecSV.set(resolvedParticleBurstDurationSec);
103
- dragSV.set(resolvedDrag);
104
- sizeMinSV.set(resolvedSizeMin);
105
- sizeMaxSV.set(resolvedSizeMax);
106
- spreadAngleSV.set(resolvedSpreadAngle);
107
- jitterXSV.set(resolvedJitterX);
108
- jitterYSV.set(resolvedJitterY);
109
- speedMinSV.set(resolvedSpeedMin);
110
- speedMaxSV.set(resolvedSpeedMax);
122
+ enabledSV.set(off ? 0 : 1);
123
+ shakeEnabledSV.set(!off && shake ? 1 : 0);
124
+ hasOnShakeListenerSV.set(!off && hasShakeListener ? 1 : 0);
125
+ scaleSV.set(scale);
126
+ downSV.set(down ? 1 : 0);
127
+ shakeIntensitySV.set(shakeIntensity);
128
+ shakeDurationSecSV.set(shakeDurationSec);
129
+ slotCountSV.set(slotCount);
130
+ burstParticleCountSV.set(burstParticleCount);
131
+ particleBurstDurationSecSV.set(particleBurstDurationSec);
132
+ dragSV.set(drag);
133
+ sizeMinSV.set(sizeMin);
134
+ sizeMaxSV.set(sizeMax);
135
+ spreadAngleSV.set(spreadAngle);
136
+ jitterXSV.set(jitterX);
137
+ jitterYSV.set(jitterY);
138
+ speedMinSV.set(speedMin);
139
+ speedMaxSV.set(speedMax);
111
140
  }, [
112
- degenOff,
113
- onShake,
114
- resolvedScale,
115
- resolvedDown,
116
- resolvedShake,
117
- resolvedShakeIntensity,
118
- resolvedShakeDurationSec,
119
- resolvedSlotCount,
120
- resolvedBurstParticleCount,
121
- resolvedParticleBurstDurationSec,
122
- resolvedDrag,
123
- resolvedSizeMin,
124
- resolvedSizeMax,
125
- resolvedSpreadAngle,
126
- resolvedJitterX,
127
- resolvedJitterY,
128
- resolvedSpeedMin,
129
- resolvedSpeedMax,
141
+ off,
142
+ scale,
143
+ down,
144
+ shake,
145
+ shakeIntensity,
146
+ shakeDurationSec,
147
+ slotCount,
148
+ burstParticleCount,
149
+ particleBurstDurationSec,
150
+ drag,
151
+ sizeMin,
152
+ sizeMax,
153
+ spreadAngle,
154
+ jitterX,
155
+ jitterY,
156
+ speedMin,
157
+ speedMax,
158
+ hasShakeListener,
130
159
  // SharedValue refs are stable (useSharedValue), so listing them satisfies
131
160
  // exhaustive-deps without ever re-running the effect.
132
161
  enabledSV,
@@ -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
+ }
@@ -1,9 +1,13 @@
1
1
  import { useDerivedValue, type SharedValue } from "react-native-reanimated";
2
+ import {
3
+ hasCandleChartData,
4
+ hasLineChartData,
5
+ } from "../core/chartDataPresence";
2
6
  import type { CandlePoint, LiveChartPoint } from "../types";
3
7
 
4
8
  /**
5
- * Data presence for {@link LiveChart}: line mode needs ≥2 points;
6
- * candle mode needs ≥2 committed candles (`liveCandle` alone does not count).
9
+ * Data presence for {@link LiveChart}: line mode needs ≥1 point;
10
+ * candle mode needs ≥1 committed candle (`liveCandle` alone does not count).
7
11
  *
8
12
  * {@link useChartReveal} seeds its first-paint state off `hasData` directly (in a
9
13
  * layout effect), so no JS-thread snapshot is read during render here.
@@ -22,9 +26,9 @@ export function useLiveChartHasData({
22
26
  const hasData = useDerivedValue(() => {
23
27
  "worklet";
24
28
  if (isCandle) {
25
- return (candles?.value.length ?? 0) >= 2;
29
+ return hasCandleChartData(candles?.value);
26
30
  }
27
- return data.value.length >= 2;
31
+ return hasLineChartData(data.value);
28
32
  });
29
33
 
30
34
  return { hasData };
@@ -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
+ }
@@ -3,6 +3,11 @@ import {
3
3
  useSharedValue,
4
4
  type SharedValue,
5
5
  } from "react-native-reanimated";
6
+ import {
7
+ hasCandleChartData,
8
+ hasLineChartData,
9
+ hasMultiSeriesChartData,
10
+ } from "../core/chartDataPresence";
6
11
  import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
7
12
 
8
13
  const STASH_MORPH_EPS = 0.01;
@@ -42,20 +47,19 @@ export function useSingleChartReverseMorphInputs({
42
47
  has: hasData.get(),
43
48
  m: morphT.get(),
44
49
  d: data.get(),
45
- cLen: candles?.get().length ?? 0,
46
50
  c: candles?.get(),
47
51
  lc: liveCandle?.get() ?? null,
48
52
  }),
49
53
  (curr) => {
50
54
  "worklet";
51
55
  if (!curr.candle) {
52
- if (curr.has && curr.d.length >= 2) {
56
+ if (curr.has && hasLineChartData(curr.d)) {
53
57
  lineStash.set(curr.d.slice());
54
58
  lineEngineData.set(curr.d);
55
59
  } else if (
56
60
  !curr.has &&
57
61
  curr.m > STASH_MORPH_EPS &&
58
- lineStash.get().length >= 2
62
+ hasLineChartData(lineStash.get())
59
63
  ) {
60
64
  lineEngineData.set(lineStash.get());
61
65
  } else {
@@ -65,14 +69,14 @@ export function useSingleChartReverseMorphInputs({
65
69
  }
66
70
 
67
71
  const cArr = curr.c;
68
- if (curr.has && curr.cLen >= 2 && cArr) {
72
+ if (curr.has && hasCandleChartData(cArr) && cArr) {
69
73
  candleStash.set(cArr.slice());
70
74
  candlesEngine.set(cArr);
71
75
  liveEngine.set(curr.lc);
72
76
  } else if (
73
77
  !curr.has &&
74
78
  curr.m > STASH_MORPH_EPS &&
75
- candleStash.get().length >= 2
79
+ hasCandleChartData(candleStash.get())
76
80
  ) {
77
81
  candlesEngine.set(candleStash.get());
78
82
  liveEngine.set(null);
@@ -118,27 +122,14 @@ export function useMultiSeriesReverseMorphInputs({
118
122
  (curr) => {
119
123
  "worklet";
120
124
  const live = curr.live;
121
-
122
- let anyReady = false;
123
- for (let i = 0; i < live.length; i++) {
124
- if (live[i].data.length >= 2) {
125
- anyReady = true;
126
- break;
127
- }
128
- }
125
+ const anyReady = hasMultiSeriesChartData(live);
129
126
 
130
127
  if (curr.has && anyReady) {
131
128
  seriesStash.set(snapshotSeriesWorklet(live));
132
129
  effectiveSeries.set(live);
133
130
  } else {
134
- let stashReady = false;
135
131
  const stash = seriesStash.get();
136
- for (let i = 0; i < stash.length; i++) {
137
- if (stash[i].data.length >= 2) {
138
- stashReady = true;
139
- break;
140
- }
141
- }
132
+ const stashReady = hasMultiSeriesChartData(stash);
142
133
 
143
134
  if (!curr.has && curr.m > STASH_MORPH_EPS && stashReady) {
144
135
  effectiveSeries.set(seriesStash.get());
@@ -95,15 +95,15 @@ export interface ThresholdSeriesGeometry {
95
95
  samples: SharedValue<number[]>;
96
96
  /** Whether any of the polyline is on-screen (drives marker-line opacity). */
97
97
  visible: SharedValue<boolean>;
98
- /** Threshold value at `now` (flat-extended past the last point) — the badge label. */
99
- currentValue: SharedValue<number>;
100
- /** Pixel-Y of `currentValue` — anchors the badge. */
101
- currentLineY: SharedValue<number>;
102
- /** Whether the badge should show: `currentLineY` on-plot AND, with
103
- * `extendToNow` off, "now" not past the series' last point. (`visible` can be
104
- * true for the polyline while the value-at-now sits outside the plot; the
105
- * badge must not draw into the gutters then.) */
106
- currentVisible: SharedValue<boolean>;
98
+ /** Threshold value at the configured badge anchor: the visible window's
99
+ * left edge (`"first"`) or its live/right edge (`"last"`). */
100
+ badgeValue: SharedValue<number>;
101
+ /** Pixel-Y of `badgeValue` — anchors the badge. */
102
+ badgeLineY: SharedValue<number>;
103
+ /** Whether the configured badge anchor is on-plot and belongs to a visible
104
+ * part of the threshold. (`visible` can still be true when that one endpoint
105
+ * is off-plot.) */
106
+ badgeVisible: SharedValue<boolean>;
107
107
  /** Pixel-X where the threshold ends: the last point's X with `extendToNow`
108
108
  * off, else {@link THRESHOLD_NO_CLIP}. The shader paints its plain
109
109
  * `restColor` right of it; the marker polyline stops there. */
@@ -118,9 +118,9 @@ const EMPTY_SAMPLES: number[] = new Array(THRESHOLD_SAMPLE_COUNT).fill(0);
118
118
  * Per-frame screen geometry for a **time-varying** threshold — a plain
119
119
  * `LiveChartPoint[]` `value` or a live `SharedValue<LiveChartPoint[]>` `series`
120
120
  * (which wins when both are given): the screen polyline (marker line +
121
- * fill-band bottom), the shader's pixel-Y `samples[]`, the current value/anchor
122
- * for the badge, and the `extendToNow` cutoff X. The array buffers ping-pong
123
- * (Reanimated only re-notifies subscribers when the returned reference
121
+ * fill-band bottom), the shader's pixel-Y `samples[]`, the configured endpoint
122
+ * value/anchor for the badge, and the `extendToNow` cutoff X. The array buffers
123
+ * ping-pong (Reanimated only re-notifies subscribers when the returned reference
124
124
  * changes). When the threshold is a constant `SharedValue<number>` every
125
125
  * worklet short-circuits cheaply and {@link useThreshold} drives the render
126
126
  * instead.
@@ -131,6 +131,7 @@ export function useThresholdSeries(
131
131
  value: ThresholdValue,
132
132
  series: SharedValue<LiveChartPoint[]> | null = null,
133
133
  extendToNow = true,
134
+ labelAnchor: "first" | "last" = "last",
134
135
  ): ThresholdSeriesGeometry {
135
136
  const cacheRef = useRef<{
136
137
  ptsA: number[];
@@ -238,15 +239,19 @@ export function useThresholdSeries(
238
239
  );
239
240
  });
240
241
 
241
- const currentValue = useDerivedValue(() => {
242
+ const badgeValue = useDerivedValue(() => {
242
243
  const pts = series ? series.get() : Array.isArray(value) ? value : null;
243
244
  if (pts === null) return NaN;
244
- return interpolateAtTime(pts, engine.timestamp.get()) ?? NaN;
245
+ const time =
246
+ labelAnchor === "first"
247
+ ? engine.timestamp.get() - engine.displayWindow.get()
248
+ : engine.timestamp.get();
249
+ return interpolateAtTime(pts, time) ?? NaN;
245
250
  });
246
251
 
247
- const currentLineY = useDerivedValue(() =>
252
+ const badgeLineY = useDerivedValue(() =>
248
253
  thresholdLineY(
249
- currentValue.get(),
254
+ badgeValue.get(),
250
255
  engine.displayMin.get(),
251
256
  engine.displayMax.get(),
252
257
  engine.canvasHeight.get(),
@@ -255,15 +260,19 @@ export function useThresholdSeries(
255
260
  ),
256
261
  );
257
262
 
258
- const currentVisible = useDerivedValue(() => {
259
- if (!extendToNow) {
263
+ const badgeVisible = useDerivedValue(() => {
264
+ if (labelAnchor === "first") {
265
+ // If the non-extended series ended before this window, there is no left
266
+ // endpoint to label even though interpolation can still clamp a value.
267
+ if (screenPts.get().length < 4) return false;
268
+ } else if (!extendToNow) {
260
269
  // The threshold ends at its last point — no badge past it.
261
270
  const pts = series ? series.get() : Array.isArray(value) ? value : null;
262
271
  if (pts === null || pts.length === 0) return false;
263
272
  if (pts[pts.length - 1].time < engine.timestamp.get()) return false;
264
273
  }
265
274
  return thresholdVisible(
266
- currentLineY.get(),
275
+ badgeLineY.get(),
267
276
  engine.canvasHeight.get(),
268
277
  padding.top,
269
278
  padding.bottom,
@@ -274,9 +283,9 @@ export function useThresholdSeries(
274
283
  screenPts,
275
284
  samples,
276
285
  visible,
277
- currentValue,
278
- currentLineY,
279
- currentVisible,
286
+ badgeValue,
287
+ badgeLineY,
288
+ badgeVisible,
280
289
  clipRightX,
281
290
  };
282
291
  }
package/src/index.ts CHANGED
@@ -39,9 +39,23 @@ export type {
39
39
  BadgeMetrics,
40
40
  BadgeStyleConfig,
41
41
  BadgeVariant,
42
+ CandleGap,
43
+ CandleGapBandStyle,
44
+ CandleGapBridgeStyle,
45
+ CandleGapKind,
46
+ CandleGapLabelStyle,
47
+ CandleGapStyle,
48
+ CandleGapsConfig,
42
49
  CandleMetrics,
43
50
  CandlePoint,
44
51
  CanvasMode,
52
+ ChartGap,
53
+ ChartGapBandStyle,
54
+ ChartGapBridgeStyle,
55
+ ChartGapKind,
56
+ ChartGapLabelStyle,
57
+ ChartGapsConfig,
58
+ ChartGapStyle,
45
59
  ChartInsets,
46
60
  ChartOverlayContext,
47
61
  ChartPlotRect,
@@ -100,6 +114,7 @@ export type {
100
114
  SeriesConfig,
101
115
  ThemeMode,
102
116
  ThresholdConfig,
117
+ ThresholdBadgeRenderProps,
103
118
  ThresholdLineConfig,
104
119
  TooltipRenderProps,
105
120
  TradeEvent,