react-native-livechart 3.12.0 → 4.1.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/BadgeOverlay.d.ts +9 -1
- package/dist/components/BadgeOverlay.d.ts.map +1 -1
- package/dist/components/ChartOverlayLayer.d.ts +19 -0
- package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
- package/dist/components/CustomMarkerOverlay.d.ts +16 -6
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +7 -1
- package/dist/components/DotOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts +6 -3
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +4 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
- package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +25 -2
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts +7 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +12 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +29 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +62 -2
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +58 -7
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/grid.d.ts +13 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +20 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/volume.d.ts +25 -0
- package/dist/draw/volume.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/overlayScale.d.ts +22 -0
- package/dist/hooks/overlayScale.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +9 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +22 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +6 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartOverlayContext.d.ts +45 -0
- package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +8 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLiveDot.d.ts +6 -1
- package/dist/hooks/useLiveDot.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +9 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +82 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +74 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/useReferenceDrag.d.ts +24 -0
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +23 -4
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +5 -1
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
- package/dist/hooks/useVisibleRange.d.ts +39 -0
- package/dist/hooks/useVisibleRange.d.ts.map +1 -0
- package/dist/hooks/useYAxis.d.ts +1 -1
- package/dist/hooks/useYAxis.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +47 -0
- package/dist/math/markerCluster.d.ts.map +1 -0
- package/dist/math/markers.d.ts +10 -0
- package/dist/math/markers.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +24 -0
- package/dist/math/referenceDrag.d.ts.map +1 -0
- package/dist/math/referenceGroup.d.ts +31 -0
- package/dist/math/referenceGroup.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +33 -7
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +524 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BadgeOverlay.tsx +26 -1
- package/src/components/ChartOverlayLayer.tsx +32 -0
- package/src/components/CustomMarkerOverlay.tsx +150 -57
- package/src/components/CustomReferenceLineOverlay.tsx +250 -0
- package/src/components/CustomTooltipOverlay.tsx +27 -7
- package/src/components/DotOverlay.tsx +9 -0
- package/src/components/LiveChart.tsx +550 -32
- package/src/components/LiveChartSeries.tsx +111 -5
- package/src/components/MarkerOverlay.tsx +92 -6
- package/src/components/MultiSeriesDots.tsx +12 -3
- package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
- package/src/components/ReferenceLineOverlay.tsx +105 -32
- package/src/components/XAxisOverlay.tsx +9 -2
- package/src/components/YAxisOverlay.tsx +49 -28
- package/src/constants.ts +17 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +133 -2
- package/src/core/useLiveChartEngine.ts +111 -11
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +71 -2
- package/src/draw/markerAtlas.ts +117 -3
- package/src/draw/volume.ts +190 -0
- package/src/hooks/crosshairShared.ts +37 -9
- package/src/hooks/overlayScale.ts +59 -0
- package/src/hooks/resolveChartLayout.ts +33 -0
- package/src/hooks/useBadge.ts +89 -21
- package/src/hooks/useCandlePaths.ts +104 -7
- package/src/hooks/useChartOverlayContext.ts +93 -0
- package/src/hooks/useChartPaths.ts +14 -1
- package/src/hooks/useCrosshair.ts +33 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +39 -11
- package/src/hooks/usePanScroll.ts +253 -0
- package/src/hooks/usePinchZoom.ts +189 -0
- package/src/hooks/useReferenceDrag.ts +254 -0
- package/src/hooks/useReferenceLine.ts +56 -9
- package/src/hooks/useReferenceLinePress.ts +16 -2
- package/src/hooks/useVisibleRange.ts +119 -0
- package/src/hooks/useYAxis.ts +2 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +35 -3
- package/src/math/referenceDrag.ts +66 -0
- package/src/math/referenceGroup.ts +65 -0
- package/src/math/referenceLines.ts +76 -19
- package/src/types.ts +544 -21
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { useLayoutEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useAnimatedReaction,
|
|
6
|
+
useDerivedValue,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
} from "react-native-reanimated";
|
|
9
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Single-series live chart. UX and prop vocabulary parallel Benji Taylor’s
|
|
@@ -17,7 +22,7 @@ import {
|
|
|
17
22
|
vec,
|
|
18
23
|
} from "@shopify/react-native-skia";
|
|
19
24
|
|
|
20
|
-
import { DEFAULT_ACCENT_COLOR } from "../constants";
|
|
25
|
+
import { DEFAULT_ACCENT_COLOR, HOLD_TO_SCRUB_MS } from "../constants";
|
|
21
26
|
import {
|
|
22
27
|
resolveAreaDots,
|
|
23
28
|
resolveAxisLabel,
|
|
@@ -27,6 +32,7 @@ import {
|
|
|
27
32
|
resolveGradient,
|
|
28
33
|
resolveGridStyle,
|
|
29
34
|
resolveLeftEdgeFade,
|
|
35
|
+
resolveMarkerCluster,
|
|
30
36
|
resolveMetrics,
|
|
31
37
|
resolvePulse,
|
|
32
38
|
resolveScrub,
|
|
@@ -35,6 +41,8 @@ import {
|
|
|
35
41
|
resolveThreshold,
|
|
36
42
|
resolveTradeStream,
|
|
37
43
|
resolveValueLine,
|
|
44
|
+
resolveVolume,
|
|
45
|
+
resolveZoom,
|
|
38
46
|
resolveXAxis,
|
|
39
47
|
resolveYAxis,
|
|
40
48
|
} from "../core/resolveConfig";
|
|
@@ -47,6 +55,7 @@ import { useBadge } from "../hooks/useBadge";
|
|
|
47
55
|
import { useCandlePaths } from "../hooks/useCandlePaths";
|
|
48
56
|
import { useCanvasLayout } from "../hooks/useCanvasLayout";
|
|
49
57
|
import { useChartColors } from "../hooks/useChartColors";
|
|
58
|
+
import { useChartOverlayContext } from "../hooks/useChartOverlayContext";
|
|
50
59
|
import { useChartPaths } from "../hooks/useChartPaths";
|
|
51
60
|
import { useChartReveal } from "../hooks/useChartReveal";
|
|
52
61
|
import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
|
|
@@ -55,9 +64,13 @@ import { useDegen } from "../hooks/useDegen";
|
|
|
55
64
|
import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
|
|
56
65
|
import { useLiveDot } from "../hooks/useLiveDot";
|
|
57
66
|
import { useMarkers } from "../hooks/useMarkers";
|
|
67
|
+
import { useReferenceDrag } from "../hooks/useReferenceDrag";
|
|
58
68
|
import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
|
|
59
69
|
import { useModeBlend } from "../hooks/useModeBlend";
|
|
60
70
|
import { useMomentum } from "../hooks/useMomentum";
|
|
71
|
+
import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
|
|
72
|
+
import { usePinchZoom } from "../hooks/usePinchZoom";
|
|
73
|
+
import { useVisibleRange } from "../hooks/useVisibleRange";
|
|
61
74
|
import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
|
|
62
75
|
import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
|
|
63
76
|
import { useThreshold } from "../hooks/useThreshold";
|
|
@@ -69,7 +82,16 @@ import {
|
|
|
69
82
|
formatValue as defaultFormatValue,
|
|
70
83
|
} from "../lib/format";
|
|
71
84
|
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
|
|
72
|
-
import {
|
|
85
|
+
import { computeScrubDotY } from "../hooks/crosshairShared";
|
|
86
|
+
import {
|
|
87
|
+
groupReferenceLines,
|
|
88
|
+
type ReferenceGrouping,
|
|
89
|
+
} from "../math/referenceGroup";
|
|
90
|
+
import {
|
|
91
|
+
collectReferenceValues,
|
|
92
|
+
referenceLineForm,
|
|
93
|
+
resolveReferenceGroupBadge,
|
|
94
|
+
} from "../math/referenceLines";
|
|
73
95
|
import {
|
|
74
96
|
applyPaletteOverride,
|
|
75
97
|
leftEdgeFadeColorsFromBgRgb,
|
|
@@ -94,8 +116,13 @@ import {
|
|
|
94
116
|
labelConnector,
|
|
95
117
|
} from "./ExtremaConnectorOverlay";
|
|
96
118
|
import { CustomMarkerOverlay } from "./CustomMarkerOverlay";
|
|
119
|
+
import {
|
|
120
|
+
CustomReferenceLineOverlay,
|
|
121
|
+
customReferenceLineFlags,
|
|
122
|
+
} from "./CustomReferenceLineOverlay";
|
|
97
123
|
import { CustomTooltipOverlay } from "./CustomTooltipOverlay";
|
|
98
124
|
import { BadgeOverlay } from "./BadgeOverlay";
|
|
125
|
+
import { ChartOverlayLayer } from "./ChartOverlayLayer";
|
|
99
126
|
import { CrosshairOverlay } from "./CrosshairOverlay";
|
|
100
127
|
import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
|
|
101
128
|
import { DotOverlay } from "./DotOverlay";
|
|
@@ -104,6 +131,7 @@ import { LoadingOverlay } from "./LoadingOverlay";
|
|
|
104
131
|
import { MarkerOverlay } from "./MarkerOverlay";
|
|
105
132
|
import { MultiSeriesTooltipStack } from "./MultiSeriesTooltipStack";
|
|
106
133
|
import { ValueTextOverlay } from "./ValueTextOverlay";
|
|
134
|
+
import { ReferenceLineGroupOverlay } from "./ReferenceLineGroupOverlay";
|
|
107
135
|
import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
|
|
108
136
|
import { ScrubActionOverlay } from "./ScrubActionOverlay";
|
|
109
137
|
import { SegmentDividerOverlay } from "./SegmentDividerOverlay";
|
|
@@ -115,6 +143,14 @@ import { YAxisOverlay } from "./YAxisOverlay";
|
|
|
115
143
|
/** Translucent fill alpha for the threshold profit/loss band. */
|
|
116
144
|
const THRESHOLD_FILL_OPACITY = 0.16;
|
|
117
145
|
|
|
146
|
+
/** Stable empty grouping result (identity-stable so downstream worklets don't
|
|
147
|
+
* re-run) used when reference-line grouping is off. */
|
|
148
|
+
const EMPTY_GROUPING: ReferenceGrouping = { hidden: [], groups: [] };
|
|
149
|
+
|
|
150
|
+
/** Stable empty number array so the live-reference-values worklet stays
|
|
151
|
+
* referentially stable (no engine re-fit) when no line is draggable. */
|
|
152
|
+
const EMPTY_NUMS: number[] = [];
|
|
153
|
+
|
|
118
154
|
/**
|
|
119
155
|
* Color stops for the threshold's hard-split vertical gradient. Both arrays pair
|
|
120
156
|
* with the `[0, t, t, 1]` split positions: index 0–1 paint above the split,
|
|
@@ -144,6 +180,7 @@ function thresholdStops(
|
|
|
144
180
|
* here so the rendered pieces (`ChartStack`, `ChartScrubLayer`, `LiveChart`) stay
|
|
145
181
|
* small and presentational.
|
|
146
182
|
*/
|
|
183
|
+
|
|
147
184
|
function useLiveChartController({
|
|
148
185
|
// ── Data ────────────────────────────────────────────────────────────────
|
|
149
186
|
data,
|
|
@@ -164,6 +201,7 @@ function useLiveChartController({
|
|
|
164
201
|
candles,
|
|
165
202
|
candleWidth = 60,
|
|
166
203
|
liveCandle,
|
|
204
|
+
volume,
|
|
167
205
|
|
|
168
206
|
// ── Behaviour ───────────────────────────────────────────────────────────
|
|
169
207
|
timeWindow = 30,
|
|
@@ -177,6 +215,8 @@ function useLiveChartController({
|
|
|
177
215
|
maxValue,
|
|
178
216
|
windowBuffer = 0,
|
|
179
217
|
nowOverride,
|
|
218
|
+
timeScroll = false,
|
|
219
|
+
zoom = false,
|
|
180
220
|
accessibilityLabel,
|
|
181
221
|
accessibilityRole = "image",
|
|
182
222
|
emptyText = "No data",
|
|
@@ -207,10 +247,14 @@ function useLiveChartController({
|
|
|
207
247
|
tradeStream,
|
|
208
248
|
degen,
|
|
209
249
|
markers,
|
|
210
|
-
|
|
250
|
+
onMarkerPress,
|
|
211
251
|
markerHitRadius = 16,
|
|
252
|
+
markerCluster,
|
|
212
253
|
renderMarker,
|
|
213
254
|
renderTooltip,
|
|
255
|
+
renderOverlay,
|
|
256
|
+
renderReferenceLine,
|
|
257
|
+
referenceLineGrouping,
|
|
214
258
|
leftEdgeFade = true,
|
|
215
259
|
|
|
216
260
|
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
@@ -219,6 +263,8 @@ function useLiveChartController({
|
|
|
219
263
|
onReferenceLinePress,
|
|
220
264
|
onGestureStart,
|
|
221
265
|
onGestureEnd,
|
|
266
|
+
onVisibleRangeChange,
|
|
267
|
+
onReachStart,
|
|
222
268
|
onDegenShake,
|
|
223
269
|
}: LiveChartProps) {
|
|
224
270
|
const emptyTradeStream = useSharedValue<TradeEvent[]>([]);
|
|
@@ -239,6 +285,10 @@ function useLiveChartController({
|
|
|
239
285
|
const scrubCfg = resolveScrub(scrub);
|
|
240
286
|
// Static charts run no gestures, so scrub-action (tap/drag to lock a price) is off.
|
|
241
287
|
const scrubActionCfg = isStatic ? null : resolveScrubAction(scrubAction);
|
|
288
|
+
// Volume bars sit below the candles — a candle-mode-only feature (inert in
|
|
289
|
+
// line mode, like the candle paths themselves).
|
|
290
|
+
const volumeCfg = isCandle ? resolveVolume(volume) : null;
|
|
291
|
+
const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
|
|
242
292
|
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
243
293
|
// Dot-lattice area fill (clipped to the under-line region). Inert in candle
|
|
244
294
|
// mode, same as the gradient fill.
|
|
@@ -264,6 +314,77 @@ function useLiveChartController({
|
|
|
264
314
|
const allRefLines = referenceLines ?? [];
|
|
265
315
|
const refValues = collectReferenceValues(allRefLines);
|
|
266
316
|
|
|
317
|
+
// Per-line live value overrides + drag flags for draggable lines and the custom
|
|
318
|
+
// `renderReferenceLine` slot. One array SharedValue each (the line count varies,
|
|
319
|
+
// so a single SharedValue beats N hooks). Seeded from each line's static `value`;
|
|
320
|
+
// the drag gesture overwrites a slot, and the effect re-seeds slots not being
|
|
321
|
+
// dragged when the props change (so a controlled `value` flows back in).
|
|
322
|
+
const dragValues = useSharedValue<number[]>([]);
|
|
323
|
+
const dragActive = useSharedValue<boolean[]>([]);
|
|
324
|
+
// Last `value` props used to seed each slot, so reconciliation can tell a
|
|
325
|
+
// controlled prop change (adopt it) from an unchanged prop (keep the dragged
|
|
326
|
+
// value — uncontrolled persistence) without resetting a drop on every re-render.
|
|
327
|
+
const seededRef = useRef<(number | undefined)[]>([]);
|
|
328
|
+
const refValueSig = allRefLines.map((l) => l.value ?? "_").join(",");
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
const active = dragActive.value;
|
|
331
|
+
const cur = dragValues.value;
|
|
332
|
+
const seeded = seededRef.current;
|
|
333
|
+
dragValues.value = allRefLines.map((l, i) => {
|
|
334
|
+
const prop = l.value ?? 0;
|
|
335
|
+
if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
|
|
336
|
+
if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
|
|
337
|
+
return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
|
|
338
|
+
});
|
|
339
|
+
seededRef.current = allRefLines.map((l) => l.value);
|
|
340
|
+
if (dragActive.value.length !== allRefLines.length) {
|
|
341
|
+
dragActive.value = allRefLines.map((_, i) => active[i] ?? false);
|
|
342
|
+
}
|
|
343
|
+
// allRefLines is rebuilt every render; key off the value signature + length.
|
|
344
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
345
|
+
}, [refValueSig, allRefLines.length]);
|
|
346
|
+
|
|
347
|
+
// Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
|
|
348
|
+
// (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
|
|
349
|
+
const refLineCustom = customReferenceLineFlags(allRefLines, renderReferenceLine);
|
|
350
|
+
|
|
351
|
+
// Live Y values of the *draggable* Form-A lines, folded into the engine's
|
|
352
|
+
// axis-range fit so dragging a line toward / past the visible edge expands the
|
|
353
|
+
// range and the axis follows the finger in one motion (the committed values are
|
|
354
|
+
// already in `refValues`). `excludeFromRange` lines opt out, matching the static
|
|
355
|
+
// fit. Identity-stable (no re-fit) when nothing is draggable.
|
|
356
|
+
const draggableRefIdx = allRefLines
|
|
357
|
+
.map((l, i) =>
|
|
358
|
+
l.draggable && !l.excludeFromRange && referenceLineForm(l) === "line"
|
|
359
|
+
? i
|
|
360
|
+
: -1,
|
|
361
|
+
)
|
|
362
|
+
.filter((i) => i >= 0);
|
|
363
|
+
const liveRefValues = useDerivedValue<number[]>(() => {
|
|
364
|
+
if (draggableRefIdx.length === 0) return EMPTY_NUMS;
|
|
365
|
+
const dv = dragValues.get();
|
|
366
|
+
const out: number[] = [];
|
|
367
|
+
for (let k = 0; k < draggableRefIdx.length; k++) {
|
|
368
|
+
const v = dv[draggableRefIdx[k]];
|
|
369
|
+
if (v != null) out.push(v);
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Reference-line grouping (collapse near-value handles). Resolved once; the
|
|
375
|
+
// per-frame clustering runs on the UI thread (see ReferenceLineGroupOverlay).
|
|
376
|
+
const refGroupingCfg =
|
|
377
|
+
typeof referenceLineGrouping === "object"
|
|
378
|
+
? referenceLineGrouping
|
|
379
|
+
: undefined;
|
|
380
|
+
const refGroupingRadius = referenceLineGrouping
|
|
381
|
+
? (refGroupingCfg?.radius ?? 18)
|
|
382
|
+
: null;
|
|
383
|
+
// Count-pill styling (same style/shape config as a per-line badge) + count
|
|
384
|
+
// formatter. Resolved once; theme color defaults are applied in the overlay.
|
|
385
|
+
const refGroupBadge = resolveReferenceGroupBadge(refGroupingCfg?.badge);
|
|
386
|
+
const refGroupFormat = refGroupingCfg?.format;
|
|
387
|
+
|
|
267
388
|
const badgeUsesRightGutter =
|
|
268
389
|
badgeCfg !== null && (badgeCfg.position ?? "right") === "right";
|
|
269
390
|
|
|
@@ -305,6 +426,47 @@ function useLiveChartController({
|
|
|
305
426
|
palette.valueFontSize * 2,
|
|
306
427
|
);
|
|
307
428
|
|
|
429
|
+
// Per-badge font (size/family/weight) override; reuses the chart font when
|
|
430
|
+
// none of the badge font knobs are set, so the gutter sizing is unchanged.
|
|
431
|
+
const badgeHasFontOverride =
|
|
432
|
+
badgeCfg?.fontSize != null ||
|
|
433
|
+
badgeCfg?.fontFamily != null ||
|
|
434
|
+
badgeCfg?.fontWeight != null;
|
|
435
|
+
const badgeFontOverride = useChartSkiaFont(
|
|
436
|
+
badgeHasFontOverride
|
|
437
|
+
? {
|
|
438
|
+
...fontProp,
|
|
439
|
+
fontFamily: badgeCfg?.fontFamily ?? fontProp?.fontFamily,
|
|
440
|
+
fontSize: badgeCfg?.fontSize ?? fontProp?.fontSize,
|
|
441
|
+
fontWeight: badgeCfg?.fontWeight ?? fontProp?.fontWeight,
|
|
442
|
+
}
|
|
443
|
+
: fontProp,
|
|
444
|
+
MONO_FONT_FAMILY,
|
|
445
|
+
palette.labelFontSize,
|
|
446
|
+
);
|
|
447
|
+
const badgeFont = badgeHasFontOverride ? badgeFontOverride : skiaFont;
|
|
448
|
+
|
|
449
|
+
// Per-badge font for the grouping count pill (same override pattern as above).
|
|
450
|
+
const refGroupBadgeHasFont =
|
|
451
|
+
refGroupBadge.fontSize != null ||
|
|
452
|
+
refGroupBadge.fontFamily != null ||
|
|
453
|
+
refGroupBadge.fontWeight != null;
|
|
454
|
+
const refGroupBadgeFontOverride = useChartSkiaFont(
|
|
455
|
+
refGroupBadgeHasFont
|
|
456
|
+
? {
|
|
457
|
+
...fontProp,
|
|
458
|
+
fontFamily: refGroupBadge.fontFamily ?? fontProp?.fontFamily,
|
|
459
|
+
fontSize: refGroupBadge.fontSize ?? fontProp?.fontSize,
|
|
460
|
+
fontWeight: refGroupBadge.fontWeight ?? fontProp?.fontWeight,
|
|
461
|
+
}
|
|
462
|
+
: fontProp,
|
|
463
|
+
MONO_FONT_FAMILY,
|
|
464
|
+
palette.labelFontSize,
|
|
465
|
+
);
|
|
466
|
+
const refGroupBadgeFont = refGroupBadgeHasFont
|
|
467
|
+
? refGroupBadgeFontOverride
|
|
468
|
+
: skiaFont;
|
|
469
|
+
|
|
308
470
|
const pulseConfig = pulseCfg
|
|
309
471
|
? {
|
|
310
472
|
maxRadius: pulseCfg.maxRadius,
|
|
@@ -328,11 +490,26 @@ function useLiveChartController({
|
|
|
328
490
|
setValueLayoutSample(value.get());
|
|
329
491
|
}, [value]);
|
|
330
492
|
|
|
493
|
+
// `scrolledBack` mirrors the UI-thread scroll state (engine.viewEnd != null)
|
|
494
|
+
// onto the JS thread (set by the reaction below, after the engine exists).
|
|
495
|
+
const [scrolledBack, setScrolledBack] = useState(false);
|
|
496
|
+
const timeScrollEnabled = Boolean(timeScroll) && !isStatic;
|
|
497
|
+
const zoomCfg = resolveZoom(zoom);
|
|
498
|
+
const zoomEnabled = zoomCfg !== null && !isStatic;
|
|
499
|
+
|
|
500
|
+
const yAxisFloat = yAxisCfg?.float ?? false;
|
|
501
|
+
// With timeScroll, the floating full-width plot engages only while scrolled
|
|
502
|
+
// back; at the live edge the chart keeps a normal right gutter so the
|
|
503
|
+
// line/candles don't sit under the floating y-axis labels + badge. Without
|
|
504
|
+
// timeScroll, float behaves as before (always full-width).
|
|
505
|
+
const effectiveYAxisFloat =
|
|
506
|
+
yAxisFloat && (!timeScrollEnabled || scrolledBack);
|
|
331
507
|
const { strokeWidth, padding: effectivePadding } = resolveChartLayout({
|
|
332
508
|
palette,
|
|
333
509
|
lineWidthOverride: lineProp?.width,
|
|
334
510
|
insetsOverride: insets,
|
|
335
511
|
yAxis: yAxisCfg !== null,
|
|
512
|
+
yAxisFloat: effectiveYAxisFloat,
|
|
336
513
|
badge: badgeCfg !== null,
|
|
337
514
|
badgeMetrics: metricsCfg.badge,
|
|
338
515
|
badgeUsesRightGutter,
|
|
@@ -342,6 +519,7 @@ function useLiveChartController({
|
|
|
342
519
|
formatValue,
|
|
343
520
|
currentValue: valueLayoutSample,
|
|
344
521
|
pulse: pulseConfig,
|
|
522
|
+
volumeBandHeight,
|
|
345
523
|
});
|
|
346
524
|
|
|
347
525
|
// ── Reveal state ────────────────────────────────────────────
|
|
@@ -378,6 +556,7 @@ function useLiveChartController({
|
|
|
378
556
|
adaptiveSpeedBoost: metricsCfg.motion.adaptiveSpeedBoost,
|
|
379
557
|
exaggerate,
|
|
380
558
|
referenceValues: refValues,
|
|
559
|
+
liveReferenceValues: liveRefValues,
|
|
381
560
|
nonNegative,
|
|
382
561
|
maxValue,
|
|
383
562
|
windowBuffer,
|
|
@@ -387,6 +566,19 @@ function useLiveChartController({
|
|
|
387
566
|
liveCandle: isCandle ? liveEngine : liveCandle,
|
|
388
567
|
});
|
|
389
568
|
|
|
569
|
+
// Mirror the UI-thread scroll state to React so the floating y-axis can keep
|
|
570
|
+
// a right gutter at the live edge and collapse it only while scrolled back.
|
|
571
|
+
// Fires once per null↔frozen transition, and only matters for float+timeScroll.
|
|
572
|
+
useAnimatedReaction(
|
|
573
|
+
() => engine.viewEnd.value != null,
|
|
574
|
+
/* istanbul ignore next -- Reanimated reaction; state mirrored on the JS thread, not exercised under Jest */
|
|
575
|
+
(isScrolled, prev) => {
|
|
576
|
+
if (isScrolled !== prev && yAxisFloat && timeScrollEnabled) {
|
|
577
|
+
scheduleOnRN(setScrolledBack, isScrolled);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
);
|
|
581
|
+
|
|
390
582
|
// ── Mode crossfade (line ↔ candle) ──────────────────────────────────
|
|
391
583
|
const { lineGroupOpacity, candleGroupOpacity } = useModeBlend(
|
|
392
584
|
isCandle,
|
|
@@ -396,6 +588,41 @@ function useLiveChartController({
|
|
|
396
588
|
// ── Per-frame derived values ───────────────────────────────────────────
|
|
397
589
|
const { layoutWidth, layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
398
590
|
|
|
591
|
+
// Reference-line grouping: cluster Form-A lines by their per-frame value-Y so a
|
|
592
|
+
// stack of nearby orders collapses into one count handle. `groupHidden` suppresses
|
|
593
|
+
// the clustered lines' individual tags; the count pills read `refGroupResult`.
|
|
594
|
+
// Identity-stable (no work) when grouping is off.
|
|
595
|
+
const refGroupResult = useDerivedValue<ReferenceGrouping>(() => {
|
|
596
|
+
if (refGroupingRadius == null) return EMPTY_GROUPING;
|
|
597
|
+
const ch = engine.canvasHeight.get();
|
|
598
|
+
const dMin = engine.displayMin.get();
|
|
599
|
+
const dMax = engine.displayMax.get();
|
|
600
|
+
const top = effectivePadding.top;
|
|
601
|
+
const bottom = ch - effectivePadding.bottom;
|
|
602
|
+
const ys: number[] = [];
|
|
603
|
+
for (let i = 0; i < allRefLines.length; i++) {
|
|
604
|
+
const l = allRefLines[i];
|
|
605
|
+
// Skip bands and lines a custom `renderReferenceLine` owns — a custom tag
|
|
606
|
+
// draws itself and isn't suppressed by grouping, so folding it into a
|
|
607
|
+
// built-in count pill would double-count it (counted *and* still shown).
|
|
608
|
+
if (
|
|
609
|
+
referenceLineForm(l) !== "line" ||
|
|
610
|
+
l.value === undefined ||
|
|
611
|
+
refLineCustom[i]
|
|
612
|
+
) {
|
|
613
|
+
ys.push(-1);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const v = dragValues.get()[i] ?? l.value;
|
|
617
|
+
const y = computeScrubDotY(v, dMin, dMax, ch, top, effectivePadding.bottom);
|
|
618
|
+
ys.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
|
|
619
|
+
}
|
|
620
|
+
return groupReferenceLines(ys, refGroupingRadius);
|
|
621
|
+
});
|
|
622
|
+
const groupHidden = useDerivedValue<boolean[]>(
|
|
623
|
+
() => refGroupResult.get().hidden,
|
|
624
|
+
);
|
|
625
|
+
|
|
399
626
|
// Threshold split geometry (shared by stroke gradient, fill band, marker line).
|
|
400
627
|
// Called unconditionally with a stand-in value when no threshold is set.
|
|
401
628
|
const thresholdGeom = useThreshold(
|
|
@@ -419,6 +646,11 @@ function useLiveChartController({
|
|
|
419
646
|
// Only build the band path when the fill is actually on.
|
|
420
647
|
thresholdCfg?.fill ? thresholdGeom.lineY : undefined,
|
|
421
648
|
lineIsLinear,
|
|
649
|
+
// Tip the line at the view-edge price (not the live value) while scrolled,
|
|
650
|
+
// matching the live dot — so the right edge doesn't drop to the off-screen
|
|
651
|
+
// live value when `followViewEdge` tracks the scrolled-back window.
|
|
652
|
+
engine.edgeValue,
|
|
653
|
+
badgeCfg?.followViewEdge ?? false,
|
|
422
654
|
);
|
|
423
655
|
|
|
424
656
|
// Area-dots fill shader color as a vec4 (channels 0..1), with the config
|
|
@@ -436,19 +668,36 @@ function useLiveChartController({
|
|
|
436
668
|
adA * (areaDotsCfg?.opacity ?? 1),
|
|
437
669
|
];
|
|
438
670
|
|
|
439
|
-
const {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
671
|
+
const {
|
|
672
|
+
upBodiesPath,
|
|
673
|
+
downBodiesPath,
|
|
674
|
+
upWicksPath,
|
|
675
|
+
downWicksPath,
|
|
676
|
+
upBarsPath,
|
|
677
|
+
downBarsPath,
|
|
678
|
+
} = useCandlePaths(
|
|
679
|
+
engine,
|
|
680
|
+
effectivePadding,
|
|
681
|
+
// Match engine: stashed candles while reverse-morphing in candle mode.
|
|
682
|
+
isCandle ? candlesEngine : candles,
|
|
683
|
+
isCandle ? liveEngine : liveCandle,
|
|
684
|
+
candleWidth,
|
|
685
|
+
isCandle,
|
|
686
|
+
metricsCfg.candle,
|
|
687
|
+
volumeBandHeight,
|
|
688
|
+
volumeCfg?.radius ?? 0,
|
|
689
|
+
!isStatic, // static: no candle-width lerp loop
|
|
690
|
+
);
|
|
691
|
+
const { dotX, dotY } = useLiveDot(
|
|
692
|
+
engine,
|
|
693
|
+
effectivePadding,
|
|
694
|
+
engine.edgeValue,
|
|
695
|
+
badgeCfg?.followViewEdge ?? false,
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
// Price↔pixel / time↔pixel bridge for a custom `renderOverlay`. Built
|
|
699
|
+
// unconditionally (hooks rule); only mounted when `renderOverlay` is provided.
|
|
700
|
+
const overlayContext = useChartOverlayContext(engine, effectivePadding);
|
|
452
701
|
|
|
453
702
|
const momentumSV = useMomentum(engine, momentum);
|
|
454
703
|
|
|
@@ -474,6 +723,7 @@ function useLiveChartController({
|
|
|
474
723
|
skiaFont,
|
|
475
724
|
yAxisCfg?.minGap ?? 36,
|
|
476
725
|
metricsCfg.grid,
|
|
726
|
+
yAxisCfg?.count ?? 0,
|
|
477
727
|
);
|
|
478
728
|
|
|
479
729
|
const { xAxisEntries } = useXAxis(
|
|
@@ -488,7 +738,7 @@ function useLiveChartController({
|
|
|
488
738
|
effectivePadding,
|
|
489
739
|
palette,
|
|
490
740
|
formatValue,
|
|
491
|
-
|
|
741
|
+
badgeFont,
|
|
492
742
|
badgeCfg?.variant ?? "default",
|
|
493
743
|
badgeCfg?.tail ?? true,
|
|
494
744
|
momentumSV,
|
|
@@ -496,6 +746,11 @@ function useLiveChartController({
|
|
|
496
746
|
badgeCfg?.background,
|
|
497
747
|
metricsCfg.badge,
|
|
498
748
|
metricsCfg.motion.badgeColorSpeed,
|
|
749
|
+
effectiveYAxisFloat,
|
|
750
|
+
engine.edgeValue,
|
|
751
|
+
badgeCfg?.followViewEdge ?? false,
|
|
752
|
+
badgeCfg?.radius,
|
|
753
|
+
badgeCfg?.textColor,
|
|
499
754
|
);
|
|
500
755
|
|
|
501
756
|
// Scrub/crosshair must see the same stash-backed candles as the engine.
|
|
@@ -509,6 +764,7 @@ function useLiveChartController({
|
|
|
509
764
|
: undefined;
|
|
510
765
|
|
|
511
766
|
const markersActive = markers != null;
|
|
767
|
+
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
512
768
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
513
769
|
// self-projects, so we only need the gesture + hit-test here. Built BEFORE
|
|
514
770
|
// `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
|
|
@@ -518,11 +774,12 @@ function useLiveChartController({
|
|
|
518
774
|
markersSV,
|
|
519
775
|
!isStatic && markersActive,
|
|
520
776
|
markerHitRadius,
|
|
521
|
-
|
|
777
|
+
onMarkerPress,
|
|
522
778
|
undefined, // seriesSV — single-series has none
|
|
523
779
|
engine.data, // anchor value-less markers to the line
|
|
524
780
|
!isStatic, // static: no marker-projection loop
|
|
525
781
|
lineIsLinear, // match marker anchoring to the rendered curve
|
|
782
|
+
markerClusterCfg, // co-located marker stacking / collapse
|
|
526
783
|
);
|
|
527
784
|
|
|
528
785
|
// Pressable reference-line badges (working orders / alerts). Built before
|
|
@@ -538,6 +795,7 @@ function useLiveChartController({
|
|
|
538
795
|
!isStatic && refPressActive,
|
|
539
796
|
markerHitRadius,
|
|
540
797
|
onReferenceLinePress,
|
|
798
|
+
dragValues,
|
|
541
799
|
);
|
|
542
800
|
|
|
543
801
|
// Combined "defer" hit-test for the scrub-action place-tap: yield to a marker or
|
|
@@ -549,6 +807,24 @@ function useLiveChartController({
|
|
|
549
807
|
return markerHitTest(x, y) || refLineHitTest(x, y);
|
|
550
808
|
};
|
|
551
809
|
|
|
810
|
+
// Time-scroll activation. `holdToScrub`: a quick one-finger drag scrolls while
|
|
811
|
+
// scrub engages on press-and-hold — so the scrub gesture needs a long-press
|
|
812
|
+
// delay (unless the caller set its own `panGestureDelay`). `timeScrollEnabled`
|
|
813
|
+
// is computed earlier (it gates the float gutter).
|
|
814
|
+
const scrollGestureMode =
|
|
815
|
+
typeof timeScroll === "object"
|
|
816
|
+
? (timeScroll.gesture ?? "holdToScrub")
|
|
817
|
+
: "holdToScrub";
|
|
818
|
+
// In holdToScrub the scrub MUST require a hold so a quick drag scrolls instead.
|
|
819
|
+
// Precedence: explicit timeScroll.scrubHoldMs, then scrub.panGestureDelay, then
|
|
820
|
+
// the default. `||` (not `??`) skips the resolved panGestureDelay's 0 default.
|
|
821
|
+
const timeScrollHoldMs =
|
|
822
|
+
typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
|
|
823
|
+
const scrubHoldMs =
|
|
824
|
+
timeScrollEnabled && scrollGestureMode === "holdToScrub"
|
|
825
|
+
? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
|
|
826
|
+
: (scrubCfg?.panGestureDelay ?? 0);
|
|
827
|
+
|
|
552
828
|
const crosshair = useCrosshair(
|
|
553
829
|
engine,
|
|
554
830
|
effectivePadding,
|
|
@@ -561,7 +837,7 @@ function useLiveChartController({
|
|
|
561
837
|
!isStatic && (scrubCfg !== null || scrubActionCfg !== null),
|
|
562
838
|
onScrub,
|
|
563
839
|
candleOpts,
|
|
564
|
-
|
|
840
|
+
scrubHoldMs,
|
|
565
841
|
onGestureStart,
|
|
566
842
|
onGestureEnd,
|
|
567
843
|
scrubActionCfg,
|
|
@@ -572,8 +848,75 @@ function useLiveChartController({
|
|
|
572
848
|
scrubCfg?.tooltipShowValue ?? true,
|
|
573
849
|
scrubCfg?.tooltipShowTime ?? true,
|
|
574
850
|
scrubCfg?.tooltipMargin ?? 8,
|
|
851
|
+
// Axis-drag time-scroll: keep the bottom "time ruler" band scroll-only so a
|
|
852
|
+
// drag there never trips the scrub crosshair.
|
|
853
|
+
timeScrollEnabled && scrollGestureMode === "axisDrag"
|
|
854
|
+
? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
|
|
855
|
+
: 0,
|
|
575
856
|
);
|
|
576
857
|
|
|
858
|
+
// ── Time-scroll (drag back through history) ───────────────────────────────
|
|
859
|
+
// Experimental: a pan freezes the window at an absolute time and resumes
|
|
860
|
+
// following once dragged back to the live edge. Pan is clamped to the earliest
|
|
861
|
+
// retained point (line or candle). See `timeScroll` for the gesture model.
|
|
862
|
+
const scrollMinTime = useDerivedValue(() => {
|
|
863
|
+
const src = isCandle ? candlesEngine.get() : lineEngineData.get();
|
|
864
|
+
return src.length > 0 ? src[0].time : engine.liveEdge.get();
|
|
865
|
+
});
|
|
866
|
+
const panScrollGesture = usePanScroll({
|
|
867
|
+
engine,
|
|
868
|
+
padding: effectivePadding,
|
|
869
|
+
minTime: scrollMinTime,
|
|
870
|
+
enabled: timeScrollEnabled,
|
|
871
|
+
mode: scrollGestureMode,
|
|
872
|
+
// Clear any live crosshair when a scroll drag takes over.
|
|
873
|
+
onScrollStart: () => {
|
|
874
|
+
"worklet";
|
|
875
|
+
crosshair.scrubActive.set(false);
|
|
876
|
+
},
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Draggable reference lines: a per-line vertical pan that grabs a line near its
|
|
880
|
+
// value and drags it along the Y-axis (with snap / bounds / callbacks). Built
|
|
881
|
+
// unconditionally for stable hook order; the gesture self-disables when no line
|
|
882
|
+
// opts in, and it's only composed into the root when `refDragEnabled`.
|
|
883
|
+
const refDragEnabled =
|
|
884
|
+
!isStatic && allRefLines.some((l) => l.draggable === true);
|
|
885
|
+
const refDragGesture = useReferenceDrag(
|
|
886
|
+
engine,
|
|
887
|
+
effectivePadding,
|
|
888
|
+
allRefLines,
|
|
889
|
+
dragValues,
|
|
890
|
+
dragActive,
|
|
891
|
+
!isStatic,
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
// Pinch-to-zoom the visible window (two-finger). Anchors at the focal point and
|
|
895
|
+
// writes viewWindow + viewEnd; composes via Simultaneous (it's two-finger, so
|
|
896
|
+
// disjoint from the one-finger pan/scrub). See `zoom`.
|
|
897
|
+
const pinchZoomGesture = usePinchZoom({
|
|
898
|
+
engine,
|
|
899
|
+
padding: effectivePadding,
|
|
900
|
+
minTime: scrollMinTime,
|
|
901
|
+
timeWindow,
|
|
902
|
+
enabled: zoomEnabled,
|
|
903
|
+
minTimeWindow: zoomCfg?.minTimeWindow,
|
|
904
|
+
maxTimeWindow: zoomCfg?.maxTimeWindow,
|
|
905
|
+
onZoomStart: () => {
|
|
906
|
+
"worklet";
|
|
907
|
+
crosshair.scrubActive.set(false);
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// Paging callbacks: report the visible range / proximity to the oldest data so
|
|
912
|
+
// a host can lazily load history. Inert unless a callback is supplied.
|
|
913
|
+
useVisibleRange({
|
|
914
|
+
engine,
|
|
915
|
+
minTime: scrollMinTime,
|
|
916
|
+
onVisibleRangeChange,
|
|
917
|
+
onReachStart,
|
|
918
|
+
});
|
|
919
|
+
|
|
577
920
|
// Scrub-action composes a Tap (place/move the reticle, press the badge, dismiss)
|
|
578
921
|
// ahead of the pan via Exclusive, so a tap is tried first and only becomes a
|
|
579
922
|
// drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
|
|
@@ -599,12 +942,37 @@ function useLiveChartController({
|
|
|
599
942
|
overlayTaps.length === 1
|
|
600
943
|
? overlayTaps[0]
|
|
601
944
|
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
602
|
-
//
|
|
603
|
-
//
|
|
945
|
+
// Always `Simultaneous`, never `Race`: on iOS the scrub pan uses
|
|
946
|
+
// `minDistance(0)`, so in a `Race` it activates on touch-down and cancels the
|
|
947
|
+
// overlay tap before it can recognize — `onMarkerPress`/`onReferenceLinePress`
|
|
948
|
+
// would never fire. Sharing the gesture space lets the tap recognize; the pan
|
|
949
|
+
// defers to a marker/badge under the finger via `deferTapHit` (see
|
|
950
|
+
// `useCrosshair`'s scrub `onStart`) so no stray crosshair is dropped there.
|
|
951
|
+
rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Compose the pan-scroll gesture. holdToScrub races the scrub/tap gestures (a
|
|
955
|
+
// quick drag scrolls; a still press-hold falls through to scrub). Axis-drag
|
|
956
|
+
// goes first via Exclusive: it fails instantly outside the axis band, so scrub
|
|
957
|
+
// runs everywhere else.
|
|
958
|
+
if (timeScrollEnabled) {
|
|
604
959
|
rootGesture =
|
|
605
|
-
|
|
606
|
-
? Gesture.
|
|
607
|
-
: Gesture.Race(
|
|
960
|
+
scrollGestureMode === "axisDrag"
|
|
961
|
+
? Gesture.Exclusive(panScrollGesture, rootGesture)
|
|
962
|
+
: Gesture.Race(panScrollGesture, rootGesture);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Draggable reference lines take priority: a vertical grab on a line drags it.
|
|
966
|
+
// The manual-activation pan fails fast off any line (or on horizontal intent), so
|
|
967
|
+
// Exclusive falls through to scrub / scroll everywhere else.
|
|
968
|
+
if (refDragEnabled) {
|
|
969
|
+
rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Pinch runs alongside everything else (two-finger, so it never competes with
|
|
973
|
+
// the one-finger pan/scrub/tap gestures for the same touch).
|
|
974
|
+
if (zoomEnabled) {
|
|
975
|
+
rootGesture = Gesture.Simultaneous(rootGesture, pinchZoomGesture);
|
|
608
976
|
}
|
|
609
977
|
|
|
610
978
|
// ── Derived render values ──────────────────────────────────────────────
|
|
@@ -662,6 +1030,7 @@ function useLiveChartController({
|
|
|
662
1030
|
extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
|
|
663
1031
|
// configs
|
|
664
1032
|
yAxisCfg,
|
|
1033
|
+
yAxisFloat: effectiveYAxisFloat,
|
|
665
1034
|
xAxisCfg,
|
|
666
1035
|
badgeCfg,
|
|
667
1036
|
scrubCfg,
|
|
@@ -679,6 +1048,16 @@ function useLiveChartController({
|
|
|
679
1048
|
leftEdgeFadeCfg,
|
|
680
1049
|
metricsCfg,
|
|
681
1050
|
allRefLines,
|
|
1051
|
+
refLineCustom,
|
|
1052
|
+
dragValues,
|
|
1053
|
+
dragActive,
|
|
1054
|
+
renderReferenceLine,
|
|
1055
|
+
refGroupingActive: refGroupingRadius != null,
|
|
1056
|
+
refGroupResult,
|
|
1057
|
+
groupHidden,
|
|
1058
|
+
refGroupBadge,
|
|
1059
|
+
refGroupBadgeFont,
|
|
1060
|
+
refGroupFormat,
|
|
682
1061
|
resolvedSegments,
|
|
683
1062
|
hasRecolorSegments,
|
|
684
1063
|
segmentGradient,
|
|
@@ -690,7 +1069,9 @@ function useLiveChartController({
|
|
|
690
1069
|
// theme / layout / fonts
|
|
691
1070
|
palette,
|
|
692
1071
|
skiaFont,
|
|
1072
|
+
fontProp,
|
|
693
1073
|
valueFont,
|
|
1074
|
+
badgeFont,
|
|
694
1075
|
strokeWidth,
|
|
695
1076
|
effectivePadding,
|
|
696
1077
|
// engine + reveal
|
|
@@ -715,6 +1096,15 @@ function useLiveChartController({
|
|
|
715
1096
|
downBodiesPath,
|
|
716
1097
|
upWicksPath,
|
|
717
1098
|
downWicksPath,
|
|
1099
|
+
upBarsPath,
|
|
1100
|
+
downBarsPath,
|
|
1101
|
+
// Volume bars: active flag, fade-in opacity, and resolved colors (default to
|
|
1102
|
+
// the candle palette). The reserved band height is read by the x-axis.
|
|
1103
|
+
volumeActive: volumeCfg !== null,
|
|
1104
|
+
volumeBandHeight,
|
|
1105
|
+
volumeOpacity: volumeCfg?.opacity ?? 1,
|
|
1106
|
+
volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
|
|
1107
|
+
volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
|
|
718
1108
|
dotX,
|
|
719
1109
|
dotY,
|
|
720
1110
|
liveDotOpacity,
|
|
@@ -730,8 +1120,11 @@ function useLiveChartController({
|
|
|
730
1120
|
rootGesture,
|
|
731
1121
|
markersActive,
|
|
732
1122
|
markersSV,
|
|
1123
|
+
markerClusterCfg,
|
|
733
1124
|
renderMarker,
|
|
734
1125
|
renderTooltip,
|
|
1126
|
+
renderOverlay,
|
|
1127
|
+
overlayContext,
|
|
735
1128
|
// selection dot: resolved config + fallback color (the chart line/accent color)
|
|
736
1129
|
selectionDot: selectionDotCfg,
|
|
737
1130
|
selectionColor: lineProp?.color ?? palette.line,
|
|
@@ -756,6 +1149,7 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
756
1149
|
const {
|
|
757
1150
|
degenShakeTransform,
|
|
758
1151
|
yAxisCfg,
|
|
1152
|
+
yAxisFloat,
|
|
759
1153
|
reveal,
|
|
760
1154
|
yAxisEntries,
|
|
761
1155
|
engine,
|
|
@@ -781,10 +1175,13 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
781
1175
|
|
|
782
1176
|
return (
|
|
783
1177
|
<Group transform={degenShakeTransform}>
|
|
784
|
-
{/* Y-axis grid
|
|
1178
|
+
{/* Y-axis. Default: grid + labels here (in a reserved gutter). Floating
|
|
1179
|
+
mode: grid only — the labels + a soft edge fade draw above the candles
|
|
1180
|
+
in ChartStack so the plot runs full-width and candles dim under them. */}
|
|
785
1181
|
{yAxisCfg && (
|
|
786
1182
|
<Group opacity={reveal.yAxisOpacity}>
|
|
787
1183
|
<YAxisOverlay
|
|
1184
|
+
variant={yAxisFloat ? "grid" : "all"}
|
|
788
1185
|
entries={yAxisEntries}
|
|
789
1186
|
engine={engine}
|
|
790
1187
|
padding={effectivePadding}
|
|
@@ -857,10 +1254,12 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
857
1254
|
effectivePadding,
|
|
858
1255
|
palette,
|
|
859
1256
|
skiaFont,
|
|
1257
|
+
fontProp,
|
|
860
1258
|
badgeCfg,
|
|
861
1259
|
valueLineCfg,
|
|
862
1260
|
dotY,
|
|
863
1261
|
allRefLines,
|
|
1262
|
+
dragValues,
|
|
864
1263
|
resolvedSegments,
|
|
865
1264
|
hasRecolorSegments,
|
|
866
1265
|
segmentGradient,
|
|
@@ -878,6 +1277,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
878
1277
|
downWicksPath,
|
|
879
1278
|
upBodiesPath,
|
|
880
1279
|
downBodiesPath,
|
|
1280
|
+
upBarsPath,
|
|
1281
|
+
downBarsPath,
|
|
1282
|
+
volumeActive,
|
|
1283
|
+
volumeBandHeight,
|
|
1284
|
+
volumeOpacity,
|
|
1285
|
+
volumeUpColor,
|
|
1286
|
+
volumeDownColor,
|
|
881
1287
|
xAxisCfg,
|
|
882
1288
|
xAxisEntries,
|
|
883
1289
|
dotX,
|
|
@@ -889,10 +1295,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
889
1295
|
degenPackRevision,
|
|
890
1296
|
markersActive,
|
|
891
1297
|
markersSV,
|
|
1298
|
+
markerClusterCfg,
|
|
892
1299
|
renderMarker,
|
|
893
1300
|
emptyText,
|
|
894
1301
|
metricsCfg,
|
|
895
1302
|
layoutWidth,
|
|
1303
|
+
yAxisCfg,
|
|
1304
|
+
yAxisFloat,
|
|
1305
|
+
yAxisEntries,
|
|
1306
|
+
badgeUsesRightGutter,
|
|
1307
|
+
gridStyleCfg,
|
|
896
1308
|
} = model;
|
|
897
1309
|
|
|
898
1310
|
return (
|
|
@@ -935,6 +1347,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
935
1347
|
palette={palette}
|
|
936
1348
|
formatValue={formatValue}
|
|
937
1349
|
font={skiaFont}
|
|
1350
|
+
fontProp={fontProp}
|
|
1351
|
+
dragValues={dragValues}
|
|
1352
|
+
index={i}
|
|
938
1353
|
/>
|
|
939
1354
|
))}
|
|
940
1355
|
|
|
@@ -997,13 +1412,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
997
1412
|
<Path
|
|
998
1413
|
path={upWicksPath}
|
|
999
1414
|
style="stroke"
|
|
1000
|
-
strokeWidth={
|
|
1415
|
+
strokeWidth={metricsCfg.candle.wickWidth}
|
|
1001
1416
|
color={palette.wickUp}
|
|
1002
1417
|
/>
|
|
1003
1418
|
<Path
|
|
1004
1419
|
path={downWicksPath}
|
|
1005
1420
|
style="stroke"
|
|
1006
|
-
strokeWidth={
|
|
1421
|
+
strokeWidth={metricsCfg.candle.wickWidth}
|
|
1007
1422
|
color={palette.wickDown}
|
|
1008
1423
|
/>
|
|
1009
1424
|
<Path path={upBodiesPath} style="fill" color={palette.candleUp} />
|
|
@@ -1014,7 +1429,41 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1014
1429
|
/>
|
|
1015
1430
|
</Group>
|
|
1016
1431
|
|
|
1017
|
-
{/*
|
|
1432
|
+
{/* Volume bars in the reserved band below the candles. Fades in with the
|
|
1433
|
+
candle group (outer opacity); the config opacity dims the whole band
|
|
1434
|
+
(inner). Up/down bars carry their own colors (default candle palette). */}
|
|
1435
|
+
{volumeActive && (
|
|
1436
|
+
<Group opacity={candleGroupOpacity}>
|
|
1437
|
+
<Group opacity={volumeOpacity}>
|
|
1438
|
+
<Path path={upBarsPath} style="fill" color={volumeUpColor} />
|
|
1439
|
+
<Path path={downBarsPath} style="fill" color={volumeDownColor} />
|
|
1440
|
+
</Group>
|
|
1441
|
+
</Group>
|
|
1442
|
+
)}
|
|
1443
|
+
|
|
1444
|
+
{/* Floating axis: the labels float ABOVE the candles (right-aligned at the
|
|
1445
|
+
edge) so the plot runs full-width and candles stay fully visible behind
|
|
1446
|
+
them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
|
|
1447
|
+
{yAxisCfg && yAxisFloat && (
|
|
1448
|
+
<Group opacity={reveal.yAxisOpacity}>
|
|
1449
|
+
<YAxisOverlay
|
|
1450
|
+
variant="labels"
|
|
1451
|
+
float
|
|
1452
|
+
entries={yAxisEntries}
|
|
1453
|
+
engine={engine}
|
|
1454
|
+
padding={effectivePadding}
|
|
1455
|
+
palette={palette}
|
|
1456
|
+
font={skiaFont}
|
|
1457
|
+
badge={badgeUsesRightGutter}
|
|
1458
|
+
badgeTail={badgeCfg?.tail ?? true}
|
|
1459
|
+
badgeMetrics={metricsCfg.badge}
|
|
1460
|
+
gridStyle={gridStyleCfg}
|
|
1461
|
+
/>
|
|
1462
|
+
</Group>
|
|
1463
|
+
)}
|
|
1464
|
+
|
|
1465
|
+
{/* X-axis time labels. With a volume band the bottom padding is inflated by
|
|
1466
|
+
the band height; pass it so the axis shifts back to the very bottom. */}
|
|
1018
1467
|
{xAxisCfg && (
|
|
1019
1468
|
<XAxisOverlay
|
|
1020
1469
|
entries={xAxisEntries}
|
|
@@ -1022,6 +1471,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1022
1471
|
padding={effectivePadding}
|
|
1023
1472
|
palette={palette}
|
|
1024
1473
|
font={skiaFont}
|
|
1474
|
+
volumeBandHeight={volumeBandHeight}
|
|
1025
1475
|
/>
|
|
1026
1476
|
)}
|
|
1027
1477
|
|
|
@@ -1039,6 +1489,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1039
1489
|
radius={dotCfg.radius}
|
|
1040
1490
|
ring={dotCfg.ring}
|
|
1041
1491
|
color={dotCfg.color}
|
|
1492
|
+
viewEnd={engine.viewEnd}
|
|
1042
1493
|
/>
|
|
1043
1494
|
</Group>
|
|
1044
1495
|
)}
|
|
@@ -1069,6 +1520,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1069
1520
|
lineData={engine.data}
|
|
1070
1521
|
lineLinear={lineIsLinear}
|
|
1071
1522
|
renderMarker={renderMarker}
|
|
1523
|
+
cluster={markerClusterCfg}
|
|
1072
1524
|
/>
|
|
1073
1525
|
</Group>
|
|
1074
1526
|
)}
|
|
@@ -1242,12 +1694,19 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
|
|
|
1242
1694
|
/** Live-price badge, drawn above the scrub dim so the dim never clips its left
|
|
1243
1695
|
* edge. Shares the degen shake transform so it tracks the shaken stack. */
|
|
1244
1696
|
function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1245
|
-
const { badgeCfg, badgeData,
|
|
1697
|
+
const { badgeCfg, badgeData, badgeFont, reveal, degenShakeTransform } = model;
|
|
1246
1698
|
if (!badgeCfg) return null;
|
|
1247
1699
|
return (
|
|
1248
1700
|
<Group transform={degenShakeTransform}>
|
|
1249
1701
|
<Group opacity={reveal.badgeOpacity}>
|
|
1250
|
-
<BadgeOverlay
|
|
1702
|
+
<BadgeOverlay
|
|
1703
|
+
badge={badgeData}
|
|
1704
|
+
font={badgeFont}
|
|
1705
|
+
borderColor={badgeCfg.borderColor}
|
|
1706
|
+
borderWidth={badgeCfg.borderWidth}
|
|
1707
|
+
offsetX={badgeCfg.offsetX}
|
|
1708
|
+
offsetY={badgeCfg.offsetY}
|
|
1709
|
+
/>
|
|
1251
1710
|
</Group>
|
|
1252
1711
|
</Group>
|
|
1253
1712
|
);
|
|
@@ -1260,11 +1719,20 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
1260
1719
|
function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1261
1720
|
const {
|
|
1262
1721
|
allRefLines,
|
|
1722
|
+
refLineCustom,
|
|
1723
|
+
dragValues,
|
|
1724
|
+
groupHidden,
|
|
1725
|
+
refGroupResult,
|
|
1726
|
+
refGroupingActive,
|
|
1727
|
+
refGroupBadge,
|
|
1728
|
+
refGroupBadgeFont,
|
|
1729
|
+
refGroupFormat,
|
|
1263
1730
|
engine,
|
|
1264
1731
|
effectivePadding,
|
|
1265
1732
|
palette,
|
|
1266
1733
|
formatValue,
|
|
1267
1734
|
skiaFont,
|
|
1735
|
+
fontProp,
|
|
1268
1736
|
degenShakeTransform,
|
|
1269
1737
|
} = model;
|
|
1270
1738
|
if (allRefLines.length === 0) return null;
|
|
@@ -1279,9 +1747,26 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
1279
1747
|
palette={palette}
|
|
1280
1748
|
formatValue={formatValue}
|
|
1281
1749
|
font={skiaFont}
|
|
1750
|
+
fontProp={fontProp}
|
|
1282
1751
|
badgeLayer
|
|
1752
|
+
dragValues={dragValues}
|
|
1753
|
+
index={i}
|
|
1754
|
+
suppressTag={refLineCustom[i]}
|
|
1755
|
+
groupHidden={refGroupingActive ? groupHidden : undefined}
|
|
1283
1756
|
/>
|
|
1284
1757
|
))}
|
|
1758
|
+
{/* Collapsed count handles for grouped (near-value) lines. */}
|
|
1759
|
+
{refGroupingActive && (
|
|
1760
|
+
<ReferenceLineGroupOverlay
|
|
1761
|
+
grouping={refGroupResult}
|
|
1762
|
+
padding={effectivePadding}
|
|
1763
|
+
canvasWidth={engine.canvasWidth}
|
|
1764
|
+
palette={palette}
|
|
1765
|
+
font={refGroupBadgeFont}
|
|
1766
|
+
badge={refGroupBadge}
|
|
1767
|
+
format={refGroupFormat}
|
|
1768
|
+
/>
|
|
1769
|
+
)}
|
|
1285
1770
|
</Group>
|
|
1286
1771
|
);
|
|
1287
1772
|
}
|
|
@@ -1338,8 +1823,16 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1338
1823
|
bottomLabelCfg,
|
|
1339
1824
|
markersActive,
|
|
1340
1825
|
markersSV,
|
|
1826
|
+
markerClusterCfg,
|
|
1341
1827
|
renderMarker,
|
|
1342
1828
|
renderTooltip,
|
|
1829
|
+
renderOverlay,
|
|
1830
|
+
renderReferenceLine,
|
|
1831
|
+
allRefLines,
|
|
1832
|
+
refLineCustom,
|
|
1833
|
+
dragValues,
|
|
1834
|
+
dragActive,
|
|
1835
|
+
overlayContext,
|
|
1343
1836
|
scrubCfg,
|
|
1344
1837
|
crosshair,
|
|
1345
1838
|
isCandle,
|
|
@@ -1424,6 +1917,23 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1424
1917
|
padding={effectivePadding}
|
|
1425
1918
|
lineData={engine.data}
|
|
1426
1919
|
lineLinear={lineIsLinear}
|
|
1920
|
+
cluster={markerClusterCfg}
|
|
1921
|
+
/>
|
|
1922
|
+
)}
|
|
1923
|
+
|
|
1924
|
+
{/* Custom-rendered reference-line tags — RN views floated over the canvas
|
|
1925
|
+
(non-Skia), pinned to each Form-A line's value. Sibling of <Canvas>.
|
|
1926
|
+
Built-in Skia tags for these lines are suppressed (no double-draw). */}
|
|
1927
|
+
{renderReferenceLine && allRefLines.length > 0 && (
|
|
1928
|
+
<CustomReferenceLineOverlay
|
|
1929
|
+
lines={allRefLines}
|
|
1930
|
+
renderReferenceLine={renderReferenceLine}
|
|
1931
|
+
custom={refLineCustom}
|
|
1932
|
+
engine={engine}
|
|
1933
|
+
padding={effectivePadding}
|
|
1934
|
+
formatValue={formatValue}
|
|
1935
|
+
dragValues={dragValues}
|
|
1936
|
+
dragActive={dragActive}
|
|
1427
1937
|
/>
|
|
1428
1938
|
)}
|
|
1429
1939
|
|
|
@@ -1445,8 +1955,16 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1445
1955
|
placement={scrubCfg.tooltipPlacement}
|
|
1446
1956
|
margin={scrubCfg.tooltipMargin}
|
|
1447
1957
|
lineTop={crosshair.tooltipLineTop}
|
|
1958
|
+
scrubDotY={crosshair.scrubDotY}
|
|
1448
1959
|
/>
|
|
1449
1960
|
)}
|
|
1961
|
+
|
|
1962
|
+
{/* Custom consumer overlay — an RN view tree floated over the canvas with
|
|
1963
|
+
the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
|
|
1964
|
+
tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
|
|
1965
|
+
{renderOverlay && (
|
|
1966
|
+
<ChartOverlayLayer render={renderOverlay} context={overlayContext} />
|
|
1967
|
+
)}
|
|
1450
1968
|
</View>
|
|
1451
1969
|
</GestureDetector>
|
|
1452
1970
|
);
|