react-native-livechart 4.21.0 → 4.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/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 +3 -1
- package/dist/components/CustomThresholdBadgeOverlay.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 +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdLineOverlay.d.ts +3 -1
- package/dist/components/ThresholdLineOverlay.d.ts.map +1 -1
- package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -1
- package/dist/core/chartDataPresence.d.ts +8 -0
- package/dist/core/chartDataPresence.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +3 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +2 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +4 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +3 -2
- package/dist/draw/markerAtlas.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.map +1 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useLiveChartHasData.d.ts +2 -2
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -1
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +10 -5
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/math/referenceLineSeries.d.ts +1 -1
- package/dist/math/referenceLineSeries.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +4 -3
- package/dist/math/threshold.d.ts.map +1 -1
- package/dist/types.d.ts +23 -8
- 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 +11 -4
- package/src/components/DegenParticlesOverlay.tsx +34 -23
- package/src/components/ExtremaConnectorOverlay.tsx +43 -3
- package/src/components/LiveChart.tsx +1709 -1025
- package/src/components/LiveChartSeries.tsx +445 -296
- package/src/components/MarkerOverlay.tsx +215 -156
- package/src/components/ReferenceLineOverlay.tsx +77 -50
- package/src/components/ReferenceLineSeriesOverlay.tsx +2 -0
- package/src/components/ThresholdLineOverlay.tsx +16 -6
- package/src/components/thresholdSplitShaderSource.ts +4 -1
- package/src/core/chartDataPresence.ts +24 -0
- package/src/core/liveChartEngineTick.ts +4 -0
- package/src/core/resolveConfig.ts +3 -0
- package/src/core/useLiveChartEngine.ts +16 -3
- package/src/core/useLiveChartSeriesEngine.ts +2 -0
- package/src/draw/markerAtlas.ts +8 -3
- package/src/hooks/useChartReveal.ts +1 -1
- package/src/hooks/useCrosshair.ts +69 -46
- package/src/hooks/useDegen.ts +82 -53
- package/src/hooks/useLiveChartHasData.ts +8 -4
- package/src/hooks/useReferenceLineSeries.ts +5 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
- package/src/hooks/useThreshold.ts +41 -15
- package/src/math/referenceLineSeries.ts +9 -4
- package/src/math/threshold.ts +10 -3
- package/src/types.ts +23 -8
|
@@ -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,
|
|
@@ -87,7 +86,11 @@ import {
|
|
|
87
86
|
computeCandleFocusClip,
|
|
88
87
|
HIDDEN_CANDLE_FOCUS_CLIP,
|
|
89
88
|
} from "../draw/candle";
|
|
90
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
dotGlowRadialOutset,
|
|
91
|
+
pulseRadialOutset,
|
|
92
|
+
type ChartPadding,
|
|
93
|
+
} from "../draw/line";
|
|
91
94
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
92
95
|
import { useBadge } from "../hooks/useBadge";
|
|
93
96
|
import { useCandleGapPaths } from "../hooks/useCandleGapPaths";
|
|
@@ -269,170 +272,119 @@ function thresholdSplitColorVecs(
|
|
|
269
272
|
/** Transparent vec4 — the band shader's "threshold ended" rest color. */
|
|
270
273
|
const TRANSPARENT_VEC4 = [0, 0, 0, 0];
|
|
271
274
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* small and presentational.
|
|
277
|
-
*/
|
|
278
|
-
|
|
279
|
-
function useLiveChartController({
|
|
280
|
-
// ── Data ────────────────────────────────────────────────────────────────
|
|
281
|
-
data,
|
|
282
|
-
value,
|
|
283
|
-
|
|
284
|
-
// ── Appearance ──────────────────────────────────────────────────────────
|
|
285
|
-
theme = "dark",
|
|
286
|
-
accentColor = DEFAULT_ACCENT_COLOR,
|
|
287
|
-
gradient = true,
|
|
288
|
-
areaDots,
|
|
289
|
-
line: lineProp,
|
|
290
|
-
font: fontProp,
|
|
291
|
-
insets,
|
|
292
|
-
style,
|
|
293
|
-
seriesOpacity,
|
|
294
|
-
canvasMode = "transparent",
|
|
295
|
-
|
|
296
|
-
// ── Candlestick ─────────────────────────────────────────────────────────
|
|
297
|
-
mode = "line",
|
|
298
|
-
candles,
|
|
299
|
-
candleWidth = 60,
|
|
300
|
-
liveCandle,
|
|
301
|
-
candleGaps,
|
|
302
|
-
lineGaps,
|
|
303
|
-
volume,
|
|
304
|
-
|
|
305
|
-
// ── Behaviour ───────────────────────────────────────────────────────────
|
|
306
|
-
timeWindow = 30,
|
|
307
|
-
paused = false,
|
|
308
|
-
loading = false,
|
|
309
|
-
transitions,
|
|
310
|
-
// `static` is a reserved word — alias it so the destructure parses.
|
|
311
|
-
static: isStatic = false,
|
|
312
|
-
snapKey,
|
|
313
|
-
smoothing = 0.08,
|
|
314
|
-
exaggerate = false,
|
|
315
|
-
nonNegative = false,
|
|
316
|
-
maxValue,
|
|
317
|
-
yRangeScale,
|
|
318
|
-
windowBuffer = 0,
|
|
319
|
-
nowOverride,
|
|
320
|
-
timeScroll = false,
|
|
321
|
-
returnToLive,
|
|
322
|
-
zoom = false,
|
|
323
|
-
accessibilityLabel,
|
|
324
|
-
accessibilityRole = "image",
|
|
325
|
-
emptyText = "No data",
|
|
326
|
-
formatValue = defaultFormatValue,
|
|
327
|
-
formatTime = defaultFormatTime,
|
|
328
|
-
|
|
329
|
-
// ── Overlays ────────────────────────────────────────────────────────────
|
|
330
|
-
yAxis = true,
|
|
331
|
-
xAxis = true,
|
|
332
|
-
axisAutoHide = false,
|
|
275
|
+
function resolveLiveChartFeatureConfig({
|
|
276
|
+
mode,
|
|
277
|
+
yAxis,
|
|
278
|
+
xAxis,
|
|
333
279
|
topLabel,
|
|
334
280
|
bottomLabel,
|
|
335
|
-
badge
|
|
336
|
-
|
|
337
|
-
pulse = true,
|
|
338
|
-
dot,
|
|
339
|
-
valueLine = true,
|
|
340
|
-
showValue = false,
|
|
341
|
-
valueMomentumColor = false,
|
|
342
|
-
referenceLines,
|
|
343
|
-
segments,
|
|
344
|
-
threshold,
|
|
345
|
-
gridStyle,
|
|
346
|
-
palette: paletteOverride,
|
|
347
|
-
metrics,
|
|
348
|
-
scrub = true,
|
|
281
|
+
badge,
|
|
282
|
+
scrub,
|
|
349
283
|
scrubAction,
|
|
284
|
+
volume,
|
|
285
|
+
candleGaps,
|
|
286
|
+
lineGaps,
|
|
287
|
+
gradient,
|
|
288
|
+
areaDots,
|
|
289
|
+
threshold,
|
|
290
|
+
valueLine,
|
|
291
|
+
isStatic,
|
|
292
|
+
pulse,
|
|
293
|
+
dot,
|
|
350
294
|
selectionDot,
|
|
351
|
-
|
|
295
|
+
gridStyle,
|
|
352
296
|
degen,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// Stand-in threshold value so `useThreshold` can be called unconditionally
|
|
381
|
-
// (hooks can't be); the geometry is ignored when no threshold is configured.
|
|
382
|
-
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 }) {
|
|
383
324
|
const isCandle = mode === "candle";
|
|
384
|
-
|
|
385
|
-
// ── Resolve feature configs ────────────────────────────────────────────
|
|
386
|
-
const yAxisCfg = resolveYAxis(yAxis);
|
|
387
|
-
const xAxisCfg = resolveXAxis(xAxis);
|
|
388
|
-
const topLabelCfg = resolveAxisLabel(topLabel);
|
|
389
|
-
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
390
325
|
const badgeCfg = resolveBadge(badge);
|
|
391
|
-
const scrubCfg = resolveScrub(scrub);
|
|
392
|
-
// Scrub + scrub-action are on-demand touch gestures (event-driven, no per-frame
|
|
393
|
-
// loop), so they stay live on static charts — `static` suppresses the continuous
|
|
394
|
-
// render loop, not interaction. A still sparkline in a list can still be scrubbed.
|
|
395
|
-
const scrubActionCfg = resolveScrubAction(scrubAction);
|
|
396
|
-
// Volume bars sit below the candles — a candle-mode-only feature (inert in
|
|
397
|
-
// line mode, like the candle paths themselves).
|
|
398
|
-
const volumeCfg = isCandle ? resolveVolume(volume) : null;
|
|
399
326
|
const chartGapsCfg = resolveCandleGaps(isCandle ? candleGaps : lineGaps);
|
|
400
|
-
const candleGapsCfg = isCandle ? chartGapsCfg : null;
|
|
401
|
-
const lineGapsCfg = isCandle ? null : chartGapsCfg;
|
|
402
|
-
const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
|
|
403
|
-
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
404
|
-
// Dot-lattice area fill (clipped to the under-line region). Inert in candle
|
|
405
|
-
// mode, same as the gradient fill.
|
|
406
|
-
const areaDotsCfg = isCandle ? null : resolveAreaDots(areaDots);
|
|
407
|
-
// Threshold split is a line-mode feature (candle bodies carry their own up/down
|
|
408
|
-
// colors), so it's inert in candle mode — same as the area gradient.
|
|
409
327
|
const thresholdCfg = isCandle ? null : resolveThreshold(threshold);
|
|
410
328
|
const thresholdSeriesSV = thresholdCfg?.series ?? null;
|
|
411
|
-
const thresholdIsSeries =
|
|
412
|
-
thresholdSeriesSV !== null || Array.isArray(thresholdCfg?.value);
|
|
413
|
-
const valueLineCfg = resolveValueLine(valueLine);
|
|
414
|
-
// Static charts run zero loops: force the pulse off so its `withRepeat`-driven
|
|
415
|
-
// ring never starts (the DotOverlay reads `pulseCfg`, so null = no pulse).
|
|
416
|
-
const pulseCfg = isStatic ? null : resolvePulse(pulse);
|
|
417
329
|
const dotCfg = resolveDot(dot);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
330
|
+
const volumeCfg = isCandle ? resolveVolume(volume) : null;
|
|
331
|
+
|
|
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
|
+
}
|
|
435
370
|
|
|
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
|
+
}) {
|
|
436
388
|
const chartGapBands: ReferenceLine[] =
|
|
437
389
|
chartGapsCfg?.gaps.flatMap((gap) => {
|
|
438
390
|
const gapStyle = chartGapsCfg.styles[gap.kind];
|
|
@@ -459,54 +411,12 @@ function useLiveChartController({
|
|
|
459
411
|
},
|
|
460
412
|
];
|
|
461
413
|
}) ?? [];
|
|
462
|
-
// Gap bands append after consumer reference lines so public line indices stay
|
|
463
|
-
// stable for callbacks. Time bands contribute no Y values or press targets.
|
|
464
|
-
const allRefLines = [...(referenceLines ?? []), ...chartGapBands];
|
|
465
|
-
const refValues = collectReferenceValues(allRefLines);
|
|
466
|
-
|
|
467
|
-
// Per-line live value overrides + drag flags for draggable lines and the custom
|
|
468
|
-
// `renderReferenceLine` slot. One array SharedValue each (the line count varies,
|
|
469
|
-
// so a single SharedValue beats N hooks). Seeded from each line's static `value`;
|
|
470
|
-
// the drag gesture overwrites a slot, and the effect re-seeds slots not being
|
|
471
|
-
// dragged when the props change (so a controlled `value` flows back in).
|
|
472
|
-
const dragValues = useSharedValue<number[]>([]);
|
|
473
|
-
const dragActive = useSharedValue<boolean[]>([]);
|
|
474
|
-
// Last `value` props used to seed each slot, so reconciliation can tell a
|
|
475
|
-
// controlled prop change (adopt it) from an unchanged prop (keep the dragged
|
|
476
|
-
// value — uncontrolled persistence) without resetting a drop on every re-render.
|
|
477
|
-
const seededRef = useRef<(number | undefined)[]>([]);
|
|
478
|
-
const refValueSig = allRefLines.map((l) => l.value ?? "_").join(",");
|
|
479
|
-
useEffect(() => {
|
|
480
|
-
const active = dragActive.get();
|
|
481
|
-
const cur = dragValues.get();
|
|
482
|
-
const seeded = seededRef.current;
|
|
483
|
-
dragValues.set(
|
|
484
|
-
allRefLines.map((l, i) => {
|
|
485
|
-
const prop = l.value ?? 0;
|
|
486
|
-
if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
|
|
487
|
-
if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
|
|
488
|
-
return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
|
|
489
|
-
}),
|
|
490
|
-
);
|
|
491
|
-
seededRef.current = allRefLines.map((l) => l.value);
|
|
492
|
-
if (dragActive.get().length !== allRefLines.length) {
|
|
493
|
-
dragActive.set(allRefLines.map((_, i) => active[i] ?? false));
|
|
494
|
-
}
|
|
495
|
-
// allRefLines is rebuilt every render; key off the value signature + length.
|
|
496
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
497
|
-
}, [refValueSig, allRefLines.length]);
|
|
498
|
-
|
|
499
|
-
// Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
|
|
500
|
-
// (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
|
|
501
|
-
// Semantic gap bands are library-owned annotations: do not leak them through
|
|
502
|
-
// the consumer's reference-line renderer or change its callback cardinality.
|
|
503
414
|
const consumerRefLines = referenceLines ?? [];
|
|
415
|
+
const allRefLines = [...consumerRefLines, ...chartGapBands];
|
|
504
416
|
const refLineCustom = [
|
|
505
417
|
...customReferenceLineFlags(consumerRefLines, renderReferenceLine),
|
|
506
418
|
...chartGapBands.map(() => false),
|
|
507
419
|
];
|
|
508
|
-
// An off-axis renderer replaces only the edge-pinned tag. A full custom tag
|
|
509
|
-
// takes precedence for the same line to avoid mounting two native tags.
|
|
510
420
|
const refLineOffAxisCustom = [
|
|
511
421
|
...customReferenceLineFlags(
|
|
512
422
|
consumerRefLines,
|
|
@@ -515,123 +425,169 @@ function useLiveChartController({
|
|
|
515
425
|
),
|
|
516
426
|
...chartGapBands.map(() => false),
|
|
517
427
|
].map((custom, index) => custom && !refLineCustom[index]);
|
|
518
|
-
const refLineKeys = referenceLineReactKeys(allRefLines);
|
|
519
|
-
// RN custom tags report their measured widths here so the Skia connector can
|
|
520
|
-
// start after the native badge instead of the hidden built-in pill.
|
|
521
|
-
const refLineCustomTagWidths = useSharedValue<number[]>([]);
|
|
522
|
-
|
|
523
|
-
// Live Y values of the *draggable* Form-A lines, folded into the engine's
|
|
524
|
-
// axis-range fit so dragging a line toward / past the visible edge expands the
|
|
525
|
-
// range and the axis follows the finger in one motion (the committed values are
|
|
526
|
-
// already in `refValues`). `excludeFromRange` lines opt out, matching the static
|
|
527
|
-
// fit. Identity-stable (no re-fit) when nothing is draggable.
|
|
528
|
-
//
|
|
529
|
-
// `threshold.includeInRange` rides the same channel: the constant benchmark
|
|
530
|
-
// contributes its live value; a series contributes its window min/max
|
|
531
|
-
// (respecting `extendToNow`), so an off-range break-even expands the axis and
|
|
532
|
-
// stays on-plot like a reference line would.
|
|
533
428
|
const draggableRefIdx: number[] = [];
|
|
534
|
-
for (let
|
|
535
|
-
const line = allRefLines[
|
|
429
|
+
for (let index = 0; index < allRefLines.length; index++) {
|
|
430
|
+
const line = allRefLines[index];
|
|
536
431
|
if (
|
|
537
432
|
line.draggable &&
|
|
538
433
|
!line.excludeFromRange &&
|
|
539
434
|
referenceLineForm(line) === "line"
|
|
540
435
|
) {
|
|
541
|
-
draggableRefIdx.push(
|
|
436
|
+
draggableRefIdx.push(index);
|
|
542
437
|
}
|
|
543
438
|
}
|
|
544
439
|
const thresholdInRange = thresholdCfg?.includeInRange === true;
|
|
545
|
-
// Constant benchmark → its live value rides this channel. A series threshold
|
|
546
|
-
// is folded inside the engine tick instead (`thresholdRangePoints`), which
|
|
547
|
-
// already knows the window bounds for the min/max.
|
|
548
440
|
const thresholdRangeValueSV =
|
|
549
441
|
thresholdInRange &&
|
|
550
442
|
thresholdCfg?.value != null &&
|
|
551
443
|
!Array.isArray(thresholdCfg.value)
|
|
552
444
|
? thresholdCfg.value
|
|
553
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[]>([]);
|
|
554
506
|
const liveRefValues = useDerivedValue<number[]>(() => {
|
|
555
|
-
if (draggableRefIdx.length === 0 && thresholdRangeValueSV === null)
|
|
507
|
+
if (draggableRefIdx.length === 0 && thresholdRangeValueSV === null) {
|
|
556
508
|
return EMPTY_NUMS;
|
|
557
|
-
|
|
509
|
+
}
|
|
510
|
+
const output: number[] = [];
|
|
558
511
|
if (draggableRefIdx.length > 0) {
|
|
559
|
-
const
|
|
560
|
-
for (let
|
|
561
|
-
const
|
|
562
|
-
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);
|
|
563
516
|
}
|
|
564
517
|
}
|
|
565
518
|
if (thresholdRangeValueSV !== null) {
|
|
566
|
-
const
|
|
567
|
-
if (Number.isFinite(
|
|
519
|
+
const value = thresholdRangeValueSV.get();
|
|
520
|
+
if (Number.isFinite(value)) output.push(value);
|
|
568
521
|
}
|
|
569
|
-
return
|
|
522
|
+
return output;
|
|
570
523
|
});
|
|
571
524
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
+
}) {
|
|
590
559
|
const palette = applyPaletteOverride(
|
|
591
560
|
resolveTheme(accentColor, theme),
|
|
592
561
|
paletteOverride,
|
|
593
562
|
);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
// reference lines; the divider/label/muted colors default to the chart palette
|
|
597
|
-
// (no per-segment base color needed). `hasRecolorSegments` is a render-time gate
|
|
598
|
-
// for the line-recolor gradient pass (per-frame visibility is handled by the
|
|
599
|
-
// gradient's transparent stops, not by mounting/unmounting the Path).
|
|
600
|
-
const resolvedSegments = (segments ?? []).map((s) =>
|
|
601
|
-
resolveSegment(s, {
|
|
563
|
+
const resolvedSegments = (segments ?? []).map((segment) =>
|
|
564
|
+
resolveSegment(segment, {
|
|
602
565
|
muted: palette.gridLabel,
|
|
603
566
|
divider: palette.refLine,
|
|
604
567
|
label: palette.refLabel,
|
|
605
568
|
}),
|
|
606
569
|
);
|
|
607
|
-
const hasRecolorSegments = resolvedSegments.some((s) => s.recolorLine);
|
|
608
|
-
|
|
609
|
-
const leftEdgeFadeCfg = resolveLeftEdgeFade(
|
|
610
|
-
leftEdgeFade,
|
|
611
|
-
leftEdgeFadeColorsFromBgRgb(palette.bgRgb),
|
|
612
|
-
);
|
|
613
|
-
|
|
614
|
-
const skiaFont = useChartSkiaFont(
|
|
615
|
-
fontProp,
|
|
616
|
-
MONO_FONT_FAMILY,
|
|
617
|
-
palette.labelFontSize,
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
// Larger font for the optional live-value text overlay (showValue).
|
|
621
|
-
const valueFont = useChartSkiaFont(
|
|
622
|
-
fontProp,
|
|
623
|
-
MONO_FONT_FAMILY,
|
|
624
|
-
palette.valueFontSize * 2,
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
// Per-badge font (size/family/weight) override; reuses the chart font when
|
|
628
|
-
// none of the badge font knobs are set, so the gutter sizing is unchanged.
|
|
629
570
|
const badgeHasFontOverride =
|
|
630
571
|
badgeCfg?.fontSize != null ||
|
|
631
572
|
badgeCfg?.fontFamily != null ||
|
|
632
573
|
badgeCfg?.fontWeight != null;
|
|
633
|
-
const
|
|
634
|
-
|
|
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
|
|
635
591
|
? {
|
|
636
592
|
...fontProp,
|
|
637
593
|
fontFamily: badgeCfg?.fontFamily ?? fontProp?.fontFamily,
|
|
@@ -639,18 +595,8 @@ function useLiveChartController({
|
|
|
639
595
|
fontWeight: badgeCfg?.fontWeight ?? fontProp?.fontWeight,
|
|
640
596
|
}
|
|
641
597
|
: fontProp,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
);
|
|
645
|
-
const badgeFont = badgeHasFontOverride ? badgeFontOverride : skiaFont;
|
|
646
|
-
|
|
647
|
-
// Per-badge font for the grouping count pill (same override pattern as above).
|
|
648
|
-
const refGroupBadgeHasFont =
|
|
649
|
-
refGroupBadge.fontSize != null ||
|
|
650
|
-
refGroupBadge.fontFamily != null ||
|
|
651
|
-
refGroupBadge.fontWeight != null;
|
|
652
|
-
const refGroupBadgeFontOverride = useChartSkiaFont(
|
|
653
|
-
refGroupBadgeHasFont
|
|
598
|
+
refGroupBadgeHasFont,
|
|
599
|
+
refGroupBadgeFontConfig: refGroupBadgeHasFont
|
|
654
600
|
? {
|
|
655
601
|
...fontProp,
|
|
656
602
|
fontFamily: refGroupBadge.fontFamily ?? fontProp?.fontFamily,
|
|
@@ -658,82 +604,1072 @@ function useLiveChartController({
|
|
|
658
604
|
fontWeight: refGroupBadge.fontWeight ?? fontProp?.fontWeight,
|
|
659
605
|
}
|
|
660
606
|
: fontProp,
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
}
|
|
667
622
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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);
|
|
672
661
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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?.extendToStart ?? true,
|
|
676
|
+
thresholdCfg?.line?.labelAnchor ?? "last",
|
|
677
|
+
),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function useThresholdShaderUniforms({
|
|
682
|
+
engine,
|
|
683
|
+
padding,
|
|
684
|
+
samples,
|
|
685
|
+
clipLeftX,
|
|
686
|
+
clipRightX,
|
|
687
|
+
thresholdCfg,
|
|
688
|
+
palette,
|
|
689
|
+
lineProp,
|
|
690
|
+
}: {
|
|
691
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
692
|
+
padding: ChartPadding;
|
|
693
|
+
samples: SharedValue<number[]>;
|
|
694
|
+
clipLeftX: SharedValue<number>;
|
|
695
|
+
clipRightX: SharedValue<number>;
|
|
696
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
697
|
+
palette: LiveChartPalette;
|
|
698
|
+
lineProp: LiveChartProps["line"];
|
|
699
|
+
}) {
|
|
700
|
+
const { stopColors, splitAbove, splitBelow, fillOpacity, lineColor } =
|
|
701
|
+
resolveThresholdColorConfig(thresholdCfg, palette, lineProp);
|
|
702
|
+
const vectors = useMemo(() => {
|
|
703
|
+
if (splitAbove === null || splitBelow === null) return null;
|
|
704
|
+
const [r, g, b, a] = parseColorRgba(lineColor);
|
|
705
|
+
return {
|
|
706
|
+
...thresholdSplitColorVecs(splitAbove, splitBelow, fillOpacity),
|
|
707
|
+
strokeRest: [r / 255, g / 255, b / 255, a],
|
|
708
|
+
};
|
|
709
|
+
}, [splitAbove, splitBelow, fillOpacity, lineColor]);
|
|
710
|
+
return {
|
|
711
|
+
thresholdStrokeColors: stopColors?.stroke ?? null,
|
|
712
|
+
thresholdFillColors: stopColors?.fill ?? null,
|
|
713
|
+
thresholdStrokeUniforms: useThresholdSplitUniforms(
|
|
714
|
+
samples,
|
|
715
|
+
engine,
|
|
716
|
+
padding,
|
|
717
|
+
vectors?.strokeAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
718
|
+
vectors?.strokeBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
719
|
+
vectors?.strokeRest ?? THRESHOLD_FALLBACK_COLOR,
|
|
720
|
+
clipLeftX,
|
|
721
|
+
clipRightX,
|
|
722
|
+
),
|
|
723
|
+
thresholdFillUniforms: useThresholdSplitUniforms(
|
|
724
|
+
samples,
|
|
725
|
+
engine,
|
|
726
|
+
padding,
|
|
727
|
+
vectors?.fillAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
728
|
+
vectors?.fillBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
729
|
+
TRANSPARENT_VEC4,
|
|
730
|
+
clipLeftX,
|
|
731
|
+
clipRightX,
|
|
732
|
+
),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function useThresholdBadgeProjection({
|
|
737
|
+
thresholdCfg,
|
|
738
|
+
thresholdIsSeries,
|
|
739
|
+
thresholdGeom,
|
|
740
|
+
thresholdSeriesGeom,
|
|
741
|
+
renderThresholdBadge,
|
|
742
|
+
formatValue,
|
|
743
|
+
}: {
|
|
744
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
745
|
+
thresholdIsSeries: boolean;
|
|
746
|
+
thresholdGeom: ReturnType<typeof useThreshold>;
|
|
747
|
+
thresholdSeriesGeom: ReturnType<typeof useThresholdSeries>;
|
|
748
|
+
renderThresholdBadge: LiveChartProps["renderThresholdBadge"];
|
|
749
|
+
formatValue: (value: number) => string;
|
|
750
|
+
}) {
|
|
751
|
+
const lineY = thresholdIsSeries
|
|
752
|
+
? thresholdSeriesGeom.badgeLineY
|
|
753
|
+
: thresholdGeom.lineY;
|
|
754
|
+
const markerVisible = thresholdIsSeries
|
|
755
|
+
? thresholdSeriesGeom.visible
|
|
756
|
+
: thresholdGeom.visible;
|
|
757
|
+
const badgeVisible = thresholdIsSeries
|
|
758
|
+
? thresholdSeriesGeom.badgeVisible
|
|
759
|
+
: thresholdGeom.visible;
|
|
760
|
+
const markerValue =
|
|
761
|
+
thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
|
|
762
|
+
? (thresholdCfg.value ?? thresholdSeriesGeom.badgeValue)
|
|
763
|
+
: thresholdSeriesGeom.badgeValue;
|
|
764
|
+
const custom = thresholdCfg?.line != null && renderThresholdBadge != null;
|
|
765
|
+
const valueStr = useDerivedValue(() =>
|
|
766
|
+
custom ? formatValue(markerValue.get()) : "",
|
|
767
|
+
);
|
|
768
|
+
return {
|
|
769
|
+
thresholdMarkerLineY: lineY,
|
|
770
|
+
thresholdMarkerVisible: markerVisible,
|
|
771
|
+
thresholdBadgeVisible: badgeVisible,
|
|
772
|
+
thresholdMarkerValue: markerValue,
|
|
773
|
+
thresholdCustomBadge:
|
|
774
|
+
thresholdCfg?.line && renderThresholdBadge
|
|
775
|
+
? renderThresholdBadge({
|
|
776
|
+
line: thresholdCfg.line,
|
|
777
|
+
value: markerValue,
|
|
778
|
+
valueStr,
|
|
779
|
+
y: lineY,
|
|
780
|
+
visible: badgeVisible,
|
|
781
|
+
})
|
|
782
|
+
: null,
|
|
783
|
+
thresholdSeriesPts: thresholdIsSeries
|
|
784
|
+
? thresholdSeriesGeom.screenPts
|
|
785
|
+
: undefined,
|
|
786
|
+
thresholdBadgeLeftAnchorX:
|
|
787
|
+
thresholdIsSeries && thresholdCfg?.extendToStart === false
|
|
788
|
+
? thresholdSeriesGeom.clipLeftX
|
|
789
|
+
: undefined,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function useLiveChartThresholdModel({
|
|
794
|
+
engine,
|
|
795
|
+
padding,
|
|
796
|
+
thresholdCfg,
|
|
797
|
+
thresholdSeriesSV,
|
|
798
|
+
thresholdIsSeries,
|
|
799
|
+
emptyThresholdValue,
|
|
800
|
+
palette,
|
|
801
|
+
lineProp,
|
|
802
|
+
renderThresholdBadge,
|
|
803
|
+
formatValue,
|
|
804
|
+
}: {
|
|
805
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
806
|
+
padding: ChartPadding;
|
|
807
|
+
thresholdCfg: ReturnType<typeof resolveThreshold>;
|
|
808
|
+
thresholdSeriesSV: SharedValue<LiveChartPoint[]> | null;
|
|
809
|
+
thresholdIsSeries: boolean;
|
|
810
|
+
emptyThresholdValue: SharedValue<number>;
|
|
811
|
+
palette: LiveChartPalette;
|
|
812
|
+
lineProp: LiveChartProps["line"];
|
|
813
|
+
renderThresholdBadge: LiveChartProps["renderThresholdBadge"];
|
|
814
|
+
formatValue: (value: number) => string;
|
|
815
|
+
}) {
|
|
816
|
+
const { thresholdSeriesHasPoints, thresholdGeom, thresholdSeriesGeom } =
|
|
817
|
+
useThresholdGeometryState({
|
|
818
|
+
engine,
|
|
819
|
+
padding,
|
|
820
|
+
thresholdCfg,
|
|
821
|
+
thresholdSeriesSV,
|
|
822
|
+
emptyThresholdValue,
|
|
823
|
+
});
|
|
824
|
+
const shaderModel = useThresholdShaderUniforms({
|
|
825
|
+
engine,
|
|
826
|
+
padding,
|
|
827
|
+
samples: thresholdSeriesGeom.samples,
|
|
828
|
+
clipLeftX: thresholdSeriesGeom.clipLeftX,
|
|
829
|
+
clipRightX: thresholdSeriesGeom.clipRightX,
|
|
830
|
+
thresholdCfg,
|
|
831
|
+
palette,
|
|
832
|
+
lineProp,
|
|
833
|
+
});
|
|
834
|
+
const badgeModel = useThresholdBadgeProjection({
|
|
835
|
+
thresholdCfg,
|
|
836
|
+
thresholdIsSeries,
|
|
837
|
+
thresholdGeom,
|
|
838
|
+
thresholdSeriesGeom,
|
|
839
|
+
renderThresholdBadge,
|
|
840
|
+
formatValue,
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
thresholdGeom,
|
|
845
|
+
thresholdSeriesHasPoints,
|
|
846
|
+
...shaderModel,
|
|
847
|
+
...badgeModel,
|
|
848
|
+
thresholdFillLineY:
|
|
849
|
+
thresholdCfg?.fill && !thresholdIsSeries
|
|
850
|
+
? thresholdGeom.lineY
|
|
851
|
+
: undefined,
|
|
852
|
+
thresholdFillSamples:
|
|
853
|
+
thresholdCfg?.fill && thresholdSeriesHasPoints
|
|
854
|
+
? thresholdSeriesGeom.samples
|
|
855
|
+
: undefined,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function resolveLiveChartInteractionConfig({
|
|
860
|
+
isCandle,
|
|
861
|
+
mode,
|
|
862
|
+
candlesEngine,
|
|
863
|
+
liveEngine,
|
|
864
|
+
candleWidth,
|
|
865
|
+
candleGapsCfg,
|
|
866
|
+
lineGapsCfg,
|
|
867
|
+
markers,
|
|
868
|
+
markerCluster,
|
|
869
|
+
onReferenceLinePress,
|
|
870
|
+
allRefLines,
|
|
871
|
+
isStatic,
|
|
872
|
+
timeScroll,
|
|
873
|
+
timeScrollEnabled,
|
|
874
|
+
scrubCfg,
|
|
875
|
+
}: {
|
|
876
|
+
isCandle: boolean;
|
|
877
|
+
mode: "line" | "candle";
|
|
878
|
+
candlesEngine: SharedValue<CandlePoint[]>;
|
|
879
|
+
liveEngine: SharedValue<CandlePoint | null>;
|
|
880
|
+
candleWidth: number;
|
|
881
|
+
candleGapsCfg: ResolvedCandleGapsConfig | null;
|
|
882
|
+
lineGapsCfg: ResolvedCandleGapsConfig | null;
|
|
883
|
+
markers: LiveChartProps["markers"];
|
|
884
|
+
markerCluster: LiveChartProps["markerCluster"];
|
|
885
|
+
onReferenceLinePress: LiveChartProps["onReferenceLinePress"];
|
|
886
|
+
allRefLines: ReferenceLine[];
|
|
887
|
+
isStatic: boolean;
|
|
888
|
+
timeScroll: LiveChartProps["timeScroll"];
|
|
889
|
+
timeScrollEnabled: boolean;
|
|
890
|
+
scrubCfg: ReturnType<typeof resolveScrub>;
|
|
891
|
+
}) {
|
|
892
|
+
const crosshairChartOpts = isCandle
|
|
893
|
+
? {
|
|
894
|
+
mode,
|
|
895
|
+
candles: candlesEngine,
|
|
896
|
+
liveCandle: liveEngine,
|
|
897
|
+
candleWidthSecs: candleWidth,
|
|
898
|
+
gaps: candleGapsCfg?.gaps,
|
|
899
|
+
bridgeNoTrades: Boolean(candleGapsCfg?.styles["no-trades"].bridge),
|
|
900
|
+
bridgeUnavailable: Boolean(candleGapsCfg?.styles.unavailable.bridge),
|
|
901
|
+
bridgeUnknown: Boolean(candleGapsCfg?.styles.unknown.bridge),
|
|
902
|
+
}
|
|
903
|
+
: lineGapsCfg
|
|
904
|
+
? {
|
|
905
|
+
mode,
|
|
906
|
+
gaps: lineGapsCfg.gaps,
|
|
907
|
+
bridgeNoTrades: Boolean(lineGapsCfg.styles["no-trades"].bridge),
|
|
908
|
+
bridgeUnavailable: Boolean(lineGapsCfg.styles.unavailable.bridge),
|
|
909
|
+
bridgeUnknown: Boolean(lineGapsCfg.styles.unknown.bridge),
|
|
910
|
+
}
|
|
911
|
+
: undefined;
|
|
912
|
+
const scrollGestureMode =
|
|
913
|
+
typeof timeScroll === "object"
|
|
914
|
+
? (timeScroll.gesture ?? "holdToScrub")
|
|
915
|
+
: "holdToScrub";
|
|
916
|
+
const timeScrollHoldMs =
|
|
917
|
+
typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
crosshairChartOpts,
|
|
921
|
+
markersActive: markers != null,
|
|
922
|
+
markerClusterCfg: resolveMarkerCluster(markerCluster),
|
|
923
|
+
refPressActive: onReferenceLinePress != null && allRefLines.length > 0,
|
|
924
|
+
refDragEnabled:
|
|
925
|
+
!isStatic && allRefLines.some((line) => line.draggable === true),
|
|
926
|
+
scrollGestureMode,
|
|
927
|
+
scrubHoldMs:
|
|
928
|
+
timeScrollEnabled && scrollGestureMode === "holdToScrub"
|
|
929
|
+
? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
|
|
930
|
+
: (scrubCfg?.panGestureDelay ?? 0),
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function composeLiveChartRootGesture({
|
|
935
|
+
crosshair,
|
|
936
|
+
markerTapGesture,
|
|
937
|
+
refLineTapGesture,
|
|
938
|
+
refDragGesture,
|
|
939
|
+
panScrollGesture,
|
|
940
|
+
pinchZoomGesture,
|
|
941
|
+
markersActive,
|
|
942
|
+
refPressActive,
|
|
943
|
+
refDragEnabled,
|
|
944
|
+
scrubActionActive,
|
|
945
|
+
timeScrollEnabled,
|
|
946
|
+
scrollGestureMode,
|
|
947
|
+
zoomEnabled,
|
|
948
|
+
}: {
|
|
949
|
+
crosshair: ReturnType<typeof useCrosshair>;
|
|
950
|
+
markerTapGesture: ReturnType<typeof useMarkers>["tapGesture"];
|
|
951
|
+
refLineTapGesture: ReturnType<typeof useReferenceLinePress>["tapGesture"];
|
|
952
|
+
refDragGesture: ReturnType<typeof useReferenceDrag>["gesture"];
|
|
953
|
+
panScrollGesture: ReturnType<typeof usePanScroll>;
|
|
954
|
+
pinchZoomGesture: ReturnType<typeof usePinchZoom>;
|
|
955
|
+
markersActive: boolean;
|
|
956
|
+
refPressActive: boolean;
|
|
957
|
+
refDragEnabled: boolean;
|
|
958
|
+
scrubActionActive: boolean;
|
|
959
|
+
timeScrollEnabled: boolean;
|
|
960
|
+
scrollGestureMode: "holdToScrub" | "axisDrag";
|
|
961
|
+
zoomEnabled: boolean;
|
|
962
|
+
}) {
|
|
963
|
+
const baseGesture =
|
|
964
|
+
scrubActionActive && crosshair.tapGesture
|
|
965
|
+
? Gesture.Exclusive(crosshair.tapGesture, crosshair.gesture)
|
|
966
|
+
: crosshair.gesture;
|
|
967
|
+
const overlayTaps = [
|
|
968
|
+
markersActive ? markerTapGesture : null,
|
|
969
|
+
refPressActive ? refLineTapGesture : null,
|
|
970
|
+
].filter(
|
|
971
|
+
(gesture): gesture is NonNullable<typeof gesture> => gesture !== null,
|
|
972
|
+
);
|
|
973
|
+
let rootGesture = baseGesture;
|
|
974
|
+
if (overlayTaps.length > 0) {
|
|
975
|
+
const tapGroup =
|
|
976
|
+
overlayTaps.length === 1
|
|
977
|
+
? overlayTaps[0]
|
|
978
|
+
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
979
|
+
rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
|
|
980
|
+
}
|
|
981
|
+
if (timeScrollEnabled) {
|
|
982
|
+
rootGesture =
|
|
983
|
+
scrollGestureMode === "axisDrag"
|
|
984
|
+
? Gesture.Exclusive(panScrollGesture, rootGesture)
|
|
985
|
+
: Gesture.Race(panScrollGesture, rootGesture);
|
|
986
|
+
}
|
|
987
|
+
if (refDragEnabled) {
|
|
988
|
+
rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
|
|
989
|
+
}
|
|
990
|
+
return zoomEnabled
|
|
991
|
+
? Gesture.Simultaneous(rootGesture, pinchZoomGesture)
|
|
992
|
+
: rootGesture;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function useAxisAutoHide({
|
|
996
|
+
config,
|
|
997
|
+
scrollActive,
|
|
998
|
+
scrubActive,
|
|
999
|
+
engine,
|
|
1000
|
+
}: {
|
|
1001
|
+
config: LiveChartProps["axisAutoHide"];
|
|
1002
|
+
scrollActive: SharedValue<boolean>;
|
|
1003
|
+
scrubActive: SharedValue<boolean>;
|
|
1004
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
1005
|
+
}) {
|
|
1006
|
+
const resolved = config === true ? {} : config === false ? null : config;
|
|
1007
|
+
const idleOpacity = resolved?.idleOpacity ?? 0;
|
|
1008
|
+
const fadeInMs = resolved?.fadeInMs ?? 60;
|
|
1009
|
+
const fadeOutMs = resolved?.fadeOutMs ?? 250;
|
|
1010
|
+
const hideAfterMs = resolved?.hideAfterMs ?? 3000;
|
|
1011
|
+
const opacity = useSharedValue(resolved ? idleOpacity : 1);
|
|
1012
|
+
const enabled = resolved !== null;
|
|
1013
|
+
const last = useRef({ enabled, idleOpacity });
|
|
1014
|
+
useEffect(() => {
|
|
1015
|
+
if (
|
|
1016
|
+
last.current.enabled === enabled &&
|
|
1017
|
+
last.current.idleOpacity === idleOpacity
|
|
1018
|
+
) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
last.current = { enabled, idleOpacity };
|
|
1022
|
+
cancelAnimation(opacity);
|
|
1023
|
+
opacity.value = enabled ? idleOpacity : 1;
|
|
1024
|
+
}, [enabled, idleOpacity, opacity]);
|
|
1025
|
+
useAnimatedReaction(
|
|
1026
|
+
() => ({
|
|
1027
|
+
gesture: scrollActive.value || scrubActive.value,
|
|
1028
|
+
viewEnd: engine.viewEnd.value,
|
|
1029
|
+
viewWindow: engine.viewWindow.value,
|
|
1030
|
+
}),
|
|
1031
|
+
(current, previous) => {
|
|
1032
|
+
if (!enabled || previous === null) return;
|
|
1033
|
+
const moved =
|
|
1034
|
+
current.gesture !== previous.gesture ||
|
|
1035
|
+
current.viewEnd !== previous.viewEnd ||
|
|
1036
|
+
current.viewWindow !== previous.viewWindow;
|
|
1037
|
+
if (!moved) return;
|
|
1038
|
+
cancelAnimation(opacity);
|
|
1039
|
+
opacity.value = current.gesture
|
|
1040
|
+
? withTiming(1, { duration: fadeInMs })
|
|
1041
|
+
: withSequence(
|
|
1042
|
+
withTiming(1, { duration: fadeInMs }),
|
|
1043
|
+
withDelay(
|
|
1044
|
+
hideAfterMs,
|
|
1045
|
+
withTiming(idleOpacity, { duration: fadeOutMs }),
|
|
1046
|
+
),
|
|
1047
|
+
);
|
|
1048
|
+
},
|
|
1049
|
+
[enabled, idleOpacity, fadeInMs, fadeOutMs, hideAfterMs],
|
|
1050
|
+
);
|
|
1051
|
+
return opacity;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function resolveLiveEngineModeInputs({
|
|
1055
|
+
isCandle,
|
|
1056
|
+
data,
|
|
1057
|
+
lineEngineData,
|
|
1058
|
+
candles,
|
|
1059
|
+
candlesEngine,
|
|
1060
|
+
liveCandle,
|
|
1061
|
+
liveEngine,
|
|
1062
|
+
candleGapsCfg,
|
|
1063
|
+
thresholdInRange,
|
|
1064
|
+
thresholdIsSeries,
|
|
1065
|
+
thresholdSeriesSV,
|
|
1066
|
+
thresholdCfg,
|
|
1067
|
+
}: {
|
|
1068
|
+
isCandle: boolean;
|
|
1069
|
+
data: LiveChartProps["data"];
|
|
1070
|
+
lineEngineData: LiveChartProps["data"];
|
|
1071
|
+
candles: LiveChartProps["candles"];
|
|
1072
|
+
candlesEngine: SharedValue<CandlePoint[]>;
|
|
1073
|
+
liveCandle: LiveChartProps["liveCandle"];
|
|
1074
|
+
liveEngine: SharedValue<CandlePoint | null>;
|
|
1075
|
+
candleGapsCfg: ResolvedCandleGapsConfig | null;
|
|
1076
|
+
thresholdInRange: boolean;
|
|
1077
|
+
thresholdIsSeries: boolean;
|
|
1078
|
+
thresholdSeriesSV: SharedValue<LiveChartPoint[]> | null;
|
|
1079
|
+
thresholdCfg: ResolvedThresholdConfig | null;
|
|
1080
|
+
}) {
|
|
1081
|
+
return {
|
|
1082
|
+
data: isCandle ? data : lineEngineData,
|
|
1083
|
+
thresholdRangePoints:
|
|
1084
|
+
thresholdInRange && thresholdIsSeries
|
|
1085
|
+
? (thresholdSeriesSV ?? (thresholdCfg?.value as LiveChartPoint[]))
|
|
1086
|
+
: undefined,
|
|
1087
|
+
thresholdRangeExtendToStart: thresholdCfg?.extendToStart ?? true,
|
|
1088
|
+
thresholdRangeExtendToNow: thresholdCfg?.extendToNow ?? true,
|
|
1089
|
+
candles: isCandle ? candlesEngine : candles,
|
|
1090
|
+
liveCandle: isCandle ? liveEngine : liveCandle,
|
|
1091
|
+
candleGaps: candleGapsCfg?.gaps,
|
|
1092
|
+
candleGapBridgeNoTrades: Boolean(candleGapsCfg?.styles["no-trades"].bridge),
|
|
1093
|
+
candleGapBridgeUnavailable: Boolean(
|
|
1094
|
+
candleGapsCfg?.styles.unavailable.bridge,
|
|
1095
|
+
),
|
|
1096
|
+
candleGapBridgeUnknown: Boolean(candleGapsCfg?.styles.unknown.bridge),
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function resolveCrosshairControllerSettings({
|
|
1101
|
+
scrubCfg,
|
|
1102
|
+
scrubActionCfg,
|
|
1103
|
+
markersActive,
|
|
1104
|
+
refPressActive,
|
|
1105
|
+
refDragEnabled,
|
|
1106
|
+
deferTapHit,
|
|
1107
|
+
timeScrollEnabled,
|
|
1108
|
+
scrollGestureMode,
|
|
1109
|
+
effectivePadding,
|
|
1110
|
+
}: {
|
|
1111
|
+
scrubCfg: ReturnType<typeof resolveScrub>;
|
|
1112
|
+
scrubActionCfg: ReturnType<typeof resolveScrubAction>;
|
|
1113
|
+
markersActive: boolean;
|
|
1114
|
+
refPressActive: boolean;
|
|
1115
|
+
refDragEnabled: boolean;
|
|
1116
|
+
deferTapHit: (x: number, y: number) => boolean;
|
|
1117
|
+
timeScrollEnabled: boolean;
|
|
1118
|
+
scrollGestureMode: "holdToScrub" | "axisDrag";
|
|
1119
|
+
effectivePadding: ChartPadding;
|
|
1120
|
+
}) {
|
|
1121
|
+
return {
|
|
1122
|
+
enabled: scrubCfg !== null || scrubActionCfg !== null,
|
|
1123
|
+
deferTapHit:
|
|
1124
|
+
markersActive || refPressActive || refDragEnabled
|
|
1125
|
+
? deferTapHit
|
|
1126
|
+
: undefined,
|
|
1127
|
+
tooltipPlacement: scrubCfg?.tooltipPlacement ?? "side",
|
|
1128
|
+
tooltipShowValue: scrubCfg?.tooltipShowValue ?? true,
|
|
1129
|
+
tooltipShowTime: scrubCfg?.tooltipShowTime ?? true,
|
|
1130
|
+
tooltipMargin: scrubCfg?.tooltipMargin ?? 8,
|
|
1131
|
+
scrubBottomExclude:
|
|
1132
|
+
timeScrollEnabled && scrollGestureMode === "axisDrag"
|
|
1133
|
+
? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
|
|
1134
|
+
: 0,
|
|
1135
|
+
clampToPlot: scrubCfg?.clampToPlot ?? false,
|
|
1136
|
+
snapToCandles: scrubCfg?.snapToCandles ?? false,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function resolveAreaDotColorVec(
|
|
1141
|
+
areaDotsCfg: ReturnType<typeof resolveAreaDots>,
|
|
1142
|
+
lineProp: LiveChartProps["line"],
|
|
1143
|
+
palette: LiveChartPalette,
|
|
1144
|
+
) {
|
|
1145
|
+
const rgb = parseColorRgb(lineProp?.color ?? palette.line);
|
|
1146
|
+
const [r, g, b, a] = parseColorRgba(
|
|
1147
|
+
areaDotsCfg?.color ?? `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.22)`,
|
|
1148
|
+
);
|
|
1149
|
+
return [r / 255, g / 255, b / 255, a * (areaDotsCfg?.opacity ?? 1)];
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function useLiveIndicatorOpacities({
|
|
1153
|
+
reveal,
|
|
1154
|
+
seriesIndicatorOpacity,
|
|
1155
|
+
seriesOpacity,
|
|
1156
|
+
scrubActive,
|
|
1157
|
+
selectionDotDuringScrub,
|
|
1158
|
+
hideLiveOnScrollBack,
|
|
1159
|
+
dotTracksParked,
|
|
1160
|
+
viewEnd,
|
|
1161
|
+
fadeOverlaysOnScrub,
|
|
1162
|
+
}: {
|
|
1163
|
+
reveal: ReturnType<typeof useChartReveal>;
|
|
1164
|
+
seriesIndicatorOpacity: SharedValue<number>;
|
|
1165
|
+
seriesOpacity: SharedValue<number>;
|
|
1166
|
+
scrubActive: SharedValue<boolean>;
|
|
1167
|
+
selectionDotDuringScrub: boolean;
|
|
1168
|
+
hideLiveOnScrollBack: boolean;
|
|
1169
|
+
dotTracksParked: boolean;
|
|
1170
|
+
viewEnd: SharedValue<number | null>;
|
|
1171
|
+
fadeOverlaysOnScrub: boolean;
|
|
1172
|
+
}) {
|
|
1173
|
+
const liveDotOpacity = useDerivedValue(
|
|
1174
|
+
() =>
|
|
1175
|
+
reveal.dotOpacity.value *
|
|
1176
|
+
(selectionDotDuringScrub && scrubActive.value ? 0 : 1) *
|
|
1177
|
+
liveIndicatorScrollOpacity(
|
|
1178
|
+
hideLiveOnScrollBack && !dotTracksParked,
|
|
1179
|
+
viewEnd.value,
|
|
1180
|
+
) *
|
|
1181
|
+
seriesIndicatorOpacity.value,
|
|
1182
|
+
);
|
|
1183
|
+
const valueLineOpacity = useDerivedValue(
|
|
1184
|
+
() =>
|
|
1185
|
+
reveal.lineOpacity.value *
|
|
1186
|
+
liveIndicatorScrollOpacity(hideLiveOnScrollBack, viewEnd.value) *
|
|
1187
|
+
seriesOpacity.value,
|
|
1188
|
+
);
|
|
1189
|
+
const liveBadgeOpacity = useDerivedValue(
|
|
1190
|
+
() =>
|
|
1191
|
+
reveal.badgeOpacity.value *
|
|
1192
|
+
liveIndicatorScrollOpacity(hideLiveOnScrollBack, viewEnd.value) *
|
|
1193
|
+
seriesOpacity.value,
|
|
1194
|
+
);
|
|
1195
|
+
const overlayScrubFade = useDerivedValue(() =>
|
|
1196
|
+
fadeOverlaysOnScrub
|
|
1197
|
+
? withTiming(scrubActive.get() ? 0 : 1, {
|
|
1198
|
+
duration: SCRUB_OVERLAY_FADE_MS,
|
|
1199
|
+
})
|
|
1200
|
+
: 1,
|
|
1201
|
+
);
|
|
1202
|
+
const markerGroupOpacity = useDerivedValue(
|
|
1203
|
+
() => reveal.dotOpacity.get() * overlayScrubFade.get(),
|
|
1204
|
+
);
|
|
1205
|
+
return {
|
|
1206
|
+
liveDotOpacity,
|
|
1207
|
+
valueLineOpacity,
|
|
1208
|
+
liveBadgeOpacity,
|
|
1209
|
+
overlayScrubFade,
|
|
1210
|
+
markerGroupOpacity,
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function useReferenceLineGrouping({
|
|
1215
|
+
radius,
|
|
1216
|
+
engine,
|
|
1217
|
+
padding,
|
|
1218
|
+
lines,
|
|
1219
|
+
custom,
|
|
1220
|
+
offAxisCustom,
|
|
1221
|
+
dragValues,
|
|
1222
|
+
}: {
|
|
1223
|
+
radius: number | null;
|
|
1224
|
+
engine: ReturnType<typeof useLiveChartEngine>;
|
|
1225
|
+
padding: ChartPadding;
|
|
1226
|
+
lines: ReferenceLine[];
|
|
1227
|
+
custom: boolean[];
|
|
1228
|
+
offAxisCustom: boolean[];
|
|
1229
|
+
dragValues: SharedValue<number[]>;
|
|
1230
|
+
}) {
|
|
1231
|
+
const result = useDerivedValue<ReferenceGrouping>(() => {
|
|
1232
|
+
if (radius == null) return EMPTY_GROUPING;
|
|
1233
|
+
const canvasHeight = engine.canvasHeight.get();
|
|
1234
|
+
const displayMin = engine.displayMin.get();
|
|
1235
|
+
const displayMax = engine.displayMax.get();
|
|
1236
|
+
const top = padding.top;
|
|
1237
|
+
const bottom = canvasHeight - padding.bottom;
|
|
1238
|
+
const yPositions: number[] = [];
|
|
1239
|
+
for (let index = 0; index < lines.length; index++) {
|
|
1240
|
+
const line = lines[index];
|
|
1241
|
+
if (
|
|
1242
|
+
referenceLineForm(line) !== "line" ||
|
|
1243
|
+
line.value === undefined ||
|
|
1244
|
+
custom[index] ||
|
|
1245
|
+
offAxisCustom[index]
|
|
1246
|
+
) {
|
|
1247
|
+
yPositions.push(-1);
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
const value = dragValues.get()[index] ?? line.value;
|
|
1251
|
+
const y = computeScrubDotY(
|
|
1252
|
+
value,
|
|
1253
|
+
displayMin,
|
|
1254
|
+
displayMax,
|
|
1255
|
+
canvasHeight,
|
|
1256
|
+
top,
|
|
1257
|
+
padding.bottom,
|
|
1258
|
+
);
|
|
1259
|
+
yPositions.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
|
|
1260
|
+
}
|
|
1261
|
+
return groupReferenceLines(yPositions, radius);
|
|
1262
|
+
});
|
|
1263
|
+
const hidden = useDerivedValue<boolean[]>(() => result.get().hidden);
|
|
1264
|
+
return { refGroupResult: result, groupHidden: hidden };
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function useLiveChartLayoutResources({
|
|
1268
|
+
value,
|
|
1269
|
+
fontProp,
|
|
1270
|
+
palette,
|
|
1271
|
+
badgeHasFontOverride,
|
|
1272
|
+
badgeFontConfig,
|
|
1273
|
+
refGroupBadgeHasFont,
|
|
1274
|
+
refGroupBadgeFontConfig,
|
|
1275
|
+
yAxisFloat,
|
|
1276
|
+
timeScrollEnabled,
|
|
1277
|
+
lineProp,
|
|
1278
|
+
insets,
|
|
1279
|
+
yAxisCfg,
|
|
1280
|
+
badgeCfg,
|
|
1281
|
+
metricsCfg,
|
|
1282
|
+
badgeUsesRightGutter,
|
|
1283
|
+
xAxisCfg,
|
|
1284
|
+
formatValue,
|
|
1285
|
+
pulseConfig,
|
|
1286
|
+
dotCfg,
|
|
1287
|
+
volumeBandHeight,
|
|
1288
|
+
}: {
|
|
1289
|
+
value: LiveChartProps["value"];
|
|
1290
|
+
fontProp: LiveChartProps["font"];
|
|
1291
|
+
palette: LiveChartPalette;
|
|
1292
|
+
badgeHasFontOverride: boolean;
|
|
1293
|
+
badgeFontConfig: LiveChartProps["font"];
|
|
1294
|
+
refGroupBadgeHasFont: boolean;
|
|
1295
|
+
refGroupBadgeFontConfig: LiveChartProps["font"];
|
|
1296
|
+
yAxisFloat: boolean;
|
|
1297
|
+
timeScrollEnabled: boolean;
|
|
1298
|
+
lineProp: LiveChartProps["line"];
|
|
1299
|
+
insets: LiveChartProps["insets"];
|
|
1300
|
+
yAxisCfg: ReturnType<typeof resolveYAxis>;
|
|
1301
|
+
badgeCfg: ReturnType<typeof resolveBadge>;
|
|
1302
|
+
metricsCfg: ReturnType<typeof resolveMetrics>;
|
|
1303
|
+
badgeUsesRightGutter: boolean;
|
|
1304
|
+
xAxisCfg: ReturnType<typeof resolveXAxis>;
|
|
1305
|
+
formatValue: (value: number) => string;
|
|
1306
|
+
pulseConfig: { maxRadius: number; strokeWidth: number } | null;
|
|
1307
|
+
dotCfg: ReturnType<typeof resolveDot>;
|
|
1308
|
+
volumeBandHeight: number;
|
|
1309
|
+
}) {
|
|
1310
|
+
const skiaFont = useChartSkiaFont(
|
|
1311
|
+
fontProp,
|
|
1312
|
+
MONO_FONT_FAMILY,
|
|
1313
|
+
palette.labelFontSize,
|
|
1314
|
+
);
|
|
1315
|
+
const valueFont = useChartSkiaFont(
|
|
1316
|
+
fontProp,
|
|
1317
|
+
MONO_FONT_FAMILY,
|
|
1318
|
+
palette.valueFontSize * 2,
|
|
1319
|
+
);
|
|
1320
|
+
const badgeFontOverride = useChartSkiaFont(
|
|
1321
|
+
badgeFontConfig,
|
|
1322
|
+
MONO_FONT_FAMILY,
|
|
1323
|
+
palette.labelFontSize,
|
|
1324
|
+
);
|
|
1325
|
+
const badgeFont = badgeHasFontOverride ? badgeFontOverride : skiaFont;
|
|
1326
|
+
const refGroupBadgeFontOverride = useChartSkiaFont(
|
|
1327
|
+
refGroupBadgeFontConfig,
|
|
1328
|
+
MONO_FONT_FAMILY,
|
|
1329
|
+
palette.labelFontSize,
|
|
1330
|
+
);
|
|
1331
|
+
const refGroupBadgeFont = refGroupBadgeHasFont
|
|
1332
|
+
? refGroupBadgeFontOverride
|
|
1333
|
+
: skiaFont;
|
|
1334
|
+
const valueLayoutSample = useInitialSharedValueSample(value);
|
|
1335
|
+
const [scrolledBack, setScrolledBack] = useState(false);
|
|
1336
|
+
const effectiveYAxisFloat =
|
|
1337
|
+
yAxisFloat && (!timeScrollEnabled || scrolledBack);
|
|
1338
|
+
const layout = resolveChartLayout({
|
|
1339
|
+
palette,
|
|
1340
|
+
lineWidthOverride: lineProp?.width,
|
|
1341
|
+
insetsOverride: insets,
|
|
1342
|
+
yAxis: yAxisCfg !== null,
|
|
1343
|
+
yAxisFloat: effectiveYAxisFloat,
|
|
1344
|
+
badge: badgeCfg !== null,
|
|
1345
|
+
badgeMetrics: metricsCfg.badge,
|
|
1346
|
+
badgeUsesRightGutter,
|
|
1347
|
+
badgeShowTail: badgeCfg?.tail ?? true,
|
|
1348
|
+
xAxis: xAxisCfg !== null,
|
|
1349
|
+
font: skiaFont,
|
|
1350
|
+
formatValue,
|
|
1351
|
+
currentValue: valueLayoutSample,
|
|
1352
|
+
pulse: pulseConfig,
|
|
1353
|
+
dotGlow: dotCfg.glow,
|
|
1354
|
+
volumeBandHeight,
|
|
1355
|
+
});
|
|
1356
|
+
return {
|
|
1357
|
+
skiaFont,
|
|
1358
|
+
valueFont,
|
|
1359
|
+
badgeFont,
|
|
1360
|
+
refGroupBadgeFont,
|
|
1361
|
+
effectiveYAxisFloat,
|
|
1362
|
+
setScrolledBack,
|
|
1363
|
+
...layout,
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function useInitialSharedValueSample(value: SharedValue<number>) {
|
|
1368
|
+
const [sample, setSample] = useState<number | undefined>(undefined);
|
|
1369
|
+
const captured = useSharedValue(false);
|
|
1370
|
+
useAnimatedReaction(
|
|
1371
|
+
() => (captured.get() ? null : value.get()),
|
|
1372
|
+
(current) => {
|
|
1373
|
+
if (current === null) return;
|
|
1374
|
+
captured.set(true);
|
|
1375
|
+
scheduleOnRN(setSample, current);
|
|
1376
|
+
},
|
|
1377
|
+
[captured, value],
|
|
1378
|
+
);
|
|
1379
|
+
return sample;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function resolveLiveChartModelDefaults({
|
|
1383
|
+
isCandle,
|
|
1384
|
+
candleWidth,
|
|
1385
|
+
loadingCfg,
|
|
1386
|
+
volumeCfg,
|
|
1387
|
+
palette,
|
|
1388
|
+
lineProp,
|
|
1389
|
+
}: {
|
|
1390
|
+
isCandle: boolean;
|
|
1391
|
+
candleWidth: number;
|
|
1392
|
+
loadingCfg: ReturnType<typeof resolveLoading>;
|
|
1393
|
+
volumeCfg: ReturnType<typeof resolveVolume>;
|
|
1394
|
+
palette: LiveChartPalette;
|
|
1395
|
+
lineProp: LiveChartProps["line"];
|
|
1396
|
+
}) {
|
|
1397
|
+
return {
|
|
1398
|
+
extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
|
|
1399
|
+
loadingLineColor: loadingCfg?.color,
|
|
1400
|
+
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
1401
|
+
loadingAmplitude: loadingCfg?.amplitude,
|
|
1402
|
+
loadingSpeed: loadingCfg?.speed,
|
|
1403
|
+
loadingAxisLabels: loadingCfg?.axisLabels ?? true,
|
|
1404
|
+
volumeOpacity: volumeCfg?.opacity ?? 1,
|
|
1405
|
+
volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
|
|
1406
|
+
volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
|
|
1407
|
+
selectionColor: lineProp?.color ?? palette.line,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Resolves props → configs → theme/layout → engine → per-frame derived values and
|
|
1413
|
+
* overlay hooks, returning a single render model. All the chart's wiring lives
|
|
1414
|
+
* here so the rendered pieces (`ChartStack`, `ChartScrubLayer`, `LiveChart`) stay
|
|
1415
|
+
* small and presentational.
|
|
1416
|
+
*/
|
|
1417
|
+
|
|
1418
|
+
function useLiveChartController({
|
|
1419
|
+
// ── Data ────────────────────────────────────────────────────────────────
|
|
1420
|
+
data,
|
|
1421
|
+
value,
|
|
1422
|
+
|
|
1423
|
+
// ── Appearance ──────────────────────────────────────────────────────────
|
|
1424
|
+
theme = "dark",
|
|
1425
|
+
accentColor = DEFAULT_ACCENT_COLOR,
|
|
1426
|
+
gradient = true,
|
|
1427
|
+
areaDots,
|
|
1428
|
+
line: lineProp,
|
|
1429
|
+
font: fontProp,
|
|
1430
|
+
insets,
|
|
1431
|
+
style,
|
|
1432
|
+
seriesOpacity,
|
|
1433
|
+
canvasMode = "transparent",
|
|
1434
|
+
|
|
1435
|
+
// ── Candlestick ─────────────────────────────────────────────────────────
|
|
1436
|
+
mode = "line",
|
|
1437
|
+
candles,
|
|
1438
|
+
candleWidth = 60,
|
|
1439
|
+
liveCandle,
|
|
1440
|
+
candleGaps,
|
|
1441
|
+
lineGaps,
|
|
1442
|
+
volume,
|
|
1443
|
+
|
|
1444
|
+
// ── Behaviour ───────────────────────────────────────────────────────────
|
|
1445
|
+
timeWindow = 30,
|
|
1446
|
+
paused = false,
|
|
1447
|
+
loading = false,
|
|
1448
|
+
transitions,
|
|
1449
|
+
// `static` is a reserved word — alias it so the destructure parses.
|
|
1450
|
+
static: isStatic = false,
|
|
1451
|
+
snapKey,
|
|
1452
|
+
smoothing = 0.08,
|
|
1453
|
+
exaggerate = false,
|
|
1454
|
+
nonNegative = false,
|
|
1455
|
+
maxValue,
|
|
1456
|
+
yRangeScale,
|
|
1457
|
+
windowBuffer = 0,
|
|
1458
|
+
nowOverride,
|
|
1459
|
+
timeScroll = false,
|
|
1460
|
+
returnToLive,
|
|
1461
|
+
zoom = false,
|
|
1462
|
+
accessibilityLabel,
|
|
1463
|
+
accessibilityRole = "image",
|
|
1464
|
+
emptyText = "No data",
|
|
1465
|
+
formatValue = defaultFormatValue,
|
|
1466
|
+
formatTime = defaultFormatTime,
|
|
1467
|
+
|
|
1468
|
+
// ── Overlays ────────────────────────────────────────────────────────────
|
|
1469
|
+
yAxis = true,
|
|
1470
|
+
xAxis = true,
|
|
1471
|
+
axisAutoHide = false,
|
|
1472
|
+
topLabel,
|
|
1473
|
+
bottomLabel,
|
|
1474
|
+
badge = true,
|
|
1475
|
+
momentum = true,
|
|
1476
|
+
pulse = true,
|
|
1477
|
+
dot,
|
|
1478
|
+
valueLine = true,
|
|
1479
|
+
showValue = false,
|
|
1480
|
+
valueMomentumColor = false,
|
|
1481
|
+
referenceLines,
|
|
1482
|
+
segments,
|
|
1483
|
+
threshold,
|
|
1484
|
+
gridStyle,
|
|
1485
|
+
palette: paletteOverride,
|
|
1486
|
+
metrics,
|
|
1487
|
+
scrub = true,
|
|
1488
|
+
scrubAction,
|
|
1489
|
+
selectionDot,
|
|
1490
|
+
tradeStream,
|
|
1491
|
+
degen,
|
|
1492
|
+
markers,
|
|
1493
|
+
onMarkerPress,
|
|
1494
|
+
markerHitRadius = 16,
|
|
1495
|
+
markerCluster,
|
|
1496
|
+
renderMarker,
|
|
1497
|
+
renderTooltip,
|
|
1498
|
+
renderOverlay,
|
|
1499
|
+
renderThresholdBadge,
|
|
1500
|
+
renderReferenceLine,
|
|
1501
|
+
renderOffAxisReferenceLine,
|
|
1502
|
+
referenceLineGrouping,
|
|
1503
|
+
leftEdgeFade = true,
|
|
1504
|
+
|
|
1505
|
+
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
1506
|
+
onScrub,
|
|
1507
|
+
onScrubAction,
|
|
1508
|
+
onReferenceLinePress,
|
|
1509
|
+
onGestureStart,
|
|
1510
|
+
onGestureEnd,
|
|
1511
|
+
onVisibleRangeChange,
|
|
1512
|
+
onReachStart,
|
|
1513
|
+
onDegenShake,
|
|
1514
|
+
}: LiveChartProps) {
|
|
1515
|
+
const fullSeriesOpacity = useSharedValue(1);
|
|
1516
|
+
const resolvedSeriesOpacity = seriesOpacity ?? fullSeriesOpacity;
|
|
1517
|
+
const emptyMarkers = useSharedValue<Marker[]>([]);
|
|
1518
|
+
const markersSV = markers ?? emptyMarkers;
|
|
1519
|
+
// Stand-in threshold value so `useThreshold` can be called unconditionally
|
|
1520
|
+
// (hooks can't be); the geometry is ignored when no threshold is configured.
|
|
1521
|
+
const emptyThresholdValue = useSharedValue(0);
|
|
1522
|
+
const {
|
|
1523
|
+
isCandle,
|
|
1524
|
+
yAxisCfg,
|
|
1525
|
+
xAxisCfg,
|
|
1526
|
+
topLabelCfg,
|
|
1527
|
+
bottomLabelCfg,
|
|
1528
|
+
badgeCfg,
|
|
1529
|
+
scrubCfg,
|
|
1530
|
+
scrubActionCfg,
|
|
1531
|
+
volumeCfg,
|
|
1532
|
+
chartGapsCfg,
|
|
1533
|
+
candleGapsCfg,
|
|
1534
|
+
lineGapsCfg,
|
|
1535
|
+
volumeBandHeight,
|
|
1536
|
+
gradientCfg,
|
|
1537
|
+
areaDotsCfg,
|
|
1538
|
+
thresholdCfg,
|
|
1539
|
+
thresholdSeriesSV,
|
|
1540
|
+
thresholdIsSeries,
|
|
1541
|
+
valueLineCfg,
|
|
1542
|
+
pulseCfg,
|
|
1543
|
+
dotCfg,
|
|
1544
|
+
dotTracksParked,
|
|
1545
|
+
selectionDotCfg,
|
|
1546
|
+
dotOuterRadius,
|
|
1547
|
+
gridStyleCfg,
|
|
1548
|
+
degenCfg,
|
|
1549
|
+
tradeStreamResolved,
|
|
1550
|
+
metricsCfg,
|
|
1551
|
+
} = resolveLiveChartFeatureConfig({
|
|
1552
|
+
mode,
|
|
1553
|
+
yAxis,
|
|
1554
|
+
xAxis,
|
|
1555
|
+
topLabel,
|
|
1556
|
+
bottomLabel,
|
|
1557
|
+
badge,
|
|
1558
|
+
scrub,
|
|
1559
|
+
scrubAction,
|
|
1560
|
+
volume,
|
|
1561
|
+
candleGaps,
|
|
1562
|
+
lineGaps,
|
|
1563
|
+
gradient,
|
|
1564
|
+
areaDots,
|
|
1565
|
+
threshold,
|
|
1566
|
+
valueLine,
|
|
1567
|
+
isStatic,
|
|
1568
|
+
pulse,
|
|
1569
|
+
dot,
|
|
1570
|
+
selectionDot,
|
|
1571
|
+
gridStyle,
|
|
1572
|
+
degen,
|
|
1573
|
+
tradeStream,
|
|
1574
|
+
metrics,
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
const {
|
|
1578
|
+
chartGapBands,
|
|
1579
|
+
allRefLines,
|
|
1580
|
+
refValues,
|
|
1581
|
+
refLineCustom,
|
|
1582
|
+
refLineOffAxisCustom,
|
|
1583
|
+
refLineKeys,
|
|
1584
|
+
draggableRefIdx,
|
|
1585
|
+
thresholdInRange,
|
|
1586
|
+
thresholdRangeValueSV,
|
|
1587
|
+
refGroupingCfg,
|
|
1588
|
+
refGroupingRadius,
|
|
1589
|
+
refGroupBadge,
|
|
1590
|
+
refGroupFormat,
|
|
1591
|
+
badgeUsesRightGutter,
|
|
1592
|
+
} = resolveLiveChartReferenceConfig({
|
|
1593
|
+
chartGapsCfg,
|
|
1594
|
+
referenceLines,
|
|
1595
|
+
renderReferenceLine,
|
|
1596
|
+
renderOffAxisReferenceLine,
|
|
1597
|
+
thresholdCfg,
|
|
1598
|
+
referenceLineGrouping,
|
|
1599
|
+
badgeCfg,
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
const { dragValues, dragActive, refLineCustomTagWidths, liveRefValues } =
|
|
1603
|
+
useLiveReferenceState(allRefLines, draggableRefIdx, thresholdRangeValueSV);
|
|
1604
|
+
|
|
1605
|
+
const {
|
|
1606
|
+
palette,
|
|
1607
|
+
resolvedSegments,
|
|
1608
|
+
hasRecolorSegments,
|
|
1609
|
+
leftEdgeFadeCfg,
|
|
1610
|
+
badgeHasFontOverride,
|
|
1611
|
+
badgeFontConfig,
|
|
1612
|
+
refGroupBadgeHasFont,
|
|
1613
|
+
refGroupBadgeFontConfig,
|
|
1614
|
+
pulseConfig,
|
|
1615
|
+
timeScrollEnabled,
|
|
1616
|
+
returnToLiveMs,
|
|
1617
|
+
timeScrollOverscroll,
|
|
1618
|
+
timeScrollFling,
|
|
1619
|
+
zoomCfg,
|
|
1620
|
+
zoomEnabled,
|
|
1621
|
+
yAxisFloat,
|
|
1622
|
+
} = resolveLiveChartPresentationConfig({
|
|
1623
|
+
accentColor,
|
|
1624
|
+
theme,
|
|
1625
|
+
paletteOverride,
|
|
1626
|
+
segments,
|
|
1627
|
+
leftEdgeFade,
|
|
1628
|
+
fontProp,
|
|
1629
|
+
badgeCfg,
|
|
1630
|
+
refGroupBadge,
|
|
1631
|
+
pulseCfg,
|
|
1632
|
+
timeScroll,
|
|
1633
|
+
isStatic,
|
|
1634
|
+
returnToLive,
|
|
1635
|
+
zoom,
|
|
1636
|
+
yAxisCfg,
|
|
1637
|
+
});
|
|
708
1638
|
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1639
|
+
const {
|
|
1640
|
+
skiaFont,
|
|
1641
|
+
valueFont,
|
|
1642
|
+
badgeFont,
|
|
1643
|
+
refGroupBadgeFont,
|
|
1644
|
+
effectiveYAxisFloat,
|
|
1645
|
+
setScrolledBack,
|
|
1646
|
+
strokeWidth,
|
|
1647
|
+
padding: effectivePadding,
|
|
1648
|
+
} = useLiveChartLayoutResources({
|
|
1649
|
+
value,
|
|
1650
|
+
fontProp,
|
|
717
1651
|
palette,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1652
|
+
badgeHasFontOverride,
|
|
1653
|
+
badgeFontConfig,
|
|
1654
|
+
refGroupBadgeHasFont,
|
|
1655
|
+
refGroupBadgeFontConfig,
|
|
1656
|
+
yAxisFloat,
|
|
1657
|
+
timeScrollEnabled,
|
|
1658
|
+
lineProp,
|
|
1659
|
+
insets,
|
|
1660
|
+
yAxisCfg,
|
|
1661
|
+
badgeCfg,
|
|
1662
|
+
metricsCfg,
|
|
724
1663
|
badgeUsesRightGutter,
|
|
725
|
-
|
|
726
|
-
xAxis: xAxisCfg !== null,
|
|
727
|
-
font: skiaFont,
|
|
1664
|
+
xAxisCfg,
|
|
728
1665
|
formatValue,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
dotGlow: dotCfg.glow,
|
|
1666
|
+
pulseConfig,
|
|
1667
|
+
dotCfg,
|
|
732
1668
|
volumeBandHeight,
|
|
733
1669
|
});
|
|
734
1670
|
|
|
735
1671
|
// ── Reveal state ────────────────────────────────────────────
|
|
736
|
-
// ≥
|
|
1672
|
+
// ≥1 line point or ≥1 committed candle; morphT=1 only when !loading && hasData.
|
|
737
1673
|
const { hasData } = useLiveChartHasData({
|
|
738
1674
|
isCandle,
|
|
739
1675
|
data,
|
|
@@ -770,8 +1706,22 @@ function useLiveChartController({
|
|
|
770
1706
|
// ── Engine ─────────────────────────────────────────────────────────────
|
|
771
1707
|
// Line mode: tick + paths use `lineEngineData` (stash when reversing). Candle mode:
|
|
772
1708
|
// parent `data` stays tick/line-morph input; OHLC uses candlesEngine + liveEngine.
|
|
1709
|
+
const engineModeInputs = resolveLiveEngineModeInputs({
|
|
1710
|
+
isCandle,
|
|
1711
|
+
data,
|
|
1712
|
+
lineEngineData,
|
|
1713
|
+
candles,
|
|
1714
|
+
candlesEngine,
|
|
1715
|
+
liveCandle,
|
|
1716
|
+
liveEngine,
|
|
1717
|
+
candleGapsCfg,
|
|
1718
|
+
thresholdInRange,
|
|
1719
|
+
thresholdIsSeries,
|
|
1720
|
+
thresholdSeriesSV,
|
|
1721
|
+
thresholdCfg,
|
|
1722
|
+
});
|
|
773
1723
|
const engine = useLiveChartEngine({
|
|
774
|
-
|
|
1724
|
+
...engineModeInputs,
|
|
775
1725
|
value,
|
|
776
1726
|
timeWindow,
|
|
777
1727
|
paused,
|
|
@@ -785,28 +1735,12 @@ function useLiveChartController({
|
|
|
785
1735
|
exaggerate,
|
|
786
1736
|
referenceValues: refValues,
|
|
787
1737
|
liveReferenceValues: liveRefValues,
|
|
788
|
-
// Series threshold with `includeInRange`: the tick folds its window min/max
|
|
789
|
-
// into the Y-range fit (the constant form rides `liveReferenceValues`).
|
|
790
|
-
thresholdRangePoints:
|
|
791
|
-
thresholdInRange && thresholdIsSeries
|
|
792
|
-
? (thresholdSeriesSV ?? (thresholdCfg?.value as LiveChartPoint[]))
|
|
793
|
-
: undefined,
|
|
794
|
-
thresholdRangeExtendToNow: thresholdCfg?.extendToNow ?? true,
|
|
795
1738
|
nonNegative,
|
|
796
1739
|
maxValue,
|
|
797
1740
|
yRangeScale,
|
|
798
1741
|
windowBuffer,
|
|
799
1742
|
nowOverride,
|
|
800
1743
|
mode,
|
|
801
|
-
candles: isCandle ? candlesEngine : candles,
|
|
802
|
-
liveCandle: isCandle ? liveEngine : liveCandle,
|
|
803
|
-
candleGaps: candleGapsCfg?.gaps,
|
|
804
|
-
candleGapBridgeNoTrades:
|
|
805
|
-
Boolean(candleGapsCfg?.styles["no-trades"].bridge),
|
|
806
|
-
candleGapBridgeUnavailable:
|
|
807
|
-
Boolean(candleGapsCfg?.styles.unavailable.bridge),
|
|
808
|
-
candleGapBridgeUnknown:
|
|
809
|
-
Boolean(candleGapsCfg?.styles.unknown.bridge),
|
|
810
1744
|
});
|
|
811
1745
|
|
|
812
1746
|
// Mirror the UI-thread scroll state to React so the floating y-axis can keep
|
|
@@ -836,174 +1770,49 @@ function useLiveChartController({
|
|
|
836
1770
|
// ── Per-frame derived values ───────────────────────────────────────────
|
|
837
1771
|
const { layoutWidth, layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
838
1772
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const dMax = engine.displayMax.get();
|
|
848
|
-
const top = effectivePadding.top;
|
|
849
|
-
const bottom = ch - effectivePadding.bottom;
|
|
850
|
-
const ys: number[] = [];
|
|
851
|
-
for (let i = 0; i < allRefLines.length; i++) {
|
|
852
|
-
const l = allRefLines[i];
|
|
853
|
-
// Skip bands and lines a custom `renderReferenceLine` owns — a custom tag
|
|
854
|
-
// draws itself and isn't suppressed by grouping, so folding it into a
|
|
855
|
-
// built-in count pill would double-count it (counted *and* still shown).
|
|
856
|
-
if (
|
|
857
|
-
referenceLineForm(l) !== "line" ||
|
|
858
|
-
l.value === undefined ||
|
|
859
|
-
refLineCustom[i] ||
|
|
860
|
-
refLineOffAxisCustom[i]
|
|
861
|
-
) {
|
|
862
|
-
ys.push(-1);
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
const v = dragValues.get()[i] ?? l.value;
|
|
866
|
-
const y = computeScrubDotY(
|
|
867
|
-
v,
|
|
868
|
-
dMin,
|
|
869
|
-
dMax,
|
|
870
|
-
ch,
|
|
871
|
-
top,
|
|
872
|
-
effectivePadding.bottom,
|
|
873
|
-
);
|
|
874
|
-
ys.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
|
|
875
|
-
}
|
|
876
|
-
return groupReferenceLines(ys, refGroupingRadius);
|
|
1773
|
+
const { refGroupResult, groupHidden } = useReferenceLineGrouping({
|
|
1774
|
+
radius: refGroupingRadius,
|
|
1775
|
+
engine,
|
|
1776
|
+
padding: effectivePadding,
|
|
1777
|
+
lines: allRefLines,
|
|
1778
|
+
custom: refLineCustom,
|
|
1779
|
+
offAxisCustom: refLineOffAxisCustom,
|
|
1780
|
+
dragValues,
|
|
877
1781
|
});
|
|
878
|
-
const groupHidden = useDerivedValue<boolean[]>(
|
|
879
|
-
() => refGroupResult.get().hidden,
|
|
880
|
-
);
|
|
881
1782
|
|
|
882
1783
|
// Threshold split geometry. Two forms, picked at render by `Array.isArray` (no
|
|
883
1784
|
// SharedValue read): a constant `SharedValue<number>` benchmark drives the
|
|
884
1785
|
// vertical hard-stop gradient (`thresholdGeom`); a time-varying `LiveChartPoint[]`
|
|
885
1786
|
// series drives the per-fragment split shader (`thresholdSeriesGeom`). Both hooks
|
|
886
1787
|
// run unconditionally — the unused one short-circuits cheaply on the UI thread.
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const thresholdGeom = useThreshold(engine, effectivePadding, thresholdValue);
|
|
905
|
-
const thresholdSeriesGeom = useThresholdSeries(
|
|
1788
|
+
const {
|
|
1789
|
+
thresholdGeom,
|
|
1790
|
+
thresholdStrokeColors,
|
|
1791
|
+
thresholdFillColors,
|
|
1792
|
+
thresholdSeriesHasPoints,
|
|
1793
|
+
thresholdStrokeUniforms,
|
|
1794
|
+
thresholdFillUniforms,
|
|
1795
|
+
thresholdMarkerLineY,
|
|
1796
|
+
thresholdMarkerVisible,
|
|
1797
|
+
thresholdBadgeVisible,
|
|
1798
|
+
thresholdMarkerValue,
|
|
1799
|
+
thresholdCustomBadge,
|
|
1800
|
+
thresholdSeriesPts,
|
|
1801
|
+
thresholdBadgeLeftAnchorX,
|
|
1802
|
+
thresholdFillLineY,
|
|
1803
|
+
thresholdFillSamples,
|
|
1804
|
+
} = useLiveChartThresholdModel({
|
|
906
1805
|
engine,
|
|
907
|
-
effectivePadding,
|
|
908
|
-
|
|
1806
|
+
padding: effectivePadding,
|
|
1807
|
+
thresholdCfg,
|
|
909
1808
|
thresholdSeriesSV,
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
// Split colors as vec4s for the series shader, memoized on the *resolved*
|
|
918
|
-
// color strings + opacity (so a threshold added after mount with default
|
|
919
|
-
// colors still computes, and the per-frame uniforms worklet isn't rebuilt
|
|
920
|
-
// every render). `strokeRest` is the plain line color painted right of the
|
|
921
|
-
// `extendToNow: false` cutoff.
|
|
922
|
-
const thresholdSplitAbove = thresholdCfg
|
|
923
|
-
? (thresholdCfg.aboveColor ?? palette.candleUp)
|
|
924
|
-
: null;
|
|
925
|
-
const thresholdSplitBelow = thresholdCfg
|
|
926
|
-
? (thresholdCfg.belowColor ?? palette.candleDown)
|
|
927
|
-
: null;
|
|
928
|
-
const thresholdFillOpacity =
|
|
929
|
-
thresholdCfg?.fillOpacity ?? THRESHOLD_FILL_OPACITY_DEFAULT;
|
|
930
|
-
const thresholdLineColorStr = lineProp?.color ?? palette.line;
|
|
931
|
-
const thresholdVecs = useMemo(
|
|
932
|
-
() =>
|
|
933
|
-
thresholdSplitAbove !== null && thresholdSplitBelow !== null
|
|
934
|
-
? {
|
|
935
|
-
...thresholdSplitColorVecs(
|
|
936
|
-
thresholdSplitAbove,
|
|
937
|
-
thresholdSplitBelow,
|
|
938
|
-
thresholdFillOpacity,
|
|
939
|
-
),
|
|
940
|
-
strokeRest: (() => {
|
|
941
|
-
const [r, g, b, a] = parseColorRgba(thresholdLineColorStr);
|
|
942
|
-
return [r / 255, g / 255, b / 255, a];
|
|
943
|
-
})(),
|
|
944
|
-
}
|
|
945
|
-
: null,
|
|
946
|
-
[
|
|
947
|
-
thresholdSplitAbove,
|
|
948
|
-
thresholdSplitBelow,
|
|
949
|
-
thresholdFillOpacity,
|
|
950
|
-
thresholdLineColorStr,
|
|
951
|
-
],
|
|
952
|
-
);
|
|
953
|
-
const thresholdStrokeUniforms = useThresholdSplitUniforms(
|
|
954
|
-
thresholdSeriesGeom.samples,
|
|
955
|
-
engine,
|
|
956
|
-
effectivePadding,
|
|
957
|
-
thresholdVecs?.strokeAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
958
|
-
thresholdVecs?.strokeBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
959
|
-
thresholdVecs?.strokeRest ?? THRESHOLD_FALLBACK_COLOR,
|
|
960
|
-
thresholdSeriesGeom.clipRightX,
|
|
961
|
-
);
|
|
962
|
-
const thresholdFillUniforms = useThresholdSplitUniforms(
|
|
963
|
-
thresholdSeriesGeom.samples,
|
|
964
|
-
engine,
|
|
965
|
-
effectivePadding,
|
|
966
|
-
thresholdVecs?.fillAbove ?? THRESHOLD_FALLBACK_COLOR,
|
|
967
|
-
thresholdVecs?.fillBelow ?? THRESHOLD_FALLBACK_COLOR,
|
|
968
|
-
TRANSPARENT_VEC4,
|
|
969
|
-
thresholdSeriesGeom.clipRightX,
|
|
970
|
-
);
|
|
971
|
-
|
|
972
|
-
// Marker line + badge sources: a series badge independently selects its first
|
|
973
|
-
// or last visible endpoint; the constant case stays at the single benchmark Y.
|
|
974
|
-
// The badge gets its own visibility because its selected endpoint can be
|
|
975
|
-
// off-plot while older polyline segments are still visible.
|
|
976
|
-
const thresholdMarkerLineY = thresholdIsSeries
|
|
977
|
-
? thresholdSeriesGeom.badgeLineY
|
|
978
|
-
: thresholdGeom.lineY;
|
|
979
|
-
const thresholdMarkerVisible = thresholdIsSeries
|
|
980
|
-
? thresholdSeriesGeom.visible
|
|
981
|
-
: thresholdGeom.visible;
|
|
982
|
-
const thresholdBadgeVisible = thresholdIsSeries
|
|
983
|
-
? thresholdSeriesGeom.badgeVisible
|
|
984
|
-
: thresholdGeom.visible;
|
|
985
|
-
const thresholdMarkerValue =
|
|
986
|
-
thresholdCfg && !thresholdIsSeries && !Array.isArray(thresholdCfg.value)
|
|
987
|
-
? (thresholdCfg.value ?? thresholdSeriesGeom.badgeValue)
|
|
988
|
-
: thresholdSeriesGeom.badgeValue;
|
|
989
|
-
const hasCustomThresholdBadge =
|
|
990
|
-
thresholdCfg?.line != null && renderThresholdBadge != null;
|
|
991
|
-
const thresholdMarkerValueStr = useDerivedValue(() =>
|
|
992
|
-
hasCustomThresholdBadge ? formatValue(thresholdMarkerValue.get()) : "",
|
|
993
|
-
);
|
|
994
|
-
const thresholdCustomBadge =
|
|
995
|
-
thresholdCfg?.line && renderThresholdBadge
|
|
996
|
-
? renderThresholdBadge({
|
|
997
|
-
line: thresholdCfg.line,
|
|
998
|
-
value: thresholdMarkerValue,
|
|
999
|
-
valueStr: thresholdMarkerValueStr,
|
|
1000
|
-
y: thresholdMarkerLineY,
|
|
1001
|
-
visible: thresholdBadgeVisible,
|
|
1002
|
-
})
|
|
1003
|
-
: null;
|
|
1004
|
-
const thresholdSeriesPts = thresholdIsSeries
|
|
1005
|
-
? thresholdSeriesGeom.screenPts
|
|
1006
|
-
: undefined;
|
|
1809
|
+
thresholdIsSeries,
|
|
1810
|
+
emptyThresholdValue,
|
|
1811
|
+
palette,
|
|
1812
|
+
lineProp,
|
|
1813
|
+
renderThresholdBadge,
|
|
1814
|
+
formatValue,
|
|
1815
|
+
});
|
|
1007
1816
|
|
|
1008
1817
|
// Straight polyline instead of the monotone cubic when line.curve === "linear".
|
|
1009
1818
|
// Shared by the path builders and the marker anchoring so glyphs sit on the
|
|
@@ -1016,7 +1825,7 @@ function useLiveChartController({
|
|
|
1016
1825
|
reveal.morphT,
|
|
1017
1826
|
// Constant threshold band closes at a single Y (the series closes along the
|
|
1018
1827
|
// polyline passed as the last arg below).
|
|
1019
|
-
|
|
1828
|
+
thresholdFillLineY,
|
|
1020
1829
|
lineIsLinear,
|
|
1021
1830
|
// The plotted line independently selects the visible edge while historical;
|
|
1022
1831
|
// badge/live-indicator options affect overlays only.
|
|
@@ -1027,9 +1836,7 @@ function useLiveChartController({
|
|
|
1027
1836
|
// (so the band matches the shader and doesn't bleed at step risers). An empty
|
|
1028
1837
|
// series builds no band (its samples are all the far-below fallback, which
|
|
1029
1838
|
// would tint the entire area under the line).
|
|
1030
|
-
|
|
1031
|
-
? thresholdSeriesGeom.samples
|
|
1032
|
-
: undefined,
|
|
1839
|
+
thresholdFillSamples,
|
|
1033
1840
|
lineProp?.simplify,
|
|
1034
1841
|
lineGapsCfg?.gaps,
|
|
1035
1842
|
);
|
|
@@ -1037,17 +1844,11 @@ function useLiveChartController({
|
|
|
1037
1844
|
// Area-dots fill shader color as a vec4 (channels 0..1), with the config
|
|
1038
1845
|
// `opacity` folded into the alpha. Defaults to a faint tint of the line/accent
|
|
1039
1846
|
// color (theme-aware) so out-of-the-box dots read as a subtle field.
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1847
|
+
const areaDotColorVec = resolveAreaDotColorVec(
|
|
1848
|
+
areaDotsCfg,
|
|
1849
|
+
lineProp,
|
|
1850
|
+
palette,
|
|
1044
1851
|
);
|
|
1045
|
-
const areaDotColorVec = [
|
|
1046
|
-
adR / 255,
|
|
1047
|
-
adG / 255,
|
|
1048
|
-
adB / 255,
|
|
1049
|
-
adA * (areaDotsCfg?.opacity ?? 1),
|
|
1050
|
-
];
|
|
1051
1852
|
|
|
1052
1853
|
const { dotX, dotY } = useLiveDot(
|
|
1053
1854
|
engine,
|
|
@@ -1078,33 +1879,31 @@ function useLiveChartController({
|
|
|
1078
1879
|
isCandle,
|
|
1079
1880
|
);
|
|
1080
1881
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const markersActive = markers != null;
|
|
1107
|
-
const markerClusterCfg = resolveMarkerCluster(markerCluster);
|
|
1882
|
+
const {
|
|
1883
|
+
crosshairChartOpts,
|
|
1884
|
+
markersActive,
|
|
1885
|
+
markerClusterCfg,
|
|
1886
|
+
refPressActive,
|
|
1887
|
+
refDragEnabled,
|
|
1888
|
+
scrollGestureMode,
|
|
1889
|
+
scrubHoldMs,
|
|
1890
|
+
} = resolveLiveChartInteractionConfig({
|
|
1891
|
+
isCandle,
|
|
1892
|
+
mode,
|
|
1893
|
+
candlesEngine,
|
|
1894
|
+
liveEngine,
|
|
1895
|
+
candleWidth,
|
|
1896
|
+
candleGapsCfg,
|
|
1897
|
+
lineGapsCfg,
|
|
1898
|
+
markers,
|
|
1899
|
+
markerCluster,
|
|
1900
|
+
onReferenceLinePress,
|
|
1901
|
+
allRefLines,
|
|
1902
|
+
isStatic,
|
|
1903
|
+
timeScroll,
|
|
1904
|
+
timeScrollEnabled,
|
|
1905
|
+
scrubCfg,
|
|
1906
|
+
});
|
|
1108
1907
|
// `projected` is used internally by the hit-test gesture; the overlay
|
|
1109
1908
|
// self-projects, so we only need the gesture + hit-test here. Built BEFORE
|
|
1110
1909
|
// `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
|
|
@@ -1124,7 +1923,6 @@ function useLiveChartController({
|
|
|
1124
1923
|
|
|
1125
1924
|
// Pressable reference-line badges (working orders / alerts). Built before
|
|
1126
1925
|
// `useCrosshair` so the scrub-action tap can defer to a badge under the finger.
|
|
1127
|
-
const refPressActive = onReferenceLinePress != null && allRefLines.length > 0;
|
|
1128
1926
|
const { tapGesture: refLineTapGesture, hitTest: refLineHitTest } =
|
|
1129
1927
|
useReferenceLinePress(
|
|
1130
1928
|
engine,
|
|
@@ -1143,8 +1941,6 @@ function useLiveChartController({
|
|
|
1143
1941
|
// unconditionally for stable hook order (and before `useCrosshair` so the scrub
|
|
1144
1942
|
// can defer to a line under the finger); the gesture self-disables when no line
|
|
1145
1943
|
// opts in, and it's only composed into the root when `refDragEnabled`.
|
|
1146
|
-
const refDragEnabled =
|
|
1147
|
-
!isStatic && allRefLines.some((l) => l.draggable === true);
|
|
1148
1944
|
const { gesture: refDragGesture, hitTest: refDragHitTest } = useReferenceDrag(
|
|
1149
1945
|
engine,
|
|
1150
1946
|
effectivePadding,
|
|
@@ -1164,24 +1960,6 @@ function useLiveChartController({
|
|
|
1164
1960
|
return markerHitTest(x, y) || refLineHitTest(x, y) || refDragHitTest(x, y);
|
|
1165
1961
|
};
|
|
1166
1962
|
|
|
1167
|
-
// Time-scroll activation. `holdToScrub`: a quick one-finger drag scrolls while
|
|
1168
|
-
// scrub engages on press-and-hold — so the scrub gesture needs a long-press
|
|
1169
|
-
// delay (unless the caller set its own `panGestureDelay`). `timeScrollEnabled`
|
|
1170
|
-
// is computed earlier (it gates the float gutter).
|
|
1171
|
-
const scrollGestureMode =
|
|
1172
|
-
typeof timeScroll === "object"
|
|
1173
|
-
? (timeScroll.gesture ?? "holdToScrub")
|
|
1174
|
-
: "holdToScrub";
|
|
1175
|
-
// In holdToScrub the scrub MUST require a hold so a quick drag scrolls instead.
|
|
1176
|
-
// Precedence: explicit timeScroll.scrubHoldMs, then scrub.panGestureDelay, then
|
|
1177
|
-
// the default. `||` (not `??`) skips the resolved panGestureDelay's 0 default.
|
|
1178
|
-
const timeScrollHoldMs =
|
|
1179
|
-
typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
|
|
1180
|
-
const scrubHoldMs =
|
|
1181
|
-
timeScrollEnabled && scrollGestureMode === "holdToScrub"
|
|
1182
|
-
? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
|
|
1183
|
-
: (scrubCfg?.panGestureDelay ?? 0);
|
|
1184
|
-
|
|
1185
1963
|
// Cross-gesture arbitration for the one-finger touch. `Gesture.Race` below is
|
|
1186
1964
|
// NOT arbitration — RNGH's Race adds no relation between its children, so both
|
|
1187
1965
|
// pans recognize independently and each can activate while the other already
|
|
@@ -1190,6 +1968,18 @@ function useLiveChartController({
|
|
|
1190
1968
|
// (written by the crosshair, read by the scroll pan) is the mirror image.
|
|
1191
1969
|
const scrollActive = useSharedValue(false);
|
|
1192
1970
|
|
|
1971
|
+
const crosshairSettings = resolveCrosshairControllerSettings({
|
|
1972
|
+
scrubCfg,
|
|
1973
|
+
scrubActionCfg,
|
|
1974
|
+
markersActive,
|
|
1975
|
+
refPressActive,
|
|
1976
|
+
refDragEnabled,
|
|
1977
|
+
deferTapHit,
|
|
1978
|
+
timeScrollEnabled,
|
|
1979
|
+
scrollGestureMode,
|
|
1980
|
+
effectivePadding,
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1193
1983
|
const crosshair = useCrosshair(
|
|
1194
1984
|
engine,
|
|
1195
1985
|
effectivePadding,
|
|
@@ -1200,7 +1990,7 @@ function useLiveChartController({
|
|
|
1200
1990
|
// Scrub / scrub-action stay live even on static charts: the gesture is
|
|
1201
1991
|
// event-driven (no per-frame loop), so a settled chart costs nothing at rest
|
|
1202
1992
|
// yet becomes scrubbable on touch. `static` only kills the continuous loop.
|
|
1203
|
-
|
|
1993
|
+
crosshairSettings.enabled,
|
|
1204
1994
|
onScrub,
|
|
1205
1995
|
crosshairChartOpts,
|
|
1206
1996
|
scrubHoldMs,
|
|
@@ -1209,20 +1999,15 @@ function useLiveChartController({
|
|
|
1209
1999
|
scrubActionCfg,
|
|
1210
2000
|
onScrubAction,
|
|
1211
2001
|
metricsCfg.badge,
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
// drag there never trips the scrub crosshair.
|
|
1219
|
-
timeScrollEnabled && scrollGestureMode === "axisDrag"
|
|
1220
|
-
? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
|
|
1221
|
-
: 0,
|
|
2002
|
+
crosshairSettings.deferTapHit,
|
|
2003
|
+
crosshairSettings.tooltipPlacement,
|
|
2004
|
+
crosshairSettings.tooltipShowValue,
|
|
2005
|
+
crosshairSettings.tooltipShowTime,
|
|
2006
|
+
crosshairSettings.tooltipMargin,
|
|
2007
|
+
crosshairSettings.scrubBottomExclude,
|
|
1222
2008
|
scrollActive,
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
scrubCfg?.snapToCandles ?? false,
|
|
2009
|
+
crosshairSettings.clampToPlot,
|
|
2010
|
+
crosshairSettings.snapToCandles,
|
|
1226
2011
|
);
|
|
1227
2012
|
|
|
1228
2013
|
// Capture only the shared value in the worklets below. Referencing
|
|
@@ -1276,77 +2061,12 @@ function useLiveChartController({
|
|
|
1276
2061
|
},
|
|
1277
2062
|
});
|
|
1278
2063
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
// underneath so a fade-in shows current labels.
|
|
1285
|
-
const axisAutoHideCfg =
|
|
1286
|
-
axisAutoHide === true ? {} : axisAutoHide === false ? null : axisAutoHide;
|
|
1287
|
-
const axisIdleOpacity = axisAutoHideCfg?.idleOpacity ?? 0;
|
|
1288
|
-
const axisFadeInMs = axisAutoHideCfg?.fadeInMs ?? 60;
|
|
1289
|
-
const axisFadeOutMs = axisAutoHideCfg?.fadeOutMs ?? 250;
|
|
1290
|
-
const axisHideAfterMs = axisAutoHideCfg?.hideAfterMs ?? 3000;
|
|
1291
|
-
const axisAutoHideOpacity = useSharedValue(
|
|
1292
|
-
axisAutoHideCfg ? axisIdleOpacity : 1,
|
|
1293
|
-
);
|
|
1294
|
-
const axisAutoHideEnabled = axisAutoHideCfg !== null;
|
|
1295
|
-
const lastAxisAutoHide = useRef({
|
|
1296
|
-
enabled: axisAutoHideEnabled,
|
|
1297
|
-
idleOpacity: axisIdleOpacity,
|
|
2064
|
+
const axisAutoHideOpacity = useAxisAutoHide({
|
|
2065
|
+
config: axisAutoHide,
|
|
2066
|
+
scrollActive,
|
|
2067
|
+
scrubActive: crosshairScrubActive,
|
|
2068
|
+
engine,
|
|
1298
2069
|
});
|
|
1299
|
-
// `useSharedValue` only reads its initial value on mount. Keep prop changes
|
|
1300
|
-
// in sync as well: turning auto-hide off must immediately restore the axes,
|
|
1301
|
-
// and turning it on starts from the configured idle opacity.
|
|
1302
|
-
useEffect(() => {
|
|
1303
|
-
if (
|
|
1304
|
-
lastAxisAutoHide.current.enabled === axisAutoHideEnabled &&
|
|
1305
|
-
lastAxisAutoHide.current.idleOpacity === axisIdleOpacity
|
|
1306
|
-
) {
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
lastAxisAutoHide.current = {
|
|
1310
|
-
enabled: axisAutoHideEnabled,
|
|
1311
|
-
idleOpacity: axisIdleOpacity,
|
|
1312
|
-
};
|
|
1313
|
-
cancelAnimation(axisAutoHideOpacity);
|
|
1314
|
-
axisAutoHideOpacity.value = axisAutoHideEnabled ? axisIdleOpacity : 1;
|
|
1315
|
-
}, [axisAutoHideEnabled, axisAutoHideOpacity, axisIdleOpacity]);
|
|
1316
|
-
useAnimatedReaction(
|
|
1317
|
-
() => ({
|
|
1318
|
-
gesture: scrollActive.value || crosshairScrubActive.value,
|
|
1319
|
-
viewEnd: engine.viewEnd.value,
|
|
1320
|
-
viewWindow: engine.viewWindow.value,
|
|
1321
|
-
}),
|
|
1322
|
-
(curr, prev) => {
|
|
1323
|
-
if (!axisAutoHideEnabled || prev === null) return;
|
|
1324
|
-
const moved =
|
|
1325
|
-
curr.gesture !== prev.gesture ||
|
|
1326
|
-
curr.viewEnd !== prev.viewEnd ||
|
|
1327
|
-
curr.viewWindow !== prev.viewWindow;
|
|
1328
|
-
if (!moved) return;
|
|
1329
|
-
cancelAnimation(axisAutoHideOpacity);
|
|
1330
|
-
axisAutoHideOpacity.value = curr.gesture
|
|
1331
|
-
? withTiming(1, { duration: axisFadeInMs })
|
|
1332
|
-
: // Movement without a held touch (gesture end, fling, pinch): show,
|
|
1333
|
-
// then fade back out once the chart goes untouched for a while.
|
|
1334
|
-
withSequence(
|
|
1335
|
-
withTiming(1, { duration: axisFadeInMs }),
|
|
1336
|
-
withDelay(
|
|
1337
|
-
axisHideAfterMs,
|
|
1338
|
-
withTiming(axisIdleOpacity, { duration: axisFadeOutMs }),
|
|
1339
|
-
),
|
|
1340
|
-
);
|
|
1341
|
-
},
|
|
1342
|
-
[
|
|
1343
|
-
axisAutoHideEnabled,
|
|
1344
|
-
axisIdleOpacity,
|
|
1345
|
-
axisFadeInMs,
|
|
1346
|
-
axisFadeOutMs,
|
|
1347
|
-
axisHideAfterMs,
|
|
1348
|
-
],
|
|
1349
|
-
);
|
|
1350
2070
|
|
|
1351
2071
|
// Paging callbacks: report the visible range / proximity to the oldest data so
|
|
1352
2072
|
// a host can lazily load history. Inert unless a callback is supplied.
|
|
@@ -1361,59 +2081,21 @@ function useLiveChartController({
|
|
|
1361
2081
|
// ahead of the pan via Exclusive, so a tap is tried first and only becomes a
|
|
1362
2082
|
// drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
|
|
1363
2083
|
// (not `Race`) prevents a jittery tap from being swallowed by the pan.
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
let rootGesture = baseGesture;
|
|
1380
|
-
if (overlayTaps.length > 0) {
|
|
1381
|
-
const tapGroup =
|
|
1382
|
-
overlayTaps.length === 1
|
|
1383
|
-
? overlayTaps[0]
|
|
1384
|
-
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
1385
|
-
// Always `Simultaneous`, never `Race`: on iOS the scrub pan uses
|
|
1386
|
-
// `minDistance(0)`, so in a `Race` it activates on touch-down and cancels the
|
|
1387
|
-
// overlay tap before it can recognize — `onMarkerPress`/`onReferenceLinePress`
|
|
1388
|
-
// would never fire. Sharing the gesture space lets the tap recognize; the pan
|
|
1389
|
-
// defers to a marker/badge under the finger via `deferTapHit` (see
|
|
1390
|
-
// `useCrosshair`'s scrub `onStart`) so no stray crosshair is dropped there.
|
|
1391
|
-
rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
// Compose the pan-scroll gesture. holdToScrub races the scrub/tap gestures (a
|
|
1395
|
-
// quick drag scrolls; a still press-hold falls through to scrub). Axis-drag
|
|
1396
|
-
// goes first via Exclusive: it fails instantly outside the axis band, so scrub
|
|
1397
|
-
// runs everywhere else.
|
|
1398
|
-
if (timeScrollEnabled) {
|
|
1399
|
-
rootGesture =
|
|
1400
|
-
scrollGestureMode === "axisDrag"
|
|
1401
|
-
? Gesture.Exclusive(panScrollGesture, rootGesture)
|
|
1402
|
-
: Gesture.Race(panScrollGesture, rootGesture);
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
// Draggable reference lines take priority: a vertical grab on a line drags it.
|
|
1406
|
-
// The manual-activation pan fails fast off any line (or on horizontal intent), so
|
|
1407
|
-
// Exclusive falls through to scrub / scroll everywhere else.
|
|
1408
|
-
if (refDragEnabled) {
|
|
1409
|
-
rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Pinch runs alongside everything else (two-finger, so it never competes with
|
|
1413
|
-
// the one-finger pan/scrub/tap gestures for the same touch).
|
|
1414
|
-
if (zoomEnabled) {
|
|
1415
|
-
rootGesture = Gesture.Simultaneous(rootGesture, pinchZoomGesture);
|
|
1416
|
-
}
|
|
2084
|
+
const rootGesture = composeLiveChartRootGesture({
|
|
2085
|
+
crosshair,
|
|
2086
|
+
markerTapGesture,
|
|
2087
|
+
refLineTapGesture,
|
|
2088
|
+
refDragGesture,
|
|
2089
|
+
panScrollGesture,
|
|
2090
|
+
pinchZoomGesture,
|
|
2091
|
+
markersActive,
|
|
2092
|
+
refPressActive,
|
|
2093
|
+
refDragEnabled,
|
|
2094
|
+
scrubActionActive: scrubActionCfg !== null,
|
|
2095
|
+
timeScrollEnabled,
|
|
2096
|
+
scrollGestureMode,
|
|
2097
|
+
zoomEnabled,
|
|
2098
|
+
});
|
|
1417
2099
|
|
|
1418
2100
|
// ── Derived render values ──────────────────────────────────────────────
|
|
1419
2101
|
const {
|
|
@@ -1446,49 +2128,33 @@ function useLiveChartController({
|
|
|
1446
2128
|
// A dot that tracks the true live point while parked is exempt from the
|
|
1447
2129
|
// scroll-back hide: it no longer marks an off-screen price, and it hides
|
|
1448
2130
|
// itself once the live point leaves the window (`useLiveDot`'s sentinel).
|
|
1449
|
-
const liveDotOpacity = useDerivedValue(
|
|
1450
|
-
() =>
|
|
1451
|
-
reveal.dotOpacity.value *
|
|
1452
|
-
(selectionDotDuringScrub && crosshairScrubActive.value ? 0 : 1) *
|
|
1453
|
-
liveIndicatorScrollOpacity(
|
|
1454
|
-
hideLiveOnScrollBack && !dotTracksParked,
|
|
1455
|
-
engine.viewEnd.value,
|
|
1456
|
-
) *
|
|
1457
|
-
seriesIndicatorOpacity.value,
|
|
1458
|
-
);
|
|
1459
|
-
// Same scrolled-back gating for the value line: it would draw a dashed line
|
|
1460
|
-
// at the live value's Y — a price that isn't in the scrolled-back view.
|
|
1461
|
-
const valueLineOpacity = useDerivedValue(
|
|
1462
|
-
() =>
|
|
1463
|
-
reveal.lineOpacity.value *
|
|
1464
|
-
liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
|
|
1465
|
-
resolvedSeriesOpacity.value,
|
|
1466
|
-
);
|
|
1467
|
-
const liveBadgeOpacity = useDerivedValue(
|
|
1468
|
-
() =>
|
|
1469
|
-
reveal.badgeOpacity.value *
|
|
1470
|
-
liveIndicatorScrollOpacity(hideLiveOnScrollBack, engine.viewEnd.value) *
|
|
1471
|
-
resolvedSeriesOpacity.value,
|
|
1472
|
-
);
|
|
1473
|
-
|
|
1474
|
-
// Fade the annotation overlays (markers + reference lines) out while scrubbing
|
|
1475
|
-
// when `scrub.hideOverlays` is set. Eased off the scrub-ACTIVE flag — not the
|
|
1476
|
-
// crosshair's edge-proximity fade, which drops to 0 near the live dot and would
|
|
1477
|
-
// resurface the overlays mid-scrub. Only this group opacity animates; the
|
|
1478
|
-
// marker atlas / reference-line draws stay intact (one batched draw each).
|
|
1479
2131
|
const fadeOverlaysOnScrub =
|
|
1480
2132
|
!isStatic && scrubCfg !== null && scrubCfg.hideOverlaysOnScrub === true;
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2133
|
+
const {
|
|
2134
|
+
liveDotOpacity,
|
|
2135
|
+
valueLineOpacity,
|
|
2136
|
+
liveBadgeOpacity,
|
|
2137
|
+
overlayScrubFade,
|
|
2138
|
+
markerGroupOpacity,
|
|
2139
|
+
} = useLiveIndicatorOpacities({
|
|
2140
|
+
reveal,
|
|
2141
|
+
seriesIndicatorOpacity,
|
|
2142
|
+
seriesOpacity: resolvedSeriesOpacity,
|
|
2143
|
+
scrubActive: crosshairScrubActive,
|
|
2144
|
+
selectionDotDuringScrub,
|
|
2145
|
+
hideLiveOnScrollBack,
|
|
2146
|
+
dotTracksParked,
|
|
2147
|
+
viewEnd: engine.viewEnd,
|
|
2148
|
+
fadeOverlaysOnScrub,
|
|
2149
|
+
});
|
|
2150
|
+
const modelDefaults = resolveLiveChartModelDefaults({
|
|
2151
|
+
isCandle,
|
|
2152
|
+
candleWidth,
|
|
2153
|
+
loadingCfg,
|
|
2154
|
+
volumeCfg,
|
|
2155
|
+
palette,
|
|
2156
|
+
lineProp,
|
|
2157
|
+
});
|
|
1492
2158
|
|
|
1493
2159
|
return {
|
|
1494
2160
|
// passthrough props the render needs
|
|
@@ -1505,9 +2171,7 @@ function useLiveChartController({
|
|
|
1505
2171
|
formatTime,
|
|
1506
2172
|
isCandle,
|
|
1507
2173
|
isStatic,
|
|
1508
|
-
|
|
1509
|
-
// candle's drawn center, not its bucket-start (left) edge. 0 in line mode.
|
|
1510
|
-
extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
|
|
2174
|
+
...modelDefaults,
|
|
1511
2175
|
// configs
|
|
1512
2176
|
yAxisCfg,
|
|
1513
2177
|
yAxisFloat: effectiveYAxisFloat,
|
|
@@ -1548,8 +2212,8 @@ function useLiveChartController({
|
|
|
1548
2212
|
hasRecolorSegments,
|
|
1549
2213
|
thresholdCfg,
|
|
1550
2214
|
thresholdGeom,
|
|
1551
|
-
thresholdStrokeColors
|
|
1552
|
-
thresholdFillColors
|
|
2215
|
+
thresholdStrokeColors,
|
|
2216
|
+
thresholdFillColors,
|
|
1553
2217
|
// Time-varying threshold (a `LiveChartPoint[]` series): the per-fragment split
|
|
1554
2218
|
// shader + polyline marker, vs. the constant case's gradient.
|
|
1555
2219
|
thresholdIsSeries,
|
|
@@ -1562,6 +2226,7 @@ function useLiveChartController({
|
|
|
1562
2226
|
thresholdMarkerValue,
|
|
1563
2227
|
thresholdCustomBadge,
|
|
1564
2228
|
thresholdSeriesPts,
|
|
2229
|
+
thresholdBadgeLeftAnchorX,
|
|
1565
2230
|
badgeUsesRightGutter,
|
|
1566
2231
|
// theme / layout / fonts
|
|
1567
2232
|
palette,
|
|
@@ -1574,13 +2239,8 @@ function useLiveChartController({
|
|
|
1574
2239
|
// engine + reveal
|
|
1575
2240
|
engine,
|
|
1576
2241
|
reveal,
|
|
2242
|
+
loadingActive,
|
|
1577
2243
|
axisAutoHideOpacity,
|
|
1578
|
-
// loading shell styling (null → not loading)
|
|
1579
|
-
loadingLineColor: loadingCfg?.color,
|
|
1580
|
-
loadingStrokeWidth: loadingCfg?.strokeWidth,
|
|
1581
|
-
loadingAmplitude: loadingCfg?.amplitude,
|
|
1582
|
-
loadingSpeed: loadingCfg?.speed,
|
|
1583
|
-
loadingAxisLabels: loadingCfg?.axisLabels ?? true,
|
|
1584
2244
|
// derived render values
|
|
1585
2245
|
backgroundColor,
|
|
1586
2246
|
gradientEnd,
|
|
@@ -1608,9 +2268,6 @@ function useLiveChartController({
|
|
|
1608
2268
|
// the candle palette). The reserved band height is read by the x-axis.
|
|
1609
2269
|
volumeActive: volumeCfg !== null,
|
|
1610
2270
|
volumeBandHeight,
|
|
1611
|
-
volumeOpacity: volumeCfg?.opacity ?? 1,
|
|
1612
|
-
volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
|
|
1613
|
-
volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
|
|
1614
2271
|
dotX,
|
|
1615
2272
|
dotY,
|
|
1616
2273
|
liveDotOpacity,
|
|
@@ -1631,7 +2288,6 @@ function useLiveChartController({
|
|
|
1631
2288
|
renderOverlay,
|
|
1632
2289
|
// selection dot: resolved config + fallback color (the chart line/accent color)
|
|
1633
2290
|
selectionDot: selectionDotCfg,
|
|
1634
|
-
selectionColor: lineProp?.color ?? palette.line,
|
|
1635
2291
|
// RN axis edge labels (floated over the canvas as a sibling layer)
|
|
1636
2292
|
topLabelCfg,
|
|
1637
2293
|
bottomLabelCfg,
|
|
@@ -1980,11 +2636,7 @@ function ChartCandleGapLayer({
|
|
|
1980
2636
|
model.metricsCfg.candle,
|
|
1981
2637
|
);
|
|
1982
2638
|
const batch = (
|
|
1983
|
-
<GapBridgePathBatch
|
|
1984
|
-
paths={paths}
|
|
1985
|
-
config={config}
|
|
1986
|
-
palette={model.palette}
|
|
1987
|
-
/>
|
|
2639
|
+
<GapBridgePathBatch paths={paths} config={config} palette={model.palette} />
|
|
1988
2640
|
);
|
|
1989
2641
|
return focusOtherCandles ? (
|
|
1990
2642
|
<>
|
|
@@ -2001,17 +2653,9 @@ function ChartCandleGapLayer({
|
|
|
2001
2653
|
/** Line-mode bridge paths live in a child so candle charts register no worklets. */
|
|
2002
2654
|
function ChartLineGapLayer({ model }: { model: LiveChartModel }) {
|
|
2003
2655
|
const config = model.lineGapsCfg!;
|
|
2004
|
-
const paths = useLineGapPaths(
|
|
2005
|
-
model.engine,
|
|
2006
|
-
model.effectivePadding,
|
|
2007
|
-
config,
|
|
2008
|
-
);
|
|
2656
|
+
const paths = useLineGapPaths(model.engine, model.effectivePadding, config);
|
|
2009
2657
|
return (
|
|
2010
|
-
<GapBridgePathBatch
|
|
2011
|
-
paths={paths}
|
|
2012
|
-
config={config}
|
|
2013
|
-
palette={model.palette}
|
|
2014
|
-
/>
|
|
2658
|
+
<GapBridgePathBatch paths={paths} config={config} palette={model.palette} />
|
|
2015
2659
|
);
|
|
2016
2660
|
}
|
|
2017
2661
|
|
|
@@ -2236,6 +2880,113 @@ function ChartCandleLayer({ model }: { model: LiveChartModel }) {
|
|
|
2236
2880
|
);
|
|
2237
2881
|
}
|
|
2238
2882
|
|
|
2883
|
+
function ChartLineStrokeShader({ model }: { model: LiveChartModel }) {
|
|
2884
|
+
const {
|
|
2885
|
+
engine,
|
|
2886
|
+
effectivePadding,
|
|
2887
|
+
palette,
|
|
2888
|
+
resolvedSegments,
|
|
2889
|
+
hasRecolorSegments,
|
|
2890
|
+
crosshair,
|
|
2891
|
+
thresholdCfg,
|
|
2892
|
+
thresholdGeom,
|
|
2893
|
+
thresholdStrokeColors,
|
|
2894
|
+
thresholdIsSeries,
|
|
2895
|
+
thresholdSeriesHasPoints,
|
|
2896
|
+
thresholdStrokeUniforms,
|
|
2897
|
+
lineProp,
|
|
2898
|
+
layoutWidth,
|
|
2899
|
+
} = model;
|
|
2900
|
+
if (thresholdIsSeries) {
|
|
2901
|
+
return thresholdSeriesHasPoints ? (
|
|
2902
|
+
<ThresholdSplitShader uniforms={thresholdStrokeUniforms} />
|
|
2903
|
+
) : null;
|
|
2904
|
+
}
|
|
2905
|
+
if (thresholdCfg && thresholdStrokeColors) {
|
|
2906
|
+
return (
|
|
2907
|
+
<LinearGradient
|
|
2908
|
+
start={vec(0, 0)}
|
|
2909
|
+
end={thresholdGeom.gradientEnd}
|
|
2910
|
+
colors={thresholdStrokeColors}
|
|
2911
|
+
positions={thresholdGeom.splitPositions}
|
|
2912
|
+
/>
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
if (hasRecolorSegments) {
|
|
2916
|
+
return (
|
|
2917
|
+
<SegmentLineGradient
|
|
2918
|
+
engine={engine}
|
|
2919
|
+
segments={resolvedSegments}
|
|
2920
|
+
padding={effectivePadding}
|
|
2921
|
+
baseColor={lineProp?.color ?? palette.line}
|
|
2922
|
+
scrubX={crosshair.scrubX}
|
|
2923
|
+
scrubActive={crosshair.scrubActive}
|
|
2924
|
+
/>
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
if (!lineProp?.colors?.length) return null;
|
|
2928
|
+
return (
|
|
2929
|
+
<LinearGradient
|
|
2930
|
+
start={vec(0, 0)}
|
|
2931
|
+
end={vec(layoutWidth, 0)}
|
|
2932
|
+
colors={lineProp.colors}
|
|
2933
|
+
/>
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
function ChartMainPlotLayer({
|
|
2938
|
+
model,
|
|
2939
|
+
yAxisEntries,
|
|
2940
|
+
}: {
|
|
2941
|
+
model: LiveChartModel;
|
|
2942
|
+
yAxisEntries: YAxisEntries | null;
|
|
2943
|
+
}) {
|
|
2944
|
+
const {
|
|
2945
|
+
engine,
|
|
2946
|
+
effectivePadding,
|
|
2947
|
+
palette,
|
|
2948
|
+
lineGroupOpacity,
|
|
2949
|
+
seriesOpacity,
|
|
2950
|
+
linePath,
|
|
2951
|
+
lineGapsCfg,
|
|
2952
|
+
strokeWidth,
|
|
2953
|
+
lineProp,
|
|
2954
|
+
isCandle,
|
|
2955
|
+
xAxisCfg,
|
|
2956
|
+
yAxisCfg,
|
|
2957
|
+
yAxisFloat,
|
|
2958
|
+
} = model;
|
|
2959
|
+
|
|
2960
|
+
return (
|
|
2961
|
+
<>
|
|
2962
|
+
<Group opacity={seriesOpacity}>
|
|
2963
|
+
<Group opacity={lineGroupOpacity}>
|
|
2964
|
+
<Path
|
|
2965
|
+
path={linePath}
|
|
2966
|
+
style="stroke"
|
|
2967
|
+
strokeWidth={strokeWidth}
|
|
2968
|
+
strokeCap={lineProp?.cap ?? "round"}
|
|
2969
|
+
strokeJoin={lineProp?.join ?? "round"}
|
|
2970
|
+
color={lineProp?.color ?? palette.line}
|
|
2971
|
+
>
|
|
2972
|
+
<ChartLineStrokeShader model={model} />
|
|
2973
|
+
</Path>
|
|
2974
|
+
{lineGapsCfg ? <ChartLineGapLayer model={model} /> : null}
|
|
2975
|
+
</Group>
|
|
2976
|
+
</Group>
|
|
2977
|
+
{isCandle ? <ChartCandleLayer model={model} /> : null}
|
|
2978
|
+
{yAxisCfg && yAxisFloat ? (
|
|
2979
|
+
<ChartYAxisLayer
|
|
2980
|
+
model={model}
|
|
2981
|
+
variant="labels"
|
|
2982
|
+
entries={yAxisEntries!}
|
|
2983
|
+
/>
|
|
2984
|
+
) : null}
|
|
2985
|
+
{xAxisCfg ? <ChartXAxisLayer model={model} /> : null}
|
|
2986
|
+
</>
|
|
2987
|
+
);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2239
2990
|
/** Main shaken chart stack drawn ABOVE the left-edge fade so the line stays crisp:
|
|
2240
2991
|
* segment dividers, value/reference lines, the line/candles, axes, dot, degen,
|
|
2241
2992
|
* markers, and the loading/empty art. Background fills are in `ChartFillLayer`
|
|
@@ -2278,6 +3029,7 @@ function ChartStack({
|
|
|
2278
3029
|
thresholdMarkerValue,
|
|
2279
3030
|
thresholdCustomBadge,
|
|
2280
3031
|
thresholdSeriesPts,
|
|
3032
|
+
thresholdBadgeLeftAnchorX,
|
|
2281
3033
|
formatValue,
|
|
2282
3034
|
lineGroupOpacity,
|
|
2283
3035
|
seriesOpacity,
|
|
@@ -2379,74 +3131,7 @@ function ChartStack({
|
|
|
2379
3131
|
/>
|
|
2380
3132
|
)}
|
|
2381
3133
|
|
|
2382
|
-
{
|
|
2383
|
-
full-width gradient paints the base color outside segments and each
|
|
2384
|
-
segment's color within — so the line itself is recolored/faded (alpha in
|
|
2385
|
-
the segment color reduces the line's opacity), not covered by an overlay. */}
|
|
2386
|
-
<Group opacity={seriesOpacity}>
|
|
2387
|
-
<Group opacity={lineGroupOpacity}>
|
|
2388
|
-
<Path
|
|
2389
|
-
path={linePath}
|
|
2390
|
-
style="stroke"
|
|
2391
|
-
strokeWidth={strokeWidth}
|
|
2392
|
-
strokeCap={lineProp?.cap ?? "round"}
|
|
2393
|
-
strokeJoin={lineProp?.join ?? "round"}
|
|
2394
|
-
color={lineProp?.color ?? palette.line}
|
|
2395
|
-
>
|
|
2396
|
-
{thresholdIsSeries ? (
|
|
2397
|
-
// Time-varying split: a per-fragment shader colors the stroke above/
|
|
2398
|
-
// below the threshold polyline. Supersedes line.colors + segments.
|
|
2399
|
-
// An empty series renders no paint child → the plain stroke color
|
|
2400
|
-
// (null here keeps the empty case out of the NaN constant gradient).
|
|
2401
|
-
thresholdSeriesHasPoints ? (
|
|
2402
|
-
<ThresholdSplitShader uniforms={thresholdStrokeUniforms} />
|
|
2403
|
-
) : null
|
|
2404
|
-
) : thresholdCfg && thresholdStrokeColors ? (
|
|
2405
|
-
// Vertical hard split at the threshold Y — supersedes line.colors and
|
|
2406
|
-
// segment recoloring for the stroke while a threshold is set.
|
|
2407
|
-
<LinearGradient
|
|
2408
|
-
start={vec(0, 0)}
|
|
2409
|
-
end={thresholdGeom.gradientEnd}
|
|
2410
|
-
colors={thresholdStrokeColors}
|
|
2411
|
-
positions={thresholdGeom.splitPositions}
|
|
2412
|
-
/>
|
|
2413
|
-
) : hasRecolorSegments ? (
|
|
2414
|
-
<SegmentLineGradient
|
|
2415
|
-
engine={engine}
|
|
2416
|
-
segments={resolvedSegments}
|
|
2417
|
-
padding={effectivePadding}
|
|
2418
|
-
baseColor={lineProp?.color ?? palette.line}
|
|
2419
|
-
scrubX={crosshair.scrubX}
|
|
2420
|
-
scrubActive={crosshair.scrubActive}
|
|
2421
|
-
/>
|
|
2422
|
-
) : lineProp?.colors?.length ? (
|
|
2423
|
-
<LinearGradient
|
|
2424
|
-
start={vec(0, 0)}
|
|
2425
|
-
end={vec(layoutWidth, 0)}
|
|
2426
|
-
colors={lineProp.colors}
|
|
2427
|
-
/>
|
|
2428
|
-
) : null}
|
|
2429
|
-
</Path>
|
|
2430
|
-
{lineGapsCfg && <ChartLineGapLayer model={model} />}
|
|
2431
|
-
</Group>
|
|
2432
|
-
</Group>
|
|
2433
|
-
|
|
2434
|
-
{isCandle && <ChartCandleLayer model={model} />}
|
|
2435
|
-
|
|
2436
|
-
{/* Floating axis: the labels float ABOVE the candles (right-aligned at the
|
|
2437
|
-
edge) so the plot runs full-width and candles stay fully visible behind
|
|
2438
|
-
them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
|
|
2439
|
-
{yAxisCfg && yAxisFloat && (
|
|
2440
|
-
<ChartYAxisLayer
|
|
2441
|
-
model={model}
|
|
2442
|
-
variant="labels"
|
|
2443
|
-
entries={yAxisEntries!}
|
|
2444
|
-
/>
|
|
2445
|
-
)}
|
|
2446
|
-
|
|
2447
|
-
{/* X-axis time labels. With a volume band the bottom padding is inflated by
|
|
2448
|
-
the band height; pass it so the axis shifts back to the very bottom. */}
|
|
2449
|
-
{xAxisCfg && <ChartXAxisLayer model={model} />}
|
|
3134
|
+
<ChartMainPlotLayer model={model} yAxisEntries={yAxisEntries} />
|
|
2450
3135
|
|
|
2451
3136
|
{/* Live dot — the badge is drawn later (after the scrub layer) so the
|
|
2452
3137
|
scrub dim never clips the live-price badge's left edge. Hidden while
|
|
@@ -2514,6 +3199,7 @@ function ChartStack({
|
|
|
2514
3199
|
palette={palette}
|
|
2515
3200
|
font={skiaFont}
|
|
2516
3201
|
formatValue={formatValue}
|
|
3202
|
+
leftAnchorX={thresholdBadgeLeftAnchorX}
|
|
2517
3203
|
/>
|
|
2518
3204
|
)}
|
|
2519
3205
|
|
|
@@ -3001,7 +3687,7 @@ function ChartCustomConsumerOverlay({ model }: { model: LiveChartModel }) {
|
|
|
3001
3687
|
return <ChartOverlayLayer render={renderOverlay!} context={overlayContext} />;
|
|
3002
3688
|
}
|
|
3003
3689
|
|
|
3004
|
-
function
|
|
3690
|
+
function ChartCanvas({
|
|
3005
3691
|
model,
|
|
3006
3692
|
yAxisEntries,
|
|
3007
3693
|
degen,
|
|
@@ -3011,36 +3697,173 @@ function ChartView({
|
|
|
3011
3697
|
degen: DegenState | null;
|
|
3012
3698
|
}) {
|
|
3013
3699
|
const {
|
|
3014
|
-
|
|
3700
|
+
canvasMode,
|
|
3701
|
+
engine,
|
|
3015
3702
|
backgroundColor,
|
|
3016
|
-
style,
|
|
3017
|
-
onLayout,
|
|
3018
|
-
accessibilityLabel,
|
|
3019
|
-
accessibilityRole,
|
|
3020
3703
|
leftEdgeFadeCfg,
|
|
3021
3704
|
effectivePadding,
|
|
3022
|
-
engine,
|
|
3023
3705
|
palette,
|
|
3024
|
-
|
|
3706
|
+
topConnector,
|
|
3707
|
+
bottomConnector,
|
|
3708
|
+
extremaTimeOffset,
|
|
3709
|
+
loadingActive,
|
|
3710
|
+
topLabelCfg,
|
|
3711
|
+
} = model;
|
|
3712
|
+
return (
|
|
3713
|
+
<Canvas
|
|
3714
|
+
key={canvasMode}
|
|
3715
|
+
style={{ flex: 1 }}
|
|
3716
|
+
opaque={canvasMode === "opaque"}
|
|
3717
|
+
>
|
|
3718
|
+
{canvasMode === "opaque" ? (
|
|
3719
|
+
<Rect
|
|
3720
|
+
x={0}
|
|
3721
|
+
y={0}
|
|
3722
|
+
width={engine.canvasWidth}
|
|
3723
|
+
height={engine.canvasHeight}
|
|
3724
|
+
color={backgroundColor}
|
|
3725
|
+
/>
|
|
3726
|
+
) : null}
|
|
3727
|
+
<ChartFillLayer model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
3728
|
+
{leftEdgeFadeCfg ? (
|
|
3729
|
+
<LeftEdgeFade
|
|
3730
|
+
paddingLeft={effectivePadding.left}
|
|
3731
|
+
fadeWidth={leftEdgeFadeCfg.width}
|
|
3732
|
+
startColor={leftEdgeFadeCfg.startColor}
|
|
3733
|
+
endColor={leftEdgeFadeCfg.endColor}
|
|
3734
|
+
engine={engine}
|
|
3735
|
+
opaqueBackgroundRgb={
|
|
3736
|
+
canvasMode === "opaque" ? palette.bgRgb : undefined
|
|
3737
|
+
}
|
|
3738
|
+
/>
|
|
3739
|
+
) : null}
|
|
3740
|
+
<ChartStack model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
3741
|
+
{topConnector || bottomConnector ? (
|
|
3742
|
+
<ExtremaConnectorOverlay
|
|
3743
|
+
engine={engine}
|
|
3744
|
+
padding={effectivePadding}
|
|
3745
|
+
extremaTimeOffset={extremaTimeOffset}
|
|
3746
|
+
top={topConnector}
|
|
3747
|
+
bottom={bottomConnector}
|
|
3748
|
+
hideExtrema={loadingActive}
|
|
3749
|
+
suppressBottomWhenCoincident={
|
|
3750
|
+
topLabelCfg?.position === "extrema" ||
|
|
3751
|
+
topLabelCfg?.position === "extrema-edge"
|
|
3752
|
+
}
|
|
3753
|
+
/>
|
|
3754
|
+
) : null}
|
|
3755
|
+
<ChartRefBadgeLayer
|
|
3756
|
+
model={model}
|
|
3757
|
+
degen={degen}
|
|
3758
|
+
yAxisEntries={yAxisEntries}
|
|
3759
|
+
/>
|
|
3760
|
+
<ChartValueOverlay model={model} degen={degen} />
|
|
3761
|
+
{model.tradeStreamResolved && model.tradeStream ? (
|
|
3762
|
+
<ChartTradeStreamLayer model={model} degen={degen} />
|
|
3763
|
+
) : null}
|
|
3764
|
+
<ChartScrubLayer model={model} degen={degen} />
|
|
3765
|
+
{model.badgeCfg ? <ChartBadgeLayer model={model} degen={degen} /> : null}
|
|
3766
|
+
<ChartScrubActionLayer model={model} />
|
|
3767
|
+
</Canvas>
|
|
3768
|
+
);
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
function ChartNativeOverlays({ model }: { model: LiveChartModel }) {
|
|
3772
|
+
const {
|
|
3025
3773
|
topLabelCfg,
|
|
3026
3774
|
bottomLabelCfg,
|
|
3775
|
+
engine,
|
|
3776
|
+
formatValue,
|
|
3777
|
+
palette,
|
|
3778
|
+
effectivePadding,
|
|
3779
|
+
extremaTimeOffset,
|
|
3780
|
+
loadingActive,
|
|
3781
|
+
thresholdCustomBadge,
|
|
3027
3782
|
thresholdCfg,
|
|
3028
3783
|
thresholdMarkerLineY,
|
|
3029
3784
|
thresholdBadgeVisible,
|
|
3030
|
-
|
|
3785
|
+
thresholdBadgeLeftAnchorX,
|
|
3031
3786
|
markersActive,
|
|
3032
3787
|
renderMarker,
|
|
3033
|
-
renderTooltip,
|
|
3034
|
-
renderOverlay,
|
|
3035
3788
|
renderReferenceLine,
|
|
3036
3789
|
renderOffAxisReferenceLine,
|
|
3037
3790
|
allRefLines,
|
|
3038
3791
|
scrubCfg,
|
|
3792
|
+
renderTooltip,
|
|
3039
3793
|
crosshair,
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3794
|
+
renderOverlay,
|
|
3795
|
+
} = model;
|
|
3796
|
+
const hasAnnotations =
|
|
3797
|
+
(markersActive && renderMarker != null) ||
|
|
3798
|
+
((renderReferenceLine != null || renderOffAxisReferenceLine != null) &&
|
|
3799
|
+
allRefLines.length > 0);
|
|
3800
|
+
return (
|
|
3801
|
+
<>
|
|
3802
|
+
{topLabelCfg || bottomLabelCfg ? (
|
|
3803
|
+
<AxisLabelOverlay
|
|
3804
|
+
topLabel={topLabelCfg}
|
|
3805
|
+
bottomLabel={bottomLabelCfg}
|
|
3806
|
+
engine={engine}
|
|
3807
|
+
formatValue={formatValue}
|
|
3808
|
+
defaultColor={palette.gridLabel}
|
|
3809
|
+
padding={effectivePadding}
|
|
3810
|
+
extremaTimeOffset={extremaTimeOffset}
|
|
3811
|
+
hideExtrema={loadingActive}
|
|
3812
|
+
/>
|
|
3813
|
+
) : null}
|
|
3814
|
+
{thresholdCustomBadge && thresholdCfg?.line ? (
|
|
3815
|
+
<CustomThresholdBadgeOverlay
|
|
3816
|
+
element={thresholdCustomBadge}
|
|
3817
|
+
engine={engine}
|
|
3818
|
+
padding={effectivePadding}
|
|
3819
|
+
y={thresholdMarkerLineY}
|
|
3820
|
+
visible={thresholdBadgeVisible}
|
|
3821
|
+
position={thresholdCfg.line.labelPosition}
|
|
3822
|
+
leftAnchorX={thresholdBadgeLeftAnchorX}
|
|
3823
|
+
/>
|
|
3824
|
+
) : null}
|
|
3825
|
+
{hasAnnotations ? <ChartCustomAnnotations model={model} /> : null}
|
|
3826
|
+
{scrubCfg && renderTooltip ? (
|
|
3827
|
+
<CustomTooltipOverlay
|
|
3828
|
+
renderTooltip={renderTooltip}
|
|
3829
|
+
scrubX={crosshair.scrubX}
|
|
3830
|
+
scrubValue={crosshair.scrubValue}
|
|
3831
|
+
scrubTime={crosshair.scrubTime}
|
|
3832
|
+
scrubActive={crosshair.scrubActive}
|
|
3833
|
+
scrubCandle={crosshair.scrubCandle}
|
|
3834
|
+
scrubGap={crosshair.scrubGap}
|
|
3835
|
+
tooltipLayout={crosshair.tooltipLayout}
|
|
3836
|
+
engine={engine}
|
|
3837
|
+
padding={effectivePadding}
|
|
3838
|
+
placement={scrubCfg.tooltipPlacement}
|
|
3839
|
+
margin={scrubCfg.tooltipMargin}
|
|
3840
|
+
crosshairFade={scrubCfg.crosshairFade}
|
|
3841
|
+
crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
|
|
3842
|
+
lineTop={crosshair.tooltipLineTop}
|
|
3843
|
+
scrubDotY={crosshair.scrubDotY}
|
|
3844
|
+
/>
|
|
3845
|
+
) : null}
|
|
3846
|
+
{renderOverlay ? <ChartCustomConsumerOverlay model={model} /> : null}
|
|
3847
|
+
</>
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
function ChartView({
|
|
3852
|
+
model,
|
|
3853
|
+
yAxisEntries,
|
|
3854
|
+
degen,
|
|
3855
|
+
}: {
|
|
3856
|
+
model: LiveChartModel;
|
|
3857
|
+
yAxisEntries: YAxisEntries | null;
|
|
3858
|
+
degen: DegenState | null;
|
|
3859
|
+
}) {
|
|
3860
|
+
const {
|
|
3861
|
+
rootGesture,
|
|
3862
|
+
backgroundColor,
|
|
3863
|
+
style,
|
|
3864
|
+
onLayout,
|
|
3865
|
+
accessibilityLabel,
|
|
3866
|
+
accessibilityRole,
|
|
3044
3867
|
} = model;
|
|
3045
3868
|
|
|
3046
3869
|
return (
|
|
@@ -3052,147 +3875,8 @@ function ChartView({
|
|
|
3052
3875
|
accessibilityLabel={accessibilityLabel}
|
|
3053
3876
|
accessibilityRole={accessibilityRole}
|
|
3054
3877
|
>
|
|
3055
|
-
{
|
|
3056
|
-
<
|
|
3057
|
-
key={canvasMode}
|
|
3058
|
-
style={{ flex: 1 }}
|
|
3059
|
-
opaque={canvasMode === "opaque"}
|
|
3060
|
-
>
|
|
3061
|
-
{canvasMode === "opaque" && (
|
|
3062
|
-
<Rect
|
|
3063
|
-
x={0}
|
|
3064
|
-
y={0}
|
|
3065
|
-
width={engine.canvasWidth}
|
|
3066
|
-
height={engine.canvasHeight}
|
|
3067
|
-
color={backgroundColor}
|
|
3068
|
-
/>
|
|
3069
|
-
)}
|
|
3070
|
-
{/* Background fills first, then the left-edge fade (a canvas-space sibling
|
|
3071
|
-
so dstOut blends correctly), then the line stack on top — so the fade
|
|
3072
|
-
softens only the fills and the line stays crisp at the left edge. */}
|
|
3073
|
-
<ChartFillLayer
|
|
3074
|
-
model={model}
|
|
3075
|
-
yAxisEntries={yAxisEntries}
|
|
3076
|
-
degen={degen}
|
|
3077
|
-
/>
|
|
3078
|
-
|
|
3079
|
-
{leftEdgeFadeCfg && (
|
|
3080
|
-
<LeftEdgeFade
|
|
3081
|
-
paddingLeft={effectivePadding.left}
|
|
3082
|
-
fadeWidth={leftEdgeFadeCfg.width}
|
|
3083
|
-
startColor={leftEdgeFadeCfg.startColor}
|
|
3084
|
-
endColor={leftEdgeFadeCfg.endColor}
|
|
3085
|
-
engine={engine}
|
|
3086
|
-
opaqueBackgroundRgb={
|
|
3087
|
-
canvasMode === "opaque" ? palette.bgRgb : undefined
|
|
3088
|
-
}
|
|
3089
|
-
/>
|
|
3090
|
-
)}
|
|
3091
|
-
|
|
3092
|
-
{/* Line stack above the fade so the line stays crisp at the left edge. */}
|
|
3093
|
-
<ChartStack model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
3094
|
-
|
|
3095
|
-
{/* "extrema-edge" connector lines (dot → edge readout), above the chart
|
|
3096
|
-
content so the dashed guide reads over the line / candles. */}
|
|
3097
|
-
{(topConnector || bottomConnector) && (
|
|
3098
|
-
<ExtremaConnectorOverlay
|
|
3099
|
-
engine={engine}
|
|
3100
|
-
padding={effectivePadding}
|
|
3101
|
-
extremaTimeOffset={extremaTimeOffset}
|
|
3102
|
-
top={topConnector}
|
|
3103
|
-
bottom={bottomConnector}
|
|
3104
|
-
/>
|
|
3105
|
-
)}
|
|
3106
|
-
|
|
3107
|
-
{/* Reference-line badges + labels above the fade so they stay crisp. */}
|
|
3108
|
-
<ChartRefBadgeLayer
|
|
3109
|
-
model={model}
|
|
3110
|
-
degen={degen}
|
|
3111
|
-
yAxisEntries={yAxisEntries}
|
|
3112
|
-
/>
|
|
3113
|
-
|
|
3114
|
-
<ChartValueOverlay model={model} degen={degen} />
|
|
3115
|
-
|
|
3116
|
-
{model.tradeStreamResolved && model.tradeStream && (
|
|
3117
|
-
<ChartTradeStreamLayer model={model} degen={degen} />
|
|
3118
|
-
)}
|
|
3119
|
-
|
|
3120
|
-
<ChartScrubLayer model={model} degen={degen} />
|
|
3121
|
-
|
|
3122
|
-
{/* Live-price badge on top of the scrub dim so the dim never clips
|
|
3123
|
-
its left edge (the badge tracks the live value, not the scrub). */}
|
|
3124
|
-
{model.badgeCfg && <ChartBadgeLayer model={model} degen={degen} />}
|
|
3125
|
-
|
|
3126
|
-
{/* Scrub-action reticle + action badge — top-most, no shake transform. */}
|
|
3127
|
-
<ChartScrubActionLayer model={model} />
|
|
3128
|
-
</Canvas>
|
|
3129
|
-
|
|
3130
|
-
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN view).
|
|
3131
|
-
Pinned to the plot's top/bottom edges via the resolved padding. */}
|
|
3132
|
-
{(topLabelCfg || bottomLabelCfg) && (
|
|
3133
|
-
<AxisLabelOverlay
|
|
3134
|
-
topLabel={topLabelCfg}
|
|
3135
|
-
bottomLabel={bottomLabelCfg}
|
|
3136
|
-
engine={engine}
|
|
3137
|
-
formatValue={formatValue}
|
|
3138
|
-
defaultColor={palette.gridLabel}
|
|
3139
|
-
padding={effectivePadding}
|
|
3140
|
-
extremaTimeOffset={extremaTimeOffset}
|
|
3141
|
-
/>
|
|
3142
|
-
)}
|
|
3143
|
-
|
|
3144
|
-
{/* Custom threshold badge — a native view positioned from the same live
|
|
3145
|
-
value/Y SharedValues as the built-in Skia badge. */}
|
|
3146
|
-
{thresholdCustomBadge && thresholdCfg?.line && (
|
|
3147
|
-
<CustomThresholdBadgeOverlay
|
|
3148
|
-
element={thresholdCustomBadge}
|
|
3149
|
-
engine={engine}
|
|
3150
|
-
padding={effectivePadding}
|
|
3151
|
-
y={thresholdMarkerLineY}
|
|
3152
|
-
visible={thresholdBadgeVisible}
|
|
3153
|
-
position={thresholdCfg.line.labelPosition}
|
|
3154
|
-
/>
|
|
3155
|
-
)}
|
|
3156
|
-
|
|
3157
|
-
{/* Custom-rendered markers — RN views floated over the canvas (non-Skia),
|
|
3158
|
-
pinned to each marker's live position. Sibling of <Canvas>. Wrapped in
|
|
3159
|
-
a box-none fade layer so `scrub.hideOverlaysOnScrub` hides them with the
|
|
3160
|
-
Skia markers (the wrapper is full-bleed; children keep their own
|
|
3161
|
-
absolute positions). */}
|
|
3162
|
-
{((markersActive && renderMarker) ||
|
|
3163
|
-
((renderReferenceLine || renderOffAxisReferenceLine) &&
|
|
3164
|
-
allRefLines.length > 0)) && (
|
|
3165
|
-
<ChartCustomAnnotations model={model} />
|
|
3166
|
-
)}
|
|
3167
|
-
|
|
3168
|
-
{/* Custom scrub tooltip — an RN view floated over the canvas (non-Skia),
|
|
3169
|
-
positioned on the UI thread. Sibling of <Canvas>. Works in line and
|
|
3170
|
-
candle mode (candle exposes the OHLC via `scrubCandle`). */}
|
|
3171
|
-
{scrubCfg && renderTooltip && (
|
|
3172
|
-
<CustomTooltipOverlay
|
|
3173
|
-
renderTooltip={renderTooltip}
|
|
3174
|
-
scrubX={crosshair.scrubX}
|
|
3175
|
-
scrubValue={crosshair.scrubValue}
|
|
3176
|
-
scrubTime={crosshair.scrubTime}
|
|
3177
|
-
scrubActive={crosshair.scrubActive}
|
|
3178
|
-
scrubCandle={crosshair.scrubCandle}
|
|
3179
|
-
scrubGap={crosshair.scrubGap}
|
|
3180
|
-
tooltipLayout={crosshair.tooltipLayout}
|
|
3181
|
-
engine={engine}
|
|
3182
|
-
padding={effectivePadding}
|
|
3183
|
-
placement={scrubCfg.tooltipPlacement}
|
|
3184
|
-
margin={scrubCfg.tooltipMargin}
|
|
3185
|
-
crosshairFade={scrubCfg.crosshairFade}
|
|
3186
|
-
crosshairFadeDistance={scrubCfg.crosshairFadeDistance}
|
|
3187
|
-
lineTop={crosshair.tooltipLineTop}
|
|
3188
|
-
scrubDotY={crosshair.scrubDotY}
|
|
3189
|
-
/>
|
|
3190
|
-
)}
|
|
3191
|
-
|
|
3192
|
-
{/* Custom consumer overlay — an RN view tree floated over the canvas with
|
|
3193
|
-
the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
|
|
3194
|
-
tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
|
|
3195
|
-
{renderOverlay && <ChartCustomConsumerOverlay model={model} />}
|
|
3878
|
+
<ChartCanvas model={model} yAxisEntries={yAxisEntries} degen={degen} />
|
|
3879
|
+
<ChartNativeOverlays model={model} />
|
|
3196
3880
|
</View>
|
|
3197
3881
|
</GestureDetector>
|
|
3198
3882
|
);
|