react-native-livechart 4.21.0 → 4.21.1

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 (48) hide show
  1. package/dist/components/AxisLabelOverlay.d.ts +5 -1
  2. package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
  3. package/dist/components/CrosshairOverlay.d.ts +4 -2
  4. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  5. package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
  6. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  7. package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
  8. package/dist/components/ExtremaConnectorOverlay.d.ts.map +1 -1
  9. package/dist/components/LiveChart.d.ts.map +1 -1
  10. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  11. package/dist/components/MarkerOverlay.d.ts +4 -2
  12. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  13. package/dist/components/ReferenceLineOverlay.d.ts +1 -1
  14. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  15. package/dist/core/chartDataPresence.d.ts +8 -0
  16. package/dist/core/chartDataPresence.d.ts.map +1 -0
  17. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  18. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  19. package/dist/draw/markerAtlas.d.ts +3 -2
  20. package/dist/draw/markerAtlas.d.ts.map +1 -1
  21. package/dist/hooks/useChartReveal.d.ts +1 -1
  22. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  23. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  24. package/dist/hooks/useDegen.d.ts.map +1 -1
  25. package/dist/hooks/useLiveChartHasData.d.ts +2 -2
  26. package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
  27. package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
  28. package/dist/types.d.ts +3 -3
  29. package/package.json +1 -1
  30. package/src/components/AxisLabelOverlay.tsx +162 -81
  31. package/src/components/CrosshairOverlay.tsx +394 -147
  32. package/src/components/CustomMarkerOverlay.tsx +9 -6
  33. package/src/components/DegenParticlesOverlay.tsx +34 -23
  34. package/src/components/ExtremaConnectorOverlay.tsx +43 -3
  35. package/src/components/LiveChart.tsx +1692 -1025
  36. package/src/components/LiveChartSeries.tsx +445 -296
  37. package/src/components/MarkerOverlay.tsx +215 -156
  38. package/src/components/ReferenceLineOverlay.tsx +77 -50
  39. package/src/core/chartDataPresence.ts +24 -0
  40. package/src/core/useLiveChartEngine.ts +6 -3
  41. package/src/core/useLiveChartSeriesEngine.ts +2 -0
  42. package/src/draw/markerAtlas.ts +8 -3
  43. package/src/hooks/useChartReveal.ts +1 -1
  44. package/src/hooks/useCrosshair.ts +69 -46
  45. package/src/hooks/useDegen.ts +82 -53
  46. package/src/hooks/useLiveChartHasData.ts +8 -4
  47. package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
  48. package/src/types.ts +3 -3
@@ -17,6 +17,11 @@ import type {
17
17
  MarkerKind,
18
18
  } from "../types";
19
19
 
20
+ export type MarkerAtlasPalette = Pick<
21
+ LiveChartPalette,
22
+ "bgRgb" | "line" | "refLine" | "dotUp" | "refLabel"
23
+ >;
24
+
20
25
  /** Default icon box (px) when `marker.size` is unset. */
21
26
  export const DEFAULT_ICON_SIZE = 16;
22
27
  /** Circular badge padding around the icon glyph + background ring width. */
