react-native-livechart 3.12.0 → 4.1.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 (140) hide show
  1. package/dist/components/BadgeOverlay.d.ts +9 -1
  2. package/dist/components/BadgeOverlay.d.ts.map +1 -1
  3. package/dist/components/ChartOverlayLayer.d.ts +19 -0
  4. package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
  5. package/dist/components/CustomMarkerOverlay.d.ts +16 -6
  6. package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
  7. package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
  8. package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
  9. package/dist/components/CustomTooltipOverlay.d.ts +5 -2
  10. package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
  11. package/dist/components/DotOverlay.d.ts +7 -1
  12. package/dist/components/DotOverlay.d.ts.map +1 -1
  13. package/dist/components/LiveChart.d.ts.map +1 -1
  14. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  15. package/dist/components/MarkerOverlay.d.ts +6 -3
  16. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  17. package/dist/components/MultiSeriesDots.d.ts +4 -1
  18. package/dist/components/MultiSeriesDots.d.ts.map +1 -1
  19. package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
  20. package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
  21. package/dist/components/ReferenceLineOverlay.d.ts +25 -2
  22. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  23. package/dist/components/XAxisOverlay.d.ts +7 -1
  24. package/dist/components/XAxisOverlay.d.ts.map +1 -1
  25. package/dist/components/YAxisOverlay.d.ts +12 -1
  26. package/dist/components/YAxisOverlay.d.ts.map +1 -1
  27. package/dist/constants.d.ts +13 -0
  28. package/dist/constants.d.ts.map +1 -1
  29. package/dist/core/liveChartEngineTick.d.ts +29 -0
  30. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  31. package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
  32. package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
  33. package/dist/core/resolveConfig.d.ts +62 -2
  34. package/dist/core/resolveConfig.d.ts.map +1 -1
  35. package/dist/core/useLiveChartEngine.d.ts +58 -7
  36. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  37. package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
  38. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  39. package/dist/draw/grid.d.ts +13 -1
  40. package/dist/draw/grid.d.ts.map +1 -1
  41. package/dist/draw/markerAtlas.d.ts +20 -2
  42. package/dist/draw/markerAtlas.d.ts.map +1 -1
  43. package/dist/draw/volume.d.ts +25 -0
  44. package/dist/draw/volume.d.ts.map +1 -0
  45. package/dist/hooks/crosshairShared.d.ts +12 -6
  46. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  47. package/dist/hooks/overlayScale.d.ts +22 -0
  48. package/dist/hooks/overlayScale.d.ts.map +1 -0
  49. package/dist/hooks/resolveChartLayout.d.ts +9 -0
  50. package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
  51. package/dist/hooks/useBadge.d.ts +22 -1
  52. package/dist/hooks/useBadge.d.ts.map +1 -1
  53. package/dist/hooks/useCandlePaths.d.ts +6 -0
  54. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  55. package/dist/hooks/useChartOverlayContext.d.ts +45 -0
  56. package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
  57. package/dist/hooks/useChartPaths.d.ts +8 -1
  58. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  59. package/dist/hooks/useCrosshair.d.ts +9 -2
  60. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  61. package/dist/hooks/useLiveDot.d.ts +6 -1
  62. package/dist/hooks/useLiveDot.d.ts.map +1 -1
  63. package/dist/hooks/useMarkers.d.ts +9 -4
  64. package/dist/hooks/useMarkers.d.ts.map +1 -1
  65. package/dist/hooks/usePanScroll.d.ts +82 -0
  66. package/dist/hooks/usePanScroll.d.ts.map +1 -0
  67. package/dist/hooks/usePinchZoom.d.ts +74 -0
  68. package/dist/hooks/usePinchZoom.d.ts.map +1 -0
  69. package/dist/hooks/useReferenceDrag.d.ts +24 -0
  70. package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
  71. package/dist/hooks/useReferenceLine.d.ts +23 -4
  72. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  73. package/dist/hooks/useReferenceLinePress.d.ts +5 -1
  74. package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
  75. package/dist/hooks/useVisibleRange.d.ts +39 -0
  76. package/dist/hooks/useVisibleRange.d.ts.map +1 -0
  77. package/dist/hooks/useYAxis.d.ts +1 -1
  78. package/dist/hooks/useYAxis.d.ts.map +1 -1
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/math/markerCluster.d.ts +47 -0
  82. package/dist/math/markerCluster.d.ts.map +1 -0
  83. package/dist/math/markers.d.ts +10 -0
  84. package/dist/math/markers.d.ts.map +1 -1
  85. package/dist/math/referenceDrag.d.ts +24 -0
  86. package/dist/math/referenceDrag.d.ts.map +1 -0
  87. package/dist/math/referenceGroup.d.ts +31 -0
  88. package/dist/math/referenceGroup.d.ts.map +1 -0
  89. package/dist/math/referenceLines.d.ts +33 -7
  90. package/dist/math/referenceLines.d.ts.map +1 -1
  91. package/dist/types.d.ts +524 -21
  92. package/dist/types.d.ts.map +1 -1
  93. package/package.json +1 -1
  94. package/src/components/BadgeOverlay.tsx +26 -1
  95. package/src/components/ChartOverlayLayer.tsx +32 -0
  96. package/src/components/CustomMarkerOverlay.tsx +150 -57
  97. package/src/components/CustomReferenceLineOverlay.tsx +250 -0
  98. package/src/components/CustomTooltipOverlay.tsx +27 -7
  99. package/src/components/DotOverlay.tsx +9 -0
  100. package/src/components/LiveChart.tsx +550 -32
  101. package/src/components/LiveChartSeries.tsx +111 -5
  102. package/src/components/MarkerOverlay.tsx +92 -6
  103. package/src/components/MultiSeriesDots.tsx +12 -3
  104. package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
  105. package/src/components/ReferenceLineOverlay.tsx +105 -32
  106. package/src/components/XAxisOverlay.tsx +9 -2
  107. package/src/components/YAxisOverlay.tsx +49 -28
  108. package/src/constants.ts +17 -0
  109. package/src/core/liveChartEngineTick.ts +81 -8
  110. package/src/core/liveChartSeriesEngineTick.ts +50 -9
  111. package/src/core/resolveConfig.ts +133 -2
  112. package/src/core/useLiveChartEngine.ts +111 -11
  113. package/src/core/useLiveChartSeriesEngine.ts +26 -3
  114. package/src/draw/grid.ts +71 -2
  115. package/src/draw/markerAtlas.ts +117 -3
  116. package/src/draw/volume.ts +190 -0
  117. package/src/hooks/crosshairShared.ts +37 -9
  118. package/src/hooks/overlayScale.ts +59 -0
  119. package/src/hooks/resolveChartLayout.ts +33 -0
  120. package/src/hooks/useBadge.ts +89 -21
  121. package/src/hooks/useCandlePaths.ts +104 -7
  122. package/src/hooks/useChartOverlayContext.ts +93 -0
  123. package/src/hooks/useChartPaths.ts +14 -1
  124. package/src/hooks/useCrosshair.ts +33 -2
  125. package/src/hooks/useLiveDot.ts +16 -5
  126. package/src/hooks/useMarkers.ts +39 -11
  127. package/src/hooks/usePanScroll.ts +253 -0
  128. package/src/hooks/usePinchZoom.ts +189 -0
  129. package/src/hooks/useReferenceDrag.ts +254 -0
  130. package/src/hooks/useReferenceLine.ts +56 -9
  131. package/src/hooks/useReferenceLinePress.ts +16 -2
  132. package/src/hooks/useVisibleRange.ts +119 -0
  133. package/src/hooks/useYAxis.ts +2 -0
  134. package/src/index.ts +17 -1
  135. package/src/math/markerCluster.ts +214 -0
  136. package/src/math/markers.ts +35 -3
  137. package/src/math/referenceDrag.ts +66 -0
  138. package/src/math/referenceGroup.ts +65 -0
  139. package/src/math/referenceLines.ts +76 -19
  140. package/src/types.ts +544 -21
