react-native-livechart 4.19.0 → 4.21.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/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/LiveChart.d.ts +2 -2
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts +2 -2
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +9 -4
- 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/liveChartEngineTick.d.ts +6 -1
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +34 -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/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/grid.d.ts +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/lineGap.d.ts +25 -0
- package/dist/draw/lineGap.d.ts.map +1 -0
- 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/useCrosshair.d.ts +9 -5
- package/dist/hooks/useCrosshair.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/usePinchZoom.d.ts +7 -0
- package/dist/hooks/usePinchZoom.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/useThreshold.d.ts +13 -13
- package/dist/hooks/useThreshold.d.ts.map +1 -1
- package/dist/hooks/useYAxis.d.ts +1 -1
- package/dist/hooks/useYAxis.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 +212 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
- package/src/components/CustomTooltipOverlay.tsx +7 -3
- package/src/components/LiveChart.tsx +353 -56
- package/src/components/LiveChartSeries.tsx +28 -6
- package/src/components/ReferenceLineOverlay.tsx +38 -7
- package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
- package/src/core/liveChartEngineTick.ts +34 -1
- package/src/core/resolveConfig.ts +197 -1
- package/src/core/useLiveChartEngine.ts +37 -5
- package/src/draw/candle.ts +1 -1
- package/src/draw/candleGap.ts +132 -0
- package/src/draw/grid.ts +34 -4
- package/src/draw/lineGap.ts +180 -0
- package/src/hooks/crosshairShared.ts +92 -11
- package/src/hooks/useCandleGapPaths.ts +88 -0
- package/src/hooks/useChartPaths.ts +81 -22
- package/src/hooks/useCrosshair.ts +126 -40
- package/src/hooks/useLineGapPaths.ts +76 -0
- package/src/hooks/usePinchZoom.ts +21 -0
- package/src/hooks/useReferenceLine.ts +6 -5
- package/src/hooks/useReferenceLineSeries.ts +101 -0
- package/src/hooks/useThreshold.ts +31 -22
- package/src/hooks/useYAxis.ts +2 -0
- package/src/index.ts +16 -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 +224 -14
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
+
|
|
4
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
5
|
+
import type { ChartPadding } from "../draw/line";
|
|
6
|
+
import { interpolateAtTime } from "../math/interpolate";
|
|
7
|
+
import { buildReferenceLineSeriesPoints } from "../math/referenceLineSeries";
|
|
8
|
+
import {
|
|
9
|
+
thresholdLineY,
|
|
10
|
+
thresholdSeriesVisible,
|
|
11
|
+
thresholdVisible,
|
|
12
|
+
} from "../math/threshold";
|
|
13
|
+
import type { LiveChartPoint } from "../types";
|
|
14
|
+
|
|
15
|
+
export interface ReferenceLineSeriesGeometry {
|
|
16
|
+
/** Visible series polyline as `[x, y, …]` screen coordinates. */
|
|
17
|
+
screenPts: SharedValue<number[]>;
|
|
18
|
+
/** Whether any series segment crosses the visible plot. */
|
|
19
|
+
visible: SharedValue<boolean>;
|
|
20
|
+
/** Series value at the chart's current right edge. */
|
|
21
|
+
currentValue: SharedValue<number>;
|
|
22
|
+
/** Canvas Y coordinate of {@link currentValue}. */
|
|
23
|
+
currentY: SharedValue<number>;
|
|
24
|
+
/** Whether the current-value label belongs inside the visible plot. */
|
|
25
|
+
currentVisible: SharedValue<boolean>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* UI-thread geometry for a static historical reference-line series. Point
|
|
30
|
+
* buffers ping-pong so Reanimated observes a changed array reference each frame
|
|
31
|
+
* without allocating new intermediate arrays.
|
|
32
|
+
*/
|
|
33
|
+
export function useReferenceLineSeries(
|
|
34
|
+
engine: ChartEngineLayout,
|
|
35
|
+
padding: ChartPadding,
|
|
36
|
+
points: LiveChartPoint[],
|
|
37
|
+
extendToNow: boolean,
|
|
38
|
+
): ReferenceLineSeriesGeometry {
|
|
39
|
+
const cacheRef = useRef<{
|
|
40
|
+
a: number[];
|
|
41
|
+
b: number[];
|
|
42
|
+
tick: boolean;
|
|
43
|
+
} | null>(null);
|
|
44
|
+
if (cacheRef.current === null) {
|
|
45
|
+
cacheRef.current = { a: [], b: [], tick: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const screenPts = useDerivedValue(() => {
|
|
49
|
+
const cache = cacheRef.current!;
|
|
50
|
+
cache.tick = !cache.tick;
|
|
51
|
+
return buildReferenceLineSeriesPoints(
|
|
52
|
+
points,
|
|
53
|
+
engine.timestamp.get(),
|
|
54
|
+
engine.displayWindow.get(),
|
|
55
|
+
engine.displayMin.get(),
|
|
56
|
+
engine.displayMax.get(),
|
|
57
|
+
engine.canvasWidth.get(),
|
|
58
|
+
engine.canvasHeight.get(),
|
|
59
|
+
padding,
|
|
60
|
+
extendToNow,
|
|
61
|
+
cache.tick ? cache.a : cache.b,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const visible = useDerivedValue(() =>
|
|
66
|
+
thresholdSeriesVisible(
|
|
67
|
+
screenPts.get(),
|
|
68
|
+
engine.canvasHeight.get(),
|
|
69
|
+
padding.top,
|
|
70
|
+
padding.bottom,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const currentValue = useDerivedValue(
|
|
75
|
+
() => interpolateAtTime(points, engine.timestamp.get()) ?? NaN,
|
|
76
|
+
);
|
|
77
|
+
const currentY = useDerivedValue(() =>
|
|
78
|
+
thresholdLineY(
|
|
79
|
+
currentValue.get(),
|
|
80
|
+
engine.displayMin.get(),
|
|
81
|
+
engine.displayMax.get(),
|
|
82
|
+
engine.canvasHeight.get(),
|
|
83
|
+
padding.top,
|
|
84
|
+
padding.bottom,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
const currentVisible = useDerivedValue(() => {
|
|
88
|
+
if (points.length === 0) return false;
|
|
89
|
+
if (!extendToNow && points[points.length - 1].time < engine.timestamp.get()) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return thresholdVisible(
|
|
93
|
+
currentY.get(),
|
|
94
|
+
engine.canvasHeight.get(),
|
|
95
|
+
padding.top,
|
|
96
|
+
padding.bottom,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return { screenPts, visible, currentValue, currentY, currentVisible };
|
|
101
|
+
}
|
|
@@ -95,15 +95,15 @@ export interface ThresholdSeriesGeometry {
|
|
|
95
95
|
samples: SharedValue<number[]>;
|
|
96
96
|
/** Whether any of the polyline is on-screen (drives marker-line opacity). */
|
|
97
97
|
visible: SharedValue<boolean>;
|
|
98
|
-
/** Threshold value at
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
98
|
+
/** Threshold value at the configured badge anchor: the visible window's
|
|
99
|
+
* left edge (`"first"`) or its live/right edge (`"last"`). */
|
|
100
|
+
badgeValue: SharedValue<number>;
|
|
101
|
+
/** Pixel-Y of `badgeValue` — anchors the badge. */
|
|
102
|
+
badgeLineY: SharedValue<number>;
|
|
103
|
+
/** Whether the configured badge anchor is on-plot and belongs to a visible
|
|
104
|
+
* part of the threshold. (`visible` can still be true when that one endpoint
|
|
105
|
+
* is off-plot.) */
|
|
106
|
+
badgeVisible: SharedValue<boolean>;
|
|
107
107
|
/** Pixel-X where the threshold ends: the last point's X with `extendToNow`
|
|
108
108
|
* off, else {@link THRESHOLD_NO_CLIP}. The shader paints its plain
|
|
109
109
|
* `restColor` right of it; the marker polyline stops there. */
|
|
@@ -118,9 +118,9 @@ const EMPTY_SAMPLES: number[] = new Array(THRESHOLD_SAMPLE_COUNT).fill(0);
|
|
|
118
118
|
* Per-frame screen geometry for a **time-varying** threshold — a plain
|
|
119
119
|
* `LiveChartPoint[]` `value` or a live `SharedValue<LiveChartPoint[]>` `series`
|
|
120
120
|
* (which wins when both are given): the screen polyline (marker line +
|
|
121
|
-
* fill-band bottom), the shader's pixel-Y `samples[]`, the
|
|
122
|
-
* for the badge, and the `extendToNow` cutoff X. The array buffers
|
|
123
|
-
* (Reanimated only re-notifies subscribers when the returned reference
|
|
121
|
+
* fill-band bottom), the shader's pixel-Y `samples[]`, the configured endpoint
|
|
122
|
+
* value/anchor for the badge, and the `extendToNow` cutoff X. The array buffers
|
|
123
|
+
* ping-pong (Reanimated only re-notifies subscribers when the returned reference
|
|
124
124
|
* changes). When the threshold is a constant `SharedValue<number>` every
|
|
125
125
|
* worklet short-circuits cheaply and {@link useThreshold} drives the render
|
|
126
126
|
* instead.
|
|
@@ -131,6 +131,7 @@ export function useThresholdSeries(
|
|
|
131
131
|
value: ThresholdValue,
|
|
132
132
|
series: SharedValue<LiveChartPoint[]> | null = null,
|
|
133
133
|
extendToNow = true,
|
|
134
|
+
labelAnchor: "first" | "last" = "last",
|
|
134
135
|
): ThresholdSeriesGeometry {
|
|
135
136
|
const cacheRef = useRef<{
|
|
136
137
|
ptsA: number[];
|
|
@@ -238,15 +239,19 @@ export function useThresholdSeries(
|
|
|
238
239
|
);
|
|
239
240
|
});
|
|
240
241
|
|
|
241
|
-
const
|
|
242
|
+
const badgeValue = useDerivedValue(() => {
|
|
242
243
|
const pts = series ? series.get() : Array.isArray(value) ? value : null;
|
|
243
244
|
if (pts === null) return NaN;
|
|
244
|
-
|
|
245
|
+
const time =
|
|
246
|
+
labelAnchor === "first"
|
|
247
|
+
? engine.timestamp.get() - engine.displayWindow.get()
|
|
248
|
+
: engine.timestamp.get();
|
|
249
|
+
return interpolateAtTime(pts, time) ?? NaN;
|
|
245
250
|
});
|
|
246
251
|
|
|
247
|
-
const
|
|
252
|
+
const badgeLineY = useDerivedValue(() =>
|
|
248
253
|
thresholdLineY(
|
|
249
|
-
|
|
254
|
+
badgeValue.get(),
|
|
250
255
|
engine.displayMin.get(),
|
|
251
256
|
engine.displayMax.get(),
|
|
252
257
|
engine.canvasHeight.get(),
|
|
@@ -255,15 +260,19 @@ export function useThresholdSeries(
|
|
|
255
260
|
),
|
|
256
261
|
);
|
|
257
262
|
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
263
|
+
const badgeVisible = useDerivedValue(() => {
|
|
264
|
+
if (labelAnchor === "first") {
|
|
265
|
+
// If the non-extended series ended before this window, there is no left
|
|
266
|
+
// endpoint to label even though interpolation can still clamp a value.
|
|
267
|
+
if (screenPts.get().length < 4) return false;
|
|
268
|
+
} else if (!extendToNow) {
|
|
260
269
|
// The threshold ends at its last point — no badge past it.
|
|
261
270
|
const pts = series ? series.get() : Array.isArray(value) ? value : null;
|
|
262
271
|
if (pts === null || pts.length === 0) return false;
|
|
263
272
|
if (pts[pts.length - 1].time < engine.timestamp.get()) return false;
|
|
264
273
|
}
|
|
265
274
|
return thresholdVisible(
|
|
266
|
-
|
|
275
|
+
badgeLineY.get(),
|
|
267
276
|
engine.canvasHeight.get(),
|
|
268
277
|
padding.top,
|
|
269
278
|
padding.bottom,
|
|
@@ -274,9 +283,9 @@ export function useThresholdSeries(
|
|
|
274
283
|
screenPts,
|
|
275
284
|
samples,
|
|
276
285
|
visible,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
badgeValue,
|
|
287
|
+
badgeLineY,
|
|
288
|
+
badgeVisible,
|
|
280
289
|
clipRightX,
|
|
281
290
|
};
|
|
282
291
|
}
|
package/src/hooks/useYAxis.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function useYAxis(
|
|
|
20
20
|
minGap = 36,
|
|
21
21
|
gridMetrics: GridMetrics = GRID_METRICS_DEFAULTS,
|
|
22
22
|
count = 0,
|
|
23
|
+
intervalScale = 1,
|
|
23
24
|
) {
|
|
24
25
|
const prevInterval = useSharedValue(0);
|
|
25
26
|
const labelAlphas = useSharedValue<Record<number, number>>({});
|
|
@@ -41,6 +42,7 @@ export function useYAxis(
|
|
|
41
42
|
minGap,
|
|
42
43
|
gridMetrics,
|
|
43
44
|
count,
|
|
45
|
+
intervalScale,
|
|
44
46
|
);
|
|
45
47
|
|
|
46
48
|
prevInterval.set(result.interval);
|
package/src/index.ts
CHANGED
|
@@ -39,9 +39,23 @@ export type {
|
|
|
39
39
|
BadgeMetrics,
|
|
40
40
|
BadgeStyleConfig,
|
|
41
41
|
BadgeVariant,
|
|
42
|
+
CandleGap,
|
|
43
|
+
CandleGapBandStyle,
|
|
44
|
+
CandleGapBridgeStyle,
|
|
45
|
+
CandleGapKind,
|
|
46
|
+
CandleGapLabelStyle,
|
|
47
|
+
CandleGapStyle,
|
|
48
|
+
CandleGapsConfig,
|
|
42
49
|
CandleMetrics,
|
|
43
50
|
CandlePoint,
|
|
44
51
|
CanvasMode,
|
|
52
|
+
ChartGap,
|
|
53
|
+
ChartGapBandStyle,
|
|
54
|
+
ChartGapBridgeStyle,
|
|
55
|
+
ChartGapKind,
|
|
56
|
+
ChartGapLabelStyle,
|
|
57
|
+
ChartGapsConfig,
|
|
58
|
+
ChartGapStyle,
|
|
45
59
|
ChartInsets,
|
|
46
60
|
ChartOverlayContext,
|
|
47
61
|
ChartPlotRect,
|
|
@@ -61,6 +75,7 @@ export type {
|
|
|
61
75
|
LegendStyle,
|
|
62
76
|
LineConfig,
|
|
63
77
|
LiveChartCoreProps,
|
|
78
|
+
LiveChartHandle,
|
|
64
79
|
LiveChartMetrics,
|
|
65
80
|
LiveChartMetricsOverride,
|
|
66
81
|
LiveChartPalette,
|
|
@@ -99,6 +114,7 @@ export type {
|
|
|
99
114
|
SeriesConfig,
|
|
100
115
|
ThemeMode,
|
|
101
116
|
ThresholdConfig,
|
|
117
|
+
ThresholdBadgeRenderProps,
|
|
102
118
|
ThresholdLineConfig,
|
|
103
119
|
TooltipRenderProps,
|
|
104
120
|
TradeEvent,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CandleGap, CandleGapKind, CandlePoint } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Built-in, overridable copy for gap bands and scrub tooltips. */
|
|
4
|
+
export function candleGapDefaultLabel(kind: CandleGapKind): string {
|
|
5
|
+
"worklet";
|
|
6
|
+
if (kind === "no-trades") return "No trades";
|
|
7
|
+
if (kind === "unavailable") return "Trading unavailable";
|
|
8
|
+
return "Data unavailable";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Find the explicit half-open gap containing `time`. Input must be start-sorted. */
|
|
12
|
+
export function pickCandleGapAtTime(
|
|
13
|
+
gaps: CandleGap[],
|
|
14
|
+
time: number,
|
|
15
|
+
): CandleGap | null {
|
|
16
|
+
"worklet";
|
|
17
|
+
let lo = 0;
|
|
18
|
+
let hi = gaps.length - 1;
|
|
19
|
+
while (lo <= hi) {
|
|
20
|
+
const mid = (lo + hi) >> 1;
|
|
21
|
+
if (gaps[mid].from <= time) lo = mid + 1;
|
|
22
|
+
else hi = mid - 1;
|
|
23
|
+
}
|
|
24
|
+
if (hi < 0) return null;
|
|
25
|
+
const gap = gaps[hi];
|
|
26
|
+
return time >= gap.from && time < gap.to ? gap : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Latest real candle strictly before `time`, preferring a newer live candle. */
|
|
30
|
+
export function previousCandleAtTime(
|
|
31
|
+
candles: CandlePoint[],
|
|
32
|
+
liveCandle: CandlePoint | null,
|
|
33
|
+
time: number,
|
|
34
|
+
): CandlePoint | null {
|
|
35
|
+
"worklet";
|
|
36
|
+
let lo = 0;
|
|
37
|
+
let hi = candles.length;
|
|
38
|
+
while (lo < hi) {
|
|
39
|
+
const mid = (lo + hi) >> 1;
|
|
40
|
+
if (candles[mid].time < time) lo = mid + 1;
|
|
41
|
+
else hi = mid;
|
|
42
|
+
}
|
|
43
|
+
let result = lo > 0 ? candles[lo - 1] : null;
|
|
44
|
+
if (
|
|
45
|
+
liveCandle &&
|
|
46
|
+
liveCandle.time < time &&
|
|
47
|
+
(!result || liveCandle.time > result.time)
|
|
48
|
+
) {
|
|
49
|
+
result = liveCandle;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Previous observed close for a bridged gap; `null` when no anchor exists. */
|
|
55
|
+
export function previousCandleCloseAtTime(
|
|
56
|
+
candles: CandlePoint[],
|
|
57
|
+
liveCandle: CandlePoint | null,
|
|
58
|
+
time: number,
|
|
59
|
+
): number | null {
|
|
60
|
+
"worklet";
|
|
61
|
+
return previousCandleAtTime(candles, liveCandle, time)?.close ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether a real candle starts in the bucket beginning at `bucketTime`. */
|
|
65
|
+
export function candleAtBucketTime(
|
|
66
|
+
candles: CandlePoint[],
|
|
67
|
+
liveCandle: CandlePoint | null,
|
|
68
|
+
bucketTime: number,
|
|
69
|
+
candleWidthSecs: number,
|
|
70
|
+
): CandlePoint | null {
|
|
71
|
+
"worklet";
|
|
72
|
+
let lo = 0;
|
|
73
|
+
let hi = candles.length - 1;
|
|
74
|
+
while (lo <= hi) {
|
|
75
|
+
const mid = (lo + hi) >> 1;
|
|
76
|
+
const t = candles[mid].time;
|
|
77
|
+
if (t < bucketTime) lo = mid + 1;
|
|
78
|
+
else if (t > bucketTime) hi = mid - 1;
|
|
79
|
+
else return candles[mid];
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
liveCandle &&
|
|
83
|
+
Math.abs(liveCandle.time - bucketTime) < candleWidthSecs * 1e-6
|
|
84
|
+
) {
|
|
85
|
+
return liveCandle;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Bucket start under `time`, aligned to the nearest preceding real candle.
|
|
92
|
+
* Returns `null` for a partial edge bucket, a bucket with real OHLC, or a gap
|
|
93
|
+
* without a previous-close anchor.
|
|
94
|
+
*/
|
|
95
|
+
export function candleGapBucketStartAtTime(
|
|
96
|
+
gap: CandleGap,
|
|
97
|
+
time: number,
|
|
98
|
+
candles: CandlePoint[],
|
|
99
|
+
liveCandle: CandlePoint | null,
|
|
100
|
+
candleWidthSecs: number,
|
|
101
|
+
): number | null {
|
|
102
|
+
"worklet";
|
|
103
|
+
if (
|
|
104
|
+
candleWidthSecs <= 0 ||
|
|
105
|
+
time < gap.from ||
|
|
106
|
+
time >= gap.to ||
|
|
107
|
+
gap.kind !== "no-trades"
|
|
108
|
+
) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const previous = previousCandleAtTime(candles, liveCandle, time);
|
|
112
|
+
if (!previous) return null;
|
|
113
|
+
const steps = Math.floor((time - previous.time) / candleWidthSecs);
|
|
114
|
+
const bucket = previous.time + steps * candleWidthSecs;
|
|
115
|
+
if (bucket < gap.from || bucket + candleWidthSecs > gap.to) return null;
|
|
116
|
+
if (candleAtBucketTime(candles, liveCandle, bucket, candleWidthSecs)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return bucket;
|
|
120
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ChartPadding } from "../draw/line";
|
|
2
|
+
import type { LiveChartPoint } from "../types";
|
|
3
|
+
import { interpolateAtTime } from "./interpolate";
|
|
4
|
+
import { thresholdLineY } from "./threshold";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Project a historical reference-line series into screen-space `[x, y, …]`
|
|
8
|
+
* points for the visible time window. Window-edge values are interpolated so
|
|
9
|
+
* the path enters and leaves the plot cleanly. The series clamps to its first
|
|
10
|
+
* value on the left and, by default, to its last value at the live edge.
|
|
11
|
+
*
|
|
12
|
+
* Worklet-safe. When `out` is supplied it is cleared and reused.
|
|
13
|
+
*/
|
|
14
|
+
export function buildReferenceLineSeriesPoints(
|
|
15
|
+
points: LiveChartPoint[],
|
|
16
|
+
now: number,
|
|
17
|
+
windowSecs: number,
|
|
18
|
+
displayMin: number,
|
|
19
|
+
displayMax: number,
|
|
20
|
+
canvasWidth: number,
|
|
21
|
+
canvasHeight: number,
|
|
22
|
+
padding: ChartPadding,
|
|
23
|
+
extendToNow: boolean,
|
|
24
|
+
out?: number[],
|
|
25
|
+
): number[] {
|
|
26
|
+
"worklet";
|
|
27
|
+
const result = out ?? [];
|
|
28
|
+
result.length = 0;
|
|
29
|
+
|
|
30
|
+
const plotLeft = padding.left;
|
|
31
|
+
const plotRight = canvasWidth - padding.right;
|
|
32
|
+
const plotWidth = plotRight - plotLeft;
|
|
33
|
+
const plotHeight = canvasHeight - padding.top - padding.bottom;
|
|
34
|
+
const valueRange = displayMax - displayMin;
|
|
35
|
+
if (
|
|
36
|
+
points.length === 0 ||
|
|
37
|
+
!(windowSecs > 0) ||
|
|
38
|
+
plotWidth <= 0 ||
|
|
39
|
+
plotHeight <= 0 ||
|
|
40
|
+
!(valueRange > 0)
|
|
41
|
+
) {
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const windowStart = now - windowSecs;
|
|
46
|
+
const lastTime = points[points.length - 1].time;
|
|
47
|
+
const endTime = extendToNow ? now : Math.min(now, lastTime);
|
|
48
|
+
if (endTime < windowStart) return result;
|
|
49
|
+
|
|
50
|
+
const startValue = interpolateAtTime(points, windowStart);
|
|
51
|
+
const endValue = interpolateAtTime(points, endTime);
|
|
52
|
+
if (
|
|
53
|
+
startValue === null ||
|
|
54
|
+
endValue === null ||
|
|
55
|
+
!Number.isFinite(startValue) ||
|
|
56
|
+
!Number.isFinite(endValue)
|
|
57
|
+
) {
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const toX = (time: number) =>
|
|
62
|
+
plotLeft + ((time - windowStart) / windowSecs) * plotWidth;
|
|
63
|
+
const toY = (value: number) =>
|
|
64
|
+
thresholdLineY(
|
|
65
|
+
value,
|
|
66
|
+
displayMin,
|
|
67
|
+
displayMax,
|
|
68
|
+
canvasHeight,
|
|
69
|
+
padding.top,
|
|
70
|
+
padding.bottom,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
result.push(plotLeft, toY(startValue));
|
|
74
|
+
for (let i = 0; i < points.length; i++) {
|
|
75
|
+
const point = points[i];
|
|
76
|
+
if (point.time <= windowStart) continue;
|
|
77
|
+
if (point.time >= endTime) break;
|
|
78
|
+
if (!Number.isFinite(point.time) || !Number.isFinite(point.value)) continue;
|
|
79
|
+
result.push(toX(point.time), toY(point.value));
|
|
80
|
+
}
|
|
81
|
+
result.push(toX(endTime), toY(endValue));
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
@@ -4,16 +4,22 @@ import type {
|
|
|
4
4
|
ReferenceLineBadgeConfig,
|
|
5
5
|
} from "../types";
|
|
6
6
|
|
|
7
|
-
/** Which of the
|
|
8
|
-
export type ReferenceLineForm =
|
|
7
|
+
/** Which of the four reference-line forms a `ReferenceLine` resolves to. */
|
|
8
|
+
export type ReferenceLineForm =
|
|
9
|
+
| "line"
|
|
10
|
+
| "series"
|
|
11
|
+
| "value-band"
|
|
12
|
+
| "time-band"
|
|
13
|
+
| "none";
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
|
-
* Classify a `ReferenceLine` into one of its
|
|
12
|
-
* documented precedence A (line) > B (value band) >
|
|
16
|
+
* Classify a `ReferenceLine` into one of its four forms, applying the
|
|
17
|
+
* documented precedence A (line) > B (series) > C (value band) > D (time band).
|
|
13
18
|
*/
|
|
14
19
|
export function referenceLineForm(rl: ReferenceLine): ReferenceLineForm {
|
|
15
20
|
"worklet";
|
|
16
21
|
if (rl.value !== undefined) return "line";
|
|
22
|
+
if (rl.series !== undefined) return "series";
|
|
17
23
|
if (rl.valueFrom !== undefined && rl.valueTo !== undefined) {
|
|
18
24
|
return "value-band";
|
|
19
25
|
}
|
|
@@ -24,7 +30,7 @@ export function referenceLineForm(rl: ReferenceLine): ReferenceLineForm {
|
|
|
24
30
|
/**
|
|
25
31
|
* Gather every Y value a set of reference lines should contribute to the
|
|
26
32
|
* axis-range computation. Lines flagged `excludeFromRange` are skipped, as are
|
|
27
|
-
* time bands (Form
|
|
33
|
+
* time bands (Form D) which constrain time, not value.
|
|
28
34
|
*/
|
|
29
35
|
export function collectReferenceValues(lines: ReferenceLine[]): number[] {
|
|
30
36
|
const out: number[] = [];
|
|
@@ -35,6 +41,22 @@ export function collectReferenceValues(lines: ReferenceLine[]): number[] {
|
|
|
35
41
|
case "line":
|
|
36
42
|
out.push(rl.value as number);
|
|
37
43
|
break;
|
|
44
|
+
case "series": {
|
|
45
|
+
const points = rl.series;
|
|
46
|
+
if (!points || points.length === 0) break;
|
|
47
|
+
let min = Infinity;
|
|
48
|
+
let max = -Infinity;
|
|
49
|
+
for (let j = 0; j < points.length; j++) {
|
|
50
|
+
const value = points[j].value;
|
|
51
|
+
if (!Number.isFinite(value)) continue;
|
|
52
|
+
if (value < min) min = value;
|
|
53
|
+
if (value > max) max = value;
|
|
54
|
+
}
|
|
55
|
+
// Only the extrema are needed by the engine, avoiding a per-frame scan
|
|
56
|
+
// through the full historical annotation series.
|
|
57
|
+
if (min !== Infinity) out.push(min, max);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
38
60
|
case "value-band":
|
|
39
61
|
out.push(rl.valueFrom as number, rl.valueTo as number);
|
|
40
62
|
break;
|
|
@@ -56,7 +78,13 @@ export function referenceLineReactKeys(
|
|
|
56
78
|
const occurrences = new Map<string, number>();
|
|
57
79
|
const keys: string[] = [];
|
|
58
80
|
for (const line of lines) {
|
|
59
|
-
|
|
81
|
+
// A series' samples are render data, not identity. Omitting their contents
|
|
82
|
+
// keeps a growing historical series from remounting its overlay every render.
|
|
83
|
+
const base =
|
|
84
|
+
line.id ??
|
|
85
|
+
stableReferenceLineSignature(
|
|
86
|
+
line.series === undefined ? line : { ...line, series: "series" },
|
|
87
|
+
);
|
|
60
88
|
const occurrence = occurrences.get(base) ?? 0;
|
|
61
89
|
occurrences.set(base, occurrence + 1);
|
|
62
90
|
keys.push(`${base}:${occurrence}`);
|
package/src/math/spline.ts
CHANGED
|
@@ -56,16 +56,26 @@ export function drawSpline(
|
|
|
56
56
|
/** Straight polyline (`lineTo` per point) instead of the monotone cubic — an
|
|
57
57
|
* angular, hard-edged line. The caller has already `moveTo`'d point 0. */
|
|
58
58
|
linear = false,
|
|
59
|
+
/** First point index to draw (inclusive). The caller must moveTo this point. */
|
|
60
|
+
startPoint = 0,
|
|
61
|
+
/** Last point index to draw (exclusive). Defaults to the full array. */
|
|
62
|
+
endPoint = pts.length >> 1,
|
|
59
63
|
) {
|
|
60
64
|
"worklet";
|
|
61
|
-
const
|
|
65
|
+
const start = Math.max(0, startPoint);
|
|
66
|
+
const end = Math.min(pts.length >> 1, endPoint);
|
|
67
|
+
const n = end - start;
|
|
62
68
|
if (n < 2) return;
|
|
63
69
|
if (linear) {
|
|
64
|
-
for (let i = 1; i < n; i++)
|
|
70
|
+
for (let i = 1; i < n; i++) {
|
|
71
|
+
const point = (start + i) * 2;
|
|
72
|
+
path.lineTo(pts[point], pts[point + 1]);
|
|
73
|
+
}
|
|
65
74
|
return;
|
|
66
75
|
}
|
|
67
76
|
if (n === 2) {
|
|
68
|
-
|
|
77
|
+
const point = (start + 1) * 2;
|
|
78
|
+
path.lineTo(pts[point], pts[point + 1]);
|
|
69
79
|
return;
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -73,7 +83,7 @@ export function drawSpline(
|
|
|
73
83
|
const delta: number[] = scratch ? scratch.delta : new Array(n - 1);
|
|
74
84
|
const h: number[] = scratch ? scratch.h : new Array(n - 1);
|
|
75
85
|
for (let i = 0; i < n - 1; i++) {
|
|
76
|
-
const i2 = i * 2;
|
|
86
|
+
const i2 = (start + i) * 2;
|
|
77
87
|
const j2 = i2 + 2;
|
|
78
88
|
h[i] = pts[j2] - pts[i2];
|
|
79
89
|
delta[i] = h[i] === 0 ? 0 : (pts[j2 + 1] - pts[i2 + 1]) / h[i];
|
|
@@ -110,7 +120,7 @@ export function drawSpline(
|
|
|
110
120
|
|
|
111
121
|
// 4. Draw bezier curves
|
|
112
122
|
for (let i = 0; i < n - 1; i++) {
|
|
113
|
-
const i2 = i * 2;
|
|
123
|
+
const i2 = (start + i) * 2;
|
|
114
124
|
const j2 = i2 + 2;
|
|
115
125
|
const hi = h[i];
|
|
116
126
|
path.cubicTo(
|