react-native-livechart 4.9.1 → 4.10.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/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +9 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +2 -0
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +2 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/delayedPanGuard.d.ts +31 -0
- package/dist/hooks/delayedPanGuard.d.ts.map +1 -0
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -1
- package/dist/hooks/useXAxis.d.ts.map +1 -1
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairOverlay.tsx +6 -1
- package/src/components/LiveChart.tsx +505 -318
- package/src/components/MarkerOverlay.tsx +11 -16
- package/src/components/YAxisOverlay.tsx +43 -1
- package/src/draw/markerAtlas.ts +22 -2
- package/src/hooks/crosshairShared.ts +19 -18
- package/src/hooks/delayedPanGuard.ts +80 -0
- package/src/hooks/useCrosshair.ts +232 -182
- package/src/hooks/useCrosshairSeries.ts +38 -0
- package/src/hooks/useXAxis.ts +21 -5
- package/src/types.ts +7 -2
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
GROUP_BADGE_SIG,
|
|
26
26
|
groupBgSig,
|
|
27
27
|
groupCountText,
|
|
28
|
+
groupCountTextWidth,
|
|
28
29
|
groupGlyphSig,
|
|
29
30
|
isConnectorMarker,
|
|
30
31
|
markerAppearanceSig,
|
|
@@ -58,7 +59,7 @@ const GROUP_CORNER_INSET = 0.82;
|
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Append a collapsed-cluster count badge (round bg + centered, proportionally
|
|
61
|
-
*
|
|
62
|
+
* laid-out digits) to the atlas blit lists at `(cx, cy)`, scaled by `mul` — full
|
|
62
63
|
* size as the default group badge, shrunk in a glyph's corner for `showGroupCount`.
|
|
63
64
|
*
|
|
64
65
|
* A **module-level** worklet (not a closure inside the per-frame derived value):
|
|
@@ -77,6 +78,7 @@ function pushGroupCountBadge(
|
|
|
77
78
|
repColor: string,
|
|
78
79
|
count: number,
|
|
79
80
|
mul: number,
|
|
81
|
+
letterSpacing: number,
|
|
80
82
|
): void {
|
|
81
83
|
"worklet";
|
|
82
84
|
const text = groupCountText(count);
|
|
@@ -87,20 +89,11 @@ function pushGroupCountBadge(
|
|
|
87
89
|
);
|
|
88
90
|
sprites.push(bg.rect);
|
|
89
91
|
}
|
|
90
|
-
// Lay digits out proportionally (each by its own ink width),
|
|
91
|
-
//
|
|
92
|
-
//
|
|
92
|
+
// Lay digits out proportionally (each by its own ink width), centered. The
|
|
93
|
+
// configurable spacing is applied between glyph ink bounds, not inferred from
|
|
94
|
+
// the widest digit — proportional fonts otherwise overlap narrow digits.
|
|
93
95
|
const S = BADGE_TEXT_SCALE;
|
|
94
|
-
|
|
95
|
-
for (let d = 0; d < 10; d++) {
|
|
96
|
-
const dw = digitWidths["" + d] ?? 0;
|
|
97
|
-
if (dw > maxDW) maxDW = dw;
|
|
98
|
-
}
|
|
99
|
-
const kern = -maxDW * S * 0.42;
|
|
100
|
-
let totalW = 0;
|
|
101
|
-
for (let c = 0; c < text.length; c++) {
|
|
102
|
-
totalW += (digitWidths[text[c]] ?? 0) * S + (c > 0 ? kern : 0);
|
|
103
|
-
}
|
|
96
|
+
const totalW = groupCountTextWidth(text, digitWidths, letterSpacing);
|
|
104
97
|
let dx = cx - (totalW * mul) / 2;
|
|
105
98
|
for (let c = 0; c < text.length; c++) {
|
|
106
99
|
const w = (digitWidths[text[c]] ?? 0) * S * mul;
|
|
@@ -117,7 +110,7 @@ function pushGroupCountBadge(
|
|
|
117
110
|
);
|
|
118
111
|
sprites.push(gc.rect);
|
|
119
112
|
}
|
|
120
|
-
dx += w +
|
|
113
|
+
dx += w + letterSpacing * mul;
|
|
121
114
|
}
|
|
122
115
|
}
|
|
123
116
|
|
|
@@ -326,7 +319,7 @@ export function MarkerOverlay({
|
|
|
326
319
|
typeof cluster.groupBadge === "object" ? cluster.groupBadge : undefined;
|
|
327
320
|
const groupBadgeImage = groupBadgeCfg?.image;
|
|
328
321
|
const groupBadgeKey = groupBadgeCfg
|
|
329
|
-
? `${groupBadgeCfg.icon ?? ""}|${groupBadgeCfg.color ?? ""}|${groupBadgeCfg.pill ? 1 : 0}|${groupBadgeCfg.size ?? ""}`
|
|
322
|
+
? `${groupBadgeCfg.icon ?? ""}|${groupBadgeCfg.color ?? ""}|${groupBadgeCfg.pill ? 1 : 0}|${groupBadgeCfg.size ?? ""}|${groupBadgeCfg.letterSpacing ?? ""}`
|
|
330
323
|
: "";
|
|
331
324
|
const atlas = useMemo(
|
|
332
325
|
() =>
|
|
@@ -431,6 +424,7 @@ export function MarkerOverlay({
|
|
|
431
424
|
repColor,
|
|
432
425
|
pt.groupCount,
|
|
433
426
|
GROUP_CORNER_SCALE,
|
|
427
|
+
groupBadgeCfg?.letterSpacing ?? 0,
|
|
434
428
|
);
|
|
435
429
|
}
|
|
436
430
|
continue;
|
|
@@ -448,6 +442,7 @@ export function MarkerOverlay({
|
|
|
448
442
|
repColor,
|
|
449
443
|
pt.groupCount,
|
|
450
444
|
1,
|
|
445
|
+
groupBadgeCfg?.letterSpacing ?? 0,
|
|
451
446
|
);
|
|
452
447
|
continue;
|
|
453
448
|
}
|
|
@@ -24,6 +24,22 @@ import { AnimatedLabel } from "./AnimatedLabel";
|
|
|
24
24
|
/** Right margin (px) for floating labels — keeps them just off the canvas edge. */
|
|
25
25
|
const FLOAT_LABEL_RIGHT_MARGIN = 6;
|
|
26
26
|
|
|
27
|
+
/** Returns true when a Y-axis label's line box intersects the live badge pill. */
|
|
28
|
+
export function yAxisLabelIntersectsBadge(
|
|
29
|
+
labelCenterY: number,
|
|
30
|
+
labelHeight: number,
|
|
31
|
+
badgeCenterY: number,
|
|
32
|
+
badgeHeight: number,
|
|
33
|
+
) {
|
|
34
|
+
"worklet";
|
|
35
|
+
const labelHalfHeight = labelHeight / 2;
|
|
36
|
+
const badgeHalfHeight = badgeHeight / 2;
|
|
37
|
+
return (
|
|
38
|
+
labelCenterY + labelHalfHeight > badgeCenterY - badgeHalfHeight &&
|
|
39
|
+
labelCenterY - labelHalfHeight < badgeCenterY + badgeHalfHeight
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export function YAxisOverlay({
|
|
28
44
|
entries,
|
|
29
45
|
engine,
|
|
@@ -33,6 +49,9 @@ export function YAxisOverlay({
|
|
|
33
49
|
badge = false,
|
|
34
50
|
badgeTail = true,
|
|
35
51
|
badgeMetrics = BADGE_METRICS_DEFAULTS,
|
|
52
|
+
badgeCenterY,
|
|
53
|
+
badgeFontSize,
|
|
54
|
+
badgeOffsetY = 0,
|
|
36
55
|
seriesLabelInset = 0,
|
|
37
56
|
gridStyle,
|
|
38
57
|
variant = "all",
|
|
@@ -49,6 +68,12 @@ export function YAxisOverlay({
|
|
|
49
68
|
badgeTail?: boolean;
|
|
50
69
|
/** Badge pill geometry tokens (kept in sync with useBadge). */
|
|
51
70
|
badgeMetrics?: BadgeMetrics;
|
|
71
|
+
/** Live badge center before its configured Y offset. Enables label collision suppression. */
|
|
72
|
+
badgeCenterY?: SharedValue<number>;
|
|
73
|
+
/** Live badge font size, used to reconstruct the pill's vertical bounds. */
|
|
74
|
+
badgeFontSize?: number;
|
|
75
|
+
/** Configured live badge Y offset. */
|
|
76
|
+
badgeOffsetY?: number;
|
|
52
77
|
/** When > 0, series labels occupy the left portion of the gutter; Y-axis labels right-align. */
|
|
53
78
|
seriesLabelInset?: number;
|
|
54
79
|
/** Grid-line styling overrides. Omit for the legacy solid 1px line. */
|
|
@@ -92,6 +117,14 @@ export function YAxisOverlay({
|
|
|
92
117
|
const w = engine.canvasWidth.get();
|
|
93
118
|
const fm = font.getMetrics();
|
|
94
119
|
const baselineOffset = (fm.ascent + fm.descent) / 2;
|
|
120
|
+
const labelHeight = fm.descent - fm.ascent;
|
|
121
|
+
const resolvedBadgeCenterY = badgeCenterY
|
|
122
|
+
? badgeCenterY.get() + badgeOffsetY
|
|
123
|
+
: null;
|
|
124
|
+
const badgeHeight =
|
|
125
|
+
badgeFontSize === undefined
|
|
126
|
+
? 0
|
|
127
|
+
: badgeFontSize + badgeMetrics.padY * 2;
|
|
95
128
|
const result: { x: number; y: number; label: string; alpha: number }[] = [];
|
|
96
129
|
for (let i = 0; i < items.length; i++) {
|
|
97
130
|
const e = items[i];
|
|
@@ -107,7 +140,16 @@ export function YAxisOverlay({
|
|
|
107
140
|
x,
|
|
108
141
|
y: e.y - baselineOffset,
|
|
109
142
|
label: e.label,
|
|
110
|
-
alpha:
|
|
143
|
+
alpha:
|
|
144
|
+
resolvedBadgeCenterY !== null &&
|
|
145
|
+
yAxisLabelIntersectsBadge(
|
|
146
|
+
e.y,
|
|
147
|
+
labelHeight,
|
|
148
|
+
resolvedBadgeCenterY,
|
|
149
|
+
badgeHeight,
|
|
150
|
+
)
|
|
151
|
+
? 0
|
|
152
|
+
: e.alpha,
|
|
111
153
|
});
|
|
112
154
|
}
|
|
113
155
|
return result;
|
package/src/draw/markerAtlas.ts
CHANGED
|
@@ -59,6 +59,21 @@ export function groupCountText(count: number): string {
|
|
|
59
59
|
return count > 99 ? "99" : `${count}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Width of proportionally laid-out count-badge text at its on-canvas scale. */
|
|
63
|
+
export function groupCountTextWidth(
|
|
64
|
+
text: string,
|
|
65
|
+
digitWidths: Record<string, number>,
|
|
66
|
+
letterSpacing = 0,
|
|
67
|
+
): number {
|
|
68
|
+
"worklet";
|
|
69
|
+
let width = 0;
|
|
70
|
+
for (let i = 0; i < text.length; i++) {
|
|
71
|
+
width += (digitWidths[text[i]] ?? 0) * BADGE_TEXT_SCALE;
|
|
72
|
+
if (i > 0) width += letterSpacing;
|
|
73
|
+
}
|
|
74
|
+
return width;
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
/** Default glyph color per kind when `marker.color` is unset. Worklet-safe so the
|
|
63
78
|
* per-frame overlay worklet can resolve a collapsed cluster's badge color. */
|
|
64
79
|
export function defaultMarkerColor(
|
|
@@ -333,10 +348,12 @@ function groupBgCellSpec(
|
|
|
333
348
|
bgColor: string,
|
|
334
349
|
uw: number,
|
|
335
350
|
gh: number,
|
|
351
|
+
letterSpacing: number,
|
|
336
352
|
): CellSpec {
|
|
337
353
|
const m2 = CELL_MARGIN * 2;
|
|
338
354
|
// Inner circle fits a centered two-digit string at BADGE_TEXT_SCALE plus padding.
|
|
339
|
-
const
|
|
355
|
+
const textWidth = 2 * uw * BADGE_TEXT_SCALE + Math.max(0, letterSpacing);
|
|
356
|
+
const innerR = Math.ceil(Math.max(textWidth, gh * BADGE_TEXT_SCALE)) / 2 + BADGE_PAD;
|
|
340
357
|
const outerR = innerR + BADGE_BORDER;
|
|
341
358
|
const diameter = 2 * outerR;
|
|
342
359
|
return {
|
|
@@ -429,7 +446,10 @@ export function buildMarkerAtlas(
|
|
|
429
446
|
colors.add(m.color ?? defaultMarkerColor(m.kind, palette));
|
|
430
447
|
}
|
|
431
448
|
for (const color of colors) {
|
|
432
|
-
specs.push({
|
|
449
|
+
specs.push({
|
|
450
|
+
sig: groupBgSig(color),
|
|
451
|
+
spec: groupBgCellSpec(color, bgColor, uw, gh, groupBadge?.letterSpacing ?? 0),
|
|
452
|
+
});
|
|
433
453
|
}
|
|
434
454
|
}
|
|
435
455
|
|
|
@@ -14,6 +14,17 @@ const TOOLTIP_EDGE_GAP = 4;
|
|
|
14
14
|
const TOOLTIP_TOP_MARGIN = 8;
|
|
15
15
|
const FADE_ZONE = 4;
|
|
16
16
|
|
|
17
|
+
/** Measure rendered text so the pill and its content share the same centre. */
|
|
18
|
+
function measureTooltipTextWidth(
|
|
19
|
+
font: SkFont,
|
|
20
|
+
text: string,
|
|
21
|
+
monoCharWidth: number,
|
|
22
|
+
): number {
|
|
23
|
+
"worklet";
|
|
24
|
+
const measured = measureFontTextWidth(font, text);
|
|
25
|
+
return measured > 0 ? measured : text.length * monoCharWidth;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export interface TooltipLayout {
|
|
18
29
|
x: number;
|
|
19
30
|
y: number;
|
|
@@ -197,10 +208,7 @@ export function computeTooltipLayout(
|
|
|
197
208
|
formatValue: (v: number) => string,
|
|
198
209
|
formatTime: (t: number) => string,
|
|
199
210
|
font: SkFont,
|
|
200
|
-
/** Monospace advance width
|
|
201
|
-
* instead of a per-frame Skia `measureText` — scrubbing re-runs this worklet
|
|
202
|
-
* every frame, and `measureText` shapes text each call (a real cost,
|
|
203
|
-
* especially in the simulator). Falls back to `measureText` when 0. */
|
|
211
|
+
/** Monospace advance width fallback when font measurement returns zero. */
|
|
204
212
|
monoCharWidth = 0,
|
|
205
213
|
/** Where the pill sits relative to the scrub line. `"side"` offsets it right
|
|
206
214
|
* (flipping left near the edge); `"top"`/`"bottom"` center it over the line,
|
|
@@ -243,14 +251,11 @@ export function computeTooltipLayout(
|
|
|
243
251
|
const totalH =
|
|
244
252
|
TOOLTIP_PAD_Y * 2 + lineH * rowCount + TOOLTIP_LINE_GAP * (rowCount - 1);
|
|
245
253
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const timeW =
|
|
251
|
-
monoCharWidth > 0
|
|
252
|
-
? timeStr.length * monoCharWidth
|
|
253
|
-
: measureFontTextWidth(font, timeStr);
|
|
254
|
+
// Use actual glyph bounds for every visible-row variant. The old
|
|
255
|
+
// character-count approximation made punctuation-heavy dates/times appear
|
|
256
|
+
// off-centre even when the pill itself was positioned correctly.
|
|
257
|
+
const valueW = measureTooltipTextWidth(font, valueStr, monoCharWidth);
|
|
258
|
+
const timeW = measureTooltipTextWidth(font, timeStr, monoCharWidth);
|
|
254
259
|
// Only the visible rows contribute to the pill width.
|
|
255
260
|
const contentW = Math.max(sv ? valueW : 0, st ? timeW : 0);
|
|
256
261
|
const pillW = contentW + TOOLTIP_PAD_X * 2;
|
|
@@ -333,8 +338,7 @@ export function computeTooltipLayoutMulti(
|
|
|
333
338
|
padding: ChartPadding,
|
|
334
339
|
canvasWidth: number,
|
|
335
340
|
font: SkFont,
|
|
336
|
-
/** Monospace advance width
|
|
337
|
-
* per-frame Skia `measureText`. See {@link computeTooltipLayout}. */
|
|
341
|
+
/** Monospace advance width fallback when font measurement returns zero. */
|
|
338
342
|
monoCharWidth = 0,
|
|
339
343
|
): TooltipLayout {
|
|
340
344
|
"worklet";
|
|
@@ -349,10 +353,7 @@ export function computeTooltipLayoutMulti(
|
|
|
349
353
|
let contentW = 0;
|
|
350
354
|
const lineWidths: number[] = [];
|
|
351
355
|
for (let i = 0; i < n; i++) {
|
|
352
|
-
const w =
|
|
353
|
-
monoCharWidth > 0
|
|
354
|
-
? lines[i].text.length * monoCharWidth
|
|
355
|
-
: measureFontTextWidth(font, lines[i].text);
|
|
356
|
+
const w = measureTooltipTextWidth(font, lines[i].text, monoCharWidth);
|
|
356
357
|
lineWidths.push(w);
|
|
357
358
|
if (w > contentW) contentW = w;
|
|
358
359
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
2
|
+
|
|
3
|
+
type BooleanSharedValue = Pick<SharedValue<boolean>, "get" | "set">;
|
|
4
|
+
|
|
5
|
+
type TouchCountEvent = {
|
|
6
|
+
numberOfTouches: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type GestureStateManager = {
|
|
10
|
+
fail: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Mark the first pointer of a delayed pan as down. Zero-delay pans are inert. */
|
|
14
|
+
export function delayedPanTouchDown(
|
|
15
|
+
delayMs: number,
|
|
16
|
+
fingerDown: BooleanSharedValue,
|
|
17
|
+
): void {
|
|
18
|
+
"worklet";
|
|
19
|
+
if (delayMs > 0) fingerDown.set(true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fail a delayed pan that is still pending when its final pointer lifts.
|
|
24
|
+
*
|
|
25
|
+
* RNGH 2.x on iOS can leave `activateAfterLongPress`'s timer armed after a
|
|
26
|
+
* stationary touch ends. Failing the pending recognizer forces the reset that
|
|
27
|
+
* cancels that timer. An already-active pan is left to finish normally.
|
|
28
|
+
*/
|
|
29
|
+
export function delayedPanTouchUp(
|
|
30
|
+
delayMs: number,
|
|
31
|
+
event: TouchCountEvent,
|
|
32
|
+
manager: GestureStateManager,
|
|
33
|
+
fingerDown: BooleanSharedValue,
|
|
34
|
+
panActivated: BooleanSharedValue,
|
|
35
|
+
): void {
|
|
36
|
+
"worklet";
|
|
37
|
+
if (delayMs <= 0 || event.numberOfTouches > 0) return;
|
|
38
|
+
fingerDown.set(false);
|
|
39
|
+
if (!panActivated.get()) manager.fail();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Clear pointer state when the native touch stream is cancelled. */
|
|
43
|
+
export function delayedPanTouchCancelled(
|
|
44
|
+
delayMs: number,
|
|
45
|
+
fingerDown: BooleanSharedValue,
|
|
46
|
+
): void {
|
|
47
|
+
"worklet";
|
|
48
|
+
if (delayMs > 0) fingerDown.set(false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Record a real delayed-pan activation, or reject an activation whose timer
|
|
53
|
+
* raced the final pointer-up. The rejected path deliberately keeps
|
|
54
|
+
* `panActivated` false so a missing native FINALIZE cannot poison the next
|
|
55
|
+
* interaction.
|
|
56
|
+
*/
|
|
57
|
+
export function shouldStartDelayedPan(
|
|
58
|
+
delayMs: number,
|
|
59
|
+
fingerDown: BooleanSharedValue,
|
|
60
|
+
panActivated: BooleanSharedValue,
|
|
61
|
+
): boolean {
|
|
62
|
+
"worklet";
|
|
63
|
+
if (delayMs <= 0) return true;
|
|
64
|
+
if (!fingerDown.get()) {
|
|
65
|
+
panActivated.set(false);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
panActivated.set(true);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Reset the delayed-pan lifecycle after native finalization. */
|
|
73
|
+
export function resetDelayedPanGuard(
|
|
74
|
+
fingerDown: BooleanSharedValue,
|
|
75
|
+
panActivated: BooleanSharedValue,
|
|
76
|
+
): void {
|
|
77
|
+
"worklet";
|
|
78
|
+
fingerDown.set(false);
|
|
79
|
+
panActivated.set(false);
|
|
80
|
+
}
|