react-native-livechart 4.18.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 (59) 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/DotOverlay.d.ts +4 -2
  4. package/dist/components/DotOverlay.d.ts.map +1 -1
  5. package/dist/components/LiveChart.d.ts.map +1 -1
  6. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  7. package/dist/components/MultiSeriesDots.d.ts +5 -3
  8. package/dist/components/MultiSeriesDots.d.ts.map +1 -1
  9. package/dist/constants.d.ts +6 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/core/resolveConfig.d.ts +17 -3
  12. package/dist/core/resolveConfig.d.ts.map +1 -1
  13. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  14. package/dist/draw/candle.d.ts +21 -0
  15. package/dist/draw/candle.d.ts.map +1 -1
  16. package/dist/draw/line.d.ts +7 -1
  17. package/dist/draw/line.d.ts.map +1 -1
  18. package/dist/hooks/resolveChartLayout.d.ts +8 -0
  19. package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
  20. package/dist/hooks/useCandlePaths.d.ts +3 -1
  21. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  22. package/dist/hooks/useChartPaths.d.ts +3 -1
  23. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  24. package/dist/hooks/useDegen.d.ts +1 -3
  25. package/dist/hooks/useDegen.d.ts.map +1 -1
  26. package/dist/hooks/useMarkers.d.ts.map +1 -1
  27. package/dist/hooks/useMultiSeriesLinePaths.d.ts +3 -1
  28. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
  29. package/dist/hooks/useSeriesIndicatorOpacity.d.ts +13 -0
  30. package/dist/hooks/useSeriesIndicatorOpacity.d.ts.map +1 -0
  31. package/dist/hooks/useTradeStream.d.ts.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/math/simplify.d.ts +20 -0
  35. package/dist/math/simplify.d.ts.map +1 -0
  36. package/dist/types.d.ts +77 -16
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/components/CrosshairOverlay.tsx +14 -9
  40. package/src/components/DotOverlay.tsx +17 -1
  41. package/src/components/LiveChart.tsx +194 -54
  42. package/src/components/LiveChartSeries.tsx +28 -10
  43. package/src/components/MultiSeriesDots.tsx +22 -3
  44. package/src/constants.ts +7 -0
  45. package/src/core/resolveConfig.ts +61 -13
  46. package/src/core/useLiveChartEngine.ts +6 -1
  47. package/src/draw/candle.ts +78 -0
  48. package/src/draw/line.ts +20 -7
  49. package/src/hooks/resolveChartLayout.ts +30 -18
  50. package/src/hooks/useCandlePaths.ts +9 -1
  51. package/src/hooks/useChartPaths.ts +37 -7
  52. package/src/hooks/useDegen.ts +2 -6
  53. package/src/hooks/useMarkers.ts +6 -2
  54. package/src/hooks/useMultiSeriesLinePaths.ts +31 -4
  55. package/src/hooks/useSeriesIndicatorOpacity.ts +56 -0
  56. package/src/hooks/useTradeStream.ts +6 -1
  57. package/src/index.ts +1 -0
  58. package/src/math/simplify.ts +197 -0
  59. package/src/types.ts +78 -16