@@ -78,7 +83,7 @@ export function groupCountTextWidth(
78
83
  * per-frame overlay worklet can resolve a collapsed cluster's badge color. */
79
84
  export function defaultMarkerColor(
80
85
  kind: MarkerKind,
81
- palette: LiveChartPalette,
86
+ palette: MarkerAtlasPalette,
82
87
  ): string {
83
88
  "worklet";
84
89
  switch (kind) {
@@ -188,7 +193,7 @@ interface CellSpec {
188
193
  * Geometry + draw routine for one marker appearance. Mirrors the per-glyph
189
194
  * rendering the old `MarkerGlyph` did inline, so the atlas is pixel-equivalent.
190
195
  */
191
- function cellSpec(m: Marker, palette: LiveChartPalette, font: SkFont): CellSpec {
196
+ function cellSpec(m: Marker, palette: MarkerAtlasPalette, font: SkFont): CellSpec {
192
197
  const color = m.color ?? defaultMarkerColor(m.kind, palette);
193
198
  const m2 = CELL_MARGIN * 2;
194
199
 
@@ -382,7 +387,7 @@ function groupBgCellSpec(
382
387
  */
383
388
  export function buildMarkerAtlas(
384
389
  markers: Marker[],
385
- palette: LiveChartPalette,
390
+ palette: MarkerAtlasPalette,
386
391
  font: SkFont,
387
392
  scale = 1,
388
393
  /** Also bake count-badge cells (digits + per-color backgrounds) for
@@ -39,7 +39,7 @@ export interface ChartRevealState {
39
39
  morphT: SharedValue<number>;
40
40
  /** True while loading=true */
41
41
  isLoading: SharedValue<boolean>;
42
- /** True when not loading and fewer than two samples. */
42
+ /** True when not loading and no samples are available. */
43
43
  isEmpty: SharedValue<boolean>;
44
44
  yAxisOpacity: SharedValue<number>;
45
45
  fillOpacity: SharedValue<number>;
@@ -114,6 +114,34 @@ export interface CrosshairChartOpts {
114
114
  bridgeUnknown?: boolean;
115
115
  }
116
116
 
117
+ function resolveCrosshairSettings(
118
+ chartOpts: CrosshairChartOpts | undefined,
119
+ scrubAction: ResolvedScrubActionConfig | null | undefined,
120
+ onScrubAction: ((point: ScrubActionPoint) => void) | undefined,
121
+ clampToPlot: boolean,
122
+ ) {
123
+ const hasScrubAction = scrubAction != null;
124
+ return {
125
+ hasScrubAction,
126
+ clampPlainScrubToPlot: clampToPlot && !hasScrubAction,
127
+ hasOnScrubAction: onScrubAction != null,
128
+ dismissOnTapOutside: scrubAction?.dismissOnTapOutside ?? false,
129
+ dismissOnAction: scrubAction?.dismissOnAction ?? false,
130
+ snapIncrement: scrubAction?.snap,
131
+ actionIcon: scrubAction?.icon ?? "+",
132
+ actionShowText: scrubAction?.text ?? true,
133
+ hasTimeBadge: scrubAction?.timeBadge ?? false,
134
+ isCandleMode: chartOpts?.mode === "candle",
135
+ candlesSV: chartOpts?.candles,
136
+ liveCandleSV: chartOpts?.liveCandle,
137
+ candleWidthSecs: chartOpts?.candleWidthSecs ?? 60,
138
+ chartGaps: chartOpts?.gaps ?? [],
139
+ bridgeNoTrades: chartOpts?.bridgeNoTrades ?? false,
140
+ bridgeUnavailable: chartOpts?.bridgeUnavailable ?? false,
141
+ bridgeUnknown: chartOpts?.bridgeUnknown ?? false,
142
+ };
143
+ }
144
+
117
145
  /**
118
146
  * Single-series crosshair + scrub. Use `useCrosshairSeries` for `LiveChartSeries`.
119
147
  */
@@ -211,24 +239,30 @@ export function useCrosshair(
211
239
  // screen Y is re-derived from it each frame (see `lockY` below) so the level
212
240
  // tracks the price as the axis rescales instead of drifting under a fixed pixel.
213
241
  const lockPriceValue = useSharedValue<number | null>(null);
214
- const hasScrubAction = scrubAction != null;
215
- const clampPlainScrubToPlot = clampToPlot && !hasScrubAction;
216
- const hasOnScrubAction = onScrubAction != null;
217
- const dismissOnTapOutside = scrubAction?.dismissOnTapOutside ?? false;
218
- const dismissOnAction = scrubAction?.dismissOnAction ?? false;
219
- const snapIncrement = scrubAction?.snap;
220
- const actionIcon = scrubAction?.icon ?? "+";
221
- const actionShowText = scrubAction?.text ?? true;
222
- const hasTimeBadge = scrubAction?.timeBadge ?? false;
223
-
224
- const isCandleMode = chartOpts?.mode === "candle";
225
- const candlesSV = chartOpts?.candles;
226
- const liveCandleSV = chartOpts?.liveCandle;
227
- const candleWidthSecs = chartOpts?.candleWidthSecs ?? 60;
228
- const chartGaps = chartOpts?.gaps ?? [];
229
- const bridgeNoTrades = chartOpts?.bridgeNoTrades ?? false;
230
- const bridgeUnavailable = chartOpts?.bridgeUnavailable ?? false;
231
- const bridgeUnknown = chartOpts?.bridgeUnknown ?? false;
242
+ const {
243
+ hasScrubAction,
244
+ clampPlainScrubToPlot,
245
+ hasOnScrubAction,
246
+ dismissOnTapOutside,
247
+ dismissOnAction,
248
+ snapIncrement,
249
+ actionIcon,
250
+ actionShowText,
251
+ hasTimeBadge,
252
+ isCandleMode,
253
+ candlesSV,
254
+ liveCandleSV,
255
+ candleWidthSecs,
256
+ chartGaps,
257
+ bridgeNoTrades,
258
+ bridgeUnavailable,
259
+ bridgeUnknown,
260
+ } = resolveCrosshairSettings(
261
+ chartOpts,
262
+ scrubAction,
263
+ onScrubAction,
264
+ clampToPlot,
265
+ );
232
266
 
233
267
  // Monospace advance width, measured once per render (not per scrub frame) so
234
268
  // the tooltip layout worklet can size text by character count instead of a
@@ -407,9 +441,7 @@ export function useCrosshair(
407
441
  const canvasWidth = engine.canvasWidth.get();
408
442
  const canvasHeight = engine.canvasHeight.get();
409
443
  const price =
410
- active && rawPrice !== null
411
- ? snapPrice(rawPrice, snapIncrement)
412
- : null;
444
+ active && rawPrice !== null ? snapPrice(rawPrice, snapIncrement) : null;
413
445
  let y = -1;
414
446
  if (
415
447
  rawPrice !== null &&
@@ -586,15 +618,7 @@ export function useCrosshair(
586
618
  string | null,
587
619
  string | null,
588
620
  ];
589
- scheduleOnRN(
590
- handleScrub,
591
- row[2],
592
- row[3],
593
- row[0],
594
- row[1],
595
- row[4],
596
- row[5],
597
- );
621
+ scheduleOnRN(handleScrub, row[2], row[3], row[0], row[1], row[4], row[5]);
598
622
  },
599
623
  );
600
624
 
@@ -667,13 +691,7 @@ export function useCrosshair(
667
691
  .onTouchesUp(
668
692
  /* istanbul ignore next */ (e, manager) => {
669
693
  "worklet";
670
- delayedPanTouchUp(
671
- longPressMs,
672
- e,
673
- manager,
674
- fingerDown,
675
- panActivated,
676
- );
694
+ delayedPanTouchUp(longPressMs, e, manager, fingerDown, panActivated);
677
695
  },
678
696
  )
679
697
  .onTouchesCancelled(
@@ -706,7 +724,12 @@ export function useCrosshair(
706
724
  // stays false and the live crosshair hides itself.
707
725
  if (hasScrubAction && lockActive.get()) {
708
726
  lockX.set(
709
- clampPlotX(e.x, padding.left, engine.canvasWidth.get(), padding.right),
727
+ clampPlotX(
728
+ e.x,
729
+ padding.left,
730
+ engine.canvasWidth.get(),
731
+ padding.right,
732
+ ),
710
733
  );
711
734
  // Freeze the chosen price (value at the pointer Y); the line's Y is
712
735
  // re-derived from it so the level stays put in PRICE as the axis rescales.
@@ -747,7 +770,12 @@ export function useCrosshair(
747
770
  if (!enabled) return;
748
771
  if (hasScrubAction && lockActive.get()) {
749
772
  lockX.set(
750
- clampPlotX(e.x, padding.left, engine.canvasWidth.get(), padding.right),
773
+ clampPlotX(
774
+ e.x,
775
+ padding.left,
776
+ engine.canvasWidth.get(),
777
+ padding.right,
778
+ ),
751
779
  );
752
780
  // Freeze the chosen price (value at the pointer Y); the line's Y is
753
781
  // re-derived from it so the level stays put in PRICE as the axis rescales.
@@ -895,13 +923,8 @@ export function useCrosshair(
895
923
  : undefined;
896
924
  // Keep the bottom axis scroll-only. Do not apply horizontal plot bounds here:
897
925
  // the action badge is intentionally tappable in the right gutter.
898
- const tapHitSlop = resolveScrubHitSlop(
899
- padding,
900
- false,
901
- scrubBottomExclude,
902
- );
903
- if (tapGesture && tapHitSlop)
904
- tapGesture = tapGesture.hitSlop(tapHitSlop);
926
+ const tapHitSlop = resolveScrubHitSlop(padding, false, scrubBottomExclude);
927
+ if (tapGesture && tapHitSlop) tapGesture = tapGesture.hitSlop(tapHitSlop);
905
928
 
906
929
  return {
907
930
  scrubX,
@@ -13,6 +13,32 @@ import type { SingleEngineState } from "../core/useLiveChartEngine";
13
13
  import { computeShake, spawnBurst, tickParticles } from "../math/degenTick";
14
14
  import type { DegenShakePayload, Momentum } from "../types";
15
15
 
16
+ function resolveDegenSettings(
17
+ config: ResolvedDegenConfig | null,
18
+ hasShakeListener: boolean,
19
+ ) {
20
+ return {
21
+ off: config === null,
22
+ scale: config?.scale ?? 1,
23
+ down: config?.downMomentum ?? false,
24
+ shake: config?.shake ?? true,
25
+ shakeIntensity: config?.shakeIntensity ?? 1,
26
+ shakeDurationSec: config?.shakeDurationSec ?? 0.45,
27
+ slotCount: config?.particleSlotCount ?? 60,
28
+ burstParticleCount: config?.burstParticleCount ?? 20,
29
+ particleBurstDurationSec: config?.particleBurstDurationSec ?? 1,
30
+ drag: config?.drag ?? 0.95,
31
+ sizeMin: config?.particleSizeMin ?? 1,
32
+ sizeMax: config?.particleSizeMax ?? 2.2,
33
+ spreadAngle: config?.spreadAngle ?? Math.PI * 1.2,
34
+ jitterX: config?.positionJitterX ?? 24,
35
+ jitterY: config?.positionJitterY ?? 8,
36
+ speedMin: config?.speedMin ?? 60,
37
+ speedMax: config?.speedMax ?? 160,
38
+ hasShakeListener,
39
+ };
40
+ }
41
+
16
42
  export function useDegen(
17
43
  engine: SingleEngineState,
18
44
  dotX: SharedValue<number>,
@@ -65,23 +91,26 @@ export function useDegen(
65
91
  onShakeRef.current?.({ direction });
66
92
  };
67
93
 
68
- const degenOff = cfg === null;
69
- const resolvedScale = cfg?.scale ?? 1;
70
- const resolvedDown = cfg?.downMomentum ?? false;
71
- const resolvedShake = cfg?.shake ?? true;
72
- const resolvedShakeIntensity = cfg?.shakeIntensity ?? 1;
73
- const resolvedShakeDurationSec = cfg?.shakeDurationSec ?? 0.45;
74
- const resolvedSlotCount = cfg?.particleSlotCount ?? 60;
75
- const resolvedBurstParticleCount = cfg?.burstParticleCount ?? 20;
76
- const resolvedParticleBurstDurationSec = cfg?.particleBurstDurationSec ?? 1.0;
77
- const resolvedDrag = cfg?.drag ?? 0.95;
78
- const resolvedSizeMin = cfg?.particleSizeMin ?? 1;
79
- const resolvedSizeMax = cfg?.particleSizeMax ?? 2.2;
80
- const resolvedSpreadAngle = cfg?.spreadAngle ?? Math.PI * 1.2;
81
- const resolvedJitterX = cfg?.positionJitterX ?? 24;
82
- const resolvedJitterY = cfg?.positionJitterY ?? 8;
83
- const resolvedSpeedMin = cfg?.speedMin ?? 60;
84
- const resolvedSpeedMax = cfg?.speedMax ?? 160;
94
+ const {
95
+ off,
96
+ scale,
97
+ down,
98
+ shake,
99
+ shakeIntensity,
100
+ shakeDurationSec,
101
+ slotCount,
102
+ burstParticleCount,
103
+ particleBurstDurationSec,
104
+ drag,
105
+ sizeMin,
106
+ sizeMax,
107
+ spreadAngle,
108
+ jitterX,
109
+ jitterY,
110
+ speedMin,
111
+ speedMax,
112
+ hasShakeListener,
113
+ } = resolveDegenSettings(cfg, onShake != null);
85
114
 
86
115
  useEffect(() => {
87
116
  // Mirror the resolved config into SharedValues for the UI-thread worklet.
@@ -90,43 +119,43 @@ export function useDegen(
90
119
  // is off, enabledSV is 0 and the frame worklet early-returns (zeroing the
91
120
  // particle buffer and shake itself), so the remaining values are inert; the
92
121
  // resolved* defaults already stand in for the null-config case.
93
- enabledSV.set(degenOff ? 0 : 1);
94
- shakeEnabledSV.set(!degenOff && resolvedShake ? 1 : 0);
95
- hasOnShakeListenerSV.set(!degenOff && onShake != null ? 1 : 0);
96
- scaleSV.set(resolvedScale);
97
- downSV.set(resolvedDown ? 1 : 0);
98
- shakeIntensitySV.set(resolvedShakeIntensity);
99
- shakeDurationSecSV.set(resolvedShakeDurationSec);
100
- slotCountSV.set(resolvedSlotCount);
101
- burstParticleCountSV.set(resolvedBurstParticleCount);
102
- particleBurstDurationSecSV.set(resolvedParticleBurstDurationSec);
103
- dragSV.set(resolvedDrag);
104
- sizeMinSV.set(resolvedSizeMin);
105
- sizeMaxSV.set(resolvedSizeMax);
106
- spreadAngleSV.set(resolvedSpreadAngle);
107
- jitterXSV.set(resolvedJitterX);
108
- jitterYSV.set(resolvedJitterY);
109
- speedMinSV.set(resolvedSpeedMin);
110
- speedMaxSV.set(resolvedSpeedMax);
122
+ enabledSV.set(off ? 0 : 1);
123
+ shakeEnabledSV.set(!off && shake ? 1 : 0);
124
+ hasOnShakeListenerSV.set(!off && hasShakeListener ? 1 : 0);
125
+ scaleSV.set(scale);
126
+ downSV.set(down ? 1 : 0);
127
+ shakeIntensitySV.set(shakeIntensity);
128
+ shakeDurationSecSV.set(shakeDurationSec);
129
+ slotCountSV.set(slotCount);
130
+ burstParticleCountSV.set(burstParticleCount);
131
+ particleBurstDurationSecSV.set(particleBurstDurationSec);
132
+ dragSV.set(drag);
133
+ sizeMinSV.set(sizeMin);
134
+ sizeMaxSV.set(sizeMax);
135
+ spreadAngleSV.set(spreadAngle);
136
+ jitterXSV.set(jitterX);
137
+ jitterYSV.set(jitterY);
138
+ speedMinSV.set(speedMin);
139
+ speedMaxSV.set(speedMax);
111
140
  }, [
112
- degenOff,
113
- onShake,
114
- resolvedScale,
115
- resolvedDown,
116
- resolvedShake,
117
- resolvedShakeIntensity,
118
- resolvedShakeDurationSec,
119
- resolvedSlotCount,
120
- resolvedBurstParticleCount,
121
- resolvedParticleBurstDurationSec,
122
- resolvedDrag,
123
- resolvedSizeMin,
124
- resolvedSizeMax,
125
- resolvedSpreadAngle,
126
- resolvedJitterX,
127
- resolvedJitterY,
128
- resolvedSpeedMin,
129
- resolvedSpeedMax,
141
+ off,
142
+ scale,
143
+ down,
144
+ shake,
145
+ shakeIntensity,
146
+ shakeDurationSec,
147
+ slotCount,
148
+ burstParticleCount,
149
+ particleBurstDurationSec,
150
+ drag,
151
+ sizeMin,
152
+ sizeMax,
153
+ spreadAngle,
154
+ jitterX,
155
+ jitterY,
156
+ speedMin,
157
+ speedMax,
158
+ hasShakeListener,
130
159
  // SharedValue refs are stable (useSharedValue), so listing them satisfies
131
160
  // exhaustive-deps without ever re-running the effect.
132
161
  enabledSV,
@@ -1,9 +1,13 @@
1
1
  import { useDerivedValue, type SharedValue } from "react-native-reanimated";
2
+ import {
3
+ hasCandleChartData,
4
+ hasLineChartData,
5
+ } from "../core/chartDataPresence";
2
6
  import type { CandlePoint, LiveChartPoint } from "../types";
3
7
 
4
8
  /**
5
- * Data presence for {@link LiveChart}: line mode needs ≥2 points;
6
- * candle mode needs ≥2 committed candles (`liveCandle` alone does not count).
9
+ * Data presence for {@link LiveChart}: line mode needs ≥1 point;
10
+ * candle mode needs ≥1 committed candle (`liveCandle` alone does not count).
7
11
  *
8
12
  * {@link useChartReveal} seeds its first-paint state off `hasData` directly (in a
9
13
  * layout effect), so no JS-thread snapshot is read during render here.
@@ -22,9 +26,9 @@ export function useLiveChartHasData({
22
26
  const hasData = useDerivedValue(() => {
23
27
  "worklet";
24
28
  if (isCandle) {
25
- return (candles?.value.length ?? 0) >= 2;
29
+ return hasCandleChartData(candles?.value);
26
30
  }
27
- return data.value.length >= 2;
31
+ return hasLineChartData(data.value);
28
32
  });
29
33
 
30
34
  return { hasData };
@@ -3,6 +3,11 @@ import {
3
3
  useSharedValue,
4
4
  type SharedValue,
5
5
  } from "react-native-reanimated";
6
+ import {
7
+ hasCandleChartData,
8
+ hasLineChartData,
9
+ hasMultiSeriesChartData,
10
+ } from "../core/chartDataPresence";
6
11
  import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
7
12
 
8
13
  const STASH_MORPH_EPS = 0.01;
@@ -42,20 +47,19 @@ export function useSingleChartReverseMorphInputs({
42
47
  has: hasData.get(),
43
48
  m: morphT.get(),
44
49
  d: data.get(),
45
- cLen: candles?.get().length ?? 0,
46
50
  c: candles?.get(),
47
51
  lc: liveCandle?.get() ?? null,
48
52
  }),
49
53
  (curr) => {
50
54
  "worklet";
51
55
  if (!curr.candle) {
52
- if (curr.has && curr.d.length >= 2) {
56
+ if (curr.has && hasLineChartData(curr.d)) {
53
57
  lineStash.set(curr.d.slice());
54
58
  lineEngineData.set(curr.d);
55
59
  } else if (
56
60
  !curr.has &&
57
61
  curr.m > STASH_MORPH_EPS &&
58
- lineStash.get().length >= 2
62
+ hasLineChartData(lineStash.get())
59
63
  ) {
60
64
  lineEngineData.set(lineStash.get());
61
65
  } else {
@@ -65,14 +69,14 @@ export function useSingleChartReverseMorphInputs({
65
69
  }
66
70
 
67
71
  const cArr = curr.c;
68
- if (curr.has && curr.cLen >= 2 && cArr) {
72
+ if (curr.has && hasCandleChartData(cArr) && cArr) {
69
73
  candleStash.set(cArr.slice());
70
74
  candlesEngine.set(cArr);
71
75
  liveEngine.set(curr.lc);
72
76
  } else if (
73
77
  !curr.has &&
74
78
  curr.m > STASH_MORPH_EPS &&
75
- candleStash.get().length >= 2
79
+ hasCandleChartData(candleStash.get())
76
80
  ) {
77
81
  candlesEngine.set(candleStash.get());
78
82
  liveEngine.set(null);
@@ -118,27 +122,14 @@ export function useMultiSeriesReverseMorphInputs({
118
122
  (curr) => {
119
123
  "worklet";
120
124
  const live = curr.live;
121
-
122
- let anyReady = false;
123
- for (let i = 0; i < live.length; i++) {
124
- if (live[i].data.length >= 2) {
125
- anyReady = true;
126
- break;
127
- }
128
- }
125
+ const anyReady = hasMultiSeriesChartData(live);
129
126
 
130
127
  if (curr.has && anyReady) {
131
128
  seriesStash.set(snapshotSeriesWorklet(live));
132
129
  effectiveSeries.set(live);
133
130
  } else {
134
- let stashReady = false;
135
131
  const stash = seriesStash.get();
136
- for (let i = 0; i < stash.length; i++) {
137
- if (stash[i].data.length >= 2) {
138
- stashReady = true;
139
- break;
140
- }
141
- }
132
+ const stashReady = hasMultiSeriesChartData(stash);
142
133
 
143
134
  if (!curr.has && curr.m > STASH_MORPH_EPS && stashReady) {
144
135
  effectiveSeries.set(seriesStash.get());
package/src/types.ts CHANGED
@@ -2261,7 +2261,7 @@ export interface LiveChartCoreProps {
2261
2261
  transitions?: boolean | TransitionConfig;
2262
2262
  /**
2263
2263
  * Breathing-line loading shell. When this becomes `false`, the chart reveals
2264
- * only if there is data (≥2 line points or ≥2 committed candles).
2264
+ * only if there is data (≥1 line point or ≥1 committed candle).
2265
2265
  *
2266
2266
  * `true` shows the shell with the defaults; pass a {@link LoadingConfig} to
2267
2267
  * restyle it — `color` / `strokeWidth` for the squiggle + skeleton, `amplitude`
@@ -2330,8 +2330,8 @@ export interface LiveChartCoreProps {
2330
2330
  */
2331
2331
  yRangeScale?: SharedValue<number>;
2332
2332
  /**
2333
- * Label in the empty state when `loading` is false and there are fewer than
2334
- * two samples (line points or committed candles). Default `"No data"`.
2333
+ * Label in the empty state when `loading` is false and there are no line
2334
+ * points or committed candles. Default `"No data"`.
2335
2335
  */
2336
2336
  emptyText?: string;
2337
2337
  /** Custom formatter for value labels (axes, badge, tooltips). Default `v => v.toFixed(2)`. */