react-native-livechart 4.18.0 → 4.20.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 (70) 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 +2 -2
  6. package/dist/components/LiveChart.d.ts.map +1 -1
  7. package/dist/components/LiveChartSeries.d.ts +2 -2
  8. package/dist/components/LiveChartSeries.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 +19 -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/grid.d.ts +1 -1
  19. package/dist/draw/grid.d.ts.map +1 -1
  20. package/dist/draw/line.d.ts +7 -1
  21. package/dist/draw/line.d.ts.map +1 -1
  22. package/dist/hooks/resolveChartLayout.d.ts +8 -0
  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/useDegen.d.ts +1 -3
  29. package/dist/hooks/useDegen.d.ts.map +1 -1
  30. package/dist/hooks/useMarkers.d.ts.map +1 -1
  31. package/dist/hooks/useMultiSeriesLinePaths.d.ts +3 -1
  32. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
  33. package/dist/hooks/usePinchZoom.d.ts +7 -0
  34. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  35. package/dist/hooks/useSeriesIndicatorOpacity.d.ts +13 -0
  36. package/dist/hooks/useSeriesIndicatorOpacity.d.ts.map +1 -0
  37. package/dist/hooks/useTradeStream.d.ts.map +1 -1
  38. package/dist/hooks/useYAxis.d.ts +1 -1
  39. package/dist/hooks/useYAxis.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/simplify.d.ts +20 -0
  43. package/dist/math/simplify.d.ts.map +1 -0
  44. package/dist/types.d.ts +101 -16
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/components/CrosshairOverlay.tsx +14 -9
  48. package/src/components/DotOverlay.tsx +17 -1
  49. package/src/components/LiveChart.tsx +227 -67
  50. package/src/components/LiveChartSeries.tsx +56 -16
  51. package/src/components/MultiSeriesDots.tsx +22 -3
  52. package/src/constants.ts +7 -0
  53. package/src/core/resolveConfig.ts +72 -14
  54. package/src/core/useLiveChartEngine.ts +6 -1
  55. package/src/draw/candle.ts +78 -0
  56. package/src/draw/grid.ts +34 -4
  57. package/src/draw/line.ts +20 -7
  58. package/src/hooks/resolveChartLayout.ts +30 -18
  59. package/src/hooks/useCandlePaths.ts +9 -1
  60. package/src/hooks/useChartPaths.ts +37 -7
  61. package/src/hooks/useDegen.ts +2 -6
  62. package/src/hooks/useMarkers.ts +6 -2
  63. package/src/hooks/useMultiSeriesLinePaths.ts +31 -4
  64. package/src/hooks/usePinchZoom.ts +21 -0
  65. package/src/hooks/useSeriesIndicatorOpacity.ts +56 -0
  66. package/src/hooks/useTradeStream.ts +6 -1
  67. package/src/hooks/useYAxis.ts +2 -0
  68. package/src/index.ts +2 -0
  69. package/src/math/simplify.ts +197 -0
  70. package/src/types.ts +103 -16
@@ -1,4 +1,12 @@
1
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ forwardRef,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
2
10
  import { StyleSheet, View } from "react-native";
