react-native-livechart 4.12.0 → 4.13.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 (47) hide show
  1. package/dist/components/CrosshairLine.d.ts +12 -4
  2. package/dist/components/CrosshairLine.d.ts.map +1 -1
  3. package/dist/components/CrosshairOverlay.d.ts +12 -2
  4. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  5. package/dist/components/CustomTooltipOverlay.d.ts +5 -2
  6. package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
  7. package/dist/components/LiveChart.d.ts.map +1 -1
  8. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  9. package/dist/components/PerSeriesTooltipOverlay.d.ts +3 -1
  10. package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -1
  11. package/dist/components/ReferenceLineOverlay.d.ts +8 -1
  12. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  13. package/dist/components/YAxisOverlay.d.ts +6 -2
  14. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  15. package/dist/core/resolveConfig.d.ts +16 -0
  16. package/dist/core/resolveConfig.d.ts.map +1 -1
  17. package/dist/draw/grid.d.ts +13 -0
  18. package/dist/draw/grid.d.ts.map +1 -1
  19. package/dist/hooks/crosshairShared.d.ts +22 -3
  20. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  21. package/dist/hooks/useCrosshair.d.ts +7 -1
  22. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  23. package/dist/hooks/useCrosshairSeries.d.ts +6 -1
  24. package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
  25. package/dist/hooks/useCrosshairVisibleOpacity.d.ts +8 -0
  26. package/dist/hooks/useCrosshairVisibleOpacity.d.ts.map +1 -0
  27. package/dist/hooks/useReferenceLine.d.ts +10 -1
  28. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  29. package/dist/types.d.ts +47 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/components/CrosshairLine.tsx +39 -11
  33. package/src/components/CrosshairOverlay.tsx +49 -15
  34. package/src/components/CustomTooltipOverlay.tsx +15 -3
  35. package/src/components/LiveChart.tsx +25 -2
  36. package/src/components/LiveChartSeries.tsx +83 -16
  37. package/src/components/PerSeriesTooltipOverlay.tsx +4 -1
  38. package/src/components/ReferenceLineOverlay.tsx +14 -0
  39. package/src/components/YAxisOverlay.tsx +34 -9
  40. package/src/core/resolveConfig.ts +29 -0
  41. package/src/draw/grid.ts +33 -0
  42. package/src/hooks/crosshairShared.ts +81 -4
  43. package/src/hooks/useCrosshair.ts +46 -28
  44. package/src/hooks/useCrosshairSeries.ts +31 -4
  45. package/src/hooks/useCrosshairVisibleOpacity.ts +57 -0
  46. package/src/hooks/useReferenceLine.ts +26 -1
  47. package/src/types.ts +47 -0
@@ -20,6 +20,7 @@ import type {
20
20
  ScrubPoint,
21
21
  } from "../types";
22
22
  import {
23
+ clampPlotX,
23
24
  computeActionBadgeLayout,
24
25
  computeCandleTooltipLayout,
25
26
  computeCrosshairOpacity,
@@ -32,10 +33,13 @@ import {
32
33
  HIDDEN_TIME_BADGE,
33
34
  HIDDEN_TOOLTIP,
34
35
  pointInRect,
36
+ resolveScrubHitSlop,
35
37
  SCRUB_ACTIVATE_X_PX,
36
38
  SCRUB_FAIL_Y_PX,
37
39
  snapPrice,
40
+ startPlainScrub,
38
41
  type CrosshairState,
42
+ updatePlainScrub,
39
43
  } from "./crosshairShared";
40
44
  import {
41
45
  delayedPanTouchCancelled,
@@ -64,18 +68,6 @@ const SCRUB_ACTION_TAP_SLOP = 10;
64
68
  */
65
69
  const SCRUB_ACTION_PRESS_HOLD_MS = 200;
66
70
 
67
- /** Clamp an X pixel to the plot's horizontal bounds. */
68
- /* istanbul ignore next -- worklet, called only from UI-thread gesture handlers */
69
- function clampPlotX(
70
- x: number,
71
- padLeft: number,
72
- canvasWidth: number,
73
- padRight: number,
74
- ): number {
75
- "worklet";
76
- return Math.min(canvasWidth - padRight, Math.max(padLeft, x));
77
- }
78
-
79
71
  /** Clamp a Y pixel to the plot's vertical bounds. */
80
72
  /* istanbul ignore next -- worklet, called only from UI-thread gesture handlers */
81
73
  function clampPlotY(
@@ -159,6 +151,12 @@ export function useCrosshair(
159
151
  * mature into a scrub (see `delayedPanGuard.shouldStartDelayedPan`).
160
152
  */
161
153
  scrollActive?: SharedValue<boolean>,
154
+ /**
155
+ * Reject plain-scrub starts beyond either horizontal plot edge and clamp
156
+ * active drags to those bounds. Scrub-action behavior is unchanged.
157
+ * Default `false`.
158
+ */
159
+ clampToPlot = false,
162
160
  ): CrosshairState {
163
161
  const scrubX = useSharedValue(-1);
164
162
  const scrubActive = useSharedValue(false);
@@ -195,6 +193,7 @@ export function useCrosshair(
195
193
  // tracks the price as the axis rescales instead of drifting under a fixed pixel.
196
194
  const lockPriceValue = useSharedValue<number | null>(null);
197
195
  const hasScrubAction = scrubAction != null;
196
+ const clampPlainScrubToPlot = clampToPlot && !hasScrubAction;
198
197
  const hasOnScrubAction = onScrubAction != null;
199
198
  const dismissOnTapOutside = scrubAction?.dismissOnTapOutside ?? false;
200
199
  const dismissOnAction = scrubAction?.dismissOnAction ?? false;
@@ -618,9 +617,15 @@ export function useCrosshair(
618
617
  // also keeps a follow-on drag from showing a crosshair — no `onUpdate`
619
618
  // guard needed. (Plain-scrub counterpart of the scrub-action tap defer.)
620
619
  if (deferTapHit !== undefined && deferTapHit(e.x, e.y)) return;
621
- scrubX.set(e.x);
622
- scrubActive.set(true);
623
- gestureStarted.set(true);
620
+ startPlainScrub(
621
+ e.x,
622
+ padding,
623
+ engine.canvasWidth.get(),
624
+ clampPlainScrubToPlot,
625
+ scrubX,
626
+ scrubActive,
627
+ gestureStarted,
628
+ );
624
629
  if (hasOnGestureStart) scheduleOnRN(handleGestureStart);
625
630
  },
626
631
  )
@@ -647,7 +652,14 @@ export function useCrosshair(
647
652
  }
648
653
  return;
649
654
  }
650
- scrubX.set(e.x);
655
+ updatePlainScrub(
656
+ e.x,
657
+ padding,
658
+ engine.canvasWidth.get(),
659
+ clampPlainScrubToPlot,
660
+ scrubX,
661
+ scrubActive,
662
+ );
651
663
  },
652
664
  )
653
665
  .onFinalize(
@@ -686,13 +698,15 @@ export function useCrosshair(
686
698
  .failOffsetY([-SCRUB_FAIL_Y_PX, SCRUB_FAIL_Y_PX]);
687
699
  }
688
700
 
689
- // Axis-drag time-scroll: carve the bottom "time ruler" band out of the scrub's
690
- // hit area so a drag starting there scrolls (the pan-scroll gesture owns it)
691
- // and never trips the crosshair. `shouldCancelWhenOutside(false)` above keeps a
692
- // scrub that *started* in the plot tracking on into the band.
693
- if (scrubBottomExclude > 0) {
694
- gesture = gesture.hitSlop({ bottom: -scrubBottomExclude });
695
- }
701
+ // Restrict recognition by touch-down position. This rejects before ACTIVE,
702
+ // leaving outside starts to competing/parent gestures. Once accepted,
703
+ // `shouldCancelWhenOutside(false)` keeps tracking beyond these bounds.
704
+ const scrubHitSlop = resolveScrubHitSlop(
705
+ padding,
706
+ clampPlainScrubToPlot,
707
+ scrubBottomExclude,
708
+ );
709
+ if (scrubHitSlop) gesture = gesture.hitSlop(scrubHitSlop);
696
710
 
697
711
  // Tap: place/move the reticle, press the action badge, or dismiss the lock.
698
712
  // Composed ahead of the pan by the controller, so a tap is never swallowed.
@@ -767,11 +781,15 @@ export function useCrosshair(
767
781
  .maxDistance(SCRUB_ACTION_TAP_SLOP)
768
782
  .onEnd(handleActionTap)
769
783
  : undefined;
770
- // Keep the axis-drag band scroll-only for taps too a tap there shouldn't
771
- // drop an order-ticket reticle.
772
- if (tapGesture && scrubBottomExclude > 0) {
773
- tapGesture = tapGesture.hitSlop({ bottom: -scrubBottomExclude });
774
- }
784
+ // Keep the bottom axis scroll-only. Do not apply horizontal plot bounds here:
785
+ // the action badge is intentionally tappable in the right gutter.
786
+ const tapHitSlop = resolveScrubHitSlop(
787
+ padding,
788
+ false,
789
+ scrubBottomExclude,
790
+ );
791
+ if (tapGesture && tapHitSlop)
792
+ tapGesture = tapGesture.hitSlop(tapHitSlop);
775
793
 
776
794
  return {
777
795
  scrubX,
@@ -22,9 +22,12 @@ import {
22
22
  computeScrubDotY,
23
23
  computeScrubTime,
24
24
  HIDDEN_TOOLTIP,
25
+ resolveScrubHitSlop,
25
26
  SCRUB_ACTIVATE_X_PX,
26
27
  SCRUB_FAIL_Y_PX,
28
+ startPlainScrub,
27
29
  type CrosshairState,
30
+ updatePlainScrub,
28
31
  } from "./crosshairShared";
29
32
  import {
30
33
  delayedPanTouchCancelled,
@@ -65,6 +68,11 @@ export function useCrosshairSeries(
65
68
  */
66
69
  scrollActive?: SharedValue<boolean>,
67
70
  tooltip?: CrosshairSeriesTooltipOptions,
71
+ /**
72
+ * Reject scrub starts beyond either horizontal plot edge and clamp active
73
+ * drags to those bounds. Default `false`.
74
+ */
75
+ clampToPlot = false,
68
76
  ): CrosshairState {
69
77
  const scrubX = useSharedValue(-1);
70
78
  const scrubActive = useSharedValue(false);
@@ -290,9 +298,15 @@ export function useCrosshairSeries(
290
298
  )
291
299
  return;
292
300
  if (!enabled) return;
293
- scrubX.set(e.x);
294
- scrubActive.set(true);
295
- gestureStarted.set(true);
301
+ startPlainScrub(
302
+ e.x,
303
+ padding,
304
+ engine.canvasWidth.get(),
305
+ clampToPlot,
306
+ scrubX,
307
+ scrubActive,
308
+ gestureStarted,
309
+ );
296
310
  if (hasOnGestureStart) scheduleOnRN(handleGestureStart);
297
311
  },
298
312
  )
@@ -300,7 +314,14 @@ export function useCrosshairSeries(
300
314
  /* istanbul ignore next */ (e) => {
301
315
  "worklet";
302
316
  if (!enabled) return;
303
- scrubX.set(e.x);
317
+ updatePlainScrub(
318
+ e.x,
319
+ padding,
320
+ engine.canvasWidth.get(),
321
+ clampToPlot,
322
+ scrubX,
323
+ scrubActive,
324
+ );
304
325
  },
305
326
  )
306
327
  .onFinalize(
@@ -326,6 +347,12 @@ export function useCrosshairSeries(
326
347
  .activeOffsetX([-SCRUB_ACTIVATE_X_PX, SCRUB_ACTIVATE_X_PX])
327
348
  .failOffsetY([-SCRUB_FAIL_Y_PX, SCRUB_FAIL_Y_PX]);
328
349
 
350
+ // Gate by touch-down position so outside starts fail before activation and
351
+ // remain available to competing/parent gestures. Accepted pans keep tracking
352
+ // outside because `shouldCancelWhenOutside(false)` is set above.
353
+ const scrubHitSlop = resolveScrubHitSlop(padding, clampToPlot);
354
+ if (scrubHitSlop) gesture = gesture.hitSlop(scrubHitSlop);
355
+
329
356
  return {
330
357
  scrubX,
331
358
  scrubActive,
@@ -0,0 +1,57 @@
1
+ import {
2
+ useDerivedValue,
3
+ type SharedValue,
4
+ } from "react-native-reanimated";
5
+ import { computeCrosshairOpacity } from "./crosshairShared";
6
+
7
+ /**
8
+ * Opacity for the visible crosshair UI. The trailing-content dim keeps using
9
+ * the edge-faded opacity directly, independent of this setting.
10
+ */
11
+ export function useCrosshairVisibleOpacity(
12
+ scrubX: SharedValue<number>,
13
+ canvasWidth: SharedValue<number>,
14
+ paddingRight: number,
15
+ active: SharedValue<number> | SharedValue<boolean>,
16
+ fade: boolean,
17
+ fadeDistance = 4,
18
+ ) {
19
+ return useDerivedValue(
20
+ () =>
21
+ resolveCrosshairVisibleOpacity(
22
+ Boolean(active.get()),
23
+ scrubX.get(),
24
+ canvasWidth.get(),
25
+ paddingRight,
26
+ fade,
27
+ fadeDistance,
28
+ ),
29
+ [
30
+ scrubX,
31
+ canvasWidth,
32
+ paddingRight,
33
+ active,
34
+ fade,
35
+ fadeDistance,
36
+ ],
37
+ );
38
+ }
39
+
40
+ export function resolveCrosshairVisibleOpacity(
41
+ active: boolean,
42
+ scrubX: number,
43
+ canvasWidth: number,
44
+ paddingRight: number,
45
+ fade: boolean,
46
+ fadeDistance = 4,
47
+ ): number {
48
+ "worklet";
49
+ if (!fade) return active ? 1 : 0;
50
+ return computeCrosshairOpacity(
51
+ active,
52
+ scrubX,
53
+ canvasWidth,
54
+ paddingRight,
55
+ fadeDistance,
56
+ );
57
+ }
@@ -2,6 +2,10 @@ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
2
2
 
3
3
  import type { SkFont } from "@shopify/react-native-skia";
4
4
  import type { ChartEngineLayout } from "../core/useLiveChartEngine";
5
+ import {
6
+ rightAnchoredYAxisColumnLayout,
7
+ type YAxisEntry,
8
+ } from "../draw/grid";
5
9
  import type { ChartPadding } from "../draw/line";
6
10
  import { measureFontTextWidth } from "../lib/measureFontTextWidth";
7
11
  import {
@@ -293,6 +297,14 @@ export function useReferenceLine(
293
297
  */
294
298
  dragValues?: SharedValue<number[]>,
295
299
  index = 0,
300
+ /** Y-axis labels used to match the right-anchored grid endpoint. */
301
+ yAxisEntries?: SharedValue<YAxisEntry[]>,
302
+ /** Enables clipping when set; matches {@link YAxisConfig.labelRightMargin}. */
303
+ labelRightMargin?: number,
304
+ /** Gap before the label column; matches {@link YAxisConfig.gridEndGap}. */
305
+ gridEndGap = 0,
306
+ /** Font used by the Y-axis labels; badge font overrides must not move the line. */
307
+ yAxisFont: SkFont = font,
296
308
  ): SharedValue<ReferenceLineLayout> {
297
309
  const form = line ? referenceLineForm(line) : "none";
298
310
  // Badge presentation depends only on the (stable) line props — resolve once.
@@ -314,7 +326,20 @@ export function useReferenceLine(
314
326
  // the badge/label anchor (x1/x2) stays at the plot edges either way.
315
327
  const fullWidth = line.fullWidth ?? false;
316
328
  const lineX1 = fullWidth ? 0 : x1;
317
- const lineX2 = fullWidth ? w : x2;
329
+ const yAxisColumn =
330
+ form === "line" &&
331
+ !fullWidth &&
332
+ yAxisEntries !== undefined &&
333
+ labelRightMargin !== undefined
334
+ ? rightAnchoredYAxisColumnLayout(
335
+ w,
336
+ yAxisEntries.value,
337
+ yAxisFont,
338
+ labelRightMargin,
339
+ gridEndGap,
340
+ )
341
+ : null;
342
+ const lineX2 = fullWidth ? w : (yAxisColumn?.gridEndX ?? x2);
318
343
 
319
344
  const fm = font.getMetrics();
320
345
  const baselineOffset = (fm.ascent + fm.descent) / 2;
package/src/types.ts CHANGED
@@ -647,6 +647,18 @@ export interface YAxisConfig {
647
647
  * (the label pool size). Omit (or `0`) for the default dynamic grid.
648
648
  */
649
649
  count?: number;
650
+ /**
651
+ * Place price labels in a shared left-aligned column whose right edge sits
652
+ * this many pixels from the canvas edge. When omitted, labels keep the
653
+ * default centered-gutter placement.
654
+ */
655
+ labelRightMargin?: number;
656
+ /**
657
+ * Gap (px) between the shared price-label column and the end of each grid or
658
+ * plain reference line. Only applies when {@link labelRightMargin} is set.
659
+ * Default `0`.
660
+ */
661
+ gridEndGap?: number;
650
662
  /**
651
663
  * Float the price axis over a full-width plot instead of reserving a right
652
664
  * gutter for it. The line/candles run all the way to the right edge, and the
@@ -831,6 +843,33 @@ export interface ScrubConfig {
831
843
  dimOpacity?: number;
832
844
  /** Vertical crosshair line stroke. Omit to use theme `crosshairLine`. */
833
845
  crosshairLineColor?: string;
846
+ /** Vertical crosshair line width in px. Default `1`. */
847
+ crosshairStrokeWidth?: number;
848
+ /**
849
+ * Extend the vertical crosshair line past the plot's top and bottom edges by
850
+ * this many px. A measured top custom-tooltip stop remains unchanged so the
851
+ * line does not draw through the tooltip. Negative values are clamped to `0`.
852
+ * Default `0`.
853
+ */
854
+ crosshairOvershoot?: number;
855
+ /**
856
+ * Fade the crosshair as it approaches the live edge. Set `false` to keep the
857
+ * line, selection dot, and tooltip fully opaque while scrubbing. The trailing
858
+ * content dim keeps its own edge fade. Default `true`.
859
+ */
860
+ crosshairFade?: boolean;
861
+ /**
862
+ * Distance in px over which the visible crosshair fades as it approaches the
863
+ * live edge. Applies to the line, selection dot, and tooltip when
864
+ * `crosshairFade` is enabled; the trailing-content dim keeps its existing
865
+ * fade. Negative values are clamped to `0`. Default `4`.
866
+ */
867
+ crosshairFadeDistance?: number;
868
+ /**
869
+ * Cap style for the vertical crosshair line. Omit to preserve the existing
870
+ * Skia default.
871
+ */
872
+ crosshairLineCap?: "butt" | "round" | "square";
834
873
  /**
835
874
  * Dash the vertical crosshair line. `true` → a default `[4, 4]` dash; an array
836
875
  * sets explicit Skia dash intervals `[on, off, …]` in px. Omit / `false` → a
@@ -898,6 +937,14 @@ export interface ScrubConfig {
898
937
  * `false` / omitted keeps the overlays visible while scrubbing (default).
899
938
  */
900
939
  hideOverlaysOnScrub?: boolean;
940
+ /**
941
+ * Keep plain scrubbing inside the horizontal plot bounds. A gesture that
942
+ * starts in the Y-axis gutter (or beyond either horizontal edge) is ignored;
943
+ * one that starts inside remains active and clamps to the nearest plot edge
944
+ * while dragging. Scrub-action gestures retain their existing behavior.
945
+ * Default `false`.
946
+ */
947
+ clampToPlot?: boolean;
901
948
  }
902
949
 
903
950
  /**