react-native-livechart 3.3.0 → 3.4.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/README.md +4 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/ReferenceLineOverlay.d.ts +10 -3
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/ScrubActionOverlay.d.ts +32 -0
- package/dist/components/ScrubActionOverlay.d.ts.map +1 -0
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +23 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +110 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +14 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +2 -0
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useReferenceLine.d.ts +36 -3
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +23 -0
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/referenceLines.d.ts +23 -0
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +131 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +165 -21
- package/src/components/LiveChartSeries.tsx +39 -2
- package/src/components/ReferenceLineOverlay.tsx +121 -94
- package/src/components/ScrubActionOverlay.tsx +187 -0
- package/src/components/XAxisOverlay.tsx +2 -1
- package/src/constants.ts +5 -0
- package/src/core/resolveConfig.ts +40 -0
- package/src/hooks/crosshairShared.ts +293 -0
- package/src/hooks/useCrosshair.ts +340 -6
- package/src/hooks/useMarkers.ts +18 -2
- package/src/hooks/useReferenceLine.ts +259 -14
- package/src/hooks/useReferenceLinePress.ts +92 -0
- package/src/index.ts +3 -0
- package/src/math/referenceLines.ts +55 -0
- package/src/types.ts +134 -6
|
@@ -58,6 +58,23 @@ export interface CrosshairState {
|
|
|
58
58
|
* (inactive / no value / degenerate range). See {@link computeScrubDotY}. */
|
|
59
59
|
scrubDotY: SharedValue<number>;
|
|
60
60
|
gesture: ReturnType<typeof Gesture.Pan>;
|
|
61
|
+
|
|
62
|
+
// ── Scrub-action ("order ticket") lock state — single-series `useCrosshair`
|
|
63
|
+
// only; undefined on the multi-series crosshair. ───────────────────────────
|
|
64
|
+
/** Whether a reticle is currently placed/locked. */
|
|
65
|
+
lockActive?: SharedValue<boolean>;
|
|
66
|
+
/** Locked reticle X in canvas px (-1 when none). */
|
|
67
|
+
lockX?: SharedValue<number>;
|
|
68
|
+
/** Locked reticle Y in canvas px (-1 when none). */
|
|
69
|
+
lockY?: SharedValue<number>;
|
|
70
|
+
/** Chosen price = value at the reticle Y (optionally snapped); null when none. */
|
|
71
|
+
lockPrice?: SharedValue<number | null>;
|
|
72
|
+
/** Right-gutter action-badge layout (also the tap hit rect). */
|
|
73
|
+
actionBadge?: SharedValue<ActionBadgeLayout>;
|
|
74
|
+
/** X-axis time-badge layout at the reticle (opt-in; hidden when off). */
|
|
75
|
+
timeBadge?: SharedValue<TimeBadgeLayout>;
|
|
76
|
+
/** Tap gesture that places/moves the reticle, presses the badge, or dismisses. */
|
|
77
|
+
tapGesture?: ReturnType<typeof Gesture.Tap>;
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
/**
|
|
@@ -83,6 +100,39 @@ export function computeScrubDotY(
|
|
|
83
100
|
return padTop + ((displayMax - value) / valRange) * chartH;
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Inverse of {@link computeScrubDotY}: maps a canvas Y pixel back to a value (a
|
|
105
|
+
* free price *level*, used by scrub-action mode where the reticle Y is the chosen
|
|
106
|
+
* price, not the line value at the reticle X). Returns null when the canvas isn't
|
|
107
|
+
* laid out. A degenerate (zero) range collapses to the single value; a Y dragged
|
|
108
|
+
* past the plot edges clamps to `displayMin` / `displayMax` rather than
|
|
109
|
+
* extrapolating to a nonsensical price.
|
|
110
|
+
*/
|
|
111
|
+
export function computeValueAtY(
|
|
112
|
+
y: number,
|
|
113
|
+
displayMin: number,
|
|
114
|
+
displayMax: number,
|
|
115
|
+
canvasHeight: number,
|
|
116
|
+
padTop: number,
|
|
117
|
+
padBottom: number,
|
|
118
|
+
): number | null {
|
|
119
|
+
"worklet";
|
|
120
|
+
const chartH = canvasHeight - padTop - padBottom;
|
|
121
|
+
if (chartH <= 0) return null;
|
|
122
|
+
const valRange = displayMax - displayMin;
|
|
123
|
+
if (valRange === 0) return displayMin;
|
|
124
|
+
const clampedY = Math.min(padTop + chartH, Math.max(padTop, y));
|
|
125
|
+
const frac = (clampedY - padTop) / chartH; // 0 at top, 1 at bottom
|
|
126
|
+
return displayMax - frac * valRange; // top → displayMax, bottom → displayMin
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Rounds `price` to the nearest `increment` (no-op when increment is falsy/≤0). */
|
|
130
|
+
export function snapPrice(price: number, increment?: number): number {
|
|
131
|
+
"worklet";
|
|
132
|
+
if (!increment || increment <= 0) return price;
|
|
133
|
+
return Math.round(price / increment) * increment;
|
|
134
|
+
}
|
|
135
|
+
|
|
86
136
|
/**
|
|
87
137
|
* Maps a scrub X position to a window timestamp.
|
|
88
138
|
* Returns -1 when inactive or when the canvas is not yet laid out.
|
|
@@ -313,6 +363,249 @@ export function deriveScrubValueSingle(
|
|
|
313
363
|
return interpolateAtTime(data, scrubTime);
|
|
314
364
|
}
|
|
315
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Right-gutter action-badge layout for scrub-action mode — two pills, vertically
|
|
368
|
+
* centered on the locked reticle Y and anchored to the canvas right edge:
|
|
369
|
+
*
|
|
370
|
+
* - a **circular** icon button (the action), and
|
|
371
|
+
* - a capsule **price pill** showing the formatted value,
|
|
372
|
+
*
|
|
373
|
+
* separated by {@link ACTION_BADGE_GAP}px (`[icon] [price]`). The price pill is
|
|
374
|
+
* right-anchored in the gutter with its text horizontally centered; a lone icon
|
|
375
|
+
* (icon-only) anchors to the plot's right edge, attached to the level line.
|
|
376
|
+
* Either element is omitted when its content is empty (icon-only, or no price).
|
|
377
|
+
* The union rect (`x/y/w/h`) is the single source of truth for the tap hit-test
|
|
378
|
+
* (see {@link pointInRect}). When not locked / not laid out / both empty, returns
|
|
379
|
+
* {@link HIDDEN_ACTION_BADGE}.
|
|
380
|
+
*
|
|
381
|
+
* The price text is sized to its actual visual bounds via `font.measureText`
|
|
382
|
+
* (run only while a reticle is shown) so the readout stays exactly centered —
|
|
383
|
+
* see the inline note on `priceTextX` below.
|
|
384
|
+
*/
|
|
385
|
+
export interface ActionBadgeLayout {
|
|
386
|
+
/** Union hit rect (icon button + price pill). */
|
|
387
|
+
x: number;
|
|
388
|
+
y: number;
|
|
389
|
+
w: number;
|
|
390
|
+
h: number;
|
|
391
|
+
/** Whether the circular icon button is shown. */
|
|
392
|
+
hasIcon: boolean;
|
|
393
|
+
/** Icon button circle center + radius (the glyph is centered on this by the overlay). */
|
|
394
|
+
iconCx: number;
|
|
395
|
+
iconCy: number;
|
|
396
|
+
iconR: number;
|
|
397
|
+
/** Whether the price pill is shown. */
|
|
398
|
+
hasPrice: boolean;
|
|
399
|
+
/** Price pill rect. */
|
|
400
|
+
priceX: number;
|
|
401
|
+
priceY: number;
|
|
402
|
+
priceW: number;
|
|
403
|
+
priceH: number;
|
|
404
|
+
/** Price text left x. */
|
|
405
|
+
priceTextX: number;
|
|
406
|
+
/** Shared baseline y for the icon glyph + price text. */
|
|
407
|
+
textY: number;
|
|
408
|
+
/** Formatted price string drawn in the price pill. */
|
|
409
|
+
priceText: string;
|
|
410
|
+
/** Whether the badge is shown (locked + laid out + has content). */
|
|
411
|
+
visible: boolean;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export const HIDDEN_ACTION_BADGE: ActionBadgeLayout = {
|
|
415
|
+
x: -400,
|
|
416
|
+
y: 0,
|
|
417
|
+
w: 0,
|
|
418
|
+
h: 0,
|
|
419
|
+
hasIcon: false,
|
|
420
|
+
iconCx: -400,
|
|
421
|
+
iconCy: 0,
|
|
422
|
+
iconR: 0,
|
|
423
|
+
hasPrice: false,
|
|
424
|
+
priceX: -400,
|
|
425
|
+
priceY: 0,
|
|
426
|
+
priceW: 0,
|
|
427
|
+
priceH: 0,
|
|
428
|
+
priceTextX: -400,
|
|
429
|
+
textY: 0,
|
|
430
|
+
priceText: "",
|
|
431
|
+
visible: false,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/** Gap between the icon button and the price pill, in px. */
|
|
435
|
+
const ACTION_BADGE_GAP = 2;
|
|
436
|
+
|
|
437
|
+
export function computeActionBadgeLayout(
|
|
438
|
+
locked: boolean,
|
|
439
|
+
lockY: number,
|
|
440
|
+
priceText: string,
|
|
441
|
+
icon: string,
|
|
442
|
+
canvasWidth: number,
|
|
443
|
+
/** Plot's right edge (= canvasWidth - padding.right) — where the level line ends. */
|
|
444
|
+
plotRight: number,
|
|
445
|
+
font: SkFont,
|
|
446
|
+
marginEdge: number,
|
|
447
|
+
padX: number,
|
|
448
|
+
padY: number,
|
|
449
|
+
): ActionBadgeLayout {
|
|
450
|
+
"worklet";
|
|
451
|
+
if (!locked || canvasWidth <= 0) return HIDDEN_ACTION_BADGE;
|
|
452
|
+
|
|
453
|
+
const hasIcon = icon.length > 0;
|
|
454
|
+
const hasPrice = priceText.length > 0;
|
|
455
|
+
if (!hasIcon && !hasPrice) return HIDDEN_ACTION_BADGE;
|
|
456
|
+
|
|
457
|
+
const pillH = font.getSize() + padY * 2;
|
|
458
|
+
const r = pillH / 2;
|
|
459
|
+
const top = lockY - pillH / 2;
|
|
460
|
+
const rightEdge = canvasWidth - marginEdge;
|
|
461
|
+
|
|
462
|
+
const fm = font.getMetrics();
|
|
463
|
+
const textY = lockY - (fm.ascent + fm.descent) / 2;
|
|
464
|
+
|
|
465
|
+
// Price pill (the readout) sits in the price-axis gutter, anchored to the edge.
|
|
466
|
+
let priceW = 0;
|
|
467
|
+
let priceX = rightEdge;
|
|
468
|
+
let priceTextX = rightEdge;
|
|
469
|
+
if (hasPrice) {
|
|
470
|
+
// Size the pill to the text's ACTUAL visual bounds (+ pad each side), then
|
|
471
|
+
// position the text origin so the *ink* — not the advance box — is centered:
|
|
472
|
+
// subtract the glyph's left side-bearing (`bounds.x`). `SkiaText` draws from
|
|
473
|
+
// the pen origin, so an origin at `padX` would leave the ink shifted by the
|
|
474
|
+
// bearing; this lands the visual center exactly on the pill center.
|
|
475
|
+
//
|
|
476
|
+
// This measures the actual string each frame rather than estimating from a
|
|
477
|
+
// per-char monospace width. A uniform width over-reserves for prices with
|
|
478
|
+
// narrow glyphs ("1", "."), which visibly de-centers the readout — and
|
|
479
|
+
// exact centering matters more here than shaving the per-frame `measureText`,
|
|
480
|
+
// which profiling put at ~4% of one core on the simulator (transient, only
|
|
481
|
+
// while a reticle is shown). See the price-pill centering tests.
|
|
482
|
+
const bounds = font.measureText(priceText);
|
|
483
|
+
priceW = bounds.width + padX * 2;
|
|
484
|
+
priceX = rightEdge - priceW;
|
|
485
|
+
priceTextX = priceX + padX - bounds.x;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Circular icon button: left of the price pill, or — when alone (icon-only) —
|
|
489
|
+
// anchored to the plot's right edge so it stays attached to the level line.
|
|
490
|
+
let iconCx = plotRight;
|
|
491
|
+
if (hasIcon && hasPrice) {
|
|
492
|
+
iconCx = priceX - ACTION_BADGE_GAP - r;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const unionLeft = hasIcon ? iconCx - r : priceX;
|
|
496
|
+
const unionRight = hasPrice ? rightEdge : iconCx + r;
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
x: unionLeft,
|
|
500
|
+
y: top,
|
|
501
|
+
w: unionRight - unionLeft,
|
|
502
|
+
h: pillH,
|
|
503
|
+
hasIcon,
|
|
504
|
+
iconCx,
|
|
505
|
+
iconCy: lockY,
|
|
506
|
+
iconR: r,
|
|
507
|
+
hasPrice,
|
|
508
|
+
priceX,
|
|
509
|
+
priceY: top,
|
|
510
|
+
priceW,
|
|
511
|
+
priceH: pillH,
|
|
512
|
+
priceTextX,
|
|
513
|
+
textY,
|
|
514
|
+
priceText,
|
|
515
|
+
visible: true,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* X-axis time-badge layout for scrub-action mode — a small capsule pinned where
|
|
521
|
+
* the reticle's vertical line meets the time axis (the bottom gutter), centered
|
|
522
|
+
* on the reticle X and clamped to stay within the canvas. Display-only (no
|
|
523
|
+
* hit-test): an opt-in readout of the date/time under the reticle, formatted by
|
|
524
|
+
* the chart's `formatTime`. Hidden when not locked / not laid out / empty text.
|
|
525
|
+
*/
|
|
526
|
+
export interface TimeBadgeLayout {
|
|
527
|
+
/** Pill rect. */
|
|
528
|
+
x: number;
|
|
529
|
+
y: number;
|
|
530
|
+
w: number;
|
|
531
|
+
h: number;
|
|
532
|
+
/** Time text left x (visual-bounds centered) + shared baseline y. */
|
|
533
|
+
textX: number;
|
|
534
|
+
textY: number;
|
|
535
|
+
/** Formatted time string drawn in the pill. */
|
|
536
|
+
timeText: string;
|
|
537
|
+
/** Whether the badge is shown (locked + laid out + has text). */
|
|
538
|
+
visible: boolean;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export const HIDDEN_TIME_BADGE: TimeBadgeLayout = {
|
|
542
|
+
x: -400,
|
|
543
|
+
y: 0,
|
|
544
|
+
w: 0,
|
|
545
|
+
h: 0,
|
|
546
|
+
textX: -400,
|
|
547
|
+
textY: 0,
|
|
548
|
+
timeText: "",
|
|
549
|
+
visible: false,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
export function computeTimeBadgeLayout(
|
|
553
|
+
locked: boolean,
|
|
554
|
+
lockX: number,
|
|
555
|
+
timeText: string,
|
|
556
|
+
canvasWidth: number,
|
|
557
|
+
/** Baseline y where the x-axis time labels sit (= canvasHeight - padding.bottom
|
|
558
|
+
* + X_AXIS_LABEL_OFFSET_Y). The pill is vertically centered on this row so it
|
|
559
|
+
* aligns with the axis values it overlays. */
|
|
560
|
+
labelBaselineY: number,
|
|
561
|
+
font: SkFont,
|
|
562
|
+
padX: number,
|
|
563
|
+
padY: number,
|
|
564
|
+
marginEdge: number,
|
|
565
|
+
): TimeBadgeLayout {
|
|
566
|
+
"worklet";
|
|
567
|
+
if (!locked || canvasWidth <= 0 || timeText.length === 0)
|
|
568
|
+
return HIDDEN_TIME_BADGE;
|
|
569
|
+
|
|
570
|
+
const pillH = font.getSize() + padY * 2;
|
|
571
|
+
// Snug to the measured text + centered ink (subtract the left side-bearing), the
|
|
572
|
+
// same approach the price pill uses.
|
|
573
|
+
const bounds = font.measureText(timeText);
|
|
574
|
+
const w = bounds.width + padX * 2;
|
|
575
|
+
|
|
576
|
+
// Center the pill under the reticle X, clamped into the canvas gutter so it
|
|
577
|
+
// never spills past either edge.
|
|
578
|
+
const minX = marginEdge;
|
|
579
|
+
const maxX = Math.max(minX, canvasWidth - marginEdge - w);
|
|
580
|
+
const x = Math.min(maxX, Math.max(minX, lockX - w / 2));
|
|
581
|
+
|
|
582
|
+
// Sit the text on the axis-label baseline (so it lines up with the time values)
|
|
583
|
+
// and center the pill vertically around that text.
|
|
584
|
+
const fm = font.getMetrics();
|
|
585
|
+
const textY = labelBaselineY;
|
|
586
|
+
const cy = labelBaselineY + (fm.ascent + fm.descent) / 2;
|
|
587
|
+
const top = cy - pillH / 2;
|
|
588
|
+
const textX = x + padX - bounds.x;
|
|
589
|
+
|
|
590
|
+
return { x, y: top, w, h: pillH, textX, textY, timeText, visible: true };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Axis-aligned point-in-rect test with an optional touch-target inflation. */
|
|
594
|
+
export function pointInRect(
|
|
595
|
+
px: number,
|
|
596
|
+
py: number,
|
|
597
|
+
rect: { x: number; y: number; w: number; h: number },
|
|
598
|
+
slop = 0,
|
|
599
|
+
): boolean {
|
|
600
|
+
"worklet";
|
|
601
|
+
return (
|
|
602
|
+
px >= rect.x - slop &&
|
|
603
|
+
px <= rect.x + rect.w + slop &&
|
|
604
|
+
py >= rect.y - slop &&
|
|
605
|
+
py <= rect.y + rect.h + slop
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
316
609
|
/** Single-series crosshair tooltip — extracted for tests. */
|
|
317
610
|
export function deriveCrosshairTooltipSingle(
|
|
318
611
|
scrubActive: boolean,
|