3
11
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
12
  import Animated, {
@@ -11,7 +19,7 @@ import Animated, {
11
19
  withSequence,
12
20
  withTiming,
13
21
  } from "react-native-reanimated";
14
- import { scheduleOnRN } from "react-native-worklets";
22
+ import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
15
23
 
16
24
  /**
17
25
  * Single-series live chart. UX and prop vocabulary parallel Benji Taylor’s
@@ -69,7 +77,12 @@ import {
69
77
  } from "../core/liveIndicatorVisibility";
70
78
  import { resolveSegment, type ResolvedSegment } from "../core/resolveSegment";
71
79
  import { useLiveChartEngine } from "../core/useLiveChartEngine";
72
- import { pulseRadialOutset } from "../draw/line";
80
+ import {
81
+ computeCandleFocusPassOpacity,
82
+ computeCandleFocusClip,
83
+ HIDDEN_CANDLE_FOCUS_CLIP,
84
+ } from "../draw/candle";
85
+ import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
73
86
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
74
87
  import { useBadge } from "../hooks/useBadge";
75
88
  import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
@@ -87,12 +100,13 @@ import { useMarkers } from "../hooks/useMarkers";
87
100
  import { useReferenceDrag } from "../hooks/useReferenceDrag";
88
101
  import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
89
102
  import { useModeBlend } from "../hooks/useModeBlend";
90
- import {
91
- resolveMomentumProp,
92
- useMomentum,
93
- } from "../hooks/useMomentum";
103
+ import { resolveMomentumProp, useMomentum } from "../hooks/useMomentum";
94
104
  import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
95
- import { usePinchZoom } from "../hooks/usePinchZoom";
105
+ import { resetPinchZoom, usePinchZoom } from "../hooks/usePinchZoom";
106
+ import {
107
+ SERIES_INDICATOR_FADE_MS,
108
+ useSeriesIndicatorOpacity,
109
+ } from "../hooks/useSeriesIndicatorOpacity";
96
110
  import { useVisibleRange } from "../hooks/useVisibleRange";
97
111
  import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
98
112
  import {
@@ -127,6 +141,8 @@ import {
127
141
  resolveTheme,
128
142
  } from "../theme";
129
143
  import type {
144
+ CandlePoint,
145
+ LiveChartHandle,
130
146
  LiveChartPalette,
131
147
  LiveChartPoint,
132
148
  LiveChartProps,
@@ -385,8 +401,12 @@ function useLiveChartController({
385
401
  const dotTracksParked =
386
402
  dotCfg.trackWhileParked && !(badgeCfg?.followViewEdge ?? false);
387
403
  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);
404
+ // Outer visible footprint of the dot, including its crisp ring and optional
405
+ // blurred glow. Used by scrub dimming so neither effect leaks through.
406
+ const dotOuterRadius = Math.max(
407
+ dotCfg.radius + (dotCfg.ring?.width ?? 0),
408
+ dotCfg.glow ? dotGlowRadialOutset(dotCfg.glow.radius, dotCfg.glow.blur) : 0,
409
+ );
390
410
  const gridStyleCfg = resolveGridStyle(gridStyle);
391
411
  // Static charts run zero loops: force degen off so `useDegen`'s frame callback
392
412
  // never starts (also passed `isStatic` below as a belt-and-braces autostart gate).
@@ -413,12 +433,14 @@ function useLiveChartController({
413
433
  const active = dragActive.get();
414
434
  const cur = dragValues.get();
415
435
  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
- }));
436
+ dragValues.set(
437
+ allRefLines.map((l, i) => {
438
+ const prop = l.value ?? 0;
439
+ if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
440
+ if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
441
+ return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
442
+ }),
443
+ );
422
444
  seededRef.current = allRefLines.map((l) => l.value);
423
445
  if (dragActive.get().length !== allRefLines.length) {
424
446
  dragActive.set(allRefLines.map((_, i) => active[i] ?? false));
@@ -653,6 +675,7 @@ function useLiveChartController({
653
675
  formatValue,
654
676
  currentValue: valueLayoutSample,
655
677
  pulse: pulseConfig,
678
+ dotGlow: dotCfg.glow,
656
679
  volumeBandHeight,
657
680
  });
658
681
 
@@ -675,6 +698,10 @@ function useLiveChartController({
675
698
  isStatic,
676
699
  transitionsCfg.reveal,
677
700
  );
701
+ const seriesIndicatorOpacity = useSeriesIndicatorOpacity(
702
+ resolvedSeriesOpacity,
703
+ isStatic || transitionsCfg.reveal === 0 ? 0 : SERIES_INDICATOR_FADE_MS,
704
+ );
678
705
 
679
706
  // After data clears, keep last snapshot until morphT finishes dropping (web parity).
680
707
  const { lineEngineData, candlesEngine, liveEngine } =
@@ -769,7 +796,8 @@ function useLiveChartController({
769
796
  if (
770
797
  referenceLineForm(l) !== "line" ||
771
798
  l.value === undefined ||
772
- refLineCustom[i] || refLineOffAxisCustom[i]
799
+ refLineCustom[i] ||
800
+ refLineOffAxisCustom[i]
773
801
  ) {
774
802
  ys.push(-1);
775
803
  continue;
@@ -926,6 +954,7 @@ function useLiveChartController({
926
954
  thresholdCfg?.fill && thresholdSeriesHasPoints
927
955
  ? thresholdSeriesGeom.samples
928
956
  : undefined,
957
+ lineProp?.simplify,
929
958
  );
930
959
 
931
960
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -968,7 +997,8 @@ function useLiveChartController({
968
997
  const displayCandleWidth = useCandleWidthLerp(
969
998
  candleWidth,
970
999
  transitionsCfg.candleLerpSpeed,
971
- !isStatic && isCandle,
1000
+ !isStatic,
1001
+ isCandle,
972
1002
  );
973
1003
 
974
1004
  // ── Overlay hooks ─────────────────────────────────────────────────────
@@ -1333,26 +1363,20 @@ function useLiveChartController({
1333
1363
  hideLiveOnScrollBack && !dotTracksParked,
1334
1364
  engine.viewEnd.value,
1335
1365
  ) *
1336
- resolvedSeriesOpacity.value,
1366
+ seriesIndicatorOpacity.value,
1337
1367
  );
1338
1368
  // Same scrolled-back gating for the value line: it would draw a dashed line
1339
1369
  // at the live value's Y — a price that isn't in the scrolled-back view.
1340
1370
  const valueLineOpacity = useDerivedValue(
1341
1371
  () =>
1342
1372
  reveal.lineOpacity.value *
1343
- liveIndicatorScrollOpacity(
1344
- hideLiveOnScrollBack,
1345
- engine.viewEnd.value,
1346
- ) *
1373
+ liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
1347
1374
  resolvedSeriesOpacity.value,
1348
1375
  );
1349
1376
  const liveBadgeOpacity = useDerivedValue(
1350
1377
  () =>
1351
1378
  reveal.badgeOpacity.value *
1352
- liveIndicatorScrollOpacity(
1353
- hideLiveOnScrollBack,
1354
- engine.viewEnd.value,
1355
- ) *
1379
+ liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
1356
1380
  resolvedSeriesOpacity.value,
1357
1381
  );
1358
1382
 
@@ -1536,8 +1560,7 @@ function ChartWithDegen({
1536
1560
  model: LiveChartModel;
1537
1561
  yAxisEntries: YAxisEntries | null;
1538
1562
  }) {
1539
- const { engine, dotX, dotY, momentumSV, degenCfg, onDegenShake, isStatic } =
1540
- model;
1563
+ const { engine, dotX, dotY, momentumSV, degenCfg, onDegenShake } = model;
1541
1564
  const state = useDegen(
1542
1565
  engine,
1543
1566
  dotX,
@@ -1545,7 +1568,6 @@ function ChartWithDegen({
1545
1568
  momentumSV,
1546
1569
  degenCfg,
1547
1570
  onDegenShake,
1548
- isStatic,
1549
1571
  );
1550
1572
  return <ChartView model={model} yAxisEntries={yAxisEntries} degen={state} />;
1551
1573
  }
@@ -1573,6 +1595,7 @@ function ChartWithYAxis({ model }: { model: LiveChartModel }) {
1573
1595
  yAxisCfg?.minGap ?? 36,
1574
1596
  metricsCfg.grid,
1575
1597
  yAxisCfg?.count ?? 0,
1598
+ yAxisCfg?.intervalScale ?? 1,
1576
1599
  );
1577
1600
  if (model.degenCfg) {
1578
1601
  return <ChartWithDegen model={model} yAxisEntries={yAxisEntries} />;
@@ -1778,6 +1801,42 @@ function ChartFillLayer({
1778
1801
  );
1779
1802
  }
1780
1803
 
1804
+ type CandlePaths = ReturnType<typeof useCandlePaths>;
1805
+
1806
+ /** One batched candle pass: two body paths and two wick paths. */
1807
+ function CandlePathBatch({
1808
+ paths,
1809
+ wickWidth,
1810
+ palette,
1811
+ }: {
1812
+ paths: CandlePaths;
1813
+ wickWidth: number;
1814
+ palette: LiveChartPalette;
1815
+ }) {
1816
+ return (
1817
+ <>
1818
+ <Path
1819
+ path={paths.upWicksPath}
1820
+ style="stroke"
1821
+ strokeWidth={wickWidth}
1822
+ color={palette.wickUp}
1823
+ />
1824
+ <Path
1825
+ path={paths.downWicksPath}
1826
+ style="stroke"
1827
+ strokeWidth={wickWidth}
1828
+ color={palette.wickDown}
1829
+ />
1830
+ <Path path={paths.upBodiesPath} style="fill" color={palette.candleUp} />
1831
+ <Path
1832
+ path={paths.downBodiesPath}
1833
+ style="fill"
1834
+ color={palette.candleDown}
1835
+ />
1836
+ </>
1837
+ );
1838
+ }
1839
+
1781
1840
  /**
1782
1841
  * Candle/volume paths are a mode-specific subsystem. Keeping their hooks in a
1783
1842
  * child means a line chart never registers the candle-width frame callback or
@@ -1799,15 +1858,10 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1799
1858
  volumeOpacity,
1800
1859
  volumeUpColor,
1801
1860
  volumeDownColor,
1861
+ scrubCfg,
1862
+ crosshair,
1802
1863
  } = model;
1803
- const {
1804
- upBodiesPath,
1805
- downBodiesPath,
1806
- upWicksPath,
1807
- downWicksPath,
1808
- upBarsPath,
1809
- downBarsPath,
1810
- } = useCandlePaths(
1864
+ const paths = useCandlePaths(
1811
1865
  engine,
1812
1866
  effectivePadding,
1813
1867
  candlesEngine,
@@ -1818,31 +1872,119 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
1818
1872
  volumeBandHeight,
1819
1873
  volumeCfg?.radius ?? 0,
1820
1874
  );
1875
+ const focusOtherCandles = scrubCfg?.dimTarget === "otherCandles";
1876
+ const candleDimOpacity = scrubCfg?.dimOpacity ?? 1;
1877
+ const candleDimFadeMs = scrubCfg?.dimFadeMs ?? 0;
1878
+ // Destructure the two SharedValues before entering worklets. Closing over the
1879
+ // whole crosshair object would also serialize its RNGH gesture instances.
1880
+ const scrubActive = crosshair.scrubActive;
1881
+ // ChartCandleLayer is single-series only; useCrosshair always supplies this
1882
+ // value (it is optional on the shared state type only for LiveChartSeries).
1883
+ const scrubCandle = crosshair.scrubCandle!;
1884
+ // Keep the last selection after scrubCandle clears so its clipped full-
1885
+ // strength pass can cover the base batch until the release fade completes.
1886
+ const lastScrubCandle = useSharedValue<CandlePoint | null>(null);
1887
+ useAnimatedReaction(
1888
+ () => (focusOtherCandles ? scrubCandle.get() : null),
1889
+ (candle) => {
1890
+ if (candle) lastScrubCandle.set(candle);
1891
+ },
1892
+ );
1893
+ const inactiveCandleOpacity = useDerivedValue(() => {
1894
+ const target =
1895
+ focusOtherCandles && scrubActive.get() && scrubCandle.get()
1896
+ ? candleDimOpacity
1897
+ : 1;
1898
+ return candleDimFadeMs > 0
1899
+ ? withTiming(target, { duration: candleDimFadeMs })
1900
+ : target;
1901
+ }, [
1902
+ focusOtherCandles,
1903
+ scrubActive,
1904
+ scrubCandle,
1905
+ candleDimOpacity,
1906
+ candleDimFadeMs,
1907
+ ]);
1908
+ const focusedCandleOpacity = useDerivedValue(() => {
1909
+ const currentCandle = scrubCandle.get();
1910
+ const focusCandle = currentCandle ?? lastScrubCandle.get();
1911
+ if (!focusOtherCandles || !focusCandle) return 0;
1912
+ return computeCandleFocusPassOpacity(
1913
+ scrubActive.get(),
1914
+ currentCandle !== null,
1915
+ inactiveCandleOpacity.get(),
1916
+ );
1917
+ }, [
1918
+ focusOtherCandles,
1919
+ scrubActive,
1920
+ scrubCandle,
1921
+ lastScrubCandle,
1922
+ inactiveCandleOpacity,
1923
+ ]);
1924
+ const focusedCandleClip = useDerivedValue(() => {
1925
+ if (!focusOtherCandles) {
1926
+ return HIDDEN_CANDLE_FOCUS_CLIP;
1927
+ }
1928
+ const focusCandle = scrubCandle.get() ?? lastScrubCandle.get();
1929
+ return computeCandleFocusClip(
1930
+ focusCandle,
1931
+ effectivePadding,
1932
+ engine.canvasWidth.get(),
1933
+ engine.canvasHeight.get(),
1934
+ engine.timestamp.get() - engine.displayWindow.get(),
1935
+ engine.displayWindow.get(),
1936
+ displayCandleWidth.get(),
1937
+ );
1938
+ }, [
1939
+ focusOtherCandles,
1940
+ scrubCandle,
1941
+ lastScrubCandle,
1942
+ effectivePadding,
1943
+ engine.canvasWidth,
1944
+ engine.canvasHeight,
1945
+ engine.timestamp,
1946
+ engine.displayWindow,
1947
+ displayCandleWidth,
1948
+ ]);
1821
1949
 
1822
1950
  return (
1823
1951
  <Group opacity={seriesOpacity}>
1824
1952
  <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} />
1953
+ {focusOtherCandles ? (
1954
+ <>
1955
+ <Group opacity={inactiveCandleOpacity}>
1956
+ <CandlePathBatch
1957
+ paths={paths}
1958
+ wickWidth={metricsCfg.candle.wickWidth}
1959
+ palette={palette}
1960
+ />
1961
+ </Group>
1962
+ <Group opacity={focusedCandleOpacity} clip={focusedCandleClip}>
1963
+ <CandlePathBatch
1964
+ paths={paths}
1965
+ wickWidth={metricsCfg.candle.wickWidth}
1966
+ palette={palette}
1967
+ />
1968
+ </Group>
1969
+ </>
1970
+ ) : (
1971
+ <CandlePathBatch
1972
+ paths={paths}
1973
+ wickWidth={metricsCfg.candle.wickWidth}
1974
+ palette={palette}
1975
+ />
1976
+ )}
1839
1977
  </Group>
1840
1978
 
1841
1979
  {volumeCfg && (
1842
1980
  <Group opacity={candleGroupOpacity}>
1843
1981
  <Group opacity={volumeOpacity}>
1844
- <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1845
- <Path path={downBarsPath} style="fill" color={volumeDownColor} />
1982
+ <Path path={paths.upBarsPath} style="fill" color={volumeUpColor} />
1983
+ <Path
1984
+ path={paths.downBarsPath}
1985
+ style="fill"
1986
+ color={volumeDownColor}
1987
+ />
1846
1988
  </Group>
1847
1989
  </Group>
1848
1990
  )}
@@ -2069,6 +2211,7 @@ function ChartStack({
2069
2211
  dotY={dotY}
2070
2212
  palette={palette}
2071
2213
  pulse={pulseCfg}
2214
+ glow={dotCfg.glow}
2072
2215
  radius={dotCfg.radius}
2073
2216
  ring={dotCfg.ring}
2074
2217
  color={dotCfg.color}
@@ -2219,6 +2362,10 @@ function ChartScrubLayer({
2219
2362
  // Skia tooltip is suppressed here while it's active — the line pill in line
2220
2363
  // mode, and the OHLC stack in candle mode (see the stack gate below).
2221
2364
  const customTooltipActive = renderTooltip != null;
2365
+ // `otherCandles` is candle-only. If a consumer switches this chart to line
2366
+ // mode without also rewriting its scrub config, preserve the standard line
2367
+ // scrub (guide, selection dot, and trailing fade).
2368
+ const dimsFuture = !isCandle || scrubCfg?.dimTarget === "future";
2222
2369
 
2223
2370
  if (!scrubCfg) return null;
2224
2371
 
@@ -2243,11 +2390,12 @@ function ChartScrubLayer({
2243
2390
  font={skiaFont}
2244
2391
  showTooltip={scrubCfg.tooltip && !customTooltipActive}
2245
2392
  lineTop={crosshair.tooltipLineTop}
2246
- selectionDot={selectionDot}
2393
+ showLine={dimsFuture}
2394
+ selectionDot={dimsFuture ? selectionDot : null}
2247
2395
  selectionY={crosshair.scrubDotY}
2248
2396
  scrubActive={crosshair.scrubActive}
2249
2397
  selectionColor={selectionColor}
2250
- dimOpacity={scrubCfg.dimOpacity}
2398
+ dimOpacity={dimsFuture ? scrubCfg.dimOpacity : 1}
2251
2399
  liveDotExtent={liveDotExtent}
2252
2400
  crosshairLineColor={scrubCfg.crosshairLineColor}
2253
2401
  crosshairStrokeWidth={scrubCfg.crosshairStrokeWidth}
@@ -2256,7 +2404,9 @@ function ChartScrubLayer({
2256
2404
  crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
2257
2405
  crosshairLineCap={scrubCfg.crosshairLineCap}
2258
2406
  crosshairDash={scrubCfg.crosshairDash}
2259
- crosshairDimColor={scrubCfg.crosshairDimColor}
2407
+ crosshairDimColor={
2408
+ dimsFuture ? scrubCfg.crosshairDimColor : undefined
2409
+ }
2260
2410
  tooltipBackground={scrubCfg.tooltipBackground}
2261
2411
  tooltipColor={scrubCfg.tooltipColor}
2262
2412
  tooltipBorderColor={scrubCfg.tooltipBorderColor}
@@ -2771,13 +2921,23 @@ function ChartView({
2771
2921
  );
2772
2922
  }
2773
2923
 
2774
- export function LiveChart(props: LiveChartProps) {
2775
- const model = useLiveChartController(props);
2776
- if (model.yAxisCfg) {
2777
- return <ChartWithYAxis model={model} />;
2778
- }
2779
- if (model.degenCfg) {
2780
- return <ChartWithDegen model={model} yAxisEntries={null} />;
2781
- }
2782
- return <ChartView model={model} yAxisEntries={null} degen={null} />;
2783
- }
2924
+ export const LiveChart = forwardRef<LiveChartHandle, LiveChartProps>(
2925
+ function LiveChart(props, ref) {
2926
+ const model = useLiveChartController(props);
2927
+ const { viewEnd, viewWindow } = model.engine;
2928
+ useImperativeHandle(
2929
+ ref,
2930
+ () => ({
2931
+ resetZoom: () => scheduleOnUI(resetPinchZoom, { viewEnd, viewWindow }),
2932
+ }),
2933
+ [viewEnd, viewWindow],
2934
+ );
2935
+ if (model.yAxisCfg) {
2936
+ return <ChartWithYAxis model={model} />;
2937
+ }
2938
+ if (model.degenCfg) {
2939
+ return <ChartWithDegen model={model} yAxisEntries={null} />;
2940
+ }
2941
+ return <ChartView model={model} yAxisEntries={null} degen={null} />;
2942
+ },
2943
+ );
@@ -5,7 +5,12 @@
5
5
  * @see https://github.com/benjitaylor/liveline
6
6
  */
7
7
  import { Canvas, Group, Rect } from "@shopify/react-native-skia";
8
- import { useLayoutEffect, useState } from "react";
8
+ import {
9
+ forwardRef,
10
+ useImperativeHandle,
11
+ useLayoutEffect,
12
+ useState,
13
+ } from "react";
9
14
  import { StyleSheet, View } from "react-native";
10
15
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
11
16
  import Animated, {
@@ -16,7 +21,7 @@ import Animated, {
16
21
  withTiming,
17
22
  type SharedValue,
18
23
  } from "react-native-reanimated";
19
- import { scheduleOnRN } from "react-native-worklets";
24
+ import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
20
25
  import {
21
26
  DEFAULT_ACCENT_COLOR,
22
27
  HOLD_TO_SCRUB_MS,
@@ -50,7 +55,7 @@ import {
50
55
  resolveZoom,
51
56
  } from "../core/resolveConfig";
52
57
  import { useLiveChartSeriesEngine } from "../core/useLiveChartSeriesEngine";
53
- import { pulseRadialOutset } from "../draw/line";
58
+ import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
54
59
  import { resolveChartLayout } from "../hooks/resolveChartLayout";
55
60
  import { useCanvasLayout } from "../hooks/useCanvasLayout";
56
61
  import { useChartReveal } from "../hooks/useChartReveal";
@@ -62,8 +67,12 @@ import { useMarkers } from "../hooks/useMarkers";
62
67
  import { useMultiSeriesDegen } from "../hooks/useMultiSeriesDegen";
63
68
  import { useMultiSeriesLinePaths } from "../hooks/useMultiSeriesLinePaths";
64
69
  import { usePanScroll } from "../hooks/usePanScroll";
65
- import { usePinchZoom } from "../hooks/usePinchZoom";
70
+ import { resetPinchZoom, usePinchZoom } from "../hooks/usePinchZoom";
66
71
  import { useMultiSeriesReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
72
+ import {
73
+ SERIES_INDICATOR_FADE_MS,
74
+ useSeriesIndicatorOpacity,
75
+ } from "../hooks/useSeriesIndicatorOpacity";
67
76
  import { useVisibleRange } from "../hooks/useVisibleRange";
68
77
  import { useXAxis } from "../hooks/useXAxis";
69
78
  import { useYAxis } from "../hooks/useYAxis";
@@ -82,7 +91,12 @@ import {
82
91
  leftEdgeFadeColorsFromBgRgb,
83
92
  resolveTheme,
84
93
  } from "../theme";
85
- import type { LiveChartSeriesProps, Marker, SeriesConfig } from "../types";
94
+ import type {
95
+ LiveChartHandle,
96
+ LiveChartSeriesProps,
97
+ Marker,
98
+ SeriesConfig,
99
+ } from "../types";
86
100
  import { AxisLabelOverlay } from "./AxisLabelOverlay";
87
101
  import { ChartOverlayLayer } from "./ChartOverlayLayer";
88
102
  import {
@@ -246,9 +260,12 @@ function useLiveChartSeriesController({
246
260
  const selectionDotCfg = resolveSelectionDot(selectionDot ?? false);
247
261
  const gridStyleCfg = resolveGridStyle(gridStyle);
248
262
  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);
263
+ // Outer visible footprint of a dot, including its crisp ring and optional
264
+ // blurred glow. Used to keep gutter labels clear of every dot effect.
265
+ const dotOuterRadius = Math.max(
266
+ dotCfg.radius + (dotCfg.ring?.width ?? 0),
267
+ dotCfg.glow ? dotGlowRadialOutset(dotCfg.glow.radius, dotCfg.glow.blur) : 0,
268
+ );
252
269
  const legendCfg = resolveLegend(legendProp);
253
270
  const degenCfg = resolveDegen(degen);
254
271
  const metricsCfg = resolveMetrics(metrics);
@@ -341,6 +358,7 @@ function useLiveChartSeriesController({
341
358
  formatValue,
342
359
  currentValue: representativeValue,
343
360
  pulse: dotCfg.pulse,
361
+ dotGlow: dotCfg.glow,
344
362
  multiSeriesDotRadius: dotOuterRadius,
345
363
  multiSeriesValueLabel: dotCfg.valueLabel,
346
364
  multiSeriesMaxLabelWidth: maxSeriesLabelWidth,
@@ -367,6 +385,10 @@ function useLiveChartSeriesController({
367
385
  false,
368
386
  transitionsCfg.reveal,
369
387
  );
388
+ const seriesIndicatorOpacity = useSeriesIndicatorOpacity(
389
+ resolvedSeriesOpacity,
390
+ transitionsCfg.reveal === 0 ? 0 : SERIES_INDICATOR_FADE_MS,
391
+ );
370
392
 
371
393
  const effectiveSeries = useMultiSeriesReverseMorphInputs({
372
394
  series,
@@ -397,6 +419,7 @@ function useLiveChartSeriesController({
397
419
  engine,
398
420
  effectivePadding,
399
421
  activeSeriesCount,
422
+ lineProp?.simplify,
400
423
  );
401
424
 
402
425
  // Per-series colors and stroke styles, derived from the off-render snapshot
@@ -437,6 +460,7 @@ function useLiveChartSeriesController({
437
460
  yAxisCfg?.minGap ?? 36,
438
461
  metricsCfg.grid,
439
462
  yAxisCfg?.count ?? 0,
463
+ yAxisCfg?.intervalScale ?? 1,
440
464
  );
441
465
 
442
466
  const { xAxisEntries } = useXAxis(
@@ -597,6 +621,7 @@ function useLiveChartSeriesController({
597
621
  // passthrough props the render needs
598
622
  series,
599
623
  seriesOpacity: resolvedSeriesOpacity,
624
+ seriesIndicatorOpacity,
600
625
  style,
601
626
  canvasMode,
602
627
  accessibilityLabel,
@@ -709,6 +734,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
709
734
  renderMarker,
710
735
  series,
711
736
  seriesOpacity,
737
+ seriesIndicatorOpacity,
712
738
  emptyText,
713
739
  loadingAxisLabels,
714
740
  metricsCfg,
@@ -798,7 +824,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
798
824
  )}
799
825
 
800
826
  {dotCfg.show && (
801
- <Group opacity={seriesOpacity}>
827
+ <Group opacity={seriesIndicatorOpacity}>
802
828
  <Group opacity={reveal.dotOpacity}>
803
829
  <MultiSeriesDots
804
830
  engine={engine}
@@ -806,6 +832,7 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
806
832
  colors={lineColors}
807
833
  radius={dotCfg.radius}
808
834
  ring={dotCfg.ring}
835
+ glow={dotCfg.glow}
809
836
  ringColor={palette.badgeOuterBg}
810
837
  color={dotCfg.color}
811
838
  pulse={dotCfg.pulse}
@@ -953,7 +980,11 @@ function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
953
980
  }
954
981
 
955
982
  /** Owns the price/time projection worklets for the multi-series custom overlay. */
956
- function SeriesCustomConsumerOverlay({ model }: { model: LiveChartSeriesModel }) {
983
+ function SeriesCustomConsumerOverlay({
984
+ model,
985
+ }: {
986
+ model: LiveChartSeriesModel;
987
+ }) {
957
988
  const { engine, effectivePadding, renderOverlay } = model;
958
989
  const overlayContext = useChartOverlayContext(engine, effectivePadding);
959
990
  return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
@@ -1006,8 +1037,19 @@ function SeriesTooltipLayer({
1006
1037
  );
1007
1038
  }
1008
1039
 
1009
- export function LiveChartSeries(props: LiveChartSeriesProps) {
1040
+ export const LiveChartSeries = forwardRef<
1041
+ LiveChartHandle,
1042
+ LiveChartSeriesProps
1043
+ >(function LiveChartSeries(props, ref) {
1010
1044
  const model = useLiveChartSeriesController(props);
1045
+ const { viewEnd, viewWindow } = model.engine;
1046
+ useImperativeHandle(
1047
+ ref,
1048
+ () => ({
1049
+ resetZoom: () => scheduleOnUI(resetPinchZoom, { viewEnd, viewWindow }),
1050
+ }),
1051
+ [viewEnd, viewWindow],
1052
+ );
1011
1053
  const {
1012
1054
  rootGesture,
1013
1055
  backgroundColor,
@@ -1146,12 +1188,10 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
1146
1188
  dimOpacity={scrubCfg.dimOpacity}
1147
1189
  liveDotExtent={liveDotExtent}
1148
1190
  crosshairLineColor={
1149
- seriesTooltipCfg?.guideColor ??
1150
- scrubCfg.crosshairLineColor
1191
+ seriesTooltipCfg?.guideColor ?? scrubCfg.crosshairLineColor
1151
1192
  }
1152
1193
  crosshairStrokeWidth={
1153
- seriesTooltipCfg?.guideWidth ??
1154
- scrubCfg.crosshairStrokeWidth
1194
+ seriesTooltipCfg?.guideWidth ?? scrubCfg.crosshairStrokeWidth
1155
1195
  }
1156
1196
  crosshairOvershoot={scrubCfg.crosshairOvershoot}
1157
1197
  crosshairFade={scrubCfg.crosshairFade}
@@ -1255,4 +1295,4 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
1255
1295
  {legendCfg.position === "bottom" ? legend : null}
1256
1296
  </View>
1257
1297
  );
1258
- }
1298
+ });