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
@@ -0,0 +1,207 @@
1
+ import {
2
+ Group,
3
+ RoundedRect,
4
+ Text as SkiaText,
5
+ type SkFont,
6
+ } from "@shopify/react-native-skia";
7
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
8
+
9
+ import type { ChartPadding } from "../draw/line";
10
+ import { measureFontTextWidth } from "../lib/measureFontTextWidth";
11
+ import type { ReferenceGrouping } from "../math/referenceGroup";
12
+ import type { ResolvedReferenceGroupBadge } from "../math/referenceLines";
13
+ import type { LiveChartPalette } from "../types";
14
+
15
+ /** Max simultaneous group handles (a fixed Skia slot pool — the per-frame group
16
+ * count varies, so we draw a fixed set of slots and hide the unused ones). */
17
+ const MAX_GROUP_PILLS = 12;
18
+ /** Horizontal padding inside the count pill, in px. */
19
+ const PILL_PAD_X = 6;
20
+ /** Vertical padding inside the count pill, in px. */
21
+ const PILL_PAD_Y = 3;
22
+ /** Pill inset from the anchored plot edge, in px. */
23
+ const EDGE_INSET = 2;
24
+
25
+ interface PillLayout {
26
+ visible: boolean;
27
+ x: number;
28
+ top: number;
29
+ w: number;
30
+ text: string;
31
+ textX: number;
32
+ textY: number;
33
+ }
34
+
35
+ const HIDDEN_PILL: PillLayout = {
36
+ visible: false,
37
+ x: 0,
38
+ top: -1,
39
+ w: 0,
40
+ text: "",
41
+ textX: 0,
42
+ textY: -1,
43
+ };
44
+
45
+ /** One collapsed-group count pill (slot `index` of the fixed pool). Reads the
46
+ * cluster at `grouping.groups[index]`; hidden when fewer groups exist this frame. */
47
+ function GroupCountPill({
48
+ grouping,
49
+ index,
50
+ padding,
51
+ canvasWidth,
52
+ font,
53
+ position,
54
+ icon,
55
+ showText,
56
+ format,
57
+ baselineOffset,
58
+ pillH,
59
+ radius,
60
+ borderWidth,
61
+ color,
62
+ background,
63
+ textColor,
64
+ }: {
65
+ grouping: SharedValue<ReferenceGrouping>;
66
+ index: number;
67
+ padding: ChartPadding;
68
+ canvasWidth: SharedValue<number>;
69
+ font: SkFont;
70
+ position: "left" | "center" | "right";
71
+ icon: string;
72
+ showText: boolean;
73
+ format?: (count: number) => string;
74
+ baselineOffset: number;
75
+ pillH: number;
76
+ radius: number;
77
+ borderWidth: number;
78
+ color: string;
79
+ background: string;
80
+ textColor: string;
81
+ }) {
82
+ const layout = useDerivedValue<PillLayout>(() => {
83
+ const gs = grouping.get().groups;
84
+ if (index >= gs.length) return HIDDEN_PILL;
85
+ const g = gs[index];
86
+ const count = showText ? (format ? format(g.count) : String(g.count)) : "";
87
+ const text = icon ? (count ? `${icon} ${count}` : icon) : count;
88
+ const w = measureFontTextWidth(font, text) + PILL_PAD_X * 2;
89
+ // Anchor the pill horizontally to the chosen plot edge (or center).
90
+ const x1 = padding.left;
91
+ const x2 = canvasWidth.get() - padding.right;
92
+ let x: number;
93
+ if (position === "right") x = x2 - EDGE_INSET - w;
94
+ else if (position === "center") x = (x1 + x2) / 2 - w / 2;
95
+ else x = x1 + EDGE_INSET;
96
+ return {
97
+ visible: true,
98
+ x,
99
+ top: g.cy - pillH / 2,
100
+ w,
101
+ text,
102
+ textX: x + PILL_PAD_X,
103
+ textY: g.cy - baselineOffset,
104
+ };
105
+ });
106
+
107
+ const opacity = useDerivedValue(() => (layout.get().visible ? 1 : 0));
108
+ const x = useDerivedValue(() => layout.get().x);
109
+ const top = useDerivedValue(() => layout.get().top);
110
+ const w = useDerivedValue(() => layout.get().w);
111
+ const text = useDerivedValue(() => layout.get().text);
112
+ const textX = useDerivedValue(() => layout.get().textX);
113
+ const textY = useDerivedValue(() => layout.get().textY);
114
+
115
+ return (
116
+ <Group opacity={opacity}>
117
+ <RoundedRect
118
+ x={x}
119
+ y={top}
120
+ width={w}
121
+ height={pillH}
122
+ r={radius}
123
+ color={background}
124
+ />
125
+ <RoundedRect
126
+ x={x}
127
+ y={top}
128
+ width={w}
129
+ height={pillH}
130
+ r={radius}
131
+ color={color}
132
+ style="stroke"
133
+ strokeWidth={borderWidth}
134
+ />
135
+ <SkiaText x={textX} y={textY} text={text} font={font} color={textColor} />
136
+ </Group>
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Draws the collapsed-group count handles (e.g. a "×3" pill) for reference-line
142
+ * grouping — one pill per multi-line cluster, pinned to the plot at the cluster
143
+ * centroid. The individual tags of clustered lines are suppressed upstream (via
144
+ * `groupHidden`), so a stack of nearby orders reads as one handle. The pill uses the
145
+ * same style/shape config as a per-line badge (`badge`): `position`, `icon`,
146
+ * `text` (showText), `background` / `borderColor` / `borderWidth`, `radius` (corner),
147
+ * `textColor`, per-badge `font`, and `offsetX` / `offsetY`; the `format` fn maps the
148
+ * count to the label. A fixed slot pool keeps the Skia tree stable while the
149
+ * per-frame group count varies.
150
+ */
151
+ export function ReferenceLineGroupOverlay({
152
+ grouping,
153
+ padding,
154
+ canvasWidth,
155
+ palette,
156
+ font,
157
+ badge,
158
+ format,
159
+ }: {
160
+ grouping: SharedValue<ReferenceGrouping>;
161
+ padding: ChartPadding;
162
+ canvasWidth: SharedValue<number>;
163
+ palette: LiveChartPalette;
164
+ font: SkFont;
165
+ badge: ResolvedReferenceGroupBadge;
166
+ format?: (count: number) => string;
167
+ }) {
168
+ const fm = font.getMetrics();
169
+ const baselineOffset = (fm.ascent + fm.descent) / 2;
170
+ const pillH = fm.descent - fm.ascent + PILL_PAD_Y * 2;
171
+
172
+ // Theme defaults for the unset colors (mirrors the per-line badge resolution).
173
+ const background = badge.background ?? palette.tooltipBg;
174
+ const borderColor = badge.borderColor ?? palette.refLine;
175
+ const textColor = badge.textColor ?? palette.refLabel;
176
+
177
+ const transform =
178
+ badge.offsetX !== 0 || badge.offsetY !== 0
179
+ ? [{ translateX: badge.offsetX }, { translateY: badge.offsetY }]
180
+ : undefined;
181
+
182
+ const slots: React.ReactElement[] = [];
183
+ for (let i = 0; i < MAX_GROUP_PILLS; i++) {
184
+ slots.push(
185
+ <GroupCountPill
186
+ key={i}
187
+ grouping={grouping}
188
+ index={i}
189
+ padding={padding}
190
+ canvasWidth={canvasWidth}
191
+ font={font}
192
+ position={badge.position}
193
+ icon={badge.icon}
194
+ showText={badge.showText}
195
+ format={format}
196
+ baselineOffset={baselineOffset}
197
+ pillH={pillH}
198
+ radius={badge.radius}
199
+ borderWidth={badge.borderWidth}
200
+ color={borderColor}
201
+ background={background}
202
+ textColor={textColor}
203
+ />,
204
+ );
205
+ }
206
+ return <Group transform={transform}>{slots}</Group>;
207
+ }
@@ -6,14 +6,16 @@ import {
6
6
  Text as SkiaText,
7
7
  type SkFont,
8
8
  } from "@shopify/react-native-skia";
9
- import { useDerivedValue } from "react-native-reanimated";
9
+ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
10
10
 
11
11
  import type { ChartEngineLayout } from "../core/useLiveChartEngine";
12
12
  import type { ChartPadding } from "../draw/line";
13
+ import { useChartSkiaFont } from "../hooks/useChartSkiaFont";
13
14
  import { usePathBuilder } from "../hooks/usePathBuilder";
14
15
  import { useReferenceLine } from "../hooks/useReferenceLine";
16
+ import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
15
17
  import { referenceLineForm, resolveReferenceBadge } from "../math/referenceLines";
16
- import type { LiveChartPalette, ReferenceLine } from "../types";
18
+ import type { FontConfig, LiveChartPalette, ReferenceLine } from "../types";
17
19
 
18
20
  /** Translucent fill alpha for value / time bands. */
19
21
  const BAND_FILL_OPACITY = 0.16;
@@ -35,7 +37,12 @@ export function ReferenceLineOverlay({
35
37
  palette,
36
38
  formatValue,
37
39
  font,
40
+ fontProp,
38
41
  badgeLayer = false,
42
+ suppressTag = false,
43
+ groupHidden,
44
+ dragValues,
45
+ index = 0,
39
46
  }: {
40
47
  engine: ChartEngineLayout;
41
48
  padding: ChartPadding;
@@ -43,6 +50,12 @@ export function ReferenceLineOverlay({
43
50
  palette: LiveChartPalette;
44
51
  formatValue: (v: number) => string;
45
52
  font: SkFont;
53
+ /**
54
+ * The chart's raw font config — used to build a per-badge font when the line's
55
+ * {@link ReferenceLineBadgeConfig} sets `fontSize` / `fontFamily` / `fontWeight`.
56
+ * When no badge font knob is set, the resolved `font` is used as-is.
57
+ */
58
+ fontProp?: FontConfig;
46
59
  /**
47
60
  * Render only the badge + label (`true`) or only the lines / bands (`false`,
48
61
  * default). The caller draws the base pass behind the chart content and the
@@ -50,13 +63,65 @@ export function ReferenceLineOverlay({
50
63
  * of being erased by the fade's `dstOut` blend.
51
64
  */
52
65
  badgeLayer?: boolean;
66
+ /**
67
+ * Suppress the built-in tag (badge pill + connector + chevron + icon + gutter
68
+ * label) for this line — used when a custom `renderReferenceLine` element owns
69
+ * the tag. The line / band stroke still draws. No effect on the base pass.
70
+ */
71
+ suppressTag?: boolean;
72
+ /**
73
+ * Per-frame grouping flags (index-aligned): when this line's slot is `true` it's
74
+ * collapsed into a group count handle, so its tag is suppressed (the line / band
75
+ * stroke still draws).
76
+ */
77
+ groupHidden?: SharedValue<boolean[]>;
78
+ /** Per-line live value overrides (dragged values), index-aligned with `referenceLines`. */
79
+ dragValues?: SharedValue<number[]>;
80
+ /** This line's index into {@link dragValues}. */
81
+ index?: number;
53
82
  }) {
54
83
  const form = referenceLineForm(line);
55
84
  const isBand = form === "value-band" || form === "time-band";
56
- const layout = useReferenceLine(engine, padding, line, formatValue, font);
57
85
 
58
86
  const color = line.color ?? palette.refLine;
59
- const labelColor = line.labelColor ?? line.color ?? palette.refLabel;
87
+
88
+ // Resolved badge appearance (badge config → fallback flat fields → theme).
89
+ const badge = resolveReferenceBadge(line);
90
+
91
+ // Per-badge font (size/family/weight) override; reuses the chart `font` when no
92
+ // badge font knob is set, so plain lines and gutter labels are unchanged. The
93
+ // built font drives pill measurement (via useReferenceLine) and the badge text.
94
+ const badgeHasFontOverride =
95
+ badge != null &&
96
+ (badge.fontSize != null ||
97
+ badge.fontFamily != null ||
98
+ badge.fontWeight != null);
99
+ const badgeFontOverride = useChartSkiaFont(
100
+ badgeHasFontOverride
101
+ ? {
102
+ ...fontProp,
103
+ fontFamily: badge?.fontFamily ?? fontProp?.fontFamily,
104
+ fontSize: badge?.fontSize ?? fontProp?.fontSize,
105
+ fontWeight: badge?.fontWeight ?? fontProp?.fontWeight,
106
+ }
107
+ : fontProp,
108
+ MONO_FONT_FAMILY,
109
+ palette.labelFontSize,
110
+ );
111
+ const badgeFont = badgeHasFontOverride ? badgeFontOverride : font;
112
+
113
+ const layout = useReferenceLine(
114
+ engine,
115
+ padding,
116
+ line,
117
+ formatValue,
118
+ badgeFont,
119
+ dragValues,
120
+ index,
121
+ );
122
+
123
+ const labelColor =
124
+ badge?.textColor ?? line.labelColor ?? line.color ?? palette.refLabel;
60
125
  const strokeWidth = line.strokeWidth ?? 1;
61
126
  const intervals = line.intervals ?? [4, 4];
62
127
 
@@ -64,11 +129,17 @@ export function ReferenceLineOverlay({
64
129
  const bandFillOpacity = line.fillOpacity ?? BAND_FILL_OPACITY;
65
130
  const hasBandBorder = isBand && line.strokeWidth !== undefined;
66
131
 
67
- // Resolved badge appearance (badge config → fallback flat fields → theme).
68
- const badge = resolveReferenceBadge(line);
69
132
  const badgeBackground = badge?.background ?? palette.tooltipBg;
70
133
  const badgeBorderColor = badge?.borderColor ?? color;
134
+ const badgeBorderWidth = badge?.borderWidth ?? 1;
71
135
  const badgeRadius = badge?.radius ?? BADGE_PILL_RADIUS;
136
+ // Nudge the whole badge (connector + pill + chevron + icon + label) off its anchor.
137
+ const badgeOffsetX = badge?.offsetX ?? 0;
138
+ const badgeOffsetY = badge?.offsetY ?? 0;
139
+ const badgeTransform =
140
+ badgeOffsetX !== 0 || badgeOffsetY !== 0
141
+ ? [{ translateX: badgeOffsetX }, { translateY: badgeOffsetY }]
142
+ : undefined;
72
143
 
73
144
  const lineBuilder = usePathBuilder();
74
145
  const bandBuilder = usePathBuilder();
@@ -79,11 +150,11 @@ export function ReferenceLineOverlay({
79
150
  const linePath = useDerivedValue(() => {
80
151
  const b = lineBuilder.value;
81
152
  const l = layout.get();
82
- // A plain full-width line. A badge instead draws a pill + a connector to the
83
- // opposite edge, below.
84
- if (l.visible && !l.badge && !isBand) {
85
- b.moveTo(l.x1, l.y);
86
- b.lineTo(l.x2, l.y);
153
+ // A plain horizontal line, drawn at the line extent (full-width when opted in).
154
+ // A non-full-width badge instead draws a pill + connector to the edge, below.
155
+ if (l.visible && l.drawLine && !isBand) {
156
+ b.moveTo(l.lineX1, l.y);
157
+ b.lineTo(l.lineX2, l.y);
87
158
  }
88
159
  return b.detach();
89
160
  });
@@ -92,10 +163,10 @@ export function ReferenceLineOverlay({
92
163
  const b = bandBuilder.value;
93
164
  const l = layout.get();
94
165
  if (l.visible && isBand) {
95
- b.moveTo(l.x1, l.y);
96
- b.lineTo(l.x2, l.y);
97
- b.lineTo(l.x2, l.yBottom);
98
- b.lineTo(l.x1, l.yBottom);
166
+ b.moveTo(l.lineX1, l.y);
167
+ b.lineTo(l.lineX2, l.y);
168
+ b.lineTo(l.lineX2, l.yBottom);
169
+ b.lineTo(l.lineX1, l.yBottom);
99
170
  b.close();
100
171
  }
101
172
  return b.detach();
@@ -107,16 +178,16 @@ export function ReferenceLineOverlay({
107
178
  if (l.visible && hasBandBorder) {
108
179
  if (form === "time-band") {
109
180
  // Vertical edges at the band's left / right.
110
- b.moveTo(l.x1, l.y);
111
- b.lineTo(l.x1, l.yBottom);
112
- b.moveTo(l.x2, l.y);
113
- b.lineTo(l.x2, l.yBottom);
181
+ b.moveTo(l.lineX1, l.y);
182
+ b.lineTo(l.lineX1, l.yBottom);
183
+ b.moveTo(l.lineX2, l.y);
184
+ b.lineTo(l.lineX2, l.yBottom);
114
185
  } else {
115
186
  // Horizontal edges at the band's top / bottom.
116
- b.moveTo(l.x1, l.y);
117
- b.lineTo(l.x2, l.y);
118
- b.moveTo(l.x1, l.yBottom);
119
- b.lineTo(l.x2, l.yBottom);
187
+ b.moveTo(l.lineX1, l.y);
188
+ b.lineTo(l.lineX2, l.y);
189
+ b.moveTo(l.lineX1, l.yBottom);
190
+ b.lineTo(l.lineX2, l.yBottom);
120
191
  }
121
192
  }
122
193
  return b.detach();
@@ -155,7 +226,7 @@ export function ReferenceLineOverlay({
155
226
 
156
227
  const lineOpacity = useDerivedValue(() => {
157
228
  const l = layout.get();
158
- return l.visible && !l.badge && !isBand ? 1 : 0;
229
+ return l.visible && l.drawLine && !isBand ? 1 : 0;
159
230
  });
160
231
  const bandOpacity = useDerivedValue(() =>
161
232
  layout.get().visible && isBand ? bandFillOpacity : 0,
@@ -165,12 +236,14 @@ export function ReferenceLineOverlay({
165
236
  );
166
237
  const badgeOpacity = useDerivedValue(() => {
167
238
  const l = layout.get();
168
- return l.visible && l.badge ? 1 : 0;
239
+ const grouped = groupHidden ? groupHidden.get()[index] === true : false;
240
+ return !suppressTag && !grouped && l.visible && l.badge ? 1 : 0;
169
241
  });
170
242
  // Text + icon ride in the badge pass too, so they stay crisp above the fade.
171
243
  const labelOpacity = useDerivedValue(() => {
172
244
  const l = layout.get();
173
- return l.visible && l.label.length > 0 ? 1 : 0;
245
+ const grouped = groupHidden ? groupHidden.get()[index] === true : false;
246
+ return !suppressTag && !grouped && l.visible && l.label.length > 0 ? 1 : 0;
174
247
  });
175
248
  const iconOpacity = useDerivedValue(() => {
176
249
  const l = layout.get();
@@ -186,7 +259,7 @@ export function ReferenceLineOverlay({
186
259
  // Font metrics depend only on the (stable) font, so read them once instead of
187
260
  // on every frame inside the pill worklet (`getMetrics` allocates + crosses JSI).
188
261
  const { ascent: fontAscent, height: pillH } = (() => {
189
- const fm = font.getMetrics();
262
+ const fm = badgeFont.getMetrics();
190
263
  return {
191
264
  ascent: fm.ascent,
192
265
  height: fm.descent - fm.ascent + BADGE_PILL_PAD_Y * 2,
@@ -238,7 +311,7 @@ export function ReferenceLineOverlay({
238
311
 
239
312
  {/* Badge pass — connector + pill + chevron + icon, above the left-edge fade. */}
240
313
  {badgeLayer && (
241
- <Group opacity={badgeOpacity}>
314
+ <Group opacity={badgeOpacity} transform={badgeTransform}>
242
315
  <Path
243
316
  path={connPath}
244
317
  style="stroke"
@@ -263,7 +336,7 @@ export function ReferenceLineOverlay({
263
336
  r={badgeRadius}
264
337
  color={badgeBorderColor}
265
338
  style="stroke"
266
- strokeWidth={1}
339
+ strokeWidth={badgeBorderWidth}
267
340
  />
268
341
  <Path
269
342
  path={chevronPath}
@@ -278,7 +351,7 @@ export function ReferenceLineOverlay({
278
351
  x={iconX}
279
352
  y={labelY}
280
353
  text={iconText}
281
- font={font}
354
+ font={badgeFont}
282
355
  color={labelColor}
283
356
  />
284
357
  </Group>
@@ -287,12 +360,12 @@ export function ReferenceLineOverlay({
287
360
 
288
361
  {/* Labels ride in the badge pass too, so they stay crisp above the fade. */}
289
362
  {badgeLayer && (
290
- <Group opacity={labelOpacity}>
363
+ <Group opacity={labelOpacity} transform={badgeTransform}>
291
364
  <SkiaText
292
365
  x={labelX}
293
366
  y={labelY}
294
367
  text={labelText}
295
- font={font}
368
+ font={badgeFont}
296
369
  color={labelColor}
297
370
  />
298
371
  </Group>
@@ -19,12 +19,19 @@ export function XAxisOverlay({
19
19
  padding,
20
20
  palette,
21
21
  font,
22
+ volumeBandHeight = 0,
22
23
  }: {
23
24
  entries: SharedValue<XAxisEntry[]>;
24
25
  engine: ChartEngineLayout;
25
26
  padding: ChartPadding;
26
27
  palette: LiveChartPalette;
27
28
  font: SkFont;
29
+ /**
30
+ * Reserved volume-band height (px) folded into `padding.bottom`. The axis line
31
+ * + labels shift back down by this so they stay at the very bottom, below the
32
+ * band, while the price plot above shrinks. `0` = no band (default).
33
+ */
34
+ volumeBandHeight?: number;
28
35
  }) {
29
36
  const axisBuilder = usePathBuilder();
30
37
 
@@ -33,7 +40,7 @@ export function XAxisOverlay({
33
40
  const b = axisBuilder.value;
34
41
  const w = engine.canvasWidth.get();
35
42
  const h = engine.canvasHeight.get();
36
- const lineY = h - padding.bottom;
43
+ const lineY = h - padding.bottom + volumeBandHeight;
37
44
 
38
45
  // Bottom axis line
39
46
  b.moveTo(padding.left, lineY);
@@ -53,7 +60,7 @@ export function XAxisOverlay({
53
60
  "worklet";
54
61
  const items = entries.get();
55
62
  const h = engine.canvasHeight.get();
56
- const y = h - padding.bottom + LABEL_OFFSET_Y;
63
+ const y = h - padding.bottom + volumeBandHeight + LABEL_OFFSET_Y;
57
64
  const n = items.length;
58
65
  const out: { x: number; y: number; label: string; alpha: number }[] = [];
59
66
  for (let i = 0; i < n; i++) {
@@ -22,6 +22,8 @@ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
22
22
  import { AnimatedLabel } from "./AnimatedLabel";
23
23
 
24
24
  const MAX_Y_LABELS = 15;
25
+ /** Right margin (px) for floating labels — keeps them just off the canvas edge. */
26
+ const FLOAT_LABEL_RIGHT_MARGIN = 6;
25
27
 
26
28
  export function YAxisOverlay({
27
29
  entries,
@@ -34,6 +36,8 @@ export function YAxisOverlay({
34
36
  badgeMetrics = BADGE_METRICS_DEFAULTS,
35
37
  seriesLabelInset = 0,
36
38
  gridStyle,
39
+ variant = "all",
40
+ float = false,
37
41
  }: {
38
42
  entries: SharedValue<YAxisEntry[]>;
39
43
  engine: ChartEngineLayout;
@@ -50,6 +54,17 @@ export function YAxisOverlay({
50
54
  seriesLabelInset?: number;
51
55
  /** Grid-line styling overrides. Omit for the legacy solid 1px line. */
52
56
  gridStyle?: ResolvedGridStyleConfig;
57
+ /**
58
+ * Which parts to draw. `"all"` (default) renders grid lines + labels together.
59
+ * `"grid"` / `"labels"` split them so a chart can draw the grid behind the data
60
+ * and the labels (over a soft fade) on top — see {@link YAxisEdgeFade}.
61
+ */
62
+ variant?: "all" | "grid" | "labels";
63
+ /**
64
+ * Floating-axis mode: right-align labels at the canvas edge (over a full-width
65
+ * plot) instead of centering them in a reserved gutter. See {@link YAxisConfig.float}.
66
+ */
67
+ float?: boolean;
53
68
  }) {
54
69
  const gridColor = gridStyle?.color ?? palette.gridLine;
55
70
  const gridWidth = gridStyle?.strokeWidth ?? 1;
@@ -59,6 +74,7 @@ export function YAxisOverlay({
59
74
 
60
75
  const gridLinesPath = useDerivedValue(() => {
61
76
  const b = gridBuilder.value;
77
+ if (variant === "labels") return b.detach();
62
78
  const items = entries.get();
63
79
  const w = engine.canvasWidth.get();
64
80
  for (let i = 0; i < items.length; i++) {
@@ -72,6 +88,7 @@ export function YAxisOverlay({
72
88
  badgeMetrics.dotGap + badgeTailAndCap(font.getSize(), badgeTail, badgeMetrics);
73
89
 
74
90
  const labelEntries = useDerivedValue(() => {
91
+ if (variant === "grid") return [];
75
92
  const items = entries.get();
76
93
  const w = engine.canvasWidth.get();
77
94
  const fm = font.getMetrics();
@@ -80,11 +97,13 @@ export function YAxisOverlay({
80
97
  for (let i = 0; i < items.length; i++) {
81
98
  const e = items[i];
82
99
  const textW = measureFontTextWidth(font, e.label);
83
- const x = badge
84
- ? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
85
- : seriesLabelInset > 0
86
- ? gutterRightAlignedTextLeftX(w, textW)
87
- : gutterCenteredTextLeftX(w, padding.right, textW);
100
+ const x = float
101
+ ? gutterRightAlignedTextLeftX(w, textW, FLOAT_LABEL_RIGHT_MARGIN)
102
+ : badge
103
+ ? pillTextLeftX(w, padding.right, leftInset, textW, badgeMetrics)
104
+ : seriesLabelInset > 0
105
+ ? gutterRightAlignedTextLeftX(w, textW)
106
+ : gutterCenteredTextLeftX(w, padding.right, textW);
88
107
  result.push({
89
108
  x,
90
109
  y: e.y - baselineOffset,
@@ -97,27 +116,30 @@ export function YAxisOverlay({
97
116
 
98
117
  return (
99
118
  <Group>
100
- <Group opacity={gridOpacity}>
101
- <Path
102
- path={gridLinesPath}
103
- style="stroke"
104
- strokeWidth={gridWidth}
105
- color={gridColor}
106
- >
107
- {gridIntervals.length > 0 && (
108
- <DashPathEffect intervals={gridIntervals} />
109
- )}
110
- </Path>
111
- </Group>
112
- {Array.from({ length: MAX_Y_LABELS }, (_, i) => (
113
- <AnimatedLabel
114
- key={i}
115
- entries={labelEntries}
116
- index={i}
117
- font={font}
118
- color={palette.gridLabel}
119
- />
120
- ))}
119
+ {variant !== "labels" && (
120
+ <Group opacity={gridOpacity}>
121
+ <Path
122
+ path={gridLinesPath}
123
+ style="stroke"
124
+ strokeWidth={gridWidth}
125
+ color={gridColor}
126
+ >
127
+ {gridIntervals.length > 0 && (
128
+ <DashPathEffect intervals={gridIntervals} />
129
+ )}
130
+ </Path>
131
+ </Group>
132
+ )}
133
+ {variant !== "grid" &&
134
+ Array.from({ length: MAX_Y_LABELS }, (_, i) => (
135
+ <AnimatedLabel
136
+ key={i}
137
+ entries={labelEntries}
138
+ index={i}
139
+ font={font}
140
+ color={palette.gridLabel}
141
+ />
142
+ ))}
121
143
  </Group>
122
144
  );
123
145
  }
package/src/constants.ts CHANGED
@@ -9,6 +9,14 @@ import type {
9
9
  /** Milliseconds per frame at 60 fps — baseline for frame-rate-independent lerp. */
10
10
  export const MS_PER_FRAME_60FPS = 16.67;
11
11
 
12
+ /**
13
+ * Default press-and-hold (ms) before scrub engages in the `holdToScrub`
14
+ * time-scroll mode, so a quick one-finger drag scrolls instead. Overridden by
15
+ * `timeScroll.scrubHoldMs`, then `scrub.panGestureDelay`. Shared by `LiveChart`
16
+ * and `LiveChartSeries`.
17
+ */
18
+ export const HOLD_TO_SCRUB_MS = 500;
19
+
12
20
  // ─── Metric default tokens (single source of truth for LiveChartMetrics) ─────
13
21
  // The resolved `metrics` config (see resolveMetrics) is assembled from these
14
22
  // objects. Draw/worklet helpers default their metric params to the matching
@@ -29,6 +37,8 @@ export const CANDLE_METRICS_DEFAULTS: CandleMetrics = {
29
37
  minBodyPx: 1,
30
38
  maxBodyPx: 40,
31
39
  bodyWidthRatio: 0.8,
40
+ bodyRadius: 0,
41
+ wickWidth: 1,
32
42
  };
33
43
 
34
44
  /** Default grid + axis-label fade speeds. */