react-native-livechart 4.21.0 → 4.22.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 (78) 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/CustomThresholdBadgeOverlay.d.ts +3 -1
  7. package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -1
  8. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  9. package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
  10. package/dist/components/ExtremaConnectorOverlay.d.ts.map +1 -1
  11. package/dist/components/LiveChart.d.ts.map +1 -1
  12. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  13. package/dist/components/MarkerOverlay.d.ts +4 -2
  14. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  15. package/dist/components/ReferenceLineOverlay.d.ts +1 -1
  16. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  17. package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -1
  18. package/dist/components/ThresholdLineOverlay.d.ts +3 -1
  19. package/dist/components/ThresholdLineOverlay.d.ts.map +1 -1
  20. package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -1
  21. package/dist/core/chartDataPresence.d.ts +8 -0
  22. package/dist/core/chartDataPresence.d.ts.map +1 -0
  23. package/dist/core/liveChartEngineTick.d.ts +3 -0
  24. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  25. package/dist/core/resolveConfig.d.ts +2 -0
  26. package/dist/core/resolveConfig.d.ts.map +1 -1
  27. package/dist/core/useLiveChartEngine.d.ts +4 -0
  28. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  29. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  30. package/dist/draw/markerAtlas.d.ts +3 -2
  31. package/dist/draw/markerAtlas.d.ts.map +1 -1
  32. package/dist/hooks/useChartReveal.d.ts +1 -1
  33. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  34. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  35. package/dist/hooks/useDegen.d.ts.map +1 -1
  36. package/dist/hooks/useLiveChartHasData.d.ts +2 -2
  37. package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
  38. package/dist/hooks/useReferenceLineSeries.d.ts +1 -1
  39. package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -1
  40. package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
  41. package/dist/hooks/useThreshold.d.ts +10 -5
  42. package/dist/hooks/useThreshold.d.ts.map +1 -1
  43. package/dist/math/referenceLineSeries.d.ts +1 -1
  44. package/dist/math/referenceLineSeries.d.ts.map +1 -1
  45. package/dist/math/threshold.d.ts +4 -3
  46. package/dist/math/threshold.d.ts.map +1 -1
  47. package/dist/types.d.ts +23 -8
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/AxisLabelOverlay.tsx +162 -81
  51. package/src/components/CrosshairOverlay.tsx +394 -147
  52. package/src/components/CustomMarkerOverlay.tsx +9 -6
  53. package/src/components/CustomThresholdBadgeOverlay.tsx +11 -4
  54. package/src/components/DegenParticlesOverlay.tsx +34 -23
  55. package/src/components/ExtremaConnectorOverlay.tsx +43 -3
  56. package/src/components/LiveChart.tsx +1709 -1025
  57. package/src/components/LiveChartSeries.tsx +445 -296
  58. package/src/components/MarkerOverlay.tsx +215 -156
  59. package/src/components/ReferenceLineOverlay.tsx +77 -50
  60. package/src/components/ReferenceLineSeriesOverlay.tsx +2 -0
  61. package/src/components/ThresholdLineOverlay.tsx +16 -6
  62. package/src/components/thresholdSplitShaderSource.ts +4 -1
  63. package/src/core/chartDataPresence.ts +24 -0
  64. package/src/core/liveChartEngineTick.ts +4 -0
  65. package/src/core/resolveConfig.ts +3 -0
  66. package/src/core/useLiveChartEngine.ts +16 -3
  67. package/src/core/useLiveChartSeriesEngine.ts +2 -0
  68. package/src/draw/markerAtlas.ts +8 -3
  69. package/src/hooks/useChartReveal.ts +1 -1
  70. package/src/hooks/useCrosshair.ts +69 -46
  71. package/src/hooks/useDegen.ts +82 -53
  72. package/src/hooks/useLiveChartHasData.ts +8 -4
  73. package/src/hooks/useReferenceLineSeries.ts +5 -0
  74. package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
  75. package/src/hooks/useThreshold.ts +41 -15
  76. package/src/math/referenceLineSeries.ts +9 -4
  77. package/src/math/threshold.ts +10 -3
  78. package/src/types.ts +23 -8
@@ -37,6 +37,9 @@ export function buildThresholdSplitShaderSource(sampleCount: number): string {
37
37
  // and glides with the window so features move fluidly, frame to frame.
38
38
  uniform float sampleLeft;
39
39
  uniform float sampleRight;
40
+ // X where the threshold starts (extendToStart: false → the first point's
41
+ // pixel-X, else a large negative sentinel). Fragments left of it use restColor.
42
+ uniform float clipLeft;
40
43
  // X where the threshold ends (extendToNow: false → the last point's pixel-X,
41
44
  // else a huge sentinel). Fragments right of it paint restColor: the plain line
42
45
  // color for the stroke, transparent for the band.
@@ -51,7 +54,7 @@ ${lookup}
51
54
  }
