react-native-livechart 4.4.0 → 4.5.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.
@@ -14,6 +14,7 @@ import type {
14
14
  LineStyleConfig,
15
15
  LiveChartMetrics,
16
16
  LiveChartMetricsOverride,
17
+ LoadingConfig,
17
18
  MarkerClusterConfig,
18
19
  DotConfig,
19
20
  DotRingConfig,
@@ -29,6 +30,7 @@ import type {
29
30
  ThresholdConfig,
30
31
  ThresholdLineConfig,
31
32
  TradeEvent,
33
+ TransitionConfig,
32
34
  ValueLineConfig,
33
35
  VolumeConfig,
34
36
  XAxisConfig,
@@ -45,6 +47,8 @@ import {
45
47
  EMPTY_STATE_METRICS_DEFAULTS,
46
48
  FADE_EDGE_WIDTH,
47
49
  GRID_METRICS_DEFAULTS,
50
+ LOADING_WAVE_AMPLITUDE,
51
+ LOADING_WAVE_SPEED,
48
52
  MOTION_METRICS_DEFAULTS,
49
53
  RETURN_TO_LIVE_MS,
50
54
  } from "../constants";
@@ -165,6 +169,8 @@ export interface ResolvedScrubConfig {
165
169
  tooltipShowTime: boolean;
166
170
  /** Press-and-hold delay (ms) before scrubbing activates. 0 = immediate. */
167
171
  panGestureDelay: number;
172
+ /** Fade markers + reference lines out while scrubbing. */
173
+ hideOverlaysOnScrub: boolean;
168
174
  }
169
175
 
170
176
  export interface ResolvedScrubActionConfig {
@@ -477,6 +483,32 @@ export function resolveReturnToLiveMs(
477
483
  return d > 0 ? d : 0;
478
484
  }
479
485
 
486
+ /**
487
+ * Resolved transition durations. `undefined` for a field means "use the
488
+ * component's built-in default" (so we don't duplicate the default constants
489
+ * here); a number is an explicit duration in ms (clamped to ≥ 0).
490
+ */
491
+ export interface ResolvedTransitionConfig {
492
+ reveal: number | undefined;
493
+ mode: number | undefined;
494
+ }
495
+
496
+ const clampMs = (v: number | undefined): number | undefined =>
497
+ v == null ? undefined : v > 0 ? v : 0;
498
+
499
+ /**
500
+ * Resolves the `transitions` prop. `false` → all transitions instant (`0`);
501
+ * `true` / omitted → defaults (both `undefined` = use the built-in durations);
502
+ * an object → per-transition overrides (an omitted field keeps its default).
503
+ */
504
+ export function resolveTransitions(
505
+ prop: boolean | TransitionConfig | undefined,
506
+ ): ResolvedTransitionConfig {
507
+ if (prop === false) return { reveal: 0, mode: 0 };
508
+ if (prop == null || prop === true) return { reveal: undefined, mode: undefined };
509
+ return { reveal: clampMs(prop.reveal), mode: clampMs(prop.mode) };
510
+ }
511
+
480
512
  const AXIS_LABEL_DEFAULTS: ResolvedAxisLabelConfig = {
481
513
  format: undefined,
482
514
  color: undefined,
@@ -571,6 +603,7 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
571
603
  tooltipShowValue: true,
572
604
  tooltipShowTime: true,
573
605
  panGestureDelay: 0,
606
+ hideOverlaysOnScrub: false,
574
607
  };
575
608
 
576
609
  /**
@@ -590,6 +623,36 @@ export function resolveScrub(
590
623
  return resolved;
591
624
  }
592
625
 
626
+ export interface ResolvedLoadingConfig {
627
+ /** undefined → palette.gridLine */
628
+ color: string | undefined;
629
+ /** undefined → the chart's line strokeWidth */
630
+ strokeWidth: number | undefined;
631
+ /** Base breathing-wave amplitude (px). */
632
+ amplitude: number;
633
+ /** Breathing-wave speed multiplier. */
634
+ speed: number;
635
+ }
636
+
637
+ const LOADING_DEFAULTS: ResolvedLoadingConfig = {
638
+ color: undefined,
639
+ strokeWidth: undefined,
640
+ amplitude: LOADING_WAVE_AMPLITUDE,
641
+ speed: LOADING_WAVE_SPEED,
642
+ };
643
+
644
+ /**
645
+ * Resolves the `loading` prop to a fully-typed config or null (not loading).
646
+ * `true` → defaults (loading on, built-in look), object → merged with defaults
647
+ * (loading on, restyled), `false` / omitted → null. So a non-null result is the
648
+ * "is loading" flag and carries the resolved styling.
649
+ */
650
+ export function resolveLoading(
651
+ prop: boolean | LoadingConfig | undefined,
652
+ ): ResolvedLoadingConfig | null {
653
+ return resolveToggle(prop, LOADING_DEFAULTS, false);
654
+ }
655
+
593
656
  const SCRUB_ACTION_DEFAULTS: ResolvedScrubActionConfig = {
594
657
  icon: "+",
595
658
  background: undefined,
@@ -35,6 +35,10 @@ export function useChartPaths(
35
35
  */
36
36
  edgeValue?: SharedValue<number>,
37
37
  followViewEdge = false,
38
+ /** Loading squiggle wave amplitude (px) for the reveal morph. Default 14. */
39
+ squiggleAmplitude = 14,
40
+ /** Loading squiggle wave speed multiplier for the reveal morph. Default 1. */
41
+ squiggleSpeed = 1,
38
42
  ) {
39
43
  const lineBuilder = usePathBuilder();
40
44
  const fillBuilder = usePathBuilder();
@@ -86,7 +90,13 @@ export function useChartPaths(
86
90
  // Compute squiggly Y values at the same X positions as the real line
87
91
  const centerY =
88
92
  (engine.canvasHeight.get() - padding.bottom + padding.top) / 2;
89
- const squigglyPts = squigglifyPts(realPts, engine.timestamp.get(), centerY);
93
+ const squigglyPts = squigglifyPts(
94
+ realPts,
95
+ engine.timestamp.get(),
96
+ centerY,
97
+ squiggleAmplitude,
98
+ squiggleSpeed,
99
+ );
90
100
 
91
101
  // Blend center-out: centre of chart reveals first, edges last
92
102
  return blendPtsY(
@@ -60,6 +60,8 @@ export function useChartReveal(
60
60
  hasData: SharedValue<boolean>,
61
61
  /** Static charts skip the entry ramp: morphT snaps to its target, no `withTiming`. */
62
62
  isStatic = false,
63
+ /** Reveal/collapse duration (ms). Default {@link CHART_REVEAL_DURATION_MS}; `0` snaps. */
64
+ revealDuration: number = CHART_REVEAL_DURATION_MS,
63
65
  ): ChartRevealState {
64
66
  // Best-guess initial so the common case — a live chart that already has data
65
67
  // and isn't loading — paints fully revealed with no flash. The reaction below
@@ -85,15 +87,18 @@ export function useChartReveal(
85
87
  return;
86
88
  }
87
89
  if (prev !== chartVisible) {
90
+ // 0ms → withTiming resolves on the next frame (effectively a snap), so an
91
+ // explicit `transitions={{ reveal: 0 }}` / `transitions={false}` removes
92
+ // the grow-in without a special-case branch.
88
93
  morphT.set(
89
94
  withTiming(chartVisible ? 1 : 0, {
90
- duration: CHART_REVEAL_DURATION_MS,
95
+ duration: revealDuration,
91
96
  easing: Easing.out(Easing.cubic),
92
97
  }),
93
98
  );
94
99
  }
95
100
  },
96
- [hasData, isStatic],
101
+ [hasData, isStatic, revealDuration],
97
102
  );
98
103
 
99
104
  const isEmpty = useDerivedValue(() => !isLoading.get() && !hasData.get());
@@ -7,7 +7,7 @@ import {
7
7
  type SharedValue,
8
8
  } from "react-native-reanimated";
9
9
 
10
- const MODE_BLEND_DURATION_MS = 300;
10
+ export const MODE_BLEND_DURATION_MS = 300;
11
11
 
12
12
  export interface ModeBlendState {
13
13
  /** 0 = fully line, 1 = fully candle */
@@ -28,17 +28,19 @@ export interface ModeBlendState {
28
28
  export function useModeBlend(
29
29
  isCandle: boolean,
30
30
  lineOpacity: SharedValue<number>,
31
+ /** Crossfade duration (ms). Default {@link MODE_BLEND_DURATION_MS}; `0` snaps. */
32
+ duration: number = MODE_BLEND_DURATION_MS,
31
33
  ): ModeBlendState {
32
34
  const modeBlend = useSharedValue(isCandle ? 1 : 0);
33
35
 
34
36
  useEffect(() => {
35
37
  modeBlend.set(
36
38
  withTiming(isCandle ? 1 : 0, {
37
- duration: MODE_BLEND_DURATION_MS,
39
+ duration,
38
40
  easing: Easing.inOut(Easing.ease),
39
41
  }),
40
42
  );
41
- }, [isCandle, modeBlend]);
43
+ }, [isCandle, modeBlend, duration]);
42
44
 
43
45
  const lineGroupOpacity = useDerivedValue(
44
46
  () => lineOpacity.get() * (1 - modeBlend.get()),
package/src/index.ts CHANGED
@@ -64,6 +64,7 @@ export type {
64
64
  LiveChartPoint,
65
65
  LiveChartProps,
66
66
  LiveChartSeriesProps,
67
+ LoadingConfig,
67
68
  Marker,
68
69
  MarkerClusterConfig,
69
70
  MarkerGroupBadge,
@@ -97,6 +98,7 @@ export type {
97
98
  ThresholdLineConfig,
98
99
  TooltipRenderProps,
99
100
  TradeEvent,
101
+ TransitionConfig,
100
102
  ValueLineConfig,
101
103
  VisibleRange,
102
104
  VolumeConfig,
@@ -4,16 +4,26 @@ import type { ChartPadding } from "../draw/line";
4
4
  * Composite sine squiggly — two overlapping frequencies with a breathing
5
5
  * amplitude envelope. Matches the original web LiveChart loading animation.
6
6
  *
7
- * amplitude = 14 * (0.4 + 0.6 * sin(0.8 * t)) // 5.6 → 22.4px breathing range
8
- * y = centerY + amplitude * (sin(0.035*x + 1.2*t) + 0.45*sin(0.08*x + 2.1*t))
7
+ * amplitude = base * (0.4 + 0.6 * sin(0.8 * t·speed)) // 0.4×→1. base
8
+ * y = centerY + amplitude * (sin(0.035*x + 1.2*t·speed) + 0.45*sin(0.08*x + 2.1*t·speed))
9
+ *
10
+ * `base` (default `14`) scales the wave height; `speed` (default `1`) scales the
11
+ * time phase so the ripple/breathing runs faster or slower (`0` freezes it).
9
12
  */
10
- export function squigglyYAt(x: number, centerY: number, t: number): number {
13
+ export function squigglyYAt(
14
+ x: number,
15
+ centerY: number,
16
+ t: number,
17
+ base = 14,
18
+ speed = 1,
19
+ ): number {
11
20
  "worklet";
12
- const amplitude = 14 * (0.4 + 0.6 * Math.sin(0.8 * t));
21
+ const ts = t * speed;
22
+ const amplitude = base * (0.4 + 0.6 * Math.sin(0.8 * ts));
13
23
  return (
14
24
  centerY +
15
25
  amplitude *
16
- (Math.sin(0.035 * x + 1.2 * t) + 0.45 * Math.sin(0.08 * x + 2.1 * t))
26
+ (Math.sin(0.035 * x + 1.2 * ts) + 0.45 * Math.sin(0.08 * x + 2.1 * ts))
17
27
  );
18
28
  }
19
29
 
@@ -27,6 +37,8 @@ export function buildSquigglyPts(
27
37
  canvasHeight: number,
28
38
  padding: ChartPadding,
29
39
  t: number,
40
+ base = 14,
41
+ speed = 1,
30
42
  ): number[] {
31
43
  "worklet";
32
44
  const leftEdge = padding.left;
@@ -41,7 +53,7 @@ export function buildSquigglyPts(
41
53
  for (let i = 0; i < count; i++) {
42
54
  const x = leftEdge + Math.min(i * step, chartW);
43
55
  pts[i * 2] = x;
44
- pts[i * 2 + 1] = squigglyYAt(x, centerY, t);
56
+ pts[i * 2 + 1] = squigglyYAt(x, centerY, t, base, speed);
45
57
  }
46
58
  return pts;
47
59
  }
@@ -54,13 +66,15 @@ export function squigglifyPts(
54
66
  flatPts: number[],
55
67
  t: number,
56
68
  centerY: number,
69
+ base = 14,
70
+ speed = 1,
57
71
  ): number[] {
58
72
  "worklet";
59
73
  const n = flatPts.length;
60
74
  const out: number[] = new Array(n);
61
75
  for (let i = 0; i < n; i += 2) {
62
76
  out[i] = flatPts[i];
63
- out[i + 1] = squigglyYAt(flatPts[i], centerY, t);
77
+ out[i + 1] = squigglyYAt(flatPts[i], centerY, t, base, speed);
64
78
  }
65
79
  return out;
66
80
  }
package/src/types.ts CHANGED
@@ -720,6 +720,23 @@ export interface ScrubConfig {
720
720
  * Default `0`.
721
721
  */
722
722
  panGestureDelay?: number;
723
+ /**
724
+ * Fade the annotation overlays — buy/sell **markers** and **reference lines**
725
+ * (both the built-in Skia tags/lines and any custom `renderMarker` /
726
+ * `renderReferenceLine` RN views) — out while scrubbing, so they don't clutter
727
+ * the crosshair read-out. Reverses on release.
728
+ *
729
+ * The fade is driven by the **scrub-active** state (not the crosshair's
730
+ * edge-proximity fade, which would resurface the overlays as the crosshair
731
+ * nears the live dot) and eased on the UI thread over `SCRUB_OVERLAY_FADE_MS`.
732
+ * It animates only a **group opacity** — the marker atlas and reference-line
733
+ * geometry are left intact (still one batched draw each), so it's far cheaper
734
+ * than emptying / rebuilding overlay data per scrub. The leading dot is
735
+ * governed separately by `selectionDot`.
736
+ *
737
+ * `false` / omitted keeps the overlays visible while scrubbing (default).
738
+ */
739
+ hideOverlaysOnScrub?: boolean;
723
740
  }
724
741
 
725
742
  /**
@@ -1586,6 +1603,55 @@ export interface VisibleRange {
1586
1603
  following: boolean;
1587
1604
  }
1588
1605
 
1606
+ /**
1607
+ * Per-transition animation durations (ms). Passed as the object form of
1608
+ * {@link LiveChartCoreProps.transitions} to tune or disable the built-in
1609
+ * animations. Omit a field to keep its default; `0` makes that transition
1610
+ * instant (snap). Setting `transitions={false}` is shorthand for all `0`.
1611
+ */
1612
+ export interface TransitionConfig {
1613
+ /**
1614
+ * Reveal / collapse transition — the grow-in when data first appears (and the
1615
+ * fade-out when it goes away or `loading` toggles). This is what plays on a
1616
+ * timeframe change and when a `line` chart's data appears (e.g. switching from
1617
+ * candle to line). Default `600`. `0` = instant.
1618
+ */
1619
+ reveal?: number;
1620
+ /**
1621
+ * Candle ↔ line crossfade duration when `mode` changes. Single-series
1622
+ * `LiveChart` only (multi-series is always lines). Default `300`. `0` = instant.
1623
+ */
1624
+ mode?: number;
1625
+ }
1626
+
1627
+ /**
1628
+ * Styling for the breathing-line loading shell (the {@link LiveChartCoreProps.loading}
1629
+ * state). Every field is optional — pass it as the object form of `loading` to
1630
+ * restyle the shell; omit a field to keep its default.
1631
+ */
1632
+ export interface LoadingConfig {
1633
+ /**
1634
+ * Color of the loading squiggle **and** the skeleton Y-axis placeholders.
1635
+ * Default: the theme's `gridLine` color.
1636
+ */
1637
+ color?: string;
1638
+ /**
1639
+ * Stroke width (px) of the loading squiggle. Default: the chart's line
1640
+ * `strokeWidth`.
1641
+ */
1642
+ strokeWidth?: number;
1643
+ /**
1644
+ * Base wave amplitude (px) of the breathing squiggle — it breathes between
1645
+ * `0.4×` and `1.0×` this. Default `14`.
1646
+ */
1647
+ amplitude?: number;
1648
+ /**
1649
+ * Multiplier on the breathing-wave animation cadence: `>1` ripples faster,
1650
+ * `<1` slower, `0` freezes it. Default `1`.
1651
+ */
1652
+ speed?: number;
1653
+ }
1654
+
1589
1655
  /** Props shared between `LiveChart` and `LiveChartSeries`. */
1590
1656
  export interface LiveChartCoreProps {
1591
1657
  /** Color scheme. Default `"dark"`. */
@@ -1602,11 +1668,27 @@ export interface LiveChartCoreProps {
1602
1668
  timeWindow?: number;
1603
1669
  /** Freeze chart scrolling. Resume catches up to real time. Default `false`. */
1604
1670
  paused?: boolean;
1671
+ /**
1672
+ * Tune or disable the built-in transition animations. `true` / omitted keeps
1673
+ * the defaults; `false` makes them all instant (no grow-in on data
1674
+ * appear/timeframe change, no candle↔line crossfade); a {@link TransitionConfig}
1675
+ * sets per-transition durations in ms (`reveal`, `mode`), where `0` snaps that
1676
+ * one. Live value tracking is governed separately by `smoothing`; static-entry
1677
+ * suppression by `static`.
1678
+ */
1679
+ transitions?: boolean | TransitionConfig;
1605
1680
  /**
1606
1681
  * Breathing-line loading shell. When this becomes `false`, the chart reveals
1607
1682
  * only if there is data (≥2 line points or ≥2 committed candles).
1608
- */
1609
- loading?: boolean;
1683
+ *
1684
+ * `true` shows the shell with the defaults; pass a {@link LoadingConfig} to
1685
+ * restyle it — `color` / `strokeWidth` for the squiggle + skeleton, `amplitude`
1686
+ * / `speed` for the breathing wave. `false` / omitted is "not loading". Toggle
1687
+ * between the config and `false` as data loads (e.g. `loading={isLoading && cfg}`).
1688
+ * The loading→live reveal duration is governed separately by
1689
+ * {@link TransitionConfig.reveal} (the `transitions` prop).
1690
+ */
1691
+ loading?: boolean | LoadingConfig;
1610
1692
  /**
1611
1693
  * Value-lerp speed — how quickly the drawn value, time window, and Y-range chase
1612
1694
  * their targets each frame (0 = frozen, 1 = instant). Equivalent to liveline's