react-native-livechart 3.11.0 → 4.0.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/BadgeOverlay.d.ts +9 -1
- package/dist/components/BadgeOverlay.d.ts.map +1 -1
- package/dist/components/ChartOverlayLayer.d.ts +19 -0
- package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
- package/dist/components/CustomMarkerOverlay.d.ts +18 -6
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +7 -1
- package/dist/components/DotOverlay.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 +8 -3
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +4 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
- package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +25 -2
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts +7 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +12 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +29 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +56 -2
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +58 -7
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +20 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/volume.d.ts +25 -0
- package/dist/draw/volume.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/overlayScale.d.ts +22 -0
- package/dist/hooks/overlayScale.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +9 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +22 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +6 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartOverlayContext.d.ts +45 -0
- package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +8 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLiveDot.d.ts +6 -1
- package/dist/hooks/useLiveDot.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +12 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +82 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +74 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/useReferenceDrag.d.ts +24 -0
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +23 -4
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +5 -1
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
- package/dist/hooks/useVisibleRange.d.ts +39 -0
- package/dist/hooks/useVisibleRange.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +47 -0
- package/dist/math/markerCluster.d.ts.map +1 -0
- package/dist/math/markers.d.ts +15 -0
- package/dist/math/markers.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +24 -0
- package/dist/math/referenceDrag.d.ts.map +1 -0
- package/dist/math/referenceGroup.d.ts +31 -0
- package/dist/math/referenceGroup.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +33 -7
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +512 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BadgeOverlay.tsx +26 -1
- package/src/components/ChartOverlayLayer.tsx +32 -0
- package/src/components/CustomMarkerOverlay.tsx +153 -53
- package/src/components/CustomReferenceLineOverlay.tsx +250 -0
- package/src/components/CustomTooltipOverlay.tsx +27 -7
- package/src/components/DotOverlay.tsx +9 -0
- package/src/components/LiveChart.tsx +561 -34
- package/src/components/LiveChartSeries.tsx +110 -5
- package/src/components/MarkerOverlay.tsx +100 -6
- package/src/components/MultiSeriesDots.tsx +12 -3
- package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
- package/src/components/ReferenceLineOverlay.tsx +105 -32
- package/src/components/XAxisOverlay.tsx +9 -2
- package/src/components/YAxisOverlay.tsx +48 -26
- package/src/constants.ts +10 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +122 -1
- package/src/core/useLiveChartEngine.ts +103 -10
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +13 -1
- package/src/draw/markerAtlas.ts +117 -3
- package/src/draw/volume.ts +190 -0
- package/src/hooks/crosshairShared.ts +37 -9
- package/src/hooks/overlayScale.ts +59 -0
- package/src/hooks/resolveChartLayout.ts +33 -0
- package/src/hooks/useBadge.ts +89 -21
- package/src/hooks/useCandlePaths.ts +104 -7
- package/src/hooks/useChartOverlayContext.ts +93 -0
- package/src/hooks/useChartPaths.ts +14 -1
- package/src/hooks/useCrosshair.ts +29 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +43 -11
- package/src/hooks/useMultiSeriesLinePaths.ts +2 -1
- package/src/hooks/usePanScroll.ts +253 -0
- package/src/hooks/usePinchZoom.ts +189 -0
- package/src/hooks/useReferenceDrag.ts +254 -0
- package/src/hooks/useReferenceLine.ts +56 -9
- package/src/hooks/useReferenceLinePress.ts +16 -2
- package/src/hooks/useVisibleRange.ts +119 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +54 -9
- package/src/math/referenceDrag.ts +66 -0
- package/src/math/referenceGroup.ts +65 -0
- package/src/math/referenceLines.ts +76 -19
- package/src/types.ts +532 -21
|
@@ -9,6 +9,20 @@ export interface EngineTickMutable {
|
|
|
9
9
|
displayMax: number;
|
|
10
10
|
displayWindow: number;
|
|
11
11
|
timestamp: number;
|
|
12
|
+
/**
|
|
13
|
+
* The right-edge time the engine would use if it were following live —
|
|
14
|
+
* `now (+ windowBuffer)`. Equals {@link timestamp} while following; when
|
|
15
|
+
* scrolled back in time (see {@link EngineTickInput.viewEnd}) it keeps
|
|
16
|
+
* advancing while `timestamp` stays frozen. Exposed so the pan-scroll gesture
|
|
17
|
+
* can clamp against the live edge and detect catch-up.
|
|
18
|
+
*/
|
|
19
|
+
liveEdge: number;
|
|
20
|
+
/**
|
|
21
|
+
* Smoothed value at the visible window's right edge: the live value while
|
|
22
|
+
* following, or the price at `viewEnd` while scrolled back. Lets a badge track
|
|
23
|
+
* the last visible price as you pan (see `badge.followViewEdge`).
|
|
24
|
+
*/
|
|
25
|
+
edgeValue: number;
|
|
12
26
|
/**
|
|
13
27
|
* Value + time of the lowest / highest data point in the visible window —
|
|
14
28
|
* the actual extrema, NOT the smoothed display bounds (which carry margin and
|
|
@@ -48,6 +62,21 @@ export interface EngineTickInput {
|
|
|
48
62
|
windowBuffer?: number;
|
|
49
63
|
/** When true, freeze the viewport timestamp and skip displayWindow lerp */
|
|
50
64
|
paused?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Absolute right-edge time (unix seconds) to freeze the window at, or
|
|
67
|
+
* `null`/`undefined` to follow the live edge. Set by the pan-scroll gesture
|
|
68
|
+
* to scroll back in time; once it reaches (or passes) the live edge the engine
|
|
69
|
+
* resumes following. Takes precedence over {@link paused}.
|
|
70
|
+
*/
|
|
71
|
+
viewEnd?: number | null;
|
|
72
|
+
/**
|
|
73
|
+
* Absolute visible-window width (seconds) to freeze at, or `null`/`undefined`
|
|
74
|
+
* to follow the configured {@link timeWindow}. Set by the pinch-zoom gesture
|
|
75
|
+
* (see `usePinchZoom` / the `zoom` prop). The symmetric counterpart of
|
|
76
|
+
* {@link viewEnd}: `viewEnd` overrides the window's right edge, `viewWindow`
|
|
77
|
+
* overrides its width. `displayWindow` eases toward this when set.
|
|
78
|
+
*/
|
|
79
|
+
viewWindow?: number | null;
|
|
51
80
|
/** Chart mode — `"candle"` uses OHLC bars for Y range instead of line points. */
|
|
52
81
|
mode?: "line" | "candle";
|
|
53
82
|
/** Committed OHLC bars (sorted by time). Used when mode is `"candle"`. */
|
|
@@ -66,9 +95,18 @@ export function tickLiveChartEngineFrame(
|
|
|
66
95
|
): void {
|
|
67
96
|
"worklet";
|
|
68
97
|
const baseNow = input.nowOverride ?? input.nowSeconds ?? Date.now() / 1000;
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
const liveEdge = baseNow + (input.windowBuffer ?? 0) * input.timeWindow;
|
|
99
|
+
state.liveEdge = liveEdge;
|
|
100
|
+
const viewEnd = input.viewEnd;
|
|
101
|
+
if (viewEnd != null && viewEnd < liveEdge) {
|
|
102
|
+
// Scrolled back in time: freeze the right edge at an absolute timestamp so
|
|
103
|
+
// the window stops tracking "now" (see usePanScroll / the `timeScroll` prop).
|
|
104
|
+
state.timestamp = viewEnd;
|
|
105
|
+
} else if (!input.paused) {
|
|
106
|
+
// Following the live edge — viewEnd is null/undefined or has caught back up.
|
|
107
|
+
state.timestamp = liveEdge;
|
|
71
108
|
}
|
|
109
|
+
// else: paused with no active pan → leave the frozen timestamp untouched.
|
|
72
110
|
|
|
73
111
|
if (input.canvasWidth === 0 || input.canvasHeight === 0) return;
|
|
74
112
|
|
|
@@ -93,12 +131,10 @@ export function tickLiveChartEngineFrame(
|
|
|
93
131
|
input.dt,
|
|
94
132
|
);
|
|
95
133
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
input.dt,
|
|
101
|
-
);
|
|
134
|
+
// Pinch-zoom: ease toward the zoom override when set, else the configured
|
|
135
|
+
// window. Mirrors the viewEnd freeze above (width vs. right edge).
|
|
136
|
+
const targetWindow = input.viewWindow ?? input.timeWindow;
|
|
137
|
+
state.displayWindow = lerp(state.displayWindow, targetWindow, speed, input.dt);
|
|
102
138
|
|
|
103
139
|
const winStart = state.timestamp - state.displayWindow;
|
|
104
140
|
|
|
@@ -232,4 +268,41 @@ export function tickLiveChartEngineFrame(
|
|
|
232
268
|
state.displayMax = lerp(state.displayMax, tMax, speed, input.dt);
|
|
233
269
|
}
|
|
234
270
|
}
|
|
271
|
+
|
|
272
|
+
// Edge value: the price at the visible window's right edge. Following live →
|
|
273
|
+
// track the live value (badge unchanged). Scrolled back → track the last
|
|
274
|
+
// visible point/candle close, lerped so a `followViewEdge` badge glides to the
|
|
275
|
+
// last price as you pan.
|
|
276
|
+
const scrolledBack = viewEnd != null && viewEnd < liveEdge;
|
|
277
|
+
if (!scrolledBack) {
|
|
278
|
+
state.edgeValue = state.displayValue;
|
|
279
|
+
} else {
|
|
280
|
+
let edgeTarget = state.displayValue;
|
|
281
|
+
if (input.mode === "candle") {
|
|
282
|
+
const cs = input.candles;
|
|
283
|
+
if (cs && cs.length > 0) {
|
|
284
|
+
let elo = 0;
|
|
285
|
+
let ehi = cs.length;
|
|
286
|
+
while (elo < ehi) {
|
|
287
|
+
const m = (elo + ehi) >> 1;
|
|
288
|
+
if (cs[m].time <= state.timestamp) elo = m + 1;
|
|
289
|
+
else ehi = m;
|
|
290
|
+
}
|
|
291
|
+
if (elo > 0) edgeTarget = cs[elo - 1].close;
|
|
292
|
+
}
|
|
293
|
+
const lc = input.liveCandle;
|
|
294
|
+
if (lc && lc.time <= state.timestamp) edgeTarget = lc.close;
|
|
295
|
+
} else {
|
|
296
|
+
const pts = input.points;
|
|
297
|
+
let elo = 0;
|
|
298
|
+
let ehi = pts.length;
|
|
299
|
+
while (elo < ehi) {
|
|
300
|
+
const m = (elo + ehi) >> 1;
|
|
301
|
+
if (pts[m].time <= state.timestamp) elo = m + 1;
|
|
302
|
+
else ehi = m;
|
|
303
|
+
}
|
|
304
|
+
if (elo > 0) edgeTarget = pts[elo - 1].value;
|
|
305
|
+
}
|
|
306
|
+
state.edgeValue = lerp(state.edgeValue, edgeTarget, speed, input.dt);
|
|
307
|
+
}
|
|
235
308
|
}
|
|
@@ -7,6 +7,12 @@ export interface MultiEngineTickMutable {
|
|
|
7
7
|
displayMax: number;
|
|
8
8
|
displayWindow: number;
|
|
9
9
|
timestamp: number;
|
|
10
|
+
/**
|
|
11
|
+
* The right-edge time the engine would use if following live (`now (+ buffer)`).
|
|
12
|
+
* Equals {@link timestamp} while following; keeps advancing while `timestamp`
|
|
13
|
+
* stays frozen when scrolled back (see {@link MultiEngineTickInput.viewEnd}).
|
|
14
|
+
*/
|
|
15
|
+
liveEdge: number;
|
|
10
16
|
displayValues: number[];
|
|
11
17
|
opacities: number[];
|
|
12
18
|
/**
|
|
@@ -44,6 +50,18 @@ export interface MultiEngineTickInput {
|
|
|
44
50
|
/** Right-edge buffer as a fraction of the time window. */
|
|
45
51
|
windowBuffer?: number;
|
|
46
52
|
paused?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Absolute right-edge time (unix seconds) to freeze the window at, or
|
|
55
|
+
* `null`/`undefined` to follow the live edge. Drives pan-scroll; takes
|
|
56
|
+
* precedence over {@link paused}.
|
|
57
|
+
*/
|
|
58
|
+
viewEnd?: number | null;
|
|
59
|
+
/**
|
|
60
|
+
* Absolute visible-window width (seconds) to freeze at, or `null`/`undefined`
|
|
61
|
+
* to follow {@link timeWindow}. Drives pinch-zoom (see `usePinchZoom`); the
|
|
62
|
+
* symmetric counterpart of {@link viewEnd}.
|
|
63
|
+
*/
|
|
64
|
+
viewWindow?: number | null;
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
/**
|
|
@@ -57,9 +75,17 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
57
75
|
): void {
|
|
58
76
|
"worklet";
|
|
59
77
|
const baseNow = input.nowOverride ?? input.nowSeconds ?? Date.now() / 1000;
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
const liveEdge = baseNow + (input.windowBuffer ?? 0) * input.timeWindow;
|
|
79
|
+
state.liveEdge = liveEdge;
|
|
80
|
+
const viewEnd = input.viewEnd;
|
|
81
|
+
if (viewEnd != null && viewEnd < liveEdge) {
|
|
82
|
+
// Scrolled back in time: freeze the right edge (see usePanScroll).
|
|
83
|
+
state.timestamp = viewEnd;
|
|
84
|
+
} else if (!input.paused) {
|
|
85
|
+
// Following the live edge — viewEnd is null/undefined or has caught back up.
|
|
86
|
+
state.timestamp = liveEdge;
|
|
62
87
|
}
|
|
88
|
+
// else: paused with no active pan → leave the frozen timestamp untouched.
|
|
63
89
|
|
|
64
90
|
if (input.canvasWidth === 0 || input.canvasHeight === 0) return;
|
|
65
91
|
|
|
@@ -77,18 +103,33 @@ export function tickLiveChartSeriesEngineFrame(
|
|
|
77
103
|
state.opacities.length = n;
|
|
78
104
|
}
|
|
79
105
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
input.dt,
|
|
85
|
-
);
|
|
106
|
+
// Pinch-zoom: ease toward the zoom override when set, else the configured
|
|
107
|
+
// window (mirrors the single-series tick).
|
|
108
|
+
const targetWindow = input.viewWindow ?? input.timeWindow;
|
|
109
|
+
state.displayWindow = lerp(state.displayWindow, targetWindow, speed, input.dt);
|
|
86
110
|
|
|
87
111
|
const winStart = state.timestamp - state.displayWindow;
|
|
88
112
|
const range = state.displayMax - state.displayMin;
|
|
89
113
|
|
|
114
|
+
// Scrolled back in time (pan/zoom): the per-series tips/dots sit at the
|
|
115
|
+
// window's right edge, so they must track each series' value AT that edge
|
|
116
|
+
// (`timestamp`), not the live value — otherwise the dot floats at the current
|
|
117
|
+
// price while the line ends in the past. Mirrors single-series `edgeValue`.
|
|
118
|
+
const scrolledBack = viewEnd != null && viewEnd < liveEdge;
|
|
119
|
+
|
|
90
120
|
for (let i = 0; i < n; i++) {
|
|
91
|
-
|
|
121
|
+
let target = series[i].value;
|
|
122
|
+
if (scrolledBack) {
|
|
123
|
+
const pts = series[i].data;
|
|
124
|
+
let elo = 0;
|
|
125
|
+
let ehi = pts.length;
|
|
126
|
+
while (elo < ehi) {
|
|
127
|
+
const m = (elo + ehi) >> 1;
|
|
128
|
+
if (pts[m].time <= state.timestamp) elo = m + 1;
|
|
129
|
+
else ehi = m;
|
|
130
|
+
}
|
|
131
|
+
if (elo > 0) target = pts[elo - 1].value;
|
|
132
|
+
}
|
|
92
133
|
const cur = state.displayValues[i];
|
|
93
134
|
const gapRatio =
|
|
94
135
|
range > 0 ? Math.min(Math.abs(target - cur) / range, 1) : 0;
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
LineStyleConfig,
|
|
15
15
|
LiveChartMetrics,
|
|
16
16
|
LiveChartMetricsOverride,
|
|
17
|
+
MarkerClusterConfig,
|
|
17
18
|
DotConfig,
|
|
18
19
|
DotRingConfig,
|
|
19
20
|
MultiSeriesDotConfig,
|
|
@@ -28,12 +29,15 @@ import type {
|
|
|
28
29
|
ThresholdLineConfig,
|
|
29
30
|
TradeEvent,
|
|
30
31
|
ValueLineConfig,
|
|
32
|
+
VolumeConfig,
|
|
31
33
|
XAxisConfig,
|
|
32
34
|
YAxisConfig,
|
|
35
|
+
ZoomConfig,
|
|
33
36
|
} from "../types";
|
|
34
37
|
|
|
35
38
|
import type { ComponentType, ReactElement } from "react";
|
|
36
39
|
import type { SharedValue } from "react-native-reanimated";
|
|
40
|
+
import type { ResolvedMarkerCluster } from "../math/markerCluster";
|
|
37
41
|
import {
|
|
38
42
|
BADGE_METRICS_DEFAULTS,
|
|
39
43
|
CANDLE_METRICS_DEFAULTS,
|
|
@@ -57,10 +61,39 @@ export interface ResolvedBadgeConfig {
|
|
|
57
61
|
tail: boolean;
|
|
58
62
|
position: "right" | "left";
|
|
59
63
|
background: string | undefined;
|
|
64
|
+
/** undefined → capsule (pillHeight / 2) at render time */
|
|
65
|
+
radius: number | undefined;
|
|
66
|
+
/** undefined → no border */
|
|
67
|
+
borderColor: string | undefined;
|
|
68
|
+
borderWidth: number;
|
|
69
|
+
/** undefined → variant/theme text color at render time */
|
|
70
|
+
textColor: string | undefined;
|
|
71
|
+
fontSize: number | undefined;
|
|
72
|
+
fontFamily: string | undefined;
|
|
73
|
+
fontWeight: FontWeight | undefined;
|
|
74
|
+
offsetX: number;
|
|
75
|
+
offsetY: number;
|
|
76
|
+
/** Track the visible window's right-edge price while scrolled back. */
|
|
77
|
+
followViewEdge: boolean;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
export interface ResolvedYAxisConfig {
|
|
63
81
|
minGap: number;
|
|
82
|
+
/** Float the axis over a full-width plot (no reserved right gutter). */
|
|
83
|
+
float: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ResolvedVolumeConfig {
|
|
87
|
+
/** undefined → use palette.candleUp at render time. */
|
|
88
|
+
upColor: string | undefined;
|
|
89
|
+
/** undefined → use palette.candleDown at render time. */
|
|
90
|
+
downColor: string | undefined;
|
|
91
|
+
/** Reserved band height (px) — the tallest a bar can be. */
|
|
92
|
+
maxHeight: number;
|
|
93
|
+
/** Corner radius (px) of bar tops. */
|
|
94
|
+
radius: number;
|
|
95
|
+
/** Opacity (0..1) applied to the whole band. */
|
|
96
|
+
opacity: number;
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
/** Resolved straight-line styling (connector, etc.). `color: undefined` → caller default. */
|
|
@@ -119,7 +152,7 @@ export interface ResolvedScrubConfig {
|
|
|
119
152
|
/** Tooltip pill corner radius in px. */
|
|
120
153
|
tooltipBorderRadius: number;
|
|
121
154
|
/** Where the tooltip pill sits relative to the scrub line. */
|
|
122
|
-
tooltipPlacement: "side" | "top" | "bottom";
|
|
155
|
+
tooltipPlacement: "side" | "top" | "bottom" | "point";
|
|
123
156
|
/** Gap (px) between the tooltip and the plot edge it's pinned to. */
|
|
124
157
|
tooltipMargin: number;
|
|
125
158
|
/** Show the value row in the default tooltip body. */
|
|
@@ -336,6 +369,16 @@ const BADGE_DEFAULTS: ResolvedBadgeConfig = {
|
|
|
336
369
|
tail: true,
|
|
337
370
|
position: "right",
|
|
338
371
|
background: undefined,
|
|
372
|
+
radius: undefined,
|
|
373
|
+
borderColor: undefined,
|
|
374
|
+
borderWidth: 1,
|
|
375
|
+
textColor: undefined,
|
|
376
|
+
fontSize: undefined,
|
|
377
|
+
fontFamily: undefined,
|
|
378
|
+
fontWeight: undefined,
|
|
379
|
+
offsetX: 0,
|
|
380
|
+
offsetY: 0,
|
|
381
|
+
followViewEdge: false,
|
|
339
382
|
};
|
|
340
383
|
|
|
341
384
|
/**
|
|
@@ -350,6 +393,7 @@ export function resolveBadge(
|
|
|
350
393
|
|
|
351
394
|
const Y_AXIS_DEFAULTS: ResolvedYAxisConfig = {
|
|
352
395
|
minGap: 36,
|
|
396
|
+
float: false,
|
|
353
397
|
};
|
|
354
398
|
|
|
355
399
|
/**
|
|
@@ -362,6 +406,47 @@ export function resolveYAxis(
|
|
|
362
406
|
return resolveToggle(prop, Y_AXIS_DEFAULTS, false);
|
|
363
407
|
}
|
|
364
408
|
|
|
409
|
+
const VOLUME_DEFAULTS: ResolvedVolumeConfig = {
|
|
410
|
+
upColor: undefined,
|
|
411
|
+
downColor: undefined,
|
|
412
|
+
maxHeight: 48,
|
|
413
|
+
radius: 2,
|
|
414
|
+
opacity: 0.6,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Resolves the `volume` prop to a fully-typed config or null (disabled).
|
|
419
|
+
* `true` → defaults, object → merged with defaults, falsy → null. Colors left
|
|
420
|
+
* `undefined` fall back to the candle palette at render time.
|
|
421
|
+
*/
|
|
422
|
+
export function resolveVolume(
|
|
423
|
+
prop: boolean | VolumeConfig | undefined,
|
|
424
|
+
): ResolvedVolumeConfig | null {
|
|
425
|
+
return resolveToggle(prop, VOLUME_DEFAULTS, false);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface ResolvedZoomConfig {
|
|
429
|
+
/** Tightest window (max zoom-in), seconds. `undefined` → `timeWindow / 8`. */
|
|
430
|
+
minTimeWindow: number | undefined;
|
|
431
|
+
/** Widest window (max zoom-out), seconds. `undefined` → full data span. */
|
|
432
|
+
maxTimeWindow: number | undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const ZOOM_DEFAULTS: ResolvedZoomConfig = {
|
|
436
|
+
minTimeWindow: undefined,
|
|
437
|
+
maxTimeWindow: undefined,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Resolves the `zoom` prop to a config or null (disabled). `true` → defaults
|
|
442
|
+
* (bounds derived at gesture time), object → merged, falsy → null.
|
|
443
|
+
*/
|
|
444
|
+
export function resolveZoom(
|
|
445
|
+
prop: boolean | ZoomConfig | undefined,
|
|
446
|
+
): ResolvedZoomConfig | null {
|
|
447
|
+
return resolveToggle(prop, ZOOM_DEFAULTS, false);
|
|
448
|
+
}
|
|
449
|
+
|
|
365
450
|
const AXIS_LABEL_DEFAULTS: ResolvedAxisLabelConfig = {
|
|
366
451
|
format: undefined,
|
|
367
452
|
color: undefined,
|
|
@@ -496,6 +581,42 @@ export function resolveScrubAction(
|
|
|
496
581
|
return resolveToggle(prop, SCRUB_ACTION_DEFAULTS, false);
|
|
497
582
|
}
|
|
498
583
|
|
|
584
|
+
/** Spacing + collapse-threshold defaults for `markerCluster: "stacked"`. */
|
|
585
|
+
const MARKER_CLUSTER_GAP = 2;
|
|
586
|
+
const MARKER_CLUSTER_MAX_BEFORE_GROUP = 5;
|
|
587
|
+
const MARKER_CLUSTER_OVERLAP = 0.75;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Resolves the `markerCluster` prop (a `"anchored"`/`"stacked"` shorthand or a
|
|
591
|
+
* {@link MarkerClusterConfig} object) to a full config. Always returns a config
|
|
592
|
+
* (never null) — `"anchored"` (the default) is a valid mode the cluster pass
|
|
593
|
+
* treats as "side offsets only, no bucketing". The object form implies
|
|
594
|
+
* `"stacked"` unless `mode` is set.
|
|
595
|
+
*/
|
|
596
|
+
export function resolveMarkerCluster(
|
|
597
|
+
prop: "anchored" | "stacked" | MarkerClusterConfig | undefined,
|
|
598
|
+
): ResolvedMarkerCluster {
|
|
599
|
+
if (typeof prop === "object") {
|
|
600
|
+
return {
|
|
601
|
+
mode: prop.mode ?? "stacked",
|
|
602
|
+
overlap: clamp01(prop.overlap ?? MARKER_CLUSTER_OVERLAP),
|
|
603
|
+
gap: MARKER_CLUSTER_GAP,
|
|
604
|
+
maxBeforeGroup: prop.maxBeforeGroup ?? MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
mode: prop === "stacked" ? "stacked" : "anchored",
|
|
609
|
+
overlap: MARKER_CLUSTER_OVERLAP,
|
|
610
|
+
gap: MARKER_CLUSTER_GAP,
|
|
611
|
+
maxBeforeGroup: MARKER_CLUSTER_MAX_BEFORE_GROUP,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Clamp to [0, 0.95] so a glyph never fully covers its neighbor (overlap 1). */
|
|
616
|
+
function clamp01(v: number): number {
|
|
617
|
+
return v < 0 ? 0 : v > 0.95 ? 0.95 : v;
|
|
618
|
+
}
|
|
619
|
+
|
|
499
620
|
const GRADIENT_DEFAULTS: ResolvedGradientConfig = {
|
|
500
621
|
topOpacity: undefined,
|
|
501
622
|
bottomOpacity: undefined,
|
|
@@ -27,6 +27,13 @@ export interface EngineConfig {
|
|
|
27
27
|
exaggerate?: boolean;
|
|
28
28
|
referenceValue?: number;
|
|
29
29
|
referenceValues?: number[];
|
|
30
|
+
/**
|
|
31
|
+
* Live, per-frame Y values to fold into the axis-range fit on top of the static
|
|
32
|
+
* {@link referenceValues} — used so a *dragging* reference line expands the range
|
|
33
|
+
* and the axis follows the finger smoothly (the committed values already sit in
|
|
34
|
+
* `referenceValues`). Read on the UI thread each frame; omit for non-draggable charts.
|
|
35
|
+
*/
|
|
36
|
+
liveReferenceValues?: SharedValue<number[]>;
|
|
30
37
|
nonNegative?: boolean;
|
|
31
38
|
maxValue?: number;
|
|
32
39
|
nowOverride?: number;
|
|
@@ -50,12 +57,14 @@ export interface ChartEngineLayout {
|
|
|
50
57
|
/** Animating window (lerps toward {@link timeWindow}); use for positioning. */
|
|
51
58
|
displayWindow: SharedValue<number>;
|
|
52
59
|
/**
|
|
53
|
-
* The target window
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
60
|
+
* The *effective* target window — the value `displayWindow` eases toward: the
|
|
61
|
+
* pinch-zoom override ({@link ChartEngineScroll.viewWindow}) when active, else
|
|
62
|
+
* the `timeWindow` prop. Tick *selection* (which X-axis labels exist) must read
|
|
63
|
+
* this, not `displayWindow`: the lerp is asymptotic and never reaches the
|
|
64
|
+
* target, so it settles just above or just below it depending on the prior
|
|
65
|
+
* window. Bucketing that off-by-epsilon value (e.g. via `niceTimeInterval`)
|
|
66
|
+
* would otherwise make the tick cadence depend on where you came from. See
|
|
67
|
+
* issue #126.
|
|
59
68
|
*/
|
|
60
69
|
timeWindow: SharedValue<number>;
|
|
61
70
|
canvasWidth: SharedValue<number>;
|
|
@@ -76,6 +85,42 @@ export interface ChartEngineExtrema {
|
|
|
76
85
|
extremaMaxTime: SharedValue<number>;
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Time-scroll state: lets a pan gesture freeze the window at an absolute time
|
|
90
|
+
* and resume following the live edge. Returned by both engines (intersected onto
|
|
91
|
+
* the state) but kept off {@link ChartEngineLayout} so overlay components — which
|
|
92
|
+
* only read the layout — don't have to carry it.
|
|
93
|
+
*/
|
|
94
|
+
export interface ChartEngineScroll {
|
|
95
|
+
/**
|
|
96
|
+
* Absolute right-edge time (unix seconds) to freeze at, or `null` to follow
|
|
97
|
+
* the live edge. Written by the pan-scroll gesture; read by the engine tick.
|
|
98
|
+
*/
|
|
99
|
+
viewEnd: SharedValue<number | null>;
|
|
100
|
+
/**
|
|
101
|
+
* The right-edge time the engine would use if following live — advances each
|
|
102
|
+
* frame even while {@link ChartEngineLayout.timestamp} is frozen by a pan.
|
|
103
|
+
*/
|
|
104
|
+
liveEdge: SharedValue<number>;
|
|
105
|
+
/**
|
|
106
|
+
* Absolute visible-window width (seconds) to freeze at, or `null` to follow
|
|
107
|
+
* the configured `timeWindow`. Written by the pinch-zoom gesture; the
|
|
108
|
+
* symmetric counterpart of {@link viewEnd} (width vs. right edge). Folded into
|
|
109
|
+
* {@link ChartEngineLayout.timeWindow}, so downstream selection/positioning
|
|
110
|
+
* picks up the zoom for free.
|
|
111
|
+
*/
|
|
112
|
+
viewWindow: SharedValue<number | null>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Single-series only: smoothed price at the visible window's right edge. */
|
|
116
|
+
export interface ChartEngineEdge {
|
|
117
|
+
/**
|
|
118
|
+
* Smoothed value at the window's right edge — the live value while following,
|
|
119
|
+
* the price at `viewEnd` while scrolled back. For a `followViewEdge` badge.
|
|
120
|
+
*/
|
|
121
|
+
edgeValue: SharedValue<number>;
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
export interface SingleEngineState extends ChartEngineLayout, ChartEngineExtrema {
|
|
80
125
|
data: SharedValue<LiveChartPoint[]>;
|
|
81
126
|
value: SharedValue<number>;
|
|
@@ -119,6 +164,14 @@ export interface EngineFrameRefs {
|
|
|
119
164
|
nowOverrideSV?: SharedValue<number | undefined>;
|
|
120
165
|
windowBufferSV?: SharedValue<number>;
|
|
121
166
|
pausedSV: SharedValue<boolean>;
|
|
167
|
+
/** Pan-scroll right-edge override (null = follow live). Optional for callers/tests. */
|
|
168
|
+
viewEndSV?: SharedValue<number | null>;
|
|
169
|
+
/** Pinch-zoom window-width override (null = follow timeWindow). Optional for callers/tests. */
|
|
170
|
+
viewWindowSV?: SharedValue<number | null>;
|
|
171
|
+
/** Receives the computed live edge each frame. Optional for callers/tests. */
|
|
172
|
+
liveEdgeSV?: SharedValue<number>;
|
|
173
|
+
/** Receives the smoothed right-edge value each frame. Optional for callers/tests. */
|
|
174
|
+
edgeValueSV?: SharedValue<number>;
|
|
122
175
|
modeSV: SharedValue<"line" | "candle">;
|
|
123
176
|
candles?: SharedValue<CandlePoint[]>;
|
|
124
177
|
liveCandle?: SharedValue<CandlePoint | null>;
|
|
@@ -144,6 +197,8 @@ export function applyLiveChartEngineFrame(
|
|
|
144
197
|
displayMax: sv.displayMax.value,
|
|
145
198
|
displayWindow: sv.displayWindow.value,
|
|
146
199
|
timestamp: sv.timestamp.value,
|
|
200
|
+
liveEdge: sv.liveEdgeSV?.value ?? 0,
|
|
201
|
+
edgeValue: sv.edgeValueSV?.value ?? 0,
|
|
147
202
|
extremaMinValue: sv.extremaMinValue.value,
|
|
148
203
|
extremaMaxValue: sv.extremaMaxValue.value,
|
|
149
204
|
extremaMinTime: sv.extremaMinTime.value,
|
|
@@ -167,6 +222,8 @@ export function applyLiveChartEngineFrame(
|
|
|
167
222
|
points: sv.data.value,
|
|
168
223
|
nowSeconds: Date.now() / 1000,
|
|
169
224
|
paused: sv.pausedSV.value,
|
|
225
|
+
viewEnd: sv.viewEndSV?.value,
|
|
226
|
+
viewWindow: sv.viewWindowSV?.value,
|
|
170
227
|
mode: sv.modeSV.value,
|
|
171
228
|
candles: sv.candles?.value,
|
|
172
229
|
liveCandle: sv.liveCandle?.value,
|
|
@@ -176,15 +233,27 @@ export function applyLiveChartEngineFrame(
|
|
|
176
233
|
sv.displayMax.value = state.displayMax;
|
|
177
234
|
sv.displayWindow.value = state.displayWindow;
|
|
178
235
|
sv.timestamp.value = state.timestamp;
|
|
236
|
+
if (sv.liveEdgeSV) sv.liveEdgeSV.value = state.liveEdge;
|
|
237
|
+
if (sv.edgeValueSV) sv.edgeValueSV.value = state.edgeValue;
|
|
179
238
|
sv.extremaMinValue.value = state.extremaMinValue;
|
|
180
239
|
sv.extremaMaxValue.value = state.extremaMaxValue;
|
|
181
240
|
sv.extremaMinTime.value = state.extremaMinTime;
|
|
182
241
|
sv.extremaMaxTime.value = state.extremaMaxTime;
|
|
183
242
|
}
|
|
184
243
|
|
|
185
|
-
export function useLiveChartEngine(
|
|
186
|
-
|
|
187
|
-
|
|
244
|
+
export function useLiveChartEngine(
|
|
245
|
+
config: EngineConfig,
|
|
246
|
+
): SingleEngineState & ChartEngineScroll & ChartEngineEdge {
|
|
247
|
+
// Pinch-zoom window-width override (null = follow the configured window).
|
|
248
|
+
// Declared first so `timeWindow` below can fold it in. Defaults to null so
|
|
249
|
+
// charts without `zoom` behave exactly as before.
|
|
250
|
+
const viewWindow = useSharedValue<number | null>(null);
|
|
251
|
+
|
|
252
|
+
// Low-frequency config → UI thread via useDerivedValue. `timeWindow` is the
|
|
253
|
+
// *effective* target window: the zoom override when set, else the prop. Both
|
|
254
|
+
// the tick's window lerp and the X-axis tick selection read it, so zoom flows
|
|
255
|
+
// downstream for free (mirrors how viewEnd drives `timestamp`).
|
|
256
|
+
const timeWindow = useDerivedValue(() => viewWindow.value ?? config.timeWindow);
|
|
188
257
|
// Static charts snap to their target in one tick (smoothing=1), so the single
|
|
189
258
|
// settle reaction below produces the final state with no per-frame easing.
|
|
190
259
|
const smoothing = useDerivedValue(() =>
|
|
@@ -193,7 +262,15 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
193
262
|
const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
|
|
194
263
|
const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
|
|
195
264
|
const referenceValue = useDerivedValue(() => config.referenceValue);
|
|
196
|
-
|
|
265
|
+
// Captured directly (not via `config.`) so Reanimated tracks the SharedValue and
|
|
266
|
+
// the merge re-runs each frame while a line is dragged; stable when idle.
|
|
267
|
+
const liveReferenceValuesSV = config.liveReferenceValues;
|
|
268
|
+
const referenceValues = useDerivedValue(() => {
|
|
269
|
+
const base = config.referenceValues;
|
|
270
|
+
const live = liveReferenceValuesSV?.value;
|
|
271
|
+
if (!live || live.length === 0) return base;
|
|
272
|
+
return base && base.length > 0 ? base.concat(live) : live;
|
|
273
|
+
});
|
|
197
274
|
const nonNegativeSV = useDerivedValue(() => config.nonNegative ?? false);
|
|
198
275
|
const maxValueSV = useDerivedValue(() => config.maxValue);
|
|
199
276
|
const nowOverrideSV = useDerivedValue(() => config.nowOverride);
|
|
@@ -212,6 +289,14 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
212
289
|
const [initialTimestamp] = useState(() => Date.now() / 1000);
|
|
213
290
|
const timestamp = useSharedValue(initialTimestamp);
|
|
214
291
|
|
|
292
|
+
// Pan-scroll state. `viewEnd` null = follow live; a number freezes the right
|
|
293
|
+
// edge at that absolute time. `liveEdge` mirrors the would-be-live timestamp
|
|
294
|
+
// each frame so the gesture can clamp / detect catch-up. Both default to
|
|
295
|
+
// "following" so charts without `timeScroll` behave exactly as before.
|
|
296
|
+
const viewEnd = useSharedValue<number | null>(null);
|
|
297
|
+
const liveEdge = useSharedValue(initialTimestamp);
|
|
298
|
+
const edgeValue = useSharedValue(0);
|
|
299
|
+
|
|
215
300
|
// Live data extrema (value + time of the visible high / low). NaN until the
|
|
216
301
|
// first tick finds data — the extrema label stays hidden until then.
|
|
217
302
|
const extremaMinValue = useSharedValue(NaN);
|
|
@@ -250,6 +335,10 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
250
335
|
nowOverrideSV,
|
|
251
336
|
windowBufferSV,
|
|
252
337
|
pausedSV,
|
|
338
|
+
viewEndSV: viewEnd,
|
|
339
|
+
viewWindowSV: viewWindow,
|
|
340
|
+
liveEdgeSV: liveEdge,
|
|
341
|
+
edgeValueSV: edgeValue,
|
|
253
342
|
modeSV,
|
|
254
343
|
candles,
|
|
255
344
|
liveCandle,
|
|
@@ -307,6 +396,10 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
|
|
|
307
396
|
canvasWidth,
|
|
308
397
|
canvasHeight,
|
|
309
398
|
timestamp,
|
|
399
|
+
viewEnd,
|
|
400
|
+
viewWindow,
|
|
401
|
+
liveEdge,
|
|
402
|
+
edgeValue,
|
|
310
403
|
extremaMinValue,
|
|
311
404
|
extremaMaxValue,
|
|
312
405
|
extremaMinTime,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { MS_PER_FRAME_60FPS } from "../constants";
|
|
9
9
|
import type { LiveChartPoint, SeriesConfig } from "../types";
|
|
10
10
|
import { tickLiveChartSeriesEngineFrame } from "./liveChartSeriesEngineTick";
|
|
11
|
-
import type { MultiEngineState } from "./useLiveChartEngine";
|
|
11
|
+
import type { ChartEngineScroll, MultiEngineState } from "./useLiveChartEngine";
|
|
12
12
|
|
|
13
13
|
export interface MultiSeriesEngineConfig {
|
|
14
14
|
series: SharedValue<SeriesConfig[]>;
|
|
@@ -47,6 +47,12 @@ export interface MultiEngineFrameRefs {
|
|
|
47
47
|
nowOverrideSV?: SharedValue<number | undefined>;
|
|
48
48
|
windowBufferSV?: SharedValue<number>;
|
|
49
49
|
pausedSV: SharedValue<boolean>;
|
|
50
|
+
/** Pan-scroll right-edge override (null = follow live). Optional for callers/tests. */
|
|
51
|
+
viewEndSV?: SharedValue<number | null>;
|
|
52
|
+
/** Pinch-zoom window-width override (null = follow timeWindow). Optional for callers/tests. */
|
|
53
|
+
viewWindowSV?: SharedValue<number | null>;
|
|
54
|
+
/** Receives the computed live edge each frame. Optional for callers/tests. */
|
|
55
|
+
liveEdgeSV?: SharedValue<number>;
|
|
50
56
|
extremaMinValue: SharedValue<number>;
|
|
51
57
|
extremaMaxValue: SharedValue<number>;
|
|
52
58
|
extremaMinTime: SharedValue<number>;
|
|
@@ -105,6 +111,7 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
105
111
|
displayMax: sv.displayMax.value,
|
|
106
112
|
displayWindow: sv.displayWindow.value,
|
|
107
113
|
timestamp: sv.timestamp.value,
|
|
114
|
+
liveEdge: sv.liveEdgeSV?.value ?? 0,
|
|
108
115
|
displayValues,
|
|
109
116
|
opacities,
|
|
110
117
|
extremaMinValue: sv.extremaMinValue.value,
|
|
@@ -129,11 +136,14 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
129
136
|
series: seriesSnap,
|
|
130
137
|
nowSeconds: Date.now() / 1000,
|
|
131
138
|
paused: sv.pausedSV.value,
|
|
139
|
+
viewEnd: sv.viewEndSV?.value,
|
|
140
|
+
viewWindow: sv.viewWindowSV?.value,
|
|
132
141
|
});
|
|
133
142
|
sv.displayMin.value = state.displayMin;
|
|
134
143
|
sv.displayMax.value = state.displayMax;
|
|
135
144
|
sv.displayWindow.value = state.displayWindow;
|
|
136
145
|
sv.timestamp.value = state.timestamp;
|
|
146
|
+
if (sv.liveEdgeSV) sv.liveEdgeSV.value = state.liveEdge;
|
|
137
147
|
sv.displaySeriesValues.value = state.displayValues;
|
|
138
148
|
sv.seriesOpacities.value = state.opacities;
|
|
139
149
|
sv.extremaMinValue.value = state.extremaMinValue;
|
|
@@ -148,8 +158,11 @@ export function applyLiveChartSeriesEngineFrame(
|
|
|
148
158
|
*/
|
|
149
159
|
export function useLiveChartSeriesEngine(
|
|
150
160
|
config: MultiSeriesEngineConfig,
|
|
151
|
-
): MultiEngineState {
|
|
152
|
-
|
|
161
|
+
): MultiEngineState & ChartEngineScroll {
|
|
162
|
+
// Pinch-zoom window-width override (null = follow the configured window).
|
|
163
|
+
// Declared first so `timeWindow` folds it in — see useLiveChartEngine.
|
|
164
|
+
const viewWindow = useSharedValue<number | null>(null);
|
|
165
|
+
const timeWindow = useDerivedValue(() => viewWindow.value ?? config.timeWindow);
|
|
153
166
|
const smoothing = useDerivedValue(() => config.smoothing);
|
|
154
167
|
const adaptiveSpeedBoostSV = useDerivedValue(() => config.adaptiveSpeedBoost);
|
|
155
168
|
const exaggerateSV = useDerivedValue(() => config.exaggerate ?? false);
|
|
@@ -170,6 +183,10 @@ export function useLiveChartSeriesEngine(
|
|
|
170
183
|
const [initialTimestamp] = useState(() => Date.now() / 1000);
|
|
171
184
|
const timestamp = useSharedValue(initialTimestamp);
|
|
172
185
|
|
|
186
|
+
// Pan-scroll state (see useLiveChartEngine). Defaults to following live.
|
|
187
|
+
const viewEnd = useSharedValue<number | null>(null);
|
|
188
|
+
const liveEdge = useSharedValue(initialTimestamp);
|
|
189
|
+
|
|
173
190
|
const displaySeriesValues = useSharedValue<number[]>([]);
|
|
174
191
|
const seriesOpacities = useSharedValue<number[]>([]);
|
|
175
192
|
|
|
@@ -217,6 +234,9 @@ export function useLiveChartSeriesEngine(
|
|
|
217
234
|
nowOverrideSV,
|
|
218
235
|
windowBufferSV,
|
|
219
236
|
pausedSV,
|
|
237
|
+
viewEndSV: viewEnd,
|
|
238
|
+
viewWindowSV: viewWindow,
|
|
239
|
+
liveEdgeSV: liveEdge,
|
|
220
240
|
extremaMinValue,
|
|
221
241
|
extremaMaxValue,
|
|
222
242
|
extremaMinTime,
|
|
@@ -237,6 +257,9 @@ export function useLiveChartSeriesEngine(
|
|
|
237
257
|
canvasWidth,
|
|
238
258
|
canvasHeight,
|
|
239
259
|
timestamp,
|
|
260
|
+
viewEnd,
|
|
261
|
+
viewWindow,
|
|
262
|
+
liveEdge,
|
|
240
263
|
series,
|
|
241
264
|
displaySeriesValues,
|
|
242
265
|
seriesOpacities,
|