react-native-livechart 3.5.1 → 3.7.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 +7 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/CustomMarkerOverlay.d.ts +27 -0
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +37 -0
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -0
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts +7 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +10 -0
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/hooks/crosshairShared.d.ts +14 -2
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -1
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairOverlay.tsx +29 -18
- package/src/components/CustomMarkerOverlay.tsx +168 -0
- package/src/components/CustomTooltipOverlay.tsx +135 -0
- package/src/components/LiveChart.tsx +59 -1
- package/src/components/LiveChartSeries.tsx +20 -0
- package/src/components/MarkerOverlay.tsx +32 -4
- package/src/core/resolveConfig.ts +15 -0
- package/src/hooks/crosshairShared.ts +60 -8
- package/src/hooks/useCrosshair.ts +13 -0
- package/src/index.ts +1 -0
- package/src/types.ts +71 -0
|
@@ -190,6 +190,18 @@ export function computeTooltipLayout(
|
|
|
190
190
|
* every frame, and `measureText` shapes text each call (a real cost,
|
|
191
191
|
* especially in the simulator). Falls back to `measureText` when 0. */
|
|
192
192
|
monoCharWidth = 0,
|
|
193
|
+
/** Where the pill sits relative to the scrub line. `"side"` offsets it right
|
|
194
|
+
* (flipping left near the edge); `"top"`/`"bottom"` center it over the line,
|
|
195
|
+
* clamped into the plot and pinned to the plot's top/bottom. */
|
|
196
|
+
placement: "side" | "top" | "bottom" = "side",
|
|
197
|
+
/** Render the value row. */
|
|
198
|
+
showValue = true,
|
|
199
|
+
/** Render the time row. */
|
|
200
|
+
showTime = true,
|
|
201
|
+
/** Canvas height in px — needed to pin `"bottom"` placement. */
|
|
202
|
+
canvasHeight = 0,
|
|
203
|
+
/** Gap in px between the pill and the plot edge it's pinned to. */
|
|
204
|
+
margin = TOOLTIP_TOP_MARGIN,
|
|
193
205
|
): TooltipLayout {
|
|
194
206
|
"worklet";
|
|
195
207
|
if (!scrubActive || scrubValue === null) return HIDDEN_TOOLTIP;
|
|
@@ -199,9 +211,19 @@ export function computeTooltipLayout(
|
|
|
199
211
|
const valueStr = formatValue(v);
|
|
200
212
|
const timeStr = formatTime(t);
|
|
201
213
|
|
|
214
|
+
// Content toggles: pick which rows render. Guard both-off → keep the time row.
|
|
215
|
+
let sv = showValue;
|
|
216
|
+
let st = showTime;
|
|
217
|
+
let rowCount = (sv ? 1 : 0) + (st ? 1 : 0);
|
|
218
|
+
if (rowCount === 0) {
|
|
219
|
+
st = true;
|
|
220
|
+
rowCount = 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
202
223
|
const fm = font.getMetrics();
|
|
203
224
|
const lineH = -fm.ascent + fm.descent;
|
|
204
|
-
const totalH =
|
|
225
|
+
const totalH =
|
|
226
|
+
TOOLTIP_PAD_Y * 2 + lineH * rowCount + TOOLTIP_LINE_GAP * (rowCount - 1);
|
|
205
227
|
|
|
206
228
|
const valueW =
|
|
207
229
|
monoCharWidth > 0
|
|
@@ -211,19 +233,39 @@ export function computeTooltipLayout(
|
|
|
211
233
|
monoCharWidth > 0
|
|
212
234
|
? timeStr.length * monoCharWidth
|
|
213
235
|
: measureFontTextWidth(font, timeStr);
|
|
214
|
-
|
|
236
|
+
// Only the visible rows contribute to the pill width.
|
|
237
|
+
const contentW = Math.max(sv ? valueW : 0, st ? timeW : 0);
|
|
215
238
|
const pillW = contentW + TOOLTIP_PAD_X * 2;
|
|
216
239
|
|
|
217
240
|
const rightEdge = canvasWidth - padding.right;
|
|
218
241
|
|
|
219
|
-
let pillX
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
let pillX: number;
|
|
243
|
+
let pillY: number;
|
|
244
|
+
if (placement === "side") {
|
|
245
|
+
pillX = scrubX + TOOLTIP_OFFSET_X;
|
|
246
|
+
if (pillX + pillW > rightEdge - TOOLTIP_EDGE_GAP) {
|
|
247
|
+
pillX = scrubX - pillW - TOOLTIP_OFFSET_X;
|
|
248
|
+
}
|
|
249
|
+
pillY = padding.top + margin;
|
|
250
|
+
} else {
|
|
251
|
+
// Centered horizontally over the scrub line, clamped into the plot.
|
|
252
|
+
const leftBound = padding.left + TOOLTIP_EDGE_GAP;
|
|
253
|
+
const rightBound = rightEdge - TOOLTIP_EDGE_GAP - pillW;
|
|
254
|
+
pillX = Math.min(
|
|
255
|
+
Math.max(scrubX - pillW / 2, leftBound),
|
|
256
|
+
Math.max(leftBound, rightBound),
|
|
257
|
+
);
|
|
258
|
+
pillY =
|
|
259
|
+
placement === "top"
|
|
260
|
+
? padding.top + margin
|
|
261
|
+
: canvasHeight - padding.bottom - margin - totalH;
|
|
222
262
|
}
|
|
223
|
-
const pillY = padding.top + TOOLTIP_TOP_MARGIN;
|
|
224
263
|
|
|
225
|
-
|
|
226
|
-
|
|
264
|
+
// line1Y = first rendered row's baseline; line2Y = second (when both shown).
|
|
265
|
+
// The overlay maps value→line1Y and time→(showValue ? line2Y : line1Y).
|
|
266
|
+
const firstBaseline = pillY + TOOLTIP_PAD_Y - fm.ascent;
|
|
267
|
+
const line1Y = firstBaseline;
|
|
268
|
+
const line2Y = firstBaseline + lineH + TOOLTIP_LINE_GAP;
|
|
227
269
|
|
|
228
270
|
const valueTextX = pillX + TOOLTIP_PAD_X + (contentW - valueW) / 2;
|
|
229
271
|
const timeTextX = pillX + TOOLTIP_PAD_X + (contentW - timeW) / 2;
|
|
@@ -618,6 +660,11 @@ export function deriveCrosshairTooltipSingle(
|
|
|
618
660
|
formatTime: (t: number) => string,
|
|
619
661
|
font: SkFont,
|
|
620
662
|
monoCharWidth = 0,
|
|
663
|
+
placement: "side" | "top" | "bottom" = "side",
|
|
664
|
+
showValue = true,
|
|
665
|
+
showTime = true,
|
|
666
|
+
canvasHeight = 0,
|
|
667
|
+
margin = TOOLTIP_TOP_MARGIN,
|
|
621
668
|
): TooltipLayout {
|
|
622
669
|
"worklet";
|
|
623
670
|
if (!scrubActive || scrubTime < 0) return HIDDEN_TOOLTIP;
|
|
@@ -632,5 +679,10 @@ export function deriveCrosshairTooltipSingle(
|
|
|
632
679
|
formatTime,
|
|
633
680
|
font,
|
|
634
681
|
monoCharWidth,
|
|
682
|
+
placement,
|
|
683
|
+
showValue,
|
|
684
|
+
showTime,
|
|
685
|
+
canvasHeight,
|
|
686
|
+
margin,
|
|
635
687
|
);
|
|
636
688
|
}
|
|
@@ -129,6 +129,14 @@ export function useCrosshair(
|
|
|
129
129
|
* Omitted when nothing coexists.
|
|
130
130
|
*/
|
|
131
131
|
deferTapHit?: (x: number, y: number) => boolean,
|
|
132
|
+
/** Where the tooltip pill sits relative to the scrub line. Default `"side"`. */
|
|
133
|
+
tooltipPlacement: "side" | "top" | "bottom" = "side",
|
|
134
|
+
/** Render the value row in the default tooltip. Default `true`. */
|
|
135
|
+
tooltipShowValue = true,
|
|
136
|
+
/** Render the time row in the default tooltip. Default `true`. */
|
|
137
|
+
tooltipShowTime = true,
|
|
138
|
+
/** Gap (px) between the tooltip and the plot edge it's pinned to. Default `8`. */
|
|
139
|
+
tooltipMargin = 8,
|
|
132
140
|
): CrosshairState {
|
|
133
141
|
const scrubX = useSharedValue(-1);
|
|
134
142
|
const scrubActive = useSharedValue(false);
|
|
@@ -247,6 +255,11 @@ export function useCrosshair(
|
|
|
247
255
|
formatTime,
|
|
248
256
|
font,
|
|
249
257
|
monoCharWidth,
|
|
258
|
+
tooltipPlacement,
|
|
259
|
+
tooltipShowValue,
|
|
260
|
+
tooltipShowTime,
|
|
261
|
+
engine.canvasHeight.get(),
|
|
262
|
+
tooltipMargin,
|
|
250
263
|
);
|
|
251
264
|
});
|
|
252
265
|
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -378,6 +378,27 @@ export interface ScrubConfig {
|
|
|
378
378
|
tooltipColor?: string;
|
|
379
379
|
/** Tooltip pill border color. Omit to use theme `tooltipBorder`. */
|
|
380
380
|
tooltipBorderColor?: string;
|
|
381
|
+
/** Tooltip pill corner radius in px. Default `5`. */
|
|
382
|
+
tooltipBorderRadius?: number;
|
|
383
|
+
/**
|
|
384
|
+
* Where the tooltip pill sits relative to the vertical scrub line.
|
|
385
|
+
* `"side"` (default) offsets it to the right of the line and flips left near
|
|
386
|
+
* the right edge. `"top"` / `"bottom"` center it horizontally over the line,
|
|
387
|
+
* clamped into the plot and pinned to the plot's top or bottom. This also
|
|
388
|
+
* drives a custom {@link LiveChartProps.renderTooltip} so it gets the same
|
|
389
|
+
* placement for free.
|
|
390
|
+
*/
|
|
391
|
+
tooltipPlacement?: "side" | "top" | "bottom";
|
|
392
|
+
/**
|
|
393
|
+
* Gap in px between the tooltip and the plot edge it's pinned to — the top for
|
|
394
|
+
* `"side"`/`"top"`, the bottom for `"bottom"`. Applies to the built-in pill and
|
|
395
|
+
* a custom {@link LiveChartProps.renderTooltip}. Default `8`.
|
|
396
|
+
*/
|
|
397
|
+
tooltipMargin?: number;
|
|
398
|
+
/** Show the value row in the default tooltip body. Default `true`. */
|
|
399
|
+
tooltipShowValue?: boolean;
|
|
400
|
+
/** Show the time row in the default tooltip body. Default `true`. */
|
|
401
|
+
tooltipShowTime?: boolean;
|
|
381
402
|
/**
|
|
382
403
|
* Press-and-hold delay in milliseconds before scrubbing activates — think of
|
|
383
404
|
* it as "press and hold to scrub." During the delay the pan is not captured,
|
|
@@ -473,6 +494,28 @@ export interface SelectionDotProps {
|
|
|
473
494
|
size: number;
|
|
474
495
|
}
|
|
475
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Context passed to a custom {@link LiveChartProps.renderTooltip}. The element
|
|
499
|
+
* you return is a React Native view that the chart floats over the canvas and
|
|
500
|
+
* positions on the UI thread (per `scrub.tooltipPlacement`) — so movement stays
|
|
501
|
+
* smooth without JS re-renders, unlike rebuilding the tooltip from the JS-thread
|
|
502
|
+
* `onScrub` callback. Bind the SharedValues here to animated text (e.g. an
|
|
503
|
+
* `Animated.createAnimatedComponent(TextInput)` driven by `useAnimatedProps`)
|
|
504
|
+
* for the value/date to update on the UI thread too.
|
|
505
|
+
*/
|
|
506
|
+
export interface TooltipRenderProps {
|
|
507
|
+
/** Interpolated value under the crosshair; `null` when none. */
|
|
508
|
+
value: SharedValue<number | null>;
|
|
509
|
+
/** Window time (unix seconds) under the crosshair. */
|
|
510
|
+
time: SharedValue<number>;
|
|
511
|
+
/** Value formatted with the chart's `formatValue` (computed UI-side). */
|
|
512
|
+
valueStr: SharedValue<string>;
|
|
513
|
+
/** Time formatted with the chart's `formatTime` (computed UI-side). */
|
|
514
|
+
timeStr: SharedValue<string>;
|
|
515
|
+
/** Whether scrubbing is currently active. */
|
|
516
|
+
active: SharedValue<boolean>;
|
|
517
|
+
}
|
|
518
|
+
|
|
476
519
|
/** Outer ring drawn around the built-in selection dot (the subtle halo). */
|
|
477
520
|
export interface SelectionDotRingConfig {
|
|
478
521
|
/** Ring color. Defaults to the dot color. */
|
|
@@ -1011,6 +1054,34 @@ export interface LiveChartCoreProps {
|
|
|
1011
1054
|
onMarkerHover?: (event: MarkerHoverEvent | null) => void;
|
|
1012
1055
|
/** Tap hit-test radius in px. Default `16` (≈ 44px touch target with the glyph). */
|
|
1013
1056
|
markerHitRadius?: number;
|
|
1057
|
+
/**
|
|
1058
|
+
* Render a marker as a custom **React Native** element instead of a built-in
|
|
1059
|
+
* Skia glyph — e.g. an `expo-blur` glass badge or any non-Skia view that the
|
|
1060
|
+
* canvas can't draw. Return an element to float it, auto-centered, at the
|
|
1061
|
+
* marker's live `(time, value)` position (tracked on the UI thread); return
|
|
1062
|
+
* `null`/`undefined` to keep the built-in atlas glyph for that marker.
|
|
1063
|
+
*
|
|
1064
|
+
* The element is rendered as an RN view layered over the canvas, so it is
|
|
1065
|
+
* crisp at native resolution. Use it sparingly (a handful of special markers):
|
|
1066
|
+
* each custom marker is its own animated view + projection, whereas built-in
|
|
1067
|
+
* glyphs batch into a single draw call. Custom-rendered markers skip the atlas
|
|
1068
|
+
* glyph entirely (no double-draw).
|
|
1069
|
+
*/
|
|
1070
|
+
renderMarker?: (marker: Marker) => ReactElement | null | undefined;
|
|
1071
|
+
/**
|
|
1072
|
+
* Render a fully custom scrub tooltip as a **React Native** element instead of
|
|
1073
|
+
* the built-in pill — the same idea as `renderMarker`. The chart floats the
|
|
1074
|
+
* returned element over the canvas and positions it on the UI thread per
|
|
1075
|
+
* `scrub.tooltipPlacement` (so it stays smooth, unlike rebuilding the tooltip
|
|
1076
|
+
* from the JS-thread `onScrub`). You own the chrome (border radius/color,
|
|
1077
|
+
* background are plain RN styles) and content — bind the {@link
|
|
1078
|
+
* TooltipRenderProps} SharedValues to animated text for the value/date.
|
|
1079
|
+
*
|
|
1080
|
+
* Return `null`/`undefined` to fall back to the built-in pill. Replaces the
|
|
1081
|
+
* default pill entirely while active. Single-series **line mode only** —
|
|
1082
|
+
* ignored in candle mode (the OHLC stack owns the tooltip there).
|
|
1083
|
+
*/
|
|
1084
|
+
renderTooltip?: (ctx: TooltipRenderProps) => ReactElement | null | undefined;
|
|
1014
1085
|
/**
|
|
1015
1086
|
* Override individual resolved-palette keys on top of the palette derived from
|
|
1016
1087
|
* `accentColor` + `theme`. Only the keys you set are replaced.
|