react-native-livechart 4.17.0 → 4.19.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 (75) hide show
  1. package/dist/components/CrosshairOverlay.d.ts +3 -1
  2. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  3. package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
  4. package/dist/components/DotOverlay.d.ts +10 -2
  5. package/dist/components/DotOverlay.d.ts.map +1 -1
  6. package/dist/components/LiveChart.d.ts.map +1 -1
  7. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  8. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  9. package/dist/components/MultiSeriesDots.d.ts +5 -3
  10. package/dist/components/MultiSeriesDots.d.ts.map +1 -1
  11. package/dist/constants.d.ts +6 -0
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/core/resolveConfig.d.ts +20 -3
  14. package/dist/core/resolveConfig.d.ts.map +1 -1
  15. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  16. package/dist/draw/candle.d.ts +21 -0
  17. package/dist/draw/candle.d.ts.map +1 -1
  18. package/dist/draw/line.d.ts +9 -3
  19. package/dist/draw/line.d.ts.map +1 -1
  20. package/dist/hooks/crosshairShared.d.ts +8 -0
  21. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  22. package/dist/hooks/resolveChartLayout.d.ts +12 -1
  23. package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
  24. package/dist/hooks/useCandlePaths.d.ts +3 -1
  25. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  26. package/dist/hooks/useChartPaths.d.ts +3 -1
  27. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  28. package/dist/hooks/useCrosshair.d.ts +7 -1
  29. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  30. package/dist/hooks/useDegen.d.ts +1 -3
  31. package/dist/hooks/useDegen.d.ts.map +1 -1
  32. package/dist/hooks/useLiveDot.d.ts +8 -2
  33. package/dist/hooks/useLiveDot.d.ts.map +1 -1
  34. package/dist/hooks/useMarkers.d.ts.map +1 -1
  35. package/dist/hooks/useMultiSeriesLinePaths.d.ts +3 -1
  36. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
  37. package/dist/hooks/useSeriesIndicatorOpacity.d.ts +13 -0
  38. package/dist/hooks/useSeriesIndicatorOpacity.d.ts.map +1 -0
  39. package/dist/hooks/useTradeStream.d.ts.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/math/markerCluster.d.ts +6 -0
  43. package/dist/math/markerCluster.d.ts.map +1 -1
  44. package/dist/math/simplify.d.ts +20 -0
  45. package/dist/math/simplify.d.ts.map +1 -0
  46. package/dist/types.d.ts +106 -17
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/components/CrosshairOverlay.tsx +14 -9
  50. package/src/components/CustomMarkerOverlay.tsx +5 -1
  51. package/src/components/DotOverlay.tsx +26 -3
  52. package/src/components/LiveChart.tsx +210 -55
  53. package/src/components/LiveChartSeries.tsx +28 -10
  54. package/src/components/MarkerOverlay.tsx +5 -1
  55. package/src/components/MultiSeriesDots.tsx +22 -3
  56. package/src/constants.ts +7 -0
  57. package/src/core/resolveConfig.ts +67 -13
  58. package/src/core/useLiveChartEngine.ts +6 -1
  59. package/src/draw/candle.ts +78 -0
  60. package/src/draw/line.ts +24 -10
  61. package/src/hooks/crosshairShared.ts +31 -0
  62. package/src/hooks/resolveChartLayout.ts +40 -16
  63. package/src/hooks/useCandlePaths.ts +9 -1
  64. package/src/hooks/useChartPaths.ts +37 -7
  65. package/src/hooks/useCrosshair.ts +29 -2
  66. package/src/hooks/useDegen.ts +2 -6
  67. package/src/hooks/useLiveDot.ts +28 -3
  68. package/src/hooks/useMarkers.ts +11 -3
  69. package/src/hooks/useMultiSeriesLinePaths.ts +31 -4
  70. package/src/hooks/useSeriesIndicatorOpacity.ts +56 -0
  71. package/src/hooks/useTradeStream.ts +6 -1
  72. package/src/index.ts +1 -0
  73. package/src/math/markerCluster.ts +15 -1
  74. package/src/math/simplify.ts +197 -0
  75. package/src/types.ts +107 -17
@@ -69,7 +69,12 @@ import {
69
69
  } from "../core/liveIndicatorVisibility";
70
70
  import { resolveSegment, type ResolvedSegment } from "../core/resolveSegment";
71
71
  import { useLiveChartEngine } from "../core/useLiveChartEngine";
72
- import { pulseRadialOutset } from "../draw/line";
72
+ import {
73
+ computeCandleFocusPassOpacity,
74
+ computeCandleFocusClip,
75
+ HIDDEN_CANDLE_FOCUS_CLIP,
76
+ } from "../draw/candle";
77
+ import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
73
78
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
74
79
  import { useBadge } from "../hooks/useBadge";
