react-native-livechart 4.19.0 → 4.21.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/CustomThresholdBadgeOverlay.d.ts +18 -0
- package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +4 -2
- package/dist/components/CustomTooltipOverlay.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/ReferenceLineOverlay.d.ts +9 -4
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +6 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +34 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +3 -1
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/candleGap.d.ts +14 -0
- package/dist/draw/candleGap.d.ts.map +1 -0
- package/dist/draw/grid.d.ts +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/lineGap.d.ts +25 -0
- package/dist/draw/lineGap.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +7 -3
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCandleGapPaths.d.ts +12 -0
- package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +4 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -5
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLineGapPaths.d.ts +10 -0
- package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +7 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +4 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
- package/dist/hooks/useThreshold.d.ts +13 -13
- package/dist/hooks/useThreshold.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/candleGaps.d.ts +18 -0
- package/dist/math/candleGaps.d.ts.map +1 -0
- package/dist/math/referenceLineSeries.d.ts +12 -0
- package/dist/math/referenceLineSeries.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +5 -5
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/math/spline.d.ts +5 -1
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +212 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
- package/src/components/CustomTooltipOverlay.tsx +7 -3
- package/src/components/LiveChart.tsx +353 -56
- package/src/components/LiveChartSeries.tsx +28 -6
- package/src/components/ReferenceLineOverlay.tsx +38 -7
- package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
- package/src/core/liveChartEngineTick.ts +34 -1
- package/src/core/resolveConfig.ts +197 -1
- package/src/core/useLiveChartEngine.ts +37 -5
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- package/src/draw/grid.ts +34 -4
- package/src/draw/lineGap.ts +180 -0
- package/src/hooks/crosshairShared.ts +92 -11
- package/src/hooks/useCandleGapPaths.ts +88 -0
- package/src/hooks/useChartPaths.ts +81 -22
- package/src/hooks/useCrosshair.ts +126 -40
- package/src/hooks/useLineGapPaths.ts +76 -0
- package/src/hooks/usePinchZoom.ts +21 -0
- package/src/hooks/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useThreshold.ts +31 -22
- package/src/hooks/useYAxis.ts +2 -0
- package/src/index.ts +16 -0
- package/src/math/candleGaps.ts +120 -0
- package/src/math/referenceLineSeries.ts +83 -0
- package/src/math/referenceLines.ts +34 -6
- package/src/math/spline.ts +15 -5
- package/src/types.ts +224 -14
|
@@ -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, {
|
|
@@ -10,8 +18,9 @@ import Animated, {
|
|
|
10
18
|
withDelay,
|
|
11
19
|
withSequence,
|
|
12
20
|
withTiming,
|
|
21
|
+
type SharedValue,
|
|
13
22
|
} from "react-native-reanimated";
|
|
14
|
-
import { scheduleOnRN } from "react-native-worklets";
|
|
23
|
+
import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Single-series live chart. UX and prop vocabulary parallel Benji Taylor’s
|
|
@@ -37,6 +46,7 @@ import {
|
|
|
37
46
|
resolveAreaDots,
|
|
38
47
|
resolveAxisLabel,
|
|
39
48
|
resolveBadge,
|
|
49
|
+
resolveCandleGaps,
|
|
40
50
|
resolveDegen,
|
|
41
51
|
resolveDot,
|
|
42
52
|
resolveGradient,
|
|
@@ -62,7 +72,10 @@ import {
|
|
|
62
72
|
resolveXAxis,
|
|
63
73
|
resolveYAxis,
|
|
64
74
|
} from "../core/resolveConfig";
|
|
65
|
-
import type {
|
|
75
|
+
import type {
|
|
76
|
+
ResolvedCandleGapsConfig,
|
|
77
|
+
ResolvedThresholdConfig,
|
|
78
|
+
} from "../core/resolveConfig";
|
|
66
79
|
import {
|
|
67
80
|
liveIndicatorScrollOpacity,
|
|
68
81
|
resolveHideLiveOnScrollBack,
|
|
@@ -77,6 +90,7 @@ import {
|
|
|
77
90
|
import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
|
|
78
91
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
79
92
|
import { useBadge } from "../hooks/useBadge";
|
|
93
|
+
import { useCandleGapPaths } from "../hooks/useCandleGapPaths";
|
|
80
94
|
import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
|
|
81
95
|
import { useCanvasLayout } from "../hooks/useCanvasLayout";
|
|
82
96
|
import { useChartColors } from "../hooks/useChartColors";
|
|
@@ -88,13 +102,14 @@ import { useCrosshair } from "../hooks/useCrosshair";
|
|
|
88
102
|
import { useDegen } from "../hooks/useDegen";
|
|
89
103
|
import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
|
|
90
104
|
import { useLiveDot } from "../hooks/useLiveDot";
|
|
105
|
+
import { useLineGapPaths } from "../hooks/useLineGapPaths";
|
|
91
106
|
import { useMarkers } from "../hooks/useMarkers";
|
|
92
107
|
import { useReferenceDrag } from "../hooks/useReferenceDrag";
|
|
93
108
|
import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
|
|
94
109
|
import { useModeBlend } from "../hooks/useModeBlend";
|
|
95
110
|
import { resolveMomentumProp, useMomentum } from "../hooks/useMomentum";
|
|
96
111
|
import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
|
|
97
|
-
import { usePinchZoom } from "../hooks/usePinchZoom";
|
|
112
|
+
import { resetPinchZoom, usePinchZoom } from "../hooks/usePinchZoom";
|
|
98
113
|
import {
|
|
99
114
|
SERIES_INDICATOR_FADE_MS,
|
|
100
115
|
useSeriesIndicatorOpacity,
|
|
@@ -114,6 +129,10 @@ import {
|
|
|
114
129
|
formatValue as defaultFormatValue,
|
|
115
130
|
} from "../lib/format";
|
|
116
131
|
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
|
|
132
|
+
import {
|
|
133
|
+
candleGapBucketStartAtTime,
|
|
134
|
+
candleGapDefaultLabel,
|
|
135
|
+
} from "../math/candleGaps";
|
|
117
136
|
import { computeScrubDotY } from "../hooks/crosshairShared";
|
|
118
137
|
import {
|
|
119
138
|
groupReferenceLines,
|
|
@@ -134,11 +153,14 @@ import {
|
|
|
134
153
|
} from "../theme";
|
|
135
154
|
import type {
|
|
136
155
|
CandlePoint,
|
|
156
|
+
LiveChartHandle,
|
|
137
157
|
LiveChartPalette,
|
|
138
158
|
LiveChartPoint,
|
|
139
159
|
LiveChartProps,
|
|
140
160
|
Marker,
|
|
161
|
+
ReferenceLine,
|
|
141
162
|
} from "../types";
|
|
163
|
+
import { CustomThresholdBadgeOverlay } from "./CustomThresholdBadgeOverlay";
|
|
142
164
|
import {
|
|
143
165
|
ThresholdBadgeOverlay,
|
|
144
166
|
ThresholdLineOverlay,
|
|
@@ -276,6 +298,8 @@ function useLiveChartController({
|
|
|
276
298
|
candles,
|
|
277
299
|
candleWidth = 60,
|
|
278
300
|
liveCandle,
|
|
301
|
+
candleGaps,
|
|
302
|
+
lineGaps,
|
|
279
303
|
volume,
|
|
280
304
|
|
|
281
305
|
// ── Behaviour ───────────────────────────────────────────────────────────
|
|
@@ -333,6 +357,7 @@ function useLiveChartController({
|
|
|
333
357
|
renderMarker,
|
|
334
358
|
renderTooltip,
|
|
335
359
|
renderOverlay,
|
|
360
|
+
renderThresholdBadge,
|
|
336
361
|
renderReferenceLine,
|
|
337
362
|
renderOffAxisReferenceLine,
|
|
338
363
|
referenceLineGrouping,
|
|
@@ -371,6 +396,9 @@ function useLiveChartController({
|
|
|
371
396
|
// Volume bars sit below the candles — a candle-mode-only feature (inert in
|
|
372
397
|
// line mode, like the candle paths themselves).
|
|
373
398
|
const volumeCfg = isCandle ? resolveVolume(volume) : null;
|
|
399
|
+
const chartGapsCfg = resolveCandleGaps(isCandle ? candleGaps : lineGaps);
|
|
400
|
+
const candleGapsCfg = isCandle ? chartGapsCfg : null;
|
|
401
|
+
const lineGapsCfg = isCandle ? null : chartGapsCfg;
|
|
374
402
|
const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
|
|
375
403
|
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
376
404
|
// Dot-lattice area fill (clipped to the under-line region). Inert in candle
|
|
@@ -405,7 +433,35 @@ function useLiveChartController({
|
|
|
405
433
|
const tradeStreamResolved = resolveTradeStream(tradeStream);
|
|
406
434
|
const metricsCfg = resolveMetrics(metrics);
|
|
407
435
|
|
|
408
|
-
const
|
|
436
|
+
const chartGapBands: ReferenceLine[] =
|
|
437
|
+
chartGapsCfg?.gaps.flatMap((gap) => {
|
|
438
|
+
const gapStyle = chartGapsCfg.styles[gap.kind];
|
|
439
|
+
const band = gapStyle.band;
|
|
440
|
+
if (band === null) return [];
|
|
441
|
+
const label = gapStyle.label;
|
|
442
|
+
return [
|
|
443
|
+
{
|
|
444
|
+
id: `chart-gap:${gap.kind}:${gap.from}:${gap.to}`,
|
|
445
|
+
from: gap.from,
|
|
446
|
+
to: gap.to,
|
|
447
|
+
label:
|
|
448
|
+
label === null
|
|
449
|
+
? undefined
|
|
450
|
+
: (gap.label ?? candleGapDefaultLabel(gap.kind)),
|
|
451
|
+
color: band.borderColor,
|
|
452
|
+
fillColor: band.fillColor,
|
|
453
|
+
fillOpacity: band.fillOpacity,
|
|
454
|
+
strokeOpacity: band.borderOpacity,
|
|
455
|
+
strokeWidth: band.borderWidth > 0 ? band.borderWidth : undefined,
|
|
456
|
+
intervals: band.intervals,
|
|
457
|
+
labelColor: label?.color,
|
|
458
|
+
labelPosition: label?.position,
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
}) ?? [];
|
|
462
|
+
// Gap bands append after consumer reference lines so public line indices stay
|
|
463
|
+
// stable for callbacks. Time bands contribute no Y values or press targets.
|
|
464
|
+
const allRefLines = [...(referenceLines ?? []), ...chartGapBands];
|
|
409
465
|
const refValues = collectReferenceValues(allRefLines);
|
|
410
466
|
|
|
411
467
|
// Per-line live value overrides + drag flags for draggable lines and the custom
|
|
@@ -442,17 +498,23 @@ function useLiveChartController({
|
|
|
442
498
|
|
|
443
499
|
// Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
|
|
444
500
|
// (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
501
|
+
// Semantic gap bands are library-owned annotations: do not leak them through
|
|
502
|
+
// the consumer's reference-line renderer or change its callback cardinality.
|
|
503
|
+
const consumerRefLines = referenceLines ?? [];
|
|
504
|
+
const refLineCustom = [
|
|
505
|
+
...customReferenceLineFlags(consumerRefLines, renderReferenceLine),
|
|
506
|
+
...chartGapBands.map(() => false),
|
|
507
|
+
];
|
|
449
508
|
// An off-axis renderer replaces only the edge-pinned tag. A full custom tag
|
|
450
509
|
// takes precedence for the same line to avoid mounting two native tags.
|
|
451
|
-
const refLineOffAxisCustom =
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
510
|
+
const refLineOffAxisCustom = [
|
|
511
|
+
...customReferenceLineFlags(
|
|
512
|
+
consumerRefLines,
|
|
513
|
+
renderOffAxisReferenceLine,
|
|
514
|
+
"off-axis",
|
|
515
|
+
),
|
|
516
|
+
...chartGapBands.map(() => false),
|
|
517
|
+
].map((custom, index) => custom && !refLineCustom[index]);
|
|
456
518
|
const refLineKeys = referenceLineReactKeys(allRefLines);
|
|
457
519
|
// RN custom tags report their measured widths here so the Skia connector can
|
|
458
520
|
// start after the native badge instead of the hidden built-in pill.
|
|
@@ -738,6 +800,13 @@ function useLiveChartController({
|
|
|
738
800
|
mode,
|
|
739
801
|
candles: isCandle ? candlesEngine : candles,
|
|
740
802
|
liveCandle: isCandle ? liveEngine : liveCandle,
|
|
803
|
+
candleGaps: candleGapsCfg?.gaps,
|
|
804
|
+
candleGapBridgeNoTrades:
|
|
805
|
+
Boolean(candleGapsCfg?.styles["no-trades"].bridge),
|
|
806
|
+
candleGapBridgeUnavailable:
|
|
807
|
+
Boolean(candleGapsCfg?.styles.unavailable.bridge),
|
|
808
|
+
candleGapBridgeUnknown:
|
|
809
|
+
Boolean(candleGapsCfg?.styles.unknown.bridge),
|
|
741
810
|
});
|
|
742
811
|
|
|
743
812
|
// Mirror the UI-thread scroll state to React so the floating y-axis can keep
|
|
@@ -839,6 +908,7 @@ function useLiveChartController({
|
|
|
839
908
|
thresholdValue,
|
|
840
909
|
thresholdSeriesSV,
|
|
841
910
|
thresholdCfg?.extendToNow ?? true,
|
|
911
|
+
thresholdCfg?.line?.labelAnchor ?? "last",
|
|
842
912
|
);
|
|
843
913
|
const thresholdStopColors = thresholdCfg
|
|
844
914
|
? thresholdStops(thresholdCfg, palette)
|
|
@@ -899,23 +969,38 @@ function useLiveChartController({
|
|
|
899
969
|
thresholdSeriesGeom.clipRightX,
|
|
900
970
|
);
|
|
901
971
|
|
|
902
|
-
// Marker line + badge sources:
|
|
903
|
-
//
|
|
904
|
-
// The badge gets its own visibility
|
|
905
|
-
//
|
|
972
|
+
// Marker line + badge sources: a series badge independently selects its first
|
|
973
|
+
// or last visible endpoint; the constant case stays at the single benchmark Y.
|
|
974
|
+
// The badge gets its own visibility because its selected endpoint can be
|
|
975
|
+
// off-plot while older polyline segments are still visible.
|
|
906
976
|
const thresholdMarkerLineY = thresholdIsSeries
|
|
907
|
-
? thresholdSeriesGeom.
|
|
977
|
+
? thresholdSeriesGeom.badgeLineY
|
|
908
978
|
: thresholdGeom.lineY;
|
|
909
979
|
const thresholdMarkerVisible = thresholdIsSeries
|
|
910
980
|
? thresholdSeriesGeom.visible
|
|
911
981
|
: thresholdGeom.visible;
|
|
912
982
|
const thresholdBadgeVisible = thresholdIsSeries
|
|
913
|
-
? thresholdSeriesGeom.
|
|
983
|
+
? thresholdSeriesGeom.badgeVisible
|
|
914
984
|
: thresholdGeom.visible;
|
|
915
985
|
const thresholdMarkerValue =
|
|
916
986
|
thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
|
|
917
|
-
? (thresholdCfg.value ?? thresholdSeriesGeom.
|
|
918
|
-
: thresholdSeriesGeom.
|
|
987
|
+
? (thresholdCfg.value ?? thresholdSeriesGeom.badgeValue)
|
|
988
|
+
: thresholdSeriesGeom.badgeValue;
|
|
989
|
+
const hasCustomThresholdBadge =
|
|
990
|
+
thresholdCfg?.line != null && renderThresholdBadge != null;
|
|
991
|
+
const thresholdMarkerValueStr = useDerivedValue(() =>
|
|
992
|
+
hasCustomThresholdBadge ? formatValue(thresholdMarkerValue.get()) : "",
|
|
993
|
+
);
|
|
994
|
+
const thresholdCustomBadge =
|
|
995
|
+
thresholdCfg?.line && renderThresholdBadge
|
|
996
|
+
? renderThresholdBadge({
|
|
997
|
+
line: thresholdCfg.line,
|
|
998
|
+
value: thresholdMarkerValue,
|
|
999
|
+
valueStr: thresholdMarkerValueStr,
|
|
1000
|
+
y: thresholdMarkerLineY,
|
|
1001
|
+
visible: thresholdBadgeVisible,
|
|
1002
|
+
})
|
|
1003
|
+
: null;
|
|
919
1004
|
const thresholdSeriesPts = thresholdIsSeries
|
|
920
1005
|
? thresholdSeriesGeom.screenPts
|
|
921
1006
|
: undefined;
|
|
@@ -946,6 +1031,7 @@ function useLiveChartController({
|
|
|
946
1031
|
? thresholdSeriesGeom.samples
|
|
947
1032
|
: undefined,
|
|
948
1033
|
lineProp?.simplify,
|
|
1034
|
+
lineGapsCfg?.gaps,
|
|
949
1035
|
);
|
|
950
1036
|
|
|
951
1037
|
// Area-dots fill shader color as a vec4 (channels 0..1), with the config
|
|
@@ -994,14 +1080,28 @@ function useLiveChartController({
|
|
|
994
1080
|
|
|
995
1081
|
// ── Overlay hooks ─────────────────────────────────────────────────────
|
|
996
1082
|
// Scrub/crosshair must see the same stash-backed candles as the engine.
|
|
997
|
-
const
|
|
1083
|
+
const crosshairChartOpts = isCandle
|
|
998
1084
|
? {
|
|
999
1085
|
mode,
|
|
1000
1086
|
candles: candlesEngine,
|
|
1001
1087
|
liveCandle: liveEngine,
|
|
1002
1088
|
candleWidthSecs: candleWidth,
|
|
1089
|
+
gaps: candleGapsCfg?.gaps,
|
|
1090
|
+
bridgeNoTrades:
|
|
1091
|
+
Boolean(candleGapsCfg?.styles["no-trades"].bridge),
|
|
1092
|
+
bridgeUnavailable:
|
|
1093
|
+
Boolean(candleGapsCfg?.styles.unavailable.bridge),
|
|
1094
|
+
bridgeUnknown: Boolean(candleGapsCfg?.styles.unknown.bridge),
|
|
1003
1095
|
}
|
|
1004
|
-
:
|
|
1096
|
+
: lineGapsCfg
|
|
1097
|
+
? {
|
|
1098
|
+
mode,
|
|
1099
|
+
gaps: lineGapsCfg.gaps,
|
|
1100
|
+
bridgeNoTrades: Boolean(lineGapsCfg.styles["no-trades"].bridge),
|
|
1101
|
+
bridgeUnavailable: Boolean(lineGapsCfg.styles.unavailable.bridge),
|
|
1102
|
+
bridgeUnknown: Boolean(lineGapsCfg.styles.unknown.bridge),
|
|
1103
|
+
}
|
|
1104
|
+
: undefined;
|
|
1005
1105
|
|
|
1006
1106
|
const markersActive = markers != null;
|
|
1007
1107
|
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
@@ -1102,7 +1202,7 @@ function useLiveChartController({
|
|
|
1102
1202
|
// yet becomes scrubbable on touch. `static` only kills the continuous loop.
|
|
1103
1203
|
scrubCfg !== null || scrubActionCfg !== null,
|
|
1104
1204
|
onScrub,
|
|
1105
|
-
|
|
1205
|
+
crosshairChartOpts,
|
|
1106
1206
|
scrubHoldMs,
|
|
1107
1207
|
onGestureStart,
|
|
1108
1208
|
onGestureEnd,
|
|
@@ -1460,6 +1560,7 @@ function useLiveChartController({
|
|
|
1460
1560
|
thresholdMarkerVisible,
|
|
1461
1561
|
thresholdBadgeVisible,
|
|
1462
1562
|
thresholdMarkerValue,
|
|
1563
|
+
thresholdCustomBadge,
|
|
1463
1564
|
thresholdSeriesPts,
|
|
1464
1565
|
badgeUsesRightGutter,
|
|
1465
1566
|
// theme / layout / fonts
|
|
@@ -1501,6 +1602,8 @@ function useLiveChartController({
|
|
|
1501
1602
|
thresholdFillPath,
|
|
1502
1603
|
lineIsLinear,
|
|
1503
1604
|
volumeCfg,
|
|
1605
|
+
candleGapsCfg,
|
|
1606
|
+
lineGapsCfg,
|
|
1504
1607
|
// Volume bars: active flag, fade-in opacity, and resolved colors (default to
|
|
1505
1608
|
// the candle palette). The reserved band height is read by the x-axis.
|
|
1506
1609
|
volumeActive: volumeCfg !== null,
|
|
@@ -1586,6 +1689,7 @@ function ChartWithYAxis({ model }: { model: LiveChartModel }) {
|
|
|
1586
1689
|
yAxisCfg?.minGap ?? 36,
|
|
1587
1690
|
metricsCfg.grid,
|
|
1588
1691
|
yAxisCfg?.count ?? 0,
|
|
1692
|
+
yAxisCfg?.intervalScale ?? 1,
|
|
1589
1693
|
);
|
|
1590
1694
|
if (model.degenCfg) {
|
|
1591
1695
|
return <ChartWithDegen model={model} yAxisEntries={yAxisEntries} />;
|
|
@@ -1792,6 +1896,124 @@ function ChartFillLayer({
|
|
|
1792
1896
|
}
|
|
1793
1897
|
|
|
1794
1898
|
type CandlePaths = ReturnType<typeof useCandlePaths>;
|
|
1899
|
+
type CandleGapPaths = ReturnType<typeof useCandleGapPaths>;
|
|
1900
|
+
type LineGapPaths = ReturnType<typeof useLineGapPaths>;
|
|
1901
|
+
|
|
1902
|
+
function GapBridgePathBatch({
|
|
1903
|
+
paths,
|
|
1904
|
+
config,
|
|
1905
|
+
palette,
|
|
1906
|
+
}: {
|
|
1907
|
+
paths: CandleGapPaths | LineGapPaths;
|
|
1908
|
+
config: ResolvedCandleGapsConfig;
|
|
1909
|
+
palette: LiveChartPalette;
|
|
1910
|
+
}) {
|
|
1911
|
+
const noTrades = config.styles["no-trades"].bridge;
|
|
1912
|
+
const unavailable = config.styles.unavailable.bridge;
|
|
1913
|
+
const unknown = config.styles.unknown.bridge;
|
|
1914
|
+
return (
|
|
1915
|
+
<>
|
|
1916
|
+
{noTrades !== null && (
|
|
1917
|
+
<Group opacity={noTrades.opacity}>
|
|
1918
|
+
<Path
|
|
1919
|
+
path={paths.noTradesPath}
|
|
1920
|
+
style="stroke"
|
|
1921
|
+
strokeWidth={noTrades.strokeWidth}
|
|
1922
|
+
strokeCap={noTrades.strokeCap}
|
|
1923
|
+
color={noTrades.color ?? palette.refLine}
|
|
1924
|
+
/>
|
|
1925
|
+
</Group>
|
|
1926
|
+
)}
|
|
1927
|
+
{unavailable !== null && (
|
|
1928
|
+
<Group opacity={unavailable.opacity}>
|
|
1929
|
+
<Path
|
|
1930
|
+
path={paths.unavailablePath}
|
|
1931
|
+
style="stroke"
|
|
1932
|
+
strokeWidth={unavailable.strokeWidth}
|
|
1933
|
+
strokeCap={unavailable.strokeCap}
|
|
1934
|
+
color={unavailable.color ?? palette.refLine}
|
|
1935
|
+
/>
|
|
1936
|
+
</Group>
|
|
1937
|
+
)}
|
|
1938
|
+
{unknown !== null && (
|
|
1939
|
+
<Group opacity={unknown.opacity}>
|
|
1940
|
+
<Path
|
|
1941
|
+
path={paths.unknownPath}
|
|
1942
|
+
style="stroke"
|
|
1943
|
+
strokeWidth={unknown.strokeWidth}
|
|
1944
|
+
strokeCap={unknown.strokeCap}
|
|
1945
|
+
color={unknown.color ?? palette.refLine}
|
|
1946
|
+
/>
|
|
1947
|
+
</Group>
|
|
1948
|
+
)}
|
|
1949
|
+
</>
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function ChartCandleGapLayer({
|
|
1954
|
+
model,
|
|
1955
|
+
config,
|
|
1956
|
+
focusOtherCandles,
|
|
1957
|
+
inactiveOpacity,
|
|
1958
|
+
focusedOpacity,
|
|
1959
|
+
focusedClip,
|
|
1960
|
+
}: {
|
|
1961
|
+
model: LiveChartModel;
|
|
1962
|
+
config: ResolvedCandleGapsConfig;
|
|
1963
|
+
focusOtherCandles: boolean;
|
|
1964
|
+
inactiveOpacity: SharedValue<number>;
|
|
1965
|
+
focusedOpacity: SharedValue<number>;
|
|
1966
|
+
focusedClip: SharedValue<{
|
|
1967
|
+
x: number;
|
|
1968
|
+
y: number;
|
|
1969
|
+
width: number;
|
|
1970
|
+
height: number;
|
|
1971
|
+
}>;
|
|
1972
|
+
}) {
|
|
1973
|
+
const paths = useCandleGapPaths(
|
|
1974
|
+
model.engine,
|
|
1975
|
+
model.effectivePadding,
|
|
1976
|
+
model.candlesEngine,
|
|
1977
|
+
model.liveEngine,
|
|
1978
|
+
model.displayCandleWidth,
|
|
1979
|
+
config,
|
|
1980
|
+
model.metricsCfg.candle,
|
|
1981
|
+
);
|
|
1982
|
+
const batch = (
|
|
1983
|
+
<GapBridgePathBatch
|
|
1984
|
+
paths={paths}
|
|
1985
|
+
config={config}
|
|
1986
|
+
palette={model.palette}
|
|
1987
|
+
/>
|
|
1988
|
+
);
|
|
1989
|
+
return focusOtherCandles ? (
|
|
1990
|
+
<>
|
|
1991
|
+
<Group opacity={inactiveOpacity}>{batch}</Group>
|
|
1992
|
+
<Group opacity={focusedOpacity} clip={focusedClip}>
|
|
1993
|
+
{batch}
|
|
1994
|
+
</Group>
|
|
1995
|
+
</>
|
|
1996
|
+
) : (
|
|
1997
|
+
batch
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/** Line-mode bridge paths live in a child so candle charts register no worklets. */
|
|
2002
|
+
function ChartLineGapLayer({ model }: { model: LiveChartModel }) {
|
|
2003
|
+
const config = model.lineGapsCfg!;
|
|
2004
|
+
const paths = useLineGapPaths(
|
|
2005
|
+
model.engine,
|
|
2006
|
+
model.effectivePadding,
|
|
2007
|
+
config,
|
|
2008
|
+
);
|
|
2009
|
+
return (
|
|
2010
|
+
<GapBridgePathBatch
|
|
2011
|
+
paths={paths}
|
|
2012
|
+
config={config}
|
|
2013
|
+
palette={model.palette}
|
|
2014
|
+
/>
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
1795
2017
|
|
|
1796
2018
|
/** One batched candle pass: two body paths and two wick paths. */
|
|
1797
2019
|
function CandlePathBatch({
|
|
@@ -1848,6 +2070,7 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1848
2070
|
volumeOpacity,
|
|
1849
2071
|
volumeUpColor,
|
|
1850
2072
|
volumeDownColor,
|
|
2073
|
+
candleGapsCfg,
|
|
1851
2074
|
scrubCfg,
|
|
1852
2075
|
crosshair,
|
|
1853
2076
|
} = model;
|
|
@@ -1865,24 +2088,42 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1865
2088
|
const focusOtherCandles = scrubCfg?.dimTarget === "otherCandles";
|
|
1866
2089
|
const candleDimOpacity = scrubCfg?.dimOpacity ?? 1;
|
|
1867
2090
|
const candleDimFadeMs = scrubCfg?.dimFadeMs ?? 0;
|
|
1868
|
-
// Destructure
|
|
2091
|
+
// Destructure SharedValues before entering worklets. Closing over the
|
|
1869
2092
|
// whole crosshair object would also serialize its RNGH gesture instances.
|
|
1870
2093
|
const scrubActive = crosshair.scrubActive;
|
|
1871
2094
|
// ChartCandleLayer is single-series only; useCrosshair always supplies this
|
|
1872
2095
|
// value (it is optional on the shared state type only for LiveChartSeries).
|
|
1873
2096
|
const scrubCandle = crosshair.scrubCandle!;
|
|
1874
|
-
|
|
2097
|
+
const scrubGap = crosshair.scrubGap!;
|
|
2098
|
+
const scrubTime = crosshair.scrubTime;
|
|
2099
|
+
const currentFocusBucketTime = useDerivedValue(() => {
|
|
2100
|
+
if (!focusOtherCandles) return null;
|
|
2101
|
+
const candle = scrubCandle.get();
|
|
2102
|
+
if (candle) return candle.time;
|
|
2103
|
+
const gap = scrubGap.get();
|
|
2104
|
+
if (!gap) return null;
|
|
2105
|
+
return candleGapBucketStartAtTime(
|
|
2106
|
+
gap,
|
|
2107
|
+
scrubTime.get(),
|
|
2108
|
+
candlesEngine.get(),
|
|
2109
|
+
liveEngine.get(),
|
|
2110
|
+
displayCandleWidth.get(),
|
|
2111
|
+
);
|
|
2112
|
+
});
|
|
2113
|
+
// Keep the last selection after the current target clears so its clipped full-
|
|
1875
2114
|
// strength pass can cover the base batch until the release fade completes.
|
|
1876
|
-
const
|
|
2115
|
+
const lastFocusBucket = useSharedValue<{ time: number } | null>(null);
|
|
1877
2116
|
useAnimatedReaction(
|
|
1878
|
-
() =>
|
|
1879
|
-
(
|
|
1880
|
-
if (
|
|
2117
|
+
() => currentFocusBucketTime.get(),
|
|
2118
|
+
(time) => {
|
|
2119
|
+
if (time !== null) lastFocusBucket.set({ time });
|
|
1881
2120
|
},
|
|
1882
2121
|
);
|
|
1883
2122
|
const inactiveCandleOpacity = useDerivedValue(() => {
|
|
1884
2123
|
const target =
|
|
1885
|
-
focusOtherCandles &&
|
|
2124
|
+
focusOtherCandles &&
|
|
2125
|
+
scrubActive.get() &&
|
|
2126
|
+
currentFocusBucketTime.get() !== null
|
|
1886
2127
|
? candleDimOpacity
|
|
1887
2128
|
: 1;
|
|
1888
2129
|
return candleDimFadeMs > 0
|
|
@@ -1891,33 +2132,36 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1891
2132
|
}, [
|
|
1892
2133
|
focusOtherCandles,
|
|
1893
2134
|
scrubActive,
|
|
1894
|
-
|
|
2135
|
+
currentFocusBucketTime,
|
|
1895
2136
|
candleDimOpacity,
|
|
1896
2137
|
candleDimFadeMs,
|
|
1897
2138
|
]);
|
|
1898
2139
|
const focusedCandleOpacity = useDerivedValue(() => {
|
|
1899
|
-
const
|
|
1900
|
-
const
|
|
1901
|
-
|
|
2140
|
+
const currentTime = currentFocusBucketTime.get();
|
|
2141
|
+
const focusBucket =
|
|
2142
|
+
currentTime === null ? lastFocusBucket.get() : { time: currentTime };
|
|
2143
|
+
if (!focusOtherCandles || !focusBucket) return 0;
|
|
1902
2144
|
return computeCandleFocusPassOpacity(
|
|
1903
2145
|
scrubActive.get(),
|
|
1904
|
-
|
|
2146
|
+
currentTime !== null,
|
|
1905
2147
|
inactiveCandleOpacity.get(),
|
|
1906
2148
|
);
|
|
1907
2149
|
}, [
|
|
1908
2150
|
focusOtherCandles,
|
|
1909
2151
|
scrubActive,
|
|
1910
|
-
|
|
1911
|
-
|
|
2152
|
+
currentFocusBucketTime,
|
|
2153
|
+
lastFocusBucket,
|
|
1912
2154
|
inactiveCandleOpacity,
|
|
1913
2155
|
]);
|
|
1914
2156
|
const focusedCandleClip = useDerivedValue(() => {
|
|
1915
2157
|
if (!focusOtherCandles) {
|
|
1916
2158
|
return HIDDEN_CANDLE_FOCUS_CLIP;
|
|
1917
2159
|
}
|
|
1918
|
-
const
|
|
2160
|
+
const currentTime = currentFocusBucketTime.get();
|
|
2161
|
+
const focusBucket =
|
|
2162
|
+
currentTime === null ? lastFocusBucket.get() : { time: currentTime };
|
|
1919
2163
|
return computeCandleFocusClip(
|
|
1920
|
-
|
|
2164
|
+
focusBucket,
|
|
1921
2165
|
effectivePadding,
|
|
1922
2166
|
engine.canvasWidth.get(),
|
|
1923
2167
|
engine.canvasHeight.get(),
|
|
@@ -1927,8 +2171,8 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1927
2171
|
);
|
|
1928
2172
|
}, [
|
|
1929
2173
|
focusOtherCandles,
|
|
1930
|
-
|
|
1931
|
-
|
|
2174
|
+
currentFocusBucketTime,
|
|
2175
|
+
lastFocusBucket,
|
|
1932
2176
|
effectivePadding,
|
|
1933
2177
|
engine.canvasWidth,
|
|
1934
2178
|
engine.canvasHeight,
|
|
@@ -1940,6 +2184,16 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1940
2184
|
return (
|
|
1941
2185
|
<Group opacity={seriesOpacity}>
|
|
1942
2186
|
<Group opacity={candleGroupOpacity}>
|
|
2187
|
+
{candleGapsCfg && (
|
|
2188
|
+
<ChartCandleGapLayer
|
|
2189
|
+
model={model}
|
|
2190
|
+
config={candleGapsCfg}
|
|
2191
|
+
focusOtherCandles={focusOtherCandles}
|
|
2192
|
+
inactiveOpacity={inactiveCandleOpacity}
|
|
2193
|
+
focusedOpacity={focusedCandleOpacity}
|
|
2194
|
+
focusedClip={focusedCandleClip}
|
|
2195
|
+
/>
|
|
2196
|
+
)}
|
|
1943
2197
|
{focusOtherCandles ? (
|
|
1944
2198
|
<>
|
|
1945
2199
|
<Group opacity={inactiveCandleOpacity}>
|
|
@@ -2022,11 +2276,13 @@ function ChartStack({
|
|
|
2022
2276
|
thresholdMarkerVisible,
|
|
2023
2277
|
thresholdBadgeVisible,
|
|
2024
2278
|
thresholdMarkerValue,
|
|
2279
|
+
thresholdCustomBadge,
|
|
2025
2280
|
thresholdSeriesPts,
|
|
2026
2281
|
formatValue,
|
|
2027
2282
|
lineGroupOpacity,
|
|
2028
2283
|
seriesOpacity,
|
|
2029
2284
|
linePath,
|
|
2285
|
+
lineGapsCfg,
|
|
2030
2286
|
lineIsLinear,
|
|
2031
2287
|
strokeWidth,
|
|
2032
2288
|
lineProp,
|
|
@@ -2171,6 +2427,7 @@ function ChartStack({
|
|
|
2171
2427
|
/>
|
|
2172
2428
|
) : null}
|
|
2173
2429
|
</Path>
|
|
2430
|
+
{lineGapsCfg && <ChartLineGapLayer model={model} />}
|
|
2174
2431
|
</Group>
|
|
2175
2432
|
</Group>
|
|
2176
2433
|
|
|
@@ -2246,7 +2503,7 @@ function ChartStack({
|
|
|
2246
2503
|
|
|
2247
2504
|
{/* Threshold label badge — on top of the line/dot/markers so it's never
|
|
2248
2505
|
painted over (the dashed marker line itself stays behind the line, above). */}
|
|
2249
|
-
{thresholdCfg?.line && (
|
|
2506
|
+
{thresholdCfg?.line && thresholdCustomBadge == null && (
|
|
2250
2507
|
<ThresholdBadgeOverlay
|
|
2251
2508
|
engine={engine}
|
|
2252
2509
|
padding={effectivePadding}
|
|
@@ -2300,15 +2557,27 @@ function ChartTradeStreamLayer({
|
|
|
2300
2557
|
tradeStream,
|
|
2301
2558
|
tradeStreamResolved,
|
|
2302
2559
|
effectivePadding,
|
|
2560
|
+
volumeBandHeight,
|
|
2303
2561
|
palette,
|
|
2304
2562
|
skiaFont,
|
|
2305
2563
|
reveal,
|
|
2306
2564
|
isStatic,
|
|
2307
2565
|
} = model;
|
|
2566
|
+
// `effectivePadding.bottom` includes the volume reservation so candle prices
|
|
2567
|
+
// stop above the bars. The trade tape should still enter at the chart's true
|
|
2568
|
+
// lower edge (the volume baseline / x-axis), so remove that reservation for
|
|
2569
|
+
// this overlay's coordinate space only.
|
|
2570
|
+
const tradeStreamPadding =
|
|
2571
|
+
volumeBandHeight > 0
|
|
2572
|
+
? {
|
|
2573
|
+
...effectivePadding,
|
|
2574
|
+
bottom: effectivePadding.bottom - volumeBandHeight,
|
|
2575
|
+
}
|
|
2576
|
+
: effectivePadding;
|
|
2308
2577
|
const tradeMarkers = useTradeStream(
|
|
2309
2578
|
engine,
|
|
2310
2579
|
tradeStream!,
|
|
2311
|
-
|
|
2580
|
+
tradeStreamPadding,
|
|
2312
2581
|
!isStatic,
|
|
2313
2582
|
!isStatic,
|
|
2314
2583
|
);
|
|
@@ -2317,7 +2586,7 @@ function ChartTradeStreamLayer({
|
|
|
2317
2586
|
<TradeStreamOverlay
|
|
2318
2587
|
markers={tradeMarkers}
|
|
2319
2588
|
palette={palette}
|
|
2320
|
-
padding={
|
|
2589
|
+
padding={tradeStreamPadding}
|
|
2321
2590
|
font={skiaFont}
|
|
2322
2591
|
opacity={reveal.dotOpacity}
|
|
2323
2592
|
labelOffsetX={tradeStreamResolved!.labelOffsetX}
|
|
@@ -2755,6 +3024,10 @@ function ChartView({
|
|
|
2755
3024
|
formatValue,
|
|
2756
3025
|
topLabelCfg,
|
|
2757
3026
|
bottomLabelCfg,
|
|
3027
|
+
thresholdCfg,
|
|
3028
|
+
thresholdMarkerLineY,
|
|
3029
|
+
thresholdBadgeVisible,
|
|
3030
|
+
thresholdCustomBadge,
|
|
2758
3031
|
markersActive,
|
|
2759
3032
|
renderMarker,
|
|
2760
3033
|
renderTooltip,
|
|
@@ -2868,6 +3141,19 @@ function ChartView({
|
|
|
2868
3141
|
/>
|
|
2869
3142
|
)}
|
|
2870
3143
|
|
|
3144
|
+
{/* Custom threshold badge — a native view positioned from the same live
|
|
3145
|
+
value/Y SharedValues as the built-in Skia badge. */}
|
|
3146
|
+
{thresholdCustomBadge && thresholdCfg?.line && (
|
|
3147
|
+
<CustomThresholdBadgeOverlay
|
|
3148
|
+
element={thresholdCustomBadge}
|
|
3149
|
+
engine={engine}
|
|
3150
|
+
padding={effectivePadding}
|
|
3151
|
+
y={thresholdMarkerLineY}
|
|
3152
|
+
visible={thresholdBadgeVisible}
|
|
3153
|
+
position={thresholdCfg.line.labelPosition}
|
|
3154
|
+
/>
|
|
3155
|
+
)}
|
|
3156
|
+
|
|
2871
3157
|
{/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
|
|
2872
3158
|
pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
|
|
2873
3159
|
a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
|
|
@@ -2890,6 +3176,7 @@ function ChartView({
|
|
|
2890
3176
|
scrubTime={crosshair.scrubTime}
|
|
2891
3177
|
scrubActive={crosshair.scrubActive}
|
|
2892
3178
|
scrubCandle={crosshair.scrubCandle}
|
|
3179
|
+
scrubGap={crosshair.scrubGap}
|
|
2893
3180
|
tooltipLayout={crosshair.tooltipLayout}
|
|
2894
3181
|
engine={engine}
|
|
2895
3182
|
padding={effectivePadding}
|
|
@@ -2911,13 +3198,23 @@ function ChartView({
|
|
|
2911
3198
|
);
|
|
2912
3199
|
}
|
|
2913
3200
|
|
|
2914
|
-
export
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3201
|
+
export const LiveChart = forwardRef<LiveChartHandle, LiveChartProps>(
|
|
3202
|
+
function LiveChart(props, ref) {
|
|
3203
|
+
const model = useLiveChartController(props);
|
|
3204
|
+
const { viewEnd, viewWindow } = model.engine;
|
|
3205
|
+
useImperativeHandle(
|
|
3206
|
+
ref,
|
|
3207
|
+
() => ({
|
|
3208
|
+
resetZoom: () => scheduleOnUI(resetPinchZoom, { viewEnd, viewWindow }),
|
|
3209
|
+
}),
|
|
3210
|
+
[viewEnd, viewWindow],
|
|
3211
|
+
);
|
|
3212
|
+
if (model.yAxisCfg) {
|
|
3213
|
+
return <ChartWithYAxis model={model} />;
|
|
3214
|
+
}
|
|
3215
|
+
if (model.degenCfg) {
|
|
3216
|
+
return <ChartWithDegen model={model} yAxisEntries={null} />;
|
|
3217
|
+
}
|
|
3218
|
+
return <ChartView model={model} yAxisEntries={null} degen={null} />;
|
|
3219
|
+
},
|
|
3220
|
+
);
|