react-native-livechart 3.8.2 → 3.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.
Files changed (43) hide show
  1. package/dist/components/AxisLabelOverlay.d.ts +19 -3
  2. package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
  3. package/dist/components/CrosshairLine.d.ts +3 -1
  4. package/dist/components/CrosshairLine.d.ts.map +1 -1
  5. package/dist/components/CrosshairOverlay.d.ts +7 -1
  6. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  7. package/dist/components/CustomTooltipOverlay.d.ts +8 -2
  8. package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
  9. package/dist/components/ExtremaConnectorOverlay.d.ts +32 -0
  10. package/dist/components/ExtremaConnectorOverlay.d.ts.map +1 -0
  11. package/dist/components/LiveChart.d.ts.map +1 -1
  12. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  13. package/dist/core/liveChartEngineTick.d.ts +11 -0
  14. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  15. package/dist/core/liveChartSeriesEngineTick.d.ts +10 -0
  16. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
  17. package/dist/core/resolveConfig.d.ts +34 -2
  18. package/dist/core/resolveConfig.d.ts.map +1 -1
  19. package/dist/core/useLiveChartEngine.d.ts +18 -2
  20. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  21. package/dist/core/useLiveChartSeriesEngine.d.ts +4 -0
  22. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  23. package/dist/hooks/crosshairShared.d.ts +12 -0
  24. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  25. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  26. package/dist/types.d.ts +88 -8
  27. package/dist/types.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/components/AxisLabelOverlay.tsx +399 -46
  30. package/src/components/CrosshairLine.tsx +7 -2
  31. package/src/components/CrosshairOverlay.tsx +22 -6
  32. package/src/components/CustomTooltipOverlay.tsx +30 -5
  33. package/src/components/ExtremaConnectorOverlay.tsx +193 -0
  34. package/src/components/LiveChart.tsx +38 -8
  35. package/src/components/LiveChartSeries.tsx +19 -0
  36. package/src/core/liveChartEngineTick.ts +51 -6
  37. package/src/core/liveChartSeriesEngineTick.ts +34 -0
  38. package/src/core/resolveConfig.ts +84 -3
  39. package/src/core/useLiveChartEngine.ts +42 -2
  40. package/src/core/useLiveChartSeriesEngine.ts +26 -0
  41. package/src/hooks/crosshairShared.ts +20 -1
  42. package/src/hooks/useCrosshair.ts +6 -0
  43. package/src/types.ts +89 -8
