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
|
@@ -18,10 +18,20 @@ export interface ReferenceLineLayout {
|
|
|
18
18
|
y: number;
|
|
19
19
|
/** Band bottom-edge y (value/time band); equals `y` for a plain line. */
|
|
20
20
|
yBottom: number;
|
|
21
|
-
/** Left x extent. */
|
|
21
|
+
/** Left x extent of the plot (badge / label / connector anchor). */
|
|
22
22
|
x1: number;
|
|
23
|
-
/** Right x extent. */
|
|
23
|
+
/** Right x extent of the plot (badge / label / connector anchor). */
|
|
24
24
|
x2: number;
|
|
25
|
+
/**
|
|
26
|
+
* Left x of the **drawn** line / band. Equals {@link x1} normally, or `0`
|
|
27
|
+
* (canvas edge) when the line is full-width. Decoupled from {@link x1} so the
|
|
28
|
+
* line can run through the gutter while the badge/label stays in the plot.
|
|
29
|
+
*/
|
|
30
|
+
lineX1: number;
|
|
31
|
+
/** Right x of the drawn line / band: {@link x2} normally, or the canvas width when full-width. */
|
|
32
|
+
lineX2: number;
|
|
33
|
+
/** Stroke the plain horizontal line (true for a plain Form-A line, or a full-width badged line). */
|
|
34
|
+
drawLine: boolean;
|
|
25
35
|
/** Resolved label text (with appended value when `showValue`); "" when hidden. */
|
|
26
36
|
label: string;
|
|
27
37
|
labelX: number;
|
|
@@ -54,6 +64,9 @@ const INVISIBLE: ReferenceLineLayout = {
|
|
|
54
64
|
yBottom: -1,
|
|
55
65
|
x1: 0,
|
|
56
66
|
x2: 0,
|
|
67
|
+
lineX1: 0,
|
|
68
|
+
lineX2: 0,
|
|
69
|
+
drawLine: false,
|
|
57
70
|
label: "",
|
|
58
71
|
labelX: 0,
|
|
59
72
|
labelY: -1,
|
|
@@ -96,7 +109,7 @@ interface BadgeGeometry {
|
|
|
96
109
|
* inside the pill, with a dashed connector running to the opposite edge.
|
|
97
110
|
*/
|
|
98
111
|
function badgeGeometry(
|
|
99
|
-
position: "left" | "right",
|
|
112
|
+
position: "left" | "center" | "right",
|
|
100
113
|
icon: string,
|
|
101
114
|
text: string,
|
|
102
115
|
hasChevron: boolean,
|
|
@@ -134,7 +147,9 @@ function badgeGeometry(
|
|
|
134
147
|
const pillX =
|
|
135
148
|
position === "right"
|
|
136
149
|
? x2 - BADGE_EDGE_INSET - pillW
|
|
137
|
-
:
|
|
150
|
+
: position === "center"
|
|
151
|
+
? (x1 + x2) / 2 - pillW / 2
|
|
152
|
+
: x1 + BADGE_EDGE_INSET;
|
|
138
153
|
|
|
139
154
|
let cursor = pillX + BADGE_PAD_X;
|
|
140
155
|
let chevronCx = -1;
|
|
@@ -159,13 +174,14 @@ function badgeGeometry(
|
|
|
159
174
|
connStart = x1;
|
|
160
175
|
connEnd = end;
|
|
161
176
|
}
|
|
162
|
-
} else {
|
|
177
|
+
} else if (position === "left") {
|
|
163
178
|
const start = pillX + pillW + 4;
|
|
164
179
|
if (start < x2) {
|
|
165
180
|
connStart = start;
|
|
166
181
|
connEnd = x2;
|
|
167
182
|
}
|
|
168
183
|
}
|
|
184
|
+
// "center" floats the pill at the value with no connector.
|
|
169
185
|
|
|
170
186
|
return { pillX, pillW, iconX, textX, chevronCx, connStart, connEnd };
|
|
171
187
|
}
|
|
@@ -213,6 +229,8 @@ export function computeReferenceBadgeRect(
|
|
|
213
229
|
dMax: number,
|
|
214
230
|
font: SkFont,
|
|
215
231
|
formatValue: (value: number) => string,
|
|
232
|
+
/** Live value override (e.g. a dragged value), defaulting to `line.value`. */
|
|
233
|
+
valueOverride?: number,
|
|
216
234
|
): ReferenceBadgeRect | null {
|
|
217
235
|
"worklet";
|
|
218
236
|
const badge = resolveReferenceBadge(line);
|
|
@@ -228,7 +246,7 @@ export function computeReferenceBadgeRect(
|
|
|
228
246
|
|
|
229
247
|
const x1 = padding.left;
|
|
230
248
|
const x2 = canvasWidth - padding.right;
|
|
231
|
-
const v = line.value;
|
|
249
|
+
const v = valueOverride ?? line.value;
|
|
232
250
|
const edge = classifyReferenceEdge(v, dMin, dMax);
|
|
233
251
|
|
|
234
252
|
let y: number;
|
|
@@ -267,6 +285,14 @@ export function useReferenceLine(
|
|
|
267
285
|
line: ReferenceLine | undefined,
|
|
268
286
|
formatValue: (v: number) => string,
|
|
269
287
|
font: SkFont,
|
|
288
|
+
/**
|
|
289
|
+
* Per-line live value overrides (e.g. dragged values), index-aligned with the
|
|
290
|
+
* chart's `referenceLines`. When provided and the slot at {@link index} holds a
|
|
291
|
+
* number, the Form-A line tracks that value instead of the static `line.value`,
|
|
292
|
+
* so a drag updates the rendered line/badge on the UI thread. Omit for static lines.
|
|
293
|
+
*/
|
|
294
|
+
dragValues?: SharedValue<number[]>,
|
|
295
|
+
index = 0,
|
|
270
296
|
): SharedValue<ReferenceLineLayout> {
|
|
271
297
|
const form = line ? referenceLineForm(line) : "none";
|
|
272
298
|
// Badge presentation depends only on the (stable) line props — resolve once.
|
|
@@ -284,6 +310,11 @@ export function useReferenceLine(
|
|
|
284
310
|
const chartH = chartBottom - chartTop;
|
|
285
311
|
const x1 = padding.left;
|
|
286
312
|
const x2 = w - padding.right;
|
|
313
|
+
// Full-width lines/bands run edge-to-edge through the gutters (0..canvas);
|
|
314
|
+
// the badge/label anchor (x1/x2) stays at the plot edges either way.
|
|
315
|
+
const fullWidth = line.fullWidth ?? false;
|
|
316
|
+
const lineX1 = fullWidth ? 0 : x1;
|
|
317
|
+
const lineX2 = fullWidth ? w : x2;
|
|
287
318
|
|
|
288
319
|
const fm = font.getMetrics();
|
|
289
320
|
const baselineOffset = (fm.ascent + fm.descent) / 2;
|
|
@@ -320,6 +351,9 @@ export function useReferenceLine(
|
|
|
320
351
|
yBottom: chartBottom,
|
|
321
352
|
x1: bx1,
|
|
322
353
|
x2: bx2,
|
|
354
|
+
// Time band is vertical and time-bounded — full-width does not apply.
|
|
355
|
+
lineX1: bx1,
|
|
356
|
+
lineX2: bx2,
|
|
323
357
|
label,
|
|
324
358
|
labelX,
|
|
325
359
|
labelY: chartTop - fm.ascent + 2,
|
|
@@ -361,6 +395,8 @@ export function useReferenceLine(
|
|
|
361
395
|
yBottom: yBot,
|
|
362
396
|
x1,
|
|
363
397
|
x2,
|
|
398
|
+
lineX1,
|
|
399
|
+
lineX2,
|
|
364
400
|
label,
|
|
365
401
|
labelX,
|
|
366
402
|
labelY: yTop - fm.ascent + 2,
|
|
@@ -369,7 +405,11 @@ export function useReferenceLine(
|
|
|
369
405
|
|
|
370
406
|
// ── Form A — horizontal line (with optional pill badge) ──────────────────
|
|
371
407
|
if (line.value === undefined) return INVISIBLE;
|
|
372
|
-
|
|
408
|
+
// Effective value: a live drag override (if present) else the static prop.
|
|
409
|
+
const v =
|
|
410
|
+
dragValues && dragValues.value[index] != null
|
|
411
|
+
? dragValues.value[index]
|
|
412
|
+
: line.value;
|
|
373
413
|
const edge = classifyReferenceEdge(v, dMin, dMax);
|
|
374
414
|
|
|
375
415
|
// Off-screen: pin the badge to the nearest edge with a chevron (when a badge
|
|
@@ -418,6 +458,10 @@ export function useReferenceLine(
|
|
|
418
458
|
yBottom: y,
|
|
419
459
|
x1,
|
|
420
460
|
x2,
|
|
461
|
+
lineX1,
|
|
462
|
+
lineX2,
|
|
463
|
+
// Full-width: the edge-to-edge line replaces the dashed connector.
|
|
464
|
+
drawLine: fullWidth,
|
|
421
465
|
label: text,
|
|
422
466
|
labelX: g.textX,
|
|
423
467
|
labelY: y - baselineOffset,
|
|
@@ -426,8 +470,8 @@ export function useReferenceLine(
|
|
|
426
470
|
pillW: g.pillW,
|
|
427
471
|
iconX: g.iconX,
|
|
428
472
|
icon: badge.icon,
|
|
429
|
-
connStart: g.connStart,
|
|
430
|
-
connEnd: g.connEnd,
|
|
473
|
+
connStart: fullWidth ? -1 : g.connStart,
|
|
474
|
+
connEnd: fullWidth ? -1 : g.connEnd,
|
|
431
475
|
};
|
|
432
476
|
}
|
|
433
477
|
|
|
@@ -449,6 +493,9 @@ export function useReferenceLine(
|
|
|
449
493
|
yBottom: y,
|
|
450
494
|
x1,
|
|
451
495
|
x2,
|
|
496
|
+
lineX1,
|
|
497
|
+
lineX2,
|
|
498
|
+
drawLine: true,
|
|
452
499
|
label,
|
|
453
500
|
labelX,
|
|
454
501
|
labelY: y - baselineOffset,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SkFont } from "@shopify/react-native-skia";
|
|
2
2
|
import { Gesture } from "react-native-gesture-handler";
|
|
3
|
-
import { useDerivedValue } from "react-native-reanimated";
|
|
3
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
4
4
|
import { scheduleOnRN } from "react-native-worklets";
|
|
5
5
|
|
|
6
6
|
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
@@ -33,6 +33,9 @@ export function useReferenceLinePress(
|
|
|
33
33
|
/** Touch-target inflation around each pill, in px. */
|
|
34
34
|
hitSlop: number,
|
|
35
35
|
onPress?: (line: ReferenceLine, index: number) => void,
|
|
36
|
+
/** Per-line live value overrides (dragged values) so a draggable line's hit-rect
|
|
37
|
+
* tracks the drag, index-aligned with `lines`. */
|
|
38
|
+
dragValues?: SharedValue<number[]>,
|
|
36
39
|
): {
|
|
37
40
|
tapGesture: ReturnType<typeof Gesture.Tap>;
|
|
38
41
|
hitTest: (x: number, y: number) => boolean;
|
|
@@ -47,9 +50,20 @@ export function useReferenceLinePress(
|
|
|
47
50
|
const dMin = engine.displayMin.value;
|
|
48
51
|
const dMax = engine.displayMax.value;
|
|
49
52
|
const out: (ReferenceBadgeRect | null)[] = [];
|
|
53
|
+
const dv = dragValues?.value;
|
|
50
54
|
for (let i = 0; i < lines.length; i++) {
|
|
51
55
|
out.push(
|
|
52
|
-
computeReferenceBadgeRect(
|
|
56
|
+
computeReferenceBadgeRect(
|
|
57
|
+
lines[i],
|
|
58
|
+
w,
|
|
59
|
+
h,
|
|
60
|
+
padding,
|
|
61
|
+
dMin,
|
|
62
|
+
dMax,
|
|
63
|
+
font,
|
|
64
|
+
formatValue,
|
|
65
|
+
dv ? dv[i] : undefined,
|
|
66
|
+
),
|
|
53
67
|
);
|
|
54
68
|
}
|
|
55
69
|
return out;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useAnimatedReaction,
|
|
4
|
+
type SharedValue,
|
|
5
|
+
} from "react-native-reanimated";
|
|
6
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
7
|
+
|
|
8
|
+
import type { VisibleRange } from "../types";
|
|
9
|
+
|
|
10
|
+
export type { VisibleRange };
|
|
11
|
+
|
|
12
|
+
/** Engine SharedValues the visible-range reaction reads. */
|
|
13
|
+
export interface VisibleRangeEngineRefs {
|
|
14
|
+
timestamp: SharedValue<number>;
|
|
15
|
+
displayWindow: SharedValue<number>;
|
|
16
|
+
viewEnd: SharedValue<number | null>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseVisibleRangeOptions {
|
|
20
|
+
engine: VisibleRangeEngineRefs;
|
|
21
|
+
/** Earliest retained time (unix seconds) — used for the `onReachStart` trigger. */
|
|
22
|
+
minTime: SharedValue<number>;
|
|
23
|
+
/** Fires (throttled to ~1 Hz) when the visible window's edges change. */
|
|
24
|
+
onVisibleRangeChange?: (range: VisibleRange) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Fires once when the window's left edge comes within one window-width of the
|
|
27
|
+
* earliest retained data — the cue to lazily page in older history. Re-arms
|
|
28
|
+
* after the edge moves back out.
|
|
29
|
+
*/
|
|
30
|
+
onReachStart?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Throttle key for a visible range: integer seconds of each edge plus the
|
|
35
|
+
* near-start flag. The reaction emits only when this changes, so a live chart
|
|
36
|
+
* (whose right edge slides every frame) notifies at most ~once per second rather
|
|
37
|
+
* than per frame, and `onReachStart` edge-triggers off the trailing flag.
|
|
38
|
+
*/
|
|
39
|
+
export function rangeSignature(
|
|
40
|
+
startSec: number,
|
|
41
|
+
endSec: number,
|
|
42
|
+
near: boolean,
|
|
43
|
+
): string {
|
|
44
|
+
"worklet";
|
|
45
|
+
return `${Math.round(startSec)}|${Math.round(endSec)}|${near ? 1 : 0}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether the window's left edge is within `threshold` seconds of the oldest data. */
|
|
49
|
+
export function isNearStart(
|
|
50
|
+
winStart: number,
|
|
51
|
+
minTime: number,
|
|
52
|
+
threshold: number,
|
|
53
|
+
): boolean {
|
|
54
|
+
"worklet";
|
|
55
|
+
return winStart <= minTime + threshold;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Bridges the UI-thread visible window to JS paging callbacks. A reaction watches
|
|
60
|
+
* the window edges and `scheduleOnRN`s `onVisibleRangeChange` (throttled) and
|
|
61
|
+
* `onReachStart` (edge-triggered near the oldest data). Inert when both callbacks
|
|
62
|
+
* are absent. Used by both `LiveChart` and `LiveChartSeries`.
|
|
63
|
+
*/
|
|
64
|
+
export function useVisibleRange({
|
|
65
|
+
engine,
|
|
66
|
+
minTime,
|
|
67
|
+
onVisibleRangeChange,
|
|
68
|
+
onReachStart,
|
|
69
|
+
}: UseVisibleRangeOptions): void {
|
|
70
|
+
// Latest callbacks behind refs so the (deps-stable) dispatchers below always
|
|
71
|
+
// call the current prop without re-subscribing the reaction each render.
|
|
72
|
+
const rangeCb = useRef(onVisibleRangeChange);
|
|
73
|
+
const reachCb = useRef(onReachStart);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
rangeCb.current = onVisibleRangeChange;
|
|
76
|
+
reachCb.current = onReachStart;
|
|
77
|
+
}, [onVisibleRangeChange, onReachStart]);
|
|
78
|
+
|
|
79
|
+
const emitRange = useCallback(
|
|
80
|
+
/* istanbul ignore next -- dispatched via scheduleOnRN from the UI thread, not in Jest */
|
|
81
|
+
(startSec: number, endSec: number, following: boolean) => {
|
|
82
|
+
rangeCb.current?.({ startSec, endSec, following });
|
|
83
|
+
},
|
|
84
|
+
[],
|
|
85
|
+
);
|
|
86
|
+
const emitReachStart = useCallback(
|
|
87
|
+
/* istanbul ignore next -- dispatched via scheduleOnRN from the UI thread, not in Jest */
|
|
88
|
+
() => {
|
|
89
|
+
reachCb.current?.();
|
|
90
|
+
},
|
|
91
|
+
[],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const active = onVisibleRangeChange != null || onReachStart != null;
|
|
95
|
+
|
|
96
|
+
useAnimatedReaction(
|
|
97
|
+
/* istanbul ignore next -- Reanimated reaction worklet; runs on the UI thread, not in Jest */
|
|
98
|
+
() => {
|
|
99
|
+
if (!active) return ""; // inert — never changes, so the reaction never fires
|
|
100
|
+
const end = engine.timestamp.value;
|
|
101
|
+
const win = engine.displayWindow.value;
|
|
102
|
+
const start = end - win;
|
|
103
|
+
return rangeSignature(start, end, isNearStart(start, minTime.value, win));
|
|
104
|
+
},
|
|
105
|
+
/* istanbul ignore next -- Reanimated reaction; dispatches on the JS thread, not exercised under Jest */
|
|
106
|
+
(curr, prev) => {
|
|
107
|
+
if (!active || curr === "" || curr === prev) return;
|
|
108
|
+
const end = engine.timestamp.value;
|
|
109
|
+
const win = engine.displayWindow.value;
|
|
110
|
+
const start = end - win;
|
|
111
|
+
const following = engine.viewEnd.value == null;
|
|
112
|
+
const near = isNearStart(start, minTime.value, win);
|
|
113
|
+
const wasNear = typeof prev === "string" && prev.endsWith("|1");
|
|
114
|
+
scheduleOnRN(emitRange, start, end, following);
|
|
115
|
+
if (near && !wasNear) scheduleOnRN(emitReachStart);
|
|
116
|
+
},
|
|
117
|
+
[active, emitRange, emitReachStart],
|
|
118
|
+
);
|
|
119
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,10 @@ export { MONO_FONT_FAMILY } from "./lib/monoFontFamily";
|
|
|
25
25
|
export { useDegen } from "./hooks/useDegen";
|
|
26
26
|
export { useTradeStream } from "./hooks/useTradeStream";
|
|
27
27
|
|
|
28
|
+
// ── renderOverlay bridge hooks (reactive price→Y / time→X for custom overlays) ──
|
|
29
|
+
|
|
30
|
+
export { usePriceY, useTimeX } from "./hooks/useChartOverlayContext";
|
|
31
|
+
|
|
28
32
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
29
33
|
|
|
30
34
|
export type {
|
|
@@ -32,10 +36,14 @@ export type {
|
|
|
32
36
|
AxisLabelConfig,
|
|
33
37
|
BadgeConfig,
|
|
34
38
|
BadgeMetrics,
|
|
39
|
+
BadgeStyleConfig,
|
|
35
40
|
BadgeVariant,
|
|
36
41
|
CandleMetrics,
|
|
37
42
|
CandlePoint,
|
|
38
43
|
ChartInsets,
|
|
44
|
+
ChartOverlayContext,
|
|
45
|
+
ChartPlotRect,
|
|
46
|
+
ChartScale,
|
|
39
47
|
ChartSegment,
|
|
40
48
|
DegenOptions,
|
|
41
49
|
DegenShakePayload,
|
|
@@ -57,8 +65,11 @@ export type {
|
|
|
57
65
|
LiveChartProps,
|
|
58
66
|
LiveChartSeriesProps,
|
|
59
67
|
Marker,
|
|
60
|
-
|
|
68
|
+
MarkerClusterConfig,
|
|
61
69
|
MarkerKind,
|
|
70
|
+
MarkerPressEvent,
|
|
71
|
+
MarkerRenderContext,
|
|
72
|
+
MarkerSide,
|
|
62
73
|
Momentum,
|
|
63
74
|
MomentumConfig,
|
|
64
75
|
MotionMetrics,
|
|
@@ -66,6 +77,8 @@ export type {
|
|
|
66
77
|
PulseConfig,
|
|
67
78
|
ReferenceLine,
|
|
68
79
|
ReferenceLineBadgeConfig,
|
|
80
|
+
ReferenceLineGroupingConfig,
|
|
81
|
+
ReferenceLineRenderProps,
|
|
69
82
|
ScrubActionConfig,
|
|
70
83
|
ScrubActionPoint,
|
|
71
84
|
ScrubConfig,
|
|
@@ -83,6 +96,9 @@ export type {
|
|
|
83
96
|
TooltipRenderProps,
|
|
84
97
|
TradeEvent,
|
|
85
98
|
ValueLineConfig,
|
|
99
|
+
VisibleRange,
|
|
100
|
+
VolumeConfig,
|
|
86
101
|
XAxisConfig,
|
|
87
102
|
YAxisConfig,
|
|
103
|
+
ZoomConfig,
|
|
88
104
|
} from "./types";
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Marker, MarkerSide } from "../types";
|
|
2
|
+
import type { ProjectedMarker } from "./markers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolved {@link LiveChartProps.markerCluster} config (see `resolveMarkerCluster`).
|
|
6
|
+
*/
|
|
7
|
+
export interface ResolvedMarkerCluster {
|
|
8
|
+
/** `"anchored"` = no collision handling (glyphs overlap). `"stacked"` = fan +
|
|
9
|
+
* collapse co-located markers. */
|
|
10
|
+
mode: "anchored" | "stacked";
|
|
11
|
+
/** Fraction (0–1) that adjacent co-located glyphs overlap when fanned. The fan
|
|
12
|
+
* step is `glyphHeight * (1 - overlap)`; `glyphHeight` is a slightly generous
|
|
13
|
+
* primitive estimate, so the *visible* overlap runs a touch lower. */
|
|
14
|
+
overlap: number;
|
|
15
|
+
/** Extra px between a sided glyph and its anchor. */
|
|
16
|
+
gap: number;
|
|
17
|
+
/** Collapse a co-located run to a single count badge once it exceeds this many. */
|
|
18
|
+
maxBeforeGroup: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ClusterMarkersOpts {
|
|
22
|
+
config: ResolvedMarkerCluster;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Glyph box used when `marker.size` is unset — mirrors `markerAtlas.DEFAULT_ICON_SIZE`. */
|
|
26
|
+
const DEFAULT_GLYPH = 16;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Estimated vertical (≈ horizontal) footprint of a marker glyph in px. Derived
|
|
30
|
+
* from primitives only (no font metrics / atlas), so the hit-test hook and the
|
|
31
|
+
* overlays compute identical offsets from identical inputs — keeping the drawn
|
|
32
|
+
* and hit-tested positions in sync. Not pixel-exact with the atlas cell; the
|
|
33
|
+
* generous default `markerHitRadius` absorbs the small difference.
|
|
34
|
+
*/
|
|
35
|
+
export function glyphHeight(m: Marker): number {
|
|
36
|
+
"worklet";
|
|
37
|
+
const size = m.size ?? DEFAULT_GLYPH;
|
|
38
|
+
// The pill badge adds padding + a background ring around the icon glyph.
|
|
39
|
+
return m.pill ? size + 8 : size;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sideOf(m: Marker): MarkerSide {
|
|
43
|
+
"worklet";
|
|
44
|
+
return m.side ?? "center";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Axis-anchored kinds (graduation stem, clawback box) whose geometry is pinned
|
|
49
|
+
* to the baseline — they aren't fixed-size glyphs, so they don't stack or
|
|
50
|
+
* collapse. Mirrors `markerAtlas.isConnectorMarker` (duplicated to keep this
|
|
51
|
+
* math module free of the Skia-importing draw layer).
|
|
52
|
+
*/
|
|
53
|
+
function isConnector(m: Marker): boolean {
|
|
54
|
+
"worklet";
|
|
55
|
+
return !m.image && !m.icon && (m.kind === "graduation" || m.kind === "clawback");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stable rank so the index sort groups markers by side. */
|
|
59
|
+
function sideRank(side: MarkerSide): number {
|
|
60
|
+
"worklet";
|
|
61
|
+
return side === "above" ? 0 : side === "center" ? 1 : 2;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lay out one co-located bucket `idx[s..e)` (already ordered by time), mutating
|
|
66
|
+
* `proj`. All glyphs sit at the same `side` height (above / below / on the line)
|
|
67
|
+
* and fan out HORIZONTALLY with a slight overlap — the "( ) ) )" stacked-coins
|
|
68
|
+
* look, centered on the time anchor. When the bucket is larger than
|
|
69
|
+
* `maxBeforeGroup` it collapses instead: one representative (the newest) is
|
|
70
|
+
* flagged `isGrouped` with `groupCount`, the rest are `hidden` and point back via
|
|
71
|
+
* `groupRep` (so a tap can gather the members).
|
|
72
|
+
*/
|
|
73
|
+
function layoutBucket(
|
|
74
|
+
markers: Marker[],
|
|
75
|
+
proj: ProjectedMarker[],
|
|
76
|
+
idx: number[],
|
|
77
|
+
s: number,
|
|
78
|
+
e: number,
|
|
79
|
+
side: MarkerSide,
|
|
80
|
+
opts: ClusterMarkersOpts,
|
|
81
|
+
): void {
|
|
82
|
+
"worklet";
|
|
83
|
+
const count = e - s;
|
|
84
|
+
const anchorX = proj[idx[s]].x;
|
|
85
|
+
const anchorY = proj[idx[s]].y;
|
|
86
|
+
const { gap, maxBeforeGroup } = opts.config;
|
|
87
|
+
|
|
88
|
+
// Tallest glyph in the bucket sets the side offset and the horizontal step.
|
|
89
|
+
let h = 0;
|
|
90
|
+
for (let k = s; k < e; k++) {
|
|
91
|
+
const gh = glyphHeight(markers[idx[k]]);
|
|
92
|
+
if (gh > h) h = gh;
|
|
93
|
+
}
|
|
94
|
+
const sideDy =
|
|
95
|
+
side === "above" ? -(h / 2 + gap) : side === "below" ? h / 2 + gap : 0;
|
|
96
|
+
|
|
97
|
+
if (count > maxBeforeGroup) {
|
|
98
|
+
// Collapse to a count badge at the representative (newest = last by time).
|
|
99
|
+
const rep = idx[e - 1];
|
|
100
|
+
for (let k = s; k < e; k++) {
|
|
101
|
+
const p = proj[idx[k]];
|
|
102
|
+
p.groupRep = rep;
|
|
103
|
+
if (idx[k] === rep) {
|
|
104
|
+
p.x = anchorX;
|
|
105
|
+
p.y = anchorY + sideDy;
|
|
106
|
+
p.isGrouped = true;
|
|
107
|
+
p.groupCount = count;
|
|
108
|
+
p.hidden = false;
|
|
109
|
+
} else {
|
|
110
|
+
p.hidden = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Horizontal overlapping fan, centered on the anchor x. Glyphs share the side
|
|
117
|
+
// height. Positions run right→left as the bucket order advances, so the newest
|
|
118
|
+
// (last, highest array index) sits LEFTMOST — and since the overlay draws in
|
|
119
|
+
// array order, it paints on top: a left-over-right cascade ("(" over ")" over ")").
|
|
120
|
+
const step = h * (1 - opts.config.overlap);
|
|
121
|
+
for (let j = 0; j < count; j++) {
|
|
122
|
+
const p = proj[idx[s + j]];
|
|
123
|
+
p.x = anchorX + ((count - 1) / 2 - j) * step;
|
|
124
|
+
p.y = anchorY + sideDy;
|
|
125
|
+
p.hidden = false;
|
|
126
|
+
p.isGrouped = false;
|
|
127
|
+
p.groupCount = 0;
|
|
128
|
+
p.groupRep = -1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Collision pass over projected markers, mutating `proj` in place.
|
|
134
|
+
*
|
|
135
|
+
* `"anchored"` mode only applies each marker's {@link Marker.side} offset (a lone
|
|
136
|
+
* glyph sitting above/below its anchor); it does no bucketing, so charts that
|
|
137
|
+
* don't use `side` are untouched. `"stacked"` mode additionally buckets
|
|
138
|
+
* co-located markers (same side, overlapping x/y) and fans or collapses them.
|
|
139
|
+
*
|
|
140
|
+
* Runs every frame on the UI thread, so grouping is zoom/scroll-reactive. Reads
|
|
141
|
+
* the *raw* projected positions from `projectMarkers` and overwrites them.
|
|
142
|
+
*/
|
|
143
|
+
export function clusterMarkers(
|
|
144
|
+
markers: Marker[],
|
|
145
|
+
proj: ProjectedMarker[],
|
|
146
|
+
opts: ClusterMarkersOpts,
|
|
147
|
+
): void {
|
|
148
|
+
"worklet";
|
|
149
|
+
const n = markers.length;
|
|
150
|
+
|
|
151
|
+
if (opts.config.mode !== "stacked") {
|
|
152
|
+
// Side-only: shift sided glyphs off their anchor; no neighbor interaction.
|
|
153
|
+
for (let i = 0; i < n; i++) {
|
|
154
|
+
const p = proj[i];
|
|
155
|
+
if (!p.visible || isConnector(markers[i])) continue;
|
|
156
|
+
const side = sideOf(markers[i]);
|
|
157
|
+
if (side === "center") continue;
|
|
158
|
+
const off = glyphHeight(markers[i]) / 2 + opts.config.gap;
|
|
159
|
+
p.y += side === "above" ? -off : off;
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Collect visible indices, then order by (side, x, y, time) so co-located
|
|
165
|
+
// same-side markers are adjacent and fan in time order.
|
|
166
|
+
const idx: number[] = [];
|
|
167
|
+
for (let i = 0; i < n; i++) if (proj[i].visible && !isConnector(markers[i])) idx.push(i);
|
|
168
|
+
idx.sort((a, b) => {
|
|
169
|
+
const sr = sideRank(sideOf(markers[a])) - sideRank(sideOf(markers[b]));
|
|
170
|
+
if (sr !== 0) return sr;
|
|
171
|
+
if (proj[a].x !== proj[b].x) return proj[a].x - proj[b].x;
|
|
172
|
+
if (proj[a].y !== proj[b].y) return proj[a].y - proj[b].y;
|
|
173
|
+
return markers[a].time - markers[b].time;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Sweep into buckets: same side and within a glyph of the bucket's anchor in
|
|
177
|
+
// BOTH axes (so two markers at the same time but far apart in value don't merge).
|
|
178
|
+
let s = 0;
|
|
179
|
+
while (s < idx.length) {
|
|
180
|
+
const side = sideOf(markers[idx[s]]);
|
|
181
|
+
const startX = proj[idx[s]].x;
|
|
182
|
+
const startY = proj[idx[s]].y;
|
|
183
|
+
const colPx = glyphHeight(markers[idx[s]]);
|
|
184
|
+
let e = s + 1;
|
|
185
|
+
while (
|
|
186
|
+
e < idx.length &&
|
|
187
|
+
sideOf(markers[idx[e]]) === side &&
|
|
188
|
+
Math.abs(proj[idx[e]].x - startX) <= colPx &&
|
|
189
|
+
Math.abs(proj[idx[e]].y - startY) <= colPx
|
|
190
|
+
) {
|
|
191
|
+
e++;
|
|
192
|
+
}
|
|
193
|
+
layoutBucket(markers, proj, idx, s, e, side, opts);
|
|
194
|
+
s = e;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Markers belonging to the collapsed cluster whose representative is `repIndex`
|
|
200
|
+
* (ordered by time). Call after {@link clusterMarkers} has populated `proj`.
|
|
201
|
+
*/
|
|
202
|
+
export function clusterMembers(
|
|
203
|
+
markers: Marker[],
|
|
204
|
+
proj: ProjectedMarker[],
|
|
205
|
+
repIndex: number,
|
|
206
|
+
): Marker[] {
|
|
207
|
+
"worklet";
|
|
208
|
+
const out: Marker[] = [];
|
|
209
|
+
for (let i = 0; i < markers.length; i++) {
|
|
210
|
+
if (proj[i] && proj[i].groupRep === repIndex) out.push(markers[i]);
|
|
211
|
+
}
|
|
212
|
+
out.sort((a, b) => a.time - b.time);
|
|
213
|
+
return out;
|
|
214
|
+
}
|