52
55
 
53
56
  half4 main(vec2 xy) {
54
- if (xy.x > clipRight) {
57
+ if (xy.x < clipLeft || xy.x > clipRight) {
55
58
  return half4(half3(restColor.rgb) * restColor.a, restColor.a);
56
59
  }
57
60
  float span = sampleRight - sampleLeft;
@@ -0,0 +1,24 @@
1
+ import type { CandlePoint, LiveChartPoint, SeriesConfig } from "../types";
2
+
3
+ /** A single historical line sample is enough to anchor the live tip. */
4
+ export function hasLineChartData(points: LiveChartPoint[]): boolean {
5
+ "worklet";
6
+ return points.length > 0;
7
+ }
8
+
9
+ /** A single committed OHLC bucket is drawable as a candle. */
10
+ export function hasCandleChartData(
11
+ candles: CandlePoint[] | undefined,
12
+ ): boolean {
13
+ "worklet";
14
+ return (candles?.length ?? 0) > 0;
15
+ }
16
+
17
+ /** Multi-series charts are non-empty when any series has one sample. */
18
+ export function hasMultiSeriesChartData(series: SeriesConfig[]): boolean {
19
+ "worklet";
20
+ for (let i = 0; i < series.length; i++) {
21
+ if (series[i].data.length > 0) return true;
22
+ }
23
+ return false;
24
+ }
@@ -61,6 +61,9 @@ export interface EngineTickInput {
61
61
  * but windowed each tick.
62
62
  */
63
63
  thresholdRangePoints?: LiveChartPoint[];
64
+ /** Whether {@link thresholdRangePoints} extends flat before its first point to
65
+ * the visible window start (`threshold.extendToStart`). Default `true`. */
66
+ thresholdRangeExtendToStart?: boolean;
64
67
  /** Whether {@link thresholdRangePoints} extends flat past its last point to
65
68
  * "now" (`threshold.extendToNow`). Default `true`. */
66
69
  thresholdRangeExtendToNow?: boolean;
@@ -367,6 +370,7 @@ export function tickLiveChartEngineFrame(
367
370
  thrPts,
368
371
  state.timestamp,
369
372
  state.displayWindow,
373
+ input.thresholdRangeExtendToStart ?? true,
370
374
  input.thresholdRangeExtendToNow ?? true,
371
375
  THRESHOLD_RANGE_SCRATCH,
372
376
  );
@@ -447,6 +447,8 @@ export interface ResolvedThresholdConfig {
447
447
  fillOpacity: number;
448
448
  /** Fold the threshold into the Y-axis range fit. */
449
449
  includeInRange: boolean;
450
+ /** Series forms: extend flat before the first point to the visible window's start. */
451
+ extendToStart: boolean;
450
452
  /** Series forms: extend flat past the last point to "now". */
451
453
  extendToNow: boolean;
452
454
  line: ResolvedThresholdLineConfig | null;
@@ -495,6 +497,7 @@ export function resolveThreshold(
495
497
  (typeof prop.fill === "object" ? prop.fill.opacity : undefined) ??
496
498
  THRESHOLD_FILL_OPACITY_DEFAULT,
497
499
  includeInRange: prop.includeInRange ?? false,
500
+ extendToStart: prop.extendToStart ?? true,
498
501
  extendToNow: prop.extendToNow ?? true,
499
502
  line: resolveThresholdLine(prop.line),
500
503
  };
@@ -54,6 +54,9 @@ export interface EngineConfig {
54
54
  * `LiveChartPoint[]` form.
55
55
  */
56
56
  thresholdRangePoints?: SharedValue<LiveChartPoint[]> | LiveChartPoint[];
57
+ /** With {@link thresholdRangePoints}: whether the series extends flat before
58
+ * its first point to the visible window start. Default `true`. */
59
+ thresholdRangeExtendToStart?: boolean;
57
60
  /** With {@link thresholdRangePoints}: whether the series extends flat past its
58
61
  * last point to "now" (`threshold.extendToNow`). Default `true`. */
59
62
  thresholdRangeExtendToNow?: boolean;
@@ -217,6 +220,7 @@ export interface EngineFrameRefs {
217
220
  referenceValues?: SharedValue<number[] | undefined>;
218
221
  /** Series threshold folded into the range fit (`threshold.includeInRange`). */
219
222
  thresholdRangePoints?: SharedValue<LiveChartPoint[] | undefined>;
223
+ thresholdRangeExtendToStart?: SharedValue<boolean>;
220
224
  thresholdRangeExtendToNow?: SharedValue<boolean>;
221
225
  nonNegativeSV?: SharedValue<boolean>;
222
226
  maxValueSV?: SharedValue<number | undefined>;
@@ -353,6 +357,8 @@ export function applyLiveChartEngineFrame(
353
357
  input.referenceValue = sv.referenceValue.value;
354
358
  input.referenceValues = sv.referenceValues?.value;
355
359
  input.thresholdRangePoints = sv.thresholdRangePoints?.value;
360
+ input.thresholdRangeExtendToStart =
361
+ sv.thresholdRangeExtendToStart?.value ?? true;
356
362
  input.thresholdRangeExtendToNow = sv.thresholdRangeExtendToNow?.value ?? true;
357
363
  input.nonNegative = sv.nonNegativeSV?.value ?? false;
358
364
  input.maxValue = sv.maxValueSV?.value;
@@ -453,6 +459,9 @@ export function useLiveChartEngine(
453
459
  const thresholdRangeExtendToNow = useDerivedValue(
454
460
  () => config.thresholdRangeExtendToNow ?? true,
455
461
  );
462
+ const thresholdRangeExtendToStart = useDerivedValue(
463
+ () => config.thresholdRangeExtendToStart ?? true,
464
+ );
456
465
  const nonNegativeSV = useDerivedValue(() => config.nonNegative ?? false);
457
466
  const maxValueSV = useDerivedValue(() => config.maxValue);
458
467
  const nowOverrideSV = useDerivedValue(() => config.nowOverride);
@@ -559,6 +568,7 @@ export function useLiveChartEngine(
559
568
  referenceValue,
560
569
  referenceValues,
561
570
  thresholdRangePoints,
571
+ thresholdRangeExtendToStart,
562
572
  thresholdRangeExtendToNow,
563
573
  nonNegativeSV,
564
574
  maxValueSV,
@@ -586,9 +596,12 @@ export function useLiveChartEngine(
586
596
  extremaMinTime,
587
597
  extremaMaxTime,
588
598
  };
589
- const [frameScratchRef] = useState(() => ({
590
- current: makeEngineFrameScratch(),
591
- }));
599
+ const frameScratchRef = useRef<EngineFrameScratch | null>(null);
600
+ if (frameScratchRef.current === null) {
601
+ // React permits this predictable lazy-ref initialization: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
602
+ // react-doctor-disable-next-line react-doctor/no-ref-current-in-render -- false positive for React's documented lazy-ref exception
603
+ frameScratchRef.current = makeEngineFrameScratch();
604
+ }
592
605
 
593
606
  // `autostart=false` registers the frame callback without running it — the live
594
607
  // loop is fully inert in static mode (the invariant that makes this worth it).
@@ -356,6 +356,8 @@ export function useLiveChartSeriesEngine(
356
356
  // Reused per-frame output buffers (ping-ponged) — see applyLiveChartSeriesEngineFrame.
357
357
  const scratchRef = useRef<MultiSeriesEngineScratch | null>(null);
358
358
  if (scratchRef.current === null) {
359
+ // React permits this predictable lazy-ref initialization: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
360
+ // react-doctor-disable-next-line react-doctor/no-ref-current-in-render -- false positive for React's documented lazy-ref exception
359
361
  scratchRef.current = makeMultiSeriesEngineScratch();
360
362
  }
361
363
 
@@ -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 };
@@ -34,6 +34,7 @@ export function useReferenceLineSeries(
34
34
  engine: ChartEngineLayout,
35
35
  padding: ChartPadding,
36
36
  points: LiveChartPoint[],
37
+ extendToStart: boolean,
37
38
  extendToNow: boolean,
38
39
  ): ReferenceLineSeriesGeometry {
39
40
  const cacheRef = useRef<{
@@ -57,6 +58,7 @@ export function useReferenceLineSeries(
57
58
  engine.canvasWidth.get(),
58
59
  engine.canvasHeight.get(),
59
60
  padding,
61
+ extendToStart,
60
62
  extendToNow,
61
63
  cache.tick ? cache.a : cache.b,
62
64
  );
@@ -86,6 +88,9 @@ export function useReferenceLineSeries(
86
88
  );
87
89
  const currentVisible = useDerivedValue(() => {
88
90
  if (points.length === 0) return false;
91
+ if (!extendToStart && points[0].time > engine.timestamp.get()) {
92
+ return false;
93
+ }
89
94
  if (!extendToNow && points[points.length - 1].time < engine.timestamp.get()) {
90
95
  return false;
91
96
  }
@@ -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());