react-native-livechart 3.11.0 → 4.0.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 (138) 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 +18 -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 +8 -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 +7 -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 +56 -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.map +1 -1
  40. package/dist/draw/markerAtlas.d.ts +20 -2
  41. package/dist/draw/markerAtlas.d.ts.map +1 -1
  42. package/dist/draw/volume.d.ts +25 -0
  43. package/dist/draw/volume.d.ts.map +1 -0
  44. package/dist/hooks/crosshairShared.d.ts +12 -6
  45. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  46. package/dist/hooks/overlayScale.d.ts +22 -0
  47. package/dist/hooks/overlayScale.d.ts.map +1 -0
  48. package/dist/hooks/resolveChartLayout.d.ts +9 -0
  49. package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
  50. package/dist/hooks/useBadge.d.ts +22 -1
  51. package/dist/hooks/useBadge.d.ts.map +1 -1
  52. package/dist/hooks/useCandlePaths.d.ts +6 -0
  53. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  54. package/dist/hooks/useChartOverlayContext.d.ts +45 -0
  55. package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
  56. package/dist/hooks/useChartPaths.d.ts +8 -1
  57. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  58. package/dist/hooks/useCrosshair.d.ts +9 -2
  59. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  60. package/dist/hooks/useLiveDot.d.ts +6 -1
  61. package/dist/hooks/useLiveDot.d.ts.map +1 -1
  62. package/dist/hooks/useMarkers.d.ts +12 -4
  63. package/dist/hooks/useMarkers.d.ts.map +1 -1
  64. package/dist/hooks/useMultiSeriesLinePaths.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/index.d.ts +2 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/math/markerCluster.d.ts +47 -0
  80. package/dist/math/markerCluster.d.ts.map +1 -0
  81. package/dist/math/markers.d.ts +15 -0
  82. package/dist/math/markers.d.ts.map +1 -1
  83. package/dist/math/referenceDrag.d.ts +24 -0
  84. package/dist/math/referenceDrag.d.ts.map +1 -0
  85. package/dist/math/referenceGroup.d.ts +31 -0
  86. package/dist/math/referenceGroup.d.ts.map +1 -0
  87. package/dist/math/referenceLines.d.ts +33 -7
  88. package/dist/math/referenceLines.d.ts.map +1 -1
  89. package/dist/types.d.ts +512 -21
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +1 -1
  92. package/src/components/BadgeOverlay.tsx +26 -1
  93. package/src/components/ChartOverlayLayer.tsx +32 -0
  94. package/src/components/CustomMarkerOverlay.tsx +153 -53
  95. package/src/components/CustomReferenceLineOverlay.tsx +250 -0
  96. package/src/components/CustomTooltipOverlay.tsx +27 -7
  97. package/src/components/DotOverlay.tsx +9 -0
  98. package/src/components/LiveChart.tsx +561 -34
  99. package/src/components/LiveChartSeries.tsx +110 -5
  100. package/src/components/MarkerOverlay.tsx +100 -6
  101. package/src/components/MultiSeriesDots.tsx +12 -3
  102. package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
  103. package/src/components/ReferenceLineOverlay.tsx +105 -32
  104. package/src/components/XAxisOverlay.tsx +9 -2
  105. package/src/components/YAxisOverlay.tsx +48 -26
  106. package/src/constants.ts +10 -0
  107. package/src/core/liveChartEngineTick.ts +81 -8
  108. package/src/core/liveChartSeriesEngineTick.ts +50 -9
  109. package/src/core/resolveConfig.ts +122 -1
  110. package/src/core/useLiveChartEngine.ts +103 -10
  111. package/src/core/useLiveChartSeriesEngine.ts +26 -3
  112. package/src/draw/grid.ts +13 -1
  113. package/src/draw/markerAtlas.ts +117 -3
  114. package/src/draw/volume.ts +190 -0
  115. package/src/hooks/crosshairShared.ts +37 -9
  116. package/src/hooks/overlayScale.ts +59 -0
  117. package/src/hooks/resolveChartLayout.ts +33 -0
  118. package/src/hooks/useBadge.ts +89 -21
  119. package/src/hooks/useCandlePaths.ts +104 -7
  120. package/src/hooks/useChartOverlayContext.ts +93 -0
  121. package/src/hooks/useChartPaths.ts +14 -1
  122. package/src/hooks/useCrosshair.ts +29 -2
  123. package/src/hooks/useLiveDot.ts +16 -5
  124. package/src/hooks/useMarkers.ts +43 -11
  125. package/src/hooks/useMultiSeriesLinePaths.ts +2 -1
  126. package/src/hooks/usePanScroll.ts +253 -0
  127. package/src/hooks/usePinchZoom.ts +189 -0
  128. package/src/hooks/useReferenceDrag.ts +254 -0
  129. package/src/hooks/useReferenceLine.ts +56 -9
  130. package/src/hooks/useReferenceLinePress.ts +16 -2
  131. package/src/hooks/useVisibleRange.ts +119 -0
  132. package/src/index.ts +17 -1
  133. package/src/math/markerCluster.ts +214 -0
  134. package/src/math/markers.ts +54 -9
  135. package/src/math/referenceDrag.ts +66 -0
  136. package/src/math/referenceGroup.ts +65 -0
  137. package/src/math/referenceLines.ts +76 -19
  138. package/src/types.ts +532 -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(
@@ -407,14 +634,23 @@ function useLiveChartController({
407
634
  ? thresholdStops(thresholdCfg, palette)
408
635
  : null;
409
636
 
637
+ // Straight polyline instead of the monotone cubic when line.curve === "linear".
638
+ // Shared by the path builders and the marker anchoring so glyphs sit on the
639
+ // rendered line rather than the phantom spline.
640
+ const lineIsLinear = lineProp?.curve === "linear";
641
+
410
642
  const { linePath, fillPath, thresholdFillPath } = useChartPaths(
411
643
  engine,
412
644
  effectivePadding,
413
645
  reveal.morphT,
414
646
  // Only build the band path when the fill is actually on.
415
647
  thresholdCfg?.fill ? thresholdGeom.lineY : undefined,
416
- // Straight polyline instead of the monotone cubic when line.curve === "linear".
417
- lineProp?.curve === "linear",
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,
418
654
  );
419
655
 
420
656
  // Area-dots fill shader color as a vec4 (channels 0..1), with the config
@@ -432,19 +668,36 @@ function useLiveChartController({
432
668
  adA * (areaDotsCfg?.opacity ?? 1),
433
669
  ];
434
670
 
435
- const { upBodiesPath, downBodiesPath, upWicksPath, downWicksPath } =
436
- useCandlePaths(
437
- engine,
438
- effectivePadding,
439
- // Match engine: stashed candles while reverse-morphing in candle mode.
440
- isCandle ? candlesEngine : candles,
441
- isCandle ? liveEngine : liveCandle,
442
- candleWidth,
443
- isCandle,
444
- metricsCfg.candle,
445
- !isStatic, // static: no candle-width lerp loop
446
- );
447
- 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);
448
701
 
449
702
  const momentumSV = useMomentum(engine, momentum);
450
703
 
@@ -484,7 +737,7 @@ function useLiveChartController({
484
737
  effectivePadding,
485
738
  palette,
486
739
  formatValue,
487
- skiaFont,
740
+ badgeFont,
488
741
  badgeCfg?.variant ?? "default",
489
742
  badgeCfg?.tail ?? true,
490
743
  momentumSV,
@@ -492,6 +745,11 @@ function useLiveChartController({
492
745
  badgeCfg?.background,
493
746
  metricsCfg.badge,
494
747
  metricsCfg.motion.badgeColorSpeed,
748
+ effectiveYAxisFloat,
749
+ engine.edgeValue,
750
+ badgeCfg?.followViewEdge ?? false,
751
+ badgeCfg?.radius,
752
+ badgeCfg?.textColor,
495
753
  );
496
754
 
497
755
  // Scrub/crosshair must see the same stash-backed candles as the engine.
@@ -505,6 +763,7 @@ function useLiveChartController({
505
763
  : undefined;
506
764
 
507
765
  const markersActive = markers != null;
766
+ const markerClusterCfg = resolveMarkerCluster(markerCluster);
508
767
  // `projected` is used internally by the hit-test gesture; the overlay
509
768
  // self-projects, so we only need the gesture + hit-test here. Built BEFORE
510
769
  // `useCrosshair` so the scrub-action tap can defer to a marker under the finger.
@@ -514,10 +773,12 @@ function useLiveChartController({
514
773
  markersSV,
515
774
  !isStatic && markersActive,
516
775
  markerHitRadius,
517
- onMarkerHover,
776
+ onMarkerPress,
518
777
  undefined, // seriesSV — single-series has none
519
778
  engine.data, // anchor value-less markers to the line
520
779
  !isStatic, // static: no marker-projection loop
780
+ lineIsLinear, // match marker anchoring to the rendered curve
781
+ markerClusterCfg, // co-located marker stacking / collapse
521
782
  );
522
783
 
523
784
  // Pressable reference-line badges (working orders / alerts). Built before
@@ -533,6 +794,7 @@ function useLiveChartController({
533
794
  !isStatic && refPressActive,
534
795
  markerHitRadius,
535
796
  onReferenceLinePress,
797
+ dragValues,
536
798
  );
537
799
 
538
800
  // Combined "defer" hit-test for the scrub-action place-tap: yield to a marker or
@@ -544,6 +806,24 @@ function useLiveChartController({
544
806
  return markerHitTest(x, y) || refLineHitTest(x, y);
545
807
  };
546
808
 
809
+ // Time-scroll activation. `holdToScrub`: a quick one-finger drag scrolls while
810
+ // scrub engages on press-and-hold — so the scrub gesture needs a long-press
811
+ // delay (unless the caller set its own `panGestureDelay`). `timeScrollEnabled`
812
+ // is computed earlier (it gates the float gutter).
813
+ const scrollGestureMode =
814
+ typeof timeScroll === "object"
815
+ ? (timeScroll.gesture ?? "holdToScrub")
816
+ : "holdToScrub";
817
+ // In holdToScrub the scrub MUST require a hold so a quick drag scrolls instead.
818
+ // Precedence: explicit timeScroll.scrubHoldMs, then scrub.panGestureDelay, then
819
+ // the default. `||` (not `??`) skips the resolved panGestureDelay's 0 default.
820
+ const timeScrollHoldMs =
821
+ typeof timeScroll === "object" ? timeScroll.scrubHoldMs : undefined;
822
+ const scrubHoldMs =
823
+ timeScrollEnabled && scrollGestureMode === "holdToScrub"
824
+ ? (timeScrollHoldMs ?? (scrubCfg?.panGestureDelay || HOLD_TO_SCRUB_MS))
825
+ : (scrubCfg?.panGestureDelay ?? 0);
826
+
547
827
  const crosshair = useCrosshair(
548
828
  engine,
549
829
  effectivePadding,
@@ -556,7 +836,7 @@ function useLiveChartController({
556
836
  !isStatic && (scrubCfg !== null || scrubActionCfg !== null),
557
837
  onScrub,
558
838
  candleOpts,
559
- scrubCfg?.panGestureDelay ?? 0,
839
+ scrubHoldMs,
560
840
  onGestureStart,
561
841
  onGestureEnd,
562
842
  scrubActionCfg,
@@ -567,8 +847,75 @@ function useLiveChartController({
567
847
  scrubCfg?.tooltipShowValue ?? true,
568
848
  scrubCfg?.tooltipShowTime ?? true,
569
849
  scrubCfg?.tooltipMargin ?? 8,
850
+ // Axis-drag time-scroll: keep the bottom "time ruler" band scroll-only so a
851
+ // drag there never trips the scrub crosshair.
852
+ timeScrollEnabled && scrollGestureMode === "axisDrag"
853
+ ? Math.max(effectivePadding.bottom, AXIS_GRAB_MIN_PX)
854
+ : 0,
855
+ );
856
+
857
+ // ── Time-scroll (drag back through history) ───────────────────────────────
858
+ // Experimental: a pan freezes the window at an absolute time and resumes
859
+ // following once dragged back to the live edge. Pan is clamped to the earliest
860
+ // retained point (line or candle). See `timeScroll` for the gesture model.
861
+ const scrollMinTime = useDerivedValue(() => {
862
+ const src = isCandle ? candlesEngine.get() : lineEngineData.get();
863
+ return src.length > 0 ? src[0].time : engine.liveEdge.get();
864
+ });
865
+ const panScrollGesture = usePanScroll({
866
+ engine,
867
+ padding: effectivePadding,
868
+ minTime: scrollMinTime,
869
+ enabled: timeScrollEnabled,
870
+ mode: scrollGestureMode,
871
+ // Clear any live crosshair when a scroll drag takes over.
872
+ onScrollStart: () => {
873
+ "worklet";
874
+ crosshair.scrubActive.set(false);
875
+ },
876
+ });
877
+
878
+ // Draggable reference lines: a per-line vertical pan that grabs a line near its
879
+ // value and drags it along the Y-axis (with snap / bounds / callbacks). Built
880
+ // unconditionally for stable hook order; the gesture self-disables when no line
881
+ // opts in, and it's only composed into the root when `refDragEnabled`.
882
+ const refDragEnabled =
883
+ !isStatic && allRefLines.some((l) => l.draggable === true);
884
+ const refDragGesture = useReferenceDrag(
885
+ engine,
886
+ effectivePadding,
887
+ allRefLines,
888
+ dragValues,
889
+ dragActive,
890
+ !isStatic,
570
891
  );
571
892
 
893
+ // Pinch-to-zoom the visible window (two-finger). Anchors at the focal point and
894
+ // writes viewWindow + viewEnd; composes via Simultaneous (it's two-finger, so
895
+ // disjoint from the one-finger pan/scrub). See `zoom`.
896
+ const pinchZoomGesture = usePinchZoom({
897
+ engine,
898
+ padding: effectivePadding,
899
+ minTime: scrollMinTime,
900
+ timeWindow,
901
+ enabled: zoomEnabled,
902
+ minTimeWindow: zoomCfg?.minTimeWindow,
903
+ maxTimeWindow: zoomCfg?.maxTimeWindow,
904
+ onZoomStart: () => {
905
+ "worklet";
906
+ crosshair.scrubActive.set(false);
907
+ },
908
+ });
909
+
910
+ // Paging callbacks: report the visible range / proximity to the oldest data so
911
+ // a host can lazily load history. Inert unless a callback is supplied.
912
+ useVisibleRange({
913
+ engine,
914
+ minTime: scrollMinTime,
915
+ onVisibleRangeChange,
916
+ onReachStart,
917
+ });
918
+
572
919
  // Scrub-action composes a Tap (place/move the reticle, press the badge, dismiss)
573
920
  // ahead of the pan via Exclusive, so a tap is tried first and only becomes a
574
921
  // drag (live-scrub, or lock-adjust once placed) if the finger moves. `Exclusive`
@@ -594,12 +941,37 @@ function useLiveChartController({
594
941
  overlayTaps.length === 1
595
942
  ? overlayTaps[0]
596
943
  : Gesture.Simultaneous(overlayTaps[0], overlayTaps[1]);
597
- // With scrub-action the action tap shares the gesture space with the overlay
598
- // taps (Simultaneous); without it the pan only needs to race the tap group.
944
+ // Always `Simultaneous`, never `Race`: on iOS the scrub pan uses
945
+ // `minDistance(0)`, so in a `Race` it activates on touch-down and cancels the
946
+ // overlay tap before it can recognize — `onMarkerPress`/`onReferenceLinePress`
947
+ // would never fire. Sharing the gesture space lets the tap recognize; the pan
948
+ // defers to a marker/badge under the finger via `deferTapHit` (see
949
+ // `useCrosshair`'s scrub `onStart`) so no stray crosshair is dropped there.
950
+ rootGesture = Gesture.Simultaneous(baseGesture, tapGroup);
951
+ }
952
+
953
+ // Compose the pan-scroll gesture. holdToScrub races the scrub/tap gestures (a
954
+ // quick drag scrolls; a still press-hold falls through to scrub). Axis-drag
955
+ // goes first via Exclusive: it fails instantly outside the axis band, so scrub
956
+ // runs everywhere else.
957
+ if (timeScrollEnabled) {
599
958
  rootGesture =
600
- scrubActionCfg !== null
601
- ? Gesture.Simultaneous(baseGesture, tapGroup)
602
- : Gesture.Race(baseGesture, tapGroup);
959
+ scrollGestureMode === "axisDrag"
960
+ ? Gesture.Exclusive(panScrollGesture, rootGesture)
961
+ : Gesture.Race(panScrollGesture, rootGesture);
962
+ }
963
+
964
+ // Draggable reference lines take priority: a vertical grab on a line drags it.
965
+ // The manual-activation pan fails fast off any line (or on horizontal intent), so
966
+ // Exclusive falls through to scrub / scroll everywhere else.
967
+ if (refDragEnabled) {
968
+ rootGesture = Gesture.Exclusive(refDragGesture, rootGesture);
969
+ }
970
+
971
+ // Pinch runs alongside everything else (two-finger, so it never competes with
972
+ // the one-finger pan/scrub/tap gestures for the same touch).
973
+ if (zoomEnabled) {
974
+ rootGesture = Gesture.Simultaneous(rootGesture, pinchZoomGesture);
603
975
  }
604
976
 
605
977
  // ── Derived render values ──────────────────────────────────────────────
@@ -657,6 +1029,7 @@ function useLiveChartController({
657
1029
  extremaTimeOffset: isCandle ? candleWidth / 2 : 0,
658
1030
  // configs
659
1031
  yAxisCfg,
1032
+ yAxisFloat: effectiveYAxisFloat,
660
1033
  xAxisCfg,
661
1034
  badgeCfg,
662
1035
  scrubCfg,
@@ -674,6 +1047,16 @@ function useLiveChartController({
674
1047
  leftEdgeFadeCfg,
675
1048
  metricsCfg,
676
1049
  allRefLines,
1050
+ refLineCustom,
1051
+ dragValues,
1052
+ dragActive,
1053
+ renderReferenceLine,
1054
+ refGroupingActive: refGroupingRadius != null,
1055
+ refGroupResult,
1056
+ groupHidden,
1057
+ refGroupBadge,
1058
+ refGroupBadgeFont,
1059
+ refGroupFormat,
677
1060
  resolvedSegments,
678
1061
  hasRecolorSegments,
679
1062
  segmentGradient,
@@ -685,7 +1068,9 @@ function useLiveChartController({
685
1068
  // theme / layout / fonts
686
1069
  palette,
687
1070
  skiaFont,
1071
+ fontProp,
688
1072
  valueFont,
1073
+ badgeFont,
689
1074
  strokeWidth,
690
1075
  effectivePadding,
691
1076
  // engine + reveal
@@ -705,10 +1090,20 @@ function useLiveChartController({
705
1090
  linePath,
706
1091
  fillPath,
707
1092
  thresholdFillPath,
1093
+ lineIsLinear,
708
1094
  upBodiesPath,
709
1095
  downBodiesPath,
710
1096
  upWicksPath,
711
1097
  downWicksPath,
1098
+ upBarsPath,
1099
+ downBarsPath,
1100
+ // Volume bars: active flag, fade-in opacity, and resolved colors (default to
1101
+ // the candle palette). The reserved band height is read by the x-axis.
1102
+ volumeActive: volumeCfg !== null,
1103
+ volumeBandHeight,
1104
+ volumeOpacity: volumeCfg?.opacity ?? 1,
1105
+ volumeUpColor: volumeCfg?.upColor ?? palette.candleUp,
1106
+ volumeDownColor: volumeCfg?.downColor ?? palette.candleDown,
712
1107
  dotX,
713
1108
  dotY,
714
1109
  liveDotOpacity,
@@ -724,8 +1119,11 @@ function useLiveChartController({
724
1119
  rootGesture,
725
1120
  markersActive,
726
1121
  markersSV,
1122
+ markerClusterCfg,
727
1123
  renderMarker,
728
1124
  renderTooltip,
1125
+ renderOverlay,
1126
+ overlayContext,
729
1127
  // selection dot: resolved config + fallback color (the chart line/accent color)
730
1128
  selectionDot: selectionDotCfg,
731
1129
  selectionColor: lineProp?.color ?? palette.line,
@@ -750,6 +1148,7 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
750
1148
  const {
751
1149
  degenShakeTransform,
752
1150
  yAxisCfg,
1151
+ yAxisFloat,
753
1152
  reveal,
754
1153
  yAxisEntries,
755
1154
  engine,
@@ -775,10 +1174,13 @@ function ChartFillLayer({ model }: { model: LiveChartModel }) {
775
1174
 
776
1175
  return (
777
1176
  <Group transform={degenShakeTransform}>
778
- {/* Y-axis grid */}
1177
+ {/* Y-axis. Default: grid + labels here (in a reserved gutter). Floating
1178
+ mode: grid only — the labels + a soft edge fade draw above the candles
1179
+ in ChartStack so the plot runs full-width and candles dim under them. */}
779
1180
  {yAxisCfg && (
780
1181
  <Group opacity={reveal.yAxisOpacity}>
781
1182
  <YAxisOverlay
1183
+ variant={yAxisFloat ? "grid" : "all"}
782
1184
  entries={yAxisEntries}
783
1185
  engine={engine}
784
1186
  padding={effectivePadding}
@@ -851,10 +1253,12 @@ function ChartStack({ model }: { model: LiveChartModel }) {
851
1253
  effectivePadding,
852
1254
  palette,
853
1255
  skiaFont,
1256
+ fontProp,
854
1257
  badgeCfg,
855
1258
  valueLineCfg,
856
1259
  dotY,
857
1260
  allRefLines,
1261
+ dragValues,
858
1262
  resolvedSegments,
859
1263
  hasRecolorSegments,
860
1264
  segmentGradient,
@@ -864,6 +1268,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
864
1268
  formatValue,
865
1269
  lineGroupOpacity,
866
1270
  linePath,
1271
+ lineIsLinear,
867
1272
  strokeWidth,
868
1273
  lineProp,
869
1274
  candleGroupOpacity,
@@ -871,6 +1276,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
871
1276
  downWicksPath,
872
1277
  upBodiesPath,
873
1278
  downBodiesPath,
1279
+ upBarsPath,
1280
+ downBarsPath,
1281
+ volumeActive,
1282
+ volumeBandHeight,
1283
+ volumeOpacity,
1284
+ volumeUpColor,
1285
+ volumeDownColor,
874
1286
  xAxisCfg,
875
1287
  xAxisEntries,
876
1288
  dotX,
@@ -882,10 +1294,16 @@ function ChartStack({ model }: { model: LiveChartModel }) {
882
1294
  degenPackRevision,
883
1295
  markersActive,
884
1296
  markersSV,
1297
+ markerClusterCfg,
885
1298
  renderMarker,
886
1299
  emptyText,
887
1300
  metricsCfg,
888
1301
  layoutWidth,
1302
+ yAxisCfg,
1303
+ yAxisFloat,
1304
+ yAxisEntries,
1305
+ badgeUsesRightGutter,
1306
+ gridStyleCfg,
889
1307
  } = model;
890
1308
 
891
1309
  return (
@@ -928,6 +1346,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
928
1346
  palette={palette}
929
1347
  formatValue={formatValue}
930
1348
  font={skiaFont}
1349
+ fontProp={fontProp}
1350
+ dragValues={dragValues}
1351
+ index={i}
931
1352
  />
932
1353
  ))}
933
1354
 
@@ -990,13 +1411,13 @@ function ChartStack({ model }: { model: LiveChartModel }) {
990
1411
  <Path
991
1412
  path={upWicksPath}
992
1413
  style="stroke"
993
- strokeWidth={1}
1414
+ strokeWidth={metricsCfg.candle.wickWidth}
994
1415
  color={palette.wickUp}
995
1416
  />
996
1417
  <Path
997
1418
  path={downWicksPath}
998
1419
  style="stroke"
999
- strokeWidth={1}
1420
+ strokeWidth={metricsCfg.candle.wickWidth}
1000
1421
  color={palette.wickDown}
1001
1422
  />
1002
1423
  <Path path={upBodiesPath} style="fill" color={palette.candleUp} />
@@ -1007,7 +1428,41 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1007
1428
  />
1008
1429
  </Group>
1009
1430
 
1010
- {/* X-axis time labels */}
1431
+ {/* Volume bars in the reserved band below the candles. Fades in with the
1432
+ candle group (outer opacity); the config opacity dims the whole band
1433
+ (inner). Up/down bars carry their own colors (default candle palette). */}
1434
+ {volumeActive && (
1435
+ <Group opacity={candleGroupOpacity}>
1436
+ <Group opacity={volumeOpacity}>
1437
+ <Path path={upBarsPath} style="fill" color={volumeUpColor} />
1438
+ <Path path={downBarsPath} style="fill" color={volumeDownColor} />
1439
+ </Group>
1440
+ </Group>
1441
+ )}
1442
+
1443
+ {/* Floating axis: the labels float ABOVE the candles (right-aligned at the
1444
+ edge) so the plot runs full-width and candles stay fully visible behind
1445
+ them. (Default non-floating axis draws grid + labels in ChartFillLayer.) */}
1446
+ {yAxisCfg && yAxisFloat && (
1447
+ <Group opacity={reveal.yAxisOpacity}>
1448
+ <YAxisOverlay
1449
+ variant="labels"
1450
+ float
1451
+ entries={yAxisEntries}
1452
+ engine={engine}
1453
+ padding={effectivePadding}
1454
+ palette={palette}
1455
+ font={skiaFont}
1456
+ badge={badgeUsesRightGutter}
1457
+ badgeTail={badgeCfg?.tail ?? true}
1458
+ badgeMetrics={metricsCfg.badge}
1459
+ gridStyle={gridStyleCfg}
1460
+ />
1461
+ </Group>
1462
+ )}
1463
+
1464
+ {/* X-axis time labels. With a volume band the bottom padding is inflated by
1465
+ the band height; pass it so the axis shifts back to the very bottom. */}
1011
1466
  {xAxisCfg && (
1012
1467
  <XAxisOverlay
1013
1468
  entries={xAxisEntries}
@@ -1015,6 +1470,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1015
1470
  padding={effectivePadding}
1016
1471
  palette={palette}
1017
1472
  font={skiaFont}
1473
+ volumeBandHeight={volumeBandHeight}
1018
1474
  />
1019
1475
  )}
1020
1476
 
@@ -1032,6 +1488,7 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1032
1488
  radius={dotCfg.radius}
1033
1489
  ring={dotCfg.ring}
1034
1490
  color={dotCfg.color}
1491
+ viewEnd={engine.viewEnd}
1035
1492
  />
1036
1493
  </Group>
1037
1494
  )}
@@ -1060,7 +1517,9 @@ function ChartStack({ model }: { model: LiveChartModel }) {
1060
1517
  palette={palette}
1061
1518
  font={skiaFont}
1062
1519
  lineData={engine.data}
1520
+ lineLinear={lineIsLinear}
1063
1521
  renderMarker={renderMarker}
1522
+ cluster={markerClusterCfg}
1064
1523
  />
1065
1524
  </Group>
1066
1525
  )}
@@ -1234,12 +1693,19 @@ function ChartValueOverlay({ model }: { model: LiveChartModel }) {
1234
1693
  /** Live-price badge, drawn above the scrub dim so the dim never clips its left
1235
1694
  * edge. Shares the degen shake transform so it tracks the shaken stack. */
1236
1695
  function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
1237
- const { badgeCfg, badgeData, skiaFont, reveal, degenShakeTransform } = model;
1696
+ const { badgeCfg, badgeData, badgeFont, reveal, degenShakeTransform } = model;
1238
1697
  if (!badgeCfg) return null;
1239
1698
  return (
1240
1699
  <Group transform={degenShakeTransform}>
1241
1700
  <Group opacity={reveal.badgeOpacity}>
1242
- <BadgeOverlay badge={badgeData} font={skiaFont} />
1701
+ <BadgeOverlay
1702
+ badge={badgeData}
1703
+ font={badgeFont}
1704
+ borderColor={badgeCfg.borderColor}
1705
+ borderWidth={badgeCfg.borderWidth}
1706
+ offsetX={badgeCfg.offsetX}
1707
+ offsetY={badgeCfg.offsetY}
1708
+ />
1243
1709
  </Group>
1244
1710
  </Group>
1245
1711
  );
@@ -1252,11 +1718,20 @@ function ChartBadgeLayer({ model }: { model: LiveChartModel }) {
1252
1718
  function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
1253
1719
  const {
1254
1720
  allRefLines,
1721
+ refLineCustom,
1722
+ dragValues,
1723
+ groupHidden,
1724
+ refGroupResult,
1725
+ refGroupingActive,
1726
+ refGroupBadge,
1727
+ refGroupBadgeFont,
1728
+ refGroupFormat,
1255
1729
  engine,
1256
1730
  effectivePadding,
1257
1731
  palette,
1258
1732
  formatValue,
1259
1733
  skiaFont,
1734
+ fontProp,
1260
1735
  degenShakeTransform,
1261
1736
  } = model;
1262
1737
  if (allRefLines.length === 0) return null;
@@ -1271,9 +1746,26 @@ function ChartRefBadgeLayer({ model }: { model: LiveChartModel }) {
1271
1746
  palette={palette}
1272
1747
  formatValue={formatValue}
1273
1748
  font={skiaFont}
1749
+ fontProp={fontProp}
1274
1750
  badgeLayer
1751
+ dragValues={dragValues}
1752
+ index={i}
1753
+ suppressTag={refLineCustom[i]}
1754
+ groupHidden={refGroupingActive ? groupHidden : undefined}
1275
1755
  />
1276
1756
  ))}
1757
+ {/* Collapsed count handles for grouped (near-value) lines. */}
1758
+ {refGroupingActive && (
1759
+ <ReferenceLineGroupOverlay
1760
+ grouping={refGroupResult}
1761
+ padding={effectivePadding}
1762
+ canvasWidth={engine.canvasWidth}
1763
+ palette={palette}
1764
+ font={refGroupBadgeFont}
1765
+ badge={refGroupBadge}
1766
+ format={refGroupFormat}
1767
+ />
1768
+ )}
1277
1769
  </Group>
1278
1770
  );
1279
1771
  }
@@ -1330,14 +1822,23 @@ export function LiveChart(props: LiveChartProps) {
1330
1822
  bottomLabelCfg,
1331
1823
  markersActive,
1332
1824
  markersSV,
1825
+ markerClusterCfg,
1333
1826
  renderMarker,
1334
1827
  renderTooltip,
1828
+ renderOverlay,
1829
+ renderReferenceLine,
1830
+ allRefLines,
1831
+ refLineCustom,
1832
+ dragValues,
1833
+ dragActive,
1834
+ overlayContext,
1335
1835
  scrubCfg,
1336
1836
  crosshair,
1337
1837
  isCandle,
1338
1838
  extremaTimeOffset,
1339
1839
  topConnector,
1340
1840
  bottomConnector,
1841
+ lineIsLinear,
1341
1842
  } = model;
1342
1843
 
1343
1844
  return (
@@ -1414,6 +1915,24 @@ export function LiveChart(props: LiveChartProps) {
1414
1915
  engine={engine}
1415
1916
  padding={effectivePadding}
1416
1917
  lineData={engine.data}
1918
+ lineLinear={lineIsLinear}
1919
+ cluster={markerClusterCfg}
1920
+ />
1921
+ )}
1922
+
1923
+ {/* Custom-rendered reference-line tags — RN views floated over the canvas
1924
+ (non-Skia), pinned to each Form-A line's value. Sibling of <Canvas>.
1925
+ Built-in Skia tags for these lines are suppressed (no double-draw). */}
1926
+ {renderReferenceLine && allRefLines.length > 0 && (
1927
+ <CustomReferenceLineOverlay
1928
+ lines={allRefLines}
1929
+ renderReferenceLine={renderReferenceLine}
1930
+ custom={refLineCustom}
1931
+ engine={engine}
1932
+ padding={effectivePadding}
1933
+ formatValue={formatValue}
1934
+ dragValues={dragValues}
1935
+ dragActive={dragActive}
1417
1936
  />
1418
1937
  )}
1419
1938
 
@@ -1435,8 +1954,16 @@ export function LiveChart(props: LiveChartProps) {
1435
1954
  placement={scrubCfg.tooltipPlacement}
1436
1955
  margin={scrubCfg.tooltipMargin}
1437
1956
  lineTop={crosshair.tooltipLineTop}
1957
+ scrubDotY={crosshair.scrubDotY}
1438
1958
  />
1439
1959
  )}
1960
+
1961
+ {/* Custom consumer overlay — an RN view tree floated over the canvas with
1962
+ the price↔pixel / time↔pixel bridge, for order / avg-entry / liquidation
1963
+ tags etc. Topmost RN sibling; `box-none` so empty areas still scrub. */}
1964
+ {renderOverlay && (
1965
+ <ChartOverlayLayer render={renderOverlay} context={overlayContext} />
1966
+ )}
1440
1967
  </View>
1441
1968
  </GestureDetector>
1442
1969
  );