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.
- package/dist/components/CrosshairOverlay.d.ts +3 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +4 -2
- package/dist/components/DotOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +5 -3
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +17 -3
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +21 -0
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/line.d.ts +7 -1
- package/dist/draw/line.d.ts.map +1 -1
- package/dist/hooks/resolveChartLayout.d.ts +8 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +3 -1
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +3 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +1 -3
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +3 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/useSeriesIndicatorOpacity.d.ts +13 -0
- package/dist/hooks/useSeriesIndicatorOpacity.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/simplify.d.ts +20 -0
- package/dist/math/simplify.d.ts.map +1 -0
- package/dist/types.d.ts +77 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairOverlay.tsx +14 -9
- package/src/components/DotOverlay.tsx +17 -1
- package/src/components/LiveChart.tsx +194 -54
- package/src/components/LiveChartSeries.tsx +28 -10
- package/src/components/MultiSeriesDots.tsx +22 -3
- package/src/constants.ts +7 -0
- package/src/core/resolveConfig.ts +61 -13
- package/src/core/useLiveChartEngine.ts +6 -1
- package/src/draw/candle.ts +78 -0
- package/src/draw/line.ts +20 -7
- package/src/hooks/resolveChartLayout.ts +30 -18
- package/src/hooks/useCandlePaths.ts +9 -1
- package/src/hooks/useChartPaths.ts +37 -7
- package/src/hooks/useDegen.ts +2 -6
- package/src/hooks/useMarkers.ts +6 -2
- package/src/hooks/useMultiSeriesLinePaths.ts +31 -4
- package/src/hooks/useSeriesIndicatorOpacity.ts +56 -0
- package/src/hooks/useTradeStream.ts +6 -1
- package/src/index.ts +1 -0
- package/src/math/simplify.ts +197 -0
- 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 {
|
|
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
|
|
389
|
-
|
|
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(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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] ||
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
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
|
|
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
|
-
|
|
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={
|
|
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
|
|
250
|
-
// Used to keep
|
|
251
|
-
const dotOuterRadius =
|
|
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={
|
|
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({
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 }}`.
|