@@ -1,7 +1,12 @@
1
- import { useLayoutEffect, useState } from "react";
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import { View } from "react-native";
3
3
  import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
- import { useDerivedValue, useSharedValue } from "react-native-reanimated";
4
+ import {
5
+ useAnimatedReaction,
6
+ useDerivedValue,
7
+ useSharedValue,
8
+ } from "react-native-reanimated";
9
+ import { scheduleOnRN } from "react-native-worklets";
5
10
 
6
11
  /**
7
12
  * Single-series live chart. UX and prop vocabulary parallel Benji Taylor’s
@@ -17,7 +22,7 @@ import {
17
22
  vec,
18
23
  } from "@shopify/react-native-skia";
19
24
 
20
- import { DEFAULT_ACCENT_COLOR } from "../constants";
25
+ import { DEFAULT_ACCENT_COLOR, HOLD_TO_SCRUB_MS } from "../constants";
21
26
  import {
22
27
  resolveAreaDots,
23
28
  resolveAxisLabel,
@@ -27,6 +32,7 @@ import {
27
32
  resolveGradient,
28
33
  resolveGridStyle,
29
34
  resolveLeftEdgeFade,
35
+ resolveMarkerCluster,
30
36
  resolveMetrics,
31
37
  resolvePulse,
32
38
  resolveScrub,
@@ -35,6 +41,8 @@ import {
35
41
  resolveThreshold,
36
42
  resolveTradeStream,
37
43
  resolveValueLine,
44
+ resolveVolume,
45
+ resolveZoom,
38
46
  resolveXAxis,
39
47
  resolveYAxis,
40
48
  } from "../core/resolveConfig";
@@ -47,6 +55,7 @@ import { useBadge } from "../hooks/useBadge";
47
55
  import { useCandlePaths } from "../hooks/useCandlePaths";
48
56
  import { useCanvasLayout } from "../hooks/useCanvasLayout";
49
57
  import { useChartColors } from "../hooks/useChartColors";
58
+ import { useChartOverlayContext } from "../hooks/useChartOverlayContext";
50
59
  import { useChartPaths } from "../hooks/useChartPaths";
51
60
  import { useChartReveal } from "../hooks/useChartReveal";
52
61
  import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
@@ -55,9 +64,13 @@ import { useDegen } from "../hooks/useDegen";
55
64
  import { useLiveChartHasData } from "../hooks/useLiveChartHasData";
56
65
  import { useLiveDot } from "../hooks/useLiveDot";
57
66
  import { useMarkers } from "../hooks/useMarkers";
67
+ import { useReferenceDrag } from "../hooks/useReferenceDrag";
58
68
  import { useReferenceLinePress } from "../hooks/useReferenceLinePress";
59
69
  import { useModeBlend } from "../hooks/useModeBlend";
60
70
  import { useMomentum } from "../hooks/useMomentum";
71
+ import { AXIS_GRAB_MIN_PX, usePanScroll } from "../hooks/usePanScroll";
72
+ import { usePinchZoom } from "../hooks/usePinchZoom";
73
+ import { useVisibleRange } from "../hooks/useVisibleRange";
61
74
  import { useSegmentLineGradient } from "../hooks/useSegmentLineGradient";
62
75
  import { useSingleChartReverseMorphInputs } from "../hooks/useReverseMorphEngineInputs";
63
76
  import { useThreshold } from "../hooks/useThreshold";
@@ -69,7 +82,16 @@ import {
69
82
  formatValue as defaultFormatValue,
70
83
  } from "../lib/format";
71
84
  import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
72
- import { collectReferenceValues } from "../math/referenceLines";
85
+ import { computeScrubDotY } from "../hooks/crosshairShared";
86
+ import {
87
+ groupReferenceLines,
88
+ type ReferenceGrouping,
89
+ } from "../math/referenceGroup";
90
+ import {
91
+ collectReferenceValues,
92
+ referenceLineForm,
93
+ resolveReferenceGroupBadge,
94
+ } from "../math/referenceLines";
73
95
  import {
74
96
  applyPaletteOverride,
75
97
  leftEdgeFadeColorsFromBgRgb,
@@ -94,8 +116,13 @@ import {
94
116
  labelConnector,
95
117
  } from "./ExtremaConnectorOverlay";
96
118
  import { CustomMarkerOverlay } from "./CustomMarkerOverlay";
119
+ import {
120
+ CustomReferenceLineOverlay,
121
+ customReferenceLineFlags,
122
+ } from "./CustomReferenceLineOverlay";
97
123
  import { CustomTooltipOverlay } from "./CustomTooltipOverlay";
98
124
  import { BadgeOverlay } from "./BadgeOverlay";
125
+ import { ChartOverlayLayer } from "./ChartOverlayLayer";
99
126
  import { CrosshairOverlay } from "./CrosshairOverlay";
100
127
  import { DegenParticlesOverlay } from "./DegenParticlesOverlay";
101
128
  import { DotOverlay } from "./DotOverlay";
@@ -104,6 +131,7 @@ import { LoadingOverlay } from "./LoadingOverlay";
104
131
  import { MarkerOverlay } from "./MarkerOverlay";
105
132
  import { MultiSeriesTooltipStack } from "./MultiSeriesTooltipStack";
106
133
  import { ValueTextOverlay } from "./ValueTextOverlay";
134
+ import { ReferenceLineGroupOverlay } from "./ReferenceLineGroupOverlay";
107
135
  import { ReferenceLineOverlay } from "./ReferenceLineOverlay";
108
136
  import { ScrubActionOverlay } from "./ScrubActionOverlay";
109
137
  import { SegmentDividerOverlay } from "./SegmentDividerOverlay";
@@ -115,6 +143,14 @@ import { YAxisOverlay } from "./YAxisOverlay";
115
143
  /** Translucent fill alpha for the threshold profit/loss band. */
116
144
  const THRESHOLD_FILL_OPACITY = 0.16;
117
145
 
