react-native-livechart 4.20.0 → 4.21.1
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/AxisLabelOverlay.d.ts +5 -1
- package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +4 -2
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomThresholdBadgeOverlay.d.ts +18 -0
- package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +4 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
- package/dist/components/ExtremaConnectorOverlay.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 +4 -2
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +10 -5
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
- package/dist/core/chartDataPresence.d.ts +8 -0
- package/dist/core/chartDataPresence.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +6 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +32 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +3 -1
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/candleGap.d.ts +14 -0
- package/dist/draw/candleGap.d.ts.map +1 -0
- package/dist/draw/lineGap.d.ts +25 -0
- package/dist/draw/lineGap.d.ts.map +1 -0
- package/dist/draw/markerAtlas.d.ts +3 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +7 -3
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCandleGapPaths.d.ts +12 -0
- package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +4 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +1 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -5
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useLineGapPaths.d.ts +10 -0
- package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +2 -2
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +4 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +13 -13
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/candleGaps.d.ts +18 -0
- package/dist/math/candleGaps.d.ts.map +1 -0
- package/dist/math/referenceLineSeries.d.ts +12 -0
- package/dist/math/referenceLineSeries.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +5 -5
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/math/spline.d.ts +5 -1
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +191 -17
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AxisLabelOverlay.tsx +162 -81
- package/src/components/CrosshairOverlay.tsx +394 -147
- package/src/components/CustomMarkerOverlay.tsx +9 -6
- package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
- package/src/components/CustomTooltipOverlay.tsx +7 -3
- package/src/components/DegenParticlesOverlay.tsx +34 -23
- package/src/components/ExtremaConnectorOverlay.tsx +43 -3
- package/src/components/LiveChart.tsx +1918 -974
- package/src/components/LiveChartSeries.tsx +445 -296
- package/src/components/MarkerOverlay.tsx +215 -156
- package/src/components/ReferenceLineOverlay.tsx +113 -55
- package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
- package/src/core/chartDataPresence.ts +24 -0
- package/src/core/liveChartEngineTick.ts +34 -1
- package/src/core/resolveConfig.ts +186 -0
- package/src/core/useLiveChartEngine.ts +36 -1
- package/src/core/useLiveChartSeriesEngine.ts +2 -0
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- package/src/draw/lineGap.ts +180 -0
- package/src/draw/markerAtlas.ts +8 -3
- package/src/hooks/crosshairShared.ts +92 -11
- package/src/hooks/useCandleGapPaths.ts +88 -0
- package/src/hooks/useChartPaths.ts +81 -22
- package/src/hooks/useChartReveal.ts +1 -1
- package/src/hooks/useCrosshair.ts +178 -69
- package/src/hooks/useDegen.ts +82 -53
- package/src/hooks/useLineGapPaths.ts +76 -0
- package/src/hooks/useLiveChartHasData.ts +8 -4
- package/src/hooks/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
- package/src/hooks/useThreshold.ts +31 -22
- package/src/index.ts +15 -0
- package/src/math/candleGaps.ts +120 -0
- package/src/math/referenceLineSeries.ts +83 -0
- package/src/math/referenceLines.ts +34 -6
- package/src/math/spline.ts +15 -5
- package/src/types.ts +202 -17
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
forwardRef,
|
|
3
3
|
useEffect,
|
|
4
4
|
useImperativeHandle,
|
|
5
|
-
useLayoutEffect,
|
|
6
5
|
useMemo,
|
|
7
6
|
useRef,
|
|
8
7
|
useState,
|
|
@@ -18,6 +17,7 @@ import Animated, {
|
|
|
18
17
|
withDelay,
|
|
19
18
|
withSequence,
|
|
20
19
|
withTiming,
|
|
20
|
+
type SharedValue,
|
|
21
21
|
} from "react-native-reanimated";
|
|
22
22
|
import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
|
|
23
23
|
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
resolveAreaDots,
|
|
46
46
|
resolveAxisLabel,
|
|
47
47
|
resolveBadge,
|
|
48
|
+
resolveCandleGaps,
|
|
48
49
|
resolveDegen,
|
|
49
50
|
resolveDot,
|
|
50
51
|
resolveGradient,
|
|
@@ -70,7 +71,10 @@ import {
|
|
|
70
71
|
resolveXAxis,
|
|
71
72
|
resolveYAxis,
|
|
72
73
|
} from "../core/resolveConfig";
|
|
73
|
-
import type {
|
|
74
|
+
import type {
|
|
75
|
+
ResolvedCandleGapsConfig,
|
|
76
|
+
ResolvedThresholdConfig,
|
|
77
|
+
} from "../core/resolveConfig";
|
|
74
78
|
import {
|
|
75
79
|
liveIndicatorScrollOpacity,
|
|
76
80
|
resolveHideLiveOnScrollBack,
|
|
@@ -82,9 +86,14 @@ import {
|
|
|
82
86
|
computeCandleFocusClip,
|
|
83
87
|
HIDDEN_CANDLE_FOCUS_CLIP,
|
|
84
88
|
} from "../draw/candle";
|
|
85
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
dotGlowRadialOutset,
|
|
91
|
+
pulseRadialOutset,
|
|
92
|
+
type ChartPadding,
|
|
93
|
+
} from "../draw/line";
|
|
86
94
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
87
95
|
import { useBadge } from "../hooks/useBadge";
|
|
96
|
+
import { useCandleGapPaths } from "../hooks/useCandleGapPaths";
|
|
88
97
|
import { useCandlePaths, useCandleWidthLerp } from "../hooks/useCandlePaths";
|
|
89
98
|
import { useCanvasLayout } from "../hooks/useCanvasLayout";
|
|
90
99
|
import { useChartColors } from "../hooks/useChartColors";
|
|
@@ -96,6 +105,7 @@ import { useCrosshair } from "../hooks/useCrosshair";
|
|
|
96
105
|
import { useDegen } from "../hooks/useDegen";
|
|
97
106
|
import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
|
|
98
107
|
import { useLiveDot } from "../hooks/useLiveDot";
|
|
108
|
+
import { useLineGapPaths } from "../hooks/useLineGapPaths";
|
|
99
109
|
import { useMarkers } from "../hooks/useMarkers";
|
|
100
110
|
import { useReferenceDrag } from "../hooks/useReferenceDrag";
|
|
101
111
|
import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
|
|
@@ -122,6 +132,10 @@ import {
|
|
|
122
132
|
formatValue as defaultFormatValue,
|
|
123
133
|
} from "../lib/format";
|
|
124
134
|
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
|
|
135
|
+
import {
|
|
136
|
+
candleGapBucketStartAtTime,
|
|
137
|
+
candleGapDefaultLabel,
|
|
138
|
+
} from "../math/candleGaps";
|
|
125
139
|
import { computeScrubDotY } from "../hooks/crosshairShared";
|
|
126
140
|
import {
|
|
127
141
|
groupReferenceLines,
|
|
@@ -147,7 +161,9 @@ import type {
|
|
|
147
161
|
LiveChartPoint,
|
|
148
162
|
LiveChartProps,
|
|
149
163
|
Marker,
|
|
164
|
+
ReferenceLine,
|
|
150
165
|
} from "../types";
|
|
166
|
+
import { CustomThresholdBadgeOverlay } from "./CustomThresholdBadgeOverlay";
|
|
151
167
|
import {
|
|
152
168
|
ThresholdBadgeOverlay,
|
|
153
169
|
ThresholdLineOverlay,
|
|
@@ -256,329 +272,322 @@ function thresholdSplitColorVecs(
|
|
|
256
272
|
/** Transparent vec4 — the band shader's "threshold ended" rest color. */
|
|
257
273
|
const TRANSPARENT_VEC4 = [0, 0, 0, 0];
|
|
258
274
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
* small and presentational.
|
|
264
|
-
*/
|
|
265
|
-
|
|
266
|
-
function useLiveChartController({
|
|
267
|
-
// ── Data ────────────────────────────────────────────────────────────────
|
|
268
|
-
data,
|
|
269
|
-
value,
|
|
270
|
-
|
|
271
|
-
// ── Appearance ──────────────────────────────────────────────────────────
|
|
272
|
-
theme = "dark",
|
|
273
|
-
accentColor = DEFAULT_ACCENT_COLOR,
|
|
274
|
-
gradient = true,
|
|
275
|
-
areaDots,
|
|
276
|
-
line: lineProp,
|
|
277
|
-
font: fontProp,
|
|
278
|
-
insets,
|
|
279
|
-
style,
|
|
280
|
-
seriesOpacity,
|
|
281
|
-
canvasMode = "transparent",
|
|
282
|
-
|
|
283
|
-
// ── Candlestick ─────────────────────────────────────────────────────────
|
|
284
|
-
mode = "line",
|
|
285
|
-
candles,
|
|
286
|
-
candleWidth = 60,
|
|
287
|
-
liveCandle,
|
|
288
|
-
volume,
|
|
289
|
-
|
|
290
|
-
// ── Behaviour ───────────────────────────────────────────────────────────
|
|
291
|
-
timeWindow = 30,
|
|
292
|
-
paused = false,
|
|
293
|
-
loading = false,
|
|
294
|
-
transitions,
|
|
295
|
-
// `static` is a reserved word — alias it so the destructure parses.
|
|
296
|
-
static: isStatic = false,
|
|
297
|
-
snapKey,
|
|
298
|
-
smoothing = 0.08,
|
|
299
|
-
exaggerate = false,
|
|
300
|
-
nonNegative = false,
|
|
301
|
-
maxValue,
|
|
302
|
-
yRangeScale,
|
|
303
|
-
windowBuffer = 0,
|
|
304
|
-
nowOverride,
|
|
305
|
-
timeScroll = false,
|
|
306
|
-
returnToLive,
|
|
307
|
-
zoom = false,
|
|
308
|
-
accessibilityLabel,
|
|
309
|
-
accessibilityRole = "image",
|
|
310
|
-
emptyText = "No data",
|
|
311
|
-
formatValue = defaultFormatValue,
|
|
312
|
-
formatTime = defaultFormatTime,
|
|
313
|
-
|
|
314
|
-
// ── Overlays ────────────────────────────────────────────────────────────
|
|
315
|
-
yAxis = true,
|
|
316
|
-
xAxis = true,
|
|
317
|
-
axisAutoHide = false,
|
|
275
|
+
function resolveLiveChartFeatureConfig({
|
|
276
|
+
mode,
|
|
277
|
+
yAxis,
|
|
278
|
+
xAxis,
|
|
318
279
|
topLabel,
|
|
319
280
|
bottomLabel,
|
|
320
|
-
badge
|
|
321
|
-
|
|
322
|
-
pulse = true,
|
|
323
|
-
dot,
|
|
324
|
-
valueLine = true,
|
|
325
|
-
showValue = false,
|
|
326
|
-
valueMomentumColor = false,
|
|
327
|
-
referenceLines,
|
|
328
|
-
segments,
|
|
329
|
-
threshold,
|
|
330
|
-
gridStyle,
|
|
331
|
-
palette: paletteOverride,
|
|
332
|
-
metrics,
|
|
333
|
-
scrub = true,
|
|
281
|
+
badge,
|
|
282
|
+
scrub,
|
|
334
283
|
scrubAction,
|
|
284
|
+
volume,
|
|
285
|
+
candleGaps,
|
|
286
|
+
lineGaps,
|
|
287
|
+
gradient,
|
|
288
|
+
areaDots,
|
|
289
|
+
threshold,
|
|
290
|
+
valueLine,
|
|
291
|
+
isStatic,
|
|
292
|
+
pulse,
|
|
293
|
+
dot,
|
|
335
294
|
selectionDot,
|
|
336
|
-
|
|
295
|
+
gridStyle,
|
|
337
296
|
degen,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// (hooks can't be); the geometry is ignored when no threshold is configured.
|
|
366
|
-
const emptyThresholdValue = useSharedValue(0);
|
|
297
|
+
tradeStream,
|
|
298
|
+
metrics,
|
|
299
|
+
}: Pick<
|
|
300
|
+
LiveChartProps,
|
|
301
|
+
| "mode"
|
|
302
|
+
| "yAxis"
|
|
303
|
+
| "xAxis"
|
|
304
|
+
| "topLabel"
|
|
305
|
+
| "bottomLabel"
|
|
306
|
+
| "badge"
|
|
307
|
+
| "scrub"
|
|
308
|
+
| "scrubAction"
|
|
309
|
+
| "volume"
|
|
310
|
+
| "candleGaps"
|
|
311
|
+
| "lineGaps"
|
|
312
|
+
| "gradient"
|
|
313
|
+
| "areaDots"
|
|
314
|
+
| "threshold"
|
|
315
|
+
| "valueLine"
|
|
316
|
+
| "pulse"
|
|
317
|
+
| "dot"
|
|
318
|
+
| "selectionDot"
|
|
319
|
+
| "gridStyle"
|
|
320
|
+
| "degen"
|
|
321
|
+
| "tradeStream"
|
|
322
|
+
| "metrics"
|
|
323
|
+
> & { isStatic: boolean }) {
|
|
367
324
|
const isCandle = mode === "candle";
|
|
368
|
-
|
|
369
|
-
// ── Resolve feature configs ────────────────────────────────────────────
|
|
370
|
-
const yAxisCfg = resolveYAxis(yAxis);
|
|
371
|
-
const xAxisCfg = resolveXAxis(xAxis);
|
|
372
|
-
const topLabelCfg = resolveAxisLabel(topLabel);
|
|
373
|
-
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
374
325
|
const badgeCfg = resolveBadge(badge);
|
|
375
|
-
const
|
|
376
|
-
// Scrub + scrub-action are on-demand touch gestures (event-driven, no per-frame
|
|
377
|
-
// loop), so they stay live on static charts — `static` suppresses the continuous
|
|
378
|
-
// render loop, not interaction. A still sparkline in a list can still be scrubbed.
|
|
379
|
-
const scrubActionCfg = resolveScrubAction(scrubAction);
|
|
380
|
-
// Volume bars sit below the candles — a candle-mode-only feature (inert in
|
|
381
|
-
// line mode, like the candle paths themselves).
|
|
382
|
-
const volumeCfg = isCandle ? resolveVolume(volume) : null;
|
|
383
|
-
const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
|
|
384
|
-
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
385
|
-
// Dot-lattice area fill (clipped to the under-line region). Inert in candle
|
|
386
|
-
// mode, same as the gradient fill.
|
|
387
|
-
const areaDotsCfg = isCandle ? null : resolveAreaDots(areaDots);
|
|
388
|
-
// Threshold split is a line-mode feature (candle bodies carry their own up/down
|
|
389
|
-
// colors), so it's inert in candle mode — same as the area gradient.
|
|
326
|
+
const chartGapsCfg = resolveCandleGaps(isCandle ? candleGaps : lineGaps);
|
|
390
327
|
const thresholdCfg = isCandle ? null : resolveThreshold(threshold);
|
|
391
328
|
const thresholdSeriesSV = thresholdCfg?.series ?? null;
|
|
392
|
-
const thresholdIsSeries =
|
|
393
|
-
thresholdSeriesSV !== null || Array.isArray(thresholdCfg?.value);
|
|
394
|
-
const valueLineCfg = resolveValueLine(valueLine);
|
|
395
|
-
// Static charts run zero loops: force the pulse off so its `withRepeat`-driven
|
|
396
|
-
// ring never starts (the DotOverlay reads `pulseCfg`, so null = no pulse).
|
|
397
|
-
const pulseCfg = isStatic ? null : resolvePulse(pulse);
|
|
398
329
|
const dotCfg = resolveDot(dot);
|
|
399
|
-
|
|
400
|
-
// must stay aligned with its badge, so the tracking flag is ignored.
|
|
401
|
-
const dotTracksParked =
|
|
402
|
-
dotCfg.trackWhileParked && !(badgeCfg?.followViewEdge ?? false);
|
|
403
|
-
const selectionDotCfg = resolveSelectionDot(selectionDot);
|
|
404
|
-
// Outer visible footprint of the dot, including its crisp ring and optional
|
|
405
|
-
// blurred glow. Used by scrub dimming so neither effect leaks through.
|
|
406
|
-
const dotOuterRadius = Math.max(
|
|
407
|
-
dotCfg.radius + (dotCfg.ring?.width ?? 0),
|
|
408
|
-
dotCfg.glow ? dotGlowRadialOutset(dotCfg.glow.radius, dotCfg.glow.blur) : 0,
|
|
409
|
-
);
|
|
410
|
-
const gridStyleCfg = resolveGridStyle(gridStyle);
|
|
411
|
-
// Static charts run zero loops: force degen off so `useDegen`'s frame callback
|
|
412
|
-
// never starts (also passed `isStatic` below as a belt-and-braces autostart gate).
|
|
413
|
-
const degenCfg = isStatic ? null : resolveDegen(degen);
|
|
414
|
-
const tradeStreamResolved = resolveTradeStream(tradeStream);
|
|
415
|
-
const metricsCfg = resolveMetrics(metrics);
|
|
416
|
-
|
|
417
|
-
const allRefLines = referenceLines ?? [];
|
|
418
|
-
const refValues = collectReferenceValues(allRefLines);
|
|
419
|
-
|
|
420
|
-
// Per-line live value overrides + drag flags for draggable lines and the custom
|
|
421
|
-
// `renderReferenceLine` slot. One array SharedValue each (the line count varies,
|
|
422
|
-
// so a single SharedValue beats N hooks). Seeded from each line's static `value`;
|
|
423
|
-
// the drag gesture overwrites a slot, and the effect re-seeds slots not being
|
|
424
|
-
// dragged when the props change (so a controlled `value` flows back in).
|
|
425
|
-
const dragValues = useSharedValue<number[]>([]);
|
|
426
|
-
const dragActive = useSharedValue<boolean[]>([]);
|
|
427
|
-
// Last `value` props used to seed each slot, so reconciliation can tell a
|
|
428
|
-
// controlled prop change (adopt it) from an unchanged prop (keep the dragged
|
|
429
|
-
// value — uncontrolled persistence) without resetting a drop on every re-render.
|
|
430
|
-
const seededRef = useRef<(number | undefined)[]>([]);
|
|
431
|
-
const refValueSig = allRefLines.map((l) => l.value ?? "_").join(",");
|
|
432
|
-
useEffect(() => {
|
|
433
|
-
const active = dragActive.get();
|
|
434
|
-
const cur = dragValues.get();
|
|
435
|
-
const seeded = seededRef.current;
|
|
436
|
-
dragValues.set(
|
|
437
|
-
allRefLines.map((l, i) => {
|
|
438
|
-
const prop = l.value ?? 0;
|
|
439
|
-
if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
|
|
440
|
-
if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
|
|
441
|
-
return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
|
|
442
|
-
}),
|
|
443
|
-
);
|
|
444
|
-
seededRef.current = allRefLines.map((l) => l.value);
|
|
445
|
-
if (dragActive.get().length !== allRefLines.length) {
|
|
446
|
-
dragActive.set(allRefLines.map((_, i) => active[i] ?? false));
|
|
447
|
-
}
|
|
448
|
-
// allRefLines is rebuilt every render; key off the value signature + length.
|
|
449
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
450
|
-
}, [refValueSig, allRefLines.length]);
|
|
330
|
+
const volumeCfg = isCandle ? resolveVolume(volume) : null;
|
|
451
331
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
332
|
+
return {
|
|
333
|
+
isCandle,
|
|
334
|
+
yAxisCfg: resolveYAxis(yAxis),
|
|
335
|
+
xAxisCfg: resolveXAxis(xAxis),
|
|
336
|
+
topLabelCfg: resolveAxisLabel(topLabel),
|
|
337
|
+
bottomLabelCfg: resolveAxisLabel(bottomLabel),
|
|
338
|
+
badgeCfg,
|
|
339
|
+
scrubCfg: resolveScrub(scrub),
|
|
340
|
+
scrubActionCfg: resolveScrubAction(scrubAction),
|
|
341
|
+
volumeCfg,
|
|
342
|
+
chartGapsCfg,
|
|
343
|
+
candleGapsCfg: isCandle ? chartGapsCfg : null,
|
|
344
|
+
lineGapsCfg: isCandle ? null : chartGapsCfg,
|
|
345
|
+
volumeBandHeight: volumeCfg?.maxHeight ?? 0,
|
|
346
|
+
gradientCfg: isCandle ? null : resolveGradient(gradient),
|
|
347
|
+
areaDotsCfg: isCandle ? null : resolveAreaDots(areaDots),
|
|
348
|
+
thresholdCfg,
|
|
349
|
+
thresholdSeriesSV,
|
|
350
|
+
thresholdIsSeries:
|
|
351
|
+
thresholdSeriesSV !== null || Array.isArray(thresholdCfg?.value),
|
|
352
|
+
valueLineCfg: resolveValueLine(valueLine),
|
|
353
|
+
pulseCfg: isStatic ? null : resolvePulse(pulse),
|
|
354
|
+
dotCfg,
|
|
355
|
+
dotTracksParked:
|
|
356
|
+
dotCfg.trackWhileParked && !(badgeCfg?.followViewEdge ?? false),
|
|
357
|
+
selectionDotCfg: resolveSelectionDot(selectionDot),
|
|
358
|
+
dotOuterRadius: Math.max(
|
|
359
|
+
dotCfg.radius + (dotCfg.ring?.width ?? 0),
|
|
360
|
+
dotCfg.glow
|
|
361
|
+
? dotGlowRadialOutset(dotCfg.glow.radius, dotCfg.glow.blur)
|
|
362
|
+
: 0,
|
|
363
|
+
),
|
|
364
|
+
gridStyleCfg: resolveGridStyle(gridStyle),
|
|
365
|
+
degenCfg: isStatic ? null : resolveDegen(degen),
|
|
366
|
+
tradeStreamResolved: resolveTradeStream(tradeStream),
|
|
367
|
+
metricsCfg: resolveMetrics(metrics),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
469
370
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
371
|
+
function resolveLiveChartReferenceConfig({
|
|
372
|
+
chartGapsCfg,
|
|
373
|
+
referenceLines,
|
|
374
|
+
renderReferenceLine,
|
|
375
|
+
renderOffAxisReferenceLine,
|
|
376
|
+
thresholdCfg,
|
|
377
|
+
referenceLineGrouping,
|
|
378
|
+
badgeCfg,
|
|
379
|
+
}: {
|
|
380
|
+
chartGapsCfg: ReturnType<typeof resolveCandleGaps>;
|
|
381
|
+
referenceLines: LiveChartProps["referenceLines"];
|
|
382
|
+
renderReferenceLine: LiveChartProps["renderReferenceLine"];
|
|
383
|
+
renderOffAxisReferenceLine: LiveChartProps["renderOffAxisReferenceLine"];
|
|
384
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
385
|
+
referenceLineGrouping: LiveChartProps["referenceLineGrouping"];
|
|
386
|
+
badgeCfg: ReturnType<typeof resolveBadge>;
|
|
387
|
+
}) {
|
|
388
|
+
const chartGapBands: ReferenceLine[] =
|
|
389
|
+
chartGapsCfg?.gaps.flatMap((gap) => {
|
|
390
|
+
const gapStyle = chartGapsCfg.styles[gap.kind];
|
|
391
|
+
const band = gapStyle.band;
|
|
392
|
+
if (band === null) return [];
|
|
393
|
+
const label = gapStyle.label;
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
id: `chart-gap:${gap.kind}:${gap.from}:${gap.to}`,
|
|
397
|
+
from: gap.from,
|
|
398
|
+
to: gap.to,
|
|
399
|
+
label:
|
|
400
|
+
label === null
|
|
401
|
+
? undefined
|
|
402
|
+
: (gap.label ?? candleGapDefaultLabel(gap.kind)),
|
|
403
|
+
color: band.borderColor,
|
|
404
|
+
fillColor: band.fillColor,
|
|
405
|
+
fillOpacity: band.fillOpacity,
|
|
406
|
+
strokeOpacity: band.borderOpacity,
|
|
407
|
+
strokeWidth: band.borderWidth > 0 ? band.borderWidth : undefined,
|
|
408
|
+
intervals: band.intervals,
|
|
409
|
+
labelColor: label?.color,
|
|
410
|
+
labelPosition: label?.position,
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
}) ?? [];
|
|
414
|
+
const consumerRefLines = referenceLines ?? [];
|
|
415
|
+
const allRefLines = [...consumerRefLines, ...chartGapBands];
|
|
416
|
+
const refLineCustom = [
|
|
417
|
+
...customReferenceLineFlags(consumerRefLines, renderReferenceLine),
|
|
418
|
+
...chartGapBands.map(() => false),
|
|
419
|
+
];
|
|
420
|
+
const refLineOffAxisCustom = [
|
|
421
|
+
...customReferenceLineFlags(
|
|
422
|
+
consumerRefLines,
|
|
423
|
+
renderOffAxisReferenceLine,
|
|
424
|
+
"off-axis",
|
|
425
|
+
),
|
|
426
|
+
...chartGapBands.map(() => false),
|
|
427
|
+
].map((custom, index) => custom && !refLineCustom[index]);
|
|
480
428
|
const draggableRefIdx: number[] = [];
|
|
481
|
-
for (let
|
|
482
|
-
const line = allRefLines[
|
|
429
|
+
for (let index = 0; index < allRefLines.length; index++) {
|
|
430
|
+
const line = allRefLines[index];
|
|
483
431
|
if (
|
|
484
432
|
line.draggable &&
|
|
485
433
|
!line.excludeFromRange &&
|
|
486
434
|
referenceLineForm(line) === "line"
|
|
487
435
|
) {
|
|
488
|
-
draggableRefIdx.push(
|
|
436
|
+
draggableRefIdx.push(index);
|
|
489
437
|
}
|
|
490
438
|
}
|
|
491
439
|
const thresholdInRange = thresholdCfg?.includeInRange === true;
|
|
492
|
-
// Constant benchmark → its live value rides this channel. A series threshold
|
|
493
|
-
// is folded inside the engine tick instead (`thresholdRangePoints`), which
|
|
494
|
-
// already knows the window bounds for the min/max.
|
|
495
440
|
const thresholdRangeValueSV =
|
|
496
441
|
thresholdInRange &&
|
|
497
442
|
thresholdCfg?.value != null &&
|
|
498
443
|
!Array.isArray(thresholdCfg.value)
|
|
499
444
|
? thresholdCfg.value
|
|
500
445
|
: null;
|
|
446
|
+
const refGroupingCfg =
|
|
447
|
+
typeof referenceLineGrouping === "object"
|
|
448
|
+
? referenceLineGrouping
|
|
449
|
+
: undefined;
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
chartGapBands,
|
|
453
|
+
allRefLines,
|
|
454
|
+
refValues: collectReferenceValues(allRefLines),
|
|
455
|
+
refLineCustom,
|
|
456
|
+
refLineOffAxisCustom,
|
|
457
|
+
refLineKeys: referenceLineReactKeys(allRefLines),
|
|
458
|
+
draggableRefIdx,
|
|
459
|
+
thresholdInRange,
|
|
460
|
+
thresholdRangeValueSV,
|
|
461
|
+
refGroupingCfg,
|
|
462
|
+
refGroupingRadius: referenceLineGrouping
|
|
463
|
+
? (refGroupingCfg?.radius ?? 18)
|
|
464
|
+
: null,
|
|
465
|
+
refGroupBadge: resolveReferenceGroupBadge(refGroupingCfg?.badge),
|
|
466
|
+
refGroupFormat: refGroupingCfg?.format,
|
|
467
|
+
badgeUsesRightGutter:
|
|
468
|
+
badgeCfg !== null && (badgeCfg.position ?? "right") === "right",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function useLiveReferenceState(
|
|
473
|
+
allRefLines: ReferenceLine[],
|
|
474
|
+
draggableRefIdx: number[],
|
|
475
|
+
thresholdRangeValueSV: SharedValue<number> | null,
|
|
476
|
+
) {
|
|
477
|
+
const dragValues = useSharedValue<number[]>([]);
|
|
478
|
+
const dragActive = useSharedValue<boolean[]>([]);
|
|
479
|
+
const seededRef = useRef<(number | undefined)[]>([]);
|
|
480
|
+
const refValueSig = allRefLines.map((line) => line.value ?? "_").join(",");
|
|
481
|
+
const propValues = useMemo<(number | undefined)[]>(() => {
|
|
482
|
+
if (refValueSig === "") return [];
|
|
483
|
+
return refValueSig
|
|
484
|
+
.split(",")
|
|
485
|
+
.map((encoded) => (encoded === "_" ? undefined : Number(encoded)));
|
|
486
|
+
}, [refValueSig]);
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
const active = dragActive.get();
|
|
489
|
+
const current = dragValues.get();
|
|
490
|
+
const seeded = seededRef.current;
|
|
491
|
+
dragValues.set(
|
|
492
|
+
propValues.map((value, index) => {
|
|
493
|
+
const prop = value ?? 0;
|
|
494
|
+
if (active[index]) return current[index] ?? prop;
|
|
495
|
+
if (value !== seeded[index]) return prop;
|
|
496
|
+
return current[index] ?? prop;
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
seededRef.current = propValues;
|
|
500
|
+
if (dragActive.get().length !== propValues.length) {
|
|
501
|
+
dragActive.set(propValues.map((_, index) => active[index] ?? false));
|
|
502
|
+
}
|
|
503
|
+
}, [dragActive, dragValues, propValues]);
|
|
504
|
+
|
|
505
|
+
const refLineCustomTagWidths = useSharedValue<number[]>([]);
|
|
501
506
|
const liveRefValues = useDerivedValue<number[]>(() => {
|
|
502
|
-
if (draggableRefIdx.length === 0 && thresholdRangeValueSV === null)
|
|
507
|
+
if (draggableRefIdx.length === 0 && thresholdRangeValueSV === null) {
|
|
503
508
|
return EMPTY_NUMS;
|
|
504
|
-
|
|
509
|
+
}
|
|
510
|
+
const output: number[] = [];
|
|
505
511
|
if (draggableRefIdx.length > 0) {
|
|
506
|
-
const
|
|
507
|
-
for (let
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
512
|
+
const values = dragValues.get();
|
|
513
|
+
for (let index = 0; index < draggableRefIdx.length; index++) {
|
|
514
|
+
const value = values[draggableRefIdx[index]];
|
|
515
|
+
if (value != null) output.push(value);
|
|
510
516
|
}
|
|
511
517
|
}
|
|
512
518
|
if (thresholdRangeValueSV !== null) {
|
|
513
|
-
const
|
|
514
|
-
if (Number.isFinite(
|
|
519
|
+
const value = thresholdRangeValueSV.get();
|
|
520
|
+
if (Number.isFinite(value)) output.push(value);
|
|
515
521
|
}
|
|
516
|
-
return
|
|
522
|
+
return output;
|
|
517
523
|
});
|
|
518
524
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
525
|
+
return { dragValues, dragActive, refLineCustomTagWidths, liveRefValues };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function resolveLiveChartPresentationConfig({
|
|
529
|
+
accentColor,
|
|
530
|
+
theme,
|
|
531
|
+
paletteOverride,
|
|
532
|
+
segments,
|
|
533
|
+
leftEdgeFade,
|
|
534
|
+
fontProp,
|
|
535
|
+
badgeCfg,
|
|
536
|
+
refGroupBadge,
|
|
537
|
+
pulseCfg,
|
|
538
|
+
timeScroll,
|
|
539
|
+
isStatic,
|
|
540
|
+
returnToLive,
|
|
541
|
+
zoom,
|
|
542
|
+
yAxisCfg,
|
|
543
|
+
}: {
|
|
544
|
+
accentColor: string;
|
|
545
|
+
theme: NonNullable<LiveChartProps["theme"]>;
|
|
546
|
+
paletteOverride: LiveChartProps["palette"];
|
|
547
|
+
segments: LiveChartProps["segments"];
|
|
548
|
+
leftEdgeFade: LiveChartProps["leftEdgeFade"];
|
|
549
|
+
fontProp: LiveChartProps["font"];
|
|
550
|
+
badgeCfg: ReturnType<typeof resolveBadge>;
|
|
551
|
+
refGroupBadge: ReturnType<typeof resolveReferenceGroupBadge>;
|
|
552
|
+
pulseCfg: ReturnType<typeof resolvePulse>;
|
|
553
|
+
timeScroll: LiveChartProps["timeScroll"];
|
|
554
|
+
isStatic: boolean;
|
|
555
|
+
returnToLive: LiveChartProps["returnToLive"];
|
|
556
|
+
zoom: LiveChartProps["zoom"];
|
|
557
|
+
yAxisCfg: ReturnType<typeof resolveYAxis>;
|
|
558
|
+
}) {
|
|
537
559
|
const palette = applyPaletteOverride(
|
|
538
560
|
resolveTheme(accentColor, theme),
|
|
539
561
|
paletteOverride,
|
|
540
562
|
);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
// reference lines; the divider/label/muted colors default to the chart palette
|
|
544
|
-
// (no per-segment base color needed). `hasRecolorSegments` is a render-time gate
|
|
545
|
-
// for the line-recolor gradient pass (per-frame visibility is handled by the
|
|
546
|
-
// gradient's transparent stops, not by mounting/unmounting the Path).
|
|
547
|
-
const resolvedSegments = (segments ?? []).map((s) =>
|
|
548
|
-
resolveSegment(s, {
|
|
563
|
+
const resolvedSegments = (segments ?? []).map((segment) =>
|
|
564
|
+
resolveSegment(segment, {
|
|
549
565
|
muted: palette.gridLabel,
|
|
550
566
|
divider: palette.refLine,
|
|
551
567
|
label: palette.refLabel,
|
|
552
568
|
}),
|
|
553
569
|
);
|
|
554
|
-
const hasRecolorSegments = resolvedSegments.some((s) => s.recolorLine);
|
|
555
|
-
|
|
556
|
-
const leftEdgeFadeCfg = resolveLeftEdgeFade(
|
|
557
|
-
leftEdgeFade,
|
|
558
|
-
leftEdgeFadeColorsFromBgRgb(palette.bgRgb),
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
const skiaFont = useChartSkiaFont(
|
|
562
|
-
fontProp,
|
|
563
|
-
MONO_FONT_FAMILY,
|
|
564
|
-
palette.labelFontSize,
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
// Larger font for the optional live-value text overlay (showValue).
|
|
568
|
-
const valueFont = useChartSkiaFont(
|
|
569
|
-
fontProp,
|
|
570
|
-
MONO_FONT_FAMILY,
|
|
571
|
-
palette.valueFontSize * 2,
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
// Per-badge font (size/family/weight) override; reuses the chart font when
|
|
575
|
-
// none of the badge font knobs are set, so the gutter sizing is unchanged.
|
|
576
570
|
const badgeHasFontOverride =
|
|
577
571
|
badgeCfg?.fontSize != null ||
|
|
578
572
|
badgeCfg?.fontFamily != null ||
|
|
579
573
|
badgeCfg?.fontWeight != null;
|
|
580
|
-
const
|
|
581
|
-
|
|
574
|
+
const refGroupBadgeHasFont =
|
|
575
|
+
refGroupBadge.fontSize != null ||
|
|
576
|
+
refGroupBadge.fontFamily != null ||
|
|
577
|
+
refGroupBadge.fontWeight != null;
|
|
578
|
+
const timeScrollEnabled = Boolean(timeScroll) && !isStatic;
|
|
579
|
+
const zoomCfg = resolveZoom(zoom);
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
palette,
|
|
583
|
+
resolvedSegments,
|
|
584
|
+
hasRecolorSegments: resolvedSegments.some((segment) => segment.recolorLine),
|
|
585
|
+
leftEdgeFadeCfg: resolveLeftEdgeFade(
|
|
586
|
+
leftEdgeFade,
|
|
587
|
+
leftEdgeFadeColorsFromBgRgb(palette.bgRgb),
|
|
588
|
+
),
|
|
589
|
+
badgeHasFontOverride,
|
|
590
|
+
badgeFontConfig: badgeHasFontOverride
|
|
582
591
|
? {
|
|
583
592
|
...fontProp,
|
|
584
593
|
fontFamily: badgeCfg?.fontFamily ?? fontProp?.fontFamily,
|
|
@@ -586,18 +595,8 @@ function useLiveChartController({
|
|
|
586
595
|
fontWeight: badgeCfg?.fontWeight ?? fontProp?.fontWeight,
|
|
587
596
|
}
|
|
588
597
|
: fontProp,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
);
|
|
592
|
-
const badgeFont = badgeHasFontOverride ? badgeFontOverride : skiaFont;
|
|
593
|
-
|
|
594
|
-
// Per-badge font for the grouping count pill (same override pattern as above).
|
|
595
|
-
const refGroupBadgeHasFont =
|
|
596
|
-
refGroupBadge.fontSize != null ||
|
|
597
|
-
refGroupBadge.fontFamily != null ||
|
|
598
|
-
refGroupBadge.fontWeight != null;
|
|
599
|
-
const refGroupBadgeFontOverride = useChartSkiaFont(
|
|
600
|
-
refGroupBadgeHasFont
|
|
598
|
+
refGroupBadgeHasFont,
|
|
599
|
+
refGroupBadgeFontConfig: refGroupBadgeHasFont
|
|
601
600
|
? {
|
|
602
601
|
...fontProp,
|
|
603
602
|
fontFamily: refGroupBadge.fontFamily ?? fontProp?.fontFamily,
|
|
@@ -605,62 +604,727 @@ function useLiveChartController({
|
|
|
605
604
|
fontWeight: refGroupBadge.fontWeight ?? fontProp?.fontWeight,
|
|
606
605
|
}
|
|
607
606
|
: fontProp,
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
607
|
+
pulseConfig: pulseCfg
|
|
608
|
+
? {
|
|
609
|
+
maxRadius: pulseCfg.maxRadius,
|
|
610
|
+
strokeWidth: pulseCfg.strokeWidth,
|
|
611
|
+
}
|
|
612
|
+
: null,
|
|
613
|
+
timeScrollEnabled,
|
|
614
|
+
returnToLiveMs: resolveReturnToLiveMs(returnToLive),
|
|
615
|
+
timeScrollOverscroll: timeScrollEnabled ? resolveOverscroll(timeScroll) : 0,
|
|
616
|
+
timeScrollFling: resolveFling(timeScroll),
|
|
617
|
+
zoomCfg,
|
|
618
|
+
zoomEnabled: zoomCfg !== null && !isStatic,
|
|
619
|
+
yAxisFloat: yAxisCfg?.float ?? false,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
614
622
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
623
|
+
function resolveThresholdColorConfig(
|
|
624
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>,
|
|
625
|
+
palette: LiveChartPalette,
|
|
626
|
+
lineProp: LiveChartProps["line"],
|
|
627
|
+
) {
|
|
628
|
+
return {
|
|
629
|
+
stopColors: thresholdCfg ? thresholdStops(thresholdCfg, palette) : null,
|
|
630
|
+
splitAbove: thresholdCfg
|
|
631
|
+
? (thresholdCfg.aboveColor ?? palette.candleUp)
|
|
632
|
+
: null,
|
|
633
|
+
splitBelow: thresholdCfg
|
|
634
|
+
? (thresholdCfg.belowColor ?? palette.candleDown)
|
|
635
|
+
: null,
|
|
636
|
+
fillOpacity: thresholdCfg?.fillOpacity ?? THRESHOLD_FILL_OPACITY_DEFAULT,
|
|
637
|
+
lineColor: lineProp?.color ?? palette.line,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function useThresholdGeometryState({
|
|
642
|
+
engine,
|
|
643
|
+
padding,
|
|
644
|
+
thresholdCfg,
|
|
645
|
+
thresholdSeriesSV,
|
|
646
|
+
emptyThresholdValue,
|
|
647
|
+
}: {
|
|
648
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
649
|
+
padding: ChartPadding;
|
|
650
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
651
|
+
thresholdSeriesSV: SharedValue<LiveChartPoint[]> | null;
|
|
652
|
+
emptyThresholdValue: SharedValue<number>;
|
|
653
|
+
}) {
|
|
654
|
+
const thresholdValue = thresholdCfg?.value ?? emptyThresholdValue;
|
|
655
|
+
const [svSeriesNonEmpty, setSvSeriesNonEmpty] = useState(false);
|
|
656
|
+
useAnimatedReaction(
|
|
657
|
+
() => (thresholdSeriesSV ? thresholdSeriesSV.get().length > 0 : false),
|
|
658
|
+
/* istanbul ignore next -- UI-thread reaction */ (hasPoints, previous) => {
|
|
659
|
+
if (hasPoints !== previous) {
|
|
660
|
+
scheduleOnRN(setSvSeriesNonEmpty, hasPoints);
|
|
619
661
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}, [value]);
|
|
638
|
-
|
|
639
|
-
// `scrolledBack` mirrors the UI-thread scroll state (engine.viewEnd != null)
|
|
640
|
-
// onto the JS thread (set by the reaction below, after the engine exists).
|
|
641
|
-
const [scrolledBack, setScrolledBack] = useState(false);
|
|
642
|
-
const timeScrollEnabled = Boolean(timeScroll) && !isStatic;
|
|
643
|
-
// Glide duration for the return-to-live animation (0 = instant). A sibling of
|
|
644
|
-
// `timeScroll` so it survives `timeScroll={false}` (the disable that triggers it).
|
|
645
|
-
const returnToLiveMs = resolveReturnToLiveMs(returnToLive);
|
|
646
|
-
// Overscroll fraction ([0, 1)) — how far pan/zoom may travel past the data
|
|
647
|
-
// bounds into blank space. 0 (the default) keeps the classic hard stops.
|
|
648
|
-
const timeScrollOverscroll = timeScrollEnabled
|
|
649
|
-
? resolveOverscroll(timeScroll)
|
|
650
|
-
: 0;
|
|
651
|
-
// Release inertia (fling) — `timeScroll.fling: false` stops the pan dead.
|
|
652
|
-
const timeScrollFling = resolveFling(timeScroll);
|
|
653
|
-
const zoomCfg = resolveZoom(zoom);
|
|
654
|
-
const zoomEnabled = zoomCfg !== null && !isStatic;
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
return {
|
|
665
|
+
thresholdSeriesHasPoints: thresholdSeriesSV
|
|
666
|
+
? svSeriesNonEmpty
|
|
667
|
+
: Array.isArray(thresholdCfg?.value) && thresholdCfg.value.length > 0,
|
|
668
|
+
thresholdGeom: useThreshold(engine, padding, thresholdValue),
|
|
669
|
+
thresholdSeriesGeom: useThresholdSeries(
|
|
670
|
+
engine,
|
|
671
|
+
padding,
|
|
672
|
+
thresholdValue,
|
|
673
|
+
thresholdSeriesSV,
|
|
674
|
+
thresholdCfg?.extendToNow ?? true,
|
|
675
|
+
thresholdCfg?.line?.labelAnchor ?? "last",
|
|
676
|
+
),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
655
679
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
680
|
+
function useThresholdShaderUniforms({
|
|
681
|
+
engine,
|
|
682
|
+
padding,
|
|
683
|
+
samples,
|
|
684
|
+
clipRightX,
|
|
685
|
+
thresholdCfg,
|
|
686
|
+
palette,
|
|
687
|
+
lineProp,
|
|
688
|
+
}: {
|
|
689
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
690
|
+
padding: ChartPadding;
|
|
691
|
+
samples: SharedValue<number[]>;
|
|
692
|
+
clipRightX: SharedValue<number>;
|
|
693
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
694
|
+
palette: LiveChartPalette;
|
|
695
|
+
lineProp: LiveChartProps["line"];
|
|
696
|
+
}) {
|
|
697
|
+
const { stopColors, splitAbove, splitBelow, fillOpacity, lineColor } =
|
|
698
|
+
resolveThresholdColorConfig(thresholdCfg, palette, lineProp);
|
|
699
|
+
const vectors = useMemo(() => {
|
|
700
|
+
if (splitAbove === null || splitBelow === null) return null;
|
|
701
|
+
const [r, g, b, a] = parseColorRgba(lineColor);
|
|
702
|
+
return {
|
|
703
|
+
...thresholdSplitColorVecs(splitAbove, splitBelow, fillOpacity),
|
|
704
|
+
strokeRest: [r / 255, g / 255, b / 255, a],
|
|
705
|
+
};
|
|
706
|
+
}, [splitAbove, splitBelow, fillOpacity, lineColor]);
|
|
707
|
+
return {
|
|
708
|
+
thresholdStrokeColors: stopColors?.stroke ?? null,
|
|
709
|
+
thresholdFillColors: stopColors?.fill ?? null,
|
|
710
|
+
thresholdStrokeUniforms: useThresholdSplitUniforms(
|
|
711
|
+
samples,
|
|
712
|
+
engine,
|
|
713
|
+
padding,
|
|
714
|
+
vectors?.strokeAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
715
|
+
vectors?.strokeBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
716
|
+
vectors?.strokeRest ?? THRESHOLD_FALLBACK_COLOR,
|
|
717
|
+
clipRightX,
|
|
718
|
+
),
|
|
719
|
+
thresholdFillUniforms: useThresholdSplitUniforms(
|
|
720
|
+
samples,
|
|
721
|
+
engine,
|
|
722
|
+
padding,
|
|
723
|
+
vectors?.fillAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
724
|
+
vectors?.fillBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
725
|
+
TRANSPARENT_VEC4,
|
|
726
|
+
clipRightX,
|
|
727
|
+
),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function useThresholdBadgeProjection({
|
|
732
|
+
thresholdCfg,
|
|
733
|
+
thresholdIsSeries,
|
|
734
|
+
thresholdGeom,
|
|
735
|
+
thresholdSeriesGeom,
|
|
736
|
+
renderThresholdBadge,
|
|
737
|
+
formatValue,
|
|
738
|
+
}: {
|
|
739
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
740
|
+
thresholdIsSeries: boolean;
|
|
741
|
+
thresholdGeom: ReturnType<typeof useThreshold>;
|
|
742
|
+
thresholdSeriesGeom: ReturnType<typeof useThresholdSeries>;
|
|
743
|
+
renderThresholdBadge: LiveChartProps["renderThresholdBadge"];
|
|
744
|
+
formatValue: (value: number) => string;
|
|
745
|
+
}) {
|
|
746
|
+
const lineY = thresholdIsSeries
|
|
747
|
+
? thresholdSeriesGeom.badgeLineY
|
|
748
|
+
: thresholdGeom.lineY;
|
|
749
|
+
const markerVisible = thresholdIsSeries
|
|
750
|
+
? thresholdSeriesGeom.visible
|
|
751
|
+
: thresholdGeom.visible;
|
|
752
|
+
const badgeVisible = thresholdIsSeries
|
|
753
|
+
? thresholdSeriesGeom.badgeVisible
|
|
754
|
+
: thresholdGeom.visible;
|
|
755
|
+
const markerValue =
|
|
756
|
+
thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
|
|
757
|
+
? (thresholdCfg.value ?? thresholdSeriesGeom.badgeValue)
|
|
758
|
+
: thresholdSeriesGeom.badgeValue;
|
|
759
|
+
const custom = thresholdCfg?.line != null && renderThresholdBadge != null;
|
|
760
|
+
const valueStr = useDerivedValue(() =>
|
|
761
|
+
custom ? formatValue(markerValue.get()) : "",
|
|
762
|
+
);
|
|
763
|
+
return {
|
|
764
|
+
thresholdMarkerLineY: lineY,
|
|
765
|
+
thresholdMarkerVisible: markerVisible,
|
|
766
|
+
thresholdBadgeVisible: badgeVisible,
|
|
767
|
+
thresholdMarkerValue: markerValue,
|
|
768
|
+
thresholdCustomBadge:
|
|
769
|
+
thresholdCfg?.line && renderThresholdBadge
|
|
770
|
+
? renderThresholdBadge({
|
|
771
|
+
line: thresholdCfg.line,
|
|
772
|
+
value: markerValue,
|
|
773
|
+
valueStr,
|
|
774
|
+
y: lineY,
|
|
775
|
+
visible: badgeVisible,
|
|
776
|
+
})
|
|
777
|
+
: null,
|
|
778
|
+
thresholdSeriesPts: thresholdIsSeries
|
|
779
|
+
? thresholdSeriesGeom.screenPts
|
|
780
|
+
: undefined,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function useLiveChartThresholdModel({
|
|
785
|
+
engine,
|
|
786
|
+
padding,
|
|
787
|
+
thresholdCfg,
|
|
788
|
+
thresholdSeriesSV,
|
|
789
|
+
thresholdIsSeries,
|
|
790
|
+
emptyThresholdValue,
|
|
791
|
+
palette,
|
|
792
|
+
lineProp,
|
|
793
|
+
renderThresholdBadge,
|
|
794
|
+
formatValue,
|
|
795
|
+
}: {
|
|
796
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
797
|
+
padding: ChartPadding;
|
|
798
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
799
|
+
thresholdSeriesSV: SharedValue<LiveChartPoint[]> | null;
|
|
800
|
+
thresholdIsSeries: boolean;
|
|
801
|
+
emptyThresholdValue: SharedValue<number>;
|
|
802
|
+
palette: LiveChartPalette;
|
|
803
|
+
lineProp: LiveChartProps["line"];
|
|
804
|
+
renderThresholdBadge: LiveChartProps["renderThresholdBadge"];
|
|
805
|
+
formatValue: (value: number) => string;
|
|
806
|
+
}) {
|
|
807
|
+
const { thresholdSeriesHasPoints, thresholdGeom, thresholdSeriesGeom } =
|
|
808
|
+
useThresholdGeometryState({
|
|
809
|
+
engine,
|
|
810
|
+
padding,
|
|
811
|
+
thresholdCfg,
|
|
812
|
+
thresholdSeriesSV,
|
|
813
|
+
emptyThresholdValue,
|
|
814
|
+
});
|
|
815
|
+
const shaderModel = useThresholdShaderUniforms({
|
|
816
|
+
engine,
|
|
817
|
+
padding,
|
|
818
|
+
samples: thresholdSeriesGeom.samples,
|
|
819
|
+
clipRightX: thresholdSeriesGeom.clipRightX,
|
|
820
|
+
thresholdCfg,
|
|
821
|
+
palette,
|
|
822
|
+
lineProp,
|
|
823
|
+
});
|
|
824
|
+
const badgeModel = useThresholdBadgeProjection({
|
|
825
|
+
thresholdCfg,
|
|
826
|
+
thresholdIsSeries,
|
|
827
|
+
thresholdGeom,
|
|
828
|
+
thresholdSeriesGeom,
|
|
829
|
+
renderThresholdBadge,
|
|
830
|
+
formatValue,
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
thresholdGeom,
|
|
835
|
+
thresholdSeriesHasPoints,
|
|
836
|
+
...shaderModel,
|
|
837
|
+
...badgeModel,
|
|
838
|
+
thresholdFillLineY:
|
|
839
|
+
thresholdCfg?.fill && !thresholdIsSeries
|
|
840
|
+
? thresholdGeom.lineY
|
|
841
|
+
: undefined,
|
|
842
|
+
thresholdFillSamples:
|
|
843
|
+
thresholdCfg?.fill && thresholdSeriesHasPoints
|
|
844
|
+
? thresholdSeriesGeom.samples
|
|
845
|
+
: undefined,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function resolveLiveChartInteractionConfig({
|
|
850
|
+
isCandle,
|
|
851
|
+
mode,
|
|
852
|
+
candlesEngine,
|
|
853
|
+
liveEngine,
|
|
854
|
+
candleWidth,
|
|
855
|
+
candleGapsCfg,
|
|
856
|
+
lineGapsCfg,
|
|
857
|
+
markers,
|
|
858
|
+
markerCluster,
|
|
859
|
+
onReferenceLinePress,
|
|
860
|
+
allRefLines,
|
|
861
|
+
isStatic,
|
|
862
|
+
timeScroll,
|
|
863
|
+
timeScrollEnabled,
|
|
864
|
+
scrubCfg,
|
|
865
|
+
}: {
|
|
866
|
+
isCandle: boolean;
|
|
867
|
+
mode: "line" | "candle";
|
|
868
|
+
candlesEngine: SharedValue<CandlePoint[]>;
|
|
869
|
+
liveEngine: SharedValue<CandlePoint | null>;
|
|
870
|
+
candleWidth: number;
|
|
871
|
+
candleGapsCfg: ResolvedCandleGapsConfig | null;
|
|
872
|
+
lineGapsCfg: ResolvedCandleGapsConfig | null;
|
|
873
|
+
markers: LiveChartProps["markers"];
|
|
874
|
+
markerCluster: LiveChartProps["markerCluster"];
|
|
875
|
+
onReferenceLinePress: LiveChartProps["onReferenceLinePress"];
|
|
876
|
+
allRefLines: ReferenceLine[];
|
|
877
|
+
isStatic: boolean;
|
|
878
|
+
timeScroll: LiveChartProps["timeScroll"];
|
|
879
|
+
timeScrollEnabled: boolean;
|
|
880
|
+
scrubCfg: ReturnType<typeof resolveScrub>;
|
|
881
|
+
}) {
|
|
882
|
+
const crosshairChartOpts = isCandle
|
|
883
|
+
? {
|
|
884
|
+
mode,
|
|
885
|
+
candles: candlesEngine,
|
|
886
|
+
liveCandle: liveEngine,
|
|
887
|
+
candleWidthSecs: candleWidth,
|
|
888
|
+
gaps: candleGapsCfg?.gaps,
|
|
889
|
+
bridgeNoTrades: Boolean(candleGapsCfg?.styles["no-trades"].bridge),
|
|
890
|
+
bridgeUnavailable: Boolean(candleGapsCfg?.styles.unavailable.bridge),
|
|
891
|
+
bridgeUnknown: Boolean(candleGapsCfg?.styles.unknown.bridge),
|
|
892
|
+
}
|
|
893
|
+
: lineGapsCfg
|
|
894
|
+
? {
|
|
895
|
+
mode,
|
|
896
|
+
gaps: lineGapsCfg.gaps,
|
|
897
|
+
bridgeNoTrades: Boolean(lineGapsCfg.styles["no-trades"].bridge),
|
|
898
|
+
bridgeUnavailable: Boolean(lineGapsCfg.styles.unavailable.bridge),
|
|
899
|
+
bridgeUnknown: Boolean(lineGapsCfg.styles.unknown.bridge),
|
|
900
|
+
}
|
|
901
|
+
: undefined;
|
|
902
|
+
const scrollGestureMode =
|
|
903
|
+
typeof timeScroll === "object"
|
|
904
|
+
? (timeScroll.gesture ?? "holdToScrub")
|
|
905
|
+
: "holdToScrub";
|
|
906
|
+
const timeScrollHoldMs =
|
|
907
|
+
typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
|
|
908
|
+
|
|
909
|
+
return {
|
|
910
|
+
crosshairChartOpts,
|
|
911
|
+
markersActive: markers != null,
|
|
912
|
+
markerClusterCfg: resolveMarkerCluster(markerCluster),
|
|
913
|
+
refPressActive: onReferenceLinePress != null && allRefLines.length > 0,
|
|
914
|
+
refDragEnabled:
|
|
915
|
+
!isStatic && allRefLines.some((line) => line.draggable === true),
|
|
916
|
+
scrollGestureMode,
|
|
917
|
+
scrubHoldMs:
|
|
918
|
+
timeScrollEnabled && scrollGestureMode === "holdToScrub"
|
|
919
|
+
? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
|
|
920
|
+
: (scrubCfg?.panGestureDelay ?? 0),
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function composeLiveChartRootGesture({
|
|
925
|
+
crosshair,
|
|
926
|
+
markerTapGesture,
|
|
927
|
+
refLineTapGesture,
|
|
928
|
+
refDragGesture,
|
|
929
|
+
panScrollGesture,
|
|
930
|
+
pinchZoomGesture,
|
|
931
|
+
markersActive,
|
|
932
|
+
refPressActive,
|
|
933
|
+
refDragEnabled,
|
|
934
|
+
scrubActionActive,
|
|
935
|
+
timeScrollEnabled,
|
|
936
|
+
scrollGestureMode,
|
|
937
|
+
zoomEnabled,
|
|
938
|
+
}: {
|
|
939
|
+
crosshair: ReturnType<typeof useCrosshair>;
|
|
940
|
+
markerTapGesture: ReturnType<typeof useMarkers>["tapGesture"];
|
|
941
|
+
refLineTapGesture: ReturnType<typeof useReferenceLinePress>["tapGesture"];
|
|
942
|
+
refDragGesture: ReturnType<typeof useReferenceDrag>["gesture"];
|
|
943
|
+
panScrollGesture: ReturnType<typeof usePanScroll>;
|
|
944
|
+
pinchZoomGesture: ReturnType<typeof usePinchZoom>;
|
|
945
|
+
markersActive: boolean;
|
|
946
|
+
refPressActive: boolean;
|
|
947
|
+
refDragEnabled: boolean;
|
|
948
|
+
scrubActionActive: boolean;
|
|
949
|
+
timeScrollEnabled: boolean;
|
|
950
|
+
scrollGestureMode: "holdToScrub" | "axisDrag";
|
|
951
|
+
zoomEnabled: boolean;
|
|
952
|
+
}) {
|
|
953
|
+
const baseGesture =
|
|
954
|
+
scrubActionActive && crosshair.tapGesture
|
|
955
|
+
? Gesture.Exclusive(crosshair.tapGesture, crosshair.gesture)
|
|
956
|
+
: crosshair.gesture;
|
|
957
|
+
const overlayTaps = [
|
|
958
|
+
markersActive ? markerTapGesture : null,
|
|
959
|
+
refPressActive ? refLineTapGesture : null,
|
|
960
|
+
].filter(
|
|
961
|
+
(gesture): gesture is NonNullable<typeof gesture> => gesture !== null,
|
|
962
|
+
);
|
|
963
|
+
let rootGesture = baseGesture;
|
|
964
|
+
if (overlayTaps.length > 0) {
|
|
965
|
+
const tapGroup =
|
|
966
|
+
overlayTaps.length === 1
|
|
967
|
+
? overlayTaps[0]
|
|
968
|
+
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
969
|
+
rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
|
|
970
|
+
}
|
|
971
|
+
if (timeScrollEnabled) {
|
|
972
|
+
rootGesture =
|
|
973
|
+
scrollGestureMode === "axisDrag"
|
|
974
|
+
? Gesture.Exclusive(panScrollGesture, rootGesture)
|
|
975
|
+
: Gesture.Race(panScrollGesture, rootGesture);
|
|
976
|
+
}
|
|
977
|
+
if (refDragEnabled) {
|
|
978
|
+
rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
|
|
979
|
+
}
|
|
980
|
+
return zoomEnabled
|
|
981
|
+
? Gesture.Simultaneous(rootGesture, pinchZoomGesture)
|
|
982
|
+
: rootGesture;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function useAxisAutoHide({
|
|
986
|
+
config,
|
|
987
|
+
scrollActive,
|
|
988
|
+
scrubActive,
|
|
989
|
+
engine,
|
|
990
|
+
}: {
|
|
991
|
+
config: LiveChartProps["axisAutoHide"];
|
|
992
|
+
scrollActive: SharedValue<boolean>;
|
|
993
|
+
scrubActive: SharedValue<boolean>;
|
|
994
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
995
|
+
}) {
|
|
996
|
+
const resolved = config === true ? {} : config === false ? null : config;
|
|
997
|
+
const idleOpacity = resolved?.idleOpacity ?? 0;
|
|
998
|
+
const fadeInMs = resolved?.fadeInMs ?? 60;
|
|
999
|
+
const fadeOutMs = resolved?.fadeOutMs ?? 250;
|
|
1000
|
+
const hideAfterMs = resolved?.hideAfterMs ?? 3000;
|
|
1001
|
+
const opacity = useSharedValue(resolved ? idleOpacity : 1);
|
|
1002
|
+
const enabled = resolved !== null;
|
|
1003
|
+
const last = useRef({ enabled, idleOpacity });
|
|
1004
|
+
useEffect(() => {
|
|
1005
|
+
if (
|
|
1006
|
+
last.current.enabled === enabled &&
|
|
1007
|
+
last.current.idleOpacity === idleOpacity
|
|
1008
|
+
) {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
last.current = { enabled, idleOpacity };
|
|
1012
|
+
cancelAnimation(opacity);
|
|
1013
|
+
opacity.value = enabled ? idleOpacity : 1;
|
|
1014
|
+
}, [enabled, idleOpacity, opacity]);
|
|
1015
|
+
useAnimatedReaction(
|
|
1016
|
+
() => ({
|
|
1017
|
+
gesture: scrollActive.value || scrubActive.value,
|
|
1018
|
+
viewEnd: engine.viewEnd.value,
|
|
1019
|
+
viewWindow: engine.viewWindow.value,
|
|
1020
|
+
}),
|
|
1021
|
+
(current, previous) => {
|
|
1022
|
+
if (!enabled || previous === null) return;
|
|
1023
|
+
const moved =
|
|
1024
|
+
current.gesture !== previous.gesture ||
|
|
1025
|
+
current.viewEnd !== previous.viewEnd ||
|
|
1026
|
+
current.viewWindow !== previous.viewWindow;
|
|
1027
|
+
if (!moved) return;
|
|
1028
|
+
cancelAnimation(opacity);
|
|
1029
|
+
opacity.value = current.gesture
|
|
1030
|
+
? withTiming(1, { duration: fadeInMs })
|
|
1031
|
+
: withSequence(
|
|
1032
|
+
withTiming(1, { duration: fadeInMs }),
|
|
1033
|
+
withDelay(
|
|
1034
|
+
hideAfterMs,
|
|
1035
|
+
withTiming(idleOpacity, { duration: fadeOutMs }),
|
|
1036
|
+
),
|
|
1037
|
+
);
|
|
1038
|
+
},
|
|
1039
|
+
[enabled, idleOpacity, fadeInMs, fadeOutMs, hideAfterMs],
|
|
1040
|
+
);
|
|
1041
|
+
return opacity;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function resolveLiveEngineModeInputs({
|
|
1045
|
+
isCandle,
|
|
1046
|
+
data,
|
|
1047
|
+
lineEngineData,
|
|
1048
|
+
candles,
|
|
1049
|
+
candlesEngine,
|
|
1050
|
+
liveCandle,
|
|
1051
|
+
liveEngine,
|
|
1052
|
+
candleGapsCfg,
|
|
1053
|
+
thresholdInRange,
|
|
1054
|
+
thresholdIsSeries,
|
|
1055
|
+
thresholdSeriesSV,
|
|
1056
|
+
thresholdCfg,
|
|
1057
|
+
}: {
|
|
1058
|
+
isCandle: boolean;
|
|
1059
|
+
data: LiveChartProps["data"];
|
|
1060
|
+
lineEngineData: LiveChartProps["data"];
|
|
1061
|
+
candles: LiveChartProps["candles"];
|
|
1062
|
+
candlesEngine: SharedValue<CandlePoint[]>;
|
|
1063
|
+
liveCandle: LiveChartProps["liveCandle"];
|
|
1064
|
+
liveEngine: SharedValue<CandlePoint | null>;
|
|
1065
|
+
candleGapsCfg: ResolvedCandleGapsConfig | null;
|
|
1066
|
+
thresholdInRange: boolean;
|
|
1067
|
+
thresholdIsSeries: boolean;
|
|
1068
|
+
thresholdSeriesSV: SharedValue<LiveChartPoint[]> | null;
|
|
1069
|
+
thresholdCfg: ResolvedThresholdConfig | null;
|
|
1070
|
+
}) {
|
|
1071
|
+
return {
|
|
1072
|
+
data: isCandle ? data : lineEngineData,
|
|
1073
|
+
thresholdRangePoints:
|
|
1074
|
+
thresholdInRange && thresholdIsSeries
|
|
1075
|
+
? (thresholdSeriesSV ?? (thresholdCfg?.value as LiveChartPoint[]))
|
|
1076
|
+
: undefined,
|
|
1077
|
+
thresholdRangeExtendToNow: thresholdCfg?.extendToNow ?? true,
|
|
1078
|
+
candles: isCandle ? candlesEngine : candles,
|
|
1079
|
+
liveCandle: isCandle ? liveEngine : liveCandle,
|
|
1080
|
+
candleGaps: candleGapsCfg?.gaps,
|
|
1081
|
+
candleGapBridgeNoTrades: Boolean(candleGapsCfg?.styles["no-trades"].bridge),
|
|
1082
|
+
candleGapBridgeUnavailable: Boolean(
|
|
1083
|
+
candleGapsCfg?.styles.unavailable.bridge,
|
|
1084
|
+
),
|
|
1085
|
+
candleGapBridgeUnknown: Boolean(candleGapsCfg?.styles.unknown.bridge),
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function resolveCrosshairControllerSettings({
|
|
1090
|
+
scrubCfg,
|
|
1091
|
+
scrubActionCfg,
|
|
1092
|
+
markersActive,
|
|
1093
|
+
refPressActive,
|
|
1094
|
+
refDragEnabled,
|
|
1095
|
+
deferTapHit,
|
|
1096
|
+
timeScrollEnabled,
|
|
1097
|
+
scrollGestureMode,
|
|
1098
|
+
effectivePadding,
|
|
1099
|
+
}: {
|
|
1100
|
+
scrubCfg: ReturnType<typeof resolveScrub>;
|
|
1101
|
+
scrubActionCfg: ReturnType<typeof resolveScrubAction>;
|
|
1102
|
+
markersActive: boolean;
|
|
1103
|
+
refPressActive: boolean;
|
|
1104
|
+
refDragEnabled: boolean;
|
|
1105
|
+
deferTapHit: (x: number, y: number) => boolean;
|
|
1106
|
+
timeScrollEnabled: boolean;
|
|
1107
|
+
scrollGestureMode: "holdToScrub" | "axisDrag";
|
|
1108
|
+
effectivePadding: ChartPadding;
|
|
1109
|
+
}) {
|
|
1110
|
+
return {
|
|
1111
|
+
enabled: scrubCfg !== null || scrubActionCfg !== null,
|
|
1112
|
+
deferTapHit:
|
|
1113
|
+
markersActive || refPressActive || refDragEnabled
|
|
1114
|
+
? deferTapHit
|
|
1115
|
+
: undefined,
|
|
1116
|
+
tooltipPlacement: scrubCfg?.tooltipPlacement ?? "side",
|
|
1117
|
+
tooltipShowValue: scrubCfg?.tooltipShowValue ?? true,
|
|
1118
|
+
tooltipShowTime: scrubCfg?.tooltipShowTime ?? true,
|
|
1119
|
+
tooltipMargin: scrubCfg?.tooltipMargin ?? 8,
|
|
1120
|
+
scrubBottomExclude:
|
|
1121
|
+
timeScrollEnabled && scrollGestureMode === "axisDrag"
|
|
1122
|
+
? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
|
|
1123
|
+
: 0,
|
|
1124
|
+
clampToPlot: scrubCfg?.clampToPlot ?? false,
|
|
1125
|
+
snapToCandles: scrubCfg?.snapToCandles ?? false,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function resolveAreaDotColorVec(
|
|
1130
|
+
areaDotsCfg: ReturnType<typeof resolveAreaDots>,
|
|
1131
|
+
lineProp: LiveChartProps["line"],
|
|
1132
|
+
palette: LiveChartPalette,
|
|
1133
|
+
) {
|
|
1134
|
+
const rgb = parseColorRgb(lineProp?.color ?? palette.line);
|
|
1135
|
+
const [r, g, b, a] = parseColorRgba(
|
|
1136
|
+
areaDotsCfg?.color ?? `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.22)`,
|
|
1137
|
+
);
|
|
1138
|
+
return [r / 255, g / 255, b / 255, a * (areaDotsCfg?.opacity ?? 1)];
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function useLiveIndicatorOpacities({
|
|
1142
|
+
reveal,
|
|
1143
|
+
seriesIndicatorOpacity,
|
|
1144
|
+
seriesOpacity,
|
|
1145
|
+
scrubActive,
|
|
1146
|
+
selectionDotDuringScrub,
|
|
1147
|
+
hideLiveOnScrollBack,
|
|
1148
|
+
dotTracksParked,
|
|
1149
|
+
viewEnd,
|
|
1150
|
+
fadeOverlaysOnScrub,
|
|
1151
|
+
}: {
|
|
1152
|
+
reveal: ReturnType<typeof useChartReveal>;
|
|
1153
|
+
seriesIndicatorOpacity: SharedValue<number>;
|
|
1154
|
+
seriesOpacity: SharedValue<number>;
|
|
1155
|
+
scrubActive: SharedValue<boolean>;
|
|
1156
|
+
selectionDotDuringScrub: boolean;
|
|
1157
|
+
hideLiveOnScrollBack: boolean;
|
|
1158
|
+
dotTracksParked: boolean;
|
|
1159
|
+
viewEnd: SharedValue<number | null>;
|
|
1160
|
+
fadeOverlaysOnScrub: boolean;
|
|
1161
|
+
}) {
|
|
1162
|
+
const liveDotOpacity = useDerivedValue(
|
|
1163
|
+
() =>
|
|
1164
|
+
reveal.dotOpacity.value *
|
|
1165
|
+
(selectionDotDuringScrub && scrubActive.value ? 0 : 1) *
|
|
1166
|
+
liveIndicatorScrollOpacity(
|
|
1167
|
+
hideLiveOnScrollBack && !dotTracksParked,
|
|
1168
|
+
viewEnd.value,
|
|
1169
|
+
) *
|
|
1170
|
+
seriesIndicatorOpacity.value,
|
|
1171
|
+
);
|
|
1172
|
+
const valueLineOpacity = useDerivedValue(
|
|
1173
|
+
() =>
|
|
1174
|
+
reveal.lineOpacity.value *
|
|
1175
|
+
liveIndicatorScrollOpacity(hideLiveOnScrollBack, viewEnd.value) *
|
|
1176
|
+
seriesOpacity.value,
|
|
1177
|
+
);
|
|
1178
|
+
const liveBadgeOpacity = useDerivedValue(
|
|
1179
|
+
() =>
|
|
1180
|
+
reveal.badgeOpacity.value *
|
|
1181
|
+
liveIndicatorScrollOpacity(hideLiveOnScrollBack, viewEnd.value) *
|
|
1182
|
+
seriesOpacity.value,
|
|
1183
|
+
);
|
|
1184
|
+
const overlayScrubFade = useDerivedValue(() =>
|
|
1185
|
+
fadeOverlaysOnScrub
|
|
1186
|
+
? withTiming(scrubActive.get() ? 0 : 1, {
|
|
1187
|
+
duration: SCRUB_OVERLAY_FADE_MS,
|
|
1188
|
+
})
|
|
1189
|
+
: 1,
|
|
1190
|
+
);
|
|
1191
|
+
const markerGroupOpacity = useDerivedValue(
|
|
1192
|
+
() => reveal.dotOpacity.get() * overlayScrubFade.get(),
|
|
1193
|
+
);
|
|
1194
|
+
return {
|
|
1195
|
+
liveDotOpacity,
|
|
1196
|
+
valueLineOpacity,
|
|
1197
|
+
liveBadgeOpacity,
|
|
1198
|
+
overlayScrubFade,
|
|
1199
|
+
markerGroupOpacity,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function useReferenceLineGrouping({
|
|
1204
|
+
radius,
|
|
1205
|
+
engine,
|
|
1206
|
+
padding,
|
|
1207
|
+
lines,
|
|
1208
|
+
custom,
|
|
1209
|
+
offAxisCustom,
|
|
1210
|
+
dragValues,
|
|
1211
|
+
}: {
|
|
1212
|
+
radius: number | null;
|
|
1213
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
1214
|
+
padding: ChartPadding;
|
|
1215
|
+
lines: ReferenceLine[];
|
|
1216
|
+
custom: boolean[];
|
|
1217
|
+
offAxisCustom: boolean[];
|
|
1218
|
+
dragValues: SharedValue<number[]>;
|
|
1219
|
+
}) {
|
|
1220
|
+
const result = useDerivedValue<ReferenceGrouping>(() => {
|
|
1221
|
+
if (radius == null) return EMPTY_GROUPING;
|
|
1222
|
+
const canvasHeight = engine.canvasHeight.get();
|
|
1223
|
+
const displayMin = engine.displayMin.get();
|
|
1224
|
+
const displayMax = engine.displayMax.get();
|
|
1225
|
+
const top = padding.top;
|
|
1226
|
+
const bottom = canvasHeight - padding.bottom;
|
|
1227
|
+
const yPositions: number[] = [];
|
|
1228
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1229
|
+
const line = lines[index];
|
|
1230
|
+
if (
|
|
1231
|
+
referenceLineForm(line) !== "line" ||
|
|
1232
|
+
line.value === undefined ||
|
|
1233
|
+
custom[index] ||
|
|
1234
|
+
offAxisCustom[index]
|
|
1235
|
+
) {
|
|
1236
|
+
yPositions.push(-1);
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
const value = dragValues.get()[index] ?? line.value;
|
|
1240
|
+
const y = computeScrubDotY(
|
|
1241
|
+
value,
|
|
1242
|
+
displayMin,
|
|
1243
|
+
displayMax,
|
|
1244
|
+
canvasHeight,
|
|
1245
|
+
top,
|
|
1246
|
+
padding.bottom,
|
|
1247
|
+
);
|
|
1248
|
+
yPositions.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
|
|
1249
|
+
}
|
|
1250
|
+
return groupReferenceLines(yPositions, radius);
|
|
1251
|
+
});
|
|
1252
|
+
const hidden = useDerivedValue<boolean[]>(() => result.get().hidden);
|
|
1253
|
+
return { refGroupResult: result, groupHidden: hidden };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function useLiveChartLayoutResources({
|
|
1257
|
+
value,
|
|
1258
|
+
fontProp,
|
|
1259
|
+
palette,
|
|
1260
|
+
badgeHasFontOverride,
|
|
1261
|
+
badgeFontConfig,
|
|
1262
|
+
refGroupBadgeHasFont,
|
|
1263
|
+
refGroupBadgeFontConfig,
|
|
1264
|
+
yAxisFloat,
|
|
1265
|
+
timeScrollEnabled,
|
|
1266
|
+
lineProp,
|
|
1267
|
+
insets,
|
|
1268
|
+
yAxisCfg,
|
|
1269
|
+
badgeCfg,
|
|
1270
|
+
metricsCfg,
|
|
1271
|
+
badgeUsesRightGutter,
|
|
1272
|
+
xAxisCfg,
|
|
1273
|
+
formatValue,
|
|
1274
|
+
pulseConfig,
|
|
1275
|
+
dotCfg,
|
|
1276
|
+
volumeBandHeight,
|
|
1277
|
+
}: {
|
|
1278
|
+
value: LiveChartProps["value"];
|
|
1279
|
+
fontProp: LiveChartProps["font"];
|
|
1280
|
+
palette: LiveChartPalette;
|
|
1281
|
+
badgeHasFontOverride: boolean;
|
|
1282
|
+
badgeFontConfig: LiveChartProps["font"];
|
|
1283
|
+
refGroupBadgeHasFont: boolean;
|
|
1284
|
+
refGroupBadgeFontConfig: LiveChartProps["font"];
|
|
1285
|
+
yAxisFloat: boolean;
|
|
1286
|
+
timeScrollEnabled: boolean;
|
|
1287
|
+
lineProp: LiveChartProps["line"];
|
|
1288
|
+
insets: LiveChartProps["insets"];
|
|
1289
|
+
yAxisCfg: ReturnType<typeof resolveYAxis>;
|
|
1290
|
+
badgeCfg: ReturnType<typeof resolveBadge>;
|
|
1291
|
+
metricsCfg: ReturnType<typeof resolveMetrics>;
|
|
1292
|
+
badgeUsesRightGutter: boolean;
|
|
1293
|
+
xAxisCfg: ReturnType<typeof resolveXAxis>;
|
|
1294
|
+
formatValue: (value: number) => string;
|
|
1295
|
+
pulseConfig: { maxRadius: number; strokeWidth: number } | null;
|
|
1296
|
+
dotCfg: ReturnType<typeof resolveDot>;
|
|
1297
|
+
volumeBandHeight: number;
|
|
1298
|
+
}) {
|
|
1299
|
+
const skiaFont = useChartSkiaFont(
|
|
1300
|
+
fontProp,
|
|
1301
|
+
MONO_FONT_FAMILY,
|
|
1302
|
+
palette.labelFontSize,
|
|
1303
|
+
);
|
|
1304
|
+
const valueFont = useChartSkiaFont(
|
|
1305
|
+
fontProp,
|
|
1306
|
+
MONO_FONT_FAMILY,
|
|
1307
|
+
palette.valueFontSize * 2,
|
|
1308
|
+
);
|
|
1309
|
+
const badgeFontOverride = useChartSkiaFont(
|
|
1310
|
+
badgeFontConfig,
|
|
1311
|
+
MONO_FONT_FAMILY,
|
|
1312
|
+
palette.labelFontSize,
|
|
1313
|
+
);
|
|
1314
|
+
const badgeFont = badgeHasFontOverride ? badgeFontOverride : skiaFont;
|
|
1315
|
+
const refGroupBadgeFontOverride = useChartSkiaFont(
|
|
1316
|
+
refGroupBadgeFontConfig,
|
|
1317
|
+
MONO_FONT_FAMILY,
|
|
1318
|
+
palette.labelFontSize,
|
|
1319
|
+
);
|
|
1320
|
+
const refGroupBadgeFont = refGroupBadgeHasFont
|
|
1321
|
+
? refGroupBadgeFontOverride
|
|
1322
|
+
: skiaFont;
|
|
1323
|
+
const valueLayoutSample = useInitialSharedValueSample(value);
|
|
1324
|
+
const [scrolledBack, setScrolledBack] = useState(false);
|
|
1325
|
+
const effectiveYAxisFloat =
|
|
662
1326
|
yAxisFloat && (!timeScrollEnabled || scrolledBack);
|
|
663
|
-
const
|
|
1327
|
+
const layout = resolveChartLayout({
|
|
664
1328
|
palette,
|
|
665
1329
|
lineWidthOverride: lineProp?.width,
|
|
666
1330
|
insetsOverride: insets,
|
|
@@ -669,18 +1333,332 @@ function useLiveChartController({
|
|
|
669
1333
|
badge: badgeCfg !== null,
|
|
670
1334
|
badgeMetrics: metricsCfg.badge,
|
|
671
1335
|
badgeUsesRightGutter,
|
|
672
|
-
badgeShowTail: badgeCfg?.tail ?? true,
|
|
673
|
-
xAxis: xAxisCfg !== null,
|
|
674
|
-
font: skiaFont,
|
|
1336
|
+
badgeShowTail: badgeCfg?.tail ?? true,
|
|
1337
|
+
xAxis: xAxisCfg !== null,
|
|
1338
|
+
font: skiaFont,
|
|
1339
|
+
formatValue,
|
|
1340
|
+
currentValue: valueLayoutSample,
|
|
1341
|
+
pulse: pulseConfig,
|
|
1342
|
+
dotGlow: dotCfg.glow,
|
|
1343
|
+
volumeBandHeight,
|
|
1344
|
+
});
|
|
1345
|
+
return {
|
|
1346
|
+
skiaFont,
|
|
1347
|
+
valueFont,
|
|
1348
|
+
badgeFont,
|
|
1349
|
+
refGroupBadgeFont,
|
|
1350
|
+
effectiveYAxisFloat,
|
|
1351
|
+
setScrolledBack,
|
|
1352
|
+
...layout,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function useInitialSharedValueSample(value: SharedValue<number>) {
|
|
1357
|
+
const [sample, setSample] = useState<number | undefined>(undefined);
|
|
1358
|
+
const captured = useSharedValue(false);
|
|
1359
|
+
useAnimatedReaction(
|
|
1360
|
+
() => (captured.get() ? null : value.get()),
|
|
1361
|
+
(current) => {
|
|
1362
|
+
if (current === null) return;
|
|
1363
|
+
captured.set(true);
|
|
1364
|
+
scheduleOnRN(setSample, current);
|
|
1365
|
+
},
|
|
1366
|
+
[captured, value],
|
|
1367
|
+
);
|
|
1368
|
+
return sample;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function resolveLiveChartModelDefaults({
|
|
1372
|
+
isCandle,
|
|
1373
|
+
candleWidth,
|
|
1374
|
+
loadingCfg,
|
|
1375
|
+
volumeCfg,
|
|
1376
|
+
palette,
|
|
1377
|
+
lineProp,
|
|
1378
|
+
}: {
|
|
1379
|
+
isCandle: boolean;
|
|
1380
|
+
candleWidth: number;
|
|
1381
|
+
loadingCfg: ReturnType<typeof resolveLoading>;
|
|
1382
|
+
volumeCfg: ReturnType<typeof resolveVolume>;
|
|
1383
|
+
palette: LiveChartPalette;
|
|
1384
|
+
lineProp: LiveChartProps["line"];
|
|
1385
|
+
}) {
|
|
1386
|
+
return {
|
|
1387
|
+
extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
|
|
1388
|
+
loadingLineColor: loadingCfg?.color,
|
|
1389
|
+
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
1390
|
+
loadingAmplitude: loadingCfg?.amplitude,
|
|
1391
|
+
loadingSpeed: loadingCfg?.speed,
|
|
1392
|
+
loadingAxisLabels: loadingCfg?.axisLabels ?? true,
|
|
1393
|
+
volumeOpacity: volumeCfg?.opacity ?? 1,
|
|
1394
|
+
volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
|
|
1395
|
+
volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
|
|
1396
|
+
selectionColor: lineProp?.color ?? palette.line,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Resolves props → configs → theme/layout → engine → per-frame derived values and
|
|
1402
|
+
* overlay hooks, returning a single render model. All the chart's wiring lives
|
|
1403
|
+
* here so the rendered pieces (`ChartStack`, `ChartScrubLayer`, `LiveChart`) stay
|
|
1404
|
+
* small and presentational.
|
|
1405
|
+
*/
|
|
1406
|
+
|
|
1407
|
+
function useLiveChartController({
|
|
1408
|
+
// ── Data ────────────────────────────────────────────────────────────────
|
|
1409
|
+
data,
|
|
1410
|
+
value,
|
|
1411
|
+
|
|
1412
|
+
// ── Appearance ──────────────────────────────────────────────────────────
|
|
1413
|
+
theme = "dark",
|
|
1414
|
+
accentColor = DEFAULT_ACCENT_COLOR,
|
|
1415
|
+
gradient = true,
|
|
1416
|
+
areaDots,
|
|
1417
|
+
line: lineProp,
|
|
1418
|
+
font: fontProp,
|
|
1419
|
+
insets,
|
|
1420
|
+
style,
|
|
1421
|
+
seriesOpacity,
|
|
1422
|
+
canvasMode = "transparent",
|
|
1423
|
+
|
|
1424
|
+
// ── Candlestick ─────────────────────────────────────────────────────────
|
|
1425
|
+
mode = "line",
|
|
1426
|
+
candles,
|
|
1427
|
+
candleWidth = 60,
|
|
1428
|
+
liveCandle,
|
|
1429
|
+
candleGaps,
|
|
1430
|
+
lineGaps,
|
|
1431
|
+
volume,
|
|
1432
|
+
|
|
1433
|
+
// ── Behaviour ───────────────────────────────────────────────────────────
|
|
1434
|
+
timeWindow = 30,
|
|
1435
|
+
paused = false,
|
|
1436
|
+
loading = false,
|
|
1437
|
+
transitions,
|
|
1438
|
+
// `static` is a reserved word — alias it so the destructure parses.
|
|
1439
|
+
static: isStatic = false,
|
|
1440
|
+
snapKey,
|
|
1441
|
+
smoothing = 0.08,
|
|
1442
|
+
exaggerate = false,
|
|
1443
|
+
nonNegative = false,
|
|
1444
|
+
maxValue,
|
|
1445
|
+
yRangeScale,
|
|
1446
|
+
windowBuffer = 0,
|
|
1447
|
+
nowOverride,
|
|
1448
|
+
timeScroll = false,
|
|
1449
|
+
returnToLive,
|
|
1450
|
+
zoom = false,
|
|
1451
|
+
accessibilityLabel,
|
|
1452
|
+
accessibilityRole = "image",
|
|
1453
|
+
emptyText = "No data",
|
|
1454
|
+
formatValue = defaultFormatValue,
|
|
1455
|
+
formatTime = defaultFormatTime,
|
|
1456
|
+
|
|
1457
|
+
// ── Overlays ────────────────────────────────────────────────────────────
|
|
1458
|
+
yAxis = true,
|
|
1459
|
+
xAxis = true,
|
|
1460
|
+
axisAutoHide = false,
|
|
1461
|
+
topLabel,
|
|
1462
|
+
bottomLabel,
|
|
1463
|
+
badge = true,
|
|
1464
|
+
momentum = true,
|
|
1465
|
+
pulse = true,
|
|
1466
|
+
dot,
|
|
1467
|
+
valueLine = true,
|
|
1468
|
+
showValue = false,
|
|
1469
|
+
valueMomentumColor = false,
|
|
1470
|
+
referenceLines,
|
|
1471
|
+
segments,
|
|
1472
|
+
threshold,
|
|
1473
|
+
gridStyle,
|
|
1474
|
+
palette: paletteOverride,
|
|
1475
|
+
metrics,
|
|
1476
|
+
scrub = true,
|
|
1477
|
+
scrubAction,
|
|
1478
|
+
selectionDot,
|
|
1479
|
+
tradeStream,
|
|
1480
|
+
degen,
|
|
1481
|
+
markers,
|
|
1482
|
+
onMarkerPress,
|
|
1483
|
+
markerHitRadius = 16,
|
|
1484
|
+
markerCluster,
|
|
1485
|
+
renderMarker,
|
|
1486
|
+
renderTooltip,
|
|
1487
|
+
renderOverlay,
|
|
1488
|
+
renderThresholdBadge,
|
|
1489
|
+
renderReferenceLine,
|
|
1490
|
+
renderOffAxisReferenceLine,
|
|
1491
|
+
referenceLineGrouping,
|
|
1492
|
+
leftEdgeFade = true,
|
|
1493
|
+
|
|
1494
|
+
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
1495
|
+
onScrub,
|
|
1496
|
+
onScrubAction,
|
|
1497
|
+
onReferenceLinePress,
|
|
1498
|
+
onGestureStart,
|
|
1499
|
+
onGestureEnd,
|
|
1500
|
+
onVisibleRangeChange,
|
|
1501
|
+
onReachStart,
|
|
1502
|
+
onDegenShake,
|
|
1503
|
+
}: LiveChartProps) {
|
|
1504
|
+
const fullSeriesOpacity = useSharedValue(1);
|
|
1505
|
+
const resolvedSeriesOpacity = seriesOpacity ?? fullSeriesOpacity;
|
|
1506
|
+
const emptyMarkers = useSharedValue<Marker[]>([]);
|
|
1507
|
+
const markersSV = markers ?? emptyMarkers;
|
|
1508
|
+
// Stand-in threshold value so `useThreshold` can be called unconditionally
|
|
1509
|
+
// (hooks can't be); the geometry is ignored when no threshold is configured.
|
|
1510
|
+
const emptyThresholdValue = useSharedValue(0);
|
|
1511
|
+
const {
|
|
1512
|
+
isCandle,
|
|
1513
|
+
yAxisCfg,
|
|
1514
|
+
xAxisCfg,
|
|
1515
|
+
topLabelCfg,
|
|
1516
|
+
bottomLabelCfg,
|
|
1517
|
+
badgeCfg,
|
|
1518
|
+
scrubCfg,
|
|
1519
|
+
scrubActionCfg,
|
|
1520
|
+
volumeCfg,
|
|
1521
|
+
chartGapsCfg,
|
|
1522
|
+
candleGapsCfg,
|
|
1523
|
+
lineGapsCfg,
|
|
1524
|
+
volumeBandHeight,
|
|
1525
|
+
gradientCfg,
|
|
1526
|
+
areaDotsCfg,
|
|
1527
|
+
thresholdCfg,
|
|
1528
|
+
thresholdSeriesSV,
|
|
1529
|
+
thresholdIsSeries,
|
|
1530
|
+
valueLineCfg,
|
|
1531
|
+
pulseCfg,
|
|
1532
|
+
dotCfg,
|
|
1533
|
+
dotTracksParked,
|
|
1534
|
+
selectionDotCfg,
|
|
1535
|
+
dotOuterRadius,
|
|
1536
|
+
gridStyleCfg,
|
|
1537
|
+
degenCfg,
|
|
1538
|
+
tradeStreamResolved,
|
|
1539
|
+
metricsCfg,
|
|
1540
|
+
} = resolveLiveChartFeatureConfig({
|
|
1541
|
+
mode,
|
|
1542
|
+
yAxis,
|
|
1543
|
+
xAxis,
|
|
1544
|
+
topLabel,
|
|
1545
|
+
bottomLabel,
|
|
1546
|
+
badge,
|
|
1547
|
+
scrub,
|
|
1548
|
+
scrubAction,
|
|
1549
|
+
volume,
|
|
1550
|
+
candleGaps,
|
|
1551
|
+
lineGaps,
|
|
1552
|
+
gradient,
|
|
1553
|
+
areaDots,
|
|
1554
|
+
threshold,
|
|
1555
|
+
valueLine,
|
|
1556
|
+
isStatic,
|
|
1557
|
+
pulse,
|
|
1558
|
+
dot,
|
|
1559
|
+
selectionDot,
|
|
1560
|
+
gridStyle,
|
|
1561
|
+
degen,
|
|
1562
|
+
tradeStream,
|
|
1563
|
+
metrics,
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
const {
|
|
1567
|
+
chartGapBands,
|
|
1568
|
+
allRefLines,
|
|
1569
|
+
refValues,
|
|
1570
|
+
refLineCustom,
|
|
1571
|
+
refLineOffAxisCustom,
|
|
1572
|
+
refLineKeys,
|
|
1573
|
+
draggableRefIdx,
|
|
1574
|
+
thresholdInRange,
|
|
1575
|
+
thresholdRangeValueSV,
|
|
1576
|
+
refGroupingCfg,
|
|
1577
|
+
refGroupingRadius,
|
|
1578
|
+
refGroupBadge,
|
|
1579
|
+
refGroupFormat,
|
|
1580
|
+
badgeUsesRightGutter,
|
|
1581
|
+
} = resolveLiveChartReferenceConfig({
|
|
1582
|
+
chartGapsCfg,
|
|
1583
|
+
referenceLines,
|
|
1584
|
+
renderReferenceLine,
|
|
1585
|
+
renderOffAxisReferenceLine,
|
|
1586
|
+
thresholdCfg,
|
|
1587
|
+
referenceLineGrouping,
|
|
1588
|
+
badgeCfg,
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
const { dragValues, dragActive, refLineCustomTagWidths, liveRefValues } =
|
|
1592
|
+
useLiveReferenceState(allRefLines, draggableRefIdx, thresholdRangeValueSV);
|
|
1593
|
+
|
|
1594
|
+
const {
|
|
1595
|
+
palette,
|
|
1596
|
+
resolvedSegments,
|
|
1597
|
+
hasRecolorSegments,
|
|
1598
|
+
leftEdgeFadeCfg,
|
|
1599
|
+
badgeHasFontOverride,
|
|
1600
|
+
badgeFontConfig,
|
|
1601
|
+
refGroupBadgeHasFont,
|
|
1602
|
+
refGroupBadgeFontConfig,
|
|
1603
|
+
pulseConfig,
|
|
1604
|
+
timeScrollEnabled,
|
|
1605
|
+
returnToLiveMs,
|
|
1606
|
+
timeScrollOverscroll,
|
|
1607
|
+
timeScrollFling,
|
|
1608
|
+
zoomCfg,
|
|
1609
|
+
zoomEnabled,
|
|
1610
|
+
yAxisFloat,
|
|
1611
|
+
} = resolveLiveChartPresentationConfig({
|
|
1612
|
+
accentColor,
|
|
1613
|
+
theme,
|
|
1614
|
+
paletteOverride,
|
|
1615
|
+
segments,
|
|
1616
|
+
leftEdgeFade,
|
|
1617
|
+
fontProp,
|
|
1618
|
+
badgeCfg,
|
|
1619
|
+
refGroupBadge,
|
|
1620
|
+
pulseCfg,
|
|
1621
|
+
timeScroll,
|
|
1622
|
+
isStatic,
|
|
1623
|
+
returnToLive,
|
|
1624
|
+
zoom,
|
|
1625
|
+
yAxisCfg,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
const {
|
|
1629
|
+
skiaFont,
|
|
1630
|
+
valueFont,
|
|
1631
|
+
badgeFont,
|
|
1632
|
+
refGroupBadgeFont,
|
|
1633
|
+
effectiveYAxisFloat,
|
|
1634
|
+
setScrolledBack,
|
|
1635
|
+
strokeWidth,
|
|
1636
|
+
padding: effectivePadding,
|
|
1637
|
+
} = useLiveChartLayoutResources({
|
|
1638
|
+
value,
|
|
1639
|
+
fontProp,
|
|
1640
|
+
palette,
|
|
1641
|
+
badgeHasFontOverride,
|
|
1642
|
+
badgeFontConfig,
|
|
1643
|
+
refGroupBadgeHasFont,
|
|
1644
|
+
refGroupBadgeFontConfig,
|
|
1645
|
+
yAxisFloat,
|
|
1646
|
+
timeScrollEnabled,
|
|
1647
|
+
lineProp,
|
|
1648
|
+
insets,
|
|
1649
|
+
yAxisCfg,
|
|
1650
|
+
badgeCfg,
|
|
1651
|
+
metricsCfg,
|
|
1652
|
+
badgeUsesRightGutter,
|
|
1653
|
+
xAxisCfg,
|
|
675
1654
|
formatValue,
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
dotGlow: dotCfg.glow,
|
|
1655
|
+
pulseConfig,
|
|
1656
|
+
dotCfg,
|
|
679
1657
|
volumeBandHeight,
|
|
680
1658
|
});
|
|
681
1659
|
|
|
682
1660
|
// ── Reveal state ────────────────────────────────────────────
|
|
683
|
-
// ≥
|
|
1661
|
+
// ≥1 line point or ≥1 committed candle; morphT=1 only when !loading && hasData.
|
|
684
1662
|
const { hasData } = useLiveChartHasData({
|
|
685
1663
|
isCandle,
|
|
686
1664
|
data,
|
|
@@ -717,8 +1695,22 @@ function useLiveChartController({
|
|
|
717
1695
|
// ── Engine ─────────────────────────────────────────────────────────────
|
|
718
1696
|
// Line mode: tick + paths use `lineEngineData` (stash when reversing). Candle mode:
|
|
719
1697
|
// parent `data` stays tick/line-morph input; OHLC uses candlesEngine + liveEngine.
|
|
1698
|
+
const engineModeInputs = resolveLiveEngineModeInputs({
|
|
1699
|
+
isCandle,
|
|
1700
|
+
data,
|
|
1701
|
+
lineEngineData,
|
|
1702
|
+
candles,
|
|
1703
|
+
candlesEngine,
|
|
1704
|
+
liveCandle,
|
|
1705
|
+
liveEngine,
|
|
1706
|
+
candleGapsCfg,
|
|
1707
|
+
thresholdInRange,
|
|
1708
|
+
thresholdIsSeries,
|
|
1709
|
+
thresholdSeriesSV,
|
|
1710
|
+
thresholdCfg,
|
|
1711
|
+
});
|
|
720
1712
|
const engine = useLiveChartEngine({
|
|
721
|
-
|
|
1713
|
+
...engineModeInputs,
|
|
722
1714
|
value,
|
|
723
1715
|
timeWindow,
|
|
724
1716
|
paused,
|
|
@@ -732,21 +1724,12 @@ function useLiveChartController({
|
|
|
732
1724
|
exaggerate,
|
|
733
1725
|
referenceValues: refValues,
|
|
734
1726
|
liveReferenceValues: liveRefValues,
|
|
735
|
-
// Series threshold with `includeInRange`: the tick folds its window min/max
|
|
736
|
-
// into the Y-range fit (the constant form rides `liveReferenceValues`).
|
|
737
|
-
thresholdRangePoints:
|
|
738
|
-
thresholdInRange && thresholdIsSeries
|
|
739
|
-
? (thresholdSeriesSV ?? (thresholdCfg?.value as LiveChartPoint[]))
|
|
740
|
-
: undefined,
|
|
741
|
-
thresholdRangeExtendToNow: thresholdCfg?.extendToNow ?? true,
|
|
742
1727
|
nonNegative,
|
|
743
1728
|
maxValue,
|
|
744
1729
|
yRangeScale,
|
|
745
1730
|
windowBuffer,
|
|
746
1731
|
nowOverride,
|
|
747
1732
|
mode,
|
|
748
|
-
candles: isCandle ? candlesEngine : candles,
|
|
749
|
-
liveCandle: isCandle ? liveEngine : liveCandle,
|
|
750
1733
|
});
|
|
751
1734
|
|
|
752
1735
|
// Mirror the UI-thread scroll state to React so the floating y-axis can keep
|
|
@@ -776,158 +1759,48 @@ function useLiveChartController({
|
|
|
776
1759
|
// ── Per-frame derived values ───────────────────────────────────────────
|
|
777
1760
|
const { layoutWidth, layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
778
1761
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const dMax = engine.displayMax.get();
|
|
788
|
-
const top = effectivePadding.top;
|
|
789
|
-
const bottom = ch - effectivePadding.bottom;
|
|
790
|
-
const ys: number[] = [];
|
|
791
|
-
for (let i = 0; i < allRefLines.length; i++) {
|
|
792
|
-
const l = allRefLines[i];
|
|
793
|
-
// Skip bands and lines a custom `renderReferenceLine` owns — a custom tag
|
|
794
|
-
// draws itself and isn't suppressed by grouping, so folding it into a
|
|
795
|
-
// built-in count pill would double-count it (counted *and* still shown).
|
|
796
|
-
if (
|
|
797
|
-
referenceLineForm(l) !== "line" ||
|
|
798
|
-
l.value === undefined ||
|
|
799
|
-
refLineCustom[i] ||
|
|
800
|
-
refLineOffAxisCustom[i]
|
|
801
|
-
) {
|
|
802
|
-
ys.push(-1);
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
const v = dragValues.get()[i] ?? l.value;
|
|
806
|
-
const y = computeScrubDotY(
|
|
807
|
-
v,
|
|
808
|
-
dMin,
|
|
809
|
-
dMax,
|
|
810
|
-
ch,
|
|
811
|
-
top,
|
|
812
|
-
effectivePadding.bottom,
|
|
813
|
-
);
|
|
814
|
-
ys.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
|
|
815
|
-
}
|
|
816
|
-
return groupReferenceLines(ys, refGroupingRadius);
|
|
1762
|
+
const { refGroupResult, groupHidden } = useReferenceLineGrouping({
|
|
1763
|
+
radius: refGroupingRadius,
|
|
1764
|
+
engine,
|
|
1765
|
+
padding: effectivePadding,
|
|
1766
|
+
lines: allRefLines,
|
|
1767
|
+
custom: refLineCustom,
|
|
1768
|
+
offAxisCustom: refLineOffAxisCustom,
|
|
1769
|
+
dragValues,
|
|
817
1770
|
});
|
|
818
|
-
const groupHidden = useDerivedValue<boolean[]>(
|
|
819
|
-
() => refGroupResult.get().hidden,
|
|
820
|
-
);
|
|
821
1771
|
|
|
822
1772
|
// Threshold split geometry. Two forms, picked at render by `Array.isArray` (no
|
|
823
1773
|
// SharedValue read): a constant `SharedValue<number>` benchmark drives the
|
|
824
1774
|
// vertical hard-stop gradient (`thresholdGeom`); a time-varying `LiveChartPoint[]`
|
|
825
1775
|
// series drives the per-fragment split shader (`thresholdSeriesGeom`). Both hooks
|
|
826
1776
|
// run unconditionally — the unused one short-circuits cheaply on the UI thread.
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
: Array.isArray(thresholdCfg?.value) && thresholdCfg.value.length > 0;
|
|
844
|
-
const thresholdGeom = useThreshold(engine, effectivePadding, thresholdValue);
|
|
845
|
-
const thresholdSeriesGeom = useThresholdSeries(
|
|
1777
|
+
const {
|
|
1778
|
+
thresholdGeom,
|
|
1779
|
+
thresholdStrokeColors,
|
|
1780
|
+
thresholdFillColors,
|
|
1781
|
+
thresholdSeriesHasPoints,
|
|
1782
|
+
thresholdStrokeUniforms,
|
|
1783
|
+
thresholdFillUniforms,
|
|
1784
|
+
thresholdMarkerLineY,
|
|
1785
|
+
thresholdMarkerVisible,
|
|
1786
|
+
thresholdBadgeVisible,
|
|
1787
|
+
thresholdMarkerValue,
|
|
1788
|
+
thresholdCustomBadge,
|
|
1789
|
+
thresholdSeriesPts,
|
|
1790
|
+
thresholdFillLineY,
|
|
1791
|
+
thresholdFillSamples,
|
|
1792
|
+
} = useLiveChartThresholdModel({
|
|
846
1793
|
engine,
|
|
847
|
-
effectivePadding,
|
|
848
|
-
|
|
1794
|
+
padding: effectivePadding,
|
|
1795
|
+
thresholdCfg,
|
|
849
1796
|
thresholdSeriesSV,
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
// color strings + opacity (so a threshold added after mount with default
|
|
858
|
-
// colors still computes, and the per-frame uniforms worklet isn't rebuilt
|
|
859
|
-
// every render). `strokeRest` is the plain line color painted right of the
|
|
860
|
-
// `extendToNow: false` cutoff.
|
|
861
|
-
const thresholdSplitAbove = thresholdCfg
|
|
862
|
-
? (thresholdCfg.aboveColor ?? palette.candleUp)
|
|
863
|
-
: null;
|
|
864
|
-
const thresholdSplitBelow = thresholdCfg
|
|
865
|
-
? (thresholdCfg.belowColor ?? palette.candleDown)
|
|
866
|
-
: null;
|
|
867
|
-
const thresholdFillOpacity =
|
|
868
|
-
thresholdCfg?.fillOpacity ?? THRESHOLD_FILL_OPACITY_DEFAULT;
|
|
869
|
-
const thresholdLineColorStr = lineProp?.color ?? palette.line;
|
|
870
|
-
const thresholdVecs = useMemo(
|
|
871
|
-
() =>
|
|
872
|
-
thresholdSplitAbove !== null && thresholdSplitBelow !== null
|
|
873
|
-
? {
|
|
874
|
-
...thresholdSplitColorVecs(
|
|
875
|
-
thresholdSplitAbove,
|
|
876
|
-
thresholdSplitBelow,
|
|
877
|
-
thresholdFillOpacity,
|
|
878
|
-
),
|
|
879
|
-
strokeRest: (() => {
|
|
880
|
-
const [r, g, b, a] = parseColorRgba(thresholdLineColorStr);
|
|
881
|
-
return [r / 255, g / 255, b / 255, a];
|
|
882
|
-
})(),
|
|
883
|
-
}
|
|
884
|
-
: null,
|
|
885
|
-
[
|
|
886
|
-
thresholdSplitAbove,
|
|
887
|
-
thresholdSplitBelow,
|
|
888
|
-
thresholdFillOpacity,
|
|
889
|
-
thresholdLineColorStr,
|
|
890
|
-
],
|
|
891
|
-
);
|
|
892
|
-
const thresholdStrokeUniforms = useThresholdSplitUniforms(
|
|
893
|
-
thresholdSeriesGeom.samples,
|
|
894
|
-
engine,
|
|
895
|
-
effectivePadding,
|
|
896
|
-
thresholdVecs?.strokeAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
897
|
-
thresholdVecs?.strokeBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
898
|
-
thresholdVecs?.strokeRest ?? THRESHOLD_FALLBACK_COLOR,
|
|
899
|
-
thresholdSeriesGeom.clipRightX,
|
|
900
|
-
);
|
|
901
|
-
const thresholdFillUniforms = useThresholdSplitUniforms(
|
|
902
|
-
thresholdSeriesGeom.samples,
|
|
903
|
-
engine,
|
|
904
|
-
effectivePadding,
|
|
905
|
-
thresholdVecs?.fillAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
906
|
-
thresholdVecs?.fillBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
907
|
-
TRANSPARENT_VEC4,
|
|
908
|
-
thresholdSeriesGeom.clipRightX,
|
|
909
|
-
);
|
|
910
|
-
|
|
911
|
-
// Marker line + badge sources: the series anchors at the value-at-now (flat-
|
|
912
|
-
// extended past its last point); the constant case at the single benchmark Y.
|
|
913
|
-
// The badge gets its own visibility — it's pinned at the value-at-now Y, which
|
|
914
|
-
// can be off-plot while older polyline segments are still visible.
|
|
915
|
-
const thresholdMarkerLineY = thresholdIsSeries
|
|
916
|
-
? thresholdSeriesGeom.currentLineY
|
|
917
|
-
: thresholdGeom.lineY;
|
|
918
|
-
const thresholdMarkerVisible = thresholdIsSeries
|
|
919
|
-
? thresholdSeriesGeom.visible
|
|
920
|
-
: thresholdGeom.visible;
|
|
921
|
-
const thresholdBadgeVisible = thresholdIsSeries
|
|
922
|
-
? thresholdSeriesGeom.currentVisible
|
|
923
|
-
: thresholdGeom.visible;
|
|
924
|
-
const thresholdMarkerValue =
|
|
925
|
-
thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
|
|
926
|
-
? (thresholdCfg.value ?? thresholdSeriesGeom.currentValue)
|
|
927
|
-
: thresholdSeriesGeom.currentValue;
|
|
928
|
-
const thresholdSeriesPts = thresholdIsSeries
|
|
929
|
-
? thresholdSeriesGeom.screenPts
|
|
930
|
-
: undefined;
|
|
1797
|
+
thresholdIsSeries,
|
|
1798
|
+
emptyThresholdValue,
|
|
1799
|
+
palette,
|
|
1800
|
+
lineProp,
|
|
1801
|
+
renderThresholdBadge,
|
|
1802
|
+
formatValue,
|
|
1803
|
+
});
|
|
931
1804
|
|
|
932
1805
|
// Straight polyline instead of the monotone cubic when line.curve === "linear".
|
|
933
1806
|
// Shared by the path builders and the marker anchoring so glyphs sit on the
|
|
@@ -940,7 +1813,7 @@ function useLiveChartController({
|
|
|
940
1813
|
reveal.morphT,
|
|
941
1814
|
// Constant threshold band closes at a single Y (the series closes along the
|
|
942
1815
|
// polyline passed as the last arg below).
|
|
943
|
-
|
|
1816
|
+
thresholdFillLineY,
|
|
944
1817
|
lineIsLinear,
|
|
945
1818
|
// The plotted line independently selects the visible edge while historical;
|
|
946
1819
|
// badge/live-indicator options affect overlays only.
|
|
@@ -951,26 +1824,19 @@ function useLiveChartController({
|
|
|
951
1824
|
// (so the band matches the shader and doesn't bleed at step risers). An empty
|
|
952
1825
|
// series builds no band (its samples are all the far-below fallback, which
|
|
953
1826
|
// would tint the entire area under the line).
|
|
954
|
-
|
|
955
|
-
? thresholdSeriesGeom.samples
|
|
956
|
-
: undefined,
|
|
1827
|
+
thresholdFillSamples,
|
|
957
1828
|
lineProp?.simplify,
|
|
1829
|
+
lineGapsCfg?.gaps,
|
|
958
1830
|
);
|
|
959
1831
|
|
|
960
1832
|
// Area-dots fill shader color as a vec4 (channels 0..1), with the config
|
|
961
1833
|
// `opacity` folded into the alpha. Defaults to a faint tint of the line/accent
|
|
962
1834
|
// color (theme-aware) so out-of-the-box dots read as a subtle field.
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1835
|
+
const areaDotColorVec = resolveAreaDotColorVec(
|
|
1836
|
+
areaDotsCfg,
|
|
1837
|
+
lineProp,
|
|
1838
|
+
palette,
|
|
967
1839
|
);
|
|
968
|
-
const areaDotColorVec = [
|
|
969
|
-
adR / 255,
|
|
970
|
-
adG / 255,
|
|
971
|
-
adB / 255,
|
|
972
|
-
adA * (areaDotsCfg?.opacity ?? 1),
|
|
973
|
-
];
|
|
974
1840
|
|
|
975
1841
|
const { dotX, dotY } = useLiveDot(
|
|
976
1842
|
engine,
|
|
@@ -1001,19 +1867,31 @@ function useLiveChartController({
|
|
|
1001
1867
|
isCandle,
|
|
1002
1868
|
);
|
|
1003
1869
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1870
|
+
const {
|
|
1871
|
+
crosshairChartOpts,
|
|
1872
|
+
markersActive,
|
|
1873
|
+
markerClusterCfg,
|
|
1874
|
+
refPressActive,
|
|
1875
|
+
refDragEnabled,
|
|
1876
|
+
scrollGestureMode,
|
|
1877
|
+
scrubHoldMs,
|
|
1878
|
+
} = resolveLiveChartInteractionConfig({
|
|
1879
|
+
isCandle,
|
|
1880
|
+
mode,
|
|
1881
|
+
candlesEngine,
|
|
1882
|
+
liveEngine,
|
|
1883
|
+
candleWidth,
|
|
1884
|
+
candleGapsCfg,
|
|
1885
|
+
lineGapsCfg,
|
|
1886
|
+
markers,
|
|
1887
|
+
markerCluster,
|
|
1888
|
+
onReferenceLinePress,
|
|
1889
|
+
allRefLines,
|
|
1890
|
+
isStatic,
|
|
1891
|
+
timeScroll,
|
|
1892
|
+
timeScrollEnabled,
|
|
1893
|
+
scrubCfg,
|
|
1894
|
+
});
|
|
1017
1895
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
1018
1896
|
// self-projects, so we only need the gesture + hit-test here. Built BEFORE
|
|
1019
1897
|
// `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
|
|
@@ -1033,7 +1911,6 @@ function useLiveChartController({
|
|
|
1033
1911
|
|
|
1034
1912
|
// Pressable reference-line badges (working orders / alerts). Built before
|
|
1035
1913
|
// `useCrosshair` so the scrub-action tap can defer to a badge under the finger.
|
|
1036
|
-
const refPressActive = onReferenceLinePress != null && allRefLines.length > 0;
|
|
1037
1914
|
const { tapGesture: refLineTapGesture, hitTest: refLineHitTest } =
|
|
1038
1915
|
useReferenceLinePress(
|
|
1039
1916
|
engine,
|
|
@@ -1052,8 +1929,6 @@ function useLiveChartController({
|
|
|
1052
1929
|
// unconditionally for stable hook order (and before `useCrosshair` so the scrub
|
|
1053
1930
|
// can defer to a line under the finger); the gesture self-disables when no line
|
|
1054
1931
|
// opts in, and it's only composed into the root when `refDragEnabled`.
|
|
1055
|
-
const refDragEnabled =
|
|
1056
|
-
!isStatic && allRefLines.some((l) => l.draggable === true);
|
|
1057
1932
|
const { gesture: refDragGesture, hitTest: refDragHitTest } = useReferenceDrag(
|
|
1058
1933
|
engine,
|
|
1059
1934
|
effectivePadding,
|
|
@@ -1073,24 +1948,6 @@ function useLiveChartController({
|
|
|
1073
1948
|
return markerHitTest(x, y) || refLineHitTest(x, y) || refDragHitTest(x, y);
|
|
1074
1949
|
};
|
|
1075
1950
|
|
|
1076
|
-
// Time-scroll activation. `holdToScrub`: a quick one-finger drag scrolls while
|
|
1077
|
-
// scrub engages on press-and-hold — so the scrub gesture needs a long-press
|
|
1078
|
-
// delay (unless the caller set its own `panGestureDelay`). `timeScrollEnabled`
|
|
1079
|
-
// is computed earlier (it gates the float gutter).
|
|
1080
|
-
const scrollGestureMode =
|
|
1081
|
-
typeof timeScroll === "object"
|
|
1082
|
-
? (timeScroll.gesture ?? "holdToScrub")
|
|
1083
|
-
: "holdToScrub";
|
|
1084
|
-
// In holdToScrub the scrub MUST require a hold so a quick drag scrolls instead.
|
|
1085
|
-
// Precedence: explicit timeScroll.scrubHoldMs, then scrub.panGestureDelay, then
|
|
1086
|
-
// the default. `||` (not `??`) skips the resolved panGestureDelay's 0 default.
|
|
1087
|
-
const timeScrollHoldMs =
|
|
1088
|
-
typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
|
|
1089
|
-
const scrubHoldMs =
|
|
1090
|
-
timeScrollEnabled && scrollGestureMode === "holdToScrub"
|
|
1091
|
-
? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
|
|
1092
|
-
: (scrubCfg?.panGestureDelay ?? 0);
|
|
1093
|
-
|
|
1094
1951
|
// Cross-gesture arbitration for the one-finger touch. `Gesture.Race` below is
|
|
1095
1952
|
// NOT arbitration — RNGH's Race adds no relation between its children, so both
|
|
1096
1953
|
// pans recognize independently and each can activate while the other already
|
|
@@ -1099,6 +1956,18 @@ function useLiveChartController({
|
|
|
1099
1956
|
// (written by the crosshair, read by the scroll pan) is the mirror image.
|
|
1100
1957
|
const scrollActive = useSharedValue(false);
|
|
1101
1958
|
|
|
1959
|
+
const crosshairSettings = resolveCrosshairControllerSettings({
|
|
1960
|
+
scrubCfg,
|
|
1961
|
+
scrubActionCfg,
|
|
1962
|
+
markersActive,
|
|
1963
|
+
refPressActive,
|
|
1964
|
+
refDragEnabled,
|
|
1965
|
+
deferTapHit,
|
|
1966
|
+
timeScrollEnabled,
|
|
1967
|
+
scrollGestureMode,
|
|
1968
|
+
effectivePadding,
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1102
1971
|
const crosshair = useCrosshair(
|
|
1103
1972
|
engine,
|
|
1104
1973
|
effectivePadding,
|
|
@@ -1109,29 +1978,24 @@ function useLiveChartController({
|
|
|
1109
1978
|
// Scrub / scrub-action stay live even on static charts: the gesture is
|
|
1110
1979
|
// event-driven (no per-frame loop), so a settled chart costs nothing at rest
|
|
1111
1980
|
// yet becomes scrubbable on touch. `static` only kills the continuous loop.
|
|
1112
|
-
|
|
1981
|
+
crosshairSettings.enabled,
|
|
1113
1982
|
onScrub,
|
|
1114
|
-
|
|
1983
|
+
crosshairChartOpts,
|
|
1115
1984
|
scrubHoldMs,
|
|
1116
1985
|
onGestureStart,
|
|
1117
1986
|
onGestureEnd,
|
|
1118
1987
|
scrubActionCfg,
|
|
1119
1988
|
onScrubAction,
|
|
1120
1989
|
metricsCfg.badge,
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
// drag there never trips the scrub crosshair.
|
|
1128
|
-
timeScrollEnabled && scrollGestureMode === "axisDrag"
|
|
1129
|
-
? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
|
|
1130
|
-
: 0,
|
|
1990
|
+
crosshairSettings.deferTapHit,
|
|
1991
|
+
crosshairSettings.tooltipPlacement,
|
|
1992
|
+
crosshairSettings.tooltipShowValue,
|
|
1993
|
+
crosshairSettings.tooltipShowTime,
|
|
1994
|
+
crosshairSettings.tooltipMargin,
|
|
1995
|
+
crosshairSettings.scrubBottomExclude,
|
|
1131
1996
|
scrollActive,
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
scrubCfg?.snapToCandles ?? false,
|
|
1997
|
+
crosshairSettings.clampToPlot,
|
|
1998
|
+
crosshairSettings.snapToCandles,
|
|
1135
1999
|
);
|
|
1136
2000
|
|
|
1137
2001
|
// Capture only the shared value in the worklets below. Referencing
|
|
@@ -1185,77 +2049,12 @@ function useLiveChartController({
|
|
|
1185
2049
|
},
|
|
1186
2050
|
});
|
|
1187
2051
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
// underneath so a fade-in shows current labels.
|
|
1194
|
-
const axisAutoHideCfg =
|
|
1195
|
-
axisAutoHide === true ? {} : axisAutoHide === false ? null : axisAutoHide;
|
|
1196
|
-
const axisIdleOpacity = axisAutoHideCfg?.idleOpacity ?? 0;
|
|
1197
|
-
const axisFadeInMs = axisAutoHideCfg?.fadeInMs ?? 60;
|
|
1198
|
-
const axisFadeOutMs = axisAutoHideCfg?.fadeOutMs ?? 250;
|
|
1199
|
-
const axisHideAfterMs = axisAutoHideCfg?.hideAfterMs ?? 3000;
|
|
1200
|
-
const axisAutoHideOpacity = useSharedValue(
|
|
1201
|
-
axisAutoHideCfg ? axisIdleOpacity : 1,
|
|
1202
|
-
);
|
|
1203
|
-
const axisAutoHideEnabled = axisAutoHideCfg !== null;
|
|
1204
|
-
const lastAxisAutoHide = useRef({
|
|
1205
|
-
enabled: axisAutoHideEnabled,
|
|
1206
|
-
idleOpacity: axisIdleOpacity,
|
|
2052
|
+
const axisAutoHideOpacity = useAxisAutoHide({
|
|
2053
|
+
config: axisAutoHide,
|
|
2054
|
+
scrollActive,
|
|
2055
|
+
scrubActive: crosshairScrubActive,
|
|
2056
|
+
engine,
|
|
1207
2057
|
});
|
|
1208
|
-
// `useSharedValue` only reads its initial value on mount. Keep prop changes
|
|
1209
|
-
// in sync as well: turning auto-hide off must immediately restore the axes,
|
|
1210
|
-
// and turning it on starts from the configured idle opacity.
|
|
1211
|
-
useEffect(() => {
|
|
1212
|
-
if (
|
|
1213
|
-
lastAxisAutoHide.current.enabled === axisAutoHideEnabled &&
|
|
1214
|
-
lastAxisAutoHide.current.idleOpacity === axisIdleOpacity
|
|
1215
|
-
) {
|
|
1216
|
-
return;
|
|
1217
|
-
}
|
|
1218
|
-
lastAxisAutoHide.current = {
|
|
1219
|
-
enabled: axisAutoHideEnabled,
|
|
1220
|
-
idleOpacity: axisIdleOpacity,
|
|
1221
|
-
};
|
|
1222
|
-
cancelAnimation(axisAutoHideOpacity);
|
|
1223
|
-
axisAutoHideOpacity.value = axisAutoHideEnabled ? axisIdleOpacity : 1;
|
|
1224
|
-
}, [axisAutoHideEnabled, axisAutoHideOpacity, axisIdleOpacity]);
|
|
1225
|
-
useAnimatedReaction(
|
|
1226
|
-
() => ({
|
|
1227
|
-
gesture: scrollActive.value || crosshairScrubActive.value,
|
|
1228
|
-
viewEnd: engine.viewEnd.value,
|
|
1229
|
-
viewWindow: engine.viewWindow.value,
|
|
1230
|
-
}),
|
|
1231
|
-
(curr, prev) => {
|
|
1232
|
-
if (!axisAutoHideEnabled || prev === null) return;
|
|
1233
|
-
const moved =
|
|
1234
|
-
curr.gesture !== prev.gesture ||
|
|
1235
|
-
curr.viewEnd !== prev.viewEnd ||
|
|
1236
|
-
curr.viewWindow !== prev.viewWindow;
|
|
1237
|
-
if (!moved) return;
|
|
1238
|
-
cancelAnimation(axisAutoHideOpacity);
|
|
1239
|
-
axisAutoHideOpacity.value = curr.gesture
|
|
1240
|
-
? withTiming(1, { duration: axisFadeInMs })
|
|
1241
|
-
: // Movement without a held touch (gesture end, fling, pinch): show,
|
|
1242
|
-
// then fade back out once the chart goes untouched for a while.
|
|
1243
|
-
withSequence(
|
|
1244
|
-
withTiming(1, { duration: axisFadeInMs }),
|
|
1245
|
-
withDelay(
|
|
1246
|
-
axisHideAfterMs,
|
|
1247
|
-
withTiming(axisIdleOpacity, { duration: axisFadeOutMs }),
|
|
1248
|
-
),
|
|
1249
|
-
);
|
|
1250
|
-
},
|
|
1251
|
-
[
|
|
1252
|
-
axisAutoHideEnabled,
|
|
1253
|
-
axisIdleOpacity,
|
|
1254
|
-
axisFadeInMs,
|
|
1255
|
-
axisFadeOutMs,
|
|
1256
|
-
axisHideAfterMs,
|
|
1257
|
-
],
|
|
1258
|
-
);
|
|
1259
2058
|
|
|
1260
2059
|
// Paging callbacks: report the visible range / proximity to the oldest data so
|
|
1261
2060
|
// a host can lazily load history. Inert unless a callback is supplied.
|
|
@@ -1270,59 +2069,21 @@ function useLiveChartController({
|
|
|
1270
2069
|
// ahead of the pan via Exclusive, so a tap is tried first and only becomes a
|
|
1271
2070
|
// drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
|
|
1272
2071
|
// (not `Race`) prevents a jittery tap from being swallowed by the pan.
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
let rootGesture = baseGesture;
|
|
1289
|
-
if (overlayTaps.length > 0) {
|
|
1290
|
-
const tapGroup =
|
|
1291
|
-
overlayTaps.length === 1
|
|
1292
|
-
? overlayTaps[0]
|
|
1293
|
-
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
1294
|
-
// Always `Simultaneous`, never `Race`: on iOS the scrub pan uses
|
|
1295
|
-
// `minDistance(0)`, so in a `Race` it activates on touch-down and cancels the
|
|
1296
|
-
// overlay tap before it can recognize — `onMarkerPress`/`onReferenceLinePress`
|
|
1297
|
-
// would never fire. Sharing the gesture space lets the tap recognize; the pan
|
|
1298
|
-
// defers to a marker/badge under the finger via `deferTapHit` (see
|
|
1299
|
-
// `useCrosshair`'s scrub `onStart`) so no stray crosshair is dropped there.
|
|
1300
|
-
rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// Compose the pan-scroll gesture. holdToScrub races the scrub/tap gestures (a
|
|
1304
|
-
// quick drag scrolls; a still press-hold falls through to scrub). Axis-drag
|
|
1305
|
-
// goes first via Exclusive: it fails instantly outside the axis band, so scrub
|
|
1306
|
-
// runs everywhere else.
|
|
1307
|
-
if (timeScrollEnabled) {
|
|
1308
|
-
rootGesture =
|
|
1309
|
-
scrollGestureMode === "axisDrag"
|
|
1310
|
-
? Gesture.Exclusive(panScrollGesture, rootGesture)
|
|
1311
|
-
: Gesture.Race(panScrollGesture, rootGesture);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// Draggable reference lines take priority: a vertical grab on a line drags it.
|
|
1315
|
-
// The manual-activation pan fails fast off any line (or on horizontal intent), so
|
|
1316
|
-
// Exclusive falls through to scrub / scroll everywhere else.
|
|
1317
|
-
if (refDragEnabled) {
|
|
1318
|
-
rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// Pinch runs alongside everything else (two-finger, so it never competes with
|
|
1322
|
-
// the one-finger pan/scrub/tap gestures for the same touch).
|
|
1323
|
-
if (zoomEnabled) {
|
|
1324
|
-
rootGesture = Gesture.Simultaneous(rootGesture, pinchZoomGesture);
|
|
1325
|
-
}
|
|
2072
|
+
const rootGesture = composeLiveChartRootGesture({
|
|
2073
|
+
crosshair,
|
|
2074
|
+
markerTapGesture,
|
|
2075
|
+
refLineTapGesture,
|
|
2076
|
+
refDragGesture,
|
|
2077
|
+
panScrollGesture,
|
|
2078
|
+
pinchZoomGesture,
|
|
2079
|
+
markersActive,
|
|
2080
|
+
refPressActive,
|
|
2081
|
+
refDragEnabled,
|
|
2082
|
+
scrubActionActive: scrubActionCfg !== null,
|
|
2083
|
+
timeScrollEnabled,
|
|
2084
|
+
scrollGestureMode,
|
|
2085
|
+
zoomEnabled,
|
|
2086
|
+
});
|
|
1326
2087
|
|
|
1327
2088
|
// ── Derived render values ──────────────────────────────────────────────
|
|
1328
2089
|
const {
|
|
@@ -1355,49 +2116,33 @@ function useLiveChartController({
|
|
|
1355
2116
|
// A dot that tracks the true live point while parked is exempt from the
|
|
1356
2117
|
// scroll-back hide: it no longer marks an off-screen price, and it hides
|
|
1357
2118
|
// itself once the live point leaves the window (`useLiveDot`'s sentinel).
|
|
1358
|
-
const liveDotOpacity = useDerivedValue(
|
|
1359
|
-
() =>
|
|
1360
|
-
reveal.dotOpacity.value *
|
|
1361
|
-
(selectionDotDuringScrub && crosshairScrubActive.value ? 0 : 1) *
|
|
1362
|
-
liveIndicatorScrollOpacity(
|
|
1363
|
-
hideLiveOnScrollBack && !dotTracksParked,
|
|
1364
|
-
engine.viewEnd.value,
|
|
1365
|
-
) *
|
|
1366
|
-
seriesIndicatorOpacity.value,
|
|
1367
|
-
);
|
|
1368
|
-
// Same scrolled-back gating for the value line: it would draw a dashed line
|
|
1369
|
-
// at the live value's Y — a price that isn't in the scrolled-back view.
|
|
1370
|
-
const valueLineOpacity = useDerivedValue(
|
|
1371
|
-
() =>
|
|
1372
|
-
reveal.lineOpacity.value *
|
|
1373
|
-
liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
|
|
1374
|
-
resolvedSeriesOpacity.value,
|
|
1375
|
-
);
|
|
1376
|
-
const liveBadgeOpacity = useDerivedValue(
|
|
1377
|
-
() =>
|
|
1378
|
-
reveal.badgeOpacity.value *
|
|
1379
|
-
liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
|
|
1380
|
-
resolvedSeriesOpacity.value,
|
|
1381
|
-
);
|
|
1382
|
-
|
|
1383
|
-
// Fade the annotation overlays (markers + reference lines) out while scrubbing
|
|
1384
|
-
// when `scrub.hideOverlays` is set. Eased off the scrub-ACTIVE flag — not the
|
|
1385
|
-
// crosshair's edge-proximity fade, which drops to 0 near the live dot and would
|
|
1386
|
-
// resurface the overlays mid-scrub. Only this group opacity animates; the
|
|
1387
|
-
// marker atlas / reference-line draws stay intact (one batched draw each).
|
|
1388
2119
|
const fadeOverlaysOnScrub =
|
|
1389
2120
|
!isStatic && scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
2121
|
+
const {
|
|
2122
|
+
liveDotOpacity,
|
|
2123
|
+
valueLineOpacity,
|
|
2124
|
+
liveBadgeOpacity,
|
|
2125
|
+
overlayScrubFade,
|
|
2126
|
+
markerGroupOpacity,
|
|
2127
|
+
} = useLiveIndicatorOpacities({
|
|
2128
|
+
reveal,
|
|
2129
|
+
seriesIndicatorOpacity,
|
|
2130
|
+
seriesOpacity: resolvedSeriesOpacity,
|
|
2131
|
+
scrubActive: crosshairScrubActive,
|
|
2132
|
+
selectionDotDuringScrub,
|
|
2133
|
+
hideLiveOnScrollBack,
|
|
2134
|
+
dotTracksParked,
|
|
2135
|
+
viewEnd: engine.viewEnd,
|
|
2136
|
+
fadeOverlaysOnScrub,
|
|
2137
|
+
});
|
|
2138
|
+
const modelDefaults = resolveLiveChartModelDefaults({
|
|
2139
|
+
isCandle,
|
|
2140
|
+
candleWidth,
|
|
2141
|
+
loadingCfg,
|
|
2142
|
+
volumeCfg,
|
|
2143
|
+
palette,
|
|
2144
|
+
lineProp,
|
|
2145
|
+
});
|
|
1401
2146
|
|
|
1402
2147
|
return {
|
|
1403
2148
|
// passthrough props the render needs
|
|
@@ -1414,9 +2159,7 @@ function useLiveChartController({
|
|
|
1414
2159
|
formatTime,
|
|
1415
2160
|
isCandle,
|
|
1416
2161
|
isStatic,
|
|
1417
|
-
|
|
1418
|
-
// candle's drawn center, not its bucket-start (left) edge. 0 in line mode.
|
|
1419
|
-
extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
|
|
2162
|
+
...modelDefaults,
|
|
1420
2163
|
// configs
|
|
1421
2164
|
yAxisCfg,
|
|
1422
2165
|
yAxisFloat: effectiveYAxisFloat,
|
|
@@ -1457,8 +2200,8 @@ function useLiveChartController({
|
|
|
1457
2200
|
hasRecolorSegments,
|
|
1458
2201
|
thresholdCfg,
|
|
1459
2202
|
thresholdGeom,
|
|
1460
|
-
thresholdStrokeColors
|
|
1461
|
-
thresholdFillColors
|
|
2203
|
+
thresholdStrokeColors,
|
|
2204
|
+
thresholdFillColors,
|
|
1462
2205
|
// Time-varying threshold (a `LiveChartPoint[]` series): the per-fragment split
|
|
1463
2206
|
// shader + polyline marker, vs. the constant case's gradient.
|
|
1464
2207
|
thresholdIsSeries,
|
|
@@ -1469,6 +2212,7 @@ function useLiveChartController({
|
|
|
1469
2212
|
thresholdMarkerVisible,
|
|
1470
2213
|
thresholdBadgeVisible,
|
|
1471
2214
|
thresholdMarkerValue,
|
|
2215
|
+
thresholdCustomBadge,
|
|
1472
2216
|
thresholdSeriesPts,
|
|
1473
2217
|
badgeUsesRightGutter,
|
|
1474
2218
|
// theme / layout / fonts
|
|
@@ -1482,13 +2226,8 @@ function useLiveChartController({
|
|
|
1482
2226
|
// engine + reveal
|
|
1483
2227
|
engine,
|
|
1484
2228
|
reveal,
|
|
2229
|
+
loadingActive,
|
|
1485
2230
|
axisAutoHideOpacity,
|
|
1486
|
-
// loading shell styling (null → not loading)
|
|
1487
|
-
loadingLineColor: loadingCfg?.color,
|
|
1488
|
-
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
1489
|
-
loadingAmplitude: loadingCfg?.amplitude,
|
|
1490
|
-
loadingSpeed: loadingCfg?.speed,
|
|
1491
|
-
loadingAxisLabels: loadingCfg?.axisLabels ?? true,
|
|
1492
2231
|
// derived render values
|
|
1493
2232
|
backgroundColor,
|
|
1494
2233
|
gradientEnd,
|
|
@@ -1510,13 +2249,12 @@ function useLiveChartController({
|
|
|
1510
2249
|
thresholdFillPath,
|
|
1511
2250
|
lineIsLinear,
|
|
1512
2251
|
volumeCfg,
|
|
2252
|
+
candleGapsCfg,
|
|
2253
|
+
lineGapsCfg,
|
|
1513
2254
|
// Volume bars: active flag, fade-in opacity, and resolved colors (default to
|
|
1514
2255
|
// the candle palette). The reserved band height is read by the x-axis.
|
|
1515
2256
|
volumeActive: volumeCfg !== null,
|
|
1516
2257
|
volumeBandHeight,
|
|
1517
|
-
volumeOpacity: volumeCfg?.opacity ?? 1,
|
|
1518
|
-
volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
|
|
1519
|
-
volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
|
|
1520
2258
|
dotX,
|
|
1521
2259
|
dotY,
|
|
1522
2260
|
liveDotOpacity,
|
|
@@ -1537,7 +2275,6 @@ function useLiveChartController({
|
|
|
1537
2275
|
renderOverlay,
|
|
1538
2276
|
// selection dot: resolved config + fallback color (the chart line/accent color)
|
|
1539
2277
|
selectionDot: selectionDotCfg,
|
|
1540
|
-
selectionColor: lineProp?.color ?? palette.line,
|
|
1541
2278
|
// RN axis edge labels (floated over the canvas as a sibling layer)
|
|
1542
2279
|
topLabelCfg,
|
|
1543
2280
|
bottomLabelCfg,
|
|
@@ -1797,11 +2534,117 @@ function ChartFillLayer({
|
|
|
1797
2534
|
</Group>
|
|
1798
2535
|
) : null)}
|
|
1799
2536
|
</Group>
|
|
1800
|
-
</Group>
|
|
2537
|
+
</Group>
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
type CandlePaths = ReturnType<typeof useCandlePaths>;
|
|
2542
|
+
type CandleGapPaths = ReturnType<typeof useCandleGapPaths>;
|
|
2543
|
+
type LineGapPaths = ReturnType<typeof useLineGapPaths>;
|
|
2544
|
+
|
|
2545
|
+
function GapBridgePathBatch({
|
|
2546
|
+
paths,
|
|
2547
|
+
config,
|
|
2548
|
+
palette,
|
|
2549
|
+
}: {
|
|
2550
|
+
paths: CandleGapPaths | LineGapPaths;
|
|
2551
|
+
config: ResolvedCandleGapsConfig;
|
|
2552
|
+
palette: LiveChartPalette;
|
|
2553
|
+
}) {
|
|
2554
|
+
const noTrades = config.styles["no-trades"].bridge;
|
|
2555
|
+
const unavailable = config.styles.unavailable.bridge;
|
|
2556
|
+
const unknown = config.styles.unknown.bridge;
|
|
2557
|
+
return (
|
|
2558
|
+
<>
|
|
2559
|
+
{noTrades !== null && (
|
|
2560
|
+
<Group opacity={noTrades.opacity}>
|
|
2561
|
+
<Path
|
|
2562
|
+
path={paths.noTradesPath}
|
|
2563
|
+
style="stroke"
|
|
2564
|
+
strokeWidth={noTrades.strokeWidth}
|
|
2565
|
+
strokeCap={noTrades.strokeCap}
|
|
2566
|
+
color={noTrades.color ?? palette.refLine}
|
|
2567
|
+
/>
|
|
2568
|
+
</Group>
|
|
2569
|
+
)}
|
|
2570
|
+
{unavailable !== null && (
|
|
2571
|
+
<Group opacity={unavailable.opacity}>
|
|
2572
|
+
<Path
|
|
2573
|
+
path={paths.unavailablePath}
|
|
2574
|
+
style="stroke"
|
|
2575
|
+
strokeWidth={unavailable.strokeWidth}
|
|
2576
|
+
strokeCap={unavailable.strokeCap}
|
|
2577
|
+
color={unavailable.color ?? palette.refLine}
|
|
2578
|
+
/>
|
|
2579
|
+
</Group>
|
|
2580
|
+
)}
|
|
2581
|
+
{unknown !== null && (
|
|
2582
|
+
<Group opacity={unknown.opacity}>
|
|
2583
|
+
<Path
|
|
2584
|
+
path={paths.unknownPath}
|
|
2585
|
+
style="stroke"
|
|
2586
|
+
strokeWidth={unknown.strokeWidth}
|
|
2587
|
+
strokeCap={unknown.strokeCap}
|
|
2588
|
+
color={unknown.color ?? palette.refLine}
|
|
2589
|
+
/>
|
|
2590
|
+
</Group>
|
|
2591
|
+
)}
|
|
2592
|
+
</>
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
function ChartCandleGapLayer({
|
|
2597
|
+
model,
|
|
2598
|
+
config,
|
|
2599
|
+
focusOtherCandles,
|
|
2600
|
+
inactiveOpacity,
|
|
2601
|
+
focusedOpacity,
|
|
2602
|
+
focusedClip,
|
|
2603
|
+
}: {
|
|
2604
|
+
model: LiveChartModel;
|
|
2605
|
+
config: ResolvedCandleGapsConfig;
|
|
2606
|
+
focusOtherCandles: boolean;
|
|
2607
|
+
inactiveOpacity: SharedValue<number>;
|
|
2608
|
+
focusedOpacity: SharedValue<number>;
|
|
2609
|
+
focusedClip: SharedValue<{
|
|
2610
|
+
x: number;
|
|
2611
|
+
y: number;
|
|
2612
|
+
width: number;
|
|
2613
|
+
height: number;
|
|
2614
|
+
}>;
|
|
2615
|
+
}) {
|
|
2616
|
+
const paths = useCandleGapPaths(
|
|
2617
|
+
model.engine,
|
|
2618
|
+
model.effectivePadding,
|
|
2619
|
+
model.candlesEngine,
|
|
2620
|
+
model.liveEngine,
|
|
2621
|
+
model.displayCandleWidth,
|
|
2622
|
+
config,
|
|
2623
|
+
model.metricsCfg.candle,
|
|
2624
|
+
);
|
|
2625
|
+
const batch = (
|
|
2626
|
+
<GapBridgePathBatch paths={paths} config={config} palette={model.palette} />
|
|
2627
|
+
);
|
|
2628
|
+
return focusOtherCandles ? (
|
|
2629
|
+
<>
|
|
2630
|
+
<Group opacity={inactiveOpacity}>{batch}</Group>
|
|
2631
|
+
<Group opacity={focusedOpacity} clip={focusedClip}>
|
|
2632
|
+
{batch}
|
|
2633
|
+
</Group>
|
|
2634
|
+
</>
|
|
2635
|
+
) : (
|
|
2636
|
+
batch
|
|
1801
2637
|
);
|
|
1802
2638
|
}
|
|
1803
2639
|
|
|
1804
|
-
|
|
2640
|
+
/** Line-mode bridge paths live in a child so candle charts register no worklets. */
|
|
2641
|
+
function ChartLineGapLayer({ model }: { model: LiveChartModel }) {
|
|
2642
|
+
const config = model.lineGapsCfg!;
|
|
2643
|
+
const paths = useLineGapPaths(model.engine, model.effectivePadding, config);
|
|
2644
|
+
return (
|
|
2645
|
+
<GapBridgePathBatch paths={paths} config={config} palette={model.palette} />
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
1805
2648
|
|
|
1806
2649
|
/** One batched candle pass: two body paths and two wick paths. */
|
|
1807
2650
|
function CandlePathBatch({
|
|
@@ -1858,6 +2701,7 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1858
2701
|
volumeOpacity,
|
|
1859
2702
|
volumeUpColor,
|
|
1860
2703
|
volumeDownColor,
|
|
2704
|
+
candleGapsCfg,
|
|
1861
2705
|
scrubCfg,
|
|
1862
2706
|
crosshair,
|
|
1863
2707
|
} = model;
|
|
@@ -1875,24 +2719,42 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1875
2719
|
const focusOtherCandles = scrubCfg?.dimTarget === "otherCandles";
|
|
1876
2720
|
const candleDimOpacity = scrubCfg?.dimOpacity ?? 1;
|
|
1877
2721
|
const candleDimFadeMs = scrubCfg?.dimFadeMs ?? 0;
|
|
1878
|
-
// Destructure
|
|
2722
|
+
// Destructure SharedValues before entering worklets. Closing over the
|
|
1879
2723
|
// whole crosshair object would also serialize its RNGH gesture instances.
|
|
1880
2724
|
const scrubActive = crosshair.scrubActive;
|
|
1881
2725
|
// ChartCandleLayer is single-series only; useCrosshair always supplies this
|
|
1882
2726
|
// value (it is optional on the shared state type only for LiveChartSeries).
|
|
1883
2727
|
const scrubCandle = crosshair.scrubCandle!;
|
|
1884
|
-
|
|
2728
|
+
const scrubGap = crosshair.scrubGap!;
|
|
2729
|
+
const scrubTime = crosshair.scrubTime;
|
|
2730
|
+
const currentFocusBucketTime = useDerivedValue(() => {
|
|
2731
|
+
if (!focusOtherCandles) return null;
|
|
2732
|
+
const candle = scrubCandle.get();
|
|
2733
|
+
if (candle) return candle.time;
|
|
2734
|
+
const gap = scrubGap.get();
|
|
2735
|
+
if (!gap) return null;
|
|
2736
|
+
return candleGapBucketStartAtTime(
|
|
2737
|
+
gap,
|
|
2738
|
+
scrubTime.get(),
|
|
2739
|
+
candlesEngine.get(),
|
|
2740
|
+
liveEngine.get(),
|
|
2741
|
+
displayCandleWidth.get(),
|
|
2742
|
+
);
|
|
2743
|
+
});
|
|
2744
|
+
// Keep the last selection after the current target clears so its clipped full-
|
|
1885
2745
|
// strength pass can cover the base batch until the release fade completes.
|
|
1886
|
-
const
|
|
2746
|
+
const lastFocusBucket = useSharedValue<{ time: number } | null>(null);
|
|
1887
2747
|
useAnimatedReaction(
|
|
1888
|
-
() =>
|
|
1889
|
-
(
|
|
1890
|
-
if (
|
|
2748
|
+
() => currentFocusBucketTime.get(),
|
|
2749
|
+
(time) => {
|
|
2750
|
+
if (time !== null) lastFocusBucket.set({ time });
|
|
1891
2751
|
},
|
|
1892
2752
|
);
|
|
1893
2753
|
const inactiveCandleOpacity = useDerivedValue(() => {
|
|
1894
2754
|
const target =
|
|
1895
|
-
focusOtherCandles &&
|
|
2755
|
+
focusOtherCandles &&
|
|
2756
|
+
scrubActive.get() &&
|
|
2757
|
+
currentFocusBucketTime.get() !== null
|
|
1896
2758
|
? candleDimOpacity
|
|
1897
2759
|
: 1;
|
|
1898
2760
|
return candleDimFadeMs > 0
|
|
@@ -1901,33 +2763,36 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1901
2763
|
}, [
|
|
1902
2764
|
focusOtherCandles,
|
|
1903
2765
|
scrubActive,
|
|
1904
|
-
|
|
2766
|
+
currentFocusBucketTime,
|
|
1905
2767
|
candleDimOpacity,
|
|
1906
2768
|
candleDimFadeMs,
|
|
1907
2769
|
]);
|
|
1908
2770
|
const focusedCandleOpacity = useDerivedValue(() => {
|
|
1909
|
-
const
|
|
1910
|
-
const
|
|
1911
|
-
|
|
2771
|
+
const currentTime = currentFocusBucketTime.get();
|
|
2772
|
+
const focusBucket =
|
|
2773
|
+
currentTime === null ? lastFocusBucket.get() : { time: currentTime };
|
|
2774
|
+
if (!focusOtherCandles || !focusBucket) return 0;
|
|
1912
2775
|
return computeCandleFocusPassOpacity(
|
|
1913
2776
|
scrubActive.get(),
|
|
1914
|
-
|
|
2777
|
+
currentTime !== null,
|
|
1915
2778
|
inactiveCandleOpacity.get(),
|
|
1916
2779
|
);
|
|
1917
2780
|
}, [
|
|
1918
2781
|
focusOtherCandles,
|
|
1919
2782
|
scrubActive,
|
|
1920
|
-
|
|
1921
|
-
|
|
2783
|
+
currentFocusBucketTime,
|
|
2784
|
+
lastFocusBucket,
|
|
1922
2785
|
inactiveCandleOpacity,
|
|
1923
2786
|
]);
|
|
1924
2787
|
const focusedCandleClip = useDerivedValue(() => {
|
|
1925
2788
|
if (!focusOtherCandles) {
|
|
1926
2789
|
return HIDDEN_CANDLE_FOCUS_CLIP;
|
|
1927
2790
|
}
|
|
1928
|
-
const
|
|
2791
|
+
const currentTime = currentFocusBucketTime.get();
|
|
2792
|
+
const focusBucket =
|
|
2793
|
+
currentTime === null ? lastFocusBucket.get() : { time: currentTime };
|
|
1929
2794
|
return computeCandleFocusClip(
|
|
1930
|
-
|
|
2795
|
+
focusBucket,
|
|
1931
2796
|
effectivePadding,
|
|
1932
2797
|
engine.canvasWidth.get(),
|
|
1933
2798
|
engine.canvasHeight.get(),
|
|
@@ -1937,8 +2802,8 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1937
2802
|
);
|
|
1938
2803
|
}, [
|
|
1939
2804
|
focusOtherCandles,
|
|
1940
|
-
|
|
1941
|
-
|
|
2805
|
+
currentFocusBucketTime,
|
|
2806
|
+
lastFocusBucket,
|
|
1942
2807
|
effectivePadding,
|
|
1943
2808
|
engine.canvasWidth,
|
|
1944
2809
|
engine.canvasHeight,
|
|
@@ -1950,6 +2815,16 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1950
2815
|
return (
|
|
1951
2816
|
<Group opacity={seriesOpacity}>
|
|
1952
2817
|
<Group opacity={candleGroupOpacity}>
|
|
2818
|
+
{candleGapsCfg && (
|
|
2819
|
+
<ChartCandleGapLayer
|
|
2820
|
+
model={model}
|
|
2821
|
+
config={candleGapsCfg}
|
|
2822
|
+
focusOtherCandles={focusOtherCandles}
|
|
2823
|
+
inactiveOpacity={inactiveCandleOpacity}
|
|
2824
|
+
focusedOpacity={focusedCandleOpacity}
|
|
2825
|
+
focusedClip={focusedCandleClip}
|
|
2826
|
+
/>
|
|
2827
|
+
)}
|
|
1953
2828
|
{focusOtherCandles ? (
|
|
1954
2829
|
<>
|
|
1955
2830
|
<Group opacity={inactiveCandleOpacity}>
|
|
@@ -1992,6 +2867,113 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
1992
2867
|
);
|
|
1993
2868
|
}
|
|
1994
2869
|
|
|
2870
|
+
function ChartLineStrokeShader({ model }: { model: LiveChartModel }) {
|
|
2871
|
+
const {
|
|
2872
|
+
engine,
|
|
2873
|
+
effectivePadding,
|
|
2874
|
+
palette,
|
|
2875
|
+
resolvedSegments,
|
|
2876
|
+
hasRecolorSegments,
|
|
2877
|
+
crosshair,
|
|
2878
|
+
thresholdCfg,
|
|
2879
|
+
thresholdGeom,
|
|
2880
|
+
thresholdStrokeColors,
|
|
2881
|
+
thresholdIsSeries,
|
|
2882
|
+
thresholdSeriesHasPoints,
|
|
2883
|
+
thresholdStrokeUniforms,
|
|
2884
|
+
lineProp,
|
|
2885
|
+
layoutWidth,
|
|
2886
|
+
} = model;
|
|
2887
|
+
if (thresholdIsSeries) {
|
|
2888
|
+
return thresholdSeriesHasPoints ? (
|
|
2889
|
+
<ThresholdSplitShader uniforms={thresholdStrokeUniforms} />
|
|
2890
|
+
) : null;
|
|
2891
|
+
}
|
|
2892
|
+
if (thresholdCfg && thresholdStrokeColors) {
|
|
2893
|
+
return (
|
|
2894
|
+
<LinearGradient
|
|
2895
|
+
start={vec(0, 0)}
|
|
2896
|
+
end={thresholdGeom.gradientEnd}
|
|
2897
|
+
colors={thresholdStrokeColors}
|
|
2898
|
+
positions={thresholdGeom.splitPositions}
|
|
2899
|
+
/>
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
if (hasRecolorSegments) {
|
|
2903
|
+
return (
|
|
2904
|
+
<SegmentLineGradient
|
|
2905
|
+
engine={engine}
|
|
2906
|
+
segments={resolvedSegments}
|
|
2907
|
+
padding={effectivePadding}
|
|
2908
|
+
baseColor={lineProp?.color ?? palette.line}
|
|
2909
|
+
scrubX={crosshair.scrubX}
|
|
2910
|
+
scrubActive={crosshair.scrubActive}
|
|
2911
|
+
/>
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
if (!lineProp?.colors?.length) return null;
|
|
2915
|
+
return (
|
|
2916
|
+
<LinearGradient
|
|
2917
|
+
start={vec(0, 0)}
|
|
2918
|
+
end={vec(layoutWidth, 0)}
|
|
2919
|
+
colors={lineProp.colors}
|
|
2920
|
+
/>
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
function ChartMainPlotLayer({
|
|
2925
|
+
model,
|
|
2926
|
+
yAxisEntries,
|
|
2927
|
+
}: {
|
|
2928
|
+
model: LiveChartModel;
|
|
2929
|
+
yAxisEntries: YAxisEntries | null;
|
|
2930
|
+
}) {
|
|
2931
|
+
const {
|
|
2932
|
+
engine,
|
|
2933
|
+
effectivePadding,
|
|
2934
|
+
palette,
|
|
2935
|
+
lineGroupOpacity,
|
|
2936
|
+
seriesOpacity,
|
|
2937
|
+
linePath,
|
|
2938
|
+
lineGapsCfg,
|
|
2939
|
+
strokeWidth,
|
|
2940
|
+
lineProp,
|
|
2941
|
+
isCandle,
|
|
2942
|
+
xAxisCfg,
|
|
2943
|
+
yAxisCfg,
|
|
2944
|
+
yAxisFloat,
|
|
2945
|
+
} = model;
|
|
2946
|
+
|
|
2947
|
+
return (
|
|
2948
|
+
<>
|
|
2949
|
+
<Group opacity={seriesOpacity}>
|
|
2950
|
+
<Group opacity={lineGroupOpacity}>
|
|
2951
|
+
<Path
|
|
2952
|
+
path={linePath}
|
|
2953
|
+
style="stroke"
|
|
2954
|
+
strokeWidth={strokeWidth}
|
|
2955
|
+
strokeCap={lineProp?.cap ?? "round"}
|
|
2956
|
+
strokeJoin={lineProp?.join ?? "round"}
|
|
2957
|
+
color={lineProp?.color ?? palette.line}
|
|
2958
|
+
>
|
|
2959
|
+
<ChartLineStrokeShader model={model} />
|
|
2960
|
+
</Path>
|
|
2961
|
+
{lineGapsCfg ? <ChartLineGapLayer model={model} /> : null}
|
|
2962
|
+
</Group>
|
|
2963
|
+
</Group>
|
|
2964
|
+
{isCandle ? <ChartCandleLayer model={model} /> : null}
|
|
2965
|
+
{yAxisCfg && yAxisFloat ? (
|
|
2966
|
+
<ChartYAxisLayer
|
|
2967
|
+
model={model}
|
|
2968
|
+
variant="labels"
|
|
2969
|
+
entries={yAxisEntries!}
|
|
2970
|
+
/>
|
|
2971
|
+
) : null}
|
|
2972
|
+
{xAxisCfg ? <ChartXAxisLayer model={model} /> : null}
|
|
2973
|
+
</>
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
|
|
1995
2977
|
/** Main shaken chart stack drawn ABOVE the left-edge fade so the line stays crisp:
|
|
1996
2978
|
* segment dividers, value/reference lines, the line/candles, axes, dot, degen,
|
|
1997
2979
|
* markers, and the loading/empty art. Background fills are in `ChartFillLayer`
|
|
@@ -2032,11 +3014,13 @@ function ChartStack({
|
|
|
2032
3014
|
thresholdMarkerVisible,
|
|
2033
3015
|
thresholdBadgeVisible,
|
|
2034
3016
|
thresholdMarkerValue,
|
|
3017
|
+
thresholdCustomBadge,
|
|
2035
3018
|
thresholdSeriesPts,
|
|
2036
3019
|
formatValue,
|
|
2037
3020
|
lineGroupOpacity,
|
|
2038
3021
|
seriesOpacity,
|
|
2039
3022
|
linePath,
|
|
3023
|
+
lineGapsCfg,
|
|
2040
3024
|
lineIsLinear,
|
|
2041
3025
|
strokeWidth,
|
|
2042
3026
|
lineProp,
|
|
@@ -2133,73 +3117,7 @@ function ChartStack({
|
|
|
2133
3117
|
/>
|
|
2134
3118
|
)}
|
|
2135
3119
|
|
|
2136
|
-
{
|
|
2137
|
-
full-width gradient paints the base color outside segments and each
|
|
2138
|
-
segment's color within — so the line itself is recolored/faded (alpha in
|
|
2139
|
-
the segment color reduces the line's opacity), not covered by an overlay. */}
|
|
2140
|
-
<Group opacity={seriesOpacity}>
|
|
2141
|
-
<Group opacity={lineGroupOpacity}>
|
|
2142
|
-
<Path
|
|
2143
|
-
path={linePath}
|
|
2144
|
-
style="stroke"
|
|
2145
|
-
strokeWidth={strokeWidth}
|
|
2146
|
-
strokeCap={lineProp?.cap ?? "round"}
|
|
2147
|
-
strokeJoin={lineProp?.join ?? "round"}
|
|
2148
|
-
color={lineProp?.color ?? palette.line}
|
|
2149
|
-
>
|
|
2150
|
-
{thresholdIsSeries ? (
|
|
2151
|
-
// Time-varying split: a per-fragment shader colors the stroke above/
|
|
2152
|
-
// below the threshold polyline. Supersedes line.colors + segments.
|
|
2153
|
-
// An empty series renders no paint child → the plain stroke color
|
|
2154
|
-
// (null here keeps the empty case out of the NaN constant gradient).
|
|
2155
|
-
thresholdSeriesHasPoints ? (
|
|
2156
|
-
<ThresholdSplitShader uniforms={thresholdStrokeUniforms} />
|
|
2157
|
-
) : null
|
|
2158
|
-
) : thresholdCfg && thresholdStrokeColors ? (
|
|
2159
|
-
// Vertical hard split at the threshold Y — supersedes line.colors and
|
|
2160
|
-
// segment recoloring for the stroke while a threshold is set.
|
|
2161
|
-
<LinearGradient
|
|
2162
|
-
start={vec(0, 0)}
|
|
2163
|
-
end={thresholdGeom.gradientEnd}
|
|
2164
|
-
colors={thresholdStrokeColors}
|
|
2165
|
-
positions={thresholdGeom.splitPositions}
|
|
2166
|
-
/>
|
|
2167
|
-
) : hasRecolorSegments ? (
|
|
2168
|
-
<SegmentLineGradient
|
|
2169
|
-
engine={engine}
|
|
2170
|
-
segments={resolvedSegments}
|
|
2171
|
-
padding={effectivePadding}
|
|
2172
|
-
baseColor={lineProp?.color ?? palette.line}
|
|
2173
|
-
scrubX={crosshair.scrubX}
|
|
2174
|
-
scrubActive={crosshair.scrubActive}
|
|
2175
|
-
/>
|
|
2176
|
-
) : lineProp?.colors?.length ? (
|
|
2177
|
-
<LinearGradient
|
|
2178
|
-
start={vec(0, 0)}
|
|
2179
|
-
end={vec(layoutWidth, 0)}
|
|
2180
|
-
colors={lineProp.colors}
|
|
2181
|
-
/>
|
|
2182
|
-
) : null}
|
|
2183
|
-
</Path>
|
|
2184
|
-
</Group>
|
|
2185
|
-
</Group>
|
|
2186
|
-
|
|
2187
|
-
{isCandle && <ChartCandleLayer model={model} />}
|
|
2188
|
-
|
|
2189
|
-
{/* Floating axis: the labels float ABOVE the candles (right-aligned at the
|
|
2190
|
-
edge) so the plot runs full-width and candles stay fully visible behind
|
|
2191
|
-
them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
|
|
2192
|
-
{yAxisCfg && yAxisFloat && (
|
|
2193
|
-
<ChartYAxisLayer
|
|
2194
|
-
model={model}
|
|
2195
|
-
variant="labels"
|
|
2196
|
-
entries={yAxisEntries!}
|
|
2197
|
-
/>
|
|
2198
|
-
)}
|
|
2199
|
-
|
|
2200
|
-
{/* X-axis time labels. With a volume band the bottom padding is inflated by
|
|
2201
|
-
the band height; pass it so the axis shifts back to the very bottom. */}
|
|
2202
|
-
{xAxisCfg && <ChartXAxisLayer model={model} />}
|
|
3120
|
+
<ChartMainPlotLayer model={model} yAxisEntries={yAxisEntries} />
|
|
2203
3121
|
|
|
2204
3122
|
{/* Live dot — the badge is drawn later (after the scrub layer) so the
|
|
2205
3123
|
scrub dim never clips the live-price badge's left edge. Hidden while
|
|
@@ -2256,7 +3174,7 @@ function ChartStack({
|
|
|
2256
3174
|
|
|
2257
3175
|
{/* Threshold label badge — on top of the line/dot/markers so it's never
|
|
2258
3176
|
painted over (the dashed marker line itself stays behind the line, above). */}
|
|
2259
|
-
{thresholdCfg?.line && (
|
|
3177
|
+
{thresholdCfg?.line && thresholdCustomBadge == null && (
|
|
2260
3178
|
<ThresholdBadgeOverlay
|
|
2261
3179
|
engine={engine}
|
|
2262
3180
|
padding={effectivePadding}
|
|
@@ -2310,15 +3228,27 @@ function ChartTradeStreamLayer({
|
|
|
2310
3228
|
tradeStream,
|
|
2311
3229
|
tradeStreamResolved,
|
|
2312
3230
|
effectivePadding,
|
|
3231
|
+
volumeBandHeight,
|
|
2313
3232
|
palette,
|
|
2314
3233
|
skiaFont,
|
|
2315
3234
|
reveal,
|
|
2316
3235
|
isStatic,
|
|
2317
3236
|
} = model;
|
|
3237
|
+
// `effectivePadding.bottom` includes the volume reservation so candle prices
|
|
3238
|
+
// stop above the bars. The trade tape should still enter at the chart's true
|
|
3239
|
+
// lower edge (the volume baseline / x-axis), so remove that reservation for
|
|
3240
|
+
// this overlay's coordinate space only.
|
|
3241
|
+
const tradeStreamPadding =
|
|
3242
|
+
volumeBandHeight > 0
|
|
3243
|
+
? {
|
|
3244
|
+
...effectivePadding,
|
|
3245
|
+
bottom: effectivePadding.bottom - volumeBandHeight,
|
|
3246
|
+
}
|
|
3247
|
+
: effectivePadding;
|
|
2318
3248
|
const tradeMarkers = useTradeStream(
|
|
2319
3249
|
engine,
|
|
2320
3250
|
tradeStream!,
|
|
2321
|
-
|
|
3251
|
+
tradeStreamPadding,
|
|
2322
3252
|
!isStatic,
|
|
2323
3253
|
!isStatic,
|
|
2324
3254
|
);
|
|
@@ -2327,7 +3257,7 @@ function ChartTradeStreamLayer({
|
|
|
2327
3257
|
<TradeStreamOverlay
|
|
2328
3258
|
markers={tradeMarkers}
|
|
2329
3259
|
palette={palette}
|
|
2330
|
-
padding={
|
|
3260
|
+
padding={tradeStreamPadding}
|
|
2331
3261
|
font={skiaFont}
|
|
2332
3262
|
opacity={reveal.dotOpacity}
|
|
2333
3263
|
labelOffsetX={tradeStreamResolved!.labelOffsetX}
|
|
@@ -2742,7 +3672,7 @@ function ChartCustomConsumerOverlay({ model }: { model: LiveChartModel }) {
|
|
|
2742
3672
|
return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
|
|
2743
3673
|
}
|
|
2744
3674
|
|
|
2745
|
-
function
|
|
3675
|
+
function ChartCanvas({
|
|
2746
3676
|
model,
|
|
2747
3677
|
yAxisEntries,
|
|
2748
3678
|
degen,
|
|
@@ -2752,32 +3682,171 @@ function ChartView({
|
|
|
2752
3682
|
degen: DegenState | null;
|
|
2753
3683
|
}) {
|
|
2754
3684
|
const {
|
|
2755
|
-
|
|
3685
|
+
canvasMode,
|
|
3686
|
+
engine,
|
|
2756
3687
|
backgroundColor,
|
|
2757
|
-
style,
|
|
2758
|
-
onLayout,
|
|
2759
|
-
accessibilityLabel,
|
|
2760
|
-
accessibilityRole,
|
|
2761
3688
|
leftEdgeFadeCfg,
|
|
2762
3689
|
effectivePadding,
|
|
2763
|
-
engine,
|
|
2764
3690
|
palette,
|
|
2765
|
-
|
|
3691
|
+
topConnector,
|
|
3692
|
+
bottomConnector,
|
|
3693
|
+
extremaTimeOffset,
|
|
3694
|
+
loadingActive,
|
|
3695
|
+
topLabelCfg,
|
|
3696
|
+
} = model;
|
|
3697
|
+
return (
|
|
3698
|
+
<Canvas
|
|
3699
|
+
key={canvasMode}
|
|
3700
|
+
style={{ flex: 1 }}
|
|
3701
|
+
opaque={canvasMode === "opaque"}
|
|
3702
|
+
>
|
|
3703
|
+
{canvasMode === "opaque" ? (
|
|
3704
|
+
<Rect
|
|
3705
|
+
x={0}
|
|
3706
|
+
y={0}
|
|
3707
|
+
width={engine.canvasWidth}
|
|
3708
|
+
height={engine.canvasHeight}
|
|
3709
|
+
color={backgroundColor}
|
|
3710
|
+
/>
|
|
3711
|
+
) : null}
|
|
3712
|
+
<ChartFillLayer model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
3713
|
+
{leftEdgeFadeCfg ? (
|
|
3714
|
+
<LeftEdgeFade
|
|
3715
|
+
paddingLeft={effectivePadding.left}
|
|
3716
|
+
fadeWidth={leftEdgeFadeCfg.width}
|
|
3717
|
+
startColor={leftEdgeFadeCfg.startColor}
|
|
3718
|
+
endColor={leftEdgeFadeCfg.endColor}
|
|
3719
|
+
engine={engine}
|
|
3720
|
+
opaqueBackgroundRgb={
|
|
3721
|
+
canvasMode === "opaque" ? palette.bgRgb : undefined
|
|
3722
|
+
}
|
|
3723
|
+
/>
|
|
3724
|
+
) : null}
|
|
3725
|
+
<ChartStack model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
3726
|
+
{topConnector || bottomConnector ? (
|
|
3727
|
+
<ExtremaConnectorOverlay
|
|
3728
|
+
engine={engine}
|
|
3729
|
+
padding={effectivePadding}
|
|
3730
|
+
extremaTimeOffset={extremaTimeOffset}
|
|
3731
|
+
top={topConnector}
|
|
3732
|
+
bottom={bottomConnector}
|
|
3733
|
+
hideExtrema={loadingActive}
|
|
3734
|
+
suppressBottomWhenCoincident={
|
|
3735
|
+
topLabelCfg?.position === "extrema" ||
|
|
3736
|
+
topLabelCfg?.position === "extrema-edge"
|
|
3737
|
+
}
|
|
3738
|
+
/>
|
|
3739
|
+
) : null}
|
|
3740
|
+
<ChartRefBadgeLayer
|
|
3741
|
+
model={model}
|
|
3742
|
+
degen={degen}
|
|
3743
|
+
yAxisEntries={yAxisEntries}
|
|
3744
|
+
/>
|
|
3745
|
+
<ChartValueOverlay model={model} degen={degen} />
|
|
3746
|
+
{model.tradeStreamResolved && model.tradeStream ? (
|
|
3747
|
+
<ChartTradeStreamLayer model={model} degen={degen} />
|
|
3748
|
+
) : null}
|
|
3749
|
+
<ChartScrubLayer model={model} degen={degen} />
|
|
3750
|
+
{model.badgeCfg ? <ChartBadgeLayer model={model} degen={degen} /> : null}
|
|
3751
|
+
<ChartScrubActionLayer model={model} />
|
|
3752
|
+
</Canvas>
|
|
3753
|
+
);
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
function ChartNativeOverlays({ model }: { model: LiveChartModel }) {
|
|
3757
|
+
const {
|
|
2766
3758
|
topLabelCfg,
|
|
2767
3759
|
bottomLabelCfg,
|
|
3760
|
+
engine,
|
|
3761
|
+
formatValue,
|
|
3762
|
+
palette,
|
|
3763
|
+
effectivePadding,
|
|
3764
|
+
extremaTimeOffset,
|
|
3765
|
+
loadingActive,
|
|
3766
|
+
thresholdCustomBadge,
|
|
3767
|
+
thresholdCfg,
|
|
3768
|
+
thresholdMarkerLineY,
|
|
3769
|
+
thresholdBadgeVisible,
|
|
2768
3770
|
markersActive,
|
|
2769
3771
|
renderMarker,
|
|
2770
|
-
renderTooltip,
|
|
2771
|
-
renderOverlay,
|
|
2772
3772
|
renderReferenceLine,
|
|
2773
3773
|
renderOffAxisReferenceLine,
|
|
2774
3774
|
allRefLines,
|
|
2775
3775
|
scrubCfg,
|
|
3776
|
+
renderTooltip,
|
|
2776
3777
|
crosshair,
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3778
|
+
renderOverlay,
|
|
3779
|
+
} = model;
|
|
3780
|
+
const hasAnnotations =
|
|
3781
|
+
(markersActive && renderMarker != null) ||
|
|
3782
|
+
((renderReferenceLine != null || renderOffAxisReferenceLine != null) &&
|
|
3783
|
+
allRefLines.length > 0);
|
|
3784
|
+
return (
|
|
3785
|
+
<>
|
|
3786
|
+
{topLabelCfg || bottomLabelCfg ? (
|
|
3787
|
+
<AxisLabelOverlay
|
|
3788
|
+
topLabel={topLabelCfg}
|
|
3789
|
+
bottomLabel={bottomLabelCfg}
|
|
3790
|
+
engine={engine}
|
|
3791
|
+
formatValue={formatValue}
|
|
3792
|
+
defaultColor={palette.gridLabel}
|
|
3793
|
+
padding={effectivePadding}
|
|
3794
|
+
extremaTimeOffset={extremaTimeOffset}
|
|
3795
|
+
hideExtrema={loadingActive}
|
|
3796
|
+
/>
|
|
3797
|
+
) : null}
|
|
3798
|
+
{thresholdCustomBadge && thresholdCfg?.line ? (
|
|
3799
|
+
<CustomThresholdBadgeOverlay
|
|
3800
|
+
element={thresholdCustomBadge}
|
|
3801
|
+
engine={engine}
|
|
3802
|
+
padding={effectivePadding}
|
|
3803
|
+
y={thresholdMarkerLineY}
|
|
3804
|
+
visible={thresholdBadgeVisible}
|
|
3805
|
+
position={thresholdCfg.line.labelPosition}
|
|
3806
|
+
/>
|
|
3807
|
+
) : null}
|
|
3808
|
+
{hasAnnotations ? <ChartCustomAnnotations model={model} /> : null}
|
|
3809
|
+
{scrubCfg && renderTooltip ? (
|
|
3810
|
+
<CustomTooltipOverlay
|
|
3811
|
+
renderTooltip={renderTooltip}
|
|
3812
|
+
scrubX={crosshair.scrubX}
|
|
3813
|
+
scrubValue={crosshair.scrubValue}
|
|
3814
|
+
scrubTime={crosshair.scrubTime}
|
|
3815
|
+
scrubActive={crosshair.scrubActive}
|
|
3816
|
+
scrubCandle={crosshair.scrubCandle}
|
|
3817
|
+
scrubGap={crosshair.scrubGap}
|
|
3818
|
+
tooltipLayout={crosshair.tooltipLayout}
|
|
3819
|
+
engine={engine}
|
|
3820
|
+
padding={effectivePadding}
|
|
3821
|
+
placement={scrubCfg.tooltipPlacement}
|
|
3822
|
+
margin={scrubCfg.tooltipMargin}
|
|
3823
|
+
crosshairFade={scrubCfg.crosshairFade}
|
|
3824
|
+
crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
|
|
3825
|
+
lineTop={crosshair.tooltipLineTop}
|
|
3826
|
+
scrubDotY={crosshair.scrubDotY}
|
|
3827
|
+
/>
|
|
3828
|
+
) : null}
|
|
3829
|
+
{renderOverlay ? <ChartCustomConsumerOverlay model={model} /> : null}
|
|
3830
|
+
</>
|
|
3831
|
+
);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
function ChartView({
|
|
3835
|
+
model,
|
|
3836
|
+
yAxisEntries,
|
|
3837
|
+
degen,
|
|
3838
|
+
}: {
|
|
3839
|
+
model: LiveChartModel;
|
|
3840
|
+
yAxisEntries: YAxisEntries | null;
|
|
3841
|
+
degen: DegenState | null;
|
|
3842
|
+
}) {
|
|
3843
|
+
const {
|
|
3844
|
+
rootGesture,
|
|
3845
|
+
backgroundColor,
|
|
3846
|
+
style,
|
|
3847
|
+
onLayout,
|
|
3848
|
+
accessibilityLabel,
|
|
3849
|
+
accessibilityRole,
|
|
2781
3850
|
} = model;
|
|
2782
3851
|
|
|
2783
3852
|
return (
|
|
@@ -2789,133 +3858,8 @@ function ChartView({
|
|
|
2789
3858
|
accessibilityLabel={accessibilityLabel}
|
|
2790
3859
|
accessibilityRole={accessibilityRole}
|
|
2791
3860
|
>
|
|
2792
|
-
{
|
|
2793
|
-
<
|
|
2794
|
-
key={canvasMode}
|
|
2795
|
-
style={{ flex: 1 }}
|
|
2796
|
-
opaque={canvasMode === "opaque"}
|
|
2797
|
-
>
|
|
2798
|
-
{canvasMode === "opaque" && (
|
|
2799
|
-
<Rect
|
|
2800
|
-
x={0}
|
|
2801
|
-
y={0}
|
|
2802
|
-
width={engine.canvasWidth}
|
|
2803
|
-
height={engine.canvasHeight}
|
|
2804
|
-
color={backgroundColor}
|
|
2805
|
-
/>
|
|
2806
|
-
)}
|
|
2807
|
-
{/* Background fills first, then the left-edge fade (a canvas-space sibling
|
|
2808
|
-
so dstOut blends correctly), then the line stack on top — so the fade
|
|
2809
|
-
softens only the fills and the line stays crisp at the left edge. */}
|
|
2810
|
-
<ChartFillLayer
|
|
2811
|
-
model={model}
|
|
2812
|
-
yAxisEntries={yAxisEntries}
|
|
2813
|
-
degen={degen}
|
|
2814
|
-
/>
|
|
2815
|
-
|
|
2816
|
-
{leftEdgeFadeCfg && (
|
|
2817
|
-
<LeftEdgeFade
|
|
2818
|
-
paddingLeft={effectivePadding.left}
|
|
2819
|
-
fadeWidth={leftEdgeFadeCfg.width}
|
|
2820
|
-
startColor={leftEdgeFadeCfg.startColor}
|
|
2821
|
-
endColor={leftEdgeFadeCfg.endColor}
|
|
2822
|
-
engine={engine}
|
|
2823
|
-
opaqueBackgroundRgb={
|
|
2824
|
-
canvasMode === "opaque" ? palette.bgRgb : undefined
|
|
2825
|
-
}
|
|
2826
|
-
/>
|
|
2827
|
-
)}
|
|
2828
|
-
|
|
2829
|
-
{/* Line stack above the fade so the line stays crisp at the left edge. */}
|
|
2830
|
-
<ChartStack model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
2831
|
-
|
|
2832
|
-
{/* "extrema-edge" connector lines (dot → edge readout), above the chart
|
|
2833
|
-
content so the dashed guide reads over the line / candles. */}
|
|
2834
|
-
{(topConnector || bottomConnector) && (
|
|
2835
|
-
<ExtremaConnectorOverlay
|
|
2836
|
-
engine={engine}
|
|
2837
|
-
padding={effectivePadding}
|
|
2838
|
-
extremaTimeOffset={extremaTimeOffset}
|
|
2839
|
-
top={topConnector}
|
|
2840
|
-
bottom={bottomConnector}
|
|
2841
|
-
/>
|
|
2842
|
-
)}
|
|
2843
|
-
|
|
2844
|
-
{/* Reference-line badges + labels above the fade so they stay crisp. */}
|
|
2845
|
-
<ChartRefBadgeLayer
|
|
2846
|
-
model={model}
|
|
2847
|
-
degen={degen}
|
|
2848
|
-
yAxisEntries={yAxisEntries}
|
|
2849
|
-
/>
|
|
2850
|
-
|
|
2851
|
-
<ChartValueOverlay model={model} degen={degen} />
|
|
2852
|
-
|
|
2853
|
-
{model.tradeStreamResolved && model.tradeStream && (
|
|
2854
|
-
<ChartTradeStreamLayer model={model} degen={degen} />
|
|
2855
|
-
)}
|
|
2856
|
-
|
|
2857
|
-
<ChartScrubLayer model={model} degen={degen} />
|
|
2858
|
-
|
|
2859
|
-
{/* Live-price badge on top of the scrub dim so the dim never clips
|
|
2860
|
-
its left edge (the badge tracks the live value, not the scrub). */}
|
|
2861
|
-
{model.badgeCfg && <ChartBadgeLayer model={model} degen={degen} />}
|
|
2862
|
-
|
|
2863
|
-
{/* Scrub-action reticle + action badge — top-most, no shake transform. */}
|
|
2864
|
-
<ChartScrubActionLayer model={model} />
|
|
2865
|
-
</Canvas>
|
|
2866
|
-
|
|
2867
|
-
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN view).
|
|
2868
|
-
Pinned to the plot's top/bottom edges via the resolved padding. */}
|
|
2869
|
-
{(topLabelCfg || bottomLabelCfg) && (
|
|
2870
|
-
<AxisLabelOverlay
|
|
2871
|
-
topLabel={topLabelCfg}
|
|
2872
|
-
bottomLabel={bottomLabelCfg}
|
|
2873
|
-
engine={engine}
|
|
2874
|
-
formatValue={formatValue}
|
|
2875
|
-
defaultColor={palette.gridLabel}
|
|
2876
|
-
padding={effectivePadding}
|
|
2877
|
-
extremaTimeOffset={extremaTimeOffset}
|
|
2878
|
-
/>
|
|
2879
|
-
)}
|
|
2880
|
-
|
|
2881
|
-
{/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
|
|
2882
|
-
pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
|
|
2883
|
-
a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
|
|
2884
|
-
Skia markers (the wrapper is full-bleed; children keep their own
|
|
2885
|
-
absolute positions). */}
|
|
2886
|
-
{((markersActive && renderMarker) ||
|
|
2887
|
-
((renderReferenceLine || renderOffAxisReferenceLine) &&
|
|
2888
|
-
allRefLines.length > 0)) && (
|
|
2889
|
-
<ChartCustomAnnotations model={model} />
|
|
2890
|
-
)}
|
|
2891
|
-
|
|
2892
|
-
{/* Custom scrub tooltip — an RN view floated over the canvas (non-Skia),
|
|
2893
|
-
positioned on the UI thread. Sibling of <Canvas>. Works in line and
|
|
2894
|
-
candle mode (candle exposes the OHLC via `scrubCandle`). */}
|
|
2895
|
-
{scrubCfg && renderTooltip && (
|
|
2896
|
-
<CustomTooltipOverlay
|
|
2897
|
-
renderTooltip={renderTooltip}
|
|
2898
|
-
scrubX={crosshair.scrubX}
|
|
2899
|
-
scrubValue={crosshair.scrubValue}
|
|
2900
|
-
scrubTime={crosshair.scrubTime}
|
|
2901
|
-
scrubActive={crosshair.scrubActive}
|
|
2902
|
-
scrubCandle={crosshair.scrubCandle}
|
|
2903
|
-
tooltipLayout={crosshair.tooltipLayout}
|
|
2904
|
-
engine={engine}
|
|
2905
|
-
padding={effectivePadding}
|
|
2906
|
-
placement={scrubCfg.tooltipPlacement}
|
|
2907
|
-
margin={scrubCfg.tooltipMargin}
|
|
2908
|
-
crosshairFade={scrubCfg.crosshairFade}
|
|
2909
|
-
crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
|
|
2910
|
-
lineTop={crosshair.tooltipLineTop}
|
|
2911
|
-
scrubDotY={crosshair.scrubDotY}
|
|
2912
|
-
/>
|
|
2913
|
-
)}
|
|
2914
|
-
|
|
2915
|
-
{/* Custom consumer overlay — an RN view tree floated over the canvas with
|
|
2916
|
-
the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
|
|
2917
|
-
tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
|
|
2918
|
-
{renderOverlay && <ChartCustomConsumerOverlay model={model} />}
|
|
3861
|
+
<ChartCanvas model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
3862
|
+
<ChartNativeOverlays model={model} />
|
|
2919
3863
|
</View>
|
|
2920
3864
|
</GestureDetector>
|
|
2921
3865
|
);
|