@@ -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,
@@ -385,8 +392,12 @@ function useLiveChartController({
385
392
  const dotTracksParked =
386
393
  dotCfg.trackWhileParked && !(badgeCfg?.followViewEdge ?? false);
387
394
  const selectionDotCfg = resolveSelectionDot(selectionDot);
388
- // Outer footprint of the dot (color-filled radius plus the halo ring).
389
- 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
+ );
390
401
  const gridStyleCfg = resolveGridStyle(gridStyle);
391
402
  // Static charts run zero loops: force degen off so `useDegen`'s frame callback
392
403
  // never starts (also passed `isStatic` below as a belt-and-braces autostart gate).
@@ -413,12 +424,14 @@ function useLiveChartController({
413
424
  const active = dragActive.get();
414
425
  const cur = dragValues.get();
415
426
  const seeded = seededRef.current;
416
- dragValues.set(allRefLines.map((l, i) => {
417
- const prop = l.value ?? 0;
418
- if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
419
- if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
420
- return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
421
- }));
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
+ );
422
435
  seededRef.current = allRefLines.map((l) => l.value);
423
436
  if (dragActive.get().length !== allRefLines.length) {
424
437
  dragActive.set(allRefLines.map((_, i) => active[i] ?? false));
@@ -653,6 +666,7 @@ function useLiveChartController({
653
666
  formatValue,
654
667
  currentValue: valueLayoutSample,
655
668
  pulse: pulseConfig,
669
+ dotGlow: dotCfg.glow,
656
670
  volumeBandHeight,
657
671
  });
658
672
 
@@ -675,6 +689,10 @@ function useLiveChartController({
675
689
  isStatic,
676
690
  transitionsCfg.reveal,
677
691
  );
692
+ const seriesIndicatorOpacity = useSeriesIndicatorOpacity(
693
+ resolvedSeriesOpacity,
694
+ isStatic || transitionsCfg.reveal === 0 ? 0 : SERIES_INDICATOR_FADE_MS,
695
+ );
678
696
 
679
697
  // After data clears, keep last snapshot until morphT finishes dropping (web parity).
680
698
  const { lineEngineData, candlesEngine, liveEngine } =
@@ -769,7 +787,8 @@ function useLiveChartController({
769
787
  if (
770
788
  referenceLineForm(l) !== "line" ||
771
789
  l.value === undefined ||
772
- refLineCustom[i] || refLineOffAxisCustom[i]
790
+ refLineCustom[i] ||
791
+ refLineOffAxisCustom[i]
773
792
  ) {
774
793
  ys.push(-1);
775
794
  continue;
@@ -926,6 +945,7 @@ function useLiveChartController({
926
945
  thresholdCfg?.fill && thresholdSeriesHasPoints
927
946
  ? thresholdSeriesGeom.samples
928
947
  : undefined,
948
+ lineProp?.simplify,
929
949
  );
930
950
 
931
951
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -968,7 +988,8 @@ function useLiveChartController({
968
988
  const displayCandleWidth = useCandleWidthLerp(
969
989
  candleWidth,
970
990
  transitionsCfg.candleLerpSpeed,
971
- !isStatic && isCandle,
991
+ !isStatic,
992
+ isCandle,
972
993
  );
973
994
 
974
995
  // ── Overlay hooks ─────────────────────────────────────────────────────
@@ -1333,26 +1354,20 @@ function useLiveChartController({
1333
1354
  hideLiveOnScrollBack && !dotTracksParked,
1334
1355
  engine.viewEnd.value,
1335
1356
  ) *
1336
- resolvedSeriesOpacity.value,
1357
+ seriesIndicatorOpacity.value,
1337
1358
  );
1338
1359
  // Same scrolled-back gating for the value line: it would draw a dashed line
1339
1360
  // at the live value's Y — a price that isn't in the scrolled-back view.
1340
1361
  const valueLineOpacity = useDerivedValue(
1341
1362
  () =>
1342
1363
  reveal.lineOpacity.value *
1343
- liveIndicatorScrollOpacity(
1344
- hideLiveOnScrollBack,
1345
- engine.viewEnd.value,
1346
- ) *
1364
+ liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
1347
1365
  resolvedSeriesOpacity.value,
1348
1366
  );
1349
1367
  const liveBadgeOpacity = useDerivedValue(
1350
1368
  () =>
1351
1369
  reveal.badgeOpacity.value *
1352
- liveIndicatorScrollOpacity(
1353
- hideLiveOnScrollBack,
1354
- engine.viewEnd.value,
1355
- ) *
1370
+ liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
1356
1371
  resolvedSeriesOpacity.value,
1357
1372
  );
1358
1373
 
@@ -1536,8 +1551,7 @@ function ChartWithDegen({
1536
1551
  model: LiveChartModel;
1537
1552
  yAxisEntries: YAxisEntries | null;
1538
1553
  }) {
1539
- const { engine, dotX, dotY, momentumSV, degenCfg, onDegenShake, isStatic } =
1540
- model;
1554
+ const { engine, dotX, dotY, momentumSV, degenCfg, onDegenShake } = model;
1541
1555
  const state = useDegen(
1542
1556
  engine,
1543
1557
  dotX,
@@ -1545,7 +1559,6 @@ function ChartWithDegen({
1545
1559
  momentumSV,
1546
1560
  degenCfg,
1547
1561
  onDegenShake,
1548
- isStatic,
1549
1562
  );
1550
1563
  return <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />;
1551
1564
  }
@@ -1778,6 +1791,42 @@ function ChartFillLayer({
1778
1791
  );
1779
1792
  }
1780
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
+
1781
1830
  /**
1782
1831
  * Candle/volume paths are a mode-specific subsystem. Keeping their hooks in a
1783
1832
  * child means a line chart never registers the candle-width frame callback or
@@ -1799,15 +1848,10 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1799
1848
  volumeOpacity,
1800
1849
  volumeUpColor,
1801
1850
  volumeDownColor,
1851
+ scrubCfg,
1852
+ crosshair,
1802
1853
  } = model;
1803
- const {
1804
- upBodiesPath,
1805
- downBodiesPath,
1806
- upWicksPath,
1807
- downWicksPath,
1808
- upBarsPath,
1809
- downBarsPath,
1810
- } = useCandlePaths(
1854
+ const paths = useCandlePaths(
1811
1855
  engine,
1812
1856
  effectivePadding,
1813
1857
  candlesEngine,
@@ -1818,31 +1862,119 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1818
1862
  volumeBandHeight,
1819
1863
  volumeCfg?.radius ?? 0,
1820
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
+ ]);
1821
1939
 
1822
1940
  return (
1823
1941
  <Group opacity={seriesOpacity}>
1824
1942
  <Group opacity={candleGroupOpacity}>
1825
- <Path
1826
- path={upWicksPath}
1827
- style="stroke"
1828
- strokeWidth={metricsCfg.candle.wickWidth}
1829
- color={palette.wickUp}
1830
- />
1831
- <Path
1832
- path={downWicksPath}
1833
- style="stroke"
1834
- strokeWidth={metricsCfg.candle.wickWidth}
1835
- color={palette.wickDown}
1836
- />
1837
- <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
1838
- <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
+ )}
1839
1967
  </Group>
1840
1968
 
1841
1969
  {volumeCfg && (
1842
1970
  <Group opacity={candleGroupOpacity}>
1843
1971
  <Group opacity={volumeOpacity}>
1844
- <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1845
- <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
+ />
1846
1978
  </Group>
1847
1979
  </Group>
1848
1980
  )}
@@ -2069,6 +2201,7 @@ function ChartStack({
2069
2201
  dotY={dotY}
2070
2202
  palette={palette}
2071
2203
  pulse={pulseCfg}
2204
+ glow={dotCfg.glow}
2072
2205
  radius={dotCfg.radius}
2073
2206
  ring={dotCfg.ring}
2074
2207
  color={dotCfg.color}
@@ -2219,6 +2352,10 @@ function ChartScrubLayer({
2219
2352
  // Skia tooltip is suppressed here while it's active — the line pill in line
2220
2353
  // mode, and the OHLC stack in candle mode (see the stack gate below).
2221
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";
2222
2359
 
2223
2360
  if (!scrubCfg) return null;
2224
2361
 
@@ -2243,11 +2380,12 @@ function ChartScrubLayer({
2243
2380
  font={skiaFont}
2244
2381
  showTooltip={scrubCfg.tooltip && !customTooltipActive}
2245
2382
  lineTop={crosshair.tooltipLineTop}
2246
- selectionDot={selectionDot}
2383
+ showLine={dimsFuture}
2384
+ selectionDot={dimsFuture ? selectionDot : null}
2247
2385
  selectionY={crosshair.scrubDotY}
2248
2386
  scrubActive={crosshair.scrubActive}
2249
2387
  selectionColor={selectionColor}
2250
- dimOpacity={scrubCfg.dimOpacity}
2388
+ dimOpacity={dimsFuture ? scrubCfg.dimOpacity : 1}
2251
2389
  liveDotExtent={liveDotExtent}
2252
2390
  crosshairLineColor={scrubCfg.crosshairLineColor}
2253
2391
  crosshairStrokeWidth={scrubCfg.crosshairStrokeWidth}
@@ -2256,7 +2394,9 @@ function ChartScrubLayer({
2256
2394
  crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
2257
2395
  crosshairLineCap={scrubCfg.crosshairLineCap}
2258
2396
  crosshairDash={scrubCfg.crosshairDash}
2259
- crosshairDimColor={scrubCfg.crosshairDimColor}
2397
+ crosshairDimColor={
2398
+ dimsFuture ? scrubCfg.crosshairDimColor : undefined
2399
+ }
2260
2400
  tooltipBackground={scrubCfg.tooltipBackground}
2261
2401
  tooltipColor={scrubCfg.tooltipColor}
2262
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}
@@ -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 }}`.