react-native-livechart 4.3.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.
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/components/LoadingOverlay.d.ts +9 -1
  4. package/dist/components/LoadingOverlay.d.ts.map +1 -1
  5. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  6. package/dist/constants.d.ts +14 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/core/resolveConfig.d.ts +35 -1
  9. package/dist/core/resolveConfig.d.ts.map +1 -1
  10. package/dist/draw/markerAtlas.d.ts +9 -2
  11. package/dist/draw/markerAtlas.d.ts.map +1 -1
  12. package/dist/hooks/useChartPaths.d.ts +5 -1
  13. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  14. package/dist/hooks/useChartReveal.d.ts +3 -1
  15. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  16. package/dist/hooks/useMarkers.d.ts.map +1 -1
  17. package/dist/hooks/useModeBlend.d.ts +4 -1
  18. package/dist/hooks/useModeBlend.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/math/squiggly.d.ts +8 -5
  24. package/dist/math/squiggly.d.ts.map +1 -1
  25. package/dist/types.d.ts +128 -2
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/LiveChart.tsx +126 -42
  29. package/src/components/LiveChartSeries.tsx +95 -26
  30. package/src/components/LoadingOverlay.tsx +25 -6
  31. package/src/components/MarkerOverlay.tsx +135 -37
  32. package/src/constants.ts +17 -0
  33. package/src/core/resolveConfig.ts +67 -0
  34. package/src/draw/markerAtlas.ts +30 -1
  35. package/src/hooks/useChartPaths.ts +11 -1
  36. package/src/hooks/useChartReveal.ts +7 -2
  37. package/src/hooks/useMarkers.ts +2 -0
  38. package/src/hooks/useModeBlend.ts +5 -3
  39. package/src/index.ts +3 -0
  40. package/src/math/markerCluster.ts +8 -1
  41. package/src/math/squiggly.ts +21 -7
  42. package/src/types.ts +131 -2
@@ -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,
@@ -634,6 +697,8 @@ export function resolveMarkerCluster(
634
697
  overlap: clamp01(prop.overlap ?? MARKER_CLUSTER_OVERLAP),
635
698
  gap: MARKER_CLUSTER_GAP,
636
699
  maxBeforeGroup: prop.maxBeforeGroup ?? MARKER_CLUSTER_MAX_BEFORE_GROUP,
700
+ groupBadge: prop.groupBadge ?? "count",
701
+ showGroupCount: prop.showGroupCount ?? false,
637
702
  };
638
703
  }
639
704
  return {
@@ -642,6 +707,8 @@ export function resolveMarkerCluster(
642
707
  overlap: MARKER_CLUSTER_OVERLAP,
643
708
  gap: MARKER_CLUSTER_GAP,
644
709
  maxBeforeGroup: MARKER_CLUSTER_MAX_BEFORE_GROUP,
710
+ groupBadge: "count",
711
+ showGroupCount: false,
645
712
  };
646
713
  }
647
714
 
