react-native-livechart 4.20.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.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 +32 -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/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/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/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 +188 -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 +320 -43
- 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 +186 -0
- package/src/core/useLiveChartEngine.ts +37 -5
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- 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/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useThreshold.ts +31 -22
- package/src/index.ts +15 -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 +199 -14
|
@@ -18,6 +18,7 @@ import Animated, {
|
|
|
18
18
|
withDelay,
|
|
19
19
|
withSequence,
|
|
20
20
|
withTiming,
|
|
21
|
+
type SharedValue,
|
|
21
22
|
} from "react-native-reanimated";
|
|
22
23
|
import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
|
|
23
24
|
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
resolveAreaDots,
|
|
46
47
|
resolveAxisLabel,
|
|
47
48
|
resolveBadge,
|
|
49
|
+
resolveCandleGaps,
|
|
48
50
|
resolveDegen,
|
|
49
51
|
resolveDot,
|
|
50
52
|
resolveGradient,
|
|
@@ -70,7 +72,10 @@ import {
|
|
|
70
72
|
resolveXAxis,
|
|
71
73
|
resolveYAxis,
|
|
72
74
|
} from "../core/resolveConfig";
|
|
73
|
-
import type {
|
|
75
|
+
import type {
|
|
76
|
+
ResolvedCandleGapsConfig,
|
|
77
|
+
ResolvedThresholdConfig,
|
|
78
|
+
} from "../core/resolveConfig";
|
|
74
79
|
import {
|
|
75
80
|
liveIndicatorScrollOpacity,
|
|
76
81
|
resolveHideLiveOnScrollBack,
|
|
@@ -85,6 +90,7 @@ import {
|
|
|
85
90
|
import { dotGlowRadialOutset, pulseRadialOutset } from "../draw/line";
|
|
86
91
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
87
92
|
import { useBadge } from "../hooks/useBadge";
|
|
93
|
+
import { useCandleGapPaths } from "../hooks/useCandleGapPaths";
|
|
88
94
|
import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
|
|
89
95
|
import { useCanvasLayout } from "../hooks/useCanvasLayout";
|
|
90
96
|
import { useChartColors } from "../hooks/useChartColors";
|
|
@@ -96,6 +102,7 @@ import { useCrosshair } from "../hooks/useCrosshair";
|
|
|
96
102
|
import { useDegen } from "../hooks/useDegen";
|
|
97
103
|
import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
|
|
98
104
|
import { useLiveDot } from "../hooks/useLiveDot";
|
|
105
|
+
import { useLineGapPaths } from "../hooks/useLineGapPaths";
|
|
99
106
|
import { useMarkers } from "../hooks/useMarkers";
|
|
100
107
|
import { useReferenceDrag } from "../hooks/useReferenceDrag";
|
|
101
108
|
import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
|
|
@@ -122,6 +129,10 @@ import {
|
|
|
122
129
|
formatValue as defaultFormatValue,
|
|
123
130
|
} from "../lib/format";
|
|
124
131
|
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
|
|
132
|
+
import {
|
|
133
|
+
candleGapBucketStartAtTime,
|
|
134
|
+
candleGapDefaultLabel,
|
|
135
|
+
} from "../math/candleGaps";
|
|
125
136
|
import { computeScrubDotY } from "../hooks/crosshairShared";
|
|
126
137
|
import {
|
|
127
138
|
groupReferenceLines,
|
|
@@ -147,7 +158,9 @@ import type {
|
|
|
147
158
|
LiveChartPoint,
|
|
148
159
|
LiveChartProps,
|
|
149
160
|
Marker,
|
|
161
|
+
ReferenceLine,
|
|
150
162
|
} from "../types";
|
|
163
|
+
import { CustomThresholdBadgeOverlay } from "./CustomThresholdBadgeOverlay";
|
|
151
164
|
import {
|
|
152
165
|
ThresholdBadgeOverlay,
|
|
153
166
|
ThresholdLineOverlay,
|
|
@@ -285,6 +298,8 @@ function useLiveChartController({
|
|
|
285
298
|
candles,
|
|
286
299
|
candleWidth = 60,
|
|
287
300
|
liveCandle,
|
|
301
|
+
candleGaps,
|
|
302
|
+
lineGaps,
|
|
288
303
|
volume,
|
|
289
304
|
|
|
290
305
|
// ── Behaviour ───────────────────────────────────────────────────────────
|
|
@@ -342,6 +357,7 @@ function useLiveChartController({
|
|
|
342
357
|
renderMarker,
|
|
343
358
|
renderTooltip,
|
|
344
359
|
renderOverlay,
|
|
360
|
+
renderThresholdBadge,
|
|
345
361
|
renderReferenceLine,
|
|
346
362
|
renderOffAxisReferenceLine,
|
|
347
363
|
referenceLineGrouping,
|
|
@@ -380,6 +396,9 @@ function useLiveChartController({
|
|
|
380
396
|
// Volume bars sit below the candles — a candle-mode-only feature (inert in
|
|
381
397
|
// line mode, like the candle paths themselves).
|
|
382
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;
|
|
383
402
|
const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
|
|
384
403
|
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
385
404
|
// Dot-lattice area fill (clipped to the under-line region). Inert in candle
|
|
@@ -414,7 +433,35 @@ function useLiveChartController({
|
|
|
414
433
|
const tradeStreamResolved = resolveTradeStream(tradeStream);
|
|
415
434
|
const metricsCfg = resolveMetrics(metrics);
|
|
416
435
|
|
|
417
|
-
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];
|
|
418
465
|
const refValues = collectReferenceValues(allRefLines);
|
|
419
466
|
|
|
420
467
|
// Per-line live value overrides + drag flags for draggable lines and the custom
|
|
@@ -451,17 +498,23 @@ function useLiveChartController({
|
|
|
451
498
|
|
|
452
499
|
// Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
|
|
453
500
|
// (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
];
|
|
458
508
|
// An off-axis renderer replaces only the edge-pinned tag. A full custom tag
|
|
459
509
|
// takes precedence for the same line to avoid mounting two native tags.
|
|
460
|
-
const refLineOffAxisCustom =
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
510
|
+
const refLineOffAxisCustom = [
|
|
511
|
+
...customReferenceLineFlags(
|
|
512
|
+
consumerRefLines,
|
|
513
|
+
renderOffAxisReferenceLine,
|
|
514
|
+
"off-axis",
|
|
515
|
+
),
|
|
516
|
+
...chartGapBands.map(() => false),
|
|
517
|
+
].map((custom, index) => custom && !refLineCustom[index]);
|
|
465
518
|
const refLineKeys = referenceLineReactKeys(allRefLines);
|
|
466
519
|
// RN custom tags report their measured widths here so the Skia connector can
|
|
467
520
|
// start after the native badge instead of the hidden built-in pill.
|
|
@@ -747,6 +800,13 @@ function useLiveChartController({
|
|
|
747
800
|
mode,
|
|
748
801
|
candles: isCandle ? candlesEngine : candles,
|
|
749
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),
|
|
750
810
|
});
|
|
751
811
|
|
|
752
812
|
// Mirror the UI-thread scroll state to React so the floating y-axis can keep
|
|
@@ -848,6 +908,7 @@ function useLiveChartController({
|
|
|
848
908
|
thresholdValue,
|
|
849
909
|
thresholdSeriesSV,
|
|
850
910
|
thresholdCfg?.extendToNow ?? true,
|
|
911
|
+
thresholdCfg?.line?.labelAnchor ?? "last",
|
|
851
912
|
);
|
|
852
913
|
const thresholdStopColors = thresholdCfg
|
|
853
914
|
? thresholdStops(thresholdCfg, palette)
|
|
@@ -908,23 +969,38 @@ function useLiveChartController({
|
|
|
908
969
|
thresholdSeriesGeom.clipRightX,
|
|
909
970
|
);
|
|
910
971
|
|
|
911
|
-
// Marker line + badge sources:
|
|
912
|
-
//
|
|
913
|
-
// The badge gets its own visibility
|
|
914
|
-
//
|
|
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.
|
|
915
976
|
const thresholdMarkerLineY = thresholdIsSeries
|
|
916
|
-
? thresholdSeriesGeom.
|
|
977
|
+
? thresholdSeriesGeom.badgeLineY
|
|
917
978
|
: thresholdGeom.lineY;
|
|
918
979
|
const thresholdMarkerVisible = thresholdIsSeries
|
|
919
980
|
? thresholdSeriesGeom.visible
|
|
920
981
|
: thresholdGeom.visible;
|
|
921
982
|
const thresholdBadgeVisible = thresholdIsSeries
|
|
922
|
-
? thresholdSeriesGeom.
|
|
983
|
+
? thresholdSeriesGeom.badgeVisible
|
|
923
984
|
: thresholdGeom.visible;
|
|
924
985
|
const thresholdMarkerValue =
|
|
925
986
|
thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
|
|
926
|
-
? (thresholdCfg.value ?? thresholdSeriesGeom.
|
|
927
|
-
: 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;
|
|
928
1004
|
const thresholdSeriesPts = thresholdIsSeries
|
|
929
1005
|
? thresholdSeriesGeom.screenPts
|
|
930
1006
|
: undefined;
|
|
@@ -955,6 +1031,7 @@ function useLiveChartController({
|
|
|
955
1031
|
? thresholdSeriesGeom.samples
|
|
956
1032
|
: undefined,
|
|
957
1033
|
lineProp?.simplify,
|
|
1034
|
+
lineGapsCfg?.gaps,
|
|
958
1035
|
);
|
|
959
1036
|
|
|
960
1037
|
// Area-dots fill shader color as a vec4 (channels 0..1), with the config
|
|
@@ -1003,14 +1080,28 @@ function useLiveChartController({
|
|
|
1003
1080
|
|
|
1004
1081
|
// ── Overlay hooks ─────────────────────────────────────────────────────
|
|
1005
1082
|
// Scrub/crosshair must see the same stash-backed candles as the engine.
|
|
1006
|
-
const
|
|
1083
|
+
const crosshairChartOpts = isCandle
|
|
1007
1084
|
? {
|
|
1008
1085
|
mode,
|
|
1009
1086
|
candles: candlesEngine,
|
|
1010
1087
|
liveCandle: liveEngine,
|
|
1011
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),
|
|
1012
1095
|
}
|
|
1013
|
-
:
|
|
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;
|
|
1014
1105
|
|
|
1015
1106
|
const markersActive = markers != null;
|
|
1016
1107
|
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
@@ -1111,7 +1202,7 @@ function useLiveChartController({
|
|
|
1111
1202
|
// yet becomes scrubbable on touch. `static` only kills the continuous loop.
|
|
1112
1203
|
scrubCfg !== null || scrubActionCfg !== null,
|
|
1113
1204
|
onScrub,
|
|
1114
|
-
|
|
1205
|
+
crosshairChartOpts,
|
|
1115
1206
|
scrubHoldMs,
|
|
1116
1207
|
onGestureStart,
|
|
1117
1208
|
onGestureEnd,
|
|
@@ -1469,6 +1560,7 @@ function useLiveChartController({
|
|
|
1469
1560
|
thresholdMarkerVisible,
|
|
1470
1561
|
thresholdBadgeVisible,
|
|
1471
1562
|
thresholdMarkerValue,
|
|
1563
|
+
thresholdCustomBadge,
|
|
1472
1564
|
thresholdSeriesPts,
|
|
1473
1565
|
badgeUsesRightGutter,
|
|
1474
1566
|
// theme / layout / fonts
|
|
@@ -1510,6 +1602,8 @@ function useLiveChartController({
|
|
|
1510
1602
|
thresholdFillPath,
|
|
1511
1603
|
lineIsLinear,
|
|
1512
1604
|
volumeCfg,
|
|
1605
|
+
candleGapsCfg,
|
|
1606
|
+
lineGapsCfg,
|
|
1513
1607
|
// Volume bars: active flag, fade-in opacity, and resolved colors (default to
|
|
1514
1608
|
// the candle palette). The reserved band height is read by the x-axis.
|
|
1515
1609
|
volumeActive: volumeCfg !== null,
|
|
@@ -1802,6 +1896,124 @@ function ChartFillLayer({
|
|
|
1802
1896
|
}
|
|
1803
1897
|
|
|
1804
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
|
+
}
|
|
1805
2017
|
|
|
1806
2018
|
/** One batched candle pass: two body paths and two wick paths. */
|
|
1807
2019
|
function CandlePathBatch({
|
|
@@ -1858,6 +2070,7 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1858
2070
|
volumeOpacity,
|
|
1859
2071
|
volumeUpColor,
|
|
1860
2072
|
volumeDownColor,
|
|
2073
|
+
candleGapsCfg,
|
|
1861
2074
|
scrubCfg,
|
|
1862
2075
|
crosshair,
|
|
1863
2076
|
} = model;
|
|
@@ -1875,24 +2088,42 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1875
2088
|
const focusOtherCandles = scrubCfg?.dimTarget === "otherCandles";
|
|
1876
2089
|
const candleDimOpacity = scrubCfg?.dimOpacity ?? 1;
|
|
1877
2090
|
const candleDimFadeMs = scrubCfg?.dimFadeMs ?? 0;
|
|
1878
|
-
// Destructure
|
|
2091
|
+
// Destructure SharedValues before entering worklets. Closing over the
|
|
1879
2092
|
// whole crosshair object would also serialize its RNGH gesture instances.
|
|
1880
2093
|
const scrubActive = crosshair.scrubActive;
|
|
1881
2094
|
// ChartCandleLayer is single-series only; useCrosshair always supplies this
|
|
1882
2095
|
// value (it is optional on the shared state type only for LiveChartSeries).
|
|
1883
2096
|
const scrubCandle = crosshair.scrubCandle!;
|
|
1884
|
-
|
|
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-
|
|
1885
2114
|
// strength pass can cover the base batch until the release fade completes.
|
|
1886
|
-
const
|
|
2115
|
+
const lastFocusBucket = useSharedValue<{ time: number } | null>(null);
|
|
1887
2116
|
useAnimatedReaction(
|
|
1888
|
-
() =>
|
|
1889
|
-
(
|
|
1890
|
-
if (
|
|
2117
|
+
() => currentFocusBucketTime.get(),
|
|
2118
|
+
(time) => {
|
|
2119
|
+
if (time !== null) lastFocusBucket.set({ time });
|
|
1891
2120
|
},
|
|
1892
2121
|
);
|
|
1893
2122
|
const inactiveCandleOpacity = useDerivedValue(() => {
|
|
1894
2123
|
const target =
|
|
1895
|
-
focusOtherCandles &&
|
|
2124
|
+
focusOtherCandles &&
|
|
2125
|
+
scrubActive.get() &&
|
|
2126
|
+
currentFocusBucketTime.get() !== null
|
|
1896
2127
|
? candleDimOpacity
|
|
1897
2128
|
: 1;
|
|
1898
2129
|
return candleDimFadeMs > 0
|
|
@@ -1901,33 +2132,36 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1901
2132
|
}, [
|
|
1902
2133
|
focusOtherCandles,
|
|
1903
2134
|
scrubActive,
|
|
1904
|
-
|
|
2135
|
+
currentFocusBucketTime,
|
|
1905
2136
|
candleDimOpacity,
|
|
1906
2137
|
candleDimFadeMs,
|
|
1907
2138
|
]);
|
|
1908
2139
|
const focusedCandleOpacity = useDerivedValue(() => {
|
|
1909
|
-
const
|
|
1910
|
-
const
|
|
1911
|
-
|
|
2140
|
+
const currentTime = currentFocusBucketTime.get();
|
|
2141
|
+
const focusBucket =
|
|
2142
|
+
currentTime === null ? lastFocusBucket.get() : { time: currentTime };
|
|
2143
|
+
if (!focusOtherCandles || !focusBucket) return 0;
|
|
1912
2144
|
return computeCandleFocusPassOpacity(
|
|
1913
2145
|
scrubActive.get(),
|
|
1914
|
-
|
|
2146
|
+
currentTime !== null,
|
|
1915
2147
|
inactiveCandleOpacity.get(),
|
|
1916
2148
|
);
|
|
1917
2149
|
}, [
|
|
1918
2150
|
focusOtherCandles,
|
|
1919
2151
|
scrubActive,
|
|
1920
|
-
|
|
1921
|
-
|
|
2152
|
+
currentFocusBucketTime,
|
|
2153
|
+
lastFocusBucket,
|
|
1922
2154
|
inactiveCandleOpacity,
|
|
1923
2155
|
]);
|
|
1924
2156
|
const focusedCandleClip = useDerivedValue(() => {
|
|
1925
2157
|
if (!focusOtherCandles) {
|
|
1926
2158
|
return HIDDEN_CANDLE_FOCUS_CLIP;
|
|
1927
2159
|
}
|
|
1928
|
-
const
|
|
2160
|
+
const currentTime = currentFocusBucketTime.get();
|
|
2161
|
+
const focusBucket =
|
|
2162
|
+
currentTime === null ? lastFocusBucket.get() : { time: currentTime };
|
|
1929
2163
|
return computeCandleFocusClip(
|
|
1930
|
-
|
|
2164
|
+
focusBucket,
|
|
1931
2165
|
effectivePadding,
|
|
1932
2166
|
engine.canvasWidth.get(),
|
|
1933
2167
|
engine.canvasHeight.get(),
|
|
@@ -1937,8 +2171,8 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1937
2171
|
);
|
|
1938
2172
|
}, [
|
|
1939
2173
|
focusOtherCandles,
|
|
1940
|
-
|
|
1941
|
-
|
|
2174
|
+
currentFocusBucketTime,
|
|
2175
|
+
lastFocusBucket,
|
|
1942
2176
|
effectivePadding,
|
|
1943
2177
|
engine.canvasWidth,
|
|
1944
2178
|
engine.canvasHeight,
|
|
@@ -1950,6 +2184,16 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1950
2184
|
return (
|
|
1951
2185
|
<Group opacity={seriesOpacity}>
|
|
1952
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
|
+
)}
|
|
1953
2197
|
{focusOtherCandles ? (
|
|
1954
2198
|
<>
|
|
1955
2199
|
<Group opacity={inactiveCandleOpacity}>
|
|
@@ -2032,11 +2276,13 @@ function ChartStack({
|
|
|
2032
2276
|
thresholdMarkerVisible,
|
|
2033
2277
|
thresholdBadgeVisible,
|
|
2034
2278
|
thresholdMarkerValue,
|
|
2279
|
+
thresholdCustomBadge,
|
|
2035
2280
|
thresholdSeriesPts,
|
|
2036
2281
|
formatValue,
|
|
2037
2282
|
lineGroupOpacity,
|
|
2038
2283
|
seriesOpacity,
|
|
2039
2284
|
linePath,
|
|
2285
|
+
lineGapsCfg,
|
|
2040
2286
|
lineIsLinear,
|
|
2041
2287
|
strokeWidth,
|
|
2042
2288
|
lineProp,
|
|
@@ -2181,6 +2427,7 @@ function ChartStack({
|
|
|
2181
2427
|
/>
|
|
2182
2428
|
) : null}
|
|
2183
2429
|
</Path>
|
|
2430
|
+
{lineGapsCfg && <ChartLineGapLayer model={model} />}
|
|
2184
2431
|
</Group>
|
|
2185
2432
|
</Group>
|
|
2186
2433
|
|
|
@@ -2256,7 +2503,7 @@ function ChartStack({
|
|
|
2256
2503
|
|
|
2257
2504
|
{/* Threshold label badge — on top of the line/dot/markers so it's never
|
|
2258
2505
|
painted over (the dashed marker line itself stays behind the line, above). */}
|
|
2259
|
-
{thresholdCfg?.line && (
|
|
2506
|
+
{thresholdCfg?.line && thresholdCustomBadge == null && (
|
|
2260
2507
|
<ThresholdBadgeOverlay
|
|
2261
2508
|
engine={engine}
|
|
2262
2509
|
padding={effectivePadding}
|
|
@@ -2310,15 +2557,27 @@ function ChartTradeStreamLayer({
|
|
|
2310
2557
|
tradeStream,
|
|
2311
2558
|
tradeStreamResolved,
|
|
2312
2559
|
effectivePadding,
|
|
2560
|
+
volumeBandHeight,
|
|
2313
2561
|
palette,
|
|
2314
2562
|
skiaFont,
|
|
2315
2563
|
reveal,
|
|
2316
2564
|
isStatic,
|
|
2317
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;
|
|
2318
2577
|
const tradeMarkers = useTradeStream(
|
|
2319
2578
|
engine,
|
|
2320
2579
|
tradeStream!,
|
|
2321
|
-
|
|
2580
|
+
tradeStreamPadding,
|
|
2322
2581
|
!isStatic,
|
|
2323
2582
|
!isStatic,
|
|
2324
2583
|
);
|
|
@@ -2327,7 +2586,7 @@ function ChartTradeStreamLayer({
|
|
|
2327
2586
|
<TradeStreamOverlay
|
|
2328
2587
|
markers={tradeMarkers}
|
|
2329
2588
|
palette={palette}
|
|
2330
|
-
padding={
|
|
2589
|
+
padding={tradeStreamPadding}
|
|
2331
2590
|
font={skiaFont}
|
|
2332
2591
|
opacity={reveal.dotOpacity}
|
|
2333
2592
|
labelOffsetX={tradeStreamResolved!.labelOffsetX}
|
|
@@ -2765,6 +3024,10 @@ function ChartView({
|
|
|
2765
3024
|
formatValue,
|
|
2766
3025
|
topLabelCfg,
|
|
2767
3026
|
bottomLabelCfg,
|
|
3027
|
+
thresholdCfg,
|
|
3028
|
+
thresholdMarkerLineY,
|
|
3029
|
+
thresholdBadgeVisible,
|
|
3030
|
+
thresholdCustomBadge,
|
|
2768
3031
|
markersActive,
|
|
2769
3032
|
renderMarker,
|
|
2770
3033
|
renderTooltip,
|
|
@@ -2878,6 +3141,19 @@ function ChartView({
|
|
|
2878
3141
|
/>
|
|
2879
3142
|
)}
|
|
2880
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
|
+
|
|
2881
3157
|
{/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
|
|
2882
3158
|
pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
|
|
2883
3159
|
a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
|
|
@@ -2900,6 +3176,7 @@ function ChartView({
|
|
|
2900
3176
|
scrubTime={crosshair.scrubTime}
|
|
2901
3177
|
scrubActive={crosshair.scrubActive}
|
|
2902
3178
|
scrubCandle={crosshair.scrubCandle}
|
|
3179
|
+
scrubGap={crosshair.scrubGap}
|
|
2903
3180
|
tooltipLayout={crosshair.tooltipLayout}
|
|
2904
3181
|
engine={engine}
|
|
2905
3182
|
padding={effectivePadding}
|