react-native-livechart 3.11.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 +18 -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 +8 -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 +12 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.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 +15 -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 +512 -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 +153 -53
- 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 +561 -34
- package/src/components/LiveChartSeries.tsx +110 -5
- package/src/components/MarkerOverlay.tsx +100 -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 +43 -11
- package/src/hooks/useMultiSeriesLinePaths.ts +2 -1
- 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 +54 -9
- 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 +532 -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(
|
|
@@ -407,14 +634,23 @@ function useLiveChartController({
|
|
|
407
634
|
? thresholdStops(thresholdCfg, palette)
|
|
408
635
|
: null;
|
|
409
636
|
|
|
637
|
+
// Straight polyline instead of the monotone cubic when line.curve === "linear".
|
|
638
|
+
// Shared by the path builders and the marker anchoring so glyphs sit on the
|
|
639
|
+
// rendered line rather than the phantom spline.
|
|
640
|
+
const lineIsLinear = lineProp?.curve === "linear";
|
|
641
|
+
|
|
410
642
|
const { linePath, fillPath, thresholdFillPath } = useChartPaths(
|
|
411
643
|
engine,
|
|
412
644
|
effectivePadding,
|
|
413
645
|
reveal.morphT,
|
|
414
646
|
// Only build the band path when the fill is actually on.
|
|
415
647
|
thresholdCfg?.fill ? thresholdGeom.lineY : undefined,
|
|
416
|
-
|
|
417
|
-
|
|
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,
|
|
418
654
|
);
|
|
419
655
|
|
|
420
656
|
// Area-dots fill shader color as a vec4 (channels 0..1), with the config
|
|
@@ -432,19 +668,36 @@ function useLiveChartController({
|
|
|
432
668
|
adA * (areaDotsCfg?.opacity ?? 1),
|
|
433
669
|
];
|
|
434
670
|
|
|
435
|
-
const {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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);
|
|
448
701
|
|
|
449
702
|
const momentumSV = useMomentum(engine, momentum);
|
|
450
703
|
|
|
@@ -484,7 +737,7 @@ function useLiveChartController({
|
|
|
484
737
|
effectivePadding,
|
|
485
738
|
palette,
|
|
486
739
|
formatValue,
|
|
487
|
-
|
|
740
|
+
badgeFont,
|
|
488
741
|
badgeCfg?.variant ?? "default",
|
|
489
742
|
badgeCfg?.tail ?? true,
|
|
490
743
|
momentumSV,
|
|
@@ -492,6 +745,11 @@ function useLiveChartController({
|
|
|
492
745
|
badgeCfg?.background,
|
|
493
746
|
metricsCfg.badge,
|
|
494
747
|
metricsCfg.motion.badgeColorSpeed,
|
|
748
|
+
effectiveYAxisFloat,
|
|
749
|
+
engine.edgeValue,
|
|
750
|
+
badgeCfg?.followViewEdge ?? false,
|
|
751
|
+
badgeCfg?.radius,
|
|
752
|
+
badgeCfg?.textColor,
|
|
495
753
|
);
|
|
496
754
|
|
|
497
755
|
// Scrub/crosshair must see the same stash-backed candles as the engine.
|
|
@@ -505,6 +763,7 @@ function useLiveChartController({
|
|
|
505
763
|
: undefined;
|
|
506
764
|
|
|
507
765
|
const markersActive = markers != null;
|
|
766
|
+
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
508
767
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
509
768
|
// self-projects, so we only need the gesture + hit-test here. Built BEFORE
|
|
510
769
|
// `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
|
|
@@ -514,10 +773,12 @@ function useLiveChartController({
|
|
|
514
773
|
markersSV,
|
|
515
774
|
!isStatic && markersActive,
|
|
516
775
|
markerHitRadius,
|
|
517
|
-
|
|
776
|
+
onMarkerPress,
|
|
518
777
|
undefined, // seriesSV — single-series has none
|
|
519
778
|
engine.data, // anchor value-less markers to the line
|
|
520
779
|
!isStatic, // static: no marker-projection loop
|
|
780
|
+
lineIsLinear, // match marker anchoring to the rendered curve
|
|
781
|
+
markerClusterCfg, // co-located marker stacking / collapse
|
|
521
782
|
);
|
|
522
783
|
|
|
523
784
|
// Pressable reference-line badges (working orders / alerts). Built before
|
|
@@ -533,6 +794,7 @@ function useLiveChartController({
|
|
|
533
794
|
!isStatic && refPressActive,
|
|
534
795
|
markerHitRadius,
|
|
535
796
|
onReferenceLinePress,
|
|
797
|
+
dragValues,
|
|
536
798
|
);
|
|
537
799
|
|
|
538
800
|
// Combined "defer" hit-test for the scrub-action place-tap: yield to a marker or
|
|
@@ -544,6 +806,24 @@ function useLiveChartController({
|
|
|
544
806
|
return markerHitTest(x, y) || refLineHitTest(x, y);
|
|
545
807
|
};
|
|
546
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
|
+
|
|
547
827
|
const crosshair = useCrosshair(
|
|
548
828
|
engine,
|
|
549
829
|
effectivePadding,
|
|
@@ -556,7 +836,7 @@ function useLiveChartController({
|
|
|
556
836
|
!isStatic && (scrubCfg !== null || scrubActionCfg !== null),
|
|
557
837
|
onScrub,
|
|
558
838
|
candleOpts,
|
|
559
|
-
|
|
839
|
+
scrubHoldMs,
|
|
560
840
|
onGestureStart,
|
|
561
841
|
onGestureEnd,
|
|
562
842
|
scrubActionCfg,
|
|
@@ -567,8 +847,75 @@ function useLiveChartController({
|
|
|
567
847
|
scrubCfg?.tooltipShowValue ?? true,
|
|
568
848
|
scrubCfg?.tooltipShowTime ?? true,
|
|
569
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,
|
|
855
|
+
);
|
|
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,
|
|
570
891
|
);
|
|
571
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
|
+
|
|
572
919
|
// Scrub-action composes a Tap (place/move the reticle, press the badge, dismiss)
|
|
573
920
|
// ahead of the pan via Exclusive, so a tap is tried first and only becomes a
|
|
574
921
|
// drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
|
|
@@ -594,12 +941,37 @@ function useLiveChartController({
|
|
|
594
941
|
overlayTaps.length === 1
|
|
595
942
|
? overlayTaps[0]
|
|
596
943
|
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
597
|
-
//
|
|
598
|
-
//
|
|
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) {
|
|
599
958
|
rootGesture =
|
|
600
|
-
|
|
601
|
-
? Gesture.
|
|
602
|
-
: 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);
|
|
603
975
|
}
|
|
604
976
|
|
|
605
977
|
// ── Derived render values ──────────────────────────────────────────────
|
|
@@ -657,6 +1029,7 @@ function useLiveChartController({
|
|
|
657
1029
|
extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
|
|
658
1030
|
// configs
|
|
659
1031
|
yAxisCfg,
|
|
1032
|
+
yAxisFloat: effectiveYAxisFloat,
|
|
660
1033
|
xAxisCfg,
|
|
661
1034
|
badgeCfg,
|
|
662
1035
|
scrubCfg,
|
|
@@ -674,6 +1047,16 @@ function useLiveChartController({
|
|
|
674
1047
|
leftEdgeFadeCfg,
|
|
675
1048
|
metricsCfg,
|
|
676
1049
|
allRefLines,
|
|
1050
|
+
refLineCustom,
|
|
1051
|
+
dragValues,
|
|
1052
|
+
dragActive,
|
|
1053
|
+
renderReferenceLine,
|
|
1054
|
+
refGroupingActive: refGroupingRadius != null,
|
|
1055
|
+
refGroupResult,
|
|
1056
|
+
groupHidden,
|
|
1057
|
+
refGroupBadge,
|
|
1058
|
+
refGroupBadgeFont,
|
|
1059
|
+
refGroupFormat,
|
|
677
1060
|
resolvedSegments,
|
|
678
1061
|
hasRecolorSegments,
|
|
679
1062
|
segmentGradient,
|
|
@@ -685,7 +1068,9 @@ function useLiveChartController({
|
|
|
685
1068
|
// theme / layout / fonts
|
|
686
1069
|
palette,
|
|
687
1070
|
skiaFont,
|
|
1071
|
+
fontProp,
|
|
688
1072
|
valueFont,
|
|
1073
|
+
badgeFont,
|
|
689
1074
|
strokeWidth,
|
|
690
1075
|
effectivePadding,
|
|
691
1076
|
// engine + reveal
|
|
@@ -705,10 +1090,20 @@ function useLiveChartController({
|
|
|
705
1090
|
linePath,
|
|
706
1091
|
fillPath,
|
|
707
1092
|
thresholdFillPath,
|
|
1093
|
+
lineIsLinear,
|
|
708
1094
|
upBodiesPath,
|
|
709
1095
|
downBodiesPath,
|
|
710
1096
|
upWicksPath,
|
|
711
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,
|
|
712
1107
|
dotX,
|
|
713
1108
|
dotY,
|
|
714
1109
|
liveDotOpacity,
|
|
@@ -724,8 +1119,11 @@ function useLiveChartController({
|
|
|
724
1119
|
rootGesture,
|
|
725
1120
|
markersActive,
|
|
726
1121
|
markersSV,
|
|
1122
|
+
markerClusterCfg,
|
|
727
1123
|
renderMarker,
|
|
728
1124
|
renderTooltip,
|
|
1125
|
+
renderOverlay,
|
|
1126
|
+
overlayContext,
|
|
729
1127
|
// selection dot: resolved config + fallback color (the chart line/accent color)
|
|
730
1128
|
selectionDot: selectionDotCfg,
|
|
731
1129
|
selectionColor: lineProp?.color ?? palette.line,
|
|
@@ -750,6 +1148,7 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
750
1148
|
const {
|
|
751
1149
|
degenShakeTransform,
|
|
752
1150
|
yAxisCfg,
|
|
1151
|
+
yAxisFloat,
|
|
753
1152
|
reveal,
|
|
754
1153
|
yAxisEntries,
|
|
755
1154
|
engine,
|
|
@@ -775,10 +1174,13 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
|
775
1174
|
|
|
776
1175
|
return (
|
|
777
1176
|
<Group transform={degenShakeTransform}>
|
|
778
|
-
{/* 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. */}
|
|
779
1180
|
{yAxisCfg && (
|
|
780
1181
|
<Group opacity={reveal.yAxisOpacity}>
|
|
781
1182
|
<YAxisOverlay
|
|
1183
|
+
variant={yAxisFloat ? "grid" : "all"}
|
|
782
1184
|
entries={yAxisEntries}
|
|
783
1185
|
engine={engine}
|
|
784
1186
|
padding={effectivePadding}
|
|
@@ -851,10 +1253,12 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
851
1253
|
effectivePadding,
|
|
852
1254
|
palette,
|
|
853
1255
|
skiaFont,
|
|
1256
|
+
fontProp,
|
|
854
1257
|
badgeCfg,
|
|
855
1258
|
valueLineCfg,
|
|
856
1259
|
dotY,
|
|
857
1260
|
allRefLines,
|
|
1261
|
+
dragValues,
|
|
858
1262
|
resolvedSegments,
|
|
859
1263
|
hasRecolorSegments,
|
|
860
1264
|
segmentGradient,
|
|
@@ -864,6 +1268,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
864
1268
|
formatValue,
|
|
865
1269
|
lineGroupOpacity,
|
|
866
1270
|
linePath,
|
|
1271
|
+
lineIsLinear,
|
|
867
1272
|
strokeWidth,
|
|
868
1273
|
lineProp,
|
|
869
1274
|
candleGroupOpacity,
|
|
@@ -871,6 +1276,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
871
1276
|
downWicksPath,
|
|
872
1277
|
upBodiesPath,
|
|
873
1278
|
downBodiesPath,
|
|
1279
|
+
upBarsPath,
|
|
1280
|
+
downBarsPath,
|
|
1281
|
+
volumeActive,
|
|
1282
|
+
volumeBandHeight,
|
|
1283
|
+
volumeOpacity,
|
|
1284
|
+
volumeUpColor,
|
|
1285
|
+
volumeDownColor,
|
|
874
1286
|
xAxisCfg,
|
|
875
1287
|
xAxisEntries,
|
|
876
1288
|
dotX,
|
|
@@ -882,10 +1294,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
882
1294
|
degenPackRevision,
|
|
883
1295
|
markersActive,
|
|
884
1296
|
markersSV,
|
|
1297
|
+
markerClusterCfg,
|
|
885
1298
|
renderMarker,
|
|
886
1299
|
emptyText,
|
|
887
1300
|
metricsCfg,
|
|
888
1301
|
layoutWidth,
|
|
1302
|
+
yAxisCfg,
|
|
1303
|
+
yAxisFloat,
|
|
1304
|
+
yAxisEntries,
|
|
1305
|
+
badgeUsesRightGutter,
|
|
1306
|
+
gridStyleCfg,
|
|
889
1307
|
} = model;
|
|
890
1308
|
|
|
891
1309
|
return (
|
|
@@ -928,6 +1346,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
928
1346
|
palette={palette}
|
|
929
1347
|
formatValue={formatValue}
|
|
930
1348
|
font={skiaFont}
|
|
1349
|
+
fontProp={fontProp}
|
|
1350
|
+
dragValues={dragValues}
|
|
1351
|
+
index={i}
|
|
931
1352
|
/>
|
|
932
1353
|
))}
|
|
933
1354
|
|
|
@@ -990,13 +1411,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
990
1411
|
<Path
|
|
991
1412
|
path={upWicksPath}
|
|
992
1413
|
style="stroke"
|
|
993
|
-
strokeWidth={
|
|
1414
|
+
strokeWidth={metricsCfg.candle.wickWidth}
|
|
994
1415
|
color={palette.wickUp}
|
|
995
1416
|
/>
|
|
996
1417
|
<Path
|
|
997
1418
|
path={downWicksPath}
|
|
998
1419
|
style="stroke"
|
|
999
|
-
strokeWidth={
|
|
1420
|
+
strokeWidth={metricsCfg.candle.wickWidth}
|
|
1000
1421
|
color={palette.wickDown}
|
|
1001
1422
|
/>
|
|
1002
1423
|
<Path path={upBodiesPath} style="fill" color={palette.candleUp} />
|
|
@@ -1007,7 +1428,41 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1007
1428
|
/>
|
|
1008
1429
|
</Group>
|
|
1009
1430
|
|
|
1010
|
-
{/*
|
|
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. */}
|
|
1011
1466
|
{xAxisCfg && (
|
|
1012
1467
|
<XAxisOverlay
|
|
1013
1468
|
entries={xAxisEntries}
|
|
@@ -1015,6 +1470,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1015
1470
|
padding={effectivePadding}
|
|
1016
1471
|
palette={palette}
|
|
1017
1472
|
font={skiaFont}
|
|
1473
|
+
volumeBandHeight={volumeBandHeight}
|
|
1018
1474
|
/>
|
|
1019
1475
|
)}
|
|
1020
1476
|
|
|
@@ -1032,6 +1488,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1032
1488
|
radius={dotCfg.radius}
|
|
1033
1489
|
ring={dotCfg.ring}
|
|
1034
1490
|
color={dotCfg.color}
|
|
1491
|
+
viewEnd={engine.viewEnd}
|
|
1035
1492
|
/>
|
|
1036
1493
|
</Group>
|
|
1037
1494
|
)}
|
|
@@ -1060,7 +1517,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
1060
1517
|
palette={palette}
|
|
1061
1518
|
font={skiaFont}
|
|
1062
1519
|
lineData={engine.data}
|
|
1520
|
+
lineLinear={lineIsLinear}
|
|
1063
1521
|
renderMarker={renderMarker}
|
|
1522
|
+
cluster={markerClusterCfg}
|
|
1064
1523
|
/>
|
|
1065
1524
|
</Group>
|
|
1066
1525
|
)}
|
|
@@ -1234,12 +1693,19 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
|
|
|
1234
1693
|
/** Live-price badge, drawn above the scrub dim so the dim never clips its left
|
|
1235
1694
|
* edge. Shares the degen shake transform so it tracks the shaken stack. */
|
|
1236
1695
|
function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1237
|
-
const { badgeCfg, badgeData,
|
|
1696
|
+
const { badgeCfg, badgeData, badgeFont, reveal, degenShakeTransform } = model;
|
|
1238
1697
|
if (!badgeCfg) return null;
|
|
1239
1698
|
return (
|
|
1240
1699
|
<Group transform={degenShakeTransform}>
|
|
1241
1700
|
<Group opacity={reveal.badgeOpacity}>
|
|
1242
|
-
<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
|
+
/>
|
|
1243
1709
|
</Group>
|
|
1244
1710
|
</Group>
|
|
1245
1711
|
);
|
|
@@ -1252,11 +1718,20 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
1252
1718
|
function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1253
1719
|
const {
|
|
1254
1720
|
allRefLines,
|
|
1721
|
+
refLineCustom,
|
|
1722
|
+
dragValues,
|
|
1723
|
+
groupHidden,
|
|
1724
|
+
refGroupResult,
|
|
1725
|
+
refGroupingActive,
|
|
1726
|
+
refGroupBadge,
|
|
1727
|
+
refGroupBadgeFont,
|
|
1728
|
+
refGroupFormat,
|
|
1255
1729
|
engine,
|
|
1256
1730
|
effectivePadding,
|
|
1257
1731
|
palette,
|
|
1258
1732
|
formatValue,
|
|
1259
1733
|
skiaFont,
|
|
1734
|
+
fontProp,
|
|
1260
1735
|
degenShakeTransform,
|
|
1261
1736
|
} = model;
|
|
1262
1737
|
if (allRefLines.length === 0) return null;
|
|
@@ -1271,9 +1746,26 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
1271
1746
|
palette={palette}
|
|
1272
1747
|
formatValue={formatValue}
|
|
1273
1748
|
font={skiaFont}
|
|
1749
|
+
fontProp={fontProp}
|
|
1274
1750
|
badgeLayer
|
|
1751
|
+
dragValues={dragValues}
|
|
1752
|
+
index={i}
|
|
1753
|
+
suppressTag={refLineCustom[i]}
|
|
1754
|
+
groupHidden={refGroupingActive ? groupHidden : undefined}
|
|
1275
1755
|
/>
|
|
1276
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
|
+
)}
|
|
1277
1769
|
</Group>
|
|
1278
1770
|
);
|
|
1279
1771
|
}
|
|
@@ -1330,14 +1822,23 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1330
1822
|
bottomLabelCfg,
|
|
1331
1823
|
markersActive,
|
|
1332
1824
|
markersSV,
|
|
1825
|
+
markerClusterCfg,
|
|
1333
1826
|
renderMarker,
|
|
1334
1827
|
renderTooltip,
|
|
1828
|
+
renderOverlay,
|
|
1829
|
+
renderReferenceLine,
|
|
1830
|
+
allRefLines,
|
|
1831
|
+
refLineCustom,
|
|
1832
|
+
dragValues,
|
|
1833
|
+
dragActive,
|
|
1834
|
+
overlayContext,
|
|
1335
1835
|
scrubCfg,
|
|
1336
1836
|
crosshair,
|
|
1337
1837
|
isCandle,
|
|
1338
1838
|
extremaTimeOffset,
|
|
1339
1839
|
topConnector,
|
|
1340
1840
|
bottomConnector,
|
|
1841
|
+
lineIsLinear,
|
|
1341
1842
|
} = model;
|
|
1342
1843
|
|
|
1343
1844
|
return (
|
|
@@ -1414,6 +1915,24 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1414
1915
|
engine={engine}
|
|
1415
1916
|
padding={effectivePadding}
|
|
1416
1917
|
lineData={engine.data}
|
|
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}
|
|
1417
1936
|
/>
|
|
1418
1937
|
)}
|
|
1419
1938
|
|
|
@@ -1435,8 +1954,16 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
1435
1954
|
placement={scrubCfg.tooltipPlacement}
|
|
1436
1955
|
margin={scrubCfg.tooltipMargin}
|
|
1437
1956
|
lineTop={crosshair.tooltipLineTop}
|
|
1957
|
+
scrubDotY={crosshair.scrubDotY}
|
|
1438
1958
|
/>
|
|
1439
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
|
+
)}
|
|
1440
1967
|
</View>
|
|
1441
1968
|
</GestureDetector>
|
|
1442
1969
|
);
|