react-native-livechart 4.20.0 → 4.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AxisLabelOverlay.d.ts +5 -1
- package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +4 -2
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomThresholdBadgeOverlay.d.ts +18 -0
- package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +4 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
- package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
- package/dist/components/ExtremaConnectorOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts +4 -2
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +10 -5
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
- package/dist/core/chartDataPresence.d.ts +8 -0
- package/dist/core/chartDataPresence.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +6 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +32 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +9 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +3 -1
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/candleGap.d.ts +14 -0
- package/dist/draw/candleGap.d.ts.map +1 -0
- package/dist/draw/lineGap.d.ts +25 -0
- package/dist/draw/lineGap.d.ts.map +1 -0
- package/dist/draw/markerAtlas.d.ts +3 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +7 -3
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCandleGapPaths.d.ts +12 -0
- package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +4 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useChartReveal.d.ts +1 -1
- package/dist/hooks/useChartReveal.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -5
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useLineGapPaths.d.ts +10 -0
- package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +2 -2
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +4 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
- package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +13 -13
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/candleGaps.d.ts +18 -0
- package/dist/math/candleGaps.d.ts.map +1 -0
- package/dist/math/referenceLineSeries.d.ts +12 -0
- package/dist/math/referenceLineSeries.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +5 -5
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/math/spline.d.ts +5 -1
- package/dist/math/spline.d.ts.map +1 -1
- package/dist/types.d.ts +191 -17
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AxisLabelOverlay.tsx +162 -81
- package/src/components/CrosshairOverlay.tsx +394 -147
- package/src/components/CustomMarkerOverlay.tsx +9 -6
- package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
- package/src/components/CustomTooltipOverlay.tsx +7 -3
- package/src/components/DegenParticlesOverlay.tsx +34 -23
- package/src/components/ExtremaConnectorOverlay.tsx +43 -3
- package/src/components/LiveChart.tsx +1918 -974
- package/src/components/LiveChartSeries.tsx +445 -296
- package/src/components/MarkerOverlay.tsx +215 -156
- package/src/components/ReferenceLineOverlay.tsx +113 -55
- package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
- package/src/core/chartDataPresence.ts +24 -0
- package/src/core/liveChartEngineTick.ts +34 -1
- package/src/core/resolveConfig.ts +186 -0
- package/src/core/useLiveChartEngine.ts +36 -1
- package/src/core/useLiveChartSeriesEngine.ts +2 -0
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- package/src/draw/lineGap.ts +180 -0
- package/src/draw/markerAtlas.ts +8 -3
- package/src/hooks/crosshairShared.ts +92 -11
- package/src/hooks/useCandleGapPaths.ts +88 -0
- package/src/hooks/useChartPaths.ts +81 -22
- package/src/hooks/useChartReveal.ts +1 -1
- package/src/hooks/useCrosshair.ts +178 -69
- package/src/hooks/useDegen.ts +82 -53
- package/src/hooks/useLineGapPaths.ts +76 -0
- package/src/hooks/useLiveChartHasData.ts +8 -4
- package/src/hooks/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
- package/src/hooks/useThreshold.ts +31 -22
- package/src/index.ts +15 -0
- package/src/math/candleGaps.ts +120 -0
- package/src/math/referenceLineSeries.ts +83 -0
- package/src/math/referenceLines.ts +34 -6
- package/src/math/spline.ts +15 -5
- package/src/types.ts +202 -17
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
type SharedValue,
|
|
18
18
|
} from "react-native-reanimated";
|
|
19
19
|
import { MS_PER_FRAME_60FPS, RETURN_TO_LIVE_MS } from "../constants";
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
CandleGap,
|
|
22
|
+
CandlePoint,
|
|
23
|
+
LiveChartPoint,
|
|
24
|
+
SeriesConfig,
|
|
25
|
+
} from "../types";
|
|
21
26
|
import {
|
|
22
27
|
tickLiveChartEngineFrame,
|
|
23
28
|
type EngineTickInput,
|
|
@@ -95,6 +100,10 @@ export interface EngineConfig {
|
|
|
95
100
|
mode?: "line" | "candle";
|
|
96
101
|
candles?: SharedValue<CandlePoint[]>;
|
|
97
102
|
liveCandle?: SharedValue<CandlePoint | null>;
|
|
103
|
+
candleGaps?: CandleGap[];
|
|
104
|
+
candleGapBridgeNoTrades?: boolean;
|
|
105
|
+
candleGapBridgeUnavailable?: boolean;
|
|
106
|
+
candleGapBridgeUnknown?: boolean;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
/** Canvas, time window, and Y-range — shared by single- and multi-series engines. */
|
|
@@ -238,6 +247,10 @@ export interface EngineFrameRefs {
|
|
|
238
247
|
modeSV: SharedValue<"line" | "candle">;
|
|
239
248
|
candles?: SharedValue<CandlePoint[]>;
|
|
240
249
|
liveCandle?: SharedValue<CandlePoint | null>;
|
|
250
|
+
candleGapsSV?: SharedValue<CandleGap[] | undefined>;
|
|
251
|
+
candleGapBridgeNoTradesSV?: SharedValue<boolean>;
|
|
252
|
+
candleGapBridgeUnavailableSV?: SharedValue<boolean>;
|
|
253
|
+
candleGapBridgeUnknownSV?: SharedValue<boolean>;
|
|
241
254
|
extremaMinValue: SharedValue<number>;
|
|
242
255
|
extremaMaxValue: SharedValue<number>;
|
|
243
256
|
extremaMinTime: SharedValue<number>;
|
|
@@ -359,6 +372,12 @@ export function applyLiveChartEngineFrame(
|
|
|
359
372
|
input.mode = sv.modeSV.value;
|
|
360
373
|
input.candles = sv.candles?.value;
|
|
361
374
|
input.liveCandle = sv.liveCandle?.value;
|
|
375
|
+
input.candleGaps = sv.candleGapsSV?.value;
|
|
376
|
+
input.candleGapBridgeNoTrades =
|
|
377
|
+
sv.candleGapBridgeNoTradesSV?.value ?? false;
|
|
378
|
+
input.candleGapBridgeUnavailable =
|
|
379
|
+
sv.candleGapBridgeUnavailableSV?.value ?? false;
|
|
380
|
+
input.candleGapBridgeUnknown = sv.candleGapBridgeUnknownSV?.value ?? false;
|
|
362
381
|
tickLiveChartEngineFrame(state, input);
|
|
363
382
|
sv.displayValue.value = state.displayValue;
|
|
364
383
|
sv.displayMin.value = state.displayMin;
|
|
@@ -452,6 +471,16 @@ export function useLiveChartEngine(
|
|
|
452
471
|
() => config.returnToLiveMs ?? RETURN_TO_LIVE_MS,
|
|
453
472
|
);
|
|
454
473
|
const modeSV = useDerivedValue(() => config.mode ?? "line");
|
|
474
|
+
const candleGapsSV = useDerivedValue(() => config.candleGaps);
|
|
475
|
+
const candleGapBridgeNoTradesSV = useDerivedValue(
|
|
476
|
+
() => config.candleGapBridgeNoTrades ?? false,
|
|
477
|
+
);
|
|
478
|
+
const candleGapBridgeUnavailableSV = useDerivedValue(
|
|
479
|
+
() => config.candleGapBridgeUnavailable ?? false,
|
|
480
|
+
);
|
|
481
|
+
const candleGapBridgeUnknownSV = useDerivedValue(
|
|
482
|
+
() => config.candleGapBridgeUnknown ?? false,
|
|
483
|
+
);
|
|
455
484
|
|
|
456
485
|
// Animation state (mutated on UI thread each frame)
|
|
457
486
|
const displayValue = useSharedValue(0);
|
|
@@ -548,6 +577,10 @@ export function useLiveChartEngine(
|
|
|
548
577
|
modeSV,
|
|
549
578
|
candles,
|
|
550
579
|
liveCandle,
|
|
580
|
+
candleGapsSV,
|
|
581
|
+
candleGapBridgeNoTradesSV,
|
|
582
|
+
candleGapBridgeUnavailableSV,
|
|
583
|
+
candleGapBridgeUnknownSV,
|
|
551
584
|
extremaMinValue,
|
|
552
585
|
extremaMaxValue,
|
|
553
586
|
extremaMinTime,
|
|
@@ -555,6 +588,8 @@ export function useLiveChartEngine(
|
|
|
555
588
|
};
|
|
556
589
|
const frameScratchRef = useRef<EngineFrameScratch | null>(null);
|
|
557
590
|
if (frameScratchRef.current === null) {
|
|
591
|
+
// React permits this predictable lazy-ref initialization: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
|
|
592
|
+
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render -- false positive for React's documented lazy-ref exception
|
|
558
593
|
frameScratchRef.current = makeEngineFrameScratch();
|
|
559
594
|
}
|
|
560
595
|
|
|
@@ -356,6 +356,8 @@ export function useLiveChartSeriesEngine(
|
|
|
356
356
|
// Reused per-frame output buffers (ping-ponged) — see applyLiveChartSeriesEngineFrame.
|
|
357
357
|
const scratchRef = useRef<MultiSeriesEngineScratch | null>(null);
|
|
358
358
|
if (scratchRef.current === null) {
|
|
359
|
+
// React permits this predictable lazy-ref initialization: https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
|
|
360
|
+
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render -- false positive for React's documented lazy-ref exception
|
|
359
361
|
scratchRef.current = makeMultiSeriesEngineScratch();
|
|
360
362
|
}
|
|
361
363
|
|
package/src/draw/candle.ts
CHANGED
|
@@ -80,7 +80,7 @@ function candleValueToY(
|
|
|
80
80
|
* repainting only the selected body and wick without building another SkPath.
|
|
81
81
|
*/
|
|
82
82
|
export function computeCandleFocusClip(
|
|
83
|
-
candle:
|
|
83
|
+
candle: { time: number } | null,
|
|
84
84
|
padding: ChartPadding,
|
|
85
85
|
canvasW: number,
|
|
86
86
|
canvasH: number,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { CANDLE_METRICS_DEFAULTS } from "../constants";
|
|
2
|
+
import {
|
|
3
|
+
candleAtBucketTime,
|
|
4
|
+
previousCandleAtTime,
|
|
5
|
+
previousCandleCloseAtTime,
|
|
6
|
+
} from "../math/candleGaps";
|
|
7
|
+
import type {
|
|
8
|
+
CandleGap,
|
|
9
|
+
CandleGapKind,
|
|
10
|
+
CandleMetrics,
|
|
11
|
+
CandlePoint,
|
|
12
|
+
} from "../types";
|
|
13
|
+
import type { ChartPadding } from "./line";
|
|
14
|
+
|
|
15
|
+
export interface CandleGapMark {
|
|
16
|
+
x1: number;
|
|
17
|
+
x2: number;
|
|
18
|
+
y: number;
|
|
19
|
+
kind: CandleGapKind;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CandleGapGeometry {
|
|
23
|
+
marks: CandleGapMark[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Pure worklet geometry for neutral previous-close marks in explicit gaps. */
|
|
27
|
+
export function buildCandleGapGeometry(
|
|
28
|
+
candles: CandlePoint[],
|
|
29
|
+
liveCandle: CandlePoint | null,
|
|
30
|
+
gaps: CandleGap[],
|
|
31
|
+
padding: ChartPadding,
|
|
32
|
+
canvasW: number,
|
|
33
|
+
canvasH: number,
|
|
34
|
+
winStart: number,
|
|
35
|
+
windowSecs: number,
|
|
36
|
+
displayMin: number,
|
|
37
|
+
displayMax: number,
|
|
38
|
+
candleWidthSecs: number,
|
|
39
|
+
bridgeNoTrades: boolean,
|
|
40
|
+
bridgeUnavailable: boolean,
|
|
41
|
+
bridgeUnknown: boolean,
|
|
42
|
+
metrics: CandleMetrics = CANDLE_METRICS_DEFAULTS,
|
|
43
|
+
): CandleGapGeometry {
|
|
44
|
+
"worklet";
|
|
45
|
+
const chartW = canvasW - padding.left - padding.right;
|
|
46
|
+
const chartH = canvasH - padding.top - padding.bottom;
|
|
47
|
+
const valueRange = displayMax - displayMin;
|
|
48
|
+
if (
|
|
49
|
+
chartW <= 0 ||
|
|
50
|
+
chartH <= 0 ||
|
|
51
|
+
valueRange <= 0 ||
|
|
52
|
+
windowSecs <= 0 ||
|
|
53
|
+
candleWidthSecs <= 0
|
|
54
|
+
) {
|
|
55
|
+
return { marks: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const slotPx = (candleWidthSecs / windowSecs) * chartW;
|
|
59
|
+
const bodyW = Math.max(
|
|
60
|
+
1,
|
|
61
|
+
Math.min(
|
|
62
|
+
slotPx * metrics.bodyWidthRatio,
|
|
63
|
+
slotPx - metrics.minGapPx,
|
|
64
|
+
metrics.maxBodyPx,
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
const winEnd = winStart + windowSecs;
|
|
68
|
+
const chartLeft = padding.left;
|
|
69
|
+
const chartRight = canvasW - padding.right;
|
|
70
|
+
const marks: CandleGapMark[] = [];
|
|
71
|
+
|
|
72
|
+
for (let gi = 0; gi < gaps.length; gi++) {
|
|
73
|
+
const gap = gaps[gi];
|
|
74
|
+
if (gap.from >= winEnd || gap.to <= winStart) continue;
|
|
75
|
+
const bridge =
|
|
76
|
+
gap.kind === "no-trades"
|
|
77
|
+
? bridgeNoTrades
|
|
78
|
+
: gap.kind === "unavailable"
|
|
79
|
+
? bridgeUnavailable
|
|
80
|
+
: bridgeUnknown;
|
|
81
|
+
if (!bridge) continue;
|
|
82
|
+
|
|
83
|
+
const seedTime = Math.max(gap.from, winStart - candleWidthSecs);
|
|
84
|
+
const previous = previousCandleAtTime(candles, liveCandle, seedTime);
|
|
85
|
+
const origin =
|
|
86
|
+
previous?.time ?? candles[0]?.time ?? liveCandle?.time ?? gap.from;
|
|
87
|
+
let bucket =
|
|
88
|
+
origin +
|
|
89
|
+
Math.ceil((gap.from - origin) / candleWidthSecs - 1e-9) *
|
|
90
|
+
candleWidthSecs;
|
|
91
|
+
if (bucket < gap.from) bucket += candleWidthSecs;
|
|
92
|
+
if (bucket + candleWidthSecs < winStart) {
|
|
93
|
+
bucket +=
|
|
94
|
+
Math.floor((winStart - bucket) / candleWidthSecs) * candleWidthSecs;
|
|
95
|
+
while (bucket + candleWidthSecs < winStart) bucket += candleWidthSecs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (
|
|
99
|
+
;
|
|
100
|
+
bucket + candleWidthSecs <= gap.to && bucket <= winEnd;
|
|
101
|
+
bucket += candleWidthSecs
|
|
102
|
+
) {
|
|
103
|
+
if (
|
|
104
|
+
candleAtBucketTime(candles, liveCandle, bucket, candleWidthSecs) !== null
|
|
105
|
+
) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const close = previousCandleCloseAtTime(candles, liveCandle, bucket);
|
|
109
|
+
if (close === null) continue;
|
|
110
|
+
|
|
111
|
+
const centerTime = bucket + candleWidthSecs / 2;
|
|
112
|
+
const centerX =
|
|
113
|
+
padding.left + ((centerTime - winStart) / windowSecs) * chartW;
|
|
114
|
+
if (
|
|
115
|
+
centerX < chartLeft - bodyW / 2 ||
|
|
116
|
+
centerX > chartRight + bodyW / 2
|
|
117
|
+
) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const y =
|
|
121
|
+
padding.top + ((displayMax - close) / valueRange) * chartH;
|
|
122
|
+
marks.push({
|
|
123
|
+
x1: Math.max(chartLeft, centerX - bodyW / 2),
|
|
124
|
+
x2: Math.min(chartRight, centerX + bodyW / 2),
|
|
125
|
+
y,
|
|
126
|
+
kind: gap.kind,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { marks };
|
|
132
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { CandleGap, CandleGapKind, LiveChartPoint } from "../types";
|
|
2
|
+
import type { ChartPadding } from "./line";
|
|
3
|
+
|
|
4
|
+
export interface LineGapBridge {
|
|
5
|
+
x1: number;
|
|
6
|
+
x2: number;
|
|
7
|
+
y: number;
|
|
8
|
+
kind: CandleGapKind;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LineGapGeometry {
|
|
12
|
+
bridges: LineGapBridge[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Whether an explicit gap conflicts with at least one real line sample. */
|
|
16
|
+
export function lineGapContainsData(
|
|
17
|
+
data: LiveChartPoint[],
|
|
18
|
+
gap: CandleGap,
|
|
19
|
+
): boolean {
|
|
20
|
+
"worklet";
|
|
21
|
+
let lo = 0;
|
|
22
|
+
let hi = data.length;
|
|
23
|
+
while (lo < hi) {
|
|
24
|
+
const mid = (lo + hi) >> 1;
|
|
25
|
+
if (data[mid].time < gap.from) lo = mid + 1;
|
|
26
|
+
else hi = mid;
|
|
27
|
+
}
|
|
28
|
+
return lo < data.length && data[lo].time < gap.to;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Latest observed line value strictly before `time`. */
|
|
32
|
+
export function previousLineValueAtTime(
|
|
33
|
+
data: LiveChartPoint[],
|
|
34
|
+
time: number,
|
|
35
|
+
): number | null {
|
|
36
|
+
"worklet";
|
|
37
|
+
let lo = 0;
|
|
38
|
+
let hi = data.length;
|
|
39
|
+
while (lo < hi) {
|
|
40
|
+
const mid = (lo + hi) >> 1;
|
|
41
|
+
if (data[mid].time < time) lo = mid + 1;
|
|
42
|
+
else hi = mid;
|
|
43
|
+
}
|
|
44
|
+
return lo > 0 ? data[lo - 1].value : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Contiguous point ranges that may be drawn without interpolating across an
|
|
49
|
+
* explicit empty interval. Each pair is `[startPoint, endPointExclusive]`.
|
|
50
|
+
* Real samples inside a declared gap make that gap inert, matching the candle
|
|
51
|
+
* contract that observed market data always wins over presentation metadata.
|
|
52
|
+
*/
|
|
53
|
+
export function buildLineGapSegmentRanges(
|
|
54
|
+
points: number[],
|
|
55
|
+
data: LiveChartPoint[],
|
|
56
|
+
gaps: CandleGap[],
|
|
57
|
+
winStart: number,
|
|
58
|
+
windowSecs: number,
|
|
59
|
+
plotLeft: number,
|
|
60
|
+
plotWidth: number,
|
|
61
|
+
out?: number[],
|
|
62
|
+
gapXsOut?: number[],
|
|
63
|
+
): number[] {
|
|
64
|
+
"worklet";
|
|
65
|
+
const ranges = out ?? [];
|
|
66
|
+
if (out) out.length = 0;
|
|
67
|
+
const count = points.length >> 1;
|
|
68
|
+
if (count < 2 || windowSecs <= 0 || plotWidth <= 0) return ranges;
|
|
69
|
+
if (gaps.length === 0) {
|
|
70
|
+
ranges.push(0, count);
|
|
71
|
+
return ranges;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const winEnd = winStart + windowSecs;
|
|
75
|
+
const xScale = plotWidth / windowSecs;
|
|
76
|
+
const gapXs = gapXsOut ?? [];
|
|
77
|
+
if (gapXsOut) gapXsOut.length = 0;
|
|
78
|
+
for (let gi = 0; gi < gaps.length; gi++) {
|
|
79
|
+
const gap = gaps[gi];
|
|
80
|
+
if (
|
|
81
|
+
gap.from >= gap.to ||
|
|
82
|
+
gap.from >= winEnd ||
|
|
83
|
+
gap.to <= winStart ||
|
|
84
|
+
lineGapContainsData(data, gap)
|
|
85
|
+
) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
gapXs.push(
|
|
89
|
+
plotLeft + (gap.from - winStart) * xScale,
|
|
90
|
+
plotLeft + (gap.to - winStart) * xScale,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (gapXs.length === 0) {
|
|
94
|
+
ranges.push(0, count);
|
|
95
|
+
return ranges;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let segmentStart = 0;
|
|
99
|
+
|
|
100
|
+
for (let i = 1; i < count; i++) {
|
|
101
|
+
const previousX = points[(i - 1) * 2];
|
|
102
|
+
const currentX = points[i * 2];
|
|
103
|
+
let breaksHere = false;
|
|
104
|
+
for (let gi = 0; gi < gapXs.length; gi += 2) {
|
|
105
|
+
const gapStartX = gapXs[gi];
|
|
106
|
+
const gapEndX = gapXs[gi + 1];
|
|
107
|
+
if (previousX <= gapStartX && currentX >= gapEndX) {
|
|
108
|
+
breaksHere = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!breaksHere) continue;
|
|
113
|
+
if (i - segmentStart >= 2) ranges.push(segmentStart, i);
|
|
114
|
+
segmentStart = i;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (count - segmentStart >= 2) ranges.push(segmentStart, count);
|
|
118
|
+
return ranges;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Pure worklet geometry for flat previous-value bridges in line gaps. */
|
|
122
|
+
export function buildLineGapGeometry(
|
|
123
|
+
data: LiveChartPoint[],
|
|
124
|
+
gaps: CandleGap[],
|
|
125
|
+
padding: ChartPadding,
|
|
126
|
+
canvasW: number,
|
|
127
|
+
canvasH: number,
|
|
128
|
+
winStart: number,
|
|
129
|
+
windowSecs: number,
|
|
130
|
+
displayMin: number,
|
|
131
|
+
displayMax: number,
|
|
132
|
+
bridgeNoTrades: boolean,
|
|
133
|
+
bridgeUnavailable: boolean,
|
|
134
|
+
bridgeUnknown: boolean,
|
|
135
|
+
): LineGapGeometry {
|
|
136
|
+
"worklet";
|
|
137
|
+
const plotWidth = canvasW - padding.left - padding.right;
|
|
138
|
+
const plotHeight = canvasH - padding.top - padding.bottom;
|
|
139
|
+
const valueRange = displayMax - displayMin;
|
|
140
|
+
if (plotWidth <= 0 || plotHeight <= 0 || valueRange <= 0 || windowSecs <= 0) {
|
|
141
|
+
return { bridges: [] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const winEnd = winStart + windowSecs;
|
|
145
|
+
const plotRight = canvasW - padding.right;
|
|
146
|
+
const bridges: LineGapBridge[] = [];
|
|
147
|
+
for (let i = 0; i < gaps.length; i++) {
|
|
148
|
+
const gap = gaps[i];
|
|
149
|
+
const bridge =
|
|
150
|
+
gap.kind === "no-trades"
|
|
151
|
+
? bridgeNoTrades
|
|
152
|
+
: gap.kind === "unavailable"
|
|
153
|
+
? bridgeUnavailable
|
|
154
|
+
: bridgeUnknown;
|
|
155
|
+
if (
|
|
156
|
+
!bridge ||
|
|
157
|
+
gap.from >= gap.to ||
|
|
158
|
+
gap.from >= winEnd ||
|
|
159
|
+
gap.to <= winStart ||
|
|
160
|
+
lineGapContainsData(data, gap)
|
|
161
|
+
) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const previousValue = previousLineValueAtTime(data, gap.from);
|
|
165
|
+
if (previousValue === null) continue;
|
|
166
|
+
|
|
167
|
+
const from = Math.max(gap.from, winStart);
|
|
168
|
+
const to = Math.min(gap.to, winEnd);
|
|
169
|
+
const x1 = padding.left + ((from - winStart) / windowSecs) * plotWidth;
|
|
170
|
+
const x2 = padding.left + ((to - winStart) / windowSecs) * plotWidth;
|
|
171
|
+
if (x2 <= padding.left || x1 >= plotRight || x2 <= x1) continue;
|
|
172
|
+
bridges.push({
|
|
173
|
+
x1: Math.max(padding.left, x1),
|
|
174
|
+
x2: Math.min(plotRight, x2),
|
|
175
|
+
y: padding.top + ((displayMax - previousValue) / valueRange) * plotHeight,
|
|
176
|
+
kind: gap.kind,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { bridges };
|
|
180
|
+
}
|
package/src/draw/markerAtlas.ts
CHANGED
|
@@ -17,6 +17,11 @@ import type {
|
|
|
17
17
|
MarkerKind,
|
|
18
18
|
} from "../types";
|
|
19
19
|
|
|
20
|
+
export type MarkerAtlasPalette = Pick<
|
|
21
|
+
LiveChartPalette,
|
|
22
|
+
"bgRgb" | "line" | "refLine" | "dotUp" | "refLabel"
|
|
23
|
+
>;
|
|
24
|
+
|
|
20
25
|
/** Default icon box (px) when `marker.size` is unset. */
|
|
21
26
|
export const DEFAULT_ICON_SIZE = 16;
|
|
22
27
|
/** Circular badge padding around the icon glyph + background ring width. */
|
|
@@ -78,7 +83,7 @@ export function groupCountTextWidth(
|
|
|
78
83
|
* per-frame overlay worklet can resolve a collapsed cluster's badge color. */
|
|
79
84
|
export function defaultMarkerColor(
|
|
80
85
|
kind: MarkerKind,
|
|
81
|
-
palette:
|
|
86
|
+
palette: MarkerAtlasPalette,
|
|
82
87
|
): string {
|
|
83
88
|
"worklet";
|
|
84
89
|
switch (kind) {
|
|
@@ -188,7 +193,7 @@ interface CellSpec {
|
|
|
188
193
|
* Geometry + draw routine for one marker appearance. Mirrors the per-glyph
|
|
189
194
|
* rendering the old `MarkerGlyph` did inline, so the atlas is pixel-equivalent.
|
|
190
195
|
*/
|
|
191
|
-
function cellSpec(m: Marker, palette:
|
|
196
|
+
function cellSpec(m: Marker, palette: MarkerAtlasPalette, font: SkFont): CellSpec {
|
|
192
197
|
const color = m.color ?? defaultMarkerColor(m.kind, palette);
|
|
193
198
|
const m2 = CELL_MARGIN * 2;
|
|
194
199
|
|
|
@@ -382,7 +387,7 @@ function groupBgCellSpec(
|
|
|
382
387
|
*/
|
|
383
388
|
export function buildMarkerAtlas(
|
|
384
389
|
markers: Marker[],
|
|
385
|
-
palette:
|
|
390
|
+
palette: MarkerAtlasPalette,
|
|
386
391
|
font: SkFont,
|
|
387
392
|
scale = 1,
|
|
388
393
|
/** Also bake count-badge cells (digits + per-color backgrounds) for
|
|
@@ -4,8 +4,13 @@ import type { DerivedValue, SharedValue } from "react-native-reanimated";
|
|
|
4
4
|
import { measureFontTextWidth } from "../lib/measureFontTextWidth";
|
|
5
5
|
import { type ChartPadding } from "../draw/line";
|
|
6
6
|
import { interpolateAtTime } from "../math/interpolate";
|
|
7
|
+
import {
|
|
8
|
+
candleGapBucketStartAtTime,
|
|
9
|
+
candleGapDefaultLabel,
|
|
10
|
+
pickCandleGapAtTime,
|
|
11
|
+
} from "../math/candleGaps";
|
|
7
12
|
import { pickCandleAtTime } from "../math/pickCandle";
|
|
8
|
-
import type { CandlePoint } from "../types";
|
|
13
|
+
import type { CandleGap, CandlePoint } from "../types";
|
|
9
14
|
|
|
10
15
|
const TOOLTIP_PAD_X = 8;
|
|
11
16
|
const TOOLTIP_PAD_Y = 6;
|
|
@@ -187,6 +192,8 @@ export interface CrosshairState {
|
|
|
187
192
|
* inactive) — single-series `useCrosshair` only; undefined on the multi-series
|
|
188
193
|
* crosshair. Surfaced for a custom candle `renderTooltip`. */
|
|
189
194
|
scrubCandle?: SharedValue<CandlePoint | null>;
|
|
195
|
+
/** Explicit line or candle gap under the crosshair; single-series only. */
|
|
196
|
+
scrubGap?: SharedValue<CandleGap | null>;
|
|
190
197
|
/** Canvas-Y where the crosshair line should start so it stops at a top-pinned
|
|
191
198
|
* custom tooltip's bottom edge instead of running up through it. Written by
|
|
192
199
|
* {@link CustomTooltipOverlay} (the label's measured bottom) when
|
|
@@ -307,6 +314,7 @@ export function snapScrubXToCandleCenter(
|
|
|
307
314
|
canvasWidth: number,
|
|
308
315
|
timestamp: number,
|
|
309
316
|
windowSecs: number,
|
|
317
|
+
gaps: CandleGap[] = [],
|
|
310
318
|
): number {
|
|
311
319
|
"worklet";
|
|
312
320
|
const chartW = canvasWidth - padding.left - padding.right;
|
|
@@ -314,7 +322,22 @@ export function snapScrubXToCandleCenter(
|
|
|
314
322
|
const winStart = timestamp - windowSecs;
|
|
315
323
|
const t = winStart + ((x - padding.left) / chartW) * windowSecs;
|
|
316
324
|
const candle = pickCandleAtTime(candles, liveCandle, t, candleWidthSecs);
|
|
317
|
-
if (!candle)
|
|
325
|
+
if (!candle) {
|
|
326
|
+
const gap = pickCandleGapAtTime(gaps, t);
|
|
327
|
+
if (!gap) return x;
|
|
328
|
+
const bucket = candleGapBucketStartAtTime(
|
|
329
|
+
gap,
|
|
330
|
+
t,
|
|
331
|
+
candles,
|
|
332
|
+
liveCandle,
|
|
333
|
+
candleWidthSecs,
|
|
334
|
+
);
|
|
335
|
+
if (bucket === null) return x;
|
|
336
|
+
return (
|
|
337
|
+
padding.left +
|
|
338
|
+
((bucket + candleWidthSecs / 2 - winStart) / windowSecs) * chartW
|
|
339
|
+
);
|
|
340
|
+
}
|
|
318
341
|
return (
|
|
319
342
|
padding.left +
|
|
320
343
|
((candle.time + candleWidthSecs / 2 - winStart) / windowSecs) * chartW
|
|
@@ -559,16 +582,29 @@ export function computeCandleTooltipLayout(
|
|
|
559
582
|
formatTime: (t: number) => string,
|
|
560
583
|
font: SkFont,
|
|
561
584
|
monoCharWidth = 0,
|
|
585
|
+
gap: CandleGap | null = null,
|
|
586
|
+
gapValue: number | null = null,
|
|
562
587
|
): TooltipLayout {
|
|
563
588
|
"worklet";
|
|
564
|
-
if (!scrubActive || !candle) return HIDDEN_TOOLTIP;
|
|
565
|
-
const lines: { text: string; dim: boolean }[] =
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
589
|
+
if (!scrubActive || (!candle && !gap)) return HIDDEN_TOOLTIP;
|
|
590
|
+
const lines: { text: string; dim: boolean }[] = candle
|
|
591
|
+
? [
|
|
592
|
+
{ text: `O ${formatValue(candle.open)}`, dim: false },
|
|
593
|
+
{ text: `H ${formatValue(candle.high)}`, dim: false },
|
|
594
|
+
{ text: `L ${formatValue(candle.low)}`, dim: false },
|
|
595
|
+
{ text: `C ${formatValue(candle.close)}`, dim: false },
|
|
596
|
+
{ text: formatTime(scrubTime), dim: true },
|
|
597
|
+
]
|
|
598
|
+
: [
|
|
599
|
+
{
|
|
600
|
+
text: gap?.label ?? candleGapDefaultLabel(gap!.kind),
|
|
601
|
+
dim: false,
|
|
602
|
+
},
|
|
603
|
+
...(gapValue === null
|
|
604
|
+
? []
|
|
605
|
+
: [{ text: `Last ${formatValue(gapValue)}`, dim: false }]),
|
|
606
|
+
{ text: formatTime(scrubTime), dim: true },
|
|
607
|
+
];
|
|
572
608
|
const layout = computeTooltipLayoutMulti(
|
|
573
609
|
scrubActive,
|
|
574
610
|
scrubX,
|
|
@@ -582,11 +618,56 @@ export function computeCandleTooltipLayout(
|
|
|
582
618
|
// OHLC stack renders `stackedLines`, but a custom `renderTooltip` reads
|
|
583
619
|
// `valueStr` / `timeStr` off the layout (same as line mode) — so a custom
|
|
584
620
|
// candle tooltip gets a ready-made close + time without its own formatter.
|
|
585
|
-
layout.valueStr =
|
|
621
|
+
layout.valueStr = candle
|
|
622
|
+
? formatValue(candle.close)
|
|
623
|
+
: gapValue === null
|
|
624
|
+
? ""
|
|
625
|
+
: formatValue(gapValue);
|
|
586
626
|
layout.timeStr = formatTime(scrubTime);
|
|
587
627
|
return layout;
|
|
588
628
|
}
|
|
589
629
|
|
|
630
|
+
/** Two-row line-mode tooltip for an explicit semantic gap. */
|
|
631
|
+
export function computeLineGapTooltipLayout(
|
|
632
|
+
scrubActive: boolean,
|
|
633
|
+
scrubX: number,
|
|
634
|
+
gap: CandleGap | null,
|
|
635
|
+
scrubTime: number,
|
|
636
|
+
padding: ChartPadding,
|
|
637
|
+
canvasWidth: number,
|
|
638
|
+
formatTime: (t: number) => string,
|
|
639
|
+
font: SkFont,
|
|
640
|
+
monoCharWidth = 0,
|
|
641
|
+
placement: "side" | "top" | "bottom" | "point" = "side",
|
|
642
|
+
showLabel = true,
|
|
643
|
+
showTime = true,
|
|
644
|
+
canvasHeight = 0,
|
|
645
|
+
margin = TOOLTIP_TOP_MARGIN,
|
|
646
|
+
scrubDotY = -1,
|
|
647
|
+
): TooltipLayout {
|
|
648
|
+
"worklet";
|
|
649
|
+
if (!gap) return HIDDEN_TOOLTIP;
|
|
650
|
+
const label = gap.label ?? candleGapDefaultLabel(gap.kind);
|
|
651
|
+
return computeTooltipLayout(
|
|
652
|
+
scrubActive,
|
|
653
|
+
scrubX,
|
|
654
|
+
0,
|
|
655
|
+
scrubTime,
|
|
656
|
+
padding,
|
|
657
|
+
canvasWidth,
|
|
658
|
+
() => label,
|
|
659
|
+
formatTime,
|
|
660
|
+
font,
|
|
661
|
+
monoCharWidth,
|
|
662
|
+
placement,
|
|
663
|
+
showLabel,
|
|
664
|
+
showTime,
|
|
665
|
+
canvasHeight,
|
|
666
|
+
margin,
|
|
667
|
+
scrubDotY,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
590
671
|
/** Single-series scrub value at window time — extracted for tests. */
|
|
591
672
|
export function deriveScrubValueSingle(
|
|
592
673
|
scrubActive: boolean,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
import type { ResolvedCandleGapsConfig } from "../core/resolveConfig";
|
|
3
|
+
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
4
|
+
import { buildCandleGapGeometry } from "../draw/candleGap";
|
|
5
|
+
import type { ChartPadding } from "../draw/line";
|
|
6
|
+
import type { CandleMetrics, CandlePoint } from "../types";
|
|
7
|
+
import { usePathBuilder } from "./usePathBuilder";
|
|
8
|
+
|
|
9
|
+
/** Batched neutral previous-close paths for each semantic candle-gap kind. */
|
|
10
|
+
export function useCandleGapPaths(
|
|
11
|
+
engine: SingleEngineState,
|
|
12
|
+
padding: ChartPadding,
|
|
13
|
+
candles: SharedValue<CandlePoint[]>,
|
|
14
|
+
liveCandle: SharedValue<CandlePoint | null>,
|
|
15
|
+
displayCandleWidth: SharedValue<number>,
|
|
16
|
+
config: ResolvedCandleGapsConfig,
|
|
17
|
+
metrics: CandleMetrics,
|
|
18
|
+
) {
|
|
19
|
+
const noTradesBuilder = usePathBuilder();
|
|
20
|
+
const unavailableBuilder = usePathBuilder();
|
|
21
|
+
const unknownBuilder = usePathBuilder();
|
|
22
|
+
|
|
23
|
+
/* istanbul ignore next -- worklet */
|
|
24
|
+
const geometry = useDerivedValue(() =>
|
|
25
|
+
buildCandleGapGeometry(
|
|
26
|
+
candles.get(),
|
|
27
|
+
liveCandle.get(),
|
|
28
|
+
config.gaps,
|
|
29
|
+
padding,
|
|
30
|
+
engine.canvasWidth.get(),
|
|
31
|
+
engine.canvasHeight.get(),
|
|
32
|
+
engine.timestamp.get() - engine.displayWindow.get(),
|
|
33
|
+
engine.displayWindow.get(),
|
|
34
|
+
engine.displayMin.get(),
|
|
35
|
+
engine.displayMax.get(),
|
|
36
|
+
displayCandleWidth.get(),
|
|
37
|
+
config.styles["no-trades"].bridge !== null,
|
|
38
|
+
config.styles.unavailable.bridge !== null,
|
|
39
|
+
config.styles.unknown.bridge !== null,
|
|
40
|
+
metrics,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/* istanbul ignore next -- worklet */
|
|
45
|
+
const noTradesPath = useDerivedValue(() => {
|
|
46
|
+
const b = noTradesBuilder.get();
|
|
47
|
+
const marks = geometry.get().marks;
|
|
48
|
+
for (let i = 0; i < marks.length; i++) {
|
|
49
|
+
const mark = marks[i];
|
|
50
|
+
if (mark.kind !== "no-trades") continue;
|
|
51
|
+
b.moveTo(mark.x1, mark.y);
|
|
52
|
+
b.lineTo(mark.x2, mark.y);
|
|
53
|
+
}
|
|
54
|
+
return b.detach();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/* istanbul ignore next -- worklet */
|
|
58
|
+
const unavailablePath = useDerivedValue(() => {
|
|
59
|
+
const b = unavailableBuilder.get();
|
|
60
|
+
const marks = geometry.get().marks;
|
|
61
|
+
for (let i = 0; i < marks.length; i++) {
|
|
62
|
+
const mark = marks[i];
|
|
63
|
+
if (mark.kind !== "unavailable") continue;
|
|
64
|
+
b.moveTo(mark.x1, mark.y);
|
|
65
|
+
b.lineTo(mark.x2, mark.y);
|
|
66
|
+
}
|
|
67
|
+
return b.detach();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/* istanbul ignore next -- worklet */
|
|
71
|
+
const unknownPath = useDerivedValue(() => {
|
|
72
|
+
const b = unknownBuilder.get();
|
|
73
|
+
const marks = geometry.get().marks;
|
|
74
|
+
for (let i = 0; i < marks.length; i++) {
|
|
75
|
+
const mark = marks[i];
|
|
76
|
+
if (mark.kind !== "unknown") continue;
|
|
77
|
+
b.moveTo(mark.x1, mark.y);
|
|
78
|
+
b.lineTo(mark.x2, mark.y);
|
|
79
|
+
}
|
|
80
|
+
return b.detach();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
noTradesPath,
|
|
85
|
+
unavailablePath,
|
|
86
|
+
unknownPath,
|
|
87
|
+
} as const;
|
|
88
|
+
}
|