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.
@@ -1,10 +1,12 @@
1
1
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
- import { View } from "react-native";
2
+ import { StyleSheet, View } from "react-native";
3
3
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
- import {
4
+ import Animated, {
5
5
  useAnimatedReaction,
6
+ useAnimatedStyle,
6
7
  useDerivedValue,
7
8
  useSharedValue,
9
+ withTiming,
8
10
  } from "react-native-reanimated";
9
11
  import { scheduleOnRN } from "react-native-worklets";
10
12
 
@@ -22,7 +24,11 @@ import {
22
24
  vec,
23
25
  } from "@shopify/react-native-skia";
24
26
 
25
- import { DEFAULT_ACCENT_COLOR, HOLD_TO_SCRUB_MS } from "../constants";
27
+ import {
28
+ DEFAULT_ACCENT_COLOR,
29
+ HOLD_TO_SCRUB_MS,
30
+ SCRUB_OVERLAY_FADE_MS,
31
+ } from "../constants";
26
32
  import {
27
33
  resolveAreaDots,
28
34
  resolveAxisLabel,
@@ -32,11 +38,13 @@ import {
32
38
  resolveGradient,
33
39
  resolveGridStyle,
34
40
  resolveLeftEdgeFade,
41
+ resolveLoading,
35
42
  resolveMarkerCluster,
36
43
  resolveMetrics,
37
44
  resolvePulse,
38
45
  resolveScrub,
39
46
  resolveScrubAction,
47
+ resolveTransitions,
40
48
  resolveReturnToLiveMs,
41
49
  resolveSelectionDot,
42
50
  resolveThreshold,
@@ -208,6 +216,7 @@ function useLiveChartController({
208
216
  timeWindow = 30,
209
217
  paused = false,
210
218
  loading = false,
219
+ transitions,
211
220
  // `static` is a reserved word — alias it so the destructure parses.
212
221
  static: isStatic = false,
213
222
  smoothing = 0.08,
@@ -535,7 +544,17 @@ function useLiveChartController({
535
544
  candles,
536
545
  });
537
546
 
538
- const reveal = useChartReveal(loading, hasData, isStatic);
547
+ // Resolve the loading shell: null = not loading, else the styled config (a
548
+ // non-null result is the "is loading" flag and carries the look).
549
+ const loadingCfg = resolveLoading(loading);
550
+ const loadingActive = loadingCfg !== null;
551
+ const transitionsCfg = resolveTransitions(transitions);
552
+ const reveal = useChartReveal(
553
+ loadingActive,
554
+ hasData,
555
+ isStatic,
556
+ transitionsCfg.reveal,
557
+ );
539
558
 
540
559
  // After data clears, keep last snapshot until morphT finishes dropping (web parity).
541
560
  const { lineEngineData, candlesEngine, liveEngine } =
@@ -594,6 +613,7 @@ function useLiveChartController({
594
613
  const { lineGroupOpacity, candleGroupOpacity } = useModeBlend(
595
614
  isCandle,
596
615
  reveal.lineOpacity,
616
+ transitionsCfg.mode,
597
617
  );
598
618
 
599
619
  // ── Per-frame derived values ───────────────────────────────────────────
@@ -662,6 +682,9 @@ function useLiveChartController({
662
682
  // live value when `followViewEdge` tracks the scrolled-back window.
663
683
  engine.edgeValue,
664
684
  badgeCfg?.followViewEdge ?? false,
685
+ // Match the standalone loading squiggle's wave during the reveal morph.
686
+ loadingCfg?.amplitude,
687
+ loadingCfg?.speed,
665
688
  );
666
689
 
667
690
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -1027,6 +1050,25 @@ function useLiveChartController({
1027
1050
  (selectionDotDuringScrub && crosshair.scrubActive.value ? 0 : 1),
1028
1051
  );
1029
1052
 
1053
+ // Fade the annotation overlays (markers + reference lines) out while scrubbing
1054
+ // when `scrub.hideOverlays` is set. Eased off the scrub-ACTIVE flag — not the
1055
+ // crosshair's edge-proximity fade, which drops to 0 near the live dot and would
1056
+ // resurface the overlays mid-scrub. Only this group opacity animates; the
1057
+ // marker atlas / reference-line draws stay intact (one batched draw each).
1058
+ const fadeOverlaysOnScrub =
1059
+ !isStatic && scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
1060
+ const overlayScrubFade = useDerivedValue(() =>
1061
+ fadeOverlaysOnScrub
1062
+ ? withTiming(crosshair.scrubActive.get() ? 0 : 1, {
1063
+ duration: SCRUB_OVERLAY_FADE_MS,
1064
+ })
1065
+ : 1,
1066
+ );
1067
+ // Markers already fade with the dot reveal; fold the scrub-hide fade in too.
1068
+ const markerGroupOpacity = useDerivedValue(
1069
+ () => reveal.dotOpacity.get() * overlayScrubFade.get(),
1070
+ );
1071
+
1030
1072
  return {
1031
1073
  // passthrough props the render needs
1032
1074
  style,
@@ -1090,6 +1132,11 @@ function useLiveChartController({
1090
1132
  // engine + reveal
1091
1133
  engine,
1092
1134
  reveal,
1135
+ // loading shell styling (null → not loading)
1136
+ loadingLineColor: loadingCfg?.color,
1137
+ loadingStrokeWidth: loadingCfg?.strokeWidth,
1138
+ loadingAmplitude: loadingCfg?.amplitude,
1139
+ loadingSpeed: loadingCfg?.speed,
1093
1140
  // derived render values
1094
1141
  backgroundColor,
1095
1142
  gradientEnd,
@@ -1121,6 +1168,8 @@ function useLiveChartController({
1121
1168
  dotX,
1122
1169
  dotY,
1123
1170
  liveDotOpacity,
1171
+ overlayScrubFade,
1172
+ markerGroupOpacity,
1124
1173
  momentumSV,
1125
1174
  tradeMarkers,
1126
1175
  degenPack,
@@ -1309,6 +1358,8 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1309
1358
  markersActive,
1310
1359
  markersSV,
1311
1360
  markerClusterCfg,
1361
+ markerGroupOpacity,
1362
+ overlayScrubFade,
1312
1363
  renderMarker,
1313
1364
  emptyText,
1314
1365
  metricsCfg,
@@ -1318,6 +1369,10 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1318
1369
  yAxisEntries,
1319
1370
  badgeUsesRightGutter,
1320
1371
  gridStyleCfg,
1372
+ loadingLineColor,
1373
+ loadingStrokeWidth,
1374
+ loadingAmplitude,
1375
+ loadingSpeed,
1321
1376
  } = model;
1322
1377
 
1323
1378
  return (
@@ -1350,21 +1405,24 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1350
1405
 
1351
1406
  {/* Index keys: reference lines are a positional array and two may share
1352
1407
  value + label (e.g. duplicate working orders at the same price), which a
1353
- content-derived key would collapse to one. */}
1354
- {allRefLines.map((rl, i) => (
1355
- <ReferenceLineOverlay
1356
- key={i}
1357
- engine={engine}
1358
- padding={effectivePadding}
1359
- line={rl}
1360
- palette={palette}
1361
- formatValue={formatValue}
1362
- font={skiaFont}
1363
- fontProp={fontProp}
1364
- dragValues={dragValues}
1365
- index={i}
1366
- />
1367
- ))}
1408
+ content-derived key would collapse to one. Wrapped in a fade group so
1409
+ `scrub.hideOverlaysOnScrub` can ease the lines out while scrubbing. */}
1410
+ <Group opacity={overlayScrubFade}>
1411
+ {allRefLines.map((rl, i) => (
1412
+ <ReferenceLineOverlay
1413
+ key={i}
1414
+ engine={engine}
1415
+ padding={effectivePadding}
1416
+ line={rl}
1417
+ palette={palette}
1418
+ formatValue={formatValue}
1419
+ font={skiaFont}
1420
+ fontProp={fontProp}
1421
+ dragValues={dragValues}
1422
+ index={i}
1423
+ />
1424
+ ))}
1425
+ </Group>
1368
1426
 
1369
1427
  {/* Threshold marker line + label (behind the chart line). */}
1370
1428
  {thresholdCfg?.line && (
@@ -1523,7 +1581,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1523
1581
  )}
1524
1582
 
1525
1583
  {markersActive && (
1526
- <Group opacity={reveal.dotOpacity}>
1584
+ <Group opacity={markerGroupOpacity}>
1527
1585
  <MarkerOverlay
1528
1586
  markers={markersSV}
1529
1587
  engine={engine}
@@ -1570,6 +1628,10 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1570
1628
  badgeTail={badgeCfg?.tail ?? true}
1571
1629
  badgeMetrics={metricsCfg.badge}
1572
1630
  emptyMetrics={metricsCfg.emptyState}
1631
+ lineColor={loadingLineColor}
1632
+ lineStrokeWidth={loadingStrokeWidth}
1633
+ waveAmplitude={loadingAmplitude}
1634
+ waveSpeed={loadingSpeed}
1573
1635
  />
1574
1636
  </Group>
1575
1637
  );
@@ -1747,10 +1809,11 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
1747
1809
  skiaFont,
1748
1810
  fontProp,
1749
1811
  degenShakeTransform,
1812
+ overlayScrubFade,
1750
1813
  } = model;
1751
1814
  if (allRefLines.length === 0) return null;
1752
1815
  return (
1753
- <Group transform={degenShakeTransform}>
1816
+ <Group transform={degenShakeTransform} opacity={overlayScrubFade}>
1754
1817
  {allRefLines.map((rl, i) => (
1755
1818
  <ReferenceLineOverlay
1756
1819
  key={i}
@@ -1853,8 +1916,15 @@ export function LiveChart(props: LiveChartProps) {
1853
1916
  topConnector,
1854
1917
  bottomConnector,
1855
1918
  lineIsLinear,
1919
+ overlayScrubFade,
1856
1920
  } = model;
1857
1921
 
1922
+ // Mirror the Skia overlay fade onto the RN custom-overlay siblings (custom
1923
+ // markers / reference-line tags) so `scrub.hideOverlaysOnScrub` hides them too.
1924
+ const overlayFadeStyle = useAnimatedStyle(() => ({
1925
+ opacity: overlayScrubFade.get(),
1926
+ }));
1927
+
1858
1928
  return (
1859
1929
  <GestureDetector gesture={rootGesture}>
1860
1930
  <View
@@ -1921,33 +1991,47 @@ export function LiveChart(props: LiveChartProps) {
1921
1991
  />
1922
1992
 
1923
1993
  {/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
1924
- pinned to each marker's live position. Sibling of <Canvas>. */}
1994
+ pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
1995
+ a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
1996
+ Skia markers (the wrapper is full-bleed; children keep their own
1997
+ absolute positions). */}
1925
1998
  {markersActive && renderMarker && (
1926
- <CustomMarkerOverlay
1927
- markers={markersSV}
1928
- renderMarker={renderMarker}
1929
- engine={engine}
1930
- padding={effectivePadding}
1931
- lineData={engine.data}
1932
- lineLinear={lineIsLinear}
1933
- cluster={markerClusterCfg}
1934
- />
1999
+ <Animated.View
2000
+ pointerEvents="box-none"
2001
+ style={[StyleSheet.absoluteFill, overlayFadeStyle]}
2002
+ >
2003
+ <CustomMarkerOverlay
2004
+ markers={markersSV}
2005
+ renderMarker={renderMarker}
2006
+ engine={engine}
2007
+ padding={effectivePadding}
2008
+ lineData={engine.data}
2009
+ lineLinear={lineIsLinear}
2010
+ cluster={markerClusterCfg}
2011
+ />
2012
+ </Animated.View>
1935
2013
  )}
1936
2014
 
1937
2015
  {/* Custom-rendered reference-line tags — RN views floated over the canvas
1938
2016
  (non-Skia), pinned to each Form-A line's value. Sibling of <Canvas>.
1939
- Built-in Skia tags for these lines are suppressed (no double-draw). */}
2017
+ Built-in Skia tags for these lines are suppressed (no double-draw).
2018
+ Same box-none fade wrapper as the custom markers above. */}
1940
2019
  {renderReferenceLine && allRefLines.length > 0 && (
1941
- <CustomReferenceLineOverlay
1942
- lines={allRefLines}
1943
- renderReferenceLine={renderReferenceLine}
1944
- custom={refLineCustom}
1945
- engine={engine}
1946
- padding={effectivePadding}
1947
- formatValue={formatValue}
1948
- dragValues={dragValues}
1949
- dragActive={dragActive}
1950
- />
2020
+ <Animated.View
2021
+ pointerEvents="box-none"
2022
+ style={[StyleSheet.absoluteFill, overlayFadeStyle]}
2023
+ >
2024
+ <CustomReferenceLineOverlay
2025
+ lines={allRefLines}
2026
+ renderReferenceLine={renderReferenceLine}
2027
+ custom={refLineCustom}
2028
+ engine={engine}
2029
+ padding={effectivePadding}
2030
+ formatValue={formatValue}
2031
+ dragValues={dragValues}
2032
+ dragActive={dragActive}
2033
+ />
2034
+ </Animated.View>
1951
2035
  )}
1952
2036
 
1953
2037
  {/* Custom scrub tooltip — an RN view floated over the canvas (non-Skia),
@@ -6,12 +6,14 @@
6
6
  */
7
7
  import { Canvas, Group } from "@shopify/react-native-skia";
8
8
  import { useLayoutEffect, useState } from "react";
9
- import { View } from "react-native";
9
+ import { StyleSheet, View } from "react-native";
10
10
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
11
- import {
11
+ import Animated, {
12
12
  useAnimatedReaction,
13
+ useAnimatedStyle,
13
14
  useDerivedValue,
14
15
  useSharedValue,
16
+ withTiming,
15
17
  type SharedValue,
16
18
  } from "react-native-reanimated";
17
19
  import { scheduleOnRN } from "react-native-worklets";
@@ -19,6 +21,7 @@ import {
19
21
  DEFAULT_ACCENT_COLOR,
20
22
  HOLD_TO_SCRUB_MS,
21
23
  MAX_MULTI_SERIES,
24
+ SCRUB_OVERLAY_FADE_MS,
22
25
  } from "../constants";
23
26
  import {
24
27
  lineColorsSignatureFromArray,
@@ -33,12 +36,14 @@ import {
33
36
  resolveGridStyle,
34
37
  resolveLeftEdgeFade,
35
38
  resolveLegend,
39
+ resolveLoading,
36
40
  resolveMarkerCluster,
37
41
  resolveMetrics,
38
42
  resolveMultiSeriesDot,
39
43
  resolveReturnToLiveMs,
40
44
  resolveScrub,
41
45
  resolveSelectionDot,
46
+ resolveTransitions,
42
47
  resolveXAxis,
43
48
  resolveYAxis,
44
49
  resolveZoom,
@@ -126,6 +131,7 @@ function useLiveChartSeriesController({
126
131
  timeWindow = 30,
127
132
  paused = false,
128
133
  loading = false,
134
+ transitions,
129
135
  smoothing = 0.08,
130
136
  exaggerate = false,
131
137
  nonNegative = false,
@@ -289,7 +295,18 @@ function useLiveChartSeriesController({
289
295
  return false;
290
296
  });
291
297
 
292
- const reveal = useChartReveal(loading, hasData);
298
+ // Resolve the loading shell: null = not loading, else the styled config.
299
+ const loadingCfg = resolveLoading(loading);
300
+ const loadingActive = loadingCfg !== null;
301
+ // Multi-series is always lines, so only the reveal transition applies (no
302
+ // candle↔line crossfade); `transitions.mode` is accepted but inert here.
303
+ const transitionsCfg = resolveTransitions(transitions);
304
+ const reveal = useChartReveal(
305
+ loadingActive,
306
+ hasData,
307
+ false,
308
+ transitionsCfg.reveal,
309
+ );
293
310
 
294
311
  const effectiveSeries = useMultiSeriesReverseMorphInputs({
295
312
  series,
@@ -453,6 +470,23 @@ function useLiveChartSeriesController({
453
470
 
454
471
  const backgroundColor = `rgb(${palette.bgRgb[0]}, ${palette.bgRgb[1]}, ${palette.bgRgb[2]})`;
455
472
 
473
+ // Fade markers + reference lines out while scrubbing when
474
+ // `scrub.hideOverlaysOnScrub` is set. Eased off the scrub-ACTIVE flag (not the
475
+ // crosshair edge fade, which would resurface them near the live dot); only a
476
+ // group opacity animates — the overlay draws stay intact. See `LiveChart`.
477
+ const fadeOverlaysOnScrub =
478
+ scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
479
+ const overlayScrubFade = useDerivedValue(() =>
480
+ fadeOverlaysOnScrub
481
+ ? withTiming(crosshair.scrubActive.get() ? 0 : 1, {
482
+ duration: SCRUB_OVERLAY_FADE_MS,
483
+ })
484
+ : 1,
485
+ );
486
+ const markerGroupOpacity = useDerivedValue(
487
+ () => reveal.dotOpacity.get() * overlayScrubFade.get(),
488
+ );
489
+
456
490
  return {
457
491
  // passthrough props the render needs
458
492
  series,
@@ -484,6 +518,11 @@ function useLiveChartSeriesController({
484
518
  // engine + reveal
485
519
  engine,
486
520
  reveal,
521
+ // loading shell styling (null → not loading)
522
+ loadingLineColor: loadingCfg?.color,
523
+ loadingStrokeWidth: loadingCfg?.strokeWidth,
524
+ loadingAmplitude: loadingCfg?.amplitude,
525
+ loadingSpeed: loadingCfg?.speed,
487
526
  effectiveSeries,
488
527
  layoutHeight,
489
528
  onLayout,
@@ -500,6 +539,8 @@ function useLiveChartSeriesController({
500
539
  markersActive,
501
540
  markersSV,
502
541
  markerClusterCfg,
542
+ markerGroupOpacity,
543
+ overlayScrubFade,
503
544
  renderMarker,
504
545
  // selection dot: resolved config + fallback color (the leading series' color)
505
546
  selectionDot: selectionDotCfg,
@@ -545,10 +586,16 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
545
586
  markersActive,
546
587
  markersSV,
547
588
  markerClusterCfg,
589
+ markerGroupOpacity,
590
+ overlayScrubFade,
548
591
  renderMarker,
549
592
  series,
550
593
  emptyText,
551
594
  metricsCfg,
595
+ loadingLineColor,
596
+ loadingStrokeWidth,
597
+ loadingAmplitude,
598
+ loadingSpeed,
552
599
  } = model;
553
600
 
554
601
  return (
@@ -570,18 +617,21 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
570
617
 
571
618
  {/* Index keys: reference lines are a positional array and two may share
572
619
  value + label (e.g. duplicate working orders at the same price), which a
573
- content-derived key would collapse to one. */}
574
- {allRefLines.map((rl, i) => (
575
- <ReferenceLineOverlay
576
- key={i}
577
- engine={engine}
578
- padding={effectivePadding}
579
- line={rl}
580
- palette={palette}
581
- formatValue={formatValue}
582
- font={skiaFont}
583
- />
584
- ))}
620
+ content-derived key would collapse to one. Fade group lets
621
+ `scrub.hideOverlaysOnScrub` ease the lines out while scrubbing. */}
622
+ <Group opacity={overlayScrubFade}>
623
+ {allRefLines.map((rl, i) => (
624
+ <ReferenceLineOverlay
625
+ key={i}
626
+ engine={engine}
627
+ padding={effectivePadding}
628
+ line={rl}
629
+ palette={palette}
630
+ formatValue={formatValue}
631
+ font={skiaFont}
632
+ />
633
+ ))}
634
+ </Group>
585
635
 
586
636
  {dotCfg.valueLine && (
587
637
  <Group opacity={reveal.lineOpacity}>
@@ -654,7 +704,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
654
704
  )}
655
705
 
656
706
  {markersActive && (
657
- <Group opacity={reveal.dotOpacity}>
707
+ <Group opacity={markerGroupOpacity}>
658
708
  <MarkerOverlay
659
709
  markers={markersSV}
660
710
  engine={engine}
@@ -680,6 +730,10 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
680
730
  strokeWidth={strokeWidth}
681
731
  badge={false}
682
732
  emptyMetrics={metricsCfg.emptyState}
733
+ lineColor={loadingLineColor}
734
+ lineStrokeWidth={loadingStrokeWidth}
735
+ waveAmplitude={loadingAmplitude}
736
+ waveSpeed={loadingSpeed}
683
737
  />
684
738
  </Group>
685
739
  );
@@ -726,10 +780,11 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
726
780
  formatValue,
727
781
  skiaFont,
728
782
  degenShakeTransform,
783
+ overlayScrubFade,
729
784
  } = model;
730
785
  if (allRefLines.length === 0) return null;
731
786
  return (
732
- <Group transform={degenShakeTransform}>
787
+ <Group transform={degenShakeTransform} opacity={overlayScrubFade}>
733
788
  {allRefLines.map((rl, i) => (
734
789
  <ReferenceLineOverlay
735
790
  key={i}
@@ -778,8 +833,15 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
778
833
  markersSV,
779
834
  markerClusterCfg,
780
835
  renderMarker,
836
+ overlayScrubFade,
781
837
  } = model;
782
838
 
839
+ // Mirror the Skia overlay fade onto the RN custom-marker sibling so
840
+ // `scrub.hideOverlaysOnScrub` hides it with the Skia markers.
841
+ const overlayFadeStyle = useAnimatedStyle(() => ({
842
+ opacity: overlayScrubFade.get(),
843
+ }));
844
+
783
845
  // Extend the scrub dim past the plot's right edge to fully cover the series
784
846
  // dots (with their halo) and pulse rings, all centered on that edge. The
785
847
  // gutter reserves room beyond this for the value/Y-axis labels, drawn on top.
@@ -876,16 +938,23 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
876
938
  />
877
939
 
878
940
  {/* Custom-rendered markers — RN views floated over the canvas
879
- (non-Skia), pinned to each marker's live position. */}
941
+ (non-Skia), pinned to each marker's live position. Box-none fade
942
+ wrapper so `scrub.hideOverlaysOnScrub` hides them with the Skia
943
+ markers (full-bleed; children keep their own absolute positions). */}
880
944
  {markersActive && renderMarker && (
881
- <CustomMarkerOverlay
882
- markers={markersSV}
883
- renderMarker={renderMarker}
884
- engine={engine}
885
- padding={effectivePadding}
886
- series={series}
887
- cluster={markerClusterCfg}
888
- />
945
+ <Animated.View
946
+ pointerEvents="box-none"
947
+ style={[StyleSheet.absoluteFill, overlayFadeStyle]}
948
+ >
949
+ <CustomMarkerOverlay
950
+ markers={markersSV}
951
+ renderMarker={renderMarker}
952
+ engine={engine}
953
+ padding={effectivePadding}
954
+ series={series}
955
+ cluster={markerClusterCfg}
956
+ />
957
+ </Animated.View>
889
958
  )}
890
959
  </View>
891
960
  </GestureDetector>
@@ -62,6 +62,10 @@ export function LoadingOverlay({
62
62
  badgeTail = true,
63
63
  badgeMetrics = BADGE_METRICS_DEFAULTS,
64
64
  emptyMetrics = EMPTY_STATE_METRICS_DEFAULTS,
65
+ lineColor,
66
+ lineStrokeWidth,
67
+ waveAmplitude = 14,
68
+ waveSpeed = 1,
65
69
  }: {
66
70
  engine: ChartEngineLayout;
67
71
  padding: ChartPadding;
@@ -73,6 +77,14 @@ export function LoadingOverlay({
73
77
  isEmpty: SharedValue<boolean> | { value: boolean };
74
78
  emptyText: string;
75
79
  strokeWidth: number;
80
+ /** Loading squiggle + skeleton color. Omit → theme `gridLine`. */
81
+ lineColor?: string;
82
+ /** Loading squiggle stroke width. Omit → `strokeWidth`. */
83
+ lineStrokeWidth?: number;
84
+ /** Breathing-wave base amplitude (px). */
85
+ waveAmplitude?: number;
86
+ /** Breathing-wave speed multiplier. */
87
+ waveSpeed?: number;
76
88
  /** Mirror the badge prop so labels align with GridOverlay's label positions. */
77
89
  badge?: boolean;
78
90
  /** Whether the badge tail spike is shown; affects the left inset used for skeleton alignment. */
@@ -86,6 +98,11 @@ export function LoadingOverlay({
86
98
  const leftInset =
87
99
  badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
88
100
 
101
+ // Loading-shell color (squiggle + skeleton placeholders) and squiggle stroke,
102
+ // both overridable via `loading={{ color, strokeWidth }}`.
103
+ const loadingColor = lineColor ?? palette.gridLine;
104
+ const loadingStroke = lineStrokeWidth ?? strokeWidth;
105
+
89
106
  // Squiggly path — built into a reused PathBuilder and detach()-ed each frame.
90
107
  const squigglyBuilder = usePathBuilder();
91
108
 
@@ -100,6 +117,8 @@ export function LoadingOverlay({
100
117
  engine.canvasHeight.get(),
101
118
  padding,
102
119
  engine.timestamp.get(),
120
+ waveAmplitude,
121
+ waveSpeed,
103
122
  );
104
123
  return buildSplineDetached(b, pts);
105
124
  });
@@ -225,8 +244,8 @@ export function LoadingOverlay({
225
244
  <Path
226
245
  path={squigglyPath}
227
246
  style="stroke"
228
- strokeWidth={strokeWidth}
229
- color={palette.gridLine}
247
+ strokeWidth={loadingStroke}
248
+ color={loadingColor}
230
249
  strokeCap="round"
231
250
  strokeJoin="round"
232
251
  />
@@ -255,7 +274,7 @@ export function LoadingOverlay({
255
274
  width={RECT_W}
256
275
  height={RECT_H}
257
276
  r={RECT_R}
258
- color={palette.gridLine}
277
+ color={loadingColor}
259
278
  />
260
279
  <RoundedRect
261
280
  x={lx}
@@ -263,7 +282,7 @@ export function LoadingOverlay({
263
282
  width={RECT_W}
264
283
  height={RECT_H}
265
284
  r={RECT_R}
266
- color={palette.gridLine}
285
+ color={loadingColor}
267
286
  />
268
287
  <RoundedRect
269
288
  x={lx}
@@ -271,7 +290,7 @@ export function LoadingOverlay({
271
290
  width={RECT_W}
272
291
  height={RECT_H}
273
292
  r={RECT_R}
274
- color={palette.gridLine}
293
+ color={loadingColor}
275
294
  />
276
295
  <RoundedRect
277
296
  x={lx}
@@ -279,7 +298,7 @@ export function LoadingOverlay({
279
298
  width={RECT_W}
280
299
  height={RECT_H}
281
300
  r={RECT_R}
282
- color={palette.gridLine}
301
+ color={loadingColor}
283
302
  />
284
303
 
285
304
  {/* Empty state label */}
package/src/constants.ts CHANGED
@@ -31,6 +31,23 @@ export const HOLD_TO_SCRUB_MS = 500;
31
31
  */
32
32
  export const RETURN_TO_LIVE_MS = 450;
33
33
 
34
+ /**
35
+ * Duration (ms) of the fade applied to annotation overlays (markers + reference
36
+ * lines) when `scrub.hideOverlaysOnScrub` is on — how long they take to ease out as a
37
+ * scrub starts and back in on release. Driven by the scrub-active flag, eased on
38
+ * the UI thread. Shared by `LiveChart` and `LiveChartSeries`.
39
+ */
40
+ export const SCRUB_OVERLAY_FADE_MS = 150;
41
+
42
+ /**
43
+ * Base wave amplitude (px) of the breathing loading squiggle — it breathes
44
+ * between `0.4×` and `1.0×` this. Overridable via `loading={{ amplitude }}`.
45
+ */
46
+ export const LOADING_WAVE_AMPLITUDE = 14;
47
+
48
+ /** Default breathing-wave speed multiplier (`1` = built-in cadence). */
49
+ export const LOADING_WAVE_SPEED = 1;
50
+
34
51
  // ─── Metric default tokens (single source of truth for LiveChartMetrics) ─────
35
52
  // The resolved `metrics` config (see resolveMetrics) is assembled from these
36
53
  // objects. Draw/worklet helpers default their metric params to the matching