react-native-livechart 4.21.0 → 4.22.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 (78) hide show
  1. package/dist/components/AxisLabelOverlay.d.ts +5 -1
  2. package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
  3. package/dist/components/CrosshairOverlay.d.ts +4 -2
  4. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  5. package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
  6. package/dist/components/CustomThresholdBadgeOverlay.d.ts +3 -1
  7. package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -1
  8. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  9. package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
  10. package/dist/components/ExtremaConnectorOverlay.d.ts.map +1 -1
  11. package/dist/components/LiveChart.d.ts.map +1 -1
  12. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  13. package/dist/components/MarkerOverlay.d.ts +4 -2
  14. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  15. package/dist/components/ReferenceLineOverlay.d.ts +1 -1
  16. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  17. package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -1
  18. package/dist/components/ThresholdLineOverlay.d.ts +3 -1
  19. package/dist/components/ThresholdLineOverlay.d.ts.map +1 -1
  20. package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -1
  21. package/dist/core/chartDataPresence.d.ts +8 -0
  22. package/dist/core/chartDataPresence.d.ts.map +1 -0
  23. package/dist/core/liveChartEngineTick.d.ts +3 -0
  24. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  25. package/dist/core/resolveConfig.d.ts +2 -0
  26. package/dist/core/resolveConfig.d.ts.map +1 -1
  27. package/dist/core/useLiveChartEngine.d.ts +4 -0
  28. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  29. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  30. package/dist/draw/markerAtlas.d.ts +3 -2
  31. package/dist/draw/markerAtlas.d.ts.map +1 -1
  32. package/dist/hooks/useChartReveal.d.ts +1 -1
  33. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  34. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  35. package/dist/hooks/useDegen.d.ts.map +1 -1
  36. package/dist/hooks/useLiveChartHasData.d.ts +2 -2
  37. package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
  38. package/dist/hooks/useReferenceLineSeries.d.ts +1 -1
  39. package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -1
  40. package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
  41. package/dist/hooks/useThreshold.d.ts +10 -5
  42. package/dist/hooks/useThreshold.d.ts.map +1 -1
  43. package/dist/math/referenceLineSeries.d.ts +1 -1
  44. package/dist/math/referenceLineSeries.d.ts.map +1 -1
  45. package/dist/math/threshold.d.ts +4 -3
  46. package/dist/math/threshold.d.ts.map +1 -1
  47. package/dist/types.d.ts +23 -8
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/AxisLabelOverlay.tsx +162 -81
  51. package/src/components/CrosshairOverlay.tsx +394 -147
  52. package/src/components/CustomMarkerOverlay.tsx +9 -6
  53. package/src/components/CustomThresholdBadgeOverlay.tsx +11 -4
  54. package/src/components/DegenParticlesOverlay.tsx +34 -23
  55. package/src/components/ExtremaConnectorOverlay.tsx +43 -3
  56. package/src/components/LiveChart.tsx +1709 -1025
  57. package/src/components/LiveChartSeries.tsx +445 -296
  58. package/src/components/MarkerOverlay.tsx +215 -156
  59. package/src/components/ReferenceLineOverlay.tsx +77 -50
  60. package/src/components/ReferenceLineSeriesOverlay.tsx +2 -0
  61. package/src/components/ThresholdLineOverlay.tsx +16 -6
  62. package/src/components/thresholdSplitShaderSource.ts +4 -1
  63. package/src/core/chartDataPresence.ts +24 -0
  64. package/src/core/liveChartEngineTick.ts +4 -0
  65. package/src/core/resolveConfig.ts +3 -0
  66. package/src/core/useLiveChartEngine.ts +16 -3
  67. package/src/core/useLiveChartSeriesEngine.ts +2 -0
  68. package/src/draw/markerAtlas.ts +8 -3
  69. package/src/hooks/useChartReveal.ts +1 -1
  70. package/src/hooks/useCrosshair.ts +69 -46
  71. package/src/hooks/useDegen.ts +82 -53
  72. package/src/hooks/useLiveChartHasData.ts +8 -4
  73. package/src/hooks/useReferenceLineSeries.ts +5 -0
  74. package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
  75. package/src/hooks/useThreshold.ts +41 -15
  76. package/src/math/referenceLineSeries.ts +9 -4
  77. package/src/math/threshold.ts +10 -3
  78. package/src/types.ts +23 -8
