react-native-livechart 3.12.0 → 4.0.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 +7 -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 +56 -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.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/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 +505 -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 +549 -32
- package/src/components/LiveChartSeries.tsx +110 -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 +48 -26
- package/src/constants.ts +10 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +122 -1
- package/src/core/useLiveChartEngine.ts +103 -10
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +13 -1
- 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 +29 -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/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 +525 -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
|
|
|
@@ -488,7 +737,7 @@ function useLiveChartController({
|
|
|
488
737
|
effectivePadding,
|
|
489
738
|
palette,
|
|
490
739
|
formatValue,
|
|
491
|
-
|
|
740
|
+
badgeFont,
|
|
492
741
|
badgeCfg?.variant ?? "default",
|
|
493
742
|
badgeCfg?.tail ?? true,
|
|
494
743
|
momentumSV,
|
|
@@ -496,6 +745,11 @@ function useLiveChartController({
|
|
|
496
745
|
badgeCfg?.background,
|
|
497
746
|
metricsCfg.badge,
|
|
498
747
|
metricsCfg.motion.badgeColorSpeed,
|
|
748
|
+
effectiveYAxisFloat,
|
|
749
|
+
engine.edgeValue,
|
|
750
|
+
badgeCfg?.followViewEdge ?? false,
|
|
751
|
+
badgeCfg?.radius,
|
|
752
|
+
badgeCfg?.textColor,
|
|
499
753
|
);
|
|
500
754
|
|
|
501
755
|
// Scrub/crosshair must see the same stash-backed candles as the engine.
|
|
@@ -509,6 +763,7 @@ function useLiveChartController({
|
|
|
509
763
|
: undefined;
|
|
510
764
|
|
|
511
765
|
const markersActive = markers != null;
|
|
766
|
+
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
512
767
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
513
768
|
// self-projects, so we only need the gesture + hit-test here. Built BEFORE
|
|
514
769
|
// `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
|
|
@@ -518,11 +773,12 @@ function useLiveChartController({
|
|
|
518
773
|
markersSV,
|
|
519
774
|
!isStatic && markersActive,
|
|
520
775
|
markerHitRadius,
|
|
521
|
-
|
|
776
|
+
onMarkerPress,
|
|
522
777
|
undefined, // seriesSV — single-series has none
|
|
523
778
|
engine.data, // anchor value-less markers to the line
|
|
524
779
|
!isStatic, // static: no marker-projection loop
|
|
525
780
|
lineIsLinear, // match marker anchoring to the rendered curve
|
|
781
|
+
markerClusterCfg, // co-located marker stacking / collapse
|
|
526
782
|
);
|
|
527
783
|
|
|
528
784
|
// Pressable reference-line badges (working orders / alerts). Built before
|
|
@@ -538,6 +794,7 @@ function useLiveChartController({
|
|
|
538
794
|
!isStatic && refPressActive,
|
|
539
795
|
markerHitRadius,
|
|
540
796
|
onReferenceLinePress,
|
|
797
|
+
dragValues,
|
|
541
798
|
);
|
|
542
799
|
|
|
543
800
|
// Combined "defer" hit-test for the scrub-action place-tap: yield to a marker or
|
|
@@ -549,6 +806,24 @@ function useLiveChartController({
|
|
|
549
806
|
return markerHitTest(x, y) || refLineHitTest(x, y);
|
|
550
807
|
};
|
|
551
808
|
|
|
809
|
+
// Time-scroll activation. `holdToScrub`: a quick one-finger drag scrolls while
|
|
810
|
+
// scrub engages on press-and-hold — so the scrub gesture needs a long-press
|
|
811
|
+
// delay (unless the caller set its own `panGestureDelay`). `timeScrollEnabled`
|
|
812
|
+
// is computed earlier (it gates the float gutter).
|
|
813
|
+
const scrollGestureMode =
|
|
814
|
+
typeof timeScroll === "object"
|
|
815
|
+
? (timeScroll.gesture ?? "holdToScrub")
|
|
816
|
+
: "holdToScrub";
|
|
817
|
+
// In holdToScrub the scrub MUST require a hold so a quick drag scrolls instead.
|
|
818
|
+
// Precedence: explicit timeScroll.scrubHoldMs, then scrub.panGestureDelay, then
|
|
819
|
+
// the default. `||` (not `??`) skips the resolved panGestureDelay's 0 default.
|
|
820
|
+
const timeScrollHoldMs =
|
|
821
|
+
typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
|
|
822
|
+
const scrubHoldMs =
|
|
823
|
+
timeScrollEnabled && scrollGestureMode === "holdToScrub"
|
|
824
|
+
? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
|
|
825
|
+
: (scrubCfg?.panGestureDelay ?? 0);
|
|
826
|
+
|
|
552
827
|
const crosshair = useCrosshair(
|
|
553
828
|
engine,
|
|
554
829
|
effectivePadding,
|
|
@@ -561,7 +836,7 @@ function useLiveChartController({
|
|
|
561
836
|
!isStatic && (scrubCfg !== null || scrubActionCfg !== null),
|
|
562
837
|
onScrub,
|
|
563
838
|
candleOpts,
|
|
564
|
-
|
|
839
|
+
scrubHoldMs,
|
|
565
840
|
onGestureStart,
|
|
566
841
|
onGestureEnd,
|
|
567
842
|
scrubActionCfg,
|
|
@@ -572,8 +847,75 @@ function useLiveChartController({
|
|
|
572
847
|
scrubCfg?.tooltipShowValue ?? true,
|
|
573
848
|
scrubCfg?.tooltipShowTime ?? true,
|
|
574
849
|
scrubCfg?.tooltipMargin ?? 8,
|
|
850
|
+
// Axis-drag time-scroll: keep the bottom "time ruler" band scroll-only so a
|
|
851
|
+
// drag there never trips the scrub crosshair.
|
|
852
|
+
timeScrollEnabled && scrollGestureMode === "axisDrag"
|
|
853
|
+
? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
|
|
854
|
+
: 0,
|
|
575
855
|
);
|
|
576
856
|
|
|
857
|
+
// ── Time-scroll (drag back through history) ───────────────────────────────
|
|
858
|
+
// Experimental: a pan freezes the window at an absolute time and resumes
|
|
859
|
+
// following once dragged back to the live edge. Pan is clamped to the earliest
|
|
860
|
+
// retained point (line or candle). See `timeScroll` for the gesture model.
|
|
861
|
+
const scrollMinTime = useDerivedValue(() => {
|
|
862
|
+
const src = isCandle ? candlesEngine.get() : lineEngineData.get();
|
|
863
|
+
return src.length > 0 ? src[0].time : engine.liveEdge.get();
|
|
864
|
+
});
|
|
865
|
+
const panScrollGesture = usePanScroll({
|
|
866
|
+
engine,
|
|
867
|
+
padding: effectivePadding,
|
|
868
|
+
minTime: scrollMinTime,
|
|
869
|
+
enabled: timeScrollEnabled,
|
|
870
|
+
mode: scrollGestureMode,
|
|
871
|
+
// Clear any live crosshair when a scroll drag takes over.
|
|
872
|
+
onScrollStart: () => {
|
|
873
|
+
"worklet";
|
|
874
|
+
crosshair.scrubActive.set(false);
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// Draggable reference lines: a per-line vertical pan that grabs a line near its
|
|
879
|
+
// value and drags it along the Y-axis (with snap / bounds / callbacks). Built
|
|
880
|
+
// unconditionally for stable hook order; the gesture self-disables when no line
|
|
881
|
+
// opts in, and it's only composed into the root when `refDragEnabled`.
|
|
882
|
+
const refDragEnabled =
|
|
883
|
+
!isStatic && allRefLines.some((l) => l.draggable === true);
|
|
884
|
+
const refDragGesture = useReferenceDrag(
|
|
885
|
+
engine,
|
|
886
|
+
effectivePadding,
|
|
887
|
+
allRefLines,
|
|
888
|
+
dragValues,
|
|
889
|
+
dragActive,
|
|
890
|
+
!isStatic,
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
// Pinch-to-zoom the visible window (two-finger). Anchors at the focal point and
|
|
894
|
+
// writes viewWindow + viewEnd; composes via Simultaneous (it's two-finger, so
|
|
895
|
+
// disjoint from the one-finger pan/scrub). See `zoom`.
|
|
896
|
+
const pinchZoomGesture = usePinchZoom({
|
|
897
|
+
engine,
|
|
898
|
+
padding: effectivePadding,
|
|
899
|
+
minTime: scrollMinTime,
|
|
900
|
+
timeWindow,
|
|
901
|
+
enabled: zoomEnabled,
|
|
902
|
+
minTimeWindow: zoomCfg?.minTimeWindow,
|
|
903
|
+
maxTimeWindow: zoomCfg?.maxTimeWindow,
|
|
904
|
+
onZoomStart: () => {
|
|
905
|
+
"worklet";
|
|
906
|
+
crosshair.scrubActive.set(false);
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// Paging callbacks: report the visible range / proximity to the oldest data so
|
|
911
|
+
// a host can lazily load history. Inert unless a callback is supplied.
|
|
912
|
+
useVisibleRange({
|
|
913
|
+
engine,
|
|
914
|
+
minTime: scrollMinTime,
|
|
915
|
+
onVisibleRangeChange,
|
|
916
|
+
onReachStart,
|
|
917
|
+
});
|
|
918
|
+
|
|
577
919
|
// Scrub-action composes a Tap (place/move the reticle, press the badge, dismiss)
|
|
578
920
|
// ahead of the pan via Exclusive, so a tap is tried first and only becomes a
|
|
579
921
|
// drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
|
|
@@ -599,12 +941,37 @@ function useLiveChartController({
|
|
|
599
941
|
overlayTaps.length === 1
|
|
600
942
|
? overlayTaps[0]
|
|
601
943
|
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
602
|
-
//
|
|
603
|
-
//
|
|
944
|
+
// Always `Simultaneous`, never `Race`: on iOS the scrub pan uses
|
|
945
|
+
// `minDistance(0)`, so in a `Race` it activates on touch-down and cancels the
|
|
946
|
+
// overlay tap before it can recognize — `onMarkerPress`/`onReferenceLinePress`
|
|
947
|
+
// would never fire. Sharing the gesture space lets the tap recognize; the pan
|
|
948
|
+
// defers to a marker/badge under the finger via `deferTapHit` (see
|
|
949
|
+
// `useCrosshair`'s scrub `onStart`) so no stray crosshair is dropped there.
|
|
950
|
+
rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Compose the pan-scroll gesture. holdToScrub races the scrub/tap gestures (a
|
|
954
|
+
// quick drag scrolls; a still press-hold falls through to scrub). Axis-drag
|
|
955
|
+
// goes first via Exclusive: it fails instantly outside the axis band, so scrub
|
|
956
|
+
// runs everywhere else.
|
|
957
|
+
if (timeScrollEnabled) {
|
|
604
958
|
rootGesture =
|
|
605
|
-
|
|
606
|
-
? Gesture.
|
|
607
|
-
: Gesture.Race(
|
|
959
|
+
scrollGestureMode === "axisDrag"
|
|
960
|
+
? Gesture.Exclusive(panScrollGesture, rootGesture)
|
|
961
|
+
: Gesture.Race(panScrollGesture, rootGesture);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Draggable reference lines take priority: a vertical grab on a line drags it.
|
|
965
|
+
// The manual-activation pan fails fast off any line (or on horizontal intent), so
|
|
966
|
+
// Exclusive falls through to scrub / scroll everywhere else.
|
|
967
|
+
if (refDragEnabled) {
|
|
968
|
+
rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Pinch runs alongside everything else (two-finger, so it never competes with
|
|
972
|
+
// the one-finger pan/scrub/tap gestures for the same touch).
|
|
973
|
+
if (zoomEnabled) {
|
|
974
|
+
rootGesture = Gesture.Simultaneous(rootGesture, pinchZoomGesture);
|
|
608
975
|
}
|
|
609
976
|
|
|
610
977
|
// ── Derived render values ──────────────────────────────────────────────
|
|
@@ -662,6 +1029,7 @@ function useLiveChartController({
|
|
|
662
1029
|
extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
|
|
663
1030
|
// configs
|
|
664
1031
|
yAxisCfg,
|
|
1032
|
+
yAxisFloat: effectiveYAxisFloat,
|
|
665
1033
|
xAxisCfg,
|
|
666
1034
|
badgeCfg,
|
|
667
1035
|
scrubCfg,
|
|
@@ -679,6 +1047,16 @@ function useLiveChartController({
|
|
|
679
1047
|
leftEdgeFadeCfg,
|
|
680
1048
|
metricsCfg,
|
|
681
1049
|
allRefLines,
|
|
1050
|
+
refLineCustom,
|
|
1051
|
+
dragValues,
|
|
1052
|
+
dragActive,
|
|
1053
|
+
renderReferenceLine,
|
|
1054
|
+
refGroupingActive: refGroupingRadius != null,
|
|
1055
|
+
refGroupResult,
|
|
1056
|
+
groupHidden,
|
|
1057
|
+
refGroupBadge,
|
|
1058
|
+
refGroupBadgeFont,
|
|
1059
|
+
refGroupFormat,
|
|
682
1060
|
resolvedSegments,
|
|
683
1061
|
hasRecolorSegments,
|
|
684
1062
|
segmentGradient,
|
|
@@ -690,7 +1068,9 @@ function useLiveChartController({
|
|
|
690
1068
|
// theme / layout / fonts
|
|
691
1069
|
palette,
|
|
692
1070
|
skiaFont,
|
|
1071
|
+
fontProp,
|
|
693
1072
|
valueFont,
|
|
1073
|
+
badgeFont,
|
|
694
1074
|
strokeWidth,
|
|
695
1075
|
effectivePadding,
|
|
696
1076
|
// engine + reveal
|
|
@@ -715,6 +1095,15 @@ function useLiveChartController({
|
|
|
715
1095
|
downBodiesPath,
|
|
716
1096
|
upWicksPath,
|
|
717
1097
|
downWicksPath,
|
|
1098
|
+
upBarsPath,
|
|
1099
|
+
downBarsPath,
|
|
1100
|
+
// Volume bars: active flag, fade-in opacity, and resolved colors (default to
|
|
1101
|
+
// the candle palette). The reserved band height is read by the x-axis.
|
|
1102
|
+
volumeActive: volumeCfg !== null,
|
|
1103
|
+
volumeBandHeight,
|
|
1104
|
+
volumeOpacity: volumeCfg?.opacity ?? 1,
|
|
1105
|
+
volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
|
|
1106
|
+
volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
|
|
718
1107
|
dotX,
|
|
719
1108
|
dotY,
|
|
720
1109
|
liveDotOpacity,
|
|
@@ -730,8 +1119,11 @@ function useLiveChartController({
|
|
|
730
1119
|
rootGesture,
|
|
731
1120
|
markersActive,
|
|
732
1121
|
markersSV,
|
|
1122
|
+
markerClusterCfg,
|
|
733
1123
|
renderMarker,
|
|
734
1124
|
renderTooltip,
|
|
1125
|
+
renderOverlay,
|
|
1126
|
+
overlayContext,
|
|
735
1127
|
// selection dot: resolved config + fallback color (the chart line/accent color)
|
|
736
1128
|
selectionDot: selectionDotCfg,
|
|
737
1129
|
selectionColor: lineProp?.color ?? palette.line,
|
|
@@ -756,6 +1148,7 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
756
1148
|
const {
|
|
757
1149
|
degenShakeTransform,
|
|
758
1150
|
yAxisCfg,
|
|
1151
|
+
yAxisFloat,
|
|
759
1152
|
reveal,
|
|
760
1153
|
yAxisEntries,
|
|
761
1154
|
engine,
|
|
@@ -781,10 +1174,13 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
781
1174
|
|
|
782
1175
|
return (
|
|
783
1176
|
<Group transform={degenShakeTransform}>
|
|
784
|
-
{/* Y-axis grid
|
|
1177
|
+
{/* Y-axis. Default: grid + labels here (in a reserved gutter). Floating
|
|
1178
|
+
mode: grid only — the labels + a soft edge fade draw above the candles
|
|
1179
|
+
in ChartStack so the plot runs full-width and candles dim under them. */}
|
|
785
1180
|
{yAxisCfg && (
|
|
786
1181
|
<Group opacity={reveal.yAxisOpacity}>
|
|
787
1182
|
<YAxisOverlay
|
|
1183
|
+
variant={yAxisFloat ? "grid" : "all"}
|
|
788
1184
|
entries={yAxisEntries}
|
|
789
1185
|
engine={engine}
|
|
790
1186
|
padding={effectivePadding}
|
|
@@ -857,10 +1253,12 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
857
1253
|
effectivePadding,
|
|
858
1254
|
palette,
|
|
859
1255
|
skiaFont,
|
|
1256
|
+
fontProp,
|
|
860
1257
|
badgeCfg,
|
|
861
1258
|
valueLineCfg,
|
|
862
1259
|
dotY,
|
|
863
1260
|
allRefLines,
|
|
1261
|
+
dragValues,
|
|
864
1262
|
resolvedSegments,
|
|
865
1263
|
hasRecolorSegments,
|
|
866
1264
|
segmentGradient,
|
|
@@ -878,6 +1276,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
878
1276
|
downWicksPath,
|
|
879
1277
|
upBodiesPath,
|
|
880
1278
|
downBodiesPath,
|
|
1279
|
+
upBarsPath,
|
|
1280
|
+
downBarsPath,
|
|
1281
|
+
volumeActive,
|
|
1282
|
+
volumeBandHeight,
|
|
1283
|
+
volumeOpacity,
|
|
1284
|
+
volumeUpColor,
|
|
1285
|
+
volumeDownColor,
|
|
881
1286
|
xAxisCfg,
|
|
882
1287
|
xAxisEntries,
|
|
883
1288
|
dotX,
|
|
@@ -889,10 +1294,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
889
1294
|
degenPackRevision,
|
|
890
1295
|
markersActive,
|
|
891
1296
|
markersSV,
|
|
1297
|
+
markerClusterCfg,
|
|
892
1298
|
renderMarker,
|
|
893
1299
|
emptyText,
|
|
894
1300
|
metricsCfg,
|
|
895
1301
|
layoutWidth,
|
|
1302
|
+
yAxisCfg,
|
|
1303
|
+
yAxisFloat,
|
|
1304
|
+
yAxisEntries,
|
|
1305
|
+
badgeUsesRightGutter,
|
|
1306
|
+
gridStyleCfg,
|
|
896
1307
|
} = model;
|
|
897
1308
|
|
|
898
1309
|
return (
|
|
@@ -935,6 +1346,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
935
1346
|
palette={palette}
|
|
936
1347
|
formatValue={formatValue}
|
|
937
1348
|
font={skiaFont}
|
|
1349
|
+
fontProp={fontProp}
|
|
1350
|
+
dragValues={dragValues}
|
|
1351
|
+
index={i}
|
|
938
1352
|
/>
|
|
939
1353
|
))}
|
|
940
1354
|
|
|
@@ -997,13 +1411,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
997
1411
|
<Path
|
|
998
1412
|
path={upWicksPath}
|
|
999
1413
|
style="stroke"
|
|
1000
|
-
strokeWidth={
|
|
1414
|
+
strokeWidth={metricsCfg.candle.wickWidth}
|
|
1001
1415
|
color={palette.wickUp}
|
|
1002
1416
|
/>
|
|
1003
1417
|
<Path
|
|
1004
1418
|
path={downWicksPath}
|
|
1005
1419
|
style="stroke"
|
|
1006
|
-
strokeWidth={
|
|
1420
|
+
strokeWidth={metricsCfg.candle.wickWidth}
|
|
1007
1421
|
color={palette.wickDown}
|
|
1008
1422
|
/>
|
|
1009
1423
|
<Path path={upBodiesPath} style="fill" color={palette.candleUp} />
|
|
@@ -1014,7 +1428,41 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1014
1428
|
/>
|
|
1015
1429
|
</Group>
|
|
1016
1430
|
|
|
1017
|
-
{/*
|
|
1431
|
+
{/* Volume bars in the reserved band below the candles. Fades in with the
|
|
1432
|
+
candle group (outer opacity); the config opacity dims the whole band
|
|
1433
|
+
(inner). Up/down bars carry their own colors (default candle palette). */}
|
|
1434
|
+
{volumeActive && (
|
|
1435
|
+
<Group opacity={candleGroupOpacity}>
|
|
1436
|
+
<Group opacity={volumeOpacity}>
|
|
1437
|
+
<Path path={upBarsPath} style="fill" color={volumeUpColor} />
|
|
1438
|
+
<Path path={downBarsPath} style="fill" color={volumeDownColor} />
|
|
1439
|
+
</Group>
|
|
1440
|
+
</Group>
|
|
1441
|
+
)}
|
|
1442
|
+
|
|
1443
|
+
{/* Floating axis: the labels float ABOVE the candles (right-aligned at the
|
|
1444
|
+
edge) so the plot runs full-width and candles stay fully visible behind
|
|
1445
|
+
them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
|
|
1446
|
+
{yAxisCfg && yAxisFloat && (
|
|
1447
|
+
<Group opacity={reveal.yAxisOpacity}>
|
|
1448
|
+
<YAxisOverlay
|
|
1449
|
+
variant="labels"
|
|
1450
|
+
float
|
|
1451
|
+
entries={yAxisEntries}
|
|
1452
|
+
engine={engine}
|
|
1453
|
+
padding={effectivePadding}
|
|
1454
|
+
palette={palette}
|
|
1455
|
+
font={skiaFont}
|
|
1456
|
+
badge={badgeUsesRightGutter}
|
|
1457
|
+
badgeTail={badgeCfg?.tail ?? true}
|
|
1458
|
+
badgeMetrics={metricsCfg.badge}
|
|
1459
|
+
gridStyle={gridStyleCfg}
|
|
1460
|
+
/>
|
|
1461
|
+
</Group>
|
|
1462
|
+
)}
|
|
1463
|
+
|
|
1464
|
+
{/* X-axis time labels. With a volume band the bottom padding is inflated by
|
|
1465
|
+
the band height; pass it so the axis shifts back to the very bottom. */}
|
|
1018
1466
|
{xAxisCfg && (
|
|
1019
1467
|
<XAxisOverlay
|
|
1020
1468
|
entries={xAxisEntries}
|
|
@@ -1022,6 +1470,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1022
1470
|
padding={effectivePadding}
|
|
1023
1471
|
palette={palette}
|
|
1024
1472
|
font={skiaFont}
|
|
1473
|
+
volumeBandHeight={volumeBandHeight}
|
|
1025
1474
|
/>
|
|
1026
1475
|
)}
|
|
1027
1476
|
|
|
@@ -1039,6 +1488,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1039
1488
|
radius={dotCfg.radius}
|
|
1040
1489
|
ring={dotCfg.ring}
|
|
1041
1490
|
color={dotCfg.color}
|
|
1491
|
+
viewEnd={engine.viewEnd}
|
|
1042
1492
|
/>
|
|
1043
1493
|
</Group>
|
|
1044
1494
|
)}
|
|
@@ -1069,6 +1519,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1069
1519
|
lineData={engine.data}
|
|
1070
1520
|
lineLinear={lineIsLinear}
|
|
1071
1521
|
renderMarker={renderMarker}
|
|
1522
|
+
cluster={markerClusterCfg}
|
|
1072
1523
|
/>
|
|
1073
1524
|
</Group>
|
|
1074
1525
|
)}
|
|
@@ -1242,12 +1693,19 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
|
|
|
1242
1693
|
/** Live-price badge, drawn above the scrub dim so the dim never clips its left
|
|
1243
1694
|
* edge. Shares the degen shake transform so it tracks the shaken stack. */
|
|
1244
1695
|
function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1245
|
-
const { badgeCfg, badgeData,
|
|
1696
|
+
const { badgeCfg, badgeData, badgeFont, reveal, degenShakeTransform } = model;
|
|
1246
1697
|
if (!badgeCfg) return null;
|
|
1247
1698
|
return (
|
|
1248
1699
|
<Group transform={degenShakeTransform}>
|
|
1249
1700
|
<Group opacity={reveal.badgeOpacity}>
|
|
1250
|
-
<BadgeOverlay
|
|
1701
|
+
<BadgeOverlay
|
|
1702
|
+
badge={badgeData}
|
|
1703
|
+
font={badgeFont}
|
|
1704
|
+
borderColor={badgeCfg.borderColor}
|
|
1705
|
+
borderWidth={badgeCfg.borderWidth}
|
|
1706
|
+
offsetX={badgeCfg.offsetX}
|
|
1707
|
+
offsetY={badgeCfg.offsetY}
|
|
1708
|
+
/>
|
|
1251
1709
|
</Group>
|
|
1252
1710
|
</Group>
|
|
1253
1711
|
);
|
|
@@ -1260,11 +1718,20 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
1260
1718
|
function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1261
1719
|
const {
|
|
1262
1720
|
allRefLines,
|
|
1721
|
+
refLineCustom,
|
|
1722
|
+
dragValues,
|
|
1723
|
+
groupHidden,
|
|
1724
|
+
refGroupResult,
|
|
1725
|
+
refGroupingActive,
|
|
1726
|
+
refGroupBadge,
|
|
1727
|
+
refGroupBadgeFont,
|
|
1728
|
+
refGroupFormat,
|
|
1263
1729
|
engine,
|
|
1264
1730
|
effectivePadding,
|
|
1265
1731
|
palette,
|
|
1266
1732
|
formatValue,
|
|
1267
1733
|
skiaFont,
|
|
1734
|
+
fontProp,
|
|
1268
1735
|
degenShakeTransform,
|
|
1269
1736
|
} = model;
|
|
1270
1737
|
if (allRefLines.length === 0) return null;
|
|
@@ -1279,9 +1746,26 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
1279
1746
|
palette={palette}
|
|
1280
1747
|
formatValue={formatValue}
|
|
1281
1748
|
font={skiaFont}
|
|
1749
|
+
fontProp={fontProp}
|
|
1282
1750
|
badgeLayer
|
|
1751
|
+
dragValues={dragValues}
|
|
1752
|
+
index={i}
|
|
1753
|
+
suppressTag={refLineCustom[i]}
|
|
1754
|
+
groupHidden={refGroupingActive ? groupHidden : undefined}
|
|
1283
1755
|
/>
|
|
1284
1756
|
))}
|
|
1757
|
+
{/* Collapsed count handles for grouped (near-value) lines. */}
|
|
1758
|
+
{refGroupingActive && (
|
|
1759
|
+
<ReferenceLineGroupOverlay
|
|
1760
|
+
grouping={refGroupResult}
|
|
1761
|
+
padding={effectivePadding}
|
|
1762
|
+
canvasWidth={engine.canvasWidth}
|
|
1763
|
+
palette={palette}
|
|
1764
|
+
font={refGroupBadgeFont}
|
|
1765
|
+
badge={refGroupBadge}
|
|
1766
|
+
format={refGroupFormat}
|
|
1767
|
+
/>
|
|
1768
|
+
)}
|
|
1285
1769
|
</Group>
|
|
1286
1770
|
);
|
|
1287
1771
|
}
|
|
@@ -1338,8 +1822,16 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1338
1822
|
bottomLabelCfg,
|
|
1339
1823
|
markersActive,
|
|
1340
1824
|
markersSV,
|
|
1825
|
+
markerClusterCfg,
|
|
1341
1826
|
renderMarker,
|
|
1342
1827
|
renderTooltip,
|
|
1828
|
+
renderOverlay,
|
|
1829
|
+
renderReferenceLine,
|
|
1830
|
+
allRefLines,
|
|
1831
|
+
refLineCustom,
|
|
1832
|
+
dragValues,
|
|
1833
|
+
dragActive,
|
|
1834
|
+
overlayContext,
|
|
1343
1835
|
scrubCfg,
|
|
1344
1836
|
crosshair,
|
|
1345
1837
|
isCandle,
|
|
@@ -1424,6 +1916,23 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1424
1916
|
padding={effectivePadding}
|
|
1425
1917
|
lineData={engine.data}
|
|
1426
1918
|
lineLinear={lineIsLinear}
|
|
1919
|
+
cluster={markerClusterCfg}
|
|
1920
|
+
/>
|
|
1921
|
+
)}
|
|
1922
|
+
|
|
1923
|
+
{/* Custom-rendered reference-line tags — RN views floated over the canvas
|
|
1924
|
+
(non-Skia), pinned to each Form-A line's value. Sibling of <Canvas>.
|
|
1925
|
+
Built-in Skia tags for these lines are suppressed (no double-draw). */}
|
|
1926
|
+
{renderReferenceLine && allRefLines.length > 0 && (
|
|
1927
|
+
<CustomReferenceLineOverlay
|
|
1928
|
+
lines={allRefLines}
|
|
1929
|
+
renderReferenceLine={renderReferenceLine}
|
|
1930
|
+
custom={refLineCustom}
|
|
1931
|
+
engine={engine}
|
|
1932
|
+
padding={effectivePadding}
|
|
1933
|
+
formatValue={formatValue}
|
|
1934
|
+
dragValues={dragValues}
|
|
1935
|
+
dragActive={dragActive}
|
|
1427
1936
|
/>
|
|
1428
1937
|
)}
|
|
1429
1938
|
|
|
@@ -1445,8 +1954,16 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1445
1954
|
placement={scrubCfg.tooltipPlacement}
|
|
1446
1955
|
margin={scrubCfg.tooltipMargin}
|
|
1447
1956
|
lineTop={crosshair.tooltipLineTop}
|
|
1957
|
+
scrubDotY={crosshair.scrubDotY}
|
|
1448
1958
|
/>
|
|
1449
1959
|
)}
|
|
1960
|
+
|
|
1961
|
+
{/* Custom consumer overlay — an RN view tree floated over the canvas with
|
|
1962
|
+
the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
|
|
1963
|
+
tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
|
|
1964
|
+
{renderOverlay && (
|
|
1965
|
+
<ChartOverlayLayer render={renderOverlay} context={overlayContext} />
|
|
1966
|
+
)}
|
|
1450
1967
|
</View>
|
|
1451
1968
|
</GestureDetector>
|
|
1452
1969
|
);
|