@@ -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`.
@@ -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());
@@ -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
  /**
@@ -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,8 +64,10 @@ export type {
64
64
  LiveChartPoint,
65
65
  LiveChartProps,
66
66
  LiveChartSeriesProps,
67
+ LoadingConfig,
67
68
  Marker,
68
69
  MarkerClusterConfig,
70
+ MarkerGroupBadge,
69
71
  MarkerKind,
70
72
  MarkerPressEvent,
71
73
  MarkerRenderContext,
@@ -96,6 +98,7 @@ export type {
96
98
  ThresholdLineConfig,
97
99
  TooltipRenderProps,
98
100
  TradeEvent,
101
+ TransitionConfig,
99
102
  ValueLineConfig,
100
103
  VisibleRange,
101
104
  VolumeConfig,
@@ -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 {
@@ -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
  /**
@@ -1133,6 +1150,53 @@ export interface MarkerClusterConfig {
1133
1150
  /** Collapse a co-located run to a single count badge once it exceeds this many.
1134
1151
  * Default `5`. */
1135
1152
  maxBeforeGroup?: number;
1153
+ /**
1154
+ * What a collapsed cluster draws in place of its members:
1155
+ * - `"count"` (default) — the built-in round count badge (a circle with the
1156
+ * member count inside).
1157
+ * - `"marker"` — the **representative marker's own glyph** (its `image`, `icon`
1158
+ * /`pill`, or `kind` shape). The representative is the newest marker in the run,
1159
+ * so a run of buy pills collapses to a buy pill — handy when the group should
1160
+ * look like its members.
1161
+ * - {@link MarkerGroupBadge} — a **dedicated group badge you supply** (your own
1162
+ * Skia `image`, or an `icon`/`pill`), independent of the member markers. Use
1163
+ * this when the collapse should look *different* from the individual markers —
1164
+ * e.g. tiny dots that collapse into a distinct "Buy 5" badge image.
1165
+ *
1166
+ * All three are Skia-drawn in the same `drawAtlas` batch (not a `renderMarker`
1167
+ * RN overlay). Pair any non-`"count"` form with {@link showGroupCount} for a
1168
+ * corner count. Default `"count"`.
1169
+ */
1170
+ groupBadge?: "count" | "marker" | MarkerGroupBadge;
1171
+ /**
1172
+ * When {@link groupBadge} is `"marker"` or a {@link MarkerGroupBadge}, also stamp
1173
+ * the member count as a small badge in the glyph's top-right corner — the
1174
+ * "Buy **5**" look. Ignored when `groupBadge` is `"count"` (the count *is* the
1175
+ * badge). Default `false`.
1176
+ */
1177
+ showGroupCount?: boolean;
1178
+ }
1179
+
1180
+ /**
1181
+ * A dedicated badge drawn for a collapsed marker cluster — your own Skia design,
1182
+ * independent of the member markers. Pass it as
1183
+ * {@link MarkerClusterConfig.groupBadge}. Glyph precedence mirrors a single marker:
1184
+ * `image` → `icon` (`pill`) → a filled dot. Requires `image` or `icon` (otherwise
1185
+ * the group falls back to the count badge).
1186
+ */
1187
+ export interface MarkerGroupBadge {
1188
+ /** Custom Skia image for the collapsed group (e.g. from `useImage`). Takes
1189
+ * precedence over {@link icon}. */
1190
+ image?: SkImage;
1191
+ /** Text / emoji glyph for the collapsed group (used when {@link image} is unset).
1192
+ * Rendered with the chart font. */
1193
+ icon?: string;
1194
+ /** Color of the `icon` (and its `pill`). Defaults to a palette accent. */
1195
+ color?: string;
1196
+ /** Wrap the `icon` in a filled circular badge in {@link color} (icon drawn white). */
1197
+ pill?: boolean;
1198
+ /** Glyph box size in px (icon font size, or image width+height). Default `16`. */
1199
+ size?: number;
1136
1200
  }
1137
1201
 
1138
1202
  /** Context passed to `renderMarker` alongside the marker (cluster / position state). */
@@ -1539,6 +1603,55 @@ export interface VisibleRange {
1539
1603
  following: boolean;
1540
1604
  }
1541
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
+
1542
1655
  /** Props shared between `LiveChart` and `LiveChartSeries`. */
1543
1656
  export interface LiveChartCoreProps {
1544
1657
  /** Color scheme. Default `"dark"`. */
@@ -1555,11 +1668,27 @@ export interface LiveChartCoreProps {
1555
1668
  timeWindow?: number;
1556
1669
  /** Freeze chart scrolling. Resume catches up to real time. Default `false`. */
1557
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;
1558
1680
  /**
1559
1681
  * Breathing-line loading shell. When this becomes `false`, the chart reveals
1560
1682
  * only if there is data (≥2 line points or ≥2 committed candles).
1561
- */
1562
- 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;
1563
1692
  /**
1564
1693
  * Value-lerp speed — how quickly the drawn value, time window, and Y-range chase
1565
1694
  * their targets each frame (0 = frozen, 1 = instant). Equivalent to liveline's