@@ -17,41 +17,7 @@ import type { ResolvedSelectionDotConfig } from "../core/resolveConfig";
17
17
  import type { ChartEngineLayout } from "../core/useLiveChartEngine";
18
18
  import { SelectionDotSlot } from "./SelectionDot";
19
19
 
20
- export function CrosshairOverlay({
21
- scrubX,
22
- crosshairOpacity,
23
- tooltipLayout,
24
- engine,
25
- padding,
26
- palette,
27
- font,
28
- showTooltip = true,
29
- children,
30
- renderTooltip,
31
- lineTop,
32
- showLine = true,
33
- selectionDot,
34
- selectionY,
35
- scrubActive,
36
- selectionColor,
37
- dimOpacity = 0.3,
38
- liveDotExtent = 0,
39
- crosshairLineColor,
40
- crosshairStrokeWidth = 1,
41
- crosshairOvershoot = 0,
42
- crosshairFade = true,
43
- crosshairFadeDistance = 4,
44
- crosshairLineCap,
45
- crosshairDash,
46
- crosshairDimColor,
47
- tooltipBackground,
48
- tooltipColor,
49
- tooltipBorderColor,
50
- tooltipBorderRadius = 5,
51
- tooltipShowValue = true,
52
- tooltipShowTime = true,
53
- opaqueCanvas = false,
54
- }: {
20
+ type CrosshairOverlayProps = {
55
21
  scrubX: SharedValue<number>;
56
22
  crosshairOpacity: SharedValue<number>;
57
23
  tooltipLayout: SharedValue<TooltipLayout>;
@@ -116,32 +82,62 @@ export function CrosshairOverlay({
116
82
  tooltipShowTime?: boolean;
117
83
  /** Paint the owned background instead of erasing destination alpha. */
118
84
  opaqueCanvas?: boolean;
119
- }) {
85
+ };
86
+
87
+ export function CrosshairOverlay({
88
+ scrubX,
89
+ crosshairOpacity,
90
+ tooltipLayout,
91
+ engine,
92
+ padding,
93
+ palette,
94
+ font,
95
+ showTooltip = true,
96
+ children,
97
+ renderTooltip,
98
+ lineTop,
99
+ showLine = true,
100
+ selectionDot,
101
+ selectionY,
102
+ scrubActive,
103
+ selectionColor,
104
+ dimOpacity = 0.3,
105
+ liveDotExtent = 0,
106
+ crosshairLineColor,
107
+ crosshairStrokeWidth = 1,
108
+ crosshairOvershoot = 0,
109
+ crosshairFade = true,
110
+ crosshairFadeDistance = 4,
111
+ crosshairLineCap,
112
+ crosshairDash,
113
+ crosshairDimColor,
114
+ tooltipBackground,
115
+ tooltipColor,
116
+ tooltipBorderColor,
117
+ tooltipBorderRadius = 5,
118
+ tooltipShowValue = true,
119
+ tooltipShowTime = true,
120
+ opaqueCanvas = false,
121
+ }: CrosshairOverlayProps) {
120
122
  // Explicit dependency arrays: with React Compiler enabled, Reanimated's
121
123
  // auto-detected worklet dependencies can change array size between renders
122
124
  // (e.g. when `liveDotExtent` flips 0 → the live-dot extent), which trips
123
125
  // React's "final argument changed size between renders" error. Listing the
124
126
  // captured plain values keeps the dependency array a constant size. SharedValue
125
127
  // reads stay reactive regardless of this list.
126
- const p1 = useDerivedValue(
127
- () => {
128
- // A top-pinned custom tooltip pushes the line's start down to its measured
129
- // bottom (lineTop) so the line stops at the label; -1 → no top tooltip.
130
- const lt = lineTop?.value ?? -1;
131
- return {
132
- x: scrubX.value,
133
- y: lt >= 0 ? lt : padding.top - crosshairOvershoot,
134
- };
135
- },
136
- [scrubX, padding.top, lineTop, crosshairOvershoot],
137
- );
128
+ const p1 = useDerivedValue(() => {
129
+ // A top-pinned custom tooltip pushes the line's start down to its measured
130
+ // bottom (lineTop) so the line stops at the label; -1 → no top tooltip.
131
+ const lt = lineTop?.value ?? -1;
132
+ return {
133
+ x: scrubX.value,
134
+ y: lt >= 0 ? lt : padding.top - crosshairOvershoot,
135
+ };
136
+ }, [scrubX, padding.top, lineTop, crosshairOvershoot]);
138
137
  const p2 = useDerivedValue(
139
138
  () => ({
140
139
  x: scrubX.value,
141
- y:
142
- engine.canvasHeight.value -
143
- padding.bottom +
144
- crosshairOvershoot,
140
+ y: engine.canvasHeight.value - padding.bottom + crosshairOvershoot,
145
141
  }),
146
142
  [scrubX, engine.canvasHeight, padding.bottom, crosshairOvershoot],
147
143
  );
@@ -197,111 +193,362 @@ export function CrosshairOverlay({
197
193
 
198
194
  return (
199
195
  <>
200
- {crosshairDimColor !== undefined ? (
201
- // Legacy: solid colored mask painted over the chart (opt-in).
202
- <Group opacity={crosshairOpacity}>
203
- <Rect
204
- x={scrubX}
205
- y={padding.top}
206
- width={dimWidth}
207
- height={dimHeight}
208
- color={crosshairDimColor}
209
- />
210
- </Group>
211
- ) : dimOpacity < 1 && opaqueCanvas ? (
196
+ <CrosshairDimLayer
197
+ scrubX={scrubX}
198
+ paddingTop={padding.top}
199
+ dimWidth={dimWidth}
200
+ dimHeight={dimHeight}
201
+ crosshairOpacity={crosshairOpacity}
202
+ crosshairDimColor={crosshairDimColor}
203
+ dimOpacity={dimOpacity}
204
+ opaqueCanvas={opaqueCanvas}
205
+ backgroundColor={backgroundColor}
206
+ opaqueDimOpacity={opaqueDimOpacity}
207
+ dimErase={dimErase}
208
+ />
209
+ <CrosshairVisibleLayer
210
+ opacity={visibleOpacity}
211
+ p1={p1}
212
+ p2={p2}
213
+ palette={palette}
214
+ showLine={showLine}
215
+ crosshairLineColor={crosshairLineColor}
216
+ crosshairStrokeWidth={crosshairStrokeWidth}
217
+ crosshairLineCap={crosshairLineCap}
218
+ crosshairDash={crosshairDash}
219
+ selectionDot={selectionDot}
220
+ scrubX={scrubX}
221
+ selectionY={selectionY}
222
+ scrubActive={scrubActive}
223
+ selectionColor={selectionColor}
224
+ showTooltip={showTooltip}
225
+ tipX={tipX}
226
+ tipY={tipY}
227
+ tipW={tipW}
228
+ tipH={tipH}
229
+ tooltipBorderRadius={tooltipBorderRadius}
230
+ tooltipBackground={tooltipBackground}
231
+ tooltipBorderColor={tooltipBorderColor}
232
+ renderTooltip={renderTooltip}
233
+ tooltipShowValue={tooltipShowValue}
234
+ effectiveTooltipShowTime={effectiveTooltipShowTime}
235
+ valueTextX={valueTextX}
236
+ timeTextX={timeTextX}
237
+ line1Y={line1Y}
238
+ line2Y={line2Y}
239
+ valueStr={valueStr}
240
+ timeStr={timeStr}
241
+ font={font}
242
+ tooltipColor={tooltipColor}
243
+ >
244
+ {children}
245
+ </CrosshairVisibleLayer>
246
+ </>
247
+ );
248
+ }
249
+
250
+ function CrosshairDimLayer({
251
+ scrubX,
252
+ paddingTop,
253
+ dimWidth,
254
+ dimHeight,
255
+ crosshairOpacity,
256
+ crosshairDimColor,
257
+ dimOpacity,
258
+ opaqueCanvas,
259
+ backgroundColor,
260
+ opaqueDimOpacity,
261
+ dimErase,
262
+ }: {
263
+ scrubX: SharedValue<number>;
264
+ paddingTop: number;
265
+ dimWidth: SharedValue<number>;
266
+ dimHeight: SharedValue<number>;
267
+ crosshairOpacity: SharedValue<number>;
268
+ crosshairDimColor?: string;
269
+ dimOpacity: number;
270
+ opaqueCanvas: boolean;
271
+ backgroundColor: string;
272
+ opaqueDimOpacity: SharedValue<number>;
273
+ dimErase: SharedValue<string>;
274
+ }) {
275
+ if (crosshairDimColor !== undefined) {
276
+ return (
277
+ <Group opacity={crosshairOpacity}>
212
278
  <Rect
213
279
  x={scrubX}
214
- y={padding.top}
280
+ y={paddingTop}
215
281
  width={dimWidth}
216
282
  height={dimHeight}
217
- color={backgroundColor}
218
- opacity={opaqueDimOpacity}
283
+ color={crosshairDimColor}
219
284
  />
220
- ) : dimOpacity < 1 ? (
221
- // Erase the trailing content's alpha so it fades to the real background
222
- // (works on any background color, unlike a colored mask).
223
- <Group blendMode="dstOut">
224
- <Rect
225
- x={scrubX}
226
- y={padding.top}
227
- width={dimWidth}
228
- height={dimHeight}
229
- color={dimErase}
230
- />
231
- </Group>
232
- ) : null}
285
+ </Group>
286
+ );
287
+ }
288
+ if (dimOpacity >= 1) return null;
289
+ if (opaqueCanvas) {
290
+ return (
291
+ <Rect
292
+ x={scrubX}
293
+ y={paddingTop}
294
+ width={dimWidth}
295
+ height={dimHeight}
296
+ color={backgroundColor}
297
+ opacity={opaqueDimOpacity}
298
+ />
299
+ );
300
+ }
301
+ return (
302
+ <Group blendMode="dstOut">
303
+ <Rect
304
+ x={scrubX}
305
+ y={paddingTop}
306
+ width={dimWidth}
307
+ height={dimHeight}
308
+ color={dimErase}
309
+ />
310
+ </Group>
311
+ );
312
+ }
233
313
 
234
- <Group opacity={visibleOpacity}>
235
- {showLine ? (
236
- <Line
237
- p1={p1}
238
- p2={p2}
239
- color={crosshairLineColor ?? palette.crosshairLine}
240
- strokeWidth={crosshairStrokeWidth}
241
- strokeCap={crosshairLineCap}
242
- >
243
- {crosshairDash ? <DashPathEffect intervals={crosshairDash} /> : null}
244
- </Line>
245
- ) : null}
314
+ function CrosshairVisibleLayer({
315
+ opacity,
316
+ p1,
317
+ p2,
318
+ palette,
319
+ showLine,
320
+ crosshairLineColor,
321
+ crosshairStrokeWidth,
322
+ crosshairLineCap,
323
+ crosshairDash,
324
+ selectionDot,
325
+ scrubX,
326
+ selectionY,
327
+ scrubActive,
328
+ selectionColor,
329
+ showTooltip,
330
+ tipX,
331
+ tipY,
332
+ tipW,
333
+ tipH,
334
+ tooltipBorderRadius,
335
+ tooltipBackground,
336
+ tooltipBorderColor,
337
+ children,
338
+ renderTooltip,
339
+ tooltipShowValue,
340
+ effectiveTooltipShowTime,
341
+ valueTextX,
342
+ timeTextX,
343
+ line1Y,
344
+ line2Y,
345
+ valueStr,
346
+ timeStr,
347
+ font,
348
+ tooltipColor,
349
+ }: {
350
+ opacity: SharedValue<number>;
351
+ p1: SharedValue<{ x: number; y: number }>;
352
+ p2: SharedValue<{ x: number; y: number }>;
353
+ palette: LiveChartPalette;
354
+ showLine: boolean;
355
+ crosshairLineColor?: string;
356
+ crosshairStrokeWidth: number;
357
+ crosshairLineCap?: "butt" | "round" | "square";
358
+ crosshairDash?: number[];
359
+ selectionDot?: ResolvedSelectionDotConfig | null;
360
+ scrubX: SharedValue<number>;
361
+ selectionY?: SharedValue<number>;
362
+ scrubActive: SharedValue<number> | SharedValue<boolean>;
363
+ selectionColor?: string;
364
+ showTooltip: boolean;
365
+ tipX: SharedValue<number>;
366
+ tipY: SharedValue<number>;
367
+ tipW: SharedValue<number>;
368
+ tipH: SharedValue<number>;
369
+ tooltipBorderRadius: number;
370
+ tooltipBackground?: string;
371
+ tooltipBorderColor?: string;
372
+ children?: ReactNode;
373
+ renderTooltip?: () => ReactNode;
374
+ tooltipShowValue: boolean;
375
+ effectiveTooltipShowTime: boolean;
376
+ valueTextX: SharedValue<number>;
377
+ timeTextX: SharedValue<number>;
378
+ line1Y: SharedValue<number>;
379
+ line2Y: SharedValue<number>;
380
+ valueStr: SharedValue<string>;
381
+ timeStr: SharedValue<string>;
382
+ font: SkFont;
383
+ tooltipColor?: string;
384
+ }) {
385
+ return (
386
+ <Group opacity={opacity}>
387
+ <CrosshairLine
388
+ show={showLine}
389
+ p1={p1}
390
+ p2={p2}
391
+ color={crosshairLineColor ?? palette.crosshairLine}
392
+ strokeWidth={crosshairStrokeWidth}
393
+ strokeCap={crosshairLineCap}
394
+ dash={crosshairDash}
395
+ />
396
+ <SelectionDotSlot
397
+ config={selectionDot}
398
+ x={scrubX}
399
+ y={selectionY}
400
+ active={scrubActive}
401
+ opacity={opacity}
402
+ color={selectionColor ?? palette.line}
403
+ />
404
+ <CrosshairTooltip
405
+ show={showTooltip}
406
+ tipX={tipX}
407
+ tipY={tipY}
408
+ tipW={tipW}
409
+ tipH={tipH}
410
+ borderRadius={tooltipBorderRadius}
411
+ background={tooltipBackground ?? palette.tooltipBg}
412
+ borderColor={tooltipBorderColor ?? palette.tooltipBorder}
413
+ renderTooltip={renderTooltip}
414
+ showValue={tooltipShowValue}
415
+ showTime={effectiveTooltipShowTime}
416
+ valueTextX={valueTextX}
417
+ timeTextX={timeTextX}
418
+ line1Y={line1Y}
419
+ line2Y={line2Y}
420
+ valueStr={valueStr}
421
+ timeStr={timeStr}
422
+ font={font}
423
+ valueColor={tooltipColor ?? palette.tooltipText}
424
+ timeColor={palette.gridLabel}
425
+ >
426
+ {children}
427
+ </CrosshairTooltip>
428
+ </Group>
429
+ );
430
+ }
246
431
 
247
- {/* Selection dot at the scrub intersection. `null` hides it; a custom
248
- component renders the consumer's dot; otherwise the built-in dot. */}
249
- <SelectionDotSlot
250
- config={selectionDot}
251
- x={scrubX}
252
- y={selectionY}
253
- active={scrubActive}
254
- opacity={visibleOpacity}
255
- color={selectionColor ?? palette.line}
256
- />
432
+ function CrosshairLine({
433
+ show,
434
+ p1,
435
+ p2,
436
+ color,
437
+ strokeWidth,
438
+ strokeCap,
439
+ dash,
440
+ }: {
441
+ show: boolean;
442
+ p1: SharedValue<{ x: number; y: number }>;
443
+ p2: SharedValue<{ x: number; y: number }>;
444
+ color: string;
445
+ strokeWidth: number;
446
+ strokeCap?: "butt" | "round" | "square";
447
+ dash?: number[];
448
+ }) {
449
+ if (!show) return null;
450
+ return (
451
+ <Line
452
+ p1={p1}
453
+ p2={p2}
454
+ color={color}
455
+ strokeWidth={strokeWidth}
456
+ strokeCap={strokeCap}
457
+ >
458
+ {dash ? <DashPathEffect intervals={dash} /> : null}
459
+ </Line>
460
+ );
461
+ }
257
462
 
258
- {showTooltip && (
259
- <>
260
- <RoundedRect
261
- x={tipX}
262
- y={tipY}
263
- width={tipW}
264
- height={tipH}
265
- r={tooltipBorderRadius}
266
- color={tooltipBackground ?? palette.tooltipBg}
463
+ function CrosshairTooltip({
464
+ show,
465
+ tipX,
466
+ tipY,
467
+ tipW,
468
+ tipH,
469
+ borderRadius,
470
+ background,
471
+ borderColor,
472
+ children,
473
+ renderTooltip,
474
+ showValue,
475
+ showTime,
476
+ valueTextX,
477
+ timeTextX,
478
+ line1Y,
479
+ line2Y,
480
+ valueStr,
481
+ timeStr,
482
+ font,
483
+ valueColor,
484
+ timeColor,
485
+ }: {
486
+ show: boolean;
487
+ tipX: SharedValue<number>;
488
+ tipY: SharedValue<number>;
489
+ tipW: SharedValue<number>;
490
+ tipH: SharedValue<number>;
491
+ borderRadius: number;
492
+ background: string;
493
+ borderColor: string;
494
+ children?: ReactNode;
495
+ renderTooltip?: () => ReactNode;
496
+ showValue: boolean;
497
+ showTime: boolean;
498
+ valueTextX: SharedValue<number>;
499
+ timeTextX: SharedValue<number>;
500
+ line1Y: SharedValue<number>;
501
+ line2Y: SharedValue<number>;
502
+ valueStr: SharedValue<string>;
503
+ timeStr: SharedValue<string>;
504
+ font: SkFont;
505
+ valueColor: string;
506
+ timeColor: string;
507
+ }) {
508
+ if (!show) return null;
509
+ const body = children ?? renderTooltip?.();
510
+ return (
511
+ <>
512
+ <RoundedRect
513
+ x={tipX}
514
+ y={tipY}
515
+ width={tipW}
516
+ height={tipH}
517
+ r={borderRadius}
518
+ color={background}
519
+ />
520
+ <RoundedRect
521
+ x={tipX}
522
+ y={tipY}
523
+ width={tipW}
524
+ height={tipH}
525
+ r={borderRadius}
526
+ color={borderColor}
527
+ style="stroke"
528
+ strokeWidth={1}
529
+ />
530
+ {body ?? (
531
+ <Group>
532
+ {showValue ? (
533
+ <SkiaText
534
+ x={valueTextX}
535
+ y={line1Y}
536
+ text={valueStr}
537
+ font={font}
538
+ color={valueColor}
267
539
  />
268
-
269
- <RoundedRect
270
- x={tipX}
271
- y={tipY}
272
- width={tipW}
273
- height={tipH}
274
- r={tooltipBorderRadius}
275
- color={tooltipBorderColor ?? palette.tooltipBorder}
276
- style="stroke"
277
- strokeWidth={1}
540
+ ) : null}
541
+ {showTime ? (
542
+ <SkiaText
543
+ x={timeTextX}
544
+ y={showValue ? line2Y : line1Y}
545
+ text={timeStr}
546
+ font={font}
547
+ color={timeColor}
278
548
  />
279
-
280
- {children ?? renderTooltip?.() ?? (
281
- <Group>
282
- {tooltipShowValue && (
283
- <SkiaText
284
- x={valueTextX}
285
- y={line1Y}
286
- text={valueStr}
287
- font={font}
288
- color={tooltipColor ?? palette.tooltipText}
289
- />
290
- )}
291
- {effectiveTooltipShowTime && (
292
- <SkiaText
293
- x={timeTextX}
294
- y={tooltipShowValue ? line2Y : line1Y}
295
- text={timeStr}
296
- font={font}
297
- color={palette.gridLabel}
298
- />
299
- )}
300
- </Group>
301
- )}
302
- </>
303
- )}
304
- </Group>
549
+ ) : null}
550
+ </Group>
551
+ )}
305
552
  </>
306
553
  );
307
554
  }
@@ -30,6 +30,14 @@ import type {
30
30
  /** Live group flags surfaced to `renderMarker` (mirrored to JS on change). */
31
31
  type GroupState = Record<string, { isGrouped: boolean; groupCount: number }>;
32
32
 
33
+ function markerIdSet(key: string): Record<string, true> {
34
+ const ids: Record<string, true> = {};
35
+ for (const id of key.split("\x1f")) {
36
+ if (id) ids[id] = true;
37
+ }
38
+ return ids;
39
+ }
40
+
33
41
  /**
34
42
  * One custom-rendered marker: a React Native element floated over the canvas and
35
43
  * pinned to the marker's live clustered `(x, y)` position. It reads its slot out
@@ -168,12 +176,7 @@ export function CustomMarkerOverlay({
168
176
 
169
177
  // Stable id-set of customized markers (used by the UI-thread map below).
170
178
  const customKey = custom.map((c) => c.marker.id).join("\x1f");
171
- const customIds = useMemo<Record<string, true>>(() => {
172
- const o: Record<string, true> = {};
173
- for (const c of custom) o[c.marker.id] = true;
174
- return o;
175
- // eslint-disable-next-line react-hooks/exhaustive-deps -- recomputed when the id set (customKey) changes
176
- }, [customKey]);
179
+ const customIds = useMemo(() => markerIdSet(customKey), [customKey]);
177
180
 
178
181
  // One project+cluster over ALL markers each frame, exposed as an id→position map
179
182
  // for just the custom markers (so views read their own slot by id). Reuses a
@@ -24,6 +24,7 @@ export function CustomThresholdBadgeOverlay({
24
24
  y,
25
25
  visible,
26
26
  position,
27
+ leftAnchorX,
27
28
  }: {
28
29
  element: React.ReactElement;
29
30
  engine: ChartEngineLayout;
@@ -31,6 +32,8 @@ export function CustomThresholdBadgeOverlay({
31
32
  y: SharedValue<number>;
32
33
  visible: SharedValue<boolean>;
33
34
  position: "left" | "right";
35
+ /** Optional first rendered X for a non-extended series badge. */
36
+ leftAnchorX?: SharedValue<number>;
34
37
  }) {
35
38
  const size = useSharedValue({ width: 0, height: 0 });
36
39
  const onLayout = (event: LayoutChangeEvent) => {
@@ -43,10 +46,14 @@ export function CustomThresholdBadgeOverlay({
43
46
  const yy = y.get();
44
47
  const measured = size.get();
45
48
  const show = visible.get() && canvasWidth > 0 && Number.isFinite(yy);
46
- const translateX =
47
- position === "right"
48
- ? canvasWidth - padding.right - EDGE_INSET - measured.width
49
- : EDGE_INSET;
49
+ let translateX = EDGE_INSET;
50
+ if (position === "right") {
51
+ translateX = canvasWidth - padding.right - EDGE_INSET - measured.width;
52
+ } else if (leftAnchorX) {
53
+ const minX = padding.left + EDGE_INSET;
54
+ const maxX = canvasWidth - padding.right - EDGE_INSET - measured.width;
55
+ translateX = Math.max(minX, Math.min(leftAnchorX.get(), maxX));
56
+ }
50
57
  return {
51
58
  opacity: show ? 1 : 0,
52
59
  transform: [