react-native-livechart 3.2.0 → 3.4.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 +6 -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/SegmentDividerOverlay.d.ts +19 -0
- package/dist/components/SegmentDividerOverlay.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 +23 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/resolveSegment.d.ts +42 -0
- package/dist/core/resolveSegment.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +110 -0
- package/dist/hooks/crosshairShared.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/useSegmentDivider.d.ts +28 -0
- package/dist/hooks/useSegmentDivider.d.ts.map +1 -0
- package/dist/hooks/useSegmentLineGradient.d.ts +20 -0
- package/dist/hooks/useSegmentLineGradient.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/segments.d.ts +46 -0
- package/dist/math/segments.d.ts.map +1 -0
- package/dist/types.d.ts +184 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +242 -26
- package/src/components/LiveChartSeries.tsx +39 -2
- package/src/components/ReferenceLineOverlay.tsx +121 -94
- package/src/components/ScrubActionOverlay.tsx +187 -0
- package/src/components/SegmentDividerOverlay.tsx +84 -0
- package/src/components/XAxisOverlay.tsx +2 -1
- package/src/constants.ts +5 -0
- package/src/core/resolveConfig.ts +40 -0
- package/src/core/resolveSegment.ts +62 -0
- package/src/hooks/crosshairShared.ts +293 -0
- 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/useSegmentDivider.ts +84 -0
- package/src/hooks/useSegmentLineGradient.ts +57 -0
- package/src/index.ts +4 -0
- package/src/math/referenceLines.ts +55 -0
- package/src/math/segments.ts +173 -0
- package/src/types.ts +192 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLayoutEffect, useState } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
|
-
import { useSharedValue } from "react-native-reanimated";
|
|
4
|
+
import { useDerivedValue, useSharedValue } from "react-native-reanimated";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Single-series live chart. UX and prop vocabulary parallel Benji Taylor’s
|
|
@@ -29,12 +29,14 @@ import {
|
|
|
29
29
|
resolveMetrics,
|
|
30
30
|
resolvePulse,
|
|
31
31
|
resolveScrub,
|
|
32
|
+
resolveScrubAction,
|
|
32
33
|
resolveSelectionDot,
|
|
33
34
|
resolveTradeStream,
|
|
34
35
|
resolveValueLine,
|
|
35
36
|
resolveXAxis,
|
|
36
37
|
resolveYAxis,
|
|
37
38
|
} from "../core/resolveConfig";
|
|
39
|
+
import { resolveSegment } from "../core/resolveSegment";
|
|
38
40
|
import { useLiveChartEngine } from "../core/useLiveChartEngine";
|
|
39
41
|
import { pulseRadialOutset } from "../draw/line";
|
|
40
42
|
import { resolveChartLayout } from "../hooks/resolveChartLayout";
|
|
@@ -50,8 +52,10 @@ import { useDegen } from "../hooks/useDegen";
|
|
|
50
52
|
import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
|
|
51
53
|
import { useLiveDot } from "../hooks/useLiveDot";
|
|
52
54
|
import { useMarkers } from "../hooks/useMarkers";
|
|
55
|
+
import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
|
|
53
56
|
import { useModeBlend } from "../hooks/useModeBlend";
|
|
54
57
|
import { useMomentum } from "../hooks/useMomentum";
|
|
58
|
+
import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
|
|
55
59
|
import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
|
|
56
60
|
import { useTradeStream } from "../hooks/useTradeStream";
|
|
57
61
|
import { useXAxis } from "../hooks/useXAxis";
|
|
@@ -79,6 +83,8 @@ import { MarkerOverlay } from "./MarkerOverlay";
|
|
|
79
83
|
import { MultiSeriesTooltipStack } from "./MultiSeriesTooltipStack";
|
|
80
84
|
import { ValueTextOverlay } from "./ValueTextOverlay";
|
|
81
85
|
import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
|
|
86
|
+
import { ScrubActionOverlay } from "./ScrubActionOverlay";
|
|
87
|
+
import { SegmentDividerOverlay } from "./SegmentDividerOverlay";
|
|
82
88
|
import { TradeStreamOverlay } from "./TradeStreamOverlay";
|
|
83
89
|
import { ValueLineOverlay } from "./ValueLineOverlay";
|
|
84
90
|
import { XAxisOverlay } from "./XAxisOverlay";
|
|
@@ -141,10 +147,12 @@ function useLiveChartController({
|
|
|
141
147
|
showValue = false,
|
|
142
148
|
valueMomentumColor = false,
|
|
143
149
|
referenceLines,
|
|
150
|
+
segments,
|
|
144
151
|
gridStyle,
|
|
145
152
|
palette: paletteOverride,
|
|
146
153
|
metrics,
|
|
147
154
|
scrub = true,
|
|
155
|
+
scrubAction,
|
|
148
156
|
selectionDot,
|
|
149
157
|
tradeStream,
|
|
150
158
|
degen,
|
|
@@ -155,6 +163,8 @@ function useLiveChartController({
|
|
|
155
163
|
|
|
156
164
|
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
157
165
|
onScrub,
|
|
166
|
+
onScrubAction,
|
|
167
|
+
onReferenceLinePress,
|
|
158
168
|
onGestureStart,
|
|
159
169
|
onGestureEnd,
|
|
160
170
|
onDegenShake,
|
|
@@ -172,6 +182,8 @@ function useLiveChartController({
|
|
|
172
182
|
const bottomLabelCfg = resolveAxisLabel(bottomLabel);
|
|
173
183
|
const badgeCfg = resolveBadge(badge);
|
|
174
184
|
const scrubCfg = resolveScrub(scrub);
|
|
185
|
+
// Static charts run no gestures, so scrub-action (tap/drag to lock a price) is off.
|
|
186
|
+
const scrubActionCfg = isStatic ? null : resolveScrubAction(scrubAction);
|
|
175
187
|
const gradientCfg = isCandle ? null : resolveGradient(gradient);
|
|
176
188
|
const valueLineCfg = resolveValueLine(valueLine);
|
|
177
189
|
// Static charts run zero loops: force the pulse off so its `withRepeat`-driven
|
|
@@ -200,6 +212,20 @@ function useLiveChartController({
|
|
|
200
212
|
paletteOverride,
|
|
201
213
|
);
|
|
202
214
|
|
|
215
|
+
// Time-range segments (sessions, after-hours, …). Resolved once per render like
|
|
216
|
+
// reference lines; the divider/label/muted colors default to the chart palette
|
|
217
|
+
// (no per-segment base color needed). `hasRecolorSegments` is a render-time gate
|
|
218
|
+
// for the line-recolor gradient pass (per-frame visibility is handled by the
|
|
219
|
+
// gradient's transparent stops, not by mounting/unmounting the Path).
|
|
220
|
+
const resolvedSegments = (segments ?? []).map((s) =>
|
|
221
|
+
resolveSegment(s, {
|
|
222
|
+
muted: palette.gridLabel,
|
|
223
|
+
divider: palette.refLine,
|
|
224
|
+
label: palette.refLabel,
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
const hasRecolorSegments = resolvedSegments.some((s) => s.recolorLine);
|
|
228
|
+
|
|
203
229
|
const leftEdgeFadeCfg = resolveLeftEdgeFade(
|
|
204
230
|
leftEdgeFade,
|
|
205
231
|
leftEdgeFadeColorsFromBgRgb(palette.bgRgb),
|
|
@@ -386,6 +412,46 @@ function useLiveChartController({
|
|
|
386
412
|
}
|
|
387
413
|
: undefined;
|
|
388
414
|
|
|
415
|
+
const markersActive = markers != null;
|
|
416
|
+
// `projected` is used internally by the hit-test gesture; the overlay
|
|
417
|
+
// self-projects, so we only need the gesture + hit-test here. Built BEFORE
|
|
418
|
+
// `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
|
|
419
|
+
const { tapGesture: markerTapGesture, hitTest: markerHitTest } = useMarkers(
|
|
420
|
+
engine,
|
|
421
|
+
effectivePadding,
|
|
422
|
+
markersSV,
|
|
423
|
+
!isStatic && markersActive,
|
|
424
|
+
markerHitRadius,
|
|
425
|
+
onMarkerHover,
|
|
426
|
+
undefined, // seriesSV — single-series has none
|
|
427
|
+
engine.data, // anchor value-less markers to the line
|
|
428
|
+
!isStatic, // static: no marker-projection loop
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Pressable reference-line badges (working orders / alerts). Built before
|
|
432
|
+
// `useCrosshair` so the scrub-action tap can defer to a badge under the finger.
|
|
433
|
+
const refPressActive = onReferenceLinePress != null && allRefLines.length > 0;
|
|
434
|
+
const { tapGesture: refLineTapGesture, hitTest: refLineHitTest } =
|
|
435
|
+
useReferenceLinePress(
|
|
436
|
+
engine,
|
|
437
|
+
effectivePadding,
|
|
438
|
+
allRefLines,
|
|
439
|
+
skiaFont,
|
|
440
|
+
formatValue,
|
|
441
|
+
!isStatic && refPressActive,
|
|
442
|
+
markerHitRadius,
|
|
443
|
+
onReferenceLinePress,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Combined "defer" hit-test for the scrub-action place-tap: yield to a marker or
|
|
447
|
+
// a pressable badge under the finger so the tap is routed there instead of
|
|
448
|
+
// dropping a reticle. (Both hit-tests return false when their feature is off.)
|
|
449
|
+
/* istanbul ignore next -- worklet runs on the UI thread, not in Jest */
|
|
450
|
+
const deferTapHit = (x: number, y: number): boolean => {
|
|
451
|
+
"worklet";
|
|
452
|
+
return markerHitTest(x, y) || refLineHitTest(x, y);
|
|
453
|
+
};
|
|
454
|
+
|
|
389
455
|
const crosshair = useCrosshair(
|
|
390
456
|
engine,
|
|
391
457
|
effectivePadding,
|
|
@@ -394,32 +460,51 @@ function useLiveChartController({
|
|
|
394
460
|
formatTime,
|
|
395
461
|
skiaFont,
|
|
396
462
|
// Static charts have an inert pan gesture — no scrub work on the UI thread.
|
|
397
|
-
|
|
463
|
+
// Scrub-action also needs the gesture live even when plain scrub is off.
|
|
464
|
+
!isStatic && (scrubCfg !== null || scrubActionCfg !== null),
|
|
398
465
|
onScrub,
|
|
399
466
|
candleOpts,
|
|
400
467
|
scrubCfg?.panGestureDelay ?? 0,
|
|
401
468
|
onGestureStart,
|
|
402
469
|
onGestureEnd,
|
|
470
|
+
scrubActionCfg,
|
|
471
|
+
onScrubAction,
|
|
472
|
+
metricsCfg.badge,
|
|
473
|
+
markersActive || refPressActive ? deferTapHit : undefined,
|
|
403
474
|
);
|
|
404
475
|
|
|
405
|
-
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
?
|
|
422
|
-
|
|
476
|
+
// Scrub-action composes a Tap (place/move the reticle, press the badge, dismiss)
|
|
477
|
+
// ahead of the pan via Exclusive, so a tap is tried first and only becomes a
|
|
478
|
+
// drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
|
|
479
|
+
// (not `Race`) prevents a jittery tap from being swallowed by the pan.
|
|
480
|
+
const baseGesture =
|
|
481
|
+
scrubActionCfg !== null && crosshair.tapGesture
|
|
482
|
+
? Gesture.Exclusive(crosshair.tapGesture, crosshair.gesture)
|
|
483
|
+
: crosshair.gesture;
|
|
484
|
+
|
|
485
|
+
// Overlay taps that hit-test discrete targets (marker dots, reference-line
|
|
486
|
+
// badges). They must all see each tap, so they're combined with `Simultaneous`
|
|
487
|
+
// (not `Race`, which cancels the loser and would drop one). The scrub-action
|
|
488
|
+
// action tap defers to them via `deferTapHit`, so a tap on an overlay is routed
|
|
489
|
+
// there instead of placing a reticle.
|
|
490
|
+
const overlayTaps = [
|
|
491
|
+
markersActive ? markerTapGesture : null,
|
|
492
|
+
refPressActive ? refLineTapGesture : null,
|
|
493
|
+
].filter((g): g is NonNullable<typeof g> => g !== null);
|
|
494
|
+
|
|
495
|
+
let rootGesture = baseGesture;
|
|
496
|
+
if (overlayTaps.length > 0) {
|
|
497
|
+
const tapGroup =
|
|
498
|
+
overlayTaps.length === 1
|
|
499
|
+
? overlayTaps[0]
|
|
500
|
+
: Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
|
|
501
|
+
// With scrub-action the action tap shares the gesture space with the overlay
|
|
502
|
+
// taps (Simultaneous); without it the pan only needs to race the tap group.
|
|
503
|
+
rootGesture =
|
|
504
|
+
scrubActionCfg !== null
|
|
505
|
+
? Gesture.Simultaneous(baseGesture, tapGroup)
|
|
506
|
+
: Gesture.Race(baseGesture, tapGroup);
|
|
507
|
+
}
|
|
423
508
|
|
|
424
509
|
// ── Derived render values ──────────────────────────────────────────────
|
|
425
510
|
const {
|
|
@@ -437,6 +522,29 @@ function useLiveChartController({
|
|
|
437
522
|
effectivePadding,
|
|
438
523
|
);
|
|
439
524
|
|
|
525
|
+
// Scrub-focus gradient painted onto the line stroke: uniform at rest, and while
|
|
526
|
+
// scrubbing (or with an `active` segment) the focused segment stays full while
|
|
527
|
+
// the others are de-emphasized. Declared after `crosshair` so it can read the
|
|
528
|
+
// live scrub state.
|
|
529
|
+
const segmentGradient = useSegmentLineGradient(
|
|
530
|
+
engine,
|
|
531
|
+
resolvedSegments,
|
|
532
|
+
effectivePadding,
|
|
533
|
+
lineProp?.color ?? palette.line,
|
|
534
|
+
crosshair.scrubX,
|
|
535
|
+
crosshair.scrubActive,
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// Hide the live dot while scrubbing when a selection dot is marking the scrub
|
|
539
|
+
// point instead — otherwise both dots show at once.
|
|
540
|
+
const selectionDotDuringScrub =
|
|
541
|
+
!isStatic && scrubCfg !== null && selectionDotCfg !== null;
|
|
542
|
+
const liveDotOpacity = useDerivedValue(
|
|
543
|
+
() =>
|
|
544
|
+
reveal.dotOpacity.value *
|
|
545
|
+
(selectionDotDuringScrub && crosshair.scrubActive.value ? 0 : 1),
|
|
546
|
+
);
|
|
547
|
+
|
|
440
548
|
return {
|
|
441
549
|
// passthrough props the render needs
|
|
442
550
|
style,
|
|
@@ -453,6 +561,7 @@ function useLiveChartController({
|
|
|
453
561
|
xAxisCfg,
|
|
454
562
|
badgeCfg,
|
|
455
563
|
scrubCfg,
|
|
564
|
+
scrubActionCfg,
|
|
456
565
|
gradientCfg,
|
|
457
566
|
valueLineCfg,
|
|
458
567
|
pulseCfg,
|
|
@@ -464,6 +573,9 @@ function useLiveChartController({
|
|
|
464
573
|
leftEdgeFadeCfg,
|
|
465
574
|
metricsCfg,
|
|
466
575
|
allRefLines,
|
|
576
|
+
resolvedSegments,
|
|
577
|
+
hasRecolorSegments,
|
|
578
|
+
segmentGradient,
|
|
467
579
|
badgeUsesRightGutter,
|
|
468
580
|
// theme / layout / fonts
|
|
469
581
|
palette,
|
|
@@ -493,6 +605,7 @@ function useLiveChartController({
|
|
|
493
605
|
downWicksPath,
|
|
494
606
|
dotX,
|
|
495
607
|
dotY,
|
|
608
|
+
liveDotOpacity,
|
|
496
609
|
momentumSV,
|
|
497
610
|
tradeMarkers,
|
|
498
611
|
degenPack,
|
|
@@ -540,6 +653,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
540
653
|
valueLineCfg,
|
|
541
654
|
dotY,
|
|
542
655
|
allRefLines,
|
|
656
|
+
resolvedSegments,
|
|
657
|
+
hasRecolorSegments,
|
|
658
|
+
segmentGradient,
|
|
543
659
|
formatValue,
|
|
544
660
|
lineGroupOpacity,
|
|
545
661
|
linePath,
|
|
@@ -553,6 +669,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
553
669
|
xAxisCfg,
|
|
554
670
|
xAxisEntries,
|
|
555
671
|
dotX,
|
|
672
|
+
liveDotOpacity,
|
|
556
673
|
pulseCfg,
|
|
557
674
|
dotCfg,
|
|
558
675
|
degenCfg,
|
|
@@ -598,6 +715,18 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
598
715
|
</Group>
|
|
599
716
|
)}
|
|
600
717
|
|
|
718
|
+
{/* Segment dividers + labels (behind the line). The scrub-focus emphasis is
|
|
719
|
+
painted on the line stroke itself, below — this overlay draws no fill. */}
|
|
720
|
+
{resolvedSegments.map((seg, i) => (
|
|
721
|
+
<SegmentDividerOverlay
|
|
722
|
+
key={`seg-${seg.from ?? "start"}-${seg.to ?? "end"}-${i}`}
|
|
723
|
+
engine={engine}
|
|
724
|
+
padding={effectivePadding}
|
|
725
|
+
segment={seg}
|
|
726
|
+
font={skiaFont}
|
|
727
|
+
/>
|
|
728
|
+
))}
|
|
729
|
+
|
|
601
730
|
{/* Value line + reference line (behind chart line) */}
|
|
602
731
|
{valueLineCfg && (
|
|
603
732
|
<Group opacity={reveal.lineOpacity}>
|
|
@@ -612,9 +741,12 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
612
741
|
</Group>
|
|
613
742
|
)}
|
|
614
743
|
|
|
615
|
-
{
|
|
744
|
+
{/* Index keys: reference lines are a positional array and two may share
|
|
745
|
+
value + label (e.g. duplicate working orders at the same price), which a
|
|
746
|
+
content-derived key would collapse to one. */}
|
|
747
|
+
{allRefLines.map((rl, i) => (
|
|
616
748
|
<ReferenceLineOverlay
|
|
617
|
-
key={
|
|
749
|
+
key={i}
|
|
618
750
|
engine={engine}
|
|
619
751
|
padding={effectivePadding}
|
|
620
752
|
line={rl}
|
|
@@ -624,7 +756,10 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
624
756
|
/>
|
|
625
757
|
))}
|
|
626
758
|
|
|
627
|
-
{/* Chart line (fades out in candle mode)
|
|
759
|
+
{/* Chart line (fades out in candle mode). When segments recolor the line, a
|
|
760
|
+
full-width gradient paints the base color outside segments and each
|
|
761
|
+
segment's color within — so the line itself is recolored/faded (alpha in
|
|
762
|
+
the segment color reduces the line's opacity), not covered by an overlay. */}
|
|
628
763
|
<Group opacity={lineGroupOpacity}>
|
|
629
764
|
<Path
|
|
630
765
|
path={linePath}
|
|
@@ -634,7 +769,14 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
634
769
|
strokeJoin="round"
|
|
635
770
|
color={lineProp?.color ?? palette.line}
|
|
636
771
|
>
|
|
637
|
-
{
|
|
772
|
+
{hasRecolorSegments ? (
|
|
773
|
+
<LinearGradient
|
|
774
|
+
start={vec(0, 0)}
|
|
775
|
+
end={segmentGradient.gradientEnd}
|
|
776
|
+
colors={segmentGradient.colors}
|
|
777
|
+
positions={segmentGradient.positions}
|
|
778
|
+
/>
|
|
779
|
+
) : lineProp?.colors?.length ? (
|
|
638
780
|
<LinearGradient
|
|
639
781
|
start={vec(0, 0)}
|
|
640
782
|
end={vec(layoutWidth, 0)}
|
|
@@ -678,9 +820,10 @@ function ChartStack({ model }: { model: LiveChartModel }) {
|
|
|
678
820
|
)}
|
|
679
821
|
|
|
680
822
|
{/* Live dot — the badge is drawn later (after the scrub layer) so the
|
|
681
|
-
scrub dim never clips the live-price badge's left edge.
|
|
823
|
+
scrub dim never clips the live-price badge's left edge. Hidden while
|
|
824
|
+
scrubbing when a selection dot marks the scrub point instead. */}
|
|
682
825
|
{dotCfg.show && (
|
|
683
|
-
<Group opacity={
|
|
826
|
+
<Group opacity={liveDotOpacity}>
|
|
684
827
|
<DotOverlay
|
|
685
828
|
dotX={dotX}
|
|
686
829
|
dotY={dotY}
|
|
@@ -873,6 +1016,73 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
|
873
1016
|
);
|
|
874
1017
|
}
|
|
875
1018
|
|
|
1019
|
+
/** Reference-line badges + labels, drawn ABOVE the left-edge fade so a
|
|
1020
|
+
* left-pinned badge (off-axis / `labelBadge`) and any label stay crisp instead
|
|
1021
|
+
* of being erased by the fade's dstOut. The lines/bands themselves render in the
|
|
1022
|
+
* base pass inside ChartStack (behind the chart content). */
|
|
1023
|
+
function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
|
|
1024
|
+
const {
|
|
1025
|
+
allRefLines,
|
|
1026
|
+
engine,
|
|
1027
|
+
effectivePadding,
|
|
1028
|
+
palette,
|
|
1029
|
+
formatValue,
|
|
1030
|
+
skiaFont,
|
|
1031
|
+
degenShakeTransform,
|
|
1032
|
+
} = model;
|
|
1033
|
+
if (allRefLines.length === 0) return null;
|
|
1034
|
+
return (
|
|
1035
|
+
<Group transform={degenShakeTransform}>
|
|
1036
|
+
{allRefLines.map((rl, i) => (
|
|
1037
|
+
<ReferenceLineOverlay
|
|
1038
|
+
key={i}
|
|
1039
|
+
engine={engine}
|
|
1040
|
+
padding={effectivePadding}
|
|
1041
|
+
line={rl}
|
|
1042
|
+
palette={palette}
|
|
1043
|
+
formatValue={formatValue}
|
|
1044
|
+
font={skiaFont}
|
|
1045
|
+
badgeLayer
|
|
1046
|
+
/>
|
|
1047
|
+
))}
|
|
1048
|
+
</Group>
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** Scrub-action ("order ticket") reticle + action badge. Drawn OUTSIDE the degen
|
|
1053
|
+
* shake group so the rendered badge stays aligned with the untransformed tap
|
|
1054
|
+
* hit-test; it tracks the locked reticle, not the shaken stack. */
|
|
1055
|
+
function ChartScrubActionLayer({ model }: { model: LiveChartModel }) {
|
|
1056
|
+
const { scrubActionCfg, crosshair, engine, effectivePadding, palette, skiaFont } =
|
|
1057
|
+
model;
|
|
1058
|
+
if (
|
|
1059
|
+
!scrubActionCfg ||
|
|
1060
|
+
!crosshair.lockActive ||
|
|
1061
|
+
!crosshair.lockX ||
|
|
1062
|
+
!crosshair.lockY ||
|
|
1063
|
+
!crosshair.actionBadge
|
|
1064
|
+
) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
return (
|
|
1068
|
+
<ScrubActionOverlay
|
|
1069
|
+
lockActive={crosshair.lockActive}
|
|
1070
|
+
lockX={crosshair.lockX}
|
|
1071
|
+
lockY={crosshair.lockY}
|
|
1072
|
+
actionBadge={crosshair.actionBadge}
|
|
1073
|
+
timeBadge={crosshair.timeBadge}
|
|
1074
|
+
engine={engine}
|
|
1075
|
+
padding={effectivePadding}
|
|
1076
|
+
palette={palette}
|
|
1077
|
+
font={skiaFont}
|
|
1078
|
+
icon={scrubActionCfg.icon}
|
|
1079
|
+
lineColor={scrubActionCfg.lineColor}
|
|
1080
|
+
background={scrubActionCfg.background}
|
|
1081
|
+
iconColor={scrubActionCfg.iconColor}
|
|
1082
|
+
/>
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
876
1086
|
export function LiveChart(props: LiveChartProps) {
|
|
877
1087
|
const model = useLiveChartController(props);
|
|
878
1088
|
const {
|
|
@@ -914,6 +1124,9 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
914
1124
|
/>
|
|
915
1125
|
)}
|
|
916
1126
|
|
|
1127
|
+
{/* Reference-line badges + labels above the fade so they stay crisp. */}
|
|
1128
|
+
<ChartRefBadgeLayer model={model} />
|
|
1129
|
+
|
|
917
1130
|
<ChartValueOverlay model={model} />
|
|
918
1131
|
|
|
919
1132
|
<ChartScrubLayer model={model} />
|
|
@@ -921,6 +1134,9 @@ export function LiveChart(props: LiveChartProps) {
|
|
|
921
1134
|
{/* Live-price badge on top of the scrub dim so the dim never clips
|
|
922
1135
|
its left edge (the badge tracks the live value, not the scrub). */}
|
|
923
1136
|
<ChartBadgeLayer model={model} />
|
|
1137
|
+
|
|
1138
|
+
{/* Scrub-action reticle + action badge — top-most, no shake transform. */}
|
|
1139
|
+
<ChartScrubActionLayer model={model} />
|
|
924
1140
|
</Canvas>
|
|
925
1141
|
|
|
926
1142
|
{/* 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}
|