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.
- package/dist/components/CrosshairOverlay.d.ts +3 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +10 -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/MarkerOverlay.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 +20 -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 +9 -3
- package/dist/draw/line.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +8 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/resolveChartLayout.d.ts +12 -1
- 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/useCrosshair.d.ts +7 -1
- package/dist/hooks/useCrosshair.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/useLiveDot.d.ts +8 -2
- package/dist/hooks/useLiveDot.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/markerCluster.d.ts +6 -0
- package/dist/math/markerCluster.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 +106 -17
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairOverlay.tsx +14 -9
- package/src/components/CustomMarkerOverlay.tsx +5 -1
- package/src/components/DotOverlay.tsx +26 -3
- package/src/components/LiveChart.tsx +210 -55
- package/src/components/LiveChartSeries.tsx +28 -10
- package/src/components/MarkerOverlay.tsx +5 -1
- package/src/components/MultiSeriesDots.tsx +22 -3
- package/src/constants.ts +7 -0
- package/src/core/resolveConfig.ts +67 -13
- package/src/core/useLiveChartEngine.ts +6 -1
- package/src/draw/candle.ts +78 -0
- package/src/draw/line.ts +24 -10
- package/src/hooks/crosshairShared.ts +31 -0
- package/src/hooks/resolveChartLayout.ts +40 -16
- package/src/hooks/useCandlePaths.ts +9 -1
- package/src/hooks/useChartPaths.ts +37 -7
- package/src/hooks/useCrosshair.ts +29 -2
- package/src/hooks/useDegen.ts +2 -6
- package/src/hooks/useLiveDot.ts +28 -3
- package/src/hooks/useMarkers.ts +11 -3
- 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/markerCluster.ts +15 -1
- package/src/math/simplify.ts +197 -0
- 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 {
|
|
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
|
|
385
|
-
|
|
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(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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] ||
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
|
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
|
-
|
|
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={
|
|
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
|
|
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}
|
|
@@ -388,7 +388,11 @@ export function MarkerOverlay({
|
|
|
388
388
|
lineData: lineData?.get(),
|
|
389
389
|
lineLinear,
|
|
390
390
|
});
|
|
391
|
-
clusterMarkers(ms, buf, {
|
|
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
|
-
/**
|
|
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 }}`.
|