react-native-livechart 4.11.0 → 4.13.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/CrosshairLine.d.ts +12 -2
- package/dist/components/CrosshairLine.d.ts.map +1 -1
- package/dist/components/CrosshairOverlay.d.ts +12 -2
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +15 -10
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +3 -5
- 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/PerSeriesTooltipOverlay.d.ts +21 -0
- package/dist/components/PerSeriesTooltipOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +27 -6
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +8 -2
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/core/liveIndicatorVisibility.d.ts +9 -0
- package/dist/core/liveIndicatorVisibility.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +45 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/draw/grid.d.ts +13 -0
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/hooks/crosshairSeries.d.ts +10 -2
- package/dist/hooks/crosshairSeries.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +55 -5
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/delayedPanGuard.d.ts +66 -8
- package/dist/hooks/delayedPanGuard.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +5 -9
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +13 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts +27 -3
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useCrosshairVisibleOpacity.d.ts +8 -0
- package/dist/hooks/useCrosshairVisibleOpacity.d.ts.map +1 -0
- package/dist/hooks/useMomentum.d.ts +1 -1
- package/dist/hooks/useMomentum.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +15 -1
- package/dist/hooks/usePanScroll.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +10 -1
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/lerp.d.ts.map +1 -1
- package/dist/math/momentum.d.ts +1 -1
- package/dist/math/momentum.d.ts.map +1 -1
- package/dist/math/referenceLines.d.ts +7 -0
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +207 -18
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairLine.tsx +39 -8
- package/src/components/CrosshairOverlay.tsx +49 -15
- package/src/components/CustomReferenceLineOverlay.tsx +42 -13
- package/src/components/CustomTooltipOverlay.tsx +15 -3
- package/src/components/DotOverlay.tsx +24 -8
- package/src/components/LiveChart.tsx +182 -39
- package/src/components/LiveChartSeries.tsx +195 -16
- package/src/components/PerSeriesTooltipOverlay.tsx +290 -0
- package/src/components/ReferenceLineOverlay.tsx +288 -141
- package/src/components/YAxisOverlay.tsx +39 -10
- package/src/core/liveIndicatorVisibility.ts +25 -0
- package/src/core/resolveConfig.ts +115 -0
- package/src/draw/grid.ts +33 -0
- package/src/hooks/crosshairSeries.ts +262 -33
- package/src/hooks/crosshairShared.ts +117 -6
- package/src/hooks/delayedPanGuard.ts +111 -6
- package/src/hooks/useChartPaths.ts +25 -15
- package/src/hooks/useCrosshair.ts +103 -32
- package/src/hooks/useCrosshairSeries.ts +126 -11
- package/src/hooks/useCrosshairVisibleOpacity.ts +57 -0
- package/src/hooks/useMomentum.ts +8 -1
- package/src/hooks/usePanScroll.ts +46 -5
- package/src/hooks/useReferenceLine.ts +26 -1
- package/src/index.ts +1 -0
- package/src/math/lerp.ts +7 -0
- package/src/math/momentum.ts +22 -5
- package/src/math/referenceLines.ts +38 -0
- package/src/types.ts +215 -18
|
@@ -11,6 +11,7 @@ import type { ReactNode } from "react";
|
|
|
11
11
|
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
12
12
|
import { type ChartPadding } from "../draw/line";
|
|
13
13
|
import { type TooltipLayout } from "../hooks/crosshairShared";
|
|
14
|
+
import { useCrosshairVisibleOpacity } from "../hooks/useCrosshairVisibleOpacity";
|
|
14
15
|
import type { LiveChartPalette } from "../types";
|
|
15
16
|
import type { ResolvedSelectionDotConfig } from "../core/resolveConfig";
|
|
16
17
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
@@ -35,6 +36,11 @@ export function CrosshairOverlay({
|
|
|
35
36
|
dimOpacity = 0.3,
|
|
36
37
|
liveDotExtent = 0,
|
|
37
38
|
crosshairLineColor,
|
|
39
|
+
crosshairStrokeWidth = 1,
|
|
40
|
+
crosshairOvershoot = 0,
|
|
41
|
+
crosshairFade = true,
|
|
42
|
+
crosshairFadeDistance = 4,
|
|
43
|
+
crosshairLineCap,
|
|
38
44
|
crosshairDash,
|
|
39
45
|
crosshairDimColor,
|
|
40
46
|
tooltipBackground,
|
|
@@ -70,7 +76,7 @@ export function CrosshairOverlay({
|
|
|
70
76
|
/** Scrub intersection Y in canvas px (the value the dot marks); -1 hides it. */
|
|
71
77
|
selectionY?: SharedValue<number>;
|
|
72
78
|
/** Whether scrubbing is active (passed through to a custom dot). */
|
|
73
|
-
scrubActive
|
|
79
|
+
scrubActive: SharedValue<number> | SharedValue<boolean>;
|
|
74
80
|
/** Fallback selection-dot color (accent / leading-series color), used when the
|
|
75
81
|
* config's own `color` is unset. */
|
|
76
82
|
selectionColor?: string;
|
|
@@ -83,6 +89,16 @@ export function CrosshairOverlay({
|
|
|
83
89
|
* reserves beyond it. Default 0. */
|
|
84
90
|
liveDotExtent?: number;
|
|
85
91
|
crosshairLineColor?: string;
|
|
92
|
+
/** Vertical crosshair line width in px. Default 1. */
|
|
93
|
+
crosshairStrokeWidth?: number;
|
|
94
|
+
/** Resolved extension past the top and bottom plot edges in px. Default 0. */
|
|
95
|
+
crosshairOvershoot?: number;
|
|
96
|
+
/** Fade the crosshair near the live edge. Default true. */
|
|
97
|
+
crosshairFade?: boolean;
|
|
98
|
+
/** Visible-crosshair fade distance near the live edge in px. Default 4. */
|
|
99
|
+
crosshairFadeDistance?: number;
|
|
100
|
+
/** Cap style for the vertical crosshair line. */
|
|
101
|
+
crosshairLineCap?: "butt" | "round" | "square";
|
|
86
102
|
/** Dash intervals `[on, off, …]` for the crosshair line; omit → solid. */
|
|
87
103
|
crosshairDash?: number[];
|
|
88
104
|
crosshairDimColor?: string;
|
|
@@ -104,21 +120,27 @@ export function CrosshairOverlay({
|
|
|
104
120
|
// React's "final argument changed size between renders" error. Listing the
|
|
105
121
|
// captured plain values keeps the dependency array a constant size. SharedValue
|
|
106
122
|
// reads stay reactive regardless of this list.
|
|
107
|
-
const p1 = useDerivedValue(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
const p1 = useDerivedValue(
|
|
124
|
+
() => {
|
|
125
|
+
// A top-pinned custom tooltip pushes the line's start down to its measured
|
|
126
|
+
// bottom (lineTop) so the line stops at the label; -1 → no top tooltip.
|
|
127
|
+
const lt = lineTop?.value ?? -1;
|
|
128
|
+
return {
|
|
129
|
+
x: scrubX.value,
|
|
130
|
+
y: lt >= 0 ? lt : padding.top - crosshairOvershoot,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
[scrubX, padding.top, lineTop, crosshairOvershoot],
|
|
134
|
+
);
|
|
116
135
|
const p2 = useDerivedValue(
|
|
117
136
|
() => ({
|
|
118
137
|
x: scrubX.value,
|
|
119
|
-
y:
|
|
138
|
+
y:
|
|
139
|
+
engine.canvasHeight.value -
|
|
140
|
+
padding.bottom +
|
|
141
|
+
crosshairOvershoot,
|
|
120
142
|
}),
|
|
121
|
-
[scrubX, engine.canvasHeight, padding.bottom],
|
|
143
|
+
[scrubX, engine.canvasHeight, padding.bottom, crosshairOvershoot],
|
|
122
144
|
);
|
|
123
145
|
|
|
124
146
|
const dimWidth = useDerivedValue(() => {
|
|
@@ -159,6 +181,17 @@ export function CrosshairOverlay({
|
|
|
159
181
|
);
|
|
160
182
|
const backgroundColor = `rgb(${palette.bgRgb[0]},${palette.bgRgb[1]},${palette.bgRgb[2]})`;
|
|
161
183
|
|
|
184
|
+
// Keep the trailing dim on its original edge fade. Only the visible
|
|
185
|
+
// crosshair group (line, dot, tooltip) uses the configurable distance.
|
|
186
|
+
const visibleOpacity = useCrosshairVisibleOpacity(
|
|
187
|
+
scrubX,
|
|
188
|
+
engine.canvasWidth,
|
|
189
|
+
padding.right,
|
|
190
|
+
scrubActive,
|
|
191
|
+
crosshairFade,
|
|
192
|
+
crosshairFadeDistance,
|
|
193
|
+
);
|
|
194
|
+
|
|
162
195
|
return (
|
|
163
196
|
<>
|
|
164
197
|
{crosshairDimColor !== undefined ? (
|
|
@@ -195,12 +228,13 @@ export function CrosshairOverlay({
|
|
|
195
228
|
</Group>
|
|
196
229
|
) : null}
|
|
197
230
|
|
|
198
|
-
<Group opacity={
|
|
231
|
+
<Group opacity={visibleOpacity}>
|
|
199
232
|
<Line
|
|
200
233
|
p1={p1}
|
|
201
234
|
p2={p2}
|
|
202
235
|
color={crosshairLineColor ?? palette.crosshairLine}
|
|
203
|
-
strokeWidth={
|
|
236
|
+
strokeWidth={crosshairStrokeWidth}
|
|
237
|
+
strokeCap={crosshairLineCap}
|
|
204
238
|
>
|
|
205
239
|
{crosshairDash ? <DashPathEffect intervals={crosshairDash} /> : null}
|
|
206
240
|
</Line>
|
|
@@ -212,7 +246,7 @@ export function CrosshairOverlay({
|
|
|
212
246
|
x={scrubX}
|
|
213
247
|
y={selectionY}
|
|
214
248
|
active={scrubActive}
|
|
215
|
-
opacity={
|
|
249
|
+
opacity={visibleOpacity}
|
|
216
250
|
color={selectionColor ?? palette.line}
|
|
217
251
|
/>
|
|
218
252
|
|
|
@@ -12,6 +12,7 @@ import { computeScrubDotY } from "../hooks/crosshairShared";
|
|
|
12
12
|
import {
|
|
13
13
|
classifyReferenceEdge,
|
|
14
14
|
referenceLineForm,
|
|
15
|
+
referenceLineReactKeys,
|
|
15
16
|
resolveReferenceBadge,
|
|
16
17
|
} from "../math/referenceLines";
|
|
17
18
|
import type { ReferenceLine, ReferenceLineRenderProps } from "../types";
|
|
@@ -39,20 +40,24 @@ function stub<T>(v: T): SharedValue<T> {
|
|
|
39
40
|
return { value: v, get: () => v } as unknown as SharedValue<T>;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** Whether a custom tag owns every state or only an off-axis tag. */
|
|
44
|
+
export type CustomReferenceLineMode = "always" | "off-axis";
|
|
45
|
+
|
|
42
46
|
/**
|
|
43
|
-
* Which Form-A reference lines a
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* the marker-atlas exclusion set). Index-aligned with `lines`.
|
|
47
|
+
* Which Form-A reference lines a custom renderer returns an element for. This
|
|
48
|
+
* initial probe determines a line's static eligibility only; `"off-axis"` still
|
|
49
|
+
* switches the built-in tag and the RN tag on the UI thread as the edge changes.
|
|
50
|
+
* Index-aligned with `lines`.
|
|
48
51
|
*/
|
|
49
52
|
export function customReferenceLineFlags(
|
|
50
53
|
lines: ReferenceLine[],
|
|
51
54
|
render?: (
|
|
52
55
|
ctx: ReferenceLineRenderProps,
|
|
53
56
|
) => React.ReactElement | null | undefined,
|
|
57
|
+
mode: CustomReferenceLineMode = "always",
|
|
54
58
|
): boolean[] {
|
|
55
59
|
if (!render) return lines.map(() => false);
|
|
60
|
+
const offAxisOnly = mode === "off-axis";
|
|
56
61
|
return lines.map((line, index) => {
|
|
57
62
|
if (referenceLineForm(line) !== "line") return false;
|
|
58
63
|
return (
|
|
@@ -62,8 +67,10 @@ export function customReferenceLineFlags(
|
|
|
62
67
|
value: stub(line.value ?? 0),
|
|
63
68
|
valueStr: stub(""),
|
|
64
69
|
y: stub(-1),
|
|
65
|
-
inRange: stub(
|
|
66
|
-
edge: stub<"above" | "in" | "below">(
|
|
70
|
+
inRange: stub(!offAxisOnly),
|
|
71
|
+
edge: stub<"above" | "in" | "below">(
|
|
72
|
+
offAxisOnly ? "above" : "in",
|
|
73
|
+
),
|
|
67
74
|
dragging: stub(false),
|
|
68
75
|
}) != null
|
|
69
76
|
);
|
|
@@ -88,6 +95,8 @@ function CustomReferenceLineView({
|
|
|
88
95
|
formatValue,
|
|
89
96
|
dragValues,
|
|
90
97
|
dragActive,
|
|
98
|
+
tagWidths,
|
|
99
|
+
offAxisOnly,
|
|
91
100
|
}: {
|
|
92
101
|
line: ReferenceLine;
|
|
93
102
|
index: number;
|
|
@@ -101,6 +110,10 @@ function CustomReferenceLineView({
|
|
|
101
110
|
dragValues?: SharedValue<number[]>;
|
|
102
111
|
/** Optional drag state used by draggable single-series lines. */
|
|
103
112
|
dragActive?: SharedValue<boolean[]>;
|
|
113
|
+
/** Measured custom-tag widths, index-aligned for the Skia connector. */
|
|
114
|
+
tagWidths?: SharedValue<number[]>;
|
|
115
|
+
/** Show this custom tag only while its line is pinned above or below the plot. */
|
|
116
|
+
offAxisOnly: boolean;
|
|
104
117
|
}) {
|
|
105
118
|
const staticValue = line.value ?? 0;
|
|
106
119
|
|
|
@@ -147,7 +160,14 @@ function CustomReferenceLineView({
|
|
|
147
160
|
const size = useSharedValue({ width: 0, height: 0 });
|
|
148
161
|
const onLayout = (e: LayoutChangeEvent) => {
|
|
149
162
|
const { width, height } = e.nativeEvent.layout;
|
|
150
|
-
size.
|
|
163
|
+
size.set({ width, height });
|
|
164
|
+
if (tagWidths) {
|
|
165
|
+
const previous = tagWidths.get();
|
|
166
|
+
if (previous[index] === width) return;
|
|
167
|
+
const widths = previous.slice();
|
|
168
|
+
widths[index] = width;
|
|
169
|
+
tagWidths.set(widths);
|
|
170
|
+
}
|
|
151
171
|
};
|
|
152
172
|
|
|
153
173
|
const anchor = resolveAnchor(line);
|
|
@@ -155,7 +175,7 @@ function CustomReferenceLineView({
|
|
|
155
175
|
const yy = y.get();
|
|
156
176
|
const s = size.get();
|
|
157
177
|
const w = engine.canvasWidth.get();
|
|
158
|
-
const visible = yy >= 0 && w > 0;
|
|
178
|
+
const visible = yy >= 0 && w > 0 && (!offAxisOnly || edge.get() !== "in");
|
|
159
179
|
const x1 = padding.left;
|
|
160
180
|
const x2 = w - padding.right;
|
|
161
181
|
let tx: number;
|
|
@@ -195,9 +215,9 @@ function CustomReferenceLineView({
|
|
|
195
215
|
* React Native overlay (NOT Skia) that floats `renderReferenceLine` elements over
|
|
196
216
|
* the canvas, one per Form-A line the consumer customizes. Rendered as a sibling
|
|
197
217
|
* of `<Canvas>` (like {@link CustomMarkerOverlay}) so the tags can be any RN view
|
|
198
|
-
* and stay crisp at native resolution. Lines whose render returns an element
|
|
199
|
-
* their built-in
|
|
200
|
-
*
|
|
218
|
+
* and stay crisp at native resolution. Lines whose render returns an element hide
|
|
219
|
+
* their built-in pill / label upstream (see {@link customReferenceLineFlags}); a
|
|
220
|
+
* badged line's Skia connector remains, measured against the native tag's width.
|
|
201
221
|
*/
|
|
202
222
|
export function CustomReferenceLineOverlay({
|
|
203
223
|
lines,
|
|
@@ -208,6 +228,8 @@ export function CustomReferenceLineOverlay({
|
|
|
208
228
|
formatValue,
|
|
209
229
|
dragValues,
|
|
210
230
|
dragActive,
|
|
231
|
+
tagWidths,
|
|
232
|
+
offAxisOnly = false,
|
|
211
233
|
}: {
|
|
212
234
|
lines: ReferenceLine[];
|
|
213
235
|
renderReferenceLine: (
|
|
@@ -226,13 +248,18 @@ export function CustomReferenceLineOverlay({
|
|
|
226
248
|
dragValues?: SharedValue<number[]>;
|
|
227
249
|
/** Optional drag state used by draggable single-series lines. */
|
|
228
250
|
dragActive?: SharedValue<boolean[]>;
|
|
251
|
+
/** Measured custom-tag widths, index-aligned for the Skia connector. */
|
|
252
|
+
tagWidths?: SharedValue<number[]>;
|
|
253
|
+
/** Keep the built-in in-range tag and show this RN tag only off-axis. */
|
|
254
|
+
offAxisOnly?: boolean;
|
|
229
255
|
}) {
|
|
230
256
|
const children: React.ReactElement[] = [];
|
|
257
|
+
const lineKeys = referenceLineReactKeys(lines);
|
|
231
258
|
for (let i = 0; i < lines.length; i++) {
|
|
232
259
|
if (!custom[i]) continue;
|
|
233
260
|
children.push(
|
|
234
261
|
<CustomReferenceLineView
|
|
235
|
-
key={i}
|
|
262
|
+
key={lineKeys[i]}
|
|
236
263
|
line={lines[i]}
|
|
237
264
|
index={i}
|
|
238
265
|
render={renderReferenceLine}
|
|
@@ -241,6 +268,8 @@ export function CustomReferenceLineOverlay({
|
|
|
241
268
|
formatValue={formatValue}
|
|
242
269
|
dragValues={dragValues}
|
|
243
270
|
dragActive={dragActive}
|
|
271
|
+
tagWidths={tagWidths}
|
|
272
|
+
offAxisOnly={offAxisOnly}
|
|
244
273
|
/>,
|
|
245
274
|
);
|
|
246
275
|
}
|
|
@@ -9,6 +9,7 @@ import Animated, {
|
|
|
9
9
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
10
10
|
import type { ChartPadding } from "../draw/line";
|
|
11
11
|
import type { TooltipLayout } from "../hooks/crosshairShared";
|
|
12
|
+
import { resolveCrosshairVisibleOpacity } from "../hooks/useCrosshairVisibleOpacity";
|
|
12
13
|
import type { CandlePoint, TooltipRenderProps } from "../types";
|
|
13
14
|
|
|
14
15
|
// Mirror the Skia tooltip's offsets (see crosshairShared.ts) so a custom pill
|
|
@@ -41,12 +42,13 @@ export function CustomTooltipOverlay({
|
|
|
41
42
|
scrubTime,
|
|
42
43
|
scrubActive,
|
|
43
44
|
scrubCandle,
|
|
44
|
-
crosshairOpacity,
|
|
45
45
|
tooltipLayout,
|
|
46
46
|
engine,
|
|
47
47
|
padding,
|
|
48
48
|
placement,
|
|
49
49
|
margin = 8,
|
|
50
|
+
crosshairFade = true,
|
|
51
|
+
crosshairFadeDistance = 4,
|
|
50
52
|
lineTop,
|
|
51
53
|
scrubDotY,
|
|
52
54
|
}: {
|
|
@@ -57,13 +59,16 @@ export function CustomTooltipOverlay({
|
|
|
57
59
|
scrubActive: SharedValue<boolean>;
|
|
58
60
|
/** OHLC candle under the crosshair (candle mode); omitted/`null` in line mode. */
|
|
59
61
|
scrubCandle?: SharedValue<CandlePoint | null>;
|
|
60
|
-
crosshairOpacity: SharedValue<number>;
|
|
61
62
|
tooltipLayout: SharedValue<TooltipLayout>;
|
|
62
63
|
engine: ChartEngineLayout;
|
|
63
64
|
padding: ChartPadding;
|
|
64
65
|
placement: "side" | "top" | "bottom" | "point";
|
|
65
66
|
/** Gap (px) between the pill and the plot edge it's pinned to. Default 8. */
|
|
66
67
|
margin?: number;
|
|
68
|
+
/** Fade the tooltip near the live edge. Default true. */
|
|
69
|
+
crosshairFade?: boolean;
|
|
70
|
+
/** Tooltip fade distance near the live edge in px. Default 4. */
|
|
71
|
+
crosshairFadeDistance?: number;
|
|
67
72
|
/** When `placement` is `"top"`, the overlay publishes the label's bottom edge
|
|
68
73
|
* (canvas Y) here so {@link CrosshairOverlay} can stop the crosshair line at
|
|
69
74
|
* the label instead of running through it; -1 when not top-pinned/active. */
|
|
@@ -154,7 +159,14 @@ export function CustomTooltipOverlay({
|
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
return {
|
|
157
|
-
opacity:
|
|
162
|
+
opacity: resolveCrosshairVisibleOpacity(
|
|
163
|
+
active,
|
|
164
|
+
sx,
|
|
165
|
+
cw,
|
|
166
|
+
padding.right,
|
|
167
|
+
crosshairFade,
|
|
168
|
+
crosshairFadeDistance,
|
|
169
|
+
),
|
|
158
170
|
transform: [{ translateX: x }, { translateY: y }],
|
|
159
171
|
};
|
|
160
172
|
});
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Circle, Group } from "@shopify/react-native-skia";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useDerivedValue,
|
|
5
|
+
useFrameCallback,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
type SharedValue,
|
|
8
|
+
} from "react-native-reanimated";
|
|
3
9
|
import type {
|
|
4
10
|
ResolvedDotRingConfig,
|
|
5
11
|
ResolvedPulseConfig,
|
|
6
12
|
} from "../core/resolveConfig";
|
|
7
13
|
import type { LiveChartPalette } from "../types";
|
|
8
|
-
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
9
14
|
|
|
10
15
|
const MIN_PULSE_RADIUS = 9;
|
|
11
16
|
|
|
@@ -19,7 +24,6 @@ export function DotOverlay({
|
|
|
19
24
|
dotX,
|
|
20
25
|
dotY,
|
|
21
26
|
palette,
|
|
22
|
-
engine,
|
|
23
27
|
pulse,
|
|
24
28
|
radius,
|
|
25
29
|
ring,
|
|
@@ -29,7 +33,6 @@ export function DotOverlay({
|
|
|
29
33
|
dotX: SharedValue<number>;
|
|
30
34
|
dotY: SharedValue<number>;
|
|
31
35
|
palette: LiveChartPalette;
|
|
32
|
-
engine: ChartEngineLayout;
|
|
33
36
|
pulse: ResolvedPulseConfig | null;
|
|
34
37
|
/** Radius of the color-filled dot in pixels. */
|
|
35
38
|
radius: number;
|
|
@@ -39,17 +42,30 @@ export function DotOverlay({
|
|
|
39
42
|
color: string | undefined;
|
|
40
43
|
/**
|
|
41
44
|
* Time-scroll right edge (`null` = following live). While scrolled back the
|
|
42
|
-
* pulse is suppressed
|
|
43
|
-
*
|
|
45
|
+
* pulse is suppressed because a "live" heartbeat on a historical point is
|
|
46
|
+
* misleading.
|
|
44
47
|
*/
|
|
45
48
|
viewEnd?: SharedValue<number | null>;
|
|
46
49
|
}) {
|
|
47
50
|
const dotColor = color ?? palette.line;
|
|
48
51
|
|
|
52
|
+
// Pulse clock: wall time, not `engine.timestamp` — a `nowOverride` freezes
|
|
53
|
+
// the engine clock between data updates, which freezes the pulse with it.
|
|
54
|
+
// Runs only while a pulse is configured (resolvePulse returns null when the
|
|
55
|
+
// chart is static, so a suspended chart burns no frames here).
|
|
56
|
+
const pulseClockMs = useSharedValue(0);
|
|
57
|
+
/* istanbul ignore next -- frame-callback worklet runs on the UI thread, not in Jest */
|
|
58
|
+
const pulseClock = useFrameCallback((frame) => {
|
|
59
|
+
pulseClockMs.value = frame.timestamp;
|
|
60
|
+
}, pulse != null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
pulseClock.setActive(pulse != null);
|
|
63
|
+
}, [pulse, pulseClock]);
|
|
64
|
+
|
|
49
65
|
const pulseRadius = useDerivedValue(() => {
|
|
50
66
|
if (!pulse) return 0;
|
|
51
67
|
if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
|
|
52
|
-
const nowMs =
|
|
68
|
+
const nowMs = pulseClockMs.value;
|
|
53
69
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
54
70
|
if (t >= 1) return 0;
|
|
55
71
|
return MIN_PULSE_RADIUS + t * (pulse.maxRadius - MIN_PULSE_RADIUS);
|
|
@@ -58,7 +74,7 @@ export function DotOverlay({
|
|
|
58
74
|
const pulseOpacity = useDerivedValue(() => {
|
|
59
75
|
if (!pulse) return 0;
|
|
60
76
|
if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
|
|
61
|
-
const nowMs =
|
|
77
|
+
const nowMs = pulseClockMs.value;
|
|
62
78
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
63
79
|
if (t >= 1) return 0;
|
|
64
80
|
return pulse.opacity * (1 - t);
|