146
+ /** Stable empty grouping result (identity-stable so downstream worklets don't
147
+ * re-run) used when reference-line grouping is off. */
148
+ const EMPTY_GROUPING: ReferenceGrouping = { hidden: [], groups: [] };
149
+
150
+ /** Stable empty number array so the live-reference-values worklet stays
151
+ * referentially stable (no engine re-fit) when no line is draggable. */
152
+ const EMPTY_NUMS: number[] = [];
153
+
118
154
  /**
119
155
  * Color stops for the threshold's hard-split vertical gradient. Both arrays pair
120
156
  * with the `[0, t, t, 1]` split positions: index 0–1 paint above the split,
@@ -144,6 +180,7 @@ function thresholdStops(
144
180
  * here so the rendered pieces (`ChartStack`, `ChartScrubLayer`, `LiveChart`) stay
145
181
  * small and presentational.
146
182
  */
183
+
147
184
  function useLiveChartController({
148
185
  // ── Data ────────────────────────────────────────────────────────────────
149
186
  data,
@@ -164,6 +201,7 @@ function useLiveChartController({
164
201
  candles,
165
202
  candleWidth = 60,
166
203
  liveCandle,
204
+ volume,
167
205
 
168
206
  // ── Behaviour ───────────────────────────────────────────────────────────
169
207
  timeWindow = 30,
@@ -177,6 +215,8 @@ function useLiveChartController({
177
215
  maxValue,
178
216
  windowBuffer = 0,
179
217
  nowOverride,
218
+ timeScroll = false,
219
+ zoom = false,
180
220
  accessibilityLabel,
181
221
  accessibilityRole = "image",
182
222
  emptyText = "No data",
@@ -207,10 +247,14 @@ function useLiveChartController({
207
247
  tradeStream,
208
248
  degen,
209
249
  markers,
210
- onMarkerHover,
250
+ onMarkerPress,
211
251
  markerHitRadius = 16,
252
+ markerCluster,
212
253
  renderMarker,
213
254
  renderTooltip,
255
+ renderOverlay,
256
+ renderReferenceLine,
257
+ referenceLineGrouping,
214
258
  leftEdgeFade = true,
215
259
 
216
260
  // ── Callbacks ───────────────────────────────────────────────────────────
@@ -219,6 +263,8 @@ function useLiveChartController({
219
263
  onReferenceLinePress,
220
264
  onGestureStart,
221
265
  onGestureEnd,
266
+ onVisibleRangeChange,
267
+ onReachStart,
222
268
  onDegenShake,
223
269
  }: LiveChartProps) {
224
270
  const emptyTradeStream = useSharedValue<TradeEvent[]>([]);
@@ -239,6 +285,10 @@ function useLiveChartController({
239
285
  const scrubCfg = resolveScrub(scrub);
240
286
  // Static charts run no gestures, so scrub-action (tap/drag to lock a price) is off.
241
287
  const scrubActionCfg = isStatic ? null : resolveScrubAction(scrubAction);
288
+ // Volume bars sit below the candles — a candle-mode-only feature (inert in
289
+ // line mode, like the candle paths themselves).
290
+ const volumeCfg = isCandle ? resolveVolume(volume) : null;
291
+ const volumeBandHeight = volumeCfg?.maxHeight ?? 0;
242
292
  const gradientCfg = isCandle ? null : resolveGradient(gradient);
243
293
  // Dot-lattice area fill (clipped to the under-line region). Inert in candle
244
294
  // mode, same as the gradient fill.
@@ -264,6 +314,77 @@ function useLiveChartController({
264
314
  const allRefLines = referenceLines ?? [];
265
315
  const refValues = collectReferenceValues(allRefLines);
266
316
 
317
+ // Per-line live value overrides + drag flags for draggable lines and the custom
318
+ // `renderReferenceLine` slot. One array SharedValue each (the line count varies,
319
+ // so a single SharedValue beats N hooks). Seeded from each line's static `value`;
320
+ // the drag gesture overwrites a slot, and the effect re-seeds slots not being
321
+ // dragged when the props change (so a controlled `value` flows back in).
322
+ const dragValues = useSharedValue<number[]>([]);
323
+ const dragActive = useSharedValue<boolean[]>([]);
324
+ // Last `value` props used to seed each slot, so reconciliation can tell a
325
+ // controlled prop change (adopt it) from an unchanged prop (keep the dragged
326
+ // value — uncontrolled persistence) without resetting a drop on every re-render.
327
+ const seededRef = useRef<(number | undefined)[]>([]);
328
+ const refValueSig = allRefLines.map((l) => l.value ?? "_").join(",");
329
+ useEffect(() => {
330
+ const active = dragActive.value;
331
+ const cur = dragValues.value;
332
+ const seeded = seededRef.current;
333
+ dragValues.value = allRefLines.map((l, i) => {
334
+ const prop = l.value ?? 0;
335
+ if (active[i]) return cur[i] ?? prop; // mid-drag → keep the dragged value
336
+ if (l.value !== seeded[i]) return prop; // prop changed → adopt (controlled)
337
+ return cur[i] ?? prop; // unchanged → keep current (uncontrolled persist)
338
+ });
339
+ seededRef.current = allRefLines.map((l) => l.value);
340
+ if (dragActive.value.length !== allRefLines.length) {
341
+ dragActive.value = allRefLines.map((_, i) => active[i] ?? false);
342
+ }
343
+ // allRefLines is rebuilt every render; key off the value signature + length.
344
+ // eslint-disable-next-line react-hooks/exhaustive-deps
345
+ }, [refValueSig, allRefLines.length]);
346
+
347
+ // Form-A lines a custom `renderReferenceLine` owns → suppress their built-in tag
348
+ // (no double-draw). Probed on the JS thread, index-aligned with `allRefLines`.
349
+ const refLineCustom = customReferenceLineFlags(allRefLines, renderReferenceLine);
350
+
351
+ // Live Y values of the *draggable* Form-A lines, folded into the engine's
352
+ // axis-range fit so dragging a line toward / past the visible edge expands the
353
+ // range and the axis follows the finger in one motion (the committed values are
354
+ // already in `refValues`). `excludeFromRange` lines opt out, matching the static
355
+ // fit. Identity-stable (no re-fit) when nothing is draggable.
356
+ const draggableRefIdx = allRefLines
357
+ .map((l, i) =>
358
+ l.draggable && !l.excludeFromRange && referenceLineForm(l) === "line"
359
+ ? i
360
+ : -1,
361
+ )
362
+ .filter((i) => i >= 0);
363
+ const liveRefValues = useDerivedValue<number[]>(() => {
364
+ if (draggableRefIdx.length === 0) return EMPTY_NUMS;
365
+ const dv = dragValues.get();
366
+ const out: number[] = [];
367
+ for (let k = 0; k < draggableRefIdx.length; k++) {
368
+ const v = dv[draggableRefIdx[k]];
369
+ if (v != null) out.push(v);
370
+ }
371
+ return out;
372
+ });
373
+
374
+ // Reference-line grouping (collapse near-value handles). Resolved once; the
375
+ // per-frame clustering runs on the UI thread (see ReferenceLineGroupOverlay).
376
+ const refGroupingCfg =
377
+ typeof referenceLineGrouping === "object"
378
+ ? referenceLineGrouping
379
+ : undefined;
380
+ const refGroupingRadius = referenceLineGrouping
381
+ ? (refGroupingCfg?.radius ?? 18)
382
+ : null;
383
+ // Count-pill styling (same style/shape config as a per-line badge) + count
384
+ // formatter. Resolved once; theme color defaults are applied in the overlay.
385
+ const refGroupBadge = resolveReferenceGroupBadge(refGroupingCfg?.badge);
386
+ const refGroupFormat = refGroupingCfg?.format;
387
+
267
388
  const badgeUsesRightGutter =
268
389
  badgeCfg !== null && (badgeCfg.position ?? "right") === "right";
269
390
 
@@ -305,6 +426,47 @@ function useLiveChartController({
305
426
  palette.valueFontSize * 2,
306
427
  );
307
428
 
429
+ // Per-badge font (size/family/weight) override; reuses the chart font when
430
+ // none of the badge font knobs are set, so the gutter sizing is unchanged.
431
+ const badgeHasFontOverride =
432
+ badgeCfg?.fontSize != null ||
433
+ badgeCfg?.fontFamily != null ||
434
+ badgeCfg?.fontWeight != null;
435
+ const badgeFontOverride = useChartSkiaFont(
436
+ badgeHasFontOverride
437
+ ? {
438
+ ...fontProp,
439
+ fontFamily: badgeCfg?.fontFamily ?? fontProp?.fontFamily,
440
+ fontSize: badgeCfg?.fontSize ?? fontProp?.fontSize,
441
+ fontWeight: badgeCfg?.fontWeight ?? fontProp?.fontWeight,
442
+ }
443
+ : fontProp,
444
+ MONO_FONT_FAMILY,
445
+ palette.labelFontSize,
446
+ );
447
+ const badgeFont = badgeHasFontOverride ? badgeFontOverride : skiaFont;
448
+
449
+ // Per-badge font for the grouping count pill (same override pattern as above).
450
+ const refGroupBadgeHasFont =
451
+ refGroupBadge.fontSize != null ||
452
+ refGroupBadge.fontFamily != null ||
453
+ refGroupBadge.fontWeight != null;
454
+ const refGroupBadgeFontOverride = useChartSkiaFont(
455
+ refGroupBadgeHasFont
456
+ ? {
457
+ ...fontProp,
458
+ fontFamily: refGroupBadge.fontFamily ?? fontProp?.fontFamily,
459
+ fontSize: refGroupBadge.fontSize ?? fontProp?.fontSize,
460
+ fontWeight: refGroupBadge.fontWeight ?? fontProp?.fontWeight,
461
+ }
462
+ : fontProp,
463
+ MONO_FONT_FAMILY,
464
+ palette.labelFontSize,
465
+ );
466
+ const refGroupBadgeFont = refGroupBadgeHasFont
467
+ ? refGroupBadgeFontOverride
468
+ : skiaFont;
469
+
308
470
  const pulseConfig = pulseCfg
309
471
  ? {
310
472
  maxRadius: pulseCfg.maxRadius,
@@ -328,11 +490,26 @@ function useLiveChartController({
328
490
  setValueLayoutSample(value.get());
329
491
  }, [value]);
330
492
 
493
+ // `scrolledBack` mirrors the UI-thread scroll state (engine.viewEnd != null)
494
+ // onto the JS thread (set by the reaction below, after the engine exists).
495
+ const [scrolledBack, setScrolledBack] = useState(false);
496
+ const timeScrollEnabled = Boolean(timeScroll) && !isStatic;
497
+ const zoomCfg = resolveZoom(zoom);
498
+ const zoomEnabled = zoomCfg !== null && !isStatic;
499
+
500
+ const yAxisFloat = yAxisCfg?.float ?? false;
501
+ // With timeScroll, the floating full-width plot engages only while scrolled
502
+ // back; at the live edge the chart keeps a normal right gutter so the
503
+ // line/candles don't sit under the floating y-axis labels + badge. Without
504
+ // timeScroll, float behaves as before (always full-width).
505
+ const effectiveYAxisFloat =
506
+ yAxisFloat && (!timeScrollEnabled || scrolledBack);
331
507
  const { strokeWidth, padding: effectivePadding } = resolveChartLayout({
332
508
  palette,
333
509
  lineWidthOverride: lineProp?.width,
334
510
  insetsOverride: insets,
335
511
  yAxis: yAxisCfg !== null,
512
+ yAxisFloat: effectiveYAxisFloat,
336
513
  badge: badgeCfg !== null,
337
514
  badgeMetrics: metricsCfg.badge,
338
515
  badgeUsesRightGutter,
@@ -342,6 +519,7 @@ function useLiveChartController({
342
519
  formatValue,
343
520
  currentValue: valueLayoutSample,
344
521
  pulse: pulseConfig,
522
+ volumeBandHeight,
345
523
  });
346
524
 
347
525
  // ── Reveal state ────────────────────────────────────────────
@@ -378,6 +556,7 @@ function useLiveChartController({
378
556
  adaptiveSpeedBoost: metricsCfg.motion.adaptiveSpeedBoost,
379
557
  exaggerate,
380
558
  referenceValues: refValues,
559
+ liveReferenceValues: liveRefValues,
381
560
  nonNegative,
382
561
  maxValue,
383
562
  windowBuffer,
@@ -387,6 +566,19 @@ function useLiveChartController({
387
566
  liveCandle: isCandle ? liveEngine : liveCandle,
388
567
  });
389
568
 
569
+ // Mirror the UI-thread scroll state to React so the floating y-axis can keep
570
+ // a right gutter at the live edge and collapse it only while scrolled back.
571
+ // Fires once per null↔frozen transition, and only matters for float+timeScroll.
572
+ useAnimatedReaction(
573
+ () => engine.viewEnd.value != null,
574
+ /* istanbul ignore next -- Reanimated reaction; state mirrored on the JS thread, not exercised under Jest */
575
+ (isScrolled, prev) => {
576
+ if (isScrolled !== prev && yAxisFloat && timeScrollEnabled) {
577
+ scheduleOnRN(setScrolledBack, isScrolled);
578
+ }
579
+ },
580
+ );
581
+
390
582
  // ── Mode crossfade (line ↔ candle) ──────────────────────────────────
391
583
  const { lineGroupOpacity, candleGroupOpacity } = useModeBlend(
392
584
  isCandle,
@@ -396,6 +588,41 @@ function useLiveChartController({
396
588
  // ── Per-frame derived values ───────────────────────────────────────────
397
589
  const { layoutWidth, layoutHeight, onLayout } = useCanvasLayout(engine);
398
590
 
591
+ // Reference-line grouping: cluster Form-A lines by their per-frame value-Y so a
592
+ // stack of nearby orders collapses into one count handle. `groupHidden` suppresses
593
+ // the clustered lines' individual tags; the count pills read `refGroupResult`.
594
+ // Identity-stable (no work) when grouping is off.
595
+ const refGroupResult = useDerivedValue<ReferenceGrouping>(() => {
596
+ if (refGroupingRadius == null) return EMPTY_GROUPING;
597
+ const ch = engine.canvasHeight.get();
598
+ const dMin = engine.displayMin.get();
599
+ const dMax = engine.displayMax.get();
600
+ const top = effectivePadding.top;
601
+ const bottom = ch - effectivePadding.bottom;
602
+ const ys: number[] = [];
603
+ for (let i = 0; i < allRefLines.length; i++) {
604
+ const l = allRefLines[i];
605
+ // Skip bands and lines a custom `renderReferenceLine` owns — a custom tag
606
+ // draws itself and isn't suppressed by grouping, so folding it into a
607
+ // built-in count pill would double-count it (counted *and* still shown).
608
+ if (
609
+ referenceLineForm(l) !== "line" ||
610
+ l.value === undefined ||
611
+ refLineCustom[i]
612
+ ) {
613
+ ys.push(-1);
614
+ continue;
615
+ }
616
+ const v = dragValues.get()[i] ?? l.value;
617
+ const y = computeScrubDotY(v, dMin, dMax, ch, top, effectivePadding.bottom);
618
+ ys.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
619
+ }
620
+ return groupReferenceLines(ys, refGroupingRadius);
621
+ });
622
+ const groupHidden = useDerivedValue<boolean[]>(
623
+ () => refGroupResult.get().hidden,
624
+ );
625
+
399
626
  // Threshold split geometry (shared by stroke gradient, fill band, marker line).
400
627
  // Called unconditionally with a stand-in value when no threshold is set.
401
628
  const thresholdGeom = useThreshold(
@@ -419,6 +646,11 @@ function useLiveChartController({
419
646
  // Only build the band path when the fill is actually on.
420
647
  thresholdCfg?.fill ? thresholdGeom.lineY : undefined,
421
648
  lineIsLinear,
649
+ // Tip the line at the view-edge price (not the live value) while scrolled,
650
+ // matching the live dot — so the right edge doesn't drop to the off-screen
651
+ // live value when `followViewEdge` tracks the scrolled-back window.
652
+ engine.edgeValue,
653
+ badgeCfg?.followViewEdge ?? false,
422
654
  );
423
655
 
424
656
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -436,19 +668,36 @@ function useLiveChartController({
436
668
  adA * (areaDotsCfg?.opacity ?? 1),
437
669
  ];
438
670
 
439
- const { upBodiesPath, downBodiesPath, upWicksPath, downWicksPath } =
440
- useCandlePaths(
441
- engine,
442
- effectivePadding,
443
- // Match engine: stashed candles while reverse-morphing in candle mode.
444
- isCandle ? candlesEngine : candles,
445
- isCandle ? liveEngine : liveCandle,
446
- candleWidth,
447
- isCandle,
448
- metricsCfg.candle,
449
- !isStatic, // static: no candle-width lerp loop
450
- );
451
- const { dotX, dotY } = useLiveDot(engine, effectivePadding);
671
+ const {
672
+ upBodiesPath,
673
+ downBodiesPath,
674
+ upWicksPath,
675
+ downWicksPath,
676
+ upBarsPath,
677
+ downBarsPath,
678
+ } = useCandlePaths(
679
+ engine,
680
+ effectivePadding,
681
+ // Match engine: stashed candles while reverse-morphing in candle mode.
682
+ isCandle ? candlesEngine : candles,
683
+ isCandle ? liveEngine : liveCandle,
684
+ candleWidth,
685
+ isCandle,
686
+ metricsCfg.candle,
687
+ volumeBandHeight,
688
+ volumeCfg?.radius ?? 0,
689
+ !isStatic, // static: no candle-width lerp loop
690
+ );
691
+ const { dotX, dotY } = useLiveDot(
692
+ engine,
693
+ effectivePadding,
694
+ engine.edgeValue,
695
+ badgeCfg?.followViewEdge ?? false,
696
+ );
697
+
698
+ // Price↔pixel / time↔pixel bridge for a custom `renderOverlay`. Built
699
+ // unconditionally (hooks rule); only mounted when `renderOverlay` is provided.
700
+ const overlayContext = useChartOverlayContext(engine, effectivePadding);
452
701
 
453
702
  const momentumSV = useMomentum(engine, momentum);
454
703
 
@@ -474,6 +723,7 @@ function useLiveChartController({
474
723
  skiaFont,
475
724
  yAxisCfg?.minGap ?? 36,
476
725
  metricsCfg.grid,
726
+ yAxisCfg?.count ?? 0,
477
727
  );
478
728
 
479
729
  const { xAxisEntries } = useXAxis(
@@ -488,7 +738,7 @@ function useLiveChartController({
488
738
  effectivePadding,
489
739
  palette,
490
740
  formatValue,
491
- skiaFont,
741
+ badgeFont,
492
742
  badgeCfg?.variant ?? "default",
493
743
  badgeCfg?.tail ?? true,
494
744
  momentumSV,
@@ -496,6 +746,11 @@ function useLiveChartController({
496
746
  badgeCfg?.background,
497
747
  metricsCfg.badge,
498
748
  metricsCfg.motion.badgeColorSpeed,
749
+ effectiveYAxisFloat,
750
+ engine.edgeValue,
751
+ badgeCfg?.followViewEdge ?? false,
752
+ badgeCfg?.radius,
753
+ badgeCfg?.textColor,
499
754
  );
500
755
 
501
756
  // Scrub/crosshair must see the same stash-backed candles as the engine.
@@ -509,6 +764,7 @@ function useLiveChartController({
509
764
  : undefined;
510
765
 
511
766
  const markersActive = markers != null;
767
+ const markerClusterCfg = resolveMarkerCluster(markerCluster);
512
768
  // `projected` is used internally by the hit-test gesture; the overlay
513
769
  // self-projects, so we only need the gesture + hit-test here. Built BEFORE
514
770
  // `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
@@ -518,11 +774,12 @@ function useLiveChartController({
518
774
  markersSV,
519
775
  !isStatic && markersActive,
520
776
  markerHitRadius,
521
- onMarkerHover,
777
+ onMarkerPress,
522
778
  undefined, // seriesSV — single-series has none
523
779
  engine.data, // anchor value-less markers to the line
524
780
  !isStatic, // static: no marker-projection loop
525
781
  lineIsLinear, // match marker anchoring to the rendered curve
782
+ markerClusterCfg, // co-located marker stacking / collapse
526
783
  );
527
784
 
528
785
  // Pressable reference-line badges (working orders / alerts). Built before
@@ -538,6 +795,7 @@ function useLiveChartController({
538
795
  !isStatic && refPressActive,
539
796
  markerHitRadius,
540
797
  onReferenceLinePress,
798
+ dragValues,
541
799
  );
542
800
 
543
801
  // Combined "defer" hit-test for the scrub-action place-tap: yield to a marker or
@@ -549,6 +807,24 @@ function useLiveChartController({
549
807
  return markerHitTest(x, y) || refLineHitTest(x, y);
550
808
  };
551
809
 
810
+ // Time-scroll activation. `holdToScrub`: a quick one-finger drag scrolls while
811
+ // scrub engages on press-and-hold — so the scrub gesture needs a long-press
812
+ // delay (unless the caller set its own `panGestureDelay`). `timeScrollEnabled`
813
+ // is computed earlier (it gates the float gutter).
814
+ const scrollGestureMode =
815
+ typeof timeScroll === "object"
816
+ ? (timeScroll.gesture ?? "holdToScrub")
817
+ : "holdToScrub";
818
+ // In holdToScrub the scrub MUST require a hold so a quick drag scrolls instead.
819
+ // Precedence: explicit timeScroll.scrubHoldMs, then scrub.panGestureDelay, then
820
+ // the default. `||` (not `??`) skips the resolved panGestureDelay's 0 default.
821
+ const timeScrollHoldMs =
822
+ typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
823
+ const scrubHoldMs =
824
+ timeScrollEnabled && scrollGestureMode === "holdToScrub"
825
+ ? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
826
+ : (scrubCfg?.panGestureDelay ?? 0);
827
+
552
828
  const crosshair = useCrosshair(
553
829
  engine,
554
830
  effectivePadding,
@@ -561,7 +837,7 @@ function useLiveChartController({
561
837
  !isStatic && (scrubCfg !== null || scrubActionCfg !== null),
562
838
  onScrub,
563
839
  candleOpts,
564
- scrubCfg?.panGestureDelay ?? 0,
840
+ scrubHoldMs,
565
841
  onGestureStart,
566
842
  onGestureEnd,
567
843
  scrubActionCfg,
@@ -572,8 +848,75 @@ function useLiveChartController({
572
848
  scrubCfg?.tooltipShowValue ?? true,
573
849
  scrubCfg?.tooltipShowTime ?? true,
574
850
  scrubCfg?.tooltipMargin ?? 8,
851
+ // Axis-drag time-scroll: keep the bottom "time ruler" band scroll-only so a
852
+ // drag there never trips the scrub crosshair.
853
+ timeScrollEnabled && scrollGestureMode === "axisDrag"
854
+ ? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
855
+ : 0,
575
856
  );
576
857
 
858
+ // ── Time-scroll (drag back through history) ───────────────────────────────
859
+ // Experimental: a pan freezes the window at an absolute time and resumes
860
+ // following once dragged back to the live edge. Pan is clamped to the earliest
861
+ // retained point (line or candle). See `timeScroll` for the gesture model.
862
+ const scrollMinTime = useDerivedValue(() => {
863
+ const src = isCandle ? candlesEngine.get() : lineEngineData.get();
864
+ return src.length > 0 ? src[0].time : engine.liveEdge.get();
865
+ });
866
+ const panScrollGesture = usePanScroll({
867
+ engine,
868
+ padding: effectivePadding,
869
+ minTime: scrollMinTime,
870
+ enabled: timeScrollEnabled,
871
+ mode: scrollGestureMode,
872
+ // Clear any live crosshair when a scroll drag takes over.
873
+ onScrollStart: () => {
874
+ "worklet";
875
+ crosshair.scrubActive.set(false);
876
+ },
877
+ });
878
+
879
+ // Draggable reference lines: a per-line vertical pan that grabs a line near its
880
+ // value and drags it along the Y-axis (with snap / bounds / callbacks). Built
881
+ // unconditionally for stable hook order; the gesture self-disables when no line
882
+ // opts in, and it's only composed into the root when `refDragEnabled`.
883
+ const refDragEnabled =
884
+ !isStatic && allRefLines.some((l) => l.draggable === true);
885
+ const refDragGesture = useReferenceDrag(
886
+ engine,
887
+ effectivePadding,
888
+ allRefLines,
889
+ dragValues,
890
+ dragActive,
891
+ !isStatic,
892
+ );
893
+
894
+ // Pinch-to-zoom the visible window (two-finger). Anchors at the focal point and
895
+ // writes viewWindow + viewEnd; composes via Simultaneous (it's two-finger, so
896
+ // disjoint from the one-finger pan/scrub). See `zoom`.
897
+ const pinchZoomGesture = usePinchZoom({
898
+ engine,
899
+ padding: effectivePadding,
900
+ minTime: scrollMinTime,
901
+ timeWindow,
902
+ enabled: zoomEnabled,
903
+ minTimeWindow: zoomCfg?.minTimeWindow,
904
+ maxTimeWindow: zoomCfg?.maxTimeWindow,
905
+ onZoomStart: () => {
906
+ "worklet";
907
+ crosshair.scrubActive.set(false);
908
+ },
909
+ });
910
+
911
+ // Paging callbacks: report the visible range / proximity to the oldest data so
912
+ // a host can lazily load history. Inert unless a callback is supplied.
913
+ useVisibleRange({
914
+ engine,
915
+ minTime: scrollMinTime,
916
+ onVisibleRangeChange,
917
+ onReachStart,
918
+ });
919
+
577
920
  // Scrub-action composes a Tap (place/move the reticle, press the badge, dismiss)
578
921
  // ahead of the pan via Exclusive, so a tap is tried first and only becomes a
579
922
  // drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
@@ -599,12 +942,37 @@ function useLiveChartController({
599
942
  overlayTaps.length === 1
600
943
  ? overlayTaps[0]
601
944
  : Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
602
- // With scrub-action the action tap shares the gesture space with the overlay
603
- // taps (Simultaneous); without it the pan only needs to race the tap group.
945
+ // Always `Simultaneous`, never `Race`: on iOS the scrub pan uses
946
+ // `minDistance(0)`, so in a `Race` it activates on touch-down and cancels the
947
+ // overlay tap before it can recognize — `onMarkerPress`/`onReferenceLinePress`
948
+ // would never fire. Sharing the gesture space lets the tap recognize; the pan
949
+ // defers to a marker/badge under the finger via `deferTapHit` (see
950
+ // `useCrosshair`'s scrub `onStart`) so no stray crosshair is dropped there.
951
+ rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
952
+ }
953
+
954
+ // Compose the pan-scroll gesture. holdToScrub races the scrub/tap gestures (a
955
+ // quick drag scrolls; a still press-hold falls through to scrub). Axis-drag
956
+ // goes first via Exclusive: it fails instantly outside the axis band, so scrub
957
+ // runs everywhere else.
958
+ if (timeScrollEnabled) {
604
959
  rootGesture =
605
- scrubActionCfg !== null
606
- ? Gesture.Simultaneous(baseGesture, tapGroup)
607
- : Gesture.Race(baseGesture, tapGroup);
960
+ scrollGestureMode === "axisDrag"
961
+ ? Gesture.Exclusive(panScrollGesture, rootGesture)
962
+ : Gesture.Race(panScrollGesture, rootGesture);
963
+ }
964
+
965
+ // Draggable reference lines take priority: a vertical grab on a line drags it.
966
+ // The manual-activation pan fails fast off any line (or on horizontal intent), so
967
+ // Exclusive falls through to scrub / scroll everywhere else.
968
+ if (refDragEnabled) {
969
+ rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
970
+ }
971
+
972
+ // Pinch runs alongside everything else (two-finger, so it never competes with
973
+ // the one-finger pan/scrub/tap gestures for the same touch).
974
+ if (zoomEnabled) {
975
+ rootGesture = Gesture.Simultaneous(rootGesture, pinchZoomGesture);
608
976
  }
609
977
 
610
978
  // ── Derived render values ──────────────────────────────────────────────
@@ -662,6 +1030,7 @@ function useLiveChartController({
662
1030
  extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
663
1031
  // configs
664
1032
  yAxisCfg,
1033
+ yAxisFloat: effectiveYAxisFloat,
665
1034
  xAxisCfg,
666
1035
  badgeCfg,
667
1036
  scrubCfg,
@@ -679,6 +1048,16 @@ function useLiveChartController({
679
1048
  leftEdgeFadeCfg,
680
1049
  metricsCfg,
681
1050
  allRefLines,
1051
+ refLineCustom,
1052
+ dragValues,
1053
+ dragActive,
1054
+ renderReferenceLine,
1055
+ refGroupingActive: refGroupingRadius != null,
1056
+ refGroupResult,
1057
+ groupHidden,
1058
+ refGroupBadge,
1059
+ refGroupBadgeFont,
1060
+ refGroupFormat,
682
1061
  resolvedSegments,
683
1062
  hasRecolorSegments,
684
1063
  segmentGradient,
@@ -690,7 +1069,9 @@ function useLiveChartController({
690
1069
  // theme / layout / fonts
691
1070
  palette,
692
1071
  skiaFont,
1072
+ fontProp,
693
1073
  valueFont,
1074
+ badgeFont,
694
1075
  strokeWidth,
695
1076
  effectivePadding,
696
1077
  // engine + reveal
@@ -715,6 +1096,15 @@ function useLiveChartController({
715
1096
  downBodiesPath,
716
1097
  upWicksPath,
717
1098
  downWicksPath,
1099
+ upBarsPath,
1100
+ downBarsPath,
1101
+ // Volume bars: active flag, fade-in opacity, and resolved colors (default to
1102
+ // the candle palette). The reserved band height is read by the x-axis.
1103
+ volumeActive: volumeCfg !== null,
1104
+ volumeBandHeight,
1105
+ volumeOpacity: volumeCfg?.opacity ?? 1,
1106
+ volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
1107
+ volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
718
1108
  dotX,
719
1109
  dotY,
720
1110
  liveDotOpacity,
@@ -730,8 +1120,11 @@ function useLiveChartController({
730
1120
  rootGesture,
731
1121
  markersActive,
732
1122
  markersSV,
1123
+ markerClusterCfg,
733
1124
  renderMarker,
734
1125
  renderTooltip,
1126
+ renderOverlay,
1127
+ overlayContext,
735
1128
  // selection dot: resolved config + fallback color (the chart line/accent color)
736
1129
  selectionDot: selectionDotCfg,
737
1130
  selectionColor: lineProp?.color ?? palette.line,
@@ -756,6 +1149,7 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
756
1149
  const {
757
1150
  degenShakeTransform,
758
1151
  yAxisCfg,
1152
+ yAxisFloat,
759
1153
  reveal,
760
1154
  yAxisEntries,
761
1155
  engine,
@@ -781,10 +1175,13 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
781
1175
 
782
1176
  return (
783
1177
  <Group transform={degenShakeTransform}>
784
- {/* Y-axis grid */}
1178
+ {/* Y-axis. Default: grid + labels here (in a reserved gutter). Floating
1179
+ mode: grid only — the labels + a soft edge fade draw above the candles
1180
+ in ChartStack so the plot runs full-width and candles dim under them. */}
785
1181
  {yAxisCfg && (
786
1182
  <Group opacity={reveal.yAxisOpacity}>
787
1183
  <YAxisOverlay
1184
+ variant={yAxisFloat ? "grid" : "all"}
788
1185
  entries={yAxisEntries}
789
1186
  engine={engine}
790
1187
  padding={effectivePadding}
@@ -857,10 +1254,12 @@ function ChartStack({ model }: { model: LiveChartModel }) {
857
1254
  effectivePadding,
858
1255
  palette,
859
1256
  skiaFont,
1257
+ fontProp,
860
1258
  badgeCfg,
861
1259
  valueLineCfg,
862
1260
  dotY,
863
1261
  allRefLines,
1262
+ dragValues,
864
1263
  resolvedSegments,
865
1264
  hasRecolorSegments,
866
1265
  segmentGradient,
@@ -878,6 +1277,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
878
1277
  downWicksPath,
879
1278
  upBodiesPath,
880
1279
  downBodiesPath,
1280
+ upBarsPath,
1281
+ downBarsPath,
1282
+ volumeActive,
1283
+ volumeBandHeight,
1284
+ volumeOpacity,
1285
+ volumeUpColor,
1286
+ volumeDownColor,
881
1287
  xAxisCfg,
882
1288
  xAxisEntries,
883
1289
  dotX,
@@ -889,10 +1295,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
889
1295
  degenPackRevision,
890
1296
  markersActive,
891
1297
  markersSV,
1298
+ markerClusterCfg,
892
1299
  renderMarker,
893
1300
  emptyText,
894
1301
  metricsCfg,
895
1302
  layoutWidth,
1303
+ yAxisCfg,
1304
+ yAxisFloat,
1305
+ yAxisEntries,
1306
+ badgeUsesRightGutter,
1307
+ gridStyleCfg,
896
1308
  } = model;
897
1309
 
898
1310
  return (
@@ -935,6 +1347,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
935
1347
  palette={palette}
936
1348
  formatValue={formatValue}
937
1349
  font={skiaFont}
1350
+ fontProp={fontProp}
1351
+ dragValues={dragValues}
1352
+ index={i}
938
1353
  />
939
1354
  ))}
940
1355
 
@@ -997,13 +1412,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
997
1412
  <Path
998
1413
  path={upWicksPath}
999
1414
  style="stroke"
1000
- strokeWidth={1}
1415
+ strokeWidth={metricsCfg.candle.wickWidth}
1001
1416
  color={palette.wickUp}
1002
1417
  />
1003
1418
  <Path
1004
1419
  path={downWicksPath}
1005
1420
  style="stroke"
1006
- strokeWidth={1}
1421
+ strokeWidth={metricsCfg.candle.wickWidth}
1007
1422
  color={palette.wickDown}
1008
1423
  />
1009
1424
  <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
@@ -1014,7 +1429,41 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1014
1429
  />
1015
1430
  </Group>
1016
1431
 
1017
- {/* X-axis time labels */}
1432
+ {/* Volume bars in the reserved band below the candles. Fades in with the
1433
+ candle group (outer opacity); the config opacity dims the whole band
1434
+ (inner). Up/down bars carry their own colors (default candle palette). */}
1435
+ {volumeActive && (
1436
+ <Group opacity={candleGroupOpacity}>
1437
+ <Group opacity={volumeOpacity}>
1438
+ <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1439
+ <Path path={downBarsPath} style="fill" color={volumeDownColor} />
1440
+ </Group>
1441
+ </Group>
1442
+ )}
1443
+
1444
+ {/* Floating axis: the labels float ABOVE the candles (right-aligned at the
1445
+ edge) so the plot runs full-width and candles stay fully visible behind
1446
+ them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
1447
+ {yAxisCfg && yAxisFloat && (
1448
+ <Group opacity={reveal.yAxisOpacity}>
1449
+ <YAxisOverlay
1450
+ variant="labels"
1451
+ float
1452
+ entries={yAxisEntries}
1453
+ engine={engine}
1454
+ padding={effectivePadding}
1455
+ palette={palette}
1456
+ font={skiaFont}
1457
+ badge={badgeUsesRightGutter}
1458
+ badgeTail={badgeCfg?.tail ?? true}
1459
+ badgeMetrics={metricsCfg.badge}
1460
+ gridStyle={gridStyleCfg}
1461
+ />
1462
+ </Group>
1463
+ )}
1464
+
1465
+ {/* X-axis time labels. With a volume band the bottom padding is inflated by
1466
+ the band height; pass it so the axis shifts back to the very bottom. */}
1018
1467
  {xAxisCfg && (
1019
1468
  <XAxisOverlay
1020
1469
  entries={xAxisEntries}
@@ -1022,6 +1471,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1022
1471
  padding={effectivePadding}
1023
1472
  palette={palette}
1024
1473
  font={skiaFont}
1474
+ volumeBandHeight={volumeBandHeight}
1025
1475
  />
1026
1476
  )}
1027
1477
 
@@ -1039,6 +1489,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1039
1489
  radius={dotCfg.radius}
1040
1490
  ring={dotCfg.ring}
1041
1491
  color={dotCfg.color}
1492
+ viewEnd={engine.viewEnd}
1042
1493
  />
1043
1494
  </Group>
1044
1495
  )}
@@ -1069,6 +1520,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1069
1520
  lineData={engine.data}
1070
1521
  lineLinear={lineIsLinear}
1071
1522
  renderMarker={renderMarker}
1523
+ cluster={markerClusterCfg}
1072
1524
  />
1073
1525
  </Group>
1074
1526
  )}
@@ -1242,12 +1694,19 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
1242
1694
  /** Live-price badge, drawn above the scrub dim so the dim never clips its left
1243
1695
  * edge. Shares the degen shake transform so it tracks the shaken stack. */
1244
1696
  function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
1245
- const { badgeCfg, badgeData, skiaFont, reveal, degenShakeTransform } = model;
1697
+ const { badgeCfg, badgeData, badgeFont, reveal, degenShakeTransform } = model;
1246
1698
  if (!badgeCfg) return null;
1247
1699
  return (
1248
1700
  <Group transform={degenShakeTransform}>
1249
1701
  <Group opacity={reveal.badgeOpacity}>
1250
- <BadgeOverlay badge={badgeData} font={skiaFont} />
1702
+ <BadgeOverlay
1703
+ badge={badgeData}
1704
+ font={badgeFont}
1705
+ borderColor={badgeCfg.borderColor}
1706
+ borderWidth={badgeCfg.borderWidth}
1707
+ offsetX={badgeCfg.offsetX}
1708
+ offsetY={badgeCfg.offsetY}
1709
+ />
1251
1710
  </Group>
1252
1711
  </Group>
1253
1712
  );
@@ -1260,11 +1719,20 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
1260
1719
  function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
1261
1720
  const {
1262
1721
  allRefLines,
1722
+ refLineCustom,
1723
+ dragValues,
1724
+ groupHidden,
1725
+ refGroupResult,
1726
+ refGroupingActive,
1727
+ refGroupBadge,
1728
+ refGroupBadgeFont,
1729
+ refGroupFormat,
1263
1730
  engine,
1264
1731
  effectivePadding,
1265
1732
  palette,
1266
1733
  formatValue,
1267
1734
  skiaFont,
1735
+ fontProp,
1268
1736
  degenShakeTransform,
1269
1737
  } = model;
1270
1738
  if (allRefLines.length === 0) return null;
@@ -1279,9 +1747,26 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
1279
1747
  palette={palette}
1280
1748
  formatValue={formatValue}
1281
1749
  font={skiaFont}
1750
+ fontProp={fontProp}
1282
1751
  badgeLayer
1752
+ dragValues={dragValues}
1753
+ index={i}
1754
+ suppressTag={refLineCustom[i]}
1755
+ groupHidden={refGroupingActive ? groupHidden : undefined}
1283
1756
  />
1284
1757
  ))}
1758
+ {/* Collapsed count handles for grouped (near-value) lines. */}
1759
+ {refGroupingActive && (
1760
+ <ReferenceLineGroupOverlay
1761
+ grouping={refGroupResult}
1762
+ padding={effectivePadding}
1763
+ canvasWidth={engine.canvasWidth}
1764
+ palette={palette}
1765
+ font={refGroupBadgeFont}
1766
+ badge={refGroupBadge}
1767
+ format={refGroupFormat}
1768
+ />
1769
+ )}
1285
1770
  </Group>
1286
1771
  );
1287
1772
  }
@@ -1338,8 +1823,16 @@ export function LiveChart(props: LiveChartProps) {
1338
1823
  bottomLabelCfg,
1339
1824
  markersActive,
1340
1825
  markersSV,
1826
+ markerClusterCfg,
1341
1827
  renderMarker,
1342
1828
  renderTooltip,
1829
+ renderOverlay,
1830
+ renderReferenceLine,
1831
+ allRefLines,
1832
+ refLineCustom,
1833
+ dragValues,
1834
+ dragActive,
1835
+ overlayContext,
1343
1836
  scrubCfg,
1344
1837
  crosshair,
1345
1838
  isCandle,
@@ -1424,6 +1917,23 @@ export function LiveChart(props: LiveChartProps) {
1424
1917
  padding={effectivePadding}
1425
1918
  lineData={engine.data}
1426
1919
  lineLinear={lineIsLinear}
1920
+ cluster={markerClusterCfg}
1921
+ />
1922
+ )}
1923
+
1924
+ {/* Custom-rendered reference-line tags — RN views floated over the canvas
1925
+ (non-Skia), pinned to each Form-A line's value. Sibling of <Canvas>.
1926
+ Built-in Skia tags for these lines are suppressed (no double-draw). */}
1927
+ {renderReferenceLine && allRefLines.length > 0 && (
1928
+ <CustomReferenceLineOverlay
1929
+ lines={allRefLines}
1930
+ renderReferenceLine={renderReferenceLine}
1931
+ custom={refLineCustom}
1932
+ engine={engine}
1933
+ padding={effectivePadding}
1934
+ formatValue={formatValue}
1935
+ dragValues={dragValues}
1936
+ dragActive={dragActive}
1427
1937
  />
1428
1938
  )}
1429
1939
 
@@ -1445,8 +1955,16 @@ export function LiveChart(props: LiveChartProps) {
1445
1955
  placement={scrubCfg.tooltipPlacement}
1446
1956
  margin={scrubCfg.tooltipMargin}
1447
1957
  lineTop={crosshair.tooltipLineTop}
1958
+ scrubDotY={crosshair.scrubDotY}
1448
1959
  />
1449
1960
  )}
1961
+
1962
+ {/* Custom consumer overlay — an RN view tree floated over the canvas with
1963
+ the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
1964
+ tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
1965
+ {renderOverlay && (
1966
+ <ChartOverlayLayer render={renderOverlay} context={overlayContext} />
1967
+ )}
1450
1968
  </View>
1451
1969
  </GestureDetector>
1452
1970
  );