@@ -10,6 +10,7 @@ import type {
10
10
  LeftEdgeFadeConfig,
11
11
  LegendConfig,
12
12
  LegendStyle,
13
+ LineStyleConfig,
13
14
  LiveChartMetrics,
14
15
  LiveChartMetricsOverride,
15
16
  DotConfig,
@@ -61,12 +62,35 @@ export interface ResolvedYAxisConfig {
61
62
  minGap: number;
62
63
  }
63
64
 
65
+ /** Resolved straight-line styling (connector, etc.). `color: undefined` → caller default. */
66
+ export interface ResolvedLineStyleConfig {
67
+ color: string | undefined;
68
+ strokeWidth: number;
69
+ /** undefined → solid (no dash). */
70
+ intervals: [number, number] | undefined;
71
+ }
72
+
64
73
  export interface ResolvedAxisLabelConfig {
65
74
  /** undefined → use the chart's `formatValue` at render time. */
66
75
  format?: (v: number) => string;
67
76
  /** undefined → use the muted default label color at render time. */
68
77
  color?: string;
69
- position: "left" | "right";
78
+ /** `"left"`/`"right"` pin to that edge; `"extrema"`(-`edge`) tracks the data point. */
79
+ position: "left" | "right" | "extrema" | "extrema-edge";
80
+ /** undefined → the built-in default text size (11). */
81
+ fontSize?: number;
82
+ /** undefined → the platform `<Text>` default weight. */
83
+ fontWeight?: FontWeight;
84
+ /** undefined → the platform `<Text>` default family. */
85
+ fontFamily?: string;
86
+ /** Extrema dot color; undefined → use `color`. */
87
+ dotColor?: string;
88
+ /** Extrema dot diameter (px); undefined → the built-in default (7). */
89
+ dotSize?: number;
90
+ /** Extrema — whether to draw the marker dot. */
91
+ dot: boolean;
92
+ /** `"extrema-edge"` connector line (dot → edge label); null → none. */
93
+ connector: ResolvedLineStyleConfig | null;
70
94
  /** When set, the built-in value label is replaced by this custom element. */
71
95
  render?: () => ReactElement | null;
72
96
  }
@@ -81,6 +105,8 @@ export interface ResolvedScrubConfig {
81
105
  dimOpacity: number;
82
106
  /** undefined → palette.crosshairLine */
83
107
  crosshairLineColor: string | undefined;
108
+ /** Dash intervals `[on, off, …]` for the crosshair line; undefined → solid. */
109
+ crosshairDash: number[] | undefined;
84
110
  /** undefined → palette.crosshairDim */
85
111
  crosshairDimColor: string | undefined;
86
112
  /** undefined → palette.tooltipBg */
@@ -331,18 +357,65 @@ const AXIS_LABEL_DEFAULTS: ResolvedAxisLabelConfig = {
331
357
  format: undefined,
332
358
  color: undefined,
333
359
  position: "right",
360
+ fontSize: undefined,
361
+ fontWeight: undefined,
362
+ fontFamily: undefined,
363
+ dotColor: undefined,
364
+ dotSize: undefined,
365
+ dot: true,
366
+ // Always overwritten by resolveAxisLabel (per-position default-on); placeholder.
367
+ connector: null,
334
368
  render: undefined,
335
369
  };
336
370
 
371
+ /** Dashed by default — a subtle guide tying the extrema dot to its edge label. */
372
+ const CONNECTOR_DEFAULTS: ResolvedLineStyleConfig = {
373
+ color: undefined,
374
+ strokeWidth: 1,
375
+ intervals: [2, 3],
376
+ };
377
+
378
+ /**
379
+ * Resolves the extrema-label `connector` sub-prop to a line style or null.
380
+ * `defaultOn` (true in `"extrema-edge"` mode) means an unset connector draws the
381
+ * dashed default; `false` → null; an object → merged; a `LineStyleConfig` is
382
+ * normalized (its `intervals` may be omitted for a solid line).
383
+ */
384
+ export function resolveConnector(
385
+ prop: boolean | LineStyleConfig | undefined,
386
+ defaultOn: boolean,
387
+ ): ResolvedLineStyleConfig | null {
388
+ if (prop === false) return null;
389
+ if (prop == null) return defaultOn ? CONNECTOR_DEFAULTS : null;
390
+ if (prop === true) return CONNECTOR_DEFAULTS;
391
+ return {
392
+ color: prop.color,
393
+ strokeWidth: prop.strokeWidth ?? CONNECTOR_DEFAULTS.strokeWidth,
394
+ // An explicit object opts into its own dash (or solid when omitted).
395
+ intervals: prop.intervals,
396
+ };
397
+ }
398
+
337
399
  /**
338
400
  * Resolves a `topLabel` / `bottomLabel` prop to a fully-typed config or null
339
401
  * (no label). Opt-in, so `undefined`/`false` → null; `true` → the built-in
340
402
  * value label with defaults; object → configured built-in (or a custom `render`).
403
+ * The `connector` defaults on (dashed) in `"extrema-edge"` mode, else off.
341
404
  */
342
405
  export function resolveAxisLabel(
343
406
  prop: boolean | AxisLabelConfig | undefined,
344
407
  ): ResolvedAxisLabelConfig | null {
345
- return resolveToggle(prop, AXIS_LABEL_DEFAULTS, false);
408
+ const resolved = resolveToggle(prop, AXIS_LABEL_DEFAULTS, false);
409
+ if (!resolved) return null;
410
+ const connectorProp =
411
+ typeof prop === "object" ? prop.connector : undefined;
412
+ return {
413
+ ...resolved,
414
+ connector: resolveConnector(
415
+ connectorProp,
416
+ resolved.position === "extrema-edge",
417
+ ),
418
+ };
346
419
  }
347
420
 
348
421
  const X_AXIS_DEFAULTS: ResolvedXAxisConfig = {
@@ -363,6 +436,7 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
363
436
  tooltip: true,
364
437
  dimOpacity: 0.3,
365
438
  crosshairLineColor: undefined,
439
+ crosshairDash: undefined,
366
440
  crosshairDimColor: undefined,
367
441
  tooltipBackground: undefined,
368
442
  tooltipColor: undefined,
@@ -382,7 +456,14 @@ const SCRUB_DEFAULTS: ResolvedScrubConfig = {
382
456
  export function resolveScrub(
383
457
  prop: boolean | ScrubConfig | undefined,
384
458
  ): ResolvedScrubConfig | null {
385
- return resolveToggle(prop, SCRUB_DEFAULTS, false);
459
+ const resolved = resolveToggle(prop, SCRUB_DEFAULTS, false);
460
+ if (resolved) {
461
+ // Normalize the dash shorthand: `true` → a default dash, an array passes
462
+ // through, anything falsy → solid (undefined).
463
+ const dash = typeof prop === "object" ? prop.crosshairDash : undefined;
464
+ resolved.crosshairDash = dash === true ? [4, 4] : dash || undefined;
465
+ }
466
+ return resolved;
386
467
  }
387
468
 
388
469
  const SCRUB_ACTION_DEFAULTS: ResolvedScrubActionConfig = {
@@ -63,13 +63,26 @@ export interface ChartEngineLayout {
63
63
  timestamp: SharedValue<number>;
64
64
  }
65
65
 
66
- export interface SingleEngineState extends ChartEngineLayout {
66
+ /**
67
+ * Live extrema (value + time of the lowest / highest data point in the visible
68
+ * window) — the raw data high/low, not the smoothed display bounds. Each field
69
+ * is `NaN` when the window holds no data. Provided by both engines so an
70
+ * `"extrema"`-positioned `topLabel` / `bottomLabel` can be pinned at the point.
71
+ */
72
+ export interface ChartEngineExtrema {
73
+ extremaMinValue: SharedValue<number>;
74
+ extremaMaxValue: SharedValue<number>;
75
+ extremaMinTime: SharedValue<number>;
76
+ extremaMaxTime: SharedValue<number>;
77
+ }
78
+
79
+ export interface SingleEngineState extends ChartEngineLayout, ChartEngineExtrema {
67
80
  data: SharedValue<LiveChartPoint[]>;
68
81
  value: SharedValue<number>;
69
82
  displayValue: SharedValue<number>;
70
83
  }
71
84
 
72
- export interface MultiEngineState extends ChartEngineLayout {
85
+ export interface MultiEngineState extends ChartEngineLayout, ChartEngineExtrema {
73
86
  data: SharedValue<LiveChartPoint[]>;
74
87
  value: SharedValue<number>;
75
88
  displayValue: SharedValue<number>;
@@ -109,6 +122,10 @@ export interface EngineFrameRefs {
109
122
  modeSV: SharedValue<"line" | "candle">;
110
123
  candles?: SharedValue<CandlePoint[]>;
111
124
  liveCandle?: SharedValue<CandlePoint | null>;
125
+ extremaMinValue: SharedValue<number>;
126
+ extremaMaxValue: SharedValue<number>;
127
+ extremaMinTime: SharedValue<number>;
128
+ extremaMaxTime: SharedValue<number>;
112
129
  }
113
130
 
114
131
  /**
@@ -127,6 +144,10 @@ export function applyLiveChartEngineFrame(
127
144
  displayMax: sv.displayMax.value,
128
145
  displayWindow: sv.displayWindow.value,
129
146
  timestamp: sv.timestamp.value,
147
+ extremaMinValue: sv.extremaMinValue.value,
148
+ extremaMaxValue: sv.extremaMaxValue.value,
149
+ extremaMinTime: sv.extremaMinTime.value,
150
+ extremaMaxTime: sv.extremaMaxTime.value,
130
151
  };
131
152
  tickLiveChartEngineFrame(state, {
132
153
  dt,
@@ -155,6 +176,10 @@ export function applyLiveChartEngineFrame(
155
176
  sv.displayMax.value = state.displayMax;
156
177
  sv.displayWindow.value = state.displayWindow;
157
178
  sv.timestamp.value = state.timestamp;
179
+ sv.extremaMinValue.value = state.extremaMinValue;
180
+ sv.extremaMaxValue.value = state.extremaMaxValue;
181
+ sv.extremaMinTime.value = state.extremaMinTime;
182
+ sv.extremaMaxTime.value = state.extremaMaxTime;
158
183
  }
159
184
 
160
185
  export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
@@ -187,6 +212,13 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
187
212
  const [initialTimestamp] = useState(() => Date.now() / 1000);
188
213
  const timestamp = useSharedValue(initialTimestamp);
189
214
 
215
+ // Live data extrema (value + time of the visible high / low). NaN until the
216
+ // first tick finds data — the extrema label stays hidden until then.
217
+ const extremaMinValue = useSharedValue(NaN);
218
+ const extremaMaxValue = useSharedValue(NaN);
219
+ const extremaMinTime = useSharedValue(NaN);
220
+ const extremaMaxTime = useSharedValue(NaN);
221
+
190
222
  // High-frequency data reads directly from the caller's shared values —
191
223
  // no useDerivedValue bridging, no closure serialization per tick.
192
224
  const { data, value, candles, liveCandle } = config;
@@ -221,6 +253,10 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
221
253
  modeSV,
222
254
  candles,
223
255
  liveCandle,
256
+ extremaMinValue,
257
+ extremaMaxValue,
258
+ extremaMinTime,
259
+ extremaMaxTime,
224
260
  };
225
261
 
226
262
  // `autostart=false` registers the frame callback without running it — the live
@@ -271,5 +307,9 @@ export function useLiveChartEngine(config: EngineConfig): SingleEngineState {
271
307
  canvasWidth,
272
308
  canvasHeight,
273
309
  timestamp,
310
+ extremaMinValue,
311
+ extremaMaxValue,
312
+ extremaMinTime,
313
+ extremaMaxTime,
274
314
  };
275
315
  }
@@ -47,6 +47,10 @@ export interface MultiEngineFrameRefs {
47
47
  nowOverrideSV?: SharedValue<number | undefined>;
48
48
  windowBufferSV?: SharedValue<number>;
49
49
  pausedSV: SharedValue<boolean>;
50
+ extremaMinValue: SharedValue<number>;
51
+ extremaMaxValue: SharedValue<number>;
52
+ extremaMinTime: SharedValue<number>;
53
+ extremaMaxTime: SharedValue<number>;
50
54
  }
51
55
 
52
56
  /**
@@ -103,6 +107,10 @@ export function applyLiveChartSeriesEngineFrame(
103
107
  timestamp: sv.timestamp.value,
104
108
  displayValues,
105
109
  opacities,
110
+ extremaMinValue: sv.extremaMinValue.value,
111
+ extremaMaxValue: sv.extremaMaxValue.value,
112
+ extremaMinTime: sv.extremaMinTime.value,
113
+ extremaMaxTime: sv.extremaMaxTime.value,
106
114
  };
107
115
  tickLiveChartSeriesEngineFrame(state, {
108
116
  dt,
@@ -128,6 +136,10 @@ export function applyLiveChartSeriesEngineFrame(
128
136
  sv.timestamp.value = state.timestamp;
129
137
  sv.displaySeriesValues.value = state.displayValues;
130
138
  sv.seriesOpacities.value = state.opacities;
139
+ sv.extremaMinValue.value = state.extremaMinValue;
140
+ sv.extremaMaxValue.value = state.extremaMaxValue;
141
+ sv.extremaMinTime.value = state.extremaMinTime;
142
+ sv.extremaMaxTime.value = state.extremaMaxTime;
131
143
  }
132
144
 
133
145
  /**
@@ -161,6 +173,12 @@ export function useLiveChartSeriesEngine(
161
173
  const displaySeriesValues = useSharedValue<number[]>([]);
162
174
  const seriesOpacities = useSharedValue<number[]>([]);
163
175
 
176
+ // Live data extrema (value + time of the visible high / low across series).
177
+ const extremaMinValue = useSharedValue(NaN);
178
+ const extremaMaxValue = useSharedValue(NaN);
179
+ const extremaMinTime = useSharedValue(NaN);
180
+ const extremaMaxTime = useSharedValue(NaN);
181
+
164
182
  const data = useSharedValue<LiveChartPoint[]>([]);
165
183
  const value = useSharedValue(0);
166
184
  const displayValue = useSharedValue(0);
@@ -199,6 +217,10 @@ export function useLiveChartSeriesEngine(
199
217
  nowOverrideSV,
200
218
  windowBufferSV,
201
219
  pausedSV,
220
+ extremaMinValue,
221
+ extremaMaxValue,
222
+ extremaMinTime,
223
+ extremaMaxTime,
202
224
  },
203
225
  scratch,
204
226
  );
@@ -218,5 +240,9 @@ export function useLiveChartSeriesEngine(
218
240
  series,
219
241
  displaySeriesValues,
220
242
  seriesOpacities,
243
+ extremaMinValue,
244
+ extremaMaxValue,
245
+ extremaMinTime,
246
+ extremaMaxTime,
221
247
  };
222
248
  }
@@ -4,6 +4,7 @@ import type { SharedValue } from "react-native-reanimated";
4
4
  import { measureFontTextWidth } from "../lib/measureFontTextWidth";
5
5
  import { type ChartPadding } from "../draw/line";
6
6
  import { interpolateAtTime } from "../math/interpolate";
7
+ import type { CandlePoint } from "../types";
7
8
 
8
9
  const TOOLTIP_PAD_X = 8;
9
10
  const TOOLTIP_PAD_Y = 6;
@@ -57,6 +58,17 @@ export interface CrosshairState {
57
58
  /** Scrub intersection Y in canvas px; -1 when there's no dot to draw
58
59
  * (inactive / no value / degenerate range). See {@link computeScrubDotY}. */
59
60
  scrubDotY: SharedValue<number>;
61
+ /** OHLC candle under the crosshair in candle mode (`null` in line mode or when
62
+ * inactive) — single-series `useCrosshair` only; undefined on the multi-series
63
+ * crosshair. Surfaced for a custom candle `renderTooltip`. */
64
+ scrubCandle?: SharedValue<CandlePoint | null>;
65
+ /** Canvas-Y where the crosshair line should start so it stops at a top-pinned
66
+ * custom tooltip's bottom edge instead of running up through it. Written by
67
+ * {@link CustomTooltipOverlay} (the label's measured bottom) when
68
+ * `tooltipPlacement: "top"` is active, read by {@link CrosshairOverlay}; -1
69
+ * means "no top tooltip" → the line starts at `padding.top` as before.
70
+ * Single-series `useCrosshair` only; undefined on the multi-series crosshair. */
71
+ tooltipLineTop?: SharedValue<number>;
60
72
  gesture: ReturnType<typeof Gesture.Pan>;
61
73
 
62
74
  // ── Scrub-action ("order ticket") lock state — single-series `useCrosshair`
@@ -383,7 +395,7 @@ export function computeCandleTooltipLayout(
383
395
  { text: `C ${formatValue(candle.close)}`, dim: false },
384
396
  { text: formatTime(scrubTime), dim: true },
385
397
  ];
386
- return computeTooltipLayoutMulti(
398
+ const layout = computeTooltipLayoutMulti(
387
399
  scrubActive,
388
400
  scrubX,
389
401
  lines,
@@ -392,6 +404,13 @@ export function computeCandleTooltipLayout(
392
404
  font,
393
405
  monoCharWidth,
394
406
  );
407
+ // Surface the close + time as the single-value strings too. The built-in
408
+ // OHLC stack renders `stackedLines`, but a custom `renderTooltip` reads
409
+ // `valueStr` / `timeStr` off the layout (same as line mode) — so a custom
410
+ // candle tooltip gets a ready-made close + time without its own formatter.
411
+ layout.valueStr = formatValue(candle.close);
412
+ layout.timeStr = formatTime(scrubTime);
413
+ return layout;
395
414
  }
396
415
 
397
416
  /** Single-series scrub value at window time — extracted for tests. */
@@ -143,6 +143,10 @@ export function useCrosshair(
143
143
  // Tracks whether the active scrub phase actually began, so a tap that never
144
144
  // activates doesn't emit a spurious onGestureEnd.
145
145
  const gestureStarted = useSharedValue(false);
146
+ // Where the crosshair line should start (canvas Y) so it stops at a top-pinned
147
+ // custom tooltip instead of running through it. -1 = no top tooltip → the line
148
+ // starts at padding.top. Written by CustomTooltipOverlay, read by CrosshairOverlay.
149
+ const tooltipLineTop = useSharedValue(-1);
146
150
 
147
151
  // Scrub-action lock state. Created unconditionally (hooks must be), but the
148
152
  // lock gestures are only wired by the controller when `scrubAction` is set.
@@ -634,9 +638,11 @@ export function useCrosshair(
634
638
  scrubActive,
635
639
  scrubTime,
636
640
  scrubValue,
641
+ scrubCandle,
637
642
  crosshairOpacity,
638
643
  tooltipLayout,
639
644
  scrubDotY,
645
+ tooltipLineTop,
640
646
  gesture,
641
647
  lockActive,
642
648
  lockX,
package/src/types.ts CHANGED
@@ -228,6 +228,20 @@ export interface ValueLineConfig {
228
228
  color?: string;
229
229
  }
230
230
 
231
+ /**
232
+ * Shared styling for a simple straight line — color, thickness, and an optional
233
+ * dash. The common shape behind the chart's secondary lines (the `valueLine`,
234
+ * the extrema-label `connector`, etc.), so they configure the same way.
235
+ */
236
+ export interface LineStyleConfig {
237
+ /** Line color. Defaults per usage (e.g. the connector uses the label color). */
238
+ color?: string;
239
+ /** Line thickness in pixels. Default `1`. */
240
+ strokeWidth?: number;
241
+ /** Dash pattern as `[dashLength, gapLength]` in pixels. Omit for a solid line. */
242
+ intervals?: [number, number];
243
+ }
244
+
231
245
  /** Main chart line styling. */
232
246
  export interface LineConfig {
233
247
  /** Stroke width of the main line in pixels. Default `2`. */
@@ -356,9 +370,52 @@ export interface AxisLabelConfig {
356
370
  format?: (v: number) => string;
357
371
  /** Text color. Defaults to a muted label color (`palette.gridLabel`). */
358
372
  color?: string;
359
- /** Horizontal alignment within the plot width. Default `"right"`. */
360
- position?: "left" | "right";
361
- /** Full custom element, floated at the edge. Overrides the built-in value label. */
373
+ /**
374
+ * Where the label sits.
375
+ * - `"left"` / `"right"` (default `"right"`) pin to that edge of the plot,
376
+ * horizontally aligned.
377
+ * - `"extrema"` — float at the **actual data point** where the value occurs
378
+ * (`topLabel` tracks the highest point, `bottomLabel` the lowest), anchored
379
+ * over the point with a marker dot, so you can see *when* the high / low
380
+ * happened. The dot and label track the point on the UI thread.
381
+ * - `"extrema-edge"` — like `"extrema"`, but the value label is pinned to the
382
+ * **top / bottom edge**, horizontally aligned with the extremum (not floated
383
+ * over the point). The marker dot still sits on the data point, joined to the
384
+ * edge label by a {@link AxisLabelConfig.connector} line. Keeps the readout on
385
+ * a clean rail while still showing where the extremum is.
386
+ */
387
+ position?: "left" | "right" | "extrema" | "extrema-edge";
388
+ /** Built-in value text size in px. Default `11`. */
389
+ fontSize?: number;
390
+ /** Built-in value text weight. Default the platform `<Text>` default. */
391
+ fontWeight?: FontWeight;
392
+ /** Built-in value text font family (e.g. a loaded monospace face). */
393
+ fontFamily?: string;
394
+ /**
395
+ * Extrema modes only — color of the marker dot at the data point. Defaults to
396
+ * `color`. Lets the dot and the value text differ.
397
+ */
398
+ dotColor?: string;
399
+ /** Extrema modes only — marker dot diameter in px. Default `7`. */
400
+ dotSize?: number;
401
+ /**
402
+ * Extrema modes only — draw the marker dot at the data point. Default `true`.
403
+ * Set `false` for a value label with no dot.
404
+ */
405
+ dot?: boolean;
406
+ /**
407
+ * `"extrema-edge"` only — the line joining the marker dot (on the data point)
408
+ * to the edge value label. `true` = a dashed default, `false` = none, or pass a
409
+ * {@link LineStyleConfig} to style it (`color` defaults to the label `color`).
410
+ * Default on (dashed) in `"extrema-edge"` mode.
411
+ */
412
+ connector?: boolean | LineStyleConfig;
413
+ /**
414
+ * Full custom element, floated at the edge (or, in an extrema mode, centered
415
+ * over the extremum point). Overrides the built-in value label (and the
416
+ * `fontSize` / `fontWeight` / `fontFamily` / `dot*` knobs above — you own the
417
+ * styling).
418
+ */
362
419
  render?: () => ReactElement | null;
363
420
  }
364
421
 
@@ -382,6 +439,12 @@ export interface ScrubConfig {
382
439
  dimOpacity?: number;
383
440
  /** Vertical crosshair line stroke. Omit to use theme `crosshairLine`. */
384
441
  crosshairLineColor?: string;
442
+ /**
443
+ * Dash the vertical crosshair line. `true` → a default `[4, 4]` dash; an array
444
+ * sets explicit Skia dash intervals `[on, off, …]` in px. Omit / `false` → a
445
+ * solid line.
446
+ */
447
+ crosshairDash?: number[] | boolean;
385
448
  /**
386
449
  * Legacy: fill the region right of the crosshair with this solid (usually
387
450
  * semi-transparent) color — a mask painted *over* the chart, so it only looks
@@ -521,16 +584,30 @@ export interface SelectionDotProps {
521
584
  * for the value/date to update on the UI thread too.
522
585
  */
523
586
  export interface TooltipRenderProps {
524
- /** Interpolated value under the crosshair; `null` when none. */
587
+ /**
588
+ * Value under the crosshair; `null` when none. In line mode this is the
589
+ * interpolated value at the scrub time; in candle mode it's the scrubbed
590
+ * candle's close (use {@link TooltipRenderProps.candle} for full OHLC).
591
+ */
525
592
  value: SharedValue<number | null>;
526
593
  /** Window time (unix seconds) under the crosshair. */
527
594
  time: SharedValue<number>;
528
- /** Value formatted with the chart's `formatValue` (computed UI-side). */
595
+ /**
596
+ * Value formatted with the chart's `formatValue` (computed UI-side). In candle
597
+ * mode this is the formatted close.
598
+ */
529
599
  valueStr: SharedValue<string>;
530
600
  /** Time formatted with the chart's `formatTime` (computed UI-side). */
531
601
  timeStr: SharedValue<string>;
532
602
  /** Whether scrubbing is currently active. */
533
603
  active: SharedValue<boolean>;
604
+ /**
605
+ * In candle mode, the OHLC candle under the crosshair (`null` when none or
606
+ * while inactive). Always `null` in line mode — bind it to render OHLC in a
607
+ * custom candlestick tooltip. Format the individual prices with your own
608
+ * worklet-safe formatter (e.g. the chart's `formatValue`).
609
+ */
610
+ candle: SharedValue<CandlePoint | null>;
534
611
  }
535
612
 
536
613
  /** Outer ring drawn around the built-in selection dot (the subtle halo). */
@@ -1094,9 +1171,13 @@ export interface LiveChartCoreProps {
1094
1171
  * background are plain RN styles) and content — bind the {@link
1095
1172
  * TooltipRenderProps} SharedValues to animated text for the value/date.
1096
1173
  *
1097
- * Return `null`/`undefined` to fall back to the built-in pill. Replaces the
1098
- * default pill entirely while active. Single-series **line mode only**
1099
- * ignored in candle mode (the OHLC stack owns the tooltip there).
1174
+ * Supplying `renderTooltip` replaces the built-in tooltip entirely while
1175
+ * scrubbing (the line pill in line mode, the OHLC stack in candle mode).
1176
+ * Returning `null`/`undefined` from a frame renders nothing for that frame
1177
+ * (e.g. to hide the tooltip in certain states) — it does *not* restore the
1178
+ * built-in pill. Works in **both line and candle mode**: in candle mode the
1179
+ * scrubbed candle is available as {@link TooltipRenderProps.candle} for
1180
+ * rendering your own OHLC readout.
1100
1181
  */
1101
1182
  renderTooltip?: (ctx: TooltipRenderProps) => ReactElement | null | undefined;
1102
1183
  /**