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.
Files changed (44) hide show
  1. package/README.md +4 -1
  2. package/dist/components/LiveChart.d.ts.map +1 -1
  3. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  4. package/dist/components/ReferenceLineOverlay.d.ts +10 -3
  5. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  6. package/dist/components/ScrubActionOverlay.d.ts +32 -0
  7. package/dist/components/ScrubActionOverlay.d.ts.map +1 -0
  8. package/dist/components/XAxisOverlay.d.ts.map +1 -1
  9. package/dist/constants.d.ts +4 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/core/resolveConfig.d.ts +23 -1
  12. package/dist/core/resolveConfig.d.ts.map +1 -1
  13. package/dist/hooks/crosshairShared.d.ts +110 -0
  14. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  15. package/dist/hooks/useCrosshair.d.ts +14 -2
  16. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  17. package/dist/hooks/useMarkers.d.ts +2 -0
  18. package/dist/hooks/useMarkers.d.ts.map +1 -1
  19. package/dist/hooks/useReferenceLine.d.ts +36 -3
  20. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  21. package/dist/hooks/useReferenceLinePress.d.ts +23 -0
  22. package/dist/hooks/useReferenceLinePress.d.ts.map +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/math/referenceLines.d.ts +23 -0
  26. package/dist/math/referenceLines.d.ts.map +1 -1
  27. package/dist/types.d.ts +131 -6
  28. package/dist/types.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/components/LiveChart.tsx +165 -21
  31. package/src/components/LiveChartSeries.tsx +39 -2
  32. package/src/components/ReferenceLineOverlay.tsx +121 -94
  33. package/src/components/ScrubActionOverlay.tsx +187 -0
  34. package/src/components/XAxisOverlay.tsx +2 -1
  35. package/src/constants.ts +5 -0
  36. package/src/core/resolveConfig.ts +40 -0
  37. package/src/hooks/crosshairShared.ts +293 -0
  38. package/src/hooks/useCrosshair.ts +340 -6
  39. package/src/hooks/useMarkers.ts +18 -2
  40. package/src/hooks/useReferenceLine.ts +259 -14
  41. package/src/hooks/useReferenceLinePress.ts +92 -0
  42. package/src/index.ts +3 -0
  43. package/src/math/referenceLines.ts +55 -0
  44. 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,