react-native-livechart 4.2.1 → 4.4.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 (39) 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/components/MarkerOverlay.d.ts.map +1 -1
  4. package/dist/constants.d.ts +6 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/core/liveChartEngineTick.d.ts +11 -0
  7. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  8. package/dist/core/liveChartSeriesEngineTick.d.ts +10 -0
  9. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
  10. package/dist/core/resolveConfig.d.ts +11 -1
  11. package/dist/core/resolveConfig.d.ts.map +1 -1
  12. package/dist/core/useLiveChartEngine.d.ts +18 -0
  13. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  14. package/dist/core/useLiveChartSeriesEngine.d.ts +17 -0
  15. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  16. package/dist/draw/markerAtlas.d.ts +9 -2
  17. package/dist/draw/markerAtlas.d.ts.map +1 -1
  18. package/dist/hooks/useMarkers.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/math/markerCluster.d.ts +8 -1
  22. package/dist/math/markerCluster.d.ts.map +1 -1
  23. package/dist/types.d.ts +82 -0
  24. package/dist/types.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/LiveChart.tsx +13 -2
  27. package/src/components/LiveChartSeries.tsx +7 -0
  28. package/src/components/MarkerOverlay.tsx +135 -37
  29. package/src/constants.ts +7 -0
  30. package/src/core/liveChartEngineTick.ts +42 -7
  31. package/src/core/liveChartSeriesEngineTick.ts +37 -5
  32. package/src/core/resolveConfig.ts +25 -0
  33. package/src/core/useLiveChartEngine.ts +71 -1
  34. package/src/core/useLiveChartSeriesEngine.ts +65 -1
  35. package/src/draw/markerAtlas.ts +30 -1
  36. package/src/hooks/useMarkers.ts +2 -0
  37. package/src/index.ts +2 -0
  38. package/src/math/markerCluster.ts +8 -1
  39. package/src/types.ts +84 -0
@@ -1,11 +1,15 @@
1
1
  import { useRef, useState } from "react";
2
2
  import {
3
+ cancelAnimation,
4
+ Easing,
5
+ useAnimatedReaction,
3
6
  useDerivedValue,
4
7
  useFrameCallback,
5
8
  useSharedValue,
9
+ withTiming,
6
10
  type SharedValue,
7
11
  } from "react-native-reanimated";
8
- import { MS_PER_FRAME_60FPS } from "../constants";
12
+ import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
9
13
  import type { LiveChartPoint, SeriesConfig } from "../types";
10
14
  import { tickLiveChartSeriesEngineFrame } from "./liveChartSeriesEngineTick";
11
15
  import type { ChartEngineScroll, MultiEngineState } from "./useLiveChartEngine";
@@ -24,6 +28,19 @@ export interface MultiSeriesEngineConfig {
24
28
  nowOverride?: number;
25
29
  windowBuffer?: number;
26
30
  paused?: boolean;
31
+ /**
32
+ * Whether the `timeScroll` gesture is active. Flipping to `false` while scrolled
33
+ * back clears {@link ChartEngineScroll.viewEnd} and **glides** the window back to
34
+ * the live edge (eased, not an instant jump), so disabling time-scroll never
35
+ * strands the chart at a past position. See #164.
36
+ */
37
+ scrollEnabled?: boolean;
38
+ /**
39
+ * Duration (ms) of the return-to-live glide when {@link scrollEnabled} flips to
40
+ * `false` while scrolled back. `0` snaps instantly. Defaults to
41
+ * {@link RETURN_TO_LIVE_MS}. Resolved from `timeScroll.returnToLive`. See #164.
42
+ */
43
+ returnToLiveMs?: number;
27
44
  }
28
45
 
