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
@@ -0,0 +1,250 @@
1
+ import { StyleSheet, View, type LayoutChangeEvent } from "react-native";
2
+ import Animated, {
3
+ useAnimatedStyle,
4
+ useDerivedValue,
5
+ useSharedValue,
6
+ type SharedValue,
7
+ } from "react-native-reanimated";
8
+
9
+ import type { ChartEngineLayout } from "../core/useLiveChartEngine";
10
+ import type { ChartPadding } from "../draw/line";
11
+ import { computeScrubDotY } from "../hooks/crosshairShared";
12
+ import {
13
+ classifyReferenceEdge,
14
+ referenceLineForm,
15
+ resolveReferenceBadge,
16
+ } from "../math/referenceLines";
17
+ import type { ReferenceLine, ReferenceLineRenderProps } from "../types";
18
+
19
+ /** Horizontal anchor for the floated tag, mirroring the built-in badge/label. */
20
+ type HAnchor = "left" | "center" | "right";
21
+
22
+ /** Pin gap from the anchored plot edge, in px (matches the badge edge inset). */
23
+ const ANCHOR_INSET = 2;
24
+ /** Off-screen edge inset for the pinned handle, in px (matches the off-axis badge). */
25
+ const EDGE_INSET = 12;
26
+
27
+ /** Where a custom tag pins horizontally: the badge position, else the label
28
+ * position, else inside the left edge. */
29
+ function resolveAnchor(line: ReferenceLine): HAnchor {
30
+ const badge = resolveReferenceBadge(line);
31
+ if (badge) return badge.position;
32
+ return line.labelPosition ?? "left";
33
+ }
34
+
35
+ /** Lightweight stand-in SharedValue for the suppression probe (see
36
+ * {@link customReferenceLineFlags}); never bound to the UI thread, so only the
37
+ * read accessors a render might touch are provided. */
38
+ function stub<T>(v: T): SharedValue<T> {
39
+ return { value: v, get: () => v } as unknown as SharedValue<T>;
40
+ }
41
+
42
+ /**
43
+ * Which Form-A reference lines a `renderReferenceLine` returns an element for —
44
+ * the set whose built-in Skia tag is suppressed (so there's no double-draw). The
45
+ * render fn is probed with placeholder SharedValues; it must decide null-ness from
46
+ * `line` / `index`, not from live values (mirrors how `renderMarker` is probed for
47
+ * the marker-atlas exclusion set). Index-aligned with `lines`.
48
+ */
49
+ export function customReferenceLineFlags(
50
+ lines: ReferenceLine[],
51
+ render?: (
52
+ ctx: ReferenceLineRenderProps,
53
+ ) => React.ReactElement | null | undefined,
54
+ ): boolean[] {
55
+ if (!render) return lines.map(() => false);
56
+ return lines.map((line, index) => {
57
+ if (referenceLineForm(line) !== "line") return false;
58
+ return (
59
+ render({
60
+ line,
61
+ index,
62
+ value: stub(line.value ?? 0),
63
+ valueStr: stub(""),
64
+ y: stub(-1),
65
+ inRange: stub(true),
66
+ edge: stub<"above" | "in" | "below">("in"),
67
+ dragging: stub(false),
68
+ }) != null
69
+ );
70
+ });
71
+ }
72
+
73
+ /**
74
+ * One custom-rendered reference-line tag: a React Native element floated over the
75
+ * canvas, vertically centered on the line's value-Y and horizontally pinned to the
76
+ * badge / label position. Mirrors `CustomMarkerView` — each tag projects its own
77
+ * value every frame, so the transform runs on the UI thread with no JS re-render
78
+ * as the chart rescales or the line is dragged. `pointerEvents="box-none"` lets
79
+ * empty space fall through to the chart gestures while an interactive leaf inside
80
+ * the custom element can still be touched.
81
+ */
82
+ function CustomReferenceLineView({
83
+ line,
84
+ index,
85
+ render,
86
+ engine,
87
+ padding,
88
+ formatValue,
89
+ dragValues,
90
+ dragActive,
91
+ }: {
92
+ line: ReferenceLine;
93
+ index: number;
94
+ render: (
95
+ ctx: ReferenceLineRenderProps,
96
+ ) => React.ReactElement | null | undefined;
97
+ engine: ChartEngineLayout;
98
+ padding: ChartPadding;
99
+ formatValue: (v: number) => string;
100
+ dragValues: SharedValue<number[]>;
101
+ dragActive: SharedValue<boolean[]>;
102
+ }) {
103
+ const staticValue = line.value ?? 0;
104
+
105
+ // Live value: a drag override (if present) else the static prop value.
106
+ const value = useDerivedValue<number>(() => {
107
+ const dv = dragValues.get()[index];
108
+ return dv != null ? dv : staticValue;
109
+ });
110
+ const valueStr = useDerivedValue(() => formatValue(value.get()));
111
+ const edge = useDerivedValue<"above" | "in" | "below">(() =>
112
+ classifyReferenceEdge(
113
+ value.get(),
114
+ engine.displayMin.get(),
115
+ engine.displayMax.get(),
116
+ ),
117
+ );
118
+ const inRange = useDerivedValue(() => edge.get() === "in");
119
+ const dragging = useDerivedValue(() => dragActive.get()[index] === true);
120
+
121
+ // Canvas Y of the value (the element's vertical center). Off-screen values pin
122
+ // to the nearest plot edge (inset), matching the built-in off-axis badge.
123
+ const y = useDerivedValue(() => {
124
+ const ch = engine.canvasHeight.get();
125
+ const raw = computeScrubDotY(
126
+ value.get(),
127
+ engine.displayMin.get(),
128
+ engine.displayMax.get(),
129
+ ch,
130
+ padding.top,
131
+ padding.bottom,
132
+ );
133
+ if (raw < 0) return -1;
134
+ const top = padding.top + EDGE_INSET;
135
+ const bottom = ch - padding.bottom - EDGE_INSET;
136
+ return Math.min(bottom, Math.max(top, raw));
137
+ });
138
+
139
+ // Measured element size, so the transform can center / pin it.
140
+ const size = useSharedValue({ width: 0, height: 0 });
141
+ const onLayout = (e: LayoutChangeEvent) => {
142
+ const { width, height } = e.nativeEvent.layout;
143
+ size.value = { width, height };
144
+ };
145
+
146
+ const anchor = resolveAnchor(line);
147
+ const animatedStyle = useAnimatedStyle(() => {
148
+ const yy = y.get();
149
+ const s = size.get();
150
+ const w = engine.canvasWidth.get();
151
+ const visible = yy >= 0 && w > 0;
152
+ const x1 = padding.left;
153
+ const x2 = w - padding.right;
154
+ let tx: number;
155
+ if (anchor === "right") tx = x2 - ANCHOR_INSET - s.width;
156
+ else if (anchor === "center") tx = (x1 + x2) / 2 - s.width / 2;
157
+ else tx = x1 + ANCHOR_INSET;
158
+ return {
159
+ opacity: visible ? 1 : 0,
160
+ transform: [{ translateX: tx }, { translateY: yy - s.height / 2 }],
161
+ };
162
+ });
163
+
164
+ const element = render({
165
+ line,
166
+ index,
167
+ value,
168
+ valueStr,
169
+ y,
170
+ inRange,
171
+ edge,
172
+ dragging,
173
+ });
174
+ if (element == null) return null;
175
+
176
+ return (
177
+ <Animated.View
178
+ pointerEvents="box-none"
179
+ onLayout={onLayout}
180
+ style={[styles.anchor, animatedStyle]}
181
+ >
182
+ {element}
183
+ </Animated.View>
184
+ );
185
+ }
186
+
187
+ /**
188
+ * React Native overlay (NOT Skia) that floats `renderReferenceLine` elements over
189
+ * the canvas, one per Form-A line the consumer customizes. Rendered as a sibling
190
+ * of `<Canvas>` (like {@link CustomMarkerOverlay}) so the tags can be any RN view
191
+ * and stay crisp at native resolution. Lines whose render returns an element have
192
+ * their built-in Skia tag suppressed upstream (see {@link customReferenceLineFlags}),
193
+ * so there's no double-draw.
194
+ */
195
+ export function CustomReferenceLineOverlay({
196
+ lines,
197
+ renderReferenceLine,
198
+ custom,
199
+ engine,
200
+ padding,
201
+ formatValue,
202
+ dragValues,
203
+ dragActive,
204
+ }: {
205
+ lines: ReferenceLine[];
206
+ renderReferenceLine: (
207
+ ctx: ReferenceLineRenderProps,
208
+ ) => React.ReactElement | null | undefined;
209
+ /**
210
+ * Index-aligned flags (from {@link customReferenceLineFlags}) marking which
211
+ * lines the render owns — only these get a floated view, matching the built-in
212
+ * tags suppressed upstream. Avoids re-probing the render here.
213
+ */
214
+ custom: boolean[];
215
+ engine: ChartEngineLayout;
216
+ padding: ChartPadding;
217
+ formatValue: (v: number) => string;
218
+ dragValues: SharedValue<number[]>;
219
+ dragActive: SharedValue<boolean[]>;
220
+ }) {
221
+ const children: React.ReactElement[] = [];
222
+ for (let i = 0; i < lines.length; i++) {
223
+ if (!custom[i]) continue;
224
+ children.push(
225
+ <CustomReferenceLineView
226
+ key={i}
227
+ line={lines[i]}
228
+ index={i}
229
+ render={renderReferenceLine}
230
+ engine={engine}
231
+ padding={padding}
232
+ formatValue={formatValue}
233
+ dragValues={dragValues}
234
+ dragActive={dragActive}
235
+ />,
236
+ );
237
+ }
238
+ if (children.length === 0) return null;
239
+
240
+ return (
241
+ <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
242
+ {children}
243
+ </View>
244
+ );
245
+ }
246
+
247
+ const styles = StyleSheet.create({
248
+ // top/left 0 + translate so the measured element can be pinned to the value.
249
+ anchor: { position: "absolute", top: 0, left: 0 },
250
+ });
@@ -48,6 +48,7 @@ export function CustomTooltipOverlay({
48
48
  placement,
49
49
  margin = 8,
50
50
  lineTop,
51
+ scrubDotY,
51
52
  }: {
52
53
  renderTooltip: (ctx: TooltipRenderProps) => React.ReactElement | null | undefined;
53
54
  scrubX: SharedValue<number>;
@@ -60,13 +61,16 @@ export function CustomTooltipOverlay({
60
61
  tooltipLayout: SharedValue<TooltipLayout>;
61
62
  engine: ChartEngineLayout;
62
63
  padding: ChartPadding;
63
- placement: "side" | "top" | "bottom";
64
+ placement: "side" | "top" | "bottom" | "point";
64
65
  /** Gap (px) between the pill and the plot edge it's pinned to. Default 8. */
65
66
  margin?: number;
66
67
  /** When `placement` is `"top"`, the overlay publishes the label's bottom edge
67
68
  * (canvas Y) here so {@link CrosshairOverlay} can stop the crosshair line at
68
69
  * the label instead of running through it; -1 when not top-pinned/active. */
69
70
  lineTop?: SharedValue<number>;
71
+ /** Scrub intersection Y in canvas px (the value the dot marks) — the anchor
72
+ * for `"point"` placement. Omitted / -1 falls back to a top pin. */
73
+ scrubDotY?: SharedValue<number>;
70
74
  }) {
71
75
  // Formatted strings come ready-made off the layout (formatted UI-side in
72
76
  // computeTooltipLayout / computeCandleTooltipLayout), so consumers don't need
@@ -125,12 +129,28 @@ export function CustomTooltipOverlay({
125
129
  Math.max(sx - s.width / 2, leftBound),
126
130
  Math.max(leftBound, rightBound),
127
131
  );
128
- y =
129
- placement === "top"
130
- ? // Pin to the canvas top edge (not the plot's inner top), so the label
131
- // sits above the data and the crosshair line can stop at it.
132
- margin
133
- : ch - padding.bottom - margin - s.height;
132
+ if (placement === "point") {
133
+ // Float just above the scrub dot, flipping below when there's no room
134
+ // above; clamp into the plot. Mirrors computeTooltipLayout's "point".
135
+ const dotY = scrubDotY?.get() ?? -1;
136
+ if (dotY < 0) {
137
+ y = padding.top + margin;
138
+ } else {
139
+ const topLimit = padding.top + EDGE_GAP;
140
+ const bottomLimit = ch - padding.bottom - EDGE_GAP - s.height;
141
+ const aboveY = dotY - margin - s.height;
142
+ const belowY = dotY + margin;
143
+ y = aboveY >= topLimit ? aboveY : belowY;
144
+ y = Math.min(Math.max(y, topLimit), Math.max(topLimit, bottomLimit));
145
+ }
146
+ } else {
147
+ y =
148
+ placement === "top"
149
+ ? // Pin to the canvas top edge (not the plot's inner top), so the
150
+ // label sits above the data and the crosshair line can stop at it.
151
+ margin
152
+ : ch - padding.bottom - margin - s.height;
153
+ }
134
154
  }
135
155
 
136
156
  return {
@@ -24,6 +24,7 @@ export function DotOverlay({
24
24
  radius,
25
25
  ring,
26
26
  color,
27
+ viewEnd,
27
28
  }: {
28
29
  dotX: SharedValue<number>;
29
30
  dotY: SharedValue<number>;
@@ -36,11 +37,18 @@ export function DotOverlay({
36
37
  ring: ResolvedDotRingConfig | null;
37
38
  /** Dot (and pulse) fill color; falls back to the chart line color. */
38
39
  color: string | undefined;
40
+ /**
41
+ * Time-scroll right edge (`null` = following live). While scrolled back the
42
+ * pulse is suppressed — it's driven by the (now frozen) view timestamp, so it
43
+ * would stick or flicker, and a "live" heartbeat on a historical point is wrong.
44
+ */
45
+ viewEnd?: SharedValue<number | null>;
39
46
  }) {
40
47
  const dotColor = color ?? palette.line;
41
48
 
42
49
  const pulseRadius = useDerivedValue(() => {
43
50
  if (!pulse) return 0;
51
+ if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
44
52
  const nowMs = engine.timestamp.value * 1000;
45
53
  const t = (nowMs % pulse.interval) / pulse.duration;
46
54
  if (t >= 1) return 0;
@@ -49,6 +57,7 @@ export function DotOverlay({
49
57
 
50
58
  const pulseOpacity = useDerivedValue(() => {
51
59
  if (!pulse) return 0;
60
+ if (viewEnd?.value != null) return 0; // scrolled back — no live pulse
52
61
  const nowMs = engine.timestamp.value * 1000;
53
62
  const t = (nowMs % pulse.interval) / pulse.duration;
54
63
  if (t >= 1) return 0;