75
80
  import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
@@ -87,12 +92,13 @@ import { useMarkers } from "../hooks/useMarkers";
87
92
  import { useReferenceDrag } from "../hooks/useReferenceDrag";
88
93
  import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
89
94
  import { useModeBlend } from "../hooks/useModeBlend";
90
- import {
91
- resolveMomentumProp,
92
- useMomentum,
93
- } from "../hooks/useMomentum";
95
+ import { resolveMomentumProp, useMomentum } from "../hooks/useMomentum";
94
96
  import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
95
97
  import { usePinchZoom } from "../hooks/usePinchZoom";
98
+ import {
99
+ SERIES_INDICATOR_FADE_MS,
100
+ useSeriesIndicatorOpacity,
101
+ } from "../hooks/useSeriesIndicatorOpacity";
96
102
  import { useVisibleRange } from "../hooks/useVisibleRange";
97
103
  import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
98
104
  import {
@@ -127,6 +133,7 @@ import {
127
133
  resolveTheme,
128
134
  } from "../theme";
129
135
  import type {
136
+ CandlePoint,
130
137
  LiveChartPalette,
131
138
  LiveChartPoint,
132
139
  LiveChartProps,
@@ -380,9 +387,17 @@ function useLiveChartController({
380
387
  // ring never starts (the DotOverlay reads `pulseCfg`, so null = no pulse).
381
388
  const pulseCfg = isStatic ? null : resolvePulse(pulse);
382
389
  const dotCfg = resolveDot(dot);
390
+ // `badge.followViewEdge` wins over `dot.trackWhileParked`: an edge-pinned dot
391
+ // must stay aligned with its badge, so the tracking flag is ignored.
392
+ const dotTracksParked =
393
+ dotCfg.trackWhileParked && !(badgeCfg?.followViewEdge ?? false);
383
394
  const selectionDotCfg = resolveSelectionDot(selectionDot);
384
- // Outer footprint of the dot (color-filled radius plus the halo ring).
385
- const dotOuterRadius = dotCfg.radius + (dotCfg.ring?.width ?? 0);
395
+ // Outer visible footprint of the dot, including its crisp ring and optional
396
+ // blurred glow. Used by scrub dimming so neither effect leaks through.
397
+ const dotOuterRadius = Math.max(
398
+ dotCfg.radius + (dotCfg.ring?.width ?? 0),
399
+ dotCfg.glow ? dotGlowRadialOutset(dotCfg.glow.radius, dotCfg.glow.blur) : 0,
400
+ );
386
401
  const gridStyleCfg = resolveGridStyle(gridStyle);
387
402
  // Static charts run zero loops: force degen off so `useDegen`'s frame callback
388
403
  // never starts (also passed `isStatic` below as a belt-and-braces autostart gate).
@@ -409,12 +424,14 @@ function useLiveChartController({
409
424
  const active = dragActive.get();
410
425
  const cur = dragValues.get();
411
426
  const seeded = seededRef.current;
412
- dragValues.set(allRefLines.map((l, i) => {
413
- const prop = l.value ?? 0;
414
- if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
415
- if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
416
- return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
417
- }));
427
+ dragValues.set(
428
+ allRefLines.map((l, i) => {
429
+ const prop = l.value ?? 0;
430
+ if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
431
+ if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
432
+ return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
433
+ }),
434
+ );
418
435
  seededRef.current = allRefLines.map((l) => l.value);
419
436
  if (dragActive.get().length !== allRefLines.length) {
420
437
  dragActive.set(allRefLines.map((_, i) => active[i] ?? false));
@@ -649,6 +666,7 @@ function useLiveChartController({
649
666
  formatValue,
650
667
  currentValue: valueLayoutSample,
651
668
  pulse: pulseConfig,
669
+ dotGlow: dotCfg.glow,
652
670
  volumeBandHeight,
653
671
  });
654
672
 
@@ -671,6 +689,10 @@ function useLiveChartController({
671
689
  isStatic,
672
690
  transitionsCfg.reveal,
673
691
  );
692
+ const seriesIndicatorOpacity = useSeriesIndicatorOpacity(
693
+ resolvedSeriesOpacity,
694
+ isStatic || transitionsCfg.reveal === 0 ? 0 : SERIES_INDICATOR_FADE_MS,
695
+ );
674
696
 
675
697
  // After data clears, keep last snapshot until morphT finishes dropping (web parity).
676
698
  const { lineEngineData, candlesEngine, liveEngine } =
@@ -765,7 +787,8 @@ function useLiveChartController({
765
787
  if (
766
788
  referenceLineForm(l) !== "line" ||
767
789
  l.value === undefined ||
768
- refLineCustom[i] || refLineOffAxisCustom[i]
790
+ refLineCustom[i] ||
791
+ refLineOffAxisCustom[i]
769
792
  ) {
770
793
  ys.push(-1);
771
794
  continue;
@@ -922,6 +945,7 @@ function useLiveChartController({
922
945
  thresholdCfg?.fill && thresholdSeriesHasPoints
923
946
  ? thresholdSeriesGeom.samples
924
947
  : undefined,
948
+ lineProp?.simplify,
925
949
  );
926
950
 
927
951
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -944,6 +968,7 @@ function useLiveChartController({
944
968
  effectivePadding,
945
969
  engine.edgeValue,
946
970
  badgeCfg?.followViewEdge ?? false,
971
+ dotCfg.trackWhileParked,
947
972
  );
948
973
 
949
974
  const momentumSV = useMomentum(engine, momentum);
@@ -963,7 +988,8 @@ function useLiveChartController({
963
988
  const displayCandleWidth = useCandleWidthLerp(
964
989
  candleWidth,
965
990
  transitionsCfg.candleLerpSpeed,
966
- !isStatic && isCandle,
991
+ !isStatic,
992
+ isCandle,
967
993
  );
968
994
 
969
995
  // ── Overlay hooks ─────────────────────────────────────────────────────
@@ -1095,6 +1121,8 @@ function useLiveChartController({
1095
1121
  : 0,
1096
1122
  scrollActive,
1097
1123
  scrubCfg?.clampToPlot ?? false,
1124
+ // Candle mode: snap the crosshair to candle centers (tick-to-tick).
1125
+ scrubCfg?.snapToCandles ?? false,
1098
1126
  );
1099
1127
 
1100
1128
  // Capture only the shared value in the worklets below. Referencing
@@ -1315,34 +1343,31 @@ function useLiveChartController({
1315
1343
  timeScroll,
1316
1344
  badgeCfg?.followViewEdge ?? false,
1317
1345
  );
1346
+ // A dot that tracks the true live point while parked is exempt from the
1347
+ // scroll-back hide: it no longer marks an off-screen price, and it hides
1348
+ // itself once the live point leaves the window (`useLiveDot`'s sentinel).
1318
1349
  const liveDotOpacity = useDerivedValue(
1319
1350
  () =>
1320
1351
  reveal.dotOpacity.value *
1321
1352
  (selectionDotDuringScrub && crosshairScrubActive.value ? 0 : 1) *
1322
1353
  liveIndicatorScrollOpacity(
1323
- hideLiveOnScrollBack,
1354
+ hideLiveOnScrollBack && !dotTracksParked,
1324
1355
  engine.viewEnd.value,
1325
1356
  ) *
1326
- resolvedSeriesOpacity.value,
1357
+ seriesIndicatorOpacity.value,
1327
1358
  );
1328
1359
  // Same scrolled-back gating for the value line: it would draw a dashed line
1329
1360
  // at the live value's Y — a price that isn't in the scrolled-back view.
1330
1361
  const valueLineOpacity = useDerivedValue(
1331
1362
  () =>
1332
1363
  reveal.lineOpacity.value *
1333
- liveIndicatorScrollOpacity(
1334
- hideLiveOnScrollBack,
1335
- engine.viewEnd.value,
1336
- ) *
1364
+ liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
1337
1365
  resolvedSeriesOpacity.value,
1338
1366
  );
1339
1367
  const liveBadgeOpacity = useDerivedValue(
1340
1368
  () =>
1341
1369
  reveal.badgeOpacity.value *
1342
- liveIndicatorScrollOpacity(
1343
- hideLiveOnScrollBack,
1344
- engine.viewEnd.value,
1345
- ) *
1370
+ liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
1346
1371
  resolvedSeriesOpacity.value,
1347
1372
  );
1348
1373
 
@@ -1396,6 +1421,7 @@ function useLiveChartController({
1396
1421
  valueLineCfg,
1397
1422
  pulseCfg,
1398
1423
  dotCfg,
1424
+ dotTracksParked,
1399
1425
  dotOuterRadius,
1400
1426
  gridStyleCfg,
1401
1427
  degenCfg,
@@ -1525,8 +1551,7 @@ function ChartWithDegen({
1525
1551
  model: LiveChartModel;
1526
1552
  yAxisEntries: YAxisEntries | null;
1527
1553
  }) {
1528
- const { engine, dotX, dotY, momentumSV, degenCfg, onDegenShake, isStatic } =
1529
- model;
1554
+ const { engine, dotX, dotY, momentumSV, degenCfg, onDegenShake } = model;
1530
1555
  const state = useDegen(
1531
1556
  engine,
1532
1557
  dotX,
@@ -1534,7 +1559,6 @@ function ChartWithDegen({
1534
1559
  momentumSV,
1535
1560
  degenCfg,
1536
1561
  onDegenShake,
1537
- isStatic,
1538
1562
  );
1539
1563
  return <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />;
1540
1564
  }
@@ -1767,6 +1791,42 @@ function ChartFillLayer({
1767
1791
  );
1768
1792
  }
1769
1793
 
1794
+ type CandlePaths = ReturnType<typeof useCandlePaths>;
1795
+
1796
+ /** One batched candle pass: two body paths and two wick paths. */
1797
+ function CandlePathBatch({
1798
+ paths,
1799
+ wickWidth,
1800
+ palette,
1801
+ }: {
1802
+ paths: CandlePaths;
1803
+ wickWidth: number;
1804
+ palette: LiveChartPalette;
1805
+ }) {
1806
+ return (
1807
+ <>
1808
+ <Path
1809
+ path={paths.upWicksPath}
1810
+ style="stroke"
1811
+ strokeWidth={wickWidth}
1812
+ color={palette.wickUp}
1813
+ />
1814
+ <Path
1815
+ path={paths.downWicksPath}
1816
+ style="stroke"
1817
+ strokeWidth={wickWidth}
1818
+ color={palette.wickDown}
1819
+ />
1820
+ <Path path={paths.upBodiesPath} style="fill" color={palette.candleUp} />
1821
+ <Path
1822
+ path={paths.downBodiesPath}
1823
+ style="fill"
1824
+ color={palette.candleDown}
1825
+ />
1826
+ </>
1827
+ );
1828
+ }
1829
+
1770
1830
  /**
1771
1831
  * Candle/volume paths are a mode-specific subsystem. Keeping their hooks in a
1772
1832
  * child means a line chart never registers the candle-width frame callback or
@@ -1788,15 +1848,10 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1788
1848
  volumeOpacity,
1789
1849
  volumeUpColor,
1790
1850
  volumeDownColor,
1851
+ scrubCfg,
1852
+ crosshair,
1791
1853
  } = model;
1792
- const {
1793
- upBodiesPath,
1794
- downBodiesPath,
1795
- upWicksPath,
1796
- downWicksPath,
1797
- upBarsPath,
1798
- downBarsPath,
1799
- } = useCandlePaths(
1854
+ const paths = useCandlePaths(
1800
1855
  engine,
1801
1856
  effectivePadding,
1802
1857
  candlesEngine,
@@ -1807,31 +1862,119 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1807
1862
  volumeBandHeight,
1808
1863
  volumeCfg?.radius ?? 0,
1809
1864
  );
1865
+ const focusOtherCandles = scrubCfg?.dimTarget === "otherCandles";
1866
+ const candleDimOpacity = scrubCfg?.dimOpacity ?? 1;
1867
+ const candleDimFadeMs = scrubCfg?.dimFadeMs ?? 0;
1868
+ // Destructure the two SharedValues before entering worklets. Closing over the
1869
+ // whole crosshair object would also serialize its RNGH gesture instances.
1870
+ const scrubActive = crosshair.scrubActive;
1871
+ // ChartCandleLayer is single-series only; useCrosshair always supplies this
1872
+ // value (it is optional on the shared state type only for LiveChartSeries).
1873
+ const scrubCandle = crosshair.scrubCandle!;
1874
+ // Keep the last selection after scrubCandle clears so its clipped full-
1875
+ // strength pass can cover the base batch until the release fade completes.
1876
+ const lastScrubCandle = useSharedValue<CandlePoint | null>(null);
1877
+ useAnimatedReaction(
1878
+ () => (focusOtherCandles ? scrubCandle.get() : null),
1879
+ (candle) => {
1880
+ if (candle) lastScrubCandle.set(candle);
1881
+ },
1882
+ );
1883
+ const inactiveCandleOpacity = useDerivedValue(() => {
1884
+ const target =
1885
+ focusOtherCandles && scrubActive.get() && scrubCandle.get()
1886
+ ? candleDimOpacity
1887
+ : 1;
1888
+ return candleDimFadeMs > 0
1889
+ ? withTiming(target, { duration: candleDimFadeMs })
1890
+ : target;
1891
+ }, [
1892
+ focusOtherCandles,
1893
+ scrubActive,
1894
+ scrubCandle,
1895
+ candleDimOpacity,
1896
+ candleDimFadeMs,
1897
+ ]);
1898
+ const focusedCandleOpacity = useDerivedValue(() => {
1899
+ const currentCandle = scrubCandle.get();
1900
+ const focusCandle = currentCandle ?? lastScrubCandle.get();
1901
+ if (!focusOtherCandles || !focusCandle) return 0;
1902
+ return computeCandleFocusPassOpacity(
1903
+ scrubActive.get(),
1904
+ currentCandle !== null,
1905
+ inactiveCandleOpacity.get(),
1906
+ );
1907
+ }, [
1908
+ focusOtherCandles,
1909
+ scrubActive,
1910
+ scrubCandle,
1911
+ lastScrubCandle,
1912
+ inactiveCandleOpacity,
1913
+ ]);
1914
+ const focusedCandleClip = useDerivedValue(() => {
1915
+ if (!focusOtherCandles) {
1916
+ return HIDDEN_CANDLE_FOCUS_CLIP;
1917
+ }
1918
+ const focusCandle = scrubCandle.get() ?? lastScrubCandle.get();
1919
+ return computeCandleFocusClip(
1920
+ focusCandle,
1921
+ effectivePadding,
1922
+ engine.canvasWidth.get(),
1923
+ engine.canvasHeight.get(),
1924
+ engine.timestamp.get() - engine.displayWindow.get(),
1925
+ engine.displayWindow.get(),
1926
+ displayCandleWidth.get(),
1927
+ );
1928
+ }, [
1929
+ focusOtherCandles,
1930
+ scrubCandle,
1931
+ lastScrubCandle,
1932
+ effectivePadding,
1933
+ engine.canvasWidth,
1934
+ engine.canvasHeight,
1935
+ engine.timestamp,
1936
+ engine.displayWindow,
1937
+ displayCandleWidth,
1938
+ ]);
1810
1939
 
1811
1940
  return (
1812
1941
  <Group opacity={seriesOpacity}>
1813
1942
  <Group opacity={candleGroupOpacity}>
1814
- <Path
1815
- path={upWicksPath}
1816
- style="stroke"
1817
- strokeWidth={metricsCfg.candle.wickWidth}
1818
- color={palette.wickUp}
1819
- />
1820
- <Path
1821
- path={downWicksPath}
1822
- style="stroke"
1823
- strokeWidth={metricsCfg.candle.wickWidth}
1824
- color={palette.wickDown}
1825
- />
1826
- <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
1827
- <Path path={downBodiesPath} style="fill" color={palette.candleDown} />
1943
+ {focusOtherCandles ? (
1944
+ <>
1945
+ <Group opacity={inactiveCandleOpacity}>
1946
+ <CandlePathBatch
1947
+ paths={paths}
1948
+ wickWidth={metricsCfg.candle.wickWidth}
1949
+ palette={palette}
1950
+ />
1951
+ </Group>
1952
+ <Group opacity={focusedCandleOpacity} clip={focusedCandleClip}>
1953
+ <CandlePathBatch
1954
+ paths={paths}
1955
+ wickWidth={metricsCfg.candle.wickWidth}
1956
+ palette={palette}
1957
+ />
1958
+ </Group>
1959
+ </>
1960
+ ) : (
1961
+ <CandlePathBatch
1962
+ paths={paths}
1963
+ wickWidth={metricsCfg.candle.wickWidth}
1964
+ palette={palette}
1965
+ />
1966
+ )}
1828
1967
  </Group>
1829
1968
 
1830
1969
  {volumeCfg && (
1831
1970
  <Group opacity={candleGroupOpacity}>
1832
1971
  <Group opacity={volumeOpacity}>
1833
- <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1834
- <Path path={downBarsPath} style="fill" color={volumeDownColor} />
1972
+ <Path path={paths.upBarsPath} style="fill" color={volumeUpColor} />
1973
+ <Path
1974
+ path={paths.downBarsPath}
1975
+ style="fill"
1976
+ color={volumeDownColor}
1977
+ />
1835
1978
  </Group>
1836
1979
  </Group>
1837
1980
  )}
@@ -1893,6 +2036,7 @@ function ChartStack({
1893
2036
  liveDotOpacity,
1894
2037
  pulseCfg,
1895
2038
  dotCfg,
2039
+ dotTracksParked,
1896
2040
  degenCfg,
1897
2041
  markersActive,
1898
2042
  markersSV,
@@ -2057,10 +2201,14 @@ function ChartStack({
2057
2201
  dotY={dotY}
2058
2202
  palette={palette}
2059
2203
  pulse={pulseCfg}
2204
+ glow={dotCfg.glow}
2060
2205
  radius={dotCfg.radius}
2061
2206
  ring={dotCfg.ring}
2062
2207
  color={dotCfg.color}
2063
2208
  viewEnd={engine.viewEnd}
2209
+ // A tracking dot marks the honest live position while parked, so
2210
+ // its heartbeat keeps pulsing (useLiveDot tracks the true point).
2211
+ pulseWhileParked={dotTracksParked}
2064
2212
  />
2065
2213
  </Group>
2066
2214
  )}
@@ -2204,6 +2352,10 @@ function ChartScrubLayer({
2204
2352
  // Skia tooltip is suppressed here while it's active — the line pill in line
2205
2353
  // mode, and the OHLC stack in candle mode (see the stack gate below).
2206
2354
  const customTooltipActive = renderTooltip != null;
2355
+ // `otherCandles` is candle-only. If a consumer switches this chart to line
2356
+ // mode without also rewriting its scrub config, preserve the standard line
2357
+ // scrub (guide, selection dot, and trailing fade).
2358
+ const dimsFuture = !isCandle || scrubCfg?.dimTarget === "future";
2207
2359
 
2208
2360
  if (!scrubCfg) return null;
2209
2361
 
@@ -2228,11 +2380,12 @@ function ChartScrubLayer({
2228
2380
  font={skiaFont}
2229
2381
  showTooltip={scrubCfg.tooltip && !customTooltipActive}
2230
2382
  lineTop={crosshair.tooltipLineTop}
2231
- selectionDot={selectionDot}
2383
+ showLine={dimsFuture}
2384
+ selectionDot={dimsFuture ? selectionDot : null}
2232
2385
  selectionY={crosshair.scrubDotY}
2233
2386
  scrubActive={crosshair.scrubActive}
2234
2387
  selectionColor={selectionColor}
2235
- dimOpacity={scrubCfg.dimOpacity}
2388
+ dimOpacity={dimsFuture ? scrubCfg.dimOpacity : 1}
2236
2389
  liveDotExtent={liveDotExtent}
2237
2390
  crosshairLineColor={scrubCfg.crosshairLineColor}
2238
2391
  crosshairStrokeWidth={scrubCfg.crosshairStrokeWidth}
@@ -2241,7 +2394,9 @@ function ChartScrubLayer({
2241
2394
  crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
2242
2395
  crosshairLineCap={scrubCfg.crosshairLineCap}
2243
2396
  crosshairDash={scrubCfg.crosshairDash}
2244
- crosshairDimColor={scrubCfg.crosshairDimColor}
2397
+ crosshairDimColor={
2398
+ dimsFuture ? scrubCfg.crosshairDimColor : undefined
2399
+ }
2245
2400
  tooltipBackground={scrubCfg.tooltipBackground}
2246
2401
  tooltipColor={scrubCfg.tooltipColor}
2247
2402
  tooltipBorderColor={scrubCfg.tooltipBorderColor}
@@ -50,7 +50,7 @@ import {
50
50
  resolveZoom,
51
51
  } from "../core/resolveConfig";
52
52
  import { useLiveChartSeriesEngine } from "../core/useLiveChartSeriesEngine";
53
- import { pulseRadialOutset } from "../draw/line";
53
+ import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
54
54
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
55
55
  import { useCanvasLayout } from "../hooks/useCanvasLayout";
56
56
  import { useChartReveal } from "../hooks/useChartReveal";
@@ -64,6 +64,10 @@ import { useMultiSeriesLinePaths } from "../hooks/useMultiSeriesLinePaths";
64
64
  import { usePanScroll } from "../hooks/usePanScroll";
65
65
  import { usePinchZoom } from "../hooks/usePinchZoom";
66
66
  import { useMultiSeriesReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
67
+ import {
68
+ SERIES_INDICATOR_FADE_MS,
69
+ useSeriesIndicatorOpacity,
70
+ } from "../hooks/useSeriesIndicatorOpacity";
67
71
  import { useVisibleRange } from "../hooks/useVisibleRange";
68
72
  import { useXAxis } from "../hooks/useXAxis";
69
73
  import { useYAxis } from "../hooks/useYAxis";
@@ -246,9 +250,12 @@ function useLiveChartSeriesController({
246
250
  const selectionDotCfg = resolveSelectionDot(selectionDot ?? false);
247
251
  const gridStyleCfg = resolveGridStyle(gridStyle);
248
252
  const dotCfg = resolveMultiSeriesDot(dotProp);
249
- // Outer footprint of a dot (the color-filled radius plus the halo ring).
250
- // Used to keep the gutter labels clear of the haloed dot.
251
- const dotOuterRadius = dotCfg.radius + (dotCfg.ring?.width ?? 0);
253
+ // Outer visible footprint of a dot, including its crisp ring and optional
254
+ // blurred glow. Used to keep gutter labels clear of every dot effect.
255
+ const dotOuterRadius = Math.max(
256
+ dotCfg.radius + (dotCfg.ring?.width ?? 0),
257
+ dotCfg.glow ? dotGlowRadialOutset(dotCfg.glow.radius, dotCfg.glow.blur) : 0,
258
+ );
252
259
  const legendCfg = resolveLegend(legendProp);
253
260
  const degenCfg = resolveDegen(degen);
254
261
  const metricsCfg = resolveMetrics(metrics);
@@ -341,6 +348,7 @@ function useLiveChartSeriesController({
341
348
  formatValue,
342
349
  currentValue: representativeValue,
343
350
  pulse: dotCfg.pulse,
351
+ dotGlow: dotCfg.glow,
344
352
  multiSeriesDotRadius: dotOuterRadius,
345
353
  multiSeriesValueLabel: dotCfg.valueLabel,
346
354
  multiSeriesMaxLabelWidth: maxSeriesLabelWidth,
@@ -367,6 +375,10 @@ function useLiveChartSeriesController({
367
375
  false,
368
376
  transitionsCfg.reveal,
369
377
  );
378
+ const seriesIndicatorOpacity = useSeriesIndicatorOpacity(
379
+ resolvedSeriesOpacity,
380
+ transitionsCfg.reveal === 0 ? 0 : SERIES_INDICATOR_FADE_MS,
381
+ );
370
382
 
371
383
  const effectiveSeries = useMultiSeriesReverseMorphInputs({
372
384
  series,
@@ -397,6 +409,7 @@ function useLiveChartSeriesController({
397
409
  engine,
398
410
  effectivePadding,
399
411
  activeSeriesCount,
412
+ lineProp?.simplify,
400
413
  );
401
414
 
402
415
  // Per-series colors and stroke styles, derived from the off-render snapshot
@@ -597,6 +610,7 @@ function useLiveChartSeriesController({
597
610
  // passthrough props the render needs
598
611
  series,
599
612
  seriesOpacity: resolvedSeriesOpacity,
613
+ seriesIndicatorOpacity,
600
614
  style,
601
615
  canvasMode,
602
616
  accessibilityLabel,
@@ -709,6 +723,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
709
723
  renderMarker,
710
724
  series,
711
725
  seriesOpacity,
726
+ seriesIndicatorOpacity,
712
727
  emptyText,
713
728
  loadingAxisLabels,
714
729
  metricsCfg,
@@ -798,7 +813,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
798
813
  )}
799
814
 
800
815
  {dotCfg.show && (
801
- <Group opacity={seriesOpacity}>
816
+ <Group opacity={seriesIndicatorOpacity}>
802
817
  <Group opacity={reveal.dotOpacity}>
803
818
  <MultiSeriesDots
804
819
  engine={engine}
@@ -806,6 +821,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
806
821
  colors={lineColors}
807
822
  radius={dotCfg.radius}
808
823
  ring={dotCfg.ring}
824
+ glow={dotCfg.glow}
809
825
  ringColor={palette.badgeOuterBg}
810
826
  color={dotCfg.color}
811
827
  pulse={dotCfg.pulse}
@@ -953,7 +969,11 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
953
969
  }
954
970
 
955
971
  /** Owns the price/time projection worklets for the multi-series custom overlay. */
956
- function SeriesCustomConsumerOverlay({ model }: { model: LiveChartSeriesModel }) {
972
+ function SeriesCustomConsumerOverlay({
973
+ model,
974
+ }: {
975
+ model: LiveChartSeriesModel;
976
+ }) {
957
977
  const { engine, effectivePadding, renderOverlay } = model;
958
978
  const overlayContext = useChartOverlayContext(engine, effectivePadding);
959
979
  return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
@@ -1146,12 +1166,10 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
1146
1166
  dimOpacity={scrubCfg.dimOpacity}
1147
1167
  liveDotExtent={liveDotExtent}
1148
1168
  crosshairLineColor={
1149
- seriesTooltipCfg?.guideColor ??
1150
- scrubCfg.crosshairLineColor
1169
+ seriesTooltipCfg?.guideColor ?? scrubCfg.crosshairLineColor
1151
1170
  }
1152
1171
  crosshairStrokeWidth={
1153
- seriesTooltipCfg?.guideWidth ??
1154
- scrubCfg.crosshairStrokeWidth
1172
+ seriesTooltipCfg?.guideWidth ?? scrubCfg.crosshairStrokeWidth
1155
1173
  }
1156
1174
  crosshairOvershoot={scrubCfg.crosshairOvershoot}
1157
1175
  crosshairFade={scrubCfg.crosshairFade}
@@ -388,7 +388,11 @@ export function MarkerOverlay({
388
388
  lineData: lineData?.get(),
389
389
  lineLinear,
390
390
  });
391
- clusterMarkers(ms, buf, { config: cluster });
391
+ clusterMarkers(ms, buf, {
392
+ config: cluster,
393
+ minY: 0,
394
+ maxY: engine.canvasHeight.get(),
395
+ });
392
396
  const atlasFrames = atlasFrameRef.current!;
393
397
  atlasFrames.tick = !atlasFrames.tick;
394
398
  const frame = atlasFrames.tick ? atlasFrames.a : atlasFrames.b;
@@ -1,8 +1,9 @@
1
- import { Circle, Group } from "@shopify/react-native-skia";
1
+ import { BlurMask, Circle, Group } from "@shopify/react-native-skia";
2
2
 
3
3
  import { useDerivedValue, type SharedValue } from "react-native-reanimated";
4
4
  import type { ChartPadding } from "../draw/line";
5
5
  import type {
6
+ ResolvedDotGlowConfig,
6
7
  ResolvedDotRingConfig,
7
8
  ResolvedPulseConfig,
8
9
  } from "../core/resolveConfig";
@@ -18,6 +19,7 @@ function SeriesDotAtIndex({
18
19
  color,
19
20
  radius,
20
21
  ring,
22
+ glow = null,
21
23
  ringColor,
22
24
  pulse,
23
25
  viewEnd,
@@ -27,8 +29,10 @@ function SeriesDotAtIndex({
27
29
  padding: ChartPadding;
28
30
  color: string;
29
31
  radius: number;
30
- /** Outer halo ring, or `null` for a flat circle. */
32
+ /** Crisp outer backing ring, or `null` for a flat circle. */
31
33
  ring: ResolvedDotRingConfig | null;
34
+ /** Optional soft static glow; unlike `pulse`, it has no animation clock. */
35
+ glow?: ResolvedDotGlowConfig | null;
32
36
  /** Fallback ring color when `ring.color` is unset (theme `badgeOuterBg`). */
33
37
  ringColor: string;
34
38
  pulse: ResolvedPulseConfig | null;
@@ -94,6 +98,17 @@ function SeriesDotAtIndex({
94
98
  opacity={pulseOpacity}
95
99
  />
96
100
  )}
101
+ {glow && (
102
+ <Circle
103
+ cx={dotX}
104
+ cy={dotY}
105
+ r={glow.radius}
106
+ color={glow.color ?? color}
107
+ opacity={glow.opacity}
108
+ >
109
+ <BlurMask blur={glow.blur} style="normal" />
110
+ </Circle>
111
+ )}
97
112
  {ring && (
98
113
  <Circle
99
114
  cx={dotX}
@@ -113,6 +128,7 @@ export function MultiSeriesDots({
113
128
  colors,
114
129
  radius,
115
130
  ring,
131
+ glow = null,
116
132
  ringColor,
117
133
  color,
118
134
  pulse,
@@ -123,8 +139,10 @@ export function MultiSeriesDots({
123
139
  padding: ChartPadding;
124
140
  colors: string[];
125
141
  radius: number;
126
- /** Outer halo ring, or `null` for flat circles. */
142
+ /** Crisp outer backing ring, or `null` for flat circles. */
127
143
  ring: ResolvedDotRingConfig | null;
144
+ /** Optional soft static glow behind every series dot. */
145
+ glow?: ResolvedDotGlowConfig | null;
128
146
  /** Fallback ring color when `ring.color` is unset (theme `badgeOuterBg`). */
129
147
  ringColor: string;
130
148
  /** Fill color override; falls back to each series' line color. */
@@ -146,6 +164,7 @@ export function MultiSeriesDots({
146
164
  color={color ?? colors[i] ?? "#ffffff"}
147
165
  radius={radius}
148
166
  ring={ring}
167
+ glow={glow}
149
168
  ringColor={ringColor}
150
169
  pulse={pulse}
151
170
  viewEnd={viewEnd}
package/src/constants.ts CHANGED
@@ -39,6 +39,13 @@ export const RETURN_TO_LIVE_MS = 450;
39
39
  */
40
40
  export const SCRUB_OVERLAY_FADE_MS = 150;
41
41
 
42
+ /**
43
+ * Duration (ms) of the candle-focus opacity transition used by
44
+ * `scrub.dimTarget: "otherCandles"`. The selected candle remains fully opaque;
45
+ * the shared candle batch eases toward `dimOpacity` and back to full strength.
46
+ */
47
+ export const SCRUB_CANDLE_DIM_FADE_MS = 60;
48
+
42
49
  /**
43
50
  * Base wave amplitude (px) of the breathing loading squiggle — it breathes
44
51
  * between `0.4×` and `1.0×` this. Overridable via `loading={{ amplitude }}`.