29
46
  export interface MultiEngineFrameRefs {
@@ -49,6 +66,10 @@ export interface MultiEngineFrameRefs {
49
66
  pausedSV: SharedValue<boolean>;
50
67
  /** Pan-scroll right-edge override (null = follow live). Optional for callers/tests. */
51
68
  viewEndSV?: SharedValue<number | null>;
69
+ /** "Return to live" glide progress (0→1); the right edge eases to live. Optional. */
70
+ returnTSV?: SharedValue<number>;
71
+ /** Frozen right-edge time the return glide starts from. Optional. */
72
+ returnFromSV?: SharedValue<number>;
52
73
  /** Pinch-zoom window-width override (null = follow timeWindow). Optional for callers/tests. */
53
74
  viewWindowSV?: SharedValue<number | null>;
54
75
  /** Receives the computed live edge each frame. Optional for callers/tests. */
@@ -137,6 +158,8 @@ export function applyLiveChartSeriesEngineFrame(
137
158
  nowSeconds: Date.now() / 1000,
138
159
  paused: sv.pausedSV.value,
139
160
  viewEnd: sv.viewEndSV?.value,
161
+ returnT: sv.returnTSV?.value,
162
+ returnFrom: sv.returnFromSV?.value,
140
163
  viewWindow: sv.viewWindowSV?.value,
141
164
  });
142
165
  sv.displayMin.value = state.displayMin;
@@ -173,6 +196,13 @@ export function useLiveChartSeriesEngine(
173
196
  const nowOverrideSV = useDerivedValue(() => config.nowOverride);
174
197
  const windowBufferSV = useDerivedValue(() => config.windowBuffer ?? 0);
175
198
  const pausedSV = useDerivedValue(() => config.paused ?? false);
199
+ // Whether time-scroll is active — drives the return-to-live reaction below.
200
+ // Defaults to enabled (legacy behavior).
201
+ const scrollEnabledSV = useDerivedValue(() => config.scrollEnabled ?? true);
202
+ // Return-to-live glide duration (ms); 0 = instant snap. Read by the reaction.
203
+ const returnToLiveMsSV = useDerivedValue(
204
+ () => config.returnToLiveMs ?? RETURN_TO_LIVE_MS,
205
+ );
176
206
 
177
207
  const displayMin = useSharedValue(0);
178
208
  const displayMax = useSharedValue(1);
@@ -186,6 +216,10 @@ export function useLiveChartSeriesEngine(
186
216
  // Pan-scroll state (see useLiveChartEngine). Defaults to following live.
187
217
  const viewEnd = useSharedValue<number | null>(null);
188
218
  const liveEdge = useSharedValue(initialTimestamp);
219
+ // "Return to live" glide (see #164) — `returnT` rests at 1; the reaction below
220
+ // animates it 0→1 from `returnFrom` when time-scroll is disabled while scrolled.
221
+ const returnT = useSharedValue(1);
222
+ const returnFrom = useSharedValue(0);
189
223
 
190
224
  const displaySeriesValues = useSharedValue<number[]>([]);
191
225
  const seriesOpacities = useSharedValue<number[]>([]);
@@ -235,6 +269,8 @@ export function useLiveChartSeriesEngine(
235
269
  windowBufferSV,
236
270
  pausedSV,
237
271
  viewEndSV: viewEnd,
272
+ returnTSV: returnT,
273
+ returnFromSV: returnFrom,
238
274
  viewWindowSV: viewWindow,
239
275
  liveEdgeSV: liveEdge,
240
276
  extremaMinValue,
@@ -246,6 +282,34 @@ export function useLiveChartSeriesEngine(
246
282
  );
247
283
  });
248
284
 
285
+ // Return the window to live when time-scroll is disabled while scrolled back
286
+ // (mirrors useLiveChartEngine): with a positive duration, glide — snapshot the
287
+ // frozen edge, clear `viewEnd`, and animate `returnT` 0→1 so the tick eases
288
+ // `returnFrom`→live; duration 0 just clears `viewEnd` for an instant snap.
289
+ // Clearing `viewEnd` also means a later re-enable resumes from live. See #164.
290
+ useAnimatedReaction(
291
+ () => scrollEnabledSV.value,
292
+ /* istanbul ignore next -- Reanimated reaction driven by a prop→derived change; not exercised under the SharedValue mock, verified in-app */
293
+ (enabled, prev) => {
294
+ if (prev === true && !enabled && viewEnd.value != null) {
295
+ const ms = returnToLiveMsSV.value;
296
+ if (ms > 0) {
297
+ returnFrom.value = viewEnd.value;
298
+ cancelAnimation(viewEnd);
299
+ viewEnd.value = null;
300
+ returnT.value = 0;
301
+ returnT.value = withTiming(1, {
302
+ duration: ms,
303
+ easing: Easing.out(Easing.cubic),
304
+ });
305
+ } else {
306
+ cancelAnimation(viewEnd);
307
+ viewEnd.value = null;
308
+ }
309
+ }
310
+ },
311
+ );
312
+
249
313
  return {
250
314
  data,
251
315
  value,
@@ -10,7 +10,12 @@ import {
10
10
  type SkPaint,
11
11
  type SkRect,
12
12
  } from "@shopify/react-native-skia";
13
- import type { LiveChartPalette, Marker, MarkerKind } from "../types";
13
+ import type {
14
+ LiveChartPalette,
15
+ Marker,
16
+ MarkerGroupBadge,
17
+ MarkerKind,
18
+ } from "../types";
14
19
 
15
20
  /** Default icon box (px) when `marker.size` is unset. */
16
21
  export const DEFAULT_ICON_SIZE = 16;
@@ -37,6 +42,10 @@ export function groupGlyphSig(ch: string): string {
37
42
  return `gd\x1f${ch}`;
38
43
  }
39
44
 
45
+ /** Atlas cell sig for a dedicated group badge (one per chart — its own custom
46
+ * image/icon, independent of the member markers). */
47
+ export const GROUP_BADGE_SIG = "grpbadge";
48
+
40
49
  /** Atlas cell sig for the round count-badge background (one fixed circle per color). */
41
50
  export function groupBgSig(color: string): string {
42
51
  "worklet";
@@ -362,6 +371,10 @@ export function buildMarkerAtlas(
362
371
  /** Also bake count-badge cells (digits + per-color backgrounds) for
363
372
  * `markerCluster: "stacked"` collapse. Off by default — no atlas bloat. */
364
373
  withGroups = false,
374
+ /** A dedicated group badge (custom image/icon) to bake one cell for, under
375
+ * {@link GROUP_BADGE_SIG} — drawn for a collapsed cluster instead of the count.
376
+ * Only baked when it carries an `image` or `icon`. */
377
+ groupBadge?: MarkerGroupBadge,
365
378
  ): MarkerAtlas {
366
379
  const seen = new Set<string>();
367
380
  const specs: { sig: string; spec: CellSpec }[] = [];
@@ -374,6 +387,22 @@ export function buildMarkerAtlas(
374
387
  specs.push({ sig, spec: cellSpec(m, palette, font) });
375
388
  }
376
389
 
390
+ // Dedicated group badge: bake one cell from its custom image/icon (reusing the
391
+ // marker cell geometry) so a collapsed cluster can blit it like any other glyph.
392
+ if (groupBadge && (groupBadge.image || groupBadge.icon)) {
393
+ const synthetic: Marker = {
394
+ id: GROUP_BADGE_SIG,
395
+ time: 0,
396
+ kind: "trade",
397
+ image: groupBadge.image,
398
+ icon: groupBadge.icon,
399
+ color: groupBadge.color,
400
+ pill: groupBadge.pill,
401
+ size: groupBadge.size,
402
+ };
403
+ specs.push({ sig: GROUP_BADGE_SIG, spec: cellSpec(synthetic, palette, font) });
404
+ }
405
+
377
406
  // Count-badge cells: 10 uniform white digit cells shared across colors, plus one
378
407
  // fixed round background per cluster color. A collapsed group composes its number
379
408
  // (centered, ≤ 2 digits) over the circle in the same `drawAtlas`.
@@ -28,6 +28,8 @@ const ANCHORED_CLUSTER: ResolvedMarkerCluster = {
28
28
  overlap: 0.75,
29
29
  gap: 2,
30
30
  maxBeforeGroup: 5,
31
+ groupBadge: "count",
32
+ showGroupCount: false,
31
33
  };
32
34
 
33
35
  /**
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export type {
66
66
  LiveChartSeriesProps,
67
67
  Marker,
68
68
  MarkerClusterConfig,
69
+ MarkerGroupBadge,
69
70
  MarkerKind,
70
71
  MarkerPressEvent,
71
72
  MarkerRenderContext,
@@ -79,6 +80,7 @@ export type {
79
80
  ReferenceLineBadgeConfig,
80
81
  ReferenceLineGroupingConfig,
81
82
  ReferenceLineRenderProps,
83
+ ReturnToLiveConfig,
82
84
  ScrubActionConfig,
83
85
  ScrubActionPoint,
84
86
  ScrubConfig,
@@ -1,4 +1,4 @@
1
- import type { Marker, MarkerSide } from "../types";
1
+ import type { Marker, MarkerGroupBadge, MarkerSide } from "../types";
2
2
  import type { ProjectedMarker } from "./markers";
3
3
 
4
4
  /**
@@ -20,6 +20,13 @@ export interface ResolvedMarkerCluster {
20
20
  gap: number;
21
21
  /** Collapse a co-located run to a single count badge once it exceeds this many. */
22
22
  maxBeforeGroup: number;
23
+ /** What a collapsed group draws: `"count"` = the round count badge (default);
24
+ * `"marker"` = the representative marker's own glyph; a {@link MarkerGroupBadge}
25
+ * = a dedicated badge (custom image/icon) independent of the members. */
26
+ groupBadge: "count" | "marker" | MarkerGroupBadge;
27
+ /** With `groupBadge: "marker"` or a dedicated badge, also stamp the member count
28
+ * in the glyph's top-right corner. Ignored for `"count"`. */
29
+ showGroupCount: boolean;
23
30
  }
24
31
 
25
32
  export interface ClusterMarkersOpts {
package/src/types.ts CHANGED
@@ -1133,6 +1133,53 @@ export interface MarkerClusterConfig {
1133
1133
  /** Collapse a co-located run to a single count badge once it exceeds this many.
1134
1134
  * Default `5`. */
1135
1135
  maxBeforeGroup?: number;
1136
+ /**
1137
+ * What a collapsed cluster draws in place of its members:
1138
+ * - `"count"` (default) — the built-in round count badge (a circle with the
1139
+ * member count inside).
1140
+ * - `"marker"` — the **representative marker's own glyph** (its `image`, `icon`
1141
+ * /`pill`, or `kind` shape). The representative is the newest marker in the run,
1142
+ * so a run of buy pills collapses to a buy pill — handy when the group should
1143
+ * look like its members.
1144
+ * - {@link MarkerGroupBadge} — a **dedicated group badge you supply** (your own
1145
+ * Skia `image`, or an `icon`/`pill`), independent of the member markers. Use
1146
+ * this when the collapse should look *different* from the individual markers —
1147
+ * e.g. tiny dots that collapse into a distinct "Buy 5" badge image.
1148
+ *
1149
+ * All three are Skia-drawn in the same `drawAtlas` batch (not a `renderMarker`
1150
+ * RN overlay). Pair any non-`"count"` form with {@link showGroupCount} for a
1151
+ * corner count. Default `"count"`.
1152
+ */
1153
+ groupBadge?: "count" | "marker" | MarkerGroupBadge;
1154
+ /**
1155
+ * When {@link groupBadge} is `"marker"` or a {@link MarkerGroupBadge}, also stamp
1156
+ * the member count as a small badge in the glyph's top-right corner — the
1157
+ * "Buy **5**" look. Ignored when `groupBadge` is `"count"` (the count *is* the
1158
+ * badge). Default `false`.
1159
+ */
1160
+ showGroupCount?: boolean;
1161
+ }
1162
+
1163
+ /**
1164
+ * A dedicated badge drawn for a collapsed marker cluster — your own Skia design,
1165
+ * independent of the member markers. Pass it as
1166
+ * {@link MarkerClusterConfig.groupBadge}. Glyph precedence mirrors a single marker:
1167
+ * `image` → `icon` (`pill`) → a filled dot. Requires `image` or `icon` (otherwise
1168
+ * the group falls back to the count badge).
1169
+ */
1170
+ export interface MarkerGroupBadge {
1171
+ /** Custom Skia image for the collapsed group (e.g. from `useImage`). Takes
1172
+ * precedence over {@link icon}. */
1173
+ image?: SkImage;
1174
+ /** Text / emoji glyph for the collapsed group (used when {@link image} is unset).
1175
+ * Rendered with the chart font. */
1176
+ icon?: string;
1177
+ /** Color of the `icon` (and its `pill`). Defaults to a palette accent. */
1178
+ color?: string;
1179
+ /** Wrap the `icon` in a filled circular badge in {@link color} (icon drawn white). */
1180
+ pill?: boolean;
1181
+ /** Glyph box size in px (icon font size, or image width+height). Default `16`. */
1182
+ size?: number;
1136
1183
  }
1137
1184
 
1138
1185
  /** Context passed to `renderMarker` alongside the marker (cluster / position state). */
@@ -1492,6 +1539,22 @@ export interface TimeScrollConfig {
1492
1539
  scrubHoldMs?: number;
1493
1540
  }
1494
1541
 
1542
+ /**
1543
+ * Tunes the "return to live" glide — the eased animation that carries the window
1544
+ * back to the live edge when `timeScroll` is switched off while scrolled back. See
1545
+ * {@link TimeScrollConfig.returnToLive}.
1546
+ *
1547
+ * @experimental
1548
+ */
1549
+ export interface ReturnToLiveConfig {
1550
+ /**
1551
+ * Glide duration in milliseconds. Default `450`. The window decelerates onto the
1552
+ * live edge (ease-out cubic). Lower = snappier; `0` is treated as no animation
1553
+ * (an instant snap).
1554
+ */
1555
+ duration?: number;
1556
+ }
1557
+
1495
1558
  /**
1496
1559
  * Pinch-to-zoom the visible time window (see {@link LiveChartCoreProps.zoom}).
1497
1560
  *
@@ -1683,6 +1746,11 @@ export interface LiveChartCoreProps {
1683
1746
  * live edge again. One-finger plot-area scrub is unchanged. Requires retained
1684
1747
  * history in `data` / `candles` to scroll into.
1685
1748
  *
1749
+ * Setting this back to `false` while scrolled back — or switching `mode` so it's
1750
+ * no longer passed — **glides** the window back to the live edge (a brief eased
1751
+ * animation, not an instant jump); it never stays frozen at the previous scroll
1752
+ * position. Tune or disable that glide with {@link returnToLive}.
1753
+ *
1686
1754
  * `true` uses the default drag-to-scroll gesture (`"holdToScrub"`); pass a
1687
1755
  * {@link TimeScrollConfig} to pick the activation (`"holdToScrub"` or
1688
1756
  * `"axisDrag"`). Default `false`.
@@ -1690,6 +1758,22 @@ export interface LiveChartCoreProps {
1690
1758
  * @experimental Prototype — gesture model and API may change.
1691
1759
  */
1692
1760
  timeScroll?: boolean | TimeScrollConfig;
1761
+ /**
1762
+ * Controls the **return-to-live glide** — the animation that carries the window
1763
+ * back to the live edge when {@link timeScroll} is switched off (set to `false`,
1764
+ * or a `mode` switch stops passing it) while scrolled back into history:
1765
+ * - `true` (default) — glide back, easing onto the live edge over `450` ms.
1766
+ * - `false` — snap instantly, no animation.
1767
+ * - {@link ReturnToLiveConfig} — glide with a custom `{ duration }` (ms).
1768
+ *
1769
+ * Kept separate from {@link timeScroll} on purpose: the common way to leave
1770
+ * time-scroll (`timeScroll={false}`) discards any `TimeScrollConfig`, so this
1771
+ * lives alongside it to stay in effect through the toggle. Only governs that
1772
+ * programmatic return; a finger fling back to live keeps its own inertia.
1773
+ *
1774
+ * @experimental
1775
+ */
1776
+ returnToLive?: boolean | ReturnToLiveConfig;
1693
1777
  /**
1694
1778
  * Pinch-to-zoom the visible time window. Two-finger pinch in/out narrows or
1695
1779
  * widens the window, anchored at the focal point between your fingers. Composes