react-native-livechart 3.3.0 → 3.5.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/README.md +5 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +10 -3
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ScrubActionOverlay.d.ts +32 -0
- package/dist/components/ScrubActionOverlay.d.ts.map +1 -0
- package/dist/components/ThresholdLineOverlay.d.ts +34 -0
- package/dist/components/ThresholdLineOverlay.d.ts.map +1 -0
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +55 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +110 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +5 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +14 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +2 -0
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +36 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +23 -0
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -0
- package/dist/hooks/useThreshold.d.ts +22 -0
- package/dist/hooks/useThreshold.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/referenceLines.d.ts +23 -0
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +25 -0
- package/dist/math/threshold.d.ts.map +1 -0
- package/dist/types.d.ts +190 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +362 -61
- package/src/components/LiveChartSeries.tsx +39 -2
- package/src/components/ReferenceLineOverlay.tsx +121 -94
- package/src/components/ScrubActionOverlay.tsx +187 -0
- package/src/components/ThresholdLineOverlay.tsx +172 -0
- package/src/components/XAxisOverlay.tsx +2 -1
- package/src/constants.ts +5 -0
- package/src/core/resolveConfig.ts +102 -0
- package/src/hooks/crosshairShared.ts +293 -0
- package/src/hooks/useChartPaths.ts +25 -1
- package/src/hooks/useCrosshair.ts +340 -6
- package/src/hooks/useMarkers.ts +18 -2
- package/src/hooks/useReferenceLine.ts +259 -14
- package/src/hooks/useReferenceLinePress.ts +92 -0
- package/src/hooks/useThreshold.ts +63 -0
- package/src/index.ts +5 -0
- package/src/math/referenceLines.ts +55 -0
- package/src/math/threshold.ts +59 -0
- package/src/types.ts +195 -6
|
@@ -29,12 +29,15 @@ import {
|
|
|
29
29
|
resolveMetrics,
|
|
30
30
|
resolvePulse,
|
|
31
31
|
resolveScrub,
|
|
32
|
+
resolveScrubAction,
|
|
32
33
|
resolveSelectionDot,
|
|
34
|
+
resolveThreshold,
|
|
33
35
|
resolveTradeStream,
|
|
34
36
|
resolveValueLine,
|
|
35
37
|
resolveXAxis,
|
|
36
38
|
resolveYAxis,
|
|
37
39
|
} from "../core/resolveConfig";
|
|
40
|
+
import type { ResolvedThresholdConfig } from "../core/resolveConfig";
|
|
38
41
|
import { resolveSegment } from "../core/resolveSegment";
|
|
39
42
|
import { useLiveChartEngine } from "../core/useLiveChartEngine";
|
|
40
43
|
import { pulseRadialOutset } from "../draw/line";
|
|
@@ -51,10 +54,12 @@ import { useDegen } from "../hooks/useDegen";
|
|
|
51
54
|
import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
|
|
52
55
|
import { useLiveDot } from "../hooks/useLiveDot";
|
|
53
56
|
import { useMarkers } from "../hooks/useMarkers";
|
|
57
|
+
import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
|
|
54
58
|
import { useModeBlend } from "../hooks/useModeBlend";
|
|
55
59
|
import { useMomentum } from "../hooks/useMomentum";
|
|
56
60
|
import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
|
|
57
61
|
import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
|
|
62
|
+
import { useThreshold } from "../hooks/useThreshold";
|
|
58
63
|
import { useTradeStream } from "../hooks/useTradeStream";
|
|
59
64
|
import { useXAxis } from "../hooks/useXAxis";
|
|
60
65
|
import { useYAxis } from "../hooks/useYAxis";
|
|
@@ -67,9 +72,19 @@ import { collectReferenceValues } from "../math/referenceLines";
|
|
|
67
72
|
import {
|
|
68
73
|
applyPaletteOverride,
|
|
69
74
|
leftEdgeFadeColorsFromBgRgb,
|
|
75
|
+
parseColorRgb,
|
|
70
76
|
resolveTheme,
|
|
71
77
|
} from "../theme";
|
|
72
|
-
import type {
|
|
78
|
+
import type {
|
|
79
|
+
LiveChartPalette,
|
|
80
|
+
LiveChartProps,
|
|
81
|
+
Marker,
|
|
82
|
+
TradeEvent,
|
|
83
|
+
} from "../types";
|
|
84
|
+
import {
|
|
85
|
+
ThresholdBadgeOverlay,
|
|
86
|
+
ThresholdLineOverlay,
|
|
87
|
+
} from "./ThresholdLineOverlay";
|
|
73
88
|
import { AxisLabelOverlay } from "./AxisLabelOverlay";
|
|
74
89
|
import { BadgeOverlay } from "./BadgeOverlay";
|
|
75
90
|
import { CrosshairOverlay } from "./CrosshairOverlay";
|
|
@@ -81,12 +96,39 @@ import { MarkerOverlay } from "./MarkerOverlay";
|
|
|
81
96
|
import { MultiSeriesTooltipStack } from "./MultiSeriesTooltipStack";
|
|
82
97
|
import { ValueTextOverlay } from "./ValueTextOverlay";
|
|
83
98
|
import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
|
|
99
|
+
import { ScrubActionOverlay } from "./ScrubActionOverlay";
|
|
84
100
|
import { SegmentDividerOverlay } from "./SegmentDividerOverlay";
|
|
85
101
|
import { TradeStreamOverlay } from "./TradeStreamOverlay";
|
|
86
102
|
import { ValueLineOverlay } from "./ValueLineOverlay";
|
|
87
103
|
import { XAxisOverlay } from "./XAxisOverlay";
|
|
88
104
|
import { YAxisOverlay } from "./YAxisOverlay";
|
|
89
105
|
|
|
106
|
+
/** Translucent fill alpha for the threshold profit/loss band. */
|
|
107
|
+
const THRESHOLD_FILL_OPACITY = 0.16;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Color stops for the threshold's hard-split vertical gradient. Both arrays pair
|
|
111
|
+
* with the `[0, t, t, 1]` split positions: index 0–1 paint above the split,
|
|
112
|
+
* 2–3 below. `stroke` is full-strength; `fill` is the same hues at
|
|
113
|
+
* {@link THRESHOLD_FILL_OPACITY} for the band. Defaults to the palette's semantic
|
|
114
|
+
* up-green / down-red when colors are omitted.
|
|
115
|
+
*/
|
|
116
|
+
function thresholdStops(
|
|
117
|
+
cfg: ResolvedThresholdConfig,
|
|
118
|
+
palette: LiveChartPalette,
|
|
119
|
+
): { stroke: string[]; fill: string[] } {
|
|
120
|
+
const above = cfg.aboveColor ?? palette.candleUp;
|
|
121
|
+
const below = cfg.belowColor ?? palette.candleDown;
|
|
122
|
+
const [ar, ag, ab] = parseColorRgb(above);
|
|
123
|
+
const [br, bg, bb] = parseColorRgb(below);
|
|
124
|
+
const aboveFill = `rgba(${ar}, ${ag}, ${ab}, ${THRESHOLD_FILL_OPACITY})`;
|
|
125
|
+
const belowFill = `rgba(${br}, ${bg}, ${bb}, ${THRESHOLD_FILL_OPACITY})`;
|
|
126
|
+
return {
|
|
127
|
+
stroke: [above, above, below, below],
|
|
128
|
+
fill: [aboveFill, aboveFill, belowFill, belowFill],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
90
132
|
/**
|
|
91
133
|
* Resolves props → configs → theme/layout → engine → per-frame derived values and
|
|
92
134
|
* overlay hooks, returning a single render model. All the chart's wiring lives
|
|
@@ -145,10 +187,12 @@ function useLiveChartController({
|
|
|
145
187
|
valueMomentumColor = false,
|
|
146
188
|
referenceLines,
|
|
147
189
|
segments,
|
|
190
|
+
threshold,
|
|
148
191
|
gridStyle,
|
|
149
192
|
palette: paletteOverride,
|
|
150
193
|
metrics,
|
|
151
194
|
scrub = true,
|
|
195
|
+
scrubAction,
|
|
152
196
|
selectionDot,
|
|
153
197
|
tradeStream,
|
|
154
198
|
degen,
|
|
@@ -159,6 +203,8 @@ function useLiveChartController({
|
|
|
159
203
|
|
|
160
204
|
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
161
205
|
onScrub,
|
|
206
|
+
onScrubAction,
|
|
207
|
+
onReferenceLinePress,
|
|
162
208
|
onGestureStart,
|
|
163
209
|
onGestureEnd,
|
|
164
210
|
onDegenShake,
|
|
@@ -167,6 +213,9 @@ function useLiveChartController({
|
|
|
167
213
|
const tradeStreamSV = tradeStream ?? emptyTradeStream;
|
|
168
214
|
const emptyMarkers = useSharedValue<Marker[]>([]);
|
|
169
215
|
const markersSV = markers ?? emptyMarkers;
|
|
216
|
+
// Stand-in threshold value so `useThreshold` can be called unconditionally
|
|
217
|
+
// (hooks can't be); the geometry is ignored when no threshold is configured.
|
|
218
|
+
const emptyThresholdValue = useSharedValue(0);
|
|
170
219
|
const isCandle = mode === "candle";
|
|
171
220
|
|
|
172
221
|
// ── Resolve feature configs ────────────────────────────────────────────
|
|
@@ -176,7 +225,12 @@ function useLiveChartController({
|
|
|
176
225
|
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
177
226
|
const badgeCfg = resolveBadge(badge);
|
|
178
227
|
const scrubCfg = resolveScrub(scrub);
|
|
228
|
+
// Static charts run no gestures, so scrub-action (tap/drag to lock a price) is off.
|
|
229
|
+
const scrubActionCfg = isStatic ? null : resolveScrubAction(scrubAction);
|
|
179
230
|
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
231
|
+
// Threshold split is a line-mode feature (candle bodies carry their own up/down
|
|
232
|
+
// colors), so it's inert in candle mode — same as the area gradient.
|
|
233
|
+
const thresholdCfg = isCandle ? null : resolveThreshold(threshold);
|
|
180
234
|
const valueLineCfg = resolveValueLine(valueLine);
|
|
181
235
|
// Static charts run zero loops: force the pulse off so its `withRepeat`-driven
|
|
182
236
|
// ring never starts (the DotOverlay reads `pulseCfg`, so null = no pulse).
|
|
@@ -327,10 +381,23 @@ function useLiveChartController({
|
|
|
327
381
|
// ── Per-frame derived values ───────────────────────────────────────────
|
|
328
382
|
const { layoutWidth, layoutHeight, onLayout } = useCanvasLayout(engine);
|
|
329
383
|
|
|
330
|
-
|
|
384
|
+
// Threshold split geometry (shared by stroke gradient, fill band, marker line).
|
|
385
|
+
// Called unconditionally with a stand-in value when no threshold is set.
|
|
386
|
+
const thresholdGeom = useThreshold(
|
|
387
|
+
engine,
|
|
388
|
+
effectivePadding,
|
|
389
|
+
thresholdCfg?.value ?? emptyThresholdValue,
|
|
390
|
+
);
|
|
391
|
+
const thresholdStopColors = thresholdCfg
|
|
392
|
+
? thresholdStops(thresholdCfg, palette)
|
|
393
|
+
: null;
|
|
394
|
+
|
|
395
|
+
const { linePath, fillPath, thresholdFillPath } = useChartPaths(
|
|
331
396
|
engine,
|
|
332
397
|
effectivePadding,
|
|
333
398
|
reveal.morphT,
|
|
399
|
+
// Only build the band path when the fill is actually on.
|
|
400
|
+
thresholdCfg?.fill ? thresholdGeom.lineY : undefined,
|
|
334
401
|
);
|
|
335
402
|
const { upBodiesPath, downBodiesPath, upWicksPath, downWicksPath } =
|
|
336
403
|
useCandlePaths(
|
|
@@ -404,6 +471,46 @@ function useLiveChartController({
|
|
|
404
471
|
}
|
|
405
472
|
: undefined;
|
|
406
473
|
|
|
474
|
+
const markersActive = markers != null;
|
|
475
|
+
// `projected` is used internally by the hit-test gesture; the overlay
|
|
476
|
+
// self-projects, so we only need the gesture + hit-test here. Built BEFORE
|
|
477
|
+
// `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
|
|
478
|
+
const { tapGesture: markerTapGesture, hitTest: markerHitTest } = useMarkers(
|
|
479
|
+
engine,
|
|
480
|
+
effectivePadding,
|
|
481
|
+
markersSV,
|
|
482
|
+
!isStatic && markersActive,
|
|
483
|
+
markerHitRadius,
|
|
484
|
+
onMarkerHover,
|
|
485
|
+
undefined, // seriesSV — single-series has none
|
|
486
|
+
engine.data, // anchor value-less markers to the line
|
|
487
|
+
!isStatic, // static: no marker-projection loop
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// Pressable reference-line badges (working orders / alerts). Built before
|
|
491
|
+
// `useCrosshair` so the scrub-action tap can defer to a badge under the finger.
|
|
492
|
+
const refPressActive = onReferenceLinePress != null && allRefLines.length > 0;
|
|
493
|
+
const { tapGesture: refLineTapGesture, hitTest: refLineHitTest } =
|
|
494
|
+
useReferenceLinePress(
|
|
495
|
+
engine,
|
|
496
|
+
effectivePadding,
|
|
497
|
+
allRefLines,
|
|
498
|
+
skiaFont,
|
|
499
|
+
formatValue,
|
|
500
|
+
!isStatic && refPressActive,
|
|
501
|
+
markerHitRadius,
|
|
502
|
+
onReferenceLinePress,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// Combined "defer" hit-test for the scrub-action place-tap: yield to a marker or
|
|
506
|
+
// a pressable badge under the finger so the tap is routed there instead of
|
|
507
|
+
// dropping a reticle. (Both hit-tests return false when their feature is off.)
|
|
508
|
+
/* istanbul ignore next -- worklet runs on the UI thread, not in Jest */
|
|
509
|
+
const deferTapHit = (x: number, y: number): boolean => {
|
|
510
|
+
"worklet";
|
|
511
|
+
return markerHitTest(x, y) || refLineHitTest(x, y);
|
|
512
|
+
};
|
|
513
|
+
|
|
407
514
|
const crosshair = useCrosshair(
|
|
408
515
|
engine,
|
|
409
516
|
effectivePadding,
|
|
@@ -412,32 +519,51 @@ function useLiveChartController({
|
|
|
412
519
|
formatTime,
|
|
413
520
|
skiaFont,
|
|
414
521
|
// Static charts have an inert pan gesture — no scrub work on the UI thread.
|
|
415
|
-
|
|
522
|
+
// Scrub-action also needs the gesture live even when plain scrub is off.
|
|
523
|
+
!isStatic && (scrubCfg !== null || scrubActionCfg !== null),
|
|
416
524
|
onScrub,
|
|
417
525
|
candleOpts,
|
|
418
526
|
scrubCfg?.panGestureDelay ?? 0,
|
|
419
527
|
onGestureStart,
|
|
420
528
|
onGestureEnd,
|
|
529
|
+
scrubActionCfg,
|
|
530
|
+
onScrubAction,
|
|
531
|
+
metricsCfg.badge,
|
|
532
|
+
markersActive || refPressActive ? deferTapHit : undefined,
|
|
421
533
|
);
|
|
422
534
|
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
?
|
|
440
|
-
|
|
535
|
+
// Scrub-action composes a Tap (place/move the reticle, press the badge, dismiss)
|
|
536
|
+
// ahead of the pan via Exclusive, so a tap is tried first and only becomes a
|
|
537
|
+
// drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
|
|
538
|
+
// (not `Race`) prevents a jittery tap from being swallowed by the pan.
|
|
539
|
+
const baseGesture =
|
|
540
|
+
scrubActionCfg !== null && crosshair.tapGesture
|
|
541
|
+
? Gesture.Exclusive(crosshair.tapGesture, crosshair.gesture)
|
|
542
|
+
: crosshair.gesture;
|
|
543
|
+
|
|
544
|
+
// Overlay taps that hit-test discrete targets (marker dots, reference-line
|
|
545
|
+
// badges). They must all see each tap, so they're combined with `Simultaneous`
|
|
546
|
+
// (not `Race`, which cancels the loser and would drop one). The scrub-action
|
|
547
|
+
// action tap defers to them via `deferTapHit`, so a tap on an overlay is routed
|
|
548
|
+
// there instead of placing a reticle.
|
|
549
|
+
const overlayTaps = [
|
|
550
|
+
markersActive ? markerTapGesture : null,
|
|
551
|
+
refPressActive ? refLineTapGesture : null,
|
|
552
|
+
].filter((g): g is NonNullable<typeof g> => g !== null);
|
|
553
|
+
|
|
554
|
+
let rootGesture = baseGesture;
|
|
555
|
+
if (overlayTaps.length > 0) {
|
|
556
|
+
const tapGroup =
|
|
557
|
+
overlayTaps.length === 1
|
|
558
|
+
? overlayTaps[0]
|
|
559
|
+
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
560
|
+
// With scrub-action the action tap shares the gesture space with the overlay
|
|
561
|
+
// taps (Simultaneous); without it the pan only needs to race the tap group.
|
|
562
|
+
rootGesture =
|
|
563
|
+
scrubActionCfg !== null
|
|
564
|
+
? Gesture.Simultaneous(baseGesture, tapGroup)
|
|
565
|
+
: Gesture.Race(baseGesture, tapGroup);
|
|
566
|
+
}
|
|
441
567
|
|
|
442
568
|
// ── Derived render values ──────────────────────────────────────────────
|
|
443
569
|
const {
|
|
@@ -494,6 +620,7 @@ function useLiveChartController({
|
|
|
494
620
|
xAxisCfg,
|
|
495
621
|
badgeCfg,
|
|
496
622
|
scrubCfg,
|
|
623
|
+
scrubActionCfg,
|
|
497
624
|
gradientCfg,
|
|
498
625
|
valueLineCfg,
|
|
499
626
|
pulseCfg,
|
|
@@ -508,6 +635,10 @@ function useLiveChartController({
|
|
|
508
635
|
resolvedSegments,
|
|
509
636
|
hasRecolorSegments,
|
|
510
637
|
segmentGradient,
|
|
638
|
+
thresholdCfg,
|
|
639
|
+
thresholdGeom,
|
|
640
|
+
thresholdStrokeColors: thresholdStopColors?.stroke ?? null,
|
|
641
|
+
thresholdFillColors: thresholdStopColors?.fill ?? null,
|
|
511
642
|
badgeUsesRightGutter,
|
|
512
643
|
// theme / layout / fonts
|
|
513
644
|
palette,
|
|
@@ -531,6 +662,7 @@ function useLiveChartController({
|
|
|
531
662
|
onLayout,
|
|
532
663
|
linePath,
|
|
533
664
|
fillPath,
|
|
665
|
+
thresholdFillPath,
|
|
534
666
|
upBodiesPath,
|
|
535
667
|
downBodiesPath,
|
|
536
668
|
upWicksPath,
|
|
@@ -561,10 +693,13 @@ function useLiveChartController({
|
|
|
561
693
|
|
|
562
694
|
type LiveChartModel = ReturnType<typeof useLiveChartController>;
|
|
563
695
|
|
|
564
|
-
/**
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Background fills drawn BENEATH the left-edge fade: the y-axis grid, the area
|
|
698
|
+
* gradient, and the threshold profit/loss band. Split out from `ChartStack` so
|
|
699
|
+
* the fade's `dstOut` only softens the fills — the line and everything above it
|
|
700
|
+
* (drawn in `ChartStack`, after the fade) stay crisp at the left edge.
|
|
701
|
+
*/
|
|
702
|
+
function ChartFillLayer({ model }: { model: LiveChartModel }) {
|
|
568
703
|
const {
|
|
569
704
|
degenShakeTransform,
|
|
570
705
|
yAxisCfg,
|
|
@@ -577,41 +712,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
577
712
|
badgeUsesRightGutter,
|
|
578
713
|
badgeCfg,
|
|
579
714
|
gridStyleCfg,
|
|
715
|
+
metricsCfg,
|
|
580
716
|
gradientCfg,
|
|
581
717
|
fillPath,
|
|
582
718
|
gradientEnd,
|
|
583
719
|
gradientColors,
|
|
584
720
|
gradientPositions,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
hasRecolorSegments,
|
|
590
|
-
segmentGradient,
|
|
591
|
-
formatValue,
|
|
592
|
-
lineGroupOpacity,
|
|
593
|
-
linePath,
|
|
594
|
-
strokeWidth,
|
|
595
|
-
lineProp,
|
|
596
|
-
candleGroupOpacity,
|
|
597
|
-
upWicksPath,
|
|
598
|
-
downWicksPath,
|
|
599
|
-
upBodiesPath,
|
|
600
|
-
downBodiesPath,
|
|
601
|
-
xAxisCfg,
|
|
602
|
-
xAxisEntries,
|
|
603
|
-
dotX,
|
|
604
|
-
liveDotOpacity,
|
|
605
|
-
pulseCfg,
|
|
606
|
-
dotCfg,
|
|
607
|
-
degenCfg,
|
|
608
|
-
degenPack,
|
|
609
|
-
degenPackRevision,
|
|
610
|
-
markersActive,
|
|
611
|
-
markersSV,
|
|
612
|
-
emptyText,
|
|
613
|
-
metricsCfg,
|
|
614
|
-
layoutWidth,
|
|
721
|
+
thresholdCfg,
|
|
722
|
+
thresholdGeom,
|
|
723
|
+
thresholdFillPath,
|
|
724
|
+
thresholdFillColors,
|
|
615
725
|
} = model;
|
|
616
726
|
|
|
617
727
|
return (
|
|
@@ -647,6 +757,75 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
647
757
|
</Group>
|
|
648
758
|
)}
|
|
649
759
|
|
|
760
|
+
{/* Threshold profit/loss band — the area between the line and the threshold,
|
|
761
|
+
hard-split at the threshold Y into the above/below colors. Independent of
|
|
762
|
+
the baseline area fill above (set `gradient={false}` for the band alone). */}
|
|
763
|
+
{thresholdCfg?.fill && thresholdFillColors && (
|
|
764
|
+
<Group opacity={reveal.fillOpacity}>
|
|
765
|
+
<Path path={thresholdFillPath} style="fill">
|
|
766
|
+
<LinearGradient
|
|
767
|
+
start={vec(0, 0)}
|
|
768
|
+
end={thresholdGeom.gradientEnd}
|
|
769
|
+
colors={thresholdFillColors}
|
|
770
|
+
positions={thresholdGeom.splitPositions}
|
|
771
|
+
/>
|
|
772
|
+
</Path>
|
|
773
|
+
</Group>
|
|
774
|
+
)}
|
|
775
|
+
</Group>
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Main shaken chart stack drawn ABOVE the left-edge fade so the line stays crisp:
|
|
780
|
+
* segment dividers, value/reference lines, the line/candles, axes, dot, degen,
|
|
781
|
+
* markers, and the loading/empty art. Background fills are in `ChartFillLayer`
|
|
782
|
+
* (below the fade); the live value text is `ChartValueOverlay` (above the fade). */
|
|
783
|
+
function ChartStack({ model }: { model: LiveChartModel }) {
|
|
784
|
+
const {
|
|
785
|
+
degenShakeTransform,
|
|
786
|
+
reveal,
|
|
787
|
+
engine,
|
|
788
|
+
effectivePadding,
|
|
789
|
+
palette,
|
|
790
|
+
skiaFont,
|
|
791
|
+
badgeCfg,
|
|
792
|
+
valueLineCfg,
|
|
793
|
+
dotY,
|
|
794
|
+
allRefLines,
|
|
795
|
+
resolvedSegments,
|
|
796
|
+
hasRecolorSegments,
|
|
797
|
+
segmentGradient,
|
|
798
|
+
thresholdCfg,
|
|
799
|
+
thresholdGeom,
|
|
800
|
+
thresholdStrokeColors,
|
|
801
|
+
formatValue,
|
|
802
|
+
lineGroupOpacity,
|
|
803
|
+
linePath,
|
|
804
|
+
strokeWidth,
|
|
805
|
+
lineProp,
|
|
806
|
+
candleGroupOpacity,
|
|
807
|
+
upWicksPath,
|
|
808
|
+
downWicksPath,
|
|
809
|
+
upBodiesPath,
|
|
810
|
+
downBodiesPath,
|
|
811
|
+
xAxisCfg,
|
|
812
|
+
xAxisEntries,
|
|
813
|
+
dotX,
|
|
814
|
+
liveDotOpacity,
|
|
815
|
+
pulseCfg,
|
|
816
|
+
dotCfg,
|
|
817
|
+
degenCfg,
|
|
818
|
+
degenPack,
|
|
819
|
+
degenPackRevision,
|
|
820
|
+
markersActive,
|
|
821
|
+
markersSV,
|
|
822
|
+
emptyText,
|
|
823
|
+
metricsCfg,
|
|
824
|
+
layoutWidth,
|
|
825
|
+
} = model;
|
|
826
|
+
|
|
827
|
+
return (
|
|
828
|
+
<Group transform={degenShakeTransform}>
|
|
650
829
|
{/* Segment dividers + labels (behind the line). The scrub-focus emphasis is
|
|
651
830
|
painted on the line stroke itself, below — this overlay draws no fill. */}
|
|
652
831
|
{resolvedSegments.map((seg, i) => (
|
|
@@ -673,9 +852,12 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
673
852
|
</Group>
|
|
674
853
|
)}
|
|
675
854
|
|
|
676
|
-
{
|
|
855
|
+
{/* Index keys: reference lines are a positional array and two may share
|
|
856
|
+
value + label (e.g. duplicate working orders at the same price), which a
|
|
857
|
+
content-derived key would collapse to one. */}
|
|
858
|
+
{allRefLines.map((rl, i) => (
|
|
677
859
|
<ReferenceLineOverlay
|
|
678
|
-
key={
|
|
860
|
+
key={i}
|
|
679
861
|
engine={engine}
|
|
680
862
|
padding={effectivePadding}
|
|
681
863
|
line={rl}
|
|
@@ -685,6 +867,21 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
685
867
|
/>
|
|
686
868
|
))}
|
|
687
869
|
|
|
870
|
+
{/* Threshold marker line + label (behind the chart line). */}
|
|
871
|
+
{thresholdCfg?.line && (
|
|
872
|
+
<ThresholdLineOverlay
|
|
873
|
+
engine={engine}
|
|
874
|
+
padding={effectivePadding}
|
|
875
|
+
lineY={thresholdGeom.lineY}
|
|
876
|
+
visible={thresholdGeom.visible}
|
|
877
|
+
value={thresholdCfg.value}
|
|
878
|
+
cfg={thresholdCfg.line}
|
|
879
|
+
palette={palette}
|
|
880
|
+
font={skiaFont}
|
|
881
|
+
formatValue={formatValue}
|
|
882
|
+
/>
|
|
883
|
+
)}
|
|
884
|
+
|
|
688
885
|
{/* Chart line (fades out in candle mode). When segments recolor the line, a
|
|
689
886
|
full-width gradient paints the base color outside segments and each
|
|
690
887
|
segment's color within — so the line itself is recolored/faded (alpha in
|
|
@@ -698,7 +895,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
698
895
|
strokeJoin="round"
|
|
699
896
|
color={lineProp?.color ?? palette.line}
|
|
700
897
|
>
|
|
701
|
-
{
|
|
898
|
+
{thresholdCfg && thresholdStrokeColors ? (
|
|
899
|
+
// Vertical hard split at the threshold Y — supersedes line.colors and
|
|
900
|
+
// segment recoloring for the stroke while a threshold is set.
|
|
901
|
+
<LinearGradient
|
|
902
|
+
start={vec(0, 0)}
|
|
903
|
+
end={thresholdGeom.gradientEnd}
|
|
904
|
+
colors={thresholdStrokeColors}
|
|
905
|
+
positions={thresholdGeom.splitPositions}
|
|
906
|
+
/>
|
|
907
|
+
) : hasRecolorSegments ? (
|
|
702
908
|
<LinearGradient
|
|
703
909
|
start={vec(0, 0)}
|
|
704
910
|
end={segmentGradient.gradientEnd}
|
|
@@ -794,7 +1000,24 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
794
1000
|
</Group>
|
|
795
1001
|
)}
|
|
796
1002
|
|
|
797
|
-
{/*
|
|
1003
|
+
{/* Threshold label badge — on top of the line/dot/markers so it's never
|
|
1004
|
+
painted over (the dashed marker line itself stays behind the line, above). */}
|
|
1005
|
+
{thresholdCfg?.line && (
|
|
1006
|
+
<ThresholdBadgeOverlay
|
|
1007
|
+
engine={engine}
|
|
1008
|
+
padding={effectivePadding}
|
|
1009
|
+
lineY={thresholdGeom.lineY}
|
|
1010
|
+
visible={thresholdGeom.visible}
|
|
1011
|
+
value={thresholdCfg.value}
|
|
1012
|
+
cfg={thresholdCfg.line}
|
|
1013
|
+
palette={palette}
|
|
1014
|
+
font={skiaFont}
|
|
1015
|
+
formatValue={formatValue}
|
|
1016
|
+
/>
|
|
1017
|
+
)}
|
|
1018
|
+
|
|
1019
|
+
{/* Loading / empty state — drawn with the line stack (above the fade) so the
|
|
1020
|
+
squiggle/empty art stays crisp, consistent with the line. */}
|
|
798
1021
|
<LoadingOverlay
|
|
799
1022
|
engine={engine}
|
|
800
1023
|
padding={effectivePadding}
|
|
@@ -945,6 +1168,73 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
945
1168
|
);
|
|
946
1169
|
}
|
|
947
1170
|
|
|
1171
|
+
/** Reference-line badges + labels, drawn ABOVE the left-edge fade so a
|
|
1172
|
+
* left-pinned badge (off-axis / `labelBadge`) and any label stay crisp instead
|
|
1173
|
+
* of being erased by the fade's dstOut. The lines/bands themselves render in the
|
|
1174
|
+
* base pass inside ChartStack (behind the chart content). */
|
|
1175
|
+
function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1176
|
+
const {
|
|
1177
|
+
allRefLines,
|
|
1178
|
+
engine,
|
|
1179
|
+
effectivePadding,
|
|
1180
|
+
palette,
|
|
1181
|
+
formatValue,
|
|
1182
|
+
skiaFont,
|
|
1183
|
+
degenShakeTransform,
|
|
1184
|
+
} = model;
|
|
1185
|
+
if (allRefLines.length === 0) return null;
|
|
1186
|
+
return (
|
|
1187
|
+
<Group transform={degenShakeTransform}>
|
|
1188
|
+
{allRefLines.map((rl, i) => (
|
|
1189
|
+
<ReferenceLineOverlay
|
|
1190
|
+
key={i}
|
|
1191
|
+
engine={engine}
|
|
1192
|
+
padding={effectivePadding}
|
|
1193
|
+
line={rl}
|
|
1194
|
+
palette={palette}
|
|
1195
|
+
formatValue={formatValue}
|
|
1196
|
+
font={skiaFont}
|
|
1197
|
+
badgeLayer
|
|
1198
|
+
/>
|
|
1199
|
+
))}
|
|
1200
|
+
</Group>
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** Scrub-action ("order ticket") reticle + action badge. Drawn OUTSIDE the degen
|
|
1205
|
+
* shake group so the rendered badge stays aligned with the untransformed tap
|
|
1206
|
+
* hit-test; it tracks the locked reticle, not the shaken stack. */
|
|
1207
|
+
function ChartScrubActionLayer({ model }: { model: LiveChartModel }) {
|
|
1208
|
+
const { scrubActionCfg, crosshair, engine, effectivePadding, palette, skiaFont } =
|
|
1209
|
+
model;
|
|
1210
|
+
if (
|
|
1211
|
+
!scrubActionCfg ||
|
|
1212
|
+
!crosshair.lockActive ||
|
|
1213
|
+
!crosshair.lockX ||
|
|
1214
|
+
!crosshair.lockY ||
|
|
1215
|
+
!crosshair.actionBadge
|
|
1216
|
+
) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
return (
|
|
1220
|
+
<ScrubActionOverlay
|
|
1221
|
+
lockActive={crosshair.lockActive}
|
|
1222
|
+
lockX={crosshair.lockX}
|
|
1223
|
+
lockY={crosshair.lockY}
|
|
1224
|
+
actionBadge={crosshair.actionBadge}
|
|
1225
|
+
timeBadge={crosshair.timeBadge}
|
|
1226
|
+
engine={engine}
|
|
1227
|
+
padding={effectivePadding}
|
|
1228
|
+
palette={palette}
|
|
1229
|
+
font={skiaFont}
|
|
1230
|
+
icon={scrubActionCfg.icon}
|
|
1231
|
+
lineColor={scrubActionCfg.lineColor}
|
|
1232
|
+
background={scrubActionCfg.background}
|
|
1233
|
+
iconColor={scrubActionCfg.iconColor}
|
|
1234
|
+
/>
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
948
1238
|
export function LiveChart(props: LiveChartProps) {
|
|
949
1239
|
const model = useLiveChartController(props);
|
|
950
1240
|
const {
|
|
@@ -973,8 +1263,10 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
973
1263
|
accessibilityRole={accessibilityRole}
|
|
974
1264
|
>
|
|
975
1265
|
<Canvas style={{ flex: 1 }}>
|
|
976
|
-
{/*
|
|
977
|
-
|
|
1266
|
+
{/* Background fills first, then the left-edge fade (a canvas-space sibling
|
|
1267
|
+
so dstOut blends correctly), then the line stack on top — so the fade
|
|
1268
|
+
softens only the fills and the line stays crisp at the left edge. */}
|
|
1269
|
+
<ChartFillLayer model={model} />
|
|
978
1270
|
|
|
979
1271
|
{leftEdgeFadeCfg && (
|
|
980
1272
|
<LeftEdgeFade
|
|
@@ -986,6 +1278,12 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
986
1278
|
/>
|
|
987
1279
|
)}
|
|
988
1280
|
|
|
1281
|
+
{/* Line stack above the fade so the line stays crisp at the left edge. */}
|
|
1282
|
+
<ChartStack model={model} />
|
|
1283
|
+
|
|
1284
|
+
{/* Reference-line badges + labels above the fade so they stay crisp. */}
|
|
1285
|
+
<ChartRefBadgeLayer model={model} />
|
|
1286
|
+
|
|
989
1287
|
<ChartValueOverlay model={model} />
|
|
990
1288
|
|
|
991
1289
|
<ChartScrubLayer model={model} />
|
|
@@ -993,6 +1291,9 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
993
1291
|
{/* Live-price badge on top of the scrub dim so the dim never clips
|
|
994
1292
|
its left edge (the badge tracks the live value, not the scrub). */}
|
|
995
1293
|
<ChartBadgeLayer model={model} />
|
|
1294
|
+
|
|
1295
|
+
{/* Scrub-action reticle + action badge — top-most, no shake transform. */}
|
|
1296
|
+
<ChartScrubActionLayer model={model} />
|
|
996
1297
|
</Canvas>
|
|
997
1298
|
|
|
998
1299
|
{/* RN labels floated over the canvas (sibling of <Canvas>, an RN view).
|
|
@@ -448,9 +448,12 @@ function SeriesChartStack({ model }: { model: LiveChartSeriesModel }) {
|
|
|
448
448
|
</Group>
|
|
449
449
|
)}
|
|
450
450
|
|
|
451
|
-
{
|
|
451
|
+
{/* Index keys: reference lines are a positional array and two may share
|
|
452
|
+
value + label (e.g. duplicate working orders at the same price), which a
|
|
453
|
+
content-derived key would collapse to one. */}
|
|
454
|
+
{allRefLines.map((rl, i) => (
|
|
452
455
|
<ReferenceLineOverlay
|
|
453
|
-
key={
|
|
456
|
+
key={i}
|
|
454
457
|
engine={engine}
|
|
455
458
|
padding={effectivePadding}
|
|
456
459
|
line={rl}
|
|
@@ -589,6 +592,37 @@ function SeriesValueLabelLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
|
589
592
|
);
|
|
590
593
|
}
|
|
591
594
|
|
|
595
|
+
/** Reference-line badges + labels, drawn ABOVE the left-edge fade so they stay
|
|
596
|
+
* crisp (the lines/bands render in the base pass inside SeriesChartStack). */
|
|
597
|
+
function SeriesRefBadgeLayer({ model }: { model: LiveChartSeriesModel }) {
|
|
598
|
+
const {
|
|
599
|
+
allRefLines,
|
|
600
|
+
engine,
|
|
601
|
+
effectivePadding,
|
|
602
|
+
palette,
|
|
603
|
+
formatValue,
|
|
604
|
+
skiaFont,
|
|
605
|
+
degenShakeTransform,
|
|
606
|
+
} = model;
|
|
607
|
+
if (allRefLines.length === 0) return null;
|
|
608
|
+
return (
|
|
609
|
+
<Group transform={degenShakeTransform}>
|
|
610
|
+
{allRefLines.map((rl, i) => (
|
|
611
|
+
<ReferenceLineOverlay
|
|
612
|
+
key={i}
|
|
613
|
+
engine={engine}
|
|
614
|
+
padding={effectivePadding}
|
|
615
|
+
line={rl}
|
|
616
|
+
palette={palette}
|
|
617
|
+
formatValue={formatValue}
|
|
618
|
+
font={skiaFont}
|
|
619
|
+
badgeLayer
|
|
620
|
+
/>
|
|
621
|
+
))}
|
|
622
|
+
</Group>
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
592
626
|
export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
593
627
|
const model = useLiveChartSeriesController(props);
|
|
594
628
|
const {
|
|
@@ -664,6 +698,9 @@ export function LiveChartSeries(props: LiveChartSeriesProps) {
|
|
|
664
698
|
/>
|
|
665
699
|
)}
|
|
666
700
|
|
|
701
|
+
{/* Reference-line badges + labels above the fade so they stay crisp. */}
|
|
702
|
+
<SeriesRefBadgeLayer model={model} />
|
|
703
|
+
|
|
667
704
|
{scrubCfg && (
|
|
668
705
|
<CrosshairLine
|
|
669
706
|
scrubX={crosshair.scrubX}
|