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.
- 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 +2 -2
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts +2 -2
- 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 +19 -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/grid.d.ts +1 -1
- package/dist/draw/grid.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/usePinchZoom.d.ts +7 -0
- package/dist/hooks/usePinchZoom.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/hooks/useYAxis.d.ts +1 -1
- package/dist/hooks/useYAxis.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 +101 -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 +227 -67
- package/src/components/LiveChartSeries.tsx +56 -16
- package/src/components/MultiSeriesDots.tsx +22 -3
- package/src/constants.ts +7 -0
- package/src/core/resolveConfig.ts +72 -14
- package/src/core/useLiveChartEngine.ts +6 -1
- package/src/draw/candle.ts +78 -0
- package/src/draw/grid.ts +34 -4
- 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/usePinchZoom.ts +21 -0
- package/src/hooks/useSeriesIndicatorOpacity.ts +56 -0
- package/src/hooks/useTradeStream.ts +6 -1
- package/src/hooks/useYAxis.ts +2 -0
- package/src/index.ts +2 -0
- package/src/math/simplify.ts +197 -0
- package/src/types.ts +103 -16
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
389
|
-
|
|
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(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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] ||
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
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
|
|
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
|
-
|
|
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={
|
|
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
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
250
|
-
// Used to keep
|
|
251
|
-
const dotOuterRadius =
|
|
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={
|
|
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({
|
|
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
|
|
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
|
+
});
|