react-native-livechart 3.12.0 → 4.1.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 +16 -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 +6 -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 +13 -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 +62 -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 +13 -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 +9 -4
- package/dist/hooks/useMarkers.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/hooks/useYAxis.d.ts +1 -1
- package/dist/hooks/useYAxis.d.ts.map +1 -1
- 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 +10 -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 +524 -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 +150 -57
- 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 +550 -32
- package/src/components/LiveChartSeries.tsx +111 -5
- package/src/components/MarkerOverlay.tsx +92 -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 +49 -28
- package/src/constants.ts +17 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +133 -2
- package/src/core/useLiveChartEngine.ts +111 -11
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +71 -2
- 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 +33 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +39 -11
- 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/hooks/useYAxis.ts +2 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +35 -3
- 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 +544 -21
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { StyleSheet, View, type LayoutChangeEvent } from "react-native";
|
|
2
|
+
import Animated, {
|
|
3
|
+
useAnimatedStyle,
|
|
4
|
+
useDerivedValue,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
|
|
9
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
10
|
+
import type { ChartPadding } from "../draw/line";
|
|
11
|
+
import { computeScrubDotY } from "../hooks/crosshairShared";
|
|
12
|
+
import {
|
|
13
|
+
classifyReferenceEdge,
|
|
14
|
+
referenceLineForm,
|
|
15
|
+
resolveReferenceBadge,
|
|
16
|
+
} from "../math/referenceLines";
|
|
17
|
+
import type { ReferenceLine, ReferenceLineRenderProps } from "../types";
|
|
18
|
+
|
|
19
|
+
/** Horizontal anchor for the floated tag, mirroring the built-in badge/label. */
|
|
20
|
+
type HAnchor = "left" | "center" | "right";
|
|
21
|
+
|
|
22
|
+
/** Pin gap from the anchored plot edge, in px (matches the badge edge inset). */
|
|
23
|
+
const ANCHOR_INSET = 2;
|
|
24
|
+
/** Off-screen edge inset for the pinned handle, in px (matches the off-axis badge). */
|
|
25
|
+
const EDGE_INSET = 12;
|
|
26
|
+
|
|
27
|
+
/** Where a custom tag pins horizontally: the badge position, else the label
|
|
28
|
+
* position, else inside the left edge. */
|
|
29
|
+
function resolveAnchor(line: ReferenceLine): HAnchor {
|
|
30
|
+
const badge = resolveReferenceBadge(line);
|
|
31
|
+
if (badge) return badge.position;
|
|
32
|
+
return line.labelPosition ?? "left";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Lightweight stand-in SharedValue for the suppression probe (see
|
|
36
|
+
* {@link customReferenceLineFlags}); never bound to the UI thread, so only the
|
|
37
|
+
* read accessors a render might touch are provided. */
|
|
38
|
+
function stub<T>(v: T): SharedValue<T> {
|
|
39
|
+
return { value: v, get: () => v } as unknown as SharedValue<T>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Which Form-A reference lines a `renderReferenceLine` returns an element for —
|
|
44
|
+
* the set whose built-in Skia tag is suppressed (so there's no double-draw). The
|
|
45
|
+
* render fn is probed with placeholder SharedValues; it must decide null-ness from
|
|
46
|
+
* `line` / `index`, not from live values (mirrors how `renderMarker` is probed for
|
|
47
|
+
* the marker-atlas exclusion set). Index-aligned with `lines`.
|
|
48
|
+
*/
|
|
49
|
+
export function customReferenceLineFlags(
|
|
50
|
+
lines: ReferenceLine[],
|
|
51
|
+
render?: (
|
|
52
|
+
ctx: ReferenceLineRenderProps,
|
|
53
|
+
) => React.ReactElement | null | undefined,
|
|
54
|
+
): boolean[] {
|
|
55
|
+
if (!render) return lines.map(() => false);
|
|
56
|
+
return lines.map((line, index) => {
|
|
57
|
+
if (referenceLineForm(line) !== "line") return false;
|
|
58
|
+
return (
|
|
59
|
+
render({
|
|
60
|
+
line,
|
|
61
|
+
index,
|
|
62
|
+
value: stub(line.value ?? 0),
|
|
63
|
+
valueStr: stub(""),
|
|
64
|
+
y: stub(-1),
|
|
65
|
+
inRange: stub(true),
|
|
66
|
+
edge: stub<"above" | "in" | "below">("in"),
|
|
67
|
+
dragging: stub(false),
|
|
68
|
+
}) != null
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One custom-rendered reference-line tag: a React Native element floated over the
|
|
75
|
+
* canvas, vertically centered on the line's value-Y and horizontally pinned to the
|
|
76
|
+
* badge / label position. Mirrors `CustomMarkerView` — each tag projects its own
|
|
77
|
+
* value every frame, so the transform runs on the UI thread with no JS re-render
|
|
78
|
+
* as the chart rescales or the line is dragged. `pointerEvents="box-none"` lets
|
|
79
|
+
* empty space fall through to the chart gestures while an interactive leaf inside
|
|
80
|
+
* the custom element can still be touched.
|
|
81
|
+
*/
|
|
82
|
+
function CustomReferenceLineView({
|
|
83
|
+
line,
|
|
84
|
+
index,
|
|
85
|
+
render,
|
|
86
|
+
engine,
|
|
87
|
+
padding,
|
|
88
|
+
formatValue,
|
|
89
|
+
dragValues,
|
|
90
|
+
dragActive,
|
|
91
|
+
}: {
|
|
92
|
+
line: ReferenceLine;
|
|
93
|
+
index: number;
|
|
94
|
+
render: (
|
|
95
|
+
ctx: ReferenceLineRenderProps,
|
|
96
|
+
) => React.ReactElement | null | undefined;
|
|
97
|
+
engine: ChartEngineLayout;
|
|
98
|
+
padding: ChartPadding;
|
|
99
|
+
formatValue: (v: number) => string;
|
|
100
|
+
dragValues: SharedValue<number[]>;
|
|
101
|
+
dragActive: SharedValue<boolean[]>;
|
|
102
|
+
}) {
|
|
103
|
+
const staticValue = line.value ?? 0;
|
|
104
|
+
|
|
105
|
+
// Live value: a drag override (if present) else the static prop value.
|
|
106
|
+
const value = useDerivedValue<number>(() => {
|
|
107
|
+
const dv = dragValues.get()[index];
|
|
108
|
+
return dv != null ? dv : staticValue;
|
|
109
|
+
});
|
|
110
|
+
const valueStr = useDerivedValue(() => formatValue(value.get()));
|
|
111
|
+
const edge = useDerivedValue<"above" | "in" | "below">(() =>
|
|
112
|
+
classifyReferenceEdge(
|
|
113
|
+
value.get(),
|
|
114
|
+
engine.displayMin.get(),
|
|
115
|
+
engine.displayMax.get(),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
const inRange = useDerivedValue(() => edge.get() === "in");
|
|
119
|
+
const dragging = useDerivedValue(() => dragActive.get()[index] === true);
|
|
120
|
+
|
|
121
|
+
// Canvas Y of the value (the element's vertical center). Off-screen values pin
|
|
122
|
+
// to the nearest plot edge (inset), matching the built-in off-axis badge.
|
|
123
|
+
const y = useDerivedValue(() => {
|
|
124
|
+
const ch = engine.canvasHeight.get();
|
|
125
|
+
const raw = computeScrubDotY(
|
|
126
|
+
value.get(),
|
|
127
|
+
engine.displayMin.get(),
|
|
128
|
+
engine.displayMax.get(),
|
|
129
|
+
ch,
|
|
130
|
+
padding.top,
|
|
131
|
+
padding.bottom,
|
|
132
|
+
);
|
|
133
|
+
if (raw < 0) return -1;
|
|
134
|
+
const top = padding.top + EDGE_INSET;
|
|
135
|
+
const bottom = ch - padding.bottom - EDGE_INSET;
|
|
136
|
+
return Math.min(bottom, Math.max(top, raw));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Measured element size, so the transform can center / pin it.
|
|
140
|
+
const size = useSharedValue({ width: 0, height: 0 });
|
|
141
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
142
|
+
const { width, height } = e.nativeEvent.layout;
|
|
143
|
+
size.value = { width, height };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const anchor = resolveAnchor(line);
|
|
147
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
148
|
+
const yy = y.get();
|
|
149
|
+
const s = size.get();
|
|
150
|
+
const w = engine.canvasWidth.get();
|
|
151
|
+
const visible = yy >= 0 && w > 0;
|
|
152
|
+
const x1 = padding.left;
|
|
153
|
+
const x2 = w - padding.right;
|
|
154
|
+
let tx: number;
|
|
155
|
+
if (anchor === "right") tx = x2 - ANCHOR_INSET - s.width;
|
|
156
|
+
else if (anchor === "center") tx = (x1 + x2) / 2 - s.width / 2;
|
|
157
|
+
else tx = x1 + ANCHOR_INSET;
|
|
158
|
+
return {
|
|
159
|
+
opacity: visible ? 1 : 0,
|
|
160
|
+
transform: [{ translateX: tx }, { translateY: yy - s.height / 2 }],
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const element = render({
|
|
165
|
+
line,
|
|
166
|
+
index,
|
|
167
|
+
value,
|
|
168
|
+
valueStr,
|
|
169
|
+
y,
|
|
170
|
+
inRange,
|
|
171
|
+
edge,
|
|
172
|
+
dragging,
|
|
173
|
+
});
|
|
174
|
+
if (element == null) return null;
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Animated.View
|
|
178
|
+
pointerEvents="box-none"
|
|
179
|
+
onLayout={onLayout}
|
|
180
|
+
style={[styles.anchor, animatedStyle]}
|
|
181
|
+
>
|
|
182
|
+
{element}
|
|
183
|
+
</Animated.View>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* React Native overlay (NOT Skia) that floats `renderReferenceLine` elements over
|
|
189
|
+
* the canvas, one per Form-A line the consumer customizes. Rendered as a sibling
|
|
190
|
+
* of `<Canvas>` (like {@link CustomMarkerOverlay}) so the tags can be any RN view
|
|
191
|
+
* and stay crisp at native resolution. Lines whose render returns an element have
|
|
192
|
+
* their built-in Skia tag suppressed upstream (see {@link customReferenceLineFlags}),
|
|
193
|
+
* so there's no double-draw.
|
|
194
|
+
*/
|
|
195
|
+
export function CustomReferenceLineOverlay({
|
|
196
|
+
lines,
|
|
197
|
+
renderReferenceLine,
|
|
198
|
+
custom,
|
|
199
|
+
engine,
|
|
200
|
+
padding,
|
|
201
|
+
formatValue,
|
|
202
|
+
dragValues,
|
|
203
|
+
dragActive,
|
|
204
|
+
}: {
|
|
205
|
+
lines: ReferenceLine[];
|
|
206
|
+
renderReferenceLine: (
|
|
207
|
+
ctx: ReferenceLineRenderProps,
|
|
208
|
+
) => React.ReactElement | null | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* Index-aligned flags (from {@link customReferenceLineFlags}) marking which
|
|
211
|
+
* lines the render owns — only these get a floated view, matching the built-in
|
|
212
|
+
* tags suppressed upstream. Avoids re-probing the render here.
|
|
213
|
+
*/
|
|
214
|
+
custom: boolean[];
|
|
215
|
+
engine: ChartEngineLayout;
|
|
216
|
+
padding: ChartPadding;
|
|
217
|
+
formatValue: (v: number) => string;
|
|
218
|
+
dragValues: SharedValue<number[]>;
|
|
219
|
+
dragActive: SharedValue<boolean[]>;
|
|
220
|
+
}) {
|
|
221
|
+
const children: React.ReactElement[] = [];
|
|
222
|
+
for (let i = 0; i < lines.length; i++) {
|
|
223
|
+
if (!custom[i]) continue;
|
|
224
|
+
children.push(
|
|
225
|
+
<CustomReferenceLineView
|
|
226
|
+
key={i}
|
|
227
|
+
line={lines[i]}
|
|
228
|
+
index={i}
|
|
229
|
+
render={renderReferenceLine}
|
|
230
|
+
engine={engine}
|
|
231
|
+
padding={padding}
|
|
232
|
+
formatValue={formatValue}
|
|
233
|
+
dragValues={dragValues}
|
|
234
|
+
dragActive={dragActive}
|
|
235
|
+
/>,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (children.length === 0) return null;
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
242
|
+
{children}
|
|
243
|
+
</View>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const styles = StyleSheet.create({
|
|
248
|
+
// top/left 0 + translate so the measured element can be pinned to the value.
|
|
249
|
+
anchor: { position: "absolute", top: 0, left: 0 },
|
|
250
|
+
});
|
|
@@ -48,6 +48,7 @@ export function CustomTooltipOverlay({
|
|
|
48
48
|
placement,
|
|
49
49
|
margin = 8,
|
|
50
50
|
lineTop,
|
|
51
|
+
scrubDotY,
|
|
51
52
|
}: {
|
|
52
53
|
renderTooltip: (ctx: TooltipRenderProps) => React.ReactElement | null | undefined;
|
|
53
54
|
scrubX: SharedValue<number>;
|
|
@@ -60,13 +61,16 @@ export function CustomTooltipOverlay({
|
|
|
60
61
|
tooltipLayout: SharedValue<TooltipLayout>;
|
|
61
62
|
engine: ChartEngineLayout;
|
|
62
63
|
padding: ChartPadding;
|
|
63
|
-
placement: "side" | "top" | "bottom";
|
|
64
|
+
placement: "side" | "top" | "bottom" | "point";
|
|
64
65
|
/** Gap (px) between the pill and the plot edge it's pinned to. Default 8. */
|
|
65
66
|
margin?: number;
|
|
66
67
|
/** When `placement` is `"top"`, the overlay publishes the label's bottom edge
|
|
67
68
|
* (canvas Y) here so {@link CrosshairOverlay} can stop the crosshair line at
|
|
68
69
|
* the label instead of running through it; -1 when not top-pinned/active. */
|
|
69
70
|
lineTop?: SharedValue<number>;
|
|
71
|
+
/** Scrub intersection Y in canvas px (the value the dot marks) — the anchor
|
|
72
|
+
* for `"point"` placement. Omitted / -1 falls back to a top pin. */
|
|
73
|
+
scrubDotY?: SharedValue<number>;
|
|
70
74
|
}) {
|
|
71
75
|
// Formatted strings come ready-made off the layout (formatted UI-side in
|
|
72
76
|
// computeTooltipLayout / computeCandleTooltipLayout), so consumers don't need
|
|
@@ -125,12 +129,28 @@ export function CustomTooltipOverlay({
|
|
|
125
129
|
Math.max(sx - s.width / 2, leftBound),
|
|
126
130
|
Math.max(leftBound, rightBound),
|
|
127
131
|
);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
if (placement === "point") {
|
|
133
|
+
// Float just above the scrub dot, flipping below when there's no room
|
|
134
|
+
// above; clamp into the plot. Mirrors computeTooltipLayout's "point".
|
|
135
|
+
const dotY = scrubDotY?.get() ?? -1;
|
|
136
|
+
if (dotY < 0) {
|
|
137
|
+
y = padding.top + margin;
|
|
138
|
+
} else {
|
|
139
|
+
const topLimit = padding.top + EDGE_GAP;
|
|
140
|
+
const bottomLimit = ch - padding.bottom - EDGE_GAP - s.height;
|
|
141
|
+
const aboveY = dotY - margin - s.height;
|
|
142
|
+
const belowY = dotY + margin;
|
|
143
|
+
y = aboveY >= topLimit ? aboveY : belowY;
|
|
144
|
+
y = Math.min(Math.max(y, topLimit), Math.max(topLimit, bottomLimit));
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
y =
|
|
148
|
+
placement === "top"
|
|
149
|
+
? // Pin to the canvas top edge (not the plot's inner top), so the
|
|
150
|
+
// label sits above the data and the crosshair line can stop at it.
|
|
151
|
+
margin
|
|
152
|
+
: ch - padding.bottom - margin - s.height;
|
|
153
|
+
}
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
return {
|
|
@@ -24,6 +24,7 @@ export function DotOverlay({
|
|
|
24
24
|
radius,
|
|
25
25
|
ring,
|
|
26
26
|
color,
|
|
27
|
+
viewEnd,
|
|
27
28
|
}: {
|
|
28
29
|
dotX: SharedValue<number>;
|
|
29
30
|
dotY: SharedValue<number>;
|
|
@@ -36,11 +37,18 @@ export function DotOverlay({
|
|
|
36
37
|
ring: ResolvedDotRingConfig | null;
|
|
37
38
|
/** Dot (and pulse) fill color; falls back to the chart line color. */
|
|
38
39
|
color: string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Time-scroll right edge (`null` = following live). While scrolled back the
|
|
42
|
+
* pulse is suppressed — it's driven by the (now frozen) view timestamp, so it
|
|
43
|
+
* would stick or flicker, and a "live" heartbeat on a historical point is wrong.
|
|
44
|
+
*/
|
|
45
|
+
viewEnd?: SharedValue<number | null>;
|
|
39
46
|
}) {
|
|
40
47
|
const dotColor = color ?? palette.line;
|
|
41
48
|
|
|
42
49
|
const pulseRadius = useDerivedValue(() => {
|
|
43
50
|
if (!pulse) return 0;
|
|
51
|
+
if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
|
|
44
52
|
const nowMs = engine.timestamp.value * 1000;
|
|
45
53
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
46
54
|
if (t >= 1) return 0;
|
|
@@ -49,6 +57,7 @@ export function DotOverlay({
|
|
|
49
57
|
|
|
50
58
|
const pulseOpacity = useDerivedValue(() => {
|
|
51
59
|
if (!pulse) return 0;
|
|
60
|
+
if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
|
|
52
61
|
const nowMs = engine.timestamp.value * 1000;
|
|
53
62
|
const t = (nowMs % pulse.interval) / pulse.duration;
|
|
54
63
|
if (t >= 1) return 0;
|