react-native-livechart 4.25.0 → 4.26.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 (42) hide show
  1. package/dist/components/LiveChart.d.ts.map +1 -1
  2. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  3. package/dist/core/liveChartEngineTick.d.ts +2 -1
  4. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  5. package/dist/core/liveChartSeriesEngineTick.d.ts +2 -1
  6. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
  7. package/dist/core/useLiveChartEngine.d.ts +3 -1
  8. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  9. package/dist/core/useLiveChartSeriesEngine.d.ts +3 -1
  10. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  11. package/dist/hooks/crosshairShared.d.ts +8 -0
  12. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  13. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  14. package/dist/hooks/useReferenceDrag.d.ts +3 -2
  15. package/dist/hooks/useReferenceDrag.d.ts.map +1 -1
  16. package/dist/hooks/useReferenceLineGrouping.d.ts +19 -0
  17. package/dist/hooks/useReferenceLineGrouping.d.ts.map +1 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/math/rangeAnimation.d.ts +4 -0
  21. package/dist/math/rangeAnimation.d.ts.map +1 -0
  22. package/dist/math/referenceDrag.d.ts +17 -5
  23. package/dist/math/referenceDrag.d.ts.map +1 -1
  24. package/dist/math/referenceGroup.d.ts +2 -2
  25. package/dist/types.d.ts +41 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/LiveChart.tsx +3 -62
  29. package/src/components/LiveChartSeries.tsx +4 -0
  30. package/src/core/liveChartEngineTick.ts +17 -6
  31. package/src/core/liveChartSeriesEngineTick.ts +18 -5
  32. package/src/core/useLiveChartEngine.ts +10 -2
  33. package/src/core/useLiveChartSeriesEngine.ts +6 -1
  34. package/src/hooks/crosshairShared.ts +29 -0
  35. package/src/hooks/useCrosshair.ts +15 -3
  36. package/src/hooks/useReferenceDrag.ts +28 -14
  37. package/src/hooks/useReferenceLineGrouping.ts +72 -0
  38. package/src/index.ts +1 -0
  39. package/src/math/rangeAnimation.ts +28 -0
  40. package/src/math/referenceDrag.ts +21 -4
  41. package/src/math/referenceGroup.ts +2 -2
  42. package/src/types.ts +42 -1
@@ -18,11 +18,7 @@ import {
18
18
  } from "../math/referenceDrag";
19
19
  import { referenceLineForm } from "../math/referenceLines";
20
20
  import type { ReferenceLine } from "../types";
21
- import {
22
- computeScrubDotY,
23
- computeValueAtY,
24
- snapPrice,
25
- } from "./crosshairShared";
21
+ import { computeValueAtY, pinnedPlotY, snapPrice } from "./crosshairShared";
26
22
 
27
23
  /** Vertical reach (px) around a line within which a touch grabs it. */
28
24
  const GRAB_SLOP = 14;
@@ -37,12 +33,13 @@ const EMPTY: never[] = [];
37
33
  * Builds the per-line **drag** gesture for draggable Form-A reference lines: grab a
38
34
  * line near its value-Y and drag vertically to set a new value, with optional
39
35
  * `snap` + `bounds` clamp. Mirrors the order-ticket reticle in {@link useCrosshair}
40
- * (value↔Y via `computeValueAtY` / `computeScrubDotY`, frozen value re-projected
36
+ * (value↔Y via `computeValueAtY` / `pinnedPlotY`, frozen value re-projected
41
37
  * each frame) but per line, writing into the shared `dragValues` array the layout
42
38
  * and overlays read.
43
39
  *
44
40
  * The pan uses `manualActivation`: it grabs only when a touch starts within
45
- * {@link GRAB_SLOP} of a draggable line, then **owns** that touch — any drag past
41
+ * {@link GRAB_SLOP} of a draggable line (and inside its `grabRange`, when it has
42
+ * one), then **owns** that touch — any drag past
46
43
  * {@link DRAG_ACTIVATE_PX} (in either axis) drags the line. A touch off every line
47
44
  * fails fast so the chart's other gestures (scrub / scroll) run everywhere else
48
45
  * (compose this ahead of them via `Gesture.Exclusive`). Crucially it no longer
@@ -86,6 +83,7 @@ export function useReferenceDrag(
86
83
  const dMax = engine.displayMax.get();
87
84
  const top = padding.top;
88
85
  const bottom = ch - padding.bottom;
86
+ if (bottom <= top) return EMPTY;
89
87
  const out: number[] = [];
90
88
  for (let i = 0; i < lines.length; i++) {
91
89
  const l = lines[i];
@@ -98,12 +96,16 @@ export function useReferenceDrag(
98
96
  continue;
99
97
  }
100
98
  const v = dragValues.get()[i] ?? l.value;
101
- const y = computeScrubDotY(v, dMin, dMax, ch, top, padding.bottom);
102
- out.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
99
+ out.push(pinnedPlotY(v, dMin, dMax, ch, top, padding.bottom));
103
100
  }
104
101
  return out;
105
102
  });
106
103
 
104
+ // Per-line grab x-ranges (`ReferenceLine.grabRange`), index-aligned with
105
+ // `lines`; null = anywhere along the line. Read by the gesture worklets below,
106
+ // which close over this render's `lines` as the callbacks do.
107
+ const grabRanges = lines.map((l) => l.grabRange ?? null);
108
+
107
109
  const dragIndex = useSharedValue(-1);
108
110
  const startX = useSharedValue(0);
109
111
  const startY = useSharedValue(0);
@@ -175,7 +177,10 @@ export function useReferenceDrag(
175
177
  "worklet";
176
178
  const t = e.changedTouches[0];
177
179
  if (!t) return;
178
- const i = nearestDraggableIndex(handleYs.get(), t.y, GRAB_SLOP);
180
+ const i = nearestDraggableIndex(handleYs.get(), t.y, GRAB_SLOP, {
181
+ x: t.x,
182
+ ranges: grabRanges,
183
+ });
179
184
  if (i < 0) {
180
185
  manager.fail();
181
186
  return;
@@ -266,13 +271,22 @@ export function useReferenceDrag(
266
271
  // is grabbed the answer is yes regardless of position: the scrub asks with its
267
272
  // touch-DOWN point, which a longer drag has carried the line away from, so the
268
273
  // geometric test alone said "no line here" and a crosshair opened mid-drag.
269
- // Otherwise it is the y-reach around the handles (x is unused — a Form-A line
270
- // spans the full width).
274
+ // Otherwise it is the y-reach around the handles, along the whole line — or,
275
+ // for a line with a `grabRange`, only inside it.
271
276
  /* istanbul ignore next -- worklet, runs on the UI thread */
272
- const hitTest = (_x: number, y: number): boolean => {
277
+ const hitTest = (x: number, y: number): boolean => {
273
278
  "worklet";
274
279
  if (!anyDraggable) return false;
275
- return referenceDragOwnsTouch(dragIndex.get(), handleYs.get(), y, GRAB_SLOP);
280
+ return referenceDragOwnsTouch(
281
+ dragIndex.get(),
282
+ handleYs.get(),
283
+ y,
284
+ GRAB_SLOP,
285
+ {
286
+ x,
287
+ ranges: grabRanges,
288
+ },
289
+ );
276
290
  };
277
291
 
278
292
  const gesture = Gesture.Pan()
@@ -0,0 +1,72 @@
1
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
2
+
3
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
4
+ import type { ChartPadding } from "../draw/line";
5
+ import { groupReferenceLines, type ReferenceGrouping } from "../math/referenceGroup";
6
+ import { referenceLineForm } from "../math/referenceLines";
7
+ import type { ReferenceLine } from "../types";
8
+ import { pinnedPlotY } from "./crosshairShared";
9
+
10
+ /** Stable result while grouping is disabled. */
11
+ const EMPTY_GROUPING: ReferenceGrouping = { hidden: [], groups: [] };
12
+
13
+ /** Cluster the built-in tags that can actually render in the current plot. */
14
+ export function useReferenceLineGrouping({
15
+ radius,
16
+ engine,
17
+ padding,
18
+ lines,
19
+ custom,
20
+ offAxisCustom,
21
+ dragValues,
22
+ }: {
23
+ radius: number | null;
24
+ engine: ChartEngineLayout;
25
+ padding: ChartPadding;
26
+ lines: ReferenceLine[];
27
+ custom: boolean[];
28
+ offAxisCustom: boolean[];
29
+ dragValues: SharedValue<number[]>;
30
+ }) {
31
+ const hasOffAxisBadge = lines.map((line) => Boolean(line.badge || line.offAxisBadge));
32
+ const result = useDerivedValue<ReferenceGrouping>(() => {
33
+ if (radius == null) return EMPTY_GROUPING;
34
+ const canvasHeight = engine.canvasHeight.get();
35
+ const displayMin = engine.displayMin.get();
36
+ const displayMax = engine.displayMax.get();
37
+ const top = padding.top;
38
+ const yPositions: number[] = [];
39
+ for (let index = 0; index < lines.length; index++) {
40
+ const line = lines[index];
41
+ if (
42
+ referenceLineForm(line) !== "line" ||
43
+ line.value === undefined ||
44
+ custom[index] ||
45
+ offAxisCustom[index]
46
+ ) {
47
+ yPositions.push(-1);
48
+ continue;
49
+ }
50
+ const value = dragValues.get()[index] ?? line.value;
51
+ // Plain off-axis lines are culled by useReferenceLine. Only a configured
52
+ // badge can contribute a visible tag to the edge's count pill.
53
+ if ((value < displayMin || value > displayMax) && !hasOffAxisBadge[index]) {
54
+ yPositions.push(-1);
55
+ continue;
56
+ }
57
+ yPositions.push(
58
+ pinnedPlotY(
59
+ value,
60
+ displayMin,
61
+ displayMax,
62
+ canvasHeight,
63
+ top,
64
+ padding.bottom,
65
+ ),
66
+ );
67
+ }
68
+ return groupReferenceLines(yPositions, radius);
69
+ });
70
+ const hidden = useDerivedValue<boolean[]>(() => result.get().hidden);
71
+ return { refGroupResult: result, groupHidden: hidden };
72
+ }
package/src/index.ts CHANGED
@@ -97,6 +97,7 @@ export type {
97
97
  MultiSeriesDotConfig,
98
98
  PerSeriesTooltipConfig,
99
99
  PulseConfig,
100
+ RangeAnimationConfig,
100
101
  ReferenceLine,
101
102
  ReferenceLineBadgeConfig,
102
103
  ReferenceLineGroupingConfig,
@@ -0,0 +1,28 @@
1
+ import type { RangeAnimationConfig } from "../types";
2
+
3
+ // Worklet closures capture local functions when the caller is initialized.
4
+ function resolveSpeed(smoothing: number, speed: number | undefined): number {
5
+ "worklet";
6
+ return speed === undefined || !Number.isFinite(speed)
7
+ ? smoothing
8
+ : Math.max(0, Math.min(1, speed));
9
+ }
10
+
11
+ /** Select per-bound easing while preserving immediate expansion and reduced motion. */
12
+ export function rangeAnimationSpeed(
13
+ smoothing: number,
14
+ config: RangeAnimationConfig | undefined,
15
+ expanding: boolean,
16
+ disjoint = false,
17
+ ): number {
18
+ "worklet";
19
+ if (smoothing >= 1 || (expanding && !config?.animateExpansion)) return 1;
20
+ const speed = resolveSpeed(
21
+ smoothing,
22
+ expanding ? config?.expansionSmoothing : config?.contractionSmoothing,
23
+ );
24
+ // Until the ranges overlap, contraction must not outrun the expanding bound.
25
+ return !expanding && disjoint && config?.animateExpansion
26
+ ? Math.min(speed, resolveSpeed(smoothing, config.expansionSmoothing))
27
+ : speed;
28
+ }
@@ -17,16 +17,29 @@ export function clampToBounds(
17
17
  return Math.min(hi, Math.max(lo, value));
18
18
  }
19
19
 
20
+ /**
21
+ * Where a touch may grab each line: the touch `x` and, index-aligned with the
22
+ * chart's `referenceLines`, each line's `grabRange` (`[from, to]`, canvas px) —
23
+ * `null` / `undefined` for a line grabbed anywhere along it.
24
+ */
25
+ export interface GrabRangeProbe {
26
+ x: number;
27
+ ranges: readonly (readonly [number, number] | null | undefined)[];
28
+ }
29
+
20
30
  /**
21
31
  * Index of the draggable line whose handle-Y is nearest the touch `y`, within
22
32
  * `slop` px — or `-1` when none is in reach. `handleYs` is index-aligned with the
23
- * chart's `referenceLines`; entries `< 0` (not draggable / off-screen / not laid
24
- * out) are skipped. Ties favor the later (topmost-drawn) line.
33
+ * chart's `referenceLines`; entries `< 0` (not draggable / not laid out) are
34
+ * skipped; off-screen lines pin to the nearest plot edge. Ties favor the later
35
+ * (topmost-drawn) line. With `grab`, a line
36
+ * whose `grabRange` does not contain the touch `x` is skipped too.
25
37
  */
26
38
  export function nearestDraggableIndex(
27
39
  handleYs: number[],
28
40
  y: number,
29
41
  slop: number,
42
+ grab?: GrabRangeProbe,
30
43
  ): number {
31
44
  "worklet";
32
45
  let best = -1;
@@ -34,6 +47,8 @@ export function nearestDraggableIndex(
34
47
  for (let i = 0; i < handleYs.length; i++) {
35
48
  const hy = handleYs[i];
36
49
  if (hy < 0) continue;
50
+ const range = grab?.ranges[i];
51
+ if (range && (grab.x < range[0] || grab.x > range[1])) continue;
37
52
  const d = Math.abs(hy - y);
38
53
  if (d <= bestDist) {
39
54
  bestDist = d;
@@ -49,17 +64,19 @@ export function nearestDraggableIndex(
49
64
  * or already dragging) owns the touch outright, wherever the asking gesture says
50
65
  * the finger is: the scrub pan can activate mid-drag and asks with its
51
66
  * touch-DOWN point, which a longer drag has carried the line away from. With no
52
- * line grabbed, it is the geometric reach test around the handles.
67
+ * line grabbed, it is the geometric reach test around the handles (and, with
68
+ * `grab`, inside each line's `grabRange`).
53
69
  */
54
70
  export function referenceDragOwnsTouch(
55
71
  dragIndex: number,
56
72
  handleYs: number[],
57
73
  y: number,
58
74
  slop: number,
75
+ grab?: GrabRangeProbe,
59
76
  ): boolean {
60
77
  "worklet";
61
78
  if (dragIndex >= 0) return true;
62
- return nearestDraggableIndex(handleYs, y, slop) >= 0;
79
+ return nearestDraggableIndex(handleYs, y, slop, grab) >= 0;
63
80
  }
64
81
 
65
82
  /**
@@ -26,8 +26,8 @@ export interface ReferenceGrouping {
26
26
 
27
27
  /**
28
28
  * Single-linkage cluster of reference-line handle Ys: lines whose sorted Y gaps are
29
- * all `<= radius` chain into one group. Entries `< 0` (not a Form-A line / off the
30
- * canvas) are ignored. Returns which lines are collapsed (`hidden`) plus a centroid
29
+ * all `<= radius` chain into one group. Entries `< 0` (not a Form-A line / not laid
30
+ * out) are ignored. Returns which lines are collapsed (`hidden`) plus a centroid
31
31
  * + count per multi-line cluster. A non-positive `radius` disables grouping.
32
32
  */
33
33
  export function groupReferenceLines(
package/src/types.ts CHANGED
@@ -191,6 +191,18 @@ export interface ReferenceLine {
191
191
  * it controlled. No effect on bands / time bands. Default `false`.
192
192
  */
193
193
  draggable?: boolean;
194
+ /**
195
+ * Where along a draggable line a press may grab it: a canvas x-range in px,
196
+ * `[from, to]`, ends included — typically the span of a custom tag drawn with
197
+ * `renderReferenceLine` (measure it with `onLayout`; a left-pinned tag starts 2 px
198
+ * inside the plot's left edge). Omit to grab the line anywhere along it (the
199
+ * default). With a range, a pan, scrub or page scroll that merely starts near the
200
+ * line no longer moves it; a press inside the range grabs it as before (same Y
201
+ * reach). `from > to` makes the line ungrabbable. A JS value: leave a little
202
+ * slack for a tag that changes width (off-axis, a live price). Applies only to
203
+ * draggable Form-A lines.
204
+ */
205
+ grabRange?: [number, number];
194
206
  /**
195
207
  * Snap the dragged value to this increment (e.g. `0.01` for cents, `0.5` for a
196
208
  * tick size) so drops land on round levels. Omit for free dragging. Applies only
@@ -334,7 +346,9 @@ export interface ReferenceLineRenderProps {
334
346
  * lines whose handles fall within {@link ReferenceLineGroupingConfig.radius} px of
335
347
  * each other collapse into a single count handle, so a cluster of nearby orders /
336
348
  * alerts reads as one tag instead of an unreadable pile. Pass `true` for defaults
337
- * or an object to tune the proximity radius.
349
+ * or an object to tune the proximity radius. Badged off-axis lines group at either
350
+ * plot edge; off-axis lines with no badge are hidden and do not count. Custom tags
351
+ * are excluded.
338
352
  */
339
353
  export interface ReferenceLineGroupingConfig {
340
354
  /**
@@ -2258,6 +2272,25 @@ export interface LoadingConfig {
2258
2272
  axisLabels?: boolean;
2259
2273
  }
2260
2274
 
2275
+ /** Y-range easing shared by both charts, independent of value/window tracking. */
2276
+ export interface RangeAnimationConfig {
2277
+ /** Ease outward bounds instead of snapping. Default `false`. */
2278
+ animateExpansion?: boolean;
2279
+ /**
2280
+ * Fraction of the remaining gap closed per 60fps frame when a bound expands.
2281
+ * Inherits `smoothing`; only used with `animateExpansion: true`. Finite values
2282
+ * are clamped to 0..1; non-finite values inherit `smoothing`.
2283
+ */
2284
+ expansionSmoothing?: number;
2285
+ /**
2286
+ * Fraction of the remaining gap closed per 60fps frame when a bound contracts.
2287
+ * Inherits `smoothing`. Finite values are clamped to 0..1; non-finite values
2288
+ * inherit `smoothing`. While animated expansion moves toward a disjoint fit,
2289
+ * contraction is limited to the expansion speed so bounds cannot cross.
2290
+ */
2291
+ contractionSmoothing?: number;
2292
+ }
2293
+
2261
2294
  /** Props shared between `LiveChart` and `LiveChartSeries`. */
2262
2295
  export interface LiveChartCoreProps {
2263
2296
  /** Color scheme. Default `"dark"`. */
@@ -2324,6 +2357,14 @@ export interface LiveChartCoreProps {
2324
2357
  * `lerpSpeed`. Default `0.08`.
2325
2358
  */
2326
2359
  smoothing?: number;
2360
+ /**
2361
+ * Control Y-range expansion and contraction independently of `smoothing` for
2362
+ * value/window tracking. Omission preserves instant expansion and eased
2363
+ * contraction. Uses frame-rate-independent exponential easing, not a fixed
2364
+ * duration. `smoothing: 1`, explicit `snapKey` changes, static charts, and active
2365
+ * manual Y-scale dragging remain immediate. Axis-label fades use `metrics.grid`.
2366
+ */
2367
+ rangeAnimation?: RangeAnimationConfig;
2327
2368
  /**
2328
2369
  * Snap the framing to its target in a single frame whenever this key changes —
2329
2370
  * without giving up smooth live ticks. On a timeframe / dataset switch, the