react-native-livechart 4.21.0 → 4.22.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 (78) hide show
  1. package/dist/components/AxisLabelOverlay.d.ts +5 -1
  2. package/dist/components/AxisLabelOverlay.d.ts.map +1 -1
  3. package/dist/components/CrosshairOverlay.d.ts +4 -2
  4. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  5. package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
  6. package/dist/components/CustomThresholdBadgeOverlay.d.ts +3 -1
  7. package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -1
  8. package/dist/components/DegenParticlesOverlay.d.ts.map +1 -1
  9. package/dist/components/ExtremaConnectorOverlay.d.ts +5 -1
  10. package/dist/components/ExtremaConnectorOverlay.d.ts.map +1 -1
  11. package/dist/components/LiveChart.d.ts.map +1 -1
  12. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  13. package/dist/components/MarkerOverlay.d.ts +4 -2
  14. package/dist/components/MarkerOverlay.d.ts.map +1 -1
  15. package/dist/components/ReferenceLineOverlay.d.ts +1 -1
  16. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  17. package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -1
  18. package/dist/components/ThresholdLineOverlay.d.ts +3 -1
  19. package/dist/components/ThresholdLineOverlay.d.ts.map +1 -1
  20. package/dist/components/thresholdSplitShaderSource.d.ts.map +1 -1
  21. package/dist/core/chartDataPresence.d.ts +8 -0
  22. package/dist/core/chartDataPresence.d.ts.map +1 -0
  23. package/dist/core/liveChartEngineTick.d.ts +3 -0
  24. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  25. package/dist/core/resolveConfig.d.ts +2 -0
  26. package/dist/core/resolveConfig.d.ts.map +1 -1
  27. package/dist/core/useLiveChartEngine.d.ts +4 -0
  28. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  29. package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
  30. package/dist/draw/markerAtlas.d.ts +3 -2
  31. package/dist/draw/markerAtlas.d.ts.map +1 -1
  32. package/dist/hooks/useChartReveal.d.ts +1 -1
  33. package/dist/hooks/useChartReveal.d.ts.map +1 -1
  34. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  35. package/dist/hooks/useDegen.d.ts.map +1 -1
  36. package/dist/hooks/useLiveChartHasData.d.ts +2 -2
  37. package/dist/hooks/useLiveChartHasData.d.ts.map +1 -1
  38. package/dist/hooks/useReferenceLineSeries.d.ts +1 -1
  39. package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -1
  40. package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -1
  41. package/dist/hooks/useThreshold.d.ts +10 -5
  42. package/dist/hooks/useThreshold.d.ts.map +1 -1
  43. package/dist/math/referenceLineSeries.d.ts +1 -1
  44. package/dist/math/referenceLineSeries.d.ts.map +1 -1
  45. package/dist/math/threshold.d.ts +4 -3
  46. package/dist/math/threshold.d.ts.map +1 -1
  47. package/dist/types.d.ts +23 -8
  48. package/dist/types.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/AxisLabelOverlay.tsx +162 -81
  51. package/src/components/CrosshairOverlay.tsx +394 -147
  52. package/src/components/CustomMarkerOverlay.tsx +9 -6
  53. package/src/components/CustomThresholdBadgeOverlay.tsx +11 -4
  54. package/src/components/DegenParticlesOverlay.tsx +34 -23
  55. package/src/components/ExtremaConnectorOverlay.tsx +43 -3
  56. package/src/components/LiveChart.tsx +1709 -1025
  57. package/src/components/LiveChartSeries.tsx +445 -296
  58. package/src/components/MarkerOverlay.tsx +215 -156
  59. package/src/components/ReferenceLineOverlay.tsx +77 -50
  60. package/src/components/ReferenceLineSeriesOverlay.tsx +2 -0
  61. package/src/components/ThresholdLineOverlay.tsx +16 -6
  62. package/src/components/thresholdSplitShaderSource.ts +4 -1
  63. package/src/core/chartDataPresence.ts +24 -0
  64. package/src/core/liveChartEngineTick.ts +4 -0
  65. package/src/core/resolveConfig.ts +3 -0
  66. package/src/core/useLiveChartEngine.ts +16 -3
  67. package/src/core/useLiveChartSeriesEngine.ts +2 -0
  68. package/src/draw/markerAtlas.ts +8 -3
  69. package/src/hooks/useChartReveal.ts +1 -1
  70. package/src/hooks/useCrosshair.ts +69 -46
  71. package/src/hooks/useDegen.ts +82 -53
  72. package/src/hooks/useLiveChartHasData.ts +8 -4
  73. package/src/hooks/useReferenceLineSeries.ts +5 -0
  74. package/src/hooks/useReverseMorphEngineInputs.ts +11 -20
  75. package/src/hooks/useThreshold.ts +41 -15
  76. package/src/math/referenceLineSeries.ts +9 -4
  77. package/src/math/threshold.ts +10 -3
  78. package/src/types.ts +23 -8
@@ -22,6 +22,8 @@ export type ThresholdValue = SharedValue<number> | LiveChartPoint[];
22
22
 
23
23
  /** `clipRight` sentinel when the threshold extends to "now" (no forward cutoff). */
24
24
  export const THRESHOLD_NO_CLIP = 1e9;
25
+ /** `clipLeft` sentinel when the threshold extends to the window start. */
26
+ export const THRESHOLD_NO_CLIP_LEFT = -1e9;
25
27
 
26
28
  export interface ThresholdGeometry {
27
29
  /** Threshold pixel-Y within the canvas, or NaN before layout / degenerate range. */
@@ -104,6 +106,9 @@ export interface ThresholdSeriesGeometry {
104
106
  * part of the threshold. (`visible` can still be true when that one endpoint
105
107
  * is off-plot.) */
106
108
  badgeVisible: SharedValue<boolean>;
109
+ /** Pixel-X where the threshold starts: the first point's X with
110
+ * `extendToStart` off, else {@link THRESHOLD_NO_CLIP_LEFT}. */
111
+ clipLeftX: SharedValue<number>;
107
112
  /** Pixel-X where the threshold ends: the last point's X with `extendToNow`
108
113
  * off, else {@link THRESHOLD_NO_CLIP}. The shader paints its plain
109
114
  * `restColor` right of it; the marker polyline stops there. */
@@ -119,7 +124,7 @@ const EMPTY_SAMPLES: number[] = new Array(THRESHOLD_SAMPLE_COUNT).fill(0);
119
124
  * `LiveChartPoint[]` `value` or a live `SharedValue<LiveChartPoint[]>` `series`
120
125
  * (which wins when both are given): the screen polyline (marker line +
121
126
  * fill-band bottom), the shader's pixel-Y `samples[]`, the configured endpoint
122
- * value/anchor for the badge, and the `extendToNow` cutoff X. The array buffers
127
+ * value/anchor for the badge, and the endpoint cutoff X values. The array buffers
123
128
  * ping-pong (Reanimated only re-notifies subscribers when the returned reference
124
129
  * changes). When the threshold is a constant `SharedValue<number>` every
125
130
  * worklet short-circuits cheaply and {@link useThreshold} drives the render
@@ -131,6 +136,7 @@ export function useThresholdSeries(
131
136
  value: ThresholdValue,
132
137
  series: SharedValue<LiveChartPoint[]> | null = null,
133
138
  extendToNow = true,
139
+ extendToStart = true,
134
140
  labelAnchor: "first" | "last" = "last",
135
141
  ): ThresholdSeriesGeometry {
136
142
  const cacheRef = useRef<{
@@ -176,6 +182,19 @@ export function useThresholdSeries(
176
182
  );
177
183
  });
178
184
 
185
+ const clipLeftX = useDerivedValue(() => {
186
+ if (extendToStart) return THRESHOLD_NO_CLIP_LEFT;
187
+ const pts = series ? series.get() : Array.isArray(value) ? value : null;
188
+ if (pts === null || pts.length === 0) return THRESHOLD_NO_CLIP_LEFT;
189
+ const win = engine.displayWindow.get();
190
+ const plotLeft = padding.left;
191
+ const plotRight = engine.canvasWidth.get() - padding.right;
192
+ if (!(win > 0) || plotRight <= plotLeft) return THRESHOLD_NO_CLIP_LEFT;
193
+ const winStart = engine.timestamp.get() - win;
194
+ const firstT = pts[0].time;
195
+ return plotLeft + ((firstT - winStart) / win) * (plotRight - plotLeft);
196
+ });
197
+
179
198
  const clipRightX = useDerivedValue(() => {
180
199
  if (extendToNow) return THRESHOLD_NO_CLIP;
181
200
  const pts = series ? series.get() : Array.isArray(value) ? value : null;
@@ -206,8 +225,9 @@ export function useThresholdSeries(
206
225
  if (n < 2) return EMPTY_PTS;
207
226
  const plotLeft = padding.left;
208
227
  const plotRight = engine.canvasWidth.get() - padding.right;
228
+ const startX = Math.max(plotLeft, clipLeftX.get());
209
229
  const endX = Math.min(plotRight, clipRightX.get());
210
- if (endX <= plotLeft) return EMPTY_PTS;
230
+ if (endX <= startX) return EMPTY_PTS;
211
231
  const cache = cacheRef.current!;
212
232
  cache.ptsTick = !cache.ptsTick;
213
233
  const buf = cache.ptsTick ? cache.ptsA : cache.ptsB;
@@ -219,11 +239,11 @@ export function useThresholdSeries(
219
239
  plotRight,
220
240
  n,
221
241
  );
222
- buf.push(plotLeft, sampleThresholdYAt(s, x0, x1, plotLeft));
242
+ buf.push(startX, sampleThresholdYAt(s, x0, x1, startX));
223
243
  const step = (x1 - x0) / (n - 1);
224
244
  for (let i = 0; i < n; i++) {
225
245
  const sx = x0 + step * i;
226
- if (sx > plotLeft && sx < endX) buf.push(sx, s[i]);
246
+ if (sx > startX && sx < endX) buf.push(sx, s[i]);
227
247
  }
228
248
  buf.push(endX, sampleThresholdYAt(s, x0, x1, endX));
229
249
  return buf;
@@ -242,10 +262,13 @@ export function useThresholdSeries(
242
262
  const badgeValue = useDerivedValue(() => {
243
263
  const pts = series ? series.get() : Array.isArray(value) ? value : null;
244
264
  if (pts === null) return NaN;
245
- const time =
246
- labelAnchor === "first"
247
- ? engine.timestamp.get() - engine.displayWindow.get()
248
- : engine.timestamp.get();
265
+ let time = engine.timestamp.get();
266
+ if (labelAnchor === "first") {
267
+ time -= engine.displayWindow.get();
268
+ if (!extendToStart && pts.length > 0 && pts[0].time > time) {
269
+ time = pts[0].time;
270
+ }
271
+ }
249
272
  return interpolateAtTime(pts, time) ?? NaN;
250
273
  });
251
274
 
@@ -261,11 +284,11 @@ export function useThresholdSeries(
261
284
  );
262
285
 
263
286
  const badgeVisible = useDerivedValue(() => {
264
- if (labelAnchor === "first") {
265
- // If the non-extended series ended before this window, there is no left
266
- // endpoint to label even though interpolation can still clamp a value.
267
- if (screenPts.get().length < 4) return false;
268
- } else if (!extendToNow) {
287
+ // There is no endpoint to label when the series does not intersect the
288
+ // effective [first point, last point] window, even though interpolation can
289
+ // still clamp a value outside it.
290
+ if (screenPts.get().length < 4) return false;
291
+ if (labelAnchor === "last" && !extendToNow) {
269
292
  // The threshold ends at its last point — no badge past it.
270
293
  const pts = series ? series.get() : Array.isArray(value) ? value : null;
271
294
  if (pts === null || pts.length === 0) return false;
@@ -286,6 +309,7 @@ export function useThresholdSeries(
286
309
  badgeValue,
287
310
  badgeLineY,
288
311
  badgeVisible,
312
+ clipLeftX,
289
313
  clipRightX,
290
314
  };
291
315
  }
@@ -295,8 +319,8 @@ export function useThresholdSeries(
295
319
  * uniforms (one `SharedValue` per paint — stroke vs. the alpha-reduced fill band).
296
320
  * The object is rebuilt each frame so the shader re-paints as `samples` advance;
297
321
  * `sampleLeft`/`sampleRight` are the gliding pixel-X span of the sample grid and
298
- * `clipRight`/`restColor` implement the `extendToNow: false` cutoff (plain line
299
- * color for the stroke, transparent for the band).
322
+ * `clipLeft`/`clipRight`/`restColor` implement the series endpoint cutoffs
323
+ * (plain line color for the stroke, transparent for the band).
300
324
  */
301
325
  export function useThresholdSplitUniforms(
302
326
  samples: SharedValue<number[]>,
@@ -305,6 +329,7 @@ export function useThresholdSplitUniforms(
305
329
  aboveColor: number[],
306
330
  belowColor: number[],
307
331
  restColor: number[],
332
+ clipLeftX: SharedValue<number>,
308
333
  clipRightX: SharedValue<number>,
309
334
  ): SharedValue<Uniforms> {
310
335
  return useDerivedValue<Uniforms>(() => {
@@ -319,6 +344,7 @@ export function useThresholdSplitUniforms(
319
344
  return {
320
345
  sampleLeft: x0,
321
346
  sampleRight: x1,
347
+ clipLeft: clipLeftX.get(),
322
348
  clipRight: clipRightX.get(),
323
349
  aboveColor,
324
350
  belowColor,
@@ -20,6 +20,7 @@ export function buildReferenceLineSeriesPoints(
20
20
  canvasWidth: number,
21
21
  canvasHeight: number,
22
22
  padding: ChartPadding,
23
+ extendToStart: boolean,
23
24
  extendToNow: boolean,
24
25
  out?: number[],
25
26
  ): number[] {
@@ -43,11 +44,15 @@ export function buildReferenceLineSeriesPoints(
43
44
  }
44
45
 
45
46
  const windowStart = now - windowSecs;
47
+ const firstTime = points[0].time;
46
48
  const lastTime = points[points.length - 1].time;
49
+ const startTime = extendToStart
50
+ ? windowStart
51
+ : Math.max(windowStart, firstTime);
47
52
  const endTime = extendToNow ? now : Math.min(now, lastTime);
48
- if (endTime < windowStart) return result;
53
+ if (endTime < startTime) return result;
49
54
 
50
- const startValue = interpolateAtTime(points, windowStart);
55
+ const startValue = interpolateAtTime(points, startTime);
51
56
  const endValue = interpolateAtTime(points, endTime);
52
57
  if (
53
58
  startValue === null ||
@@ -70,10 +75,10 @@ export function buildReferenceLineSeriesPoints(
70
75
  padding.bottom,
71
76
  );
72
77
 
73
- result.push(plotLeft, toY(startValue));
78
+ result.push(toX(startTime), toY(startValue));
74
79
  for (let i = 0; i < points.length; i++) {
75
80
  const point = points[i];
76
- if (point.time <= windowStart) continue;
81
+ if (point.time <= startTime) continue;
77
82
  if (point.time >= endTime) break;
78
83
  if (!Number.isFinite(point.time) || !Number.isFinite(point.value)) continue;
79
84
  result.push(toX(point.time), toY(point.value));
@@ -237,8 +237,9 @@ export function thresholdDashPhase(
237
237
  * Min/max of a threshold series over the visible window `[now - windowSecs,
238
238
  * now]` — the values folded into the engine's Y-range fit when
239
239
  * `threshold.includeInRange` is set (like reference-line values). Uses the
240
- * clamped window-edge values plus every interior point; with `extendToNow` off,
241
- * the window end clamps to the series' last point (nothing projects forward).
240
+ * clamped window-edge values plus every interior point. With `extendToStart`
241
+ * off, the window start clamps to the series' first point; with `extendToNow`
242
+ * off, the window end clamps to its last point.
242
243
  * Writes `[min, max]` into `out` and returns it, or returns null when the
243
244
  * series is empty / entirely outside the effective window / non-finite.
244
245
  */
@@ -246,18 +247,24 @@ export function thresholdRangeMinMax(
246
247
  points: LiveChartPoint[],
247
248
  now: number,
248
249
  windowSecs: number,
250
+ extendToStart: boolean,
249
251
  extendToNow: boolean,
250
252
  out: [number, number],
251
253
  ): [number, number] | null {
252
254
  "worklet";
253
255
  if (points.length === 0 || !(windowSecs > 0)) return null;
254
- const tStart = now - windowSecs;
256
+ let tStart = now - windowSecs;
255
257
  let tEnd = now;
258
+ if (!extendToStart) {
259
+ const firstT = points[0].time;
260
+ if (firstT > tStart) tStart = firstT;
261
+ }
256
262
  if (!extendToNow) {
257
263
  const lastT = points[points.length - 1].time;
258
264
  if (lastT < tStart) return null;
259
265
  if (lastT < tEnd) tEnd = lastT;
260
266
  }
267
+ if (tEnd < tStart) return null;
261
268
  let mn = interpolateAtTime(points, tStart)!;
262
269
  let mx = mn;
263
270
  const vEnd = interpolateAtTime(points, tEnd)!;
package/src/types.ts CHANGED
@@ -74,10 +74,16 @@ export interface ReferenceLine {
74
74
  /**
75
75
  * Form B — a time-varying reference line. Points use unix-second timestamps
76
76
  * and should be sorted oldest to newest. The first value extends to the left
77
- * edge; the last value extends to the live edge unless {@link extendToNow} is
78
- * `false`. Supported by line and candle charts.
77
+ * edge unless {@link extendToStart} is `false`; the last value extends to the
78
+ * live edge unless {@link extendToNow} is `false`. Supported by line and
79
+ * candle charts.
79
80
  */
80
81
  series?: LiveChartPoint[];
82
+ /**
83
+ * Form B — extend the series' first value flat to the chart's left edge.
84
+ * Set `false` to start at the first point. Default `true`.
85
+ */
86
+ extendToStart?: boolean;
81
87
  /**
82
88
  * Form B — extend the series' last value flat to the chart's live edge.
83
89
  * Set `false` to stop at the last point. Default `true`.
@@ -493,8 +499,8 @@ export interface ThresholdConfig {
493
499
  * - **`LiveChartPoint[]`** — a *time-varying* threshold (e.g. a historical
494
500
  * break-even that steps up as you average in). The stroke split, fill band
495
501
  * and marker line follow the series point-for-point. The series clamps to its
496
- * first/last value outside its own time range, so a threshold whose last
497
- * point sits behind the live edge extends as a flat line to "now" (see
502
+ * first/last value outside its own time range, so it extends flat to the
503
+ * visible edges by default (see {@link extendToStart} and
498
504
  * {@link extendToNow}). Flows in on re-render — pass a stable (memoized)
499
505
  * array and reserve it for thresholds that change occasionally; for a
500
506
  * threshold series that updates live, use {@link series} instead.
@@ -531,10 +537,19 @@ export interface ThresholdConfig {
531
537
  * Fold the threshold into the Y-axis range fit — like reference lines — so a
532
538
  * benchmark outside the data's own range stays on-plot instead of rendering
533
539
  * invisibly (marker off-plot, whole line one color). For a series, the values
534
- * visible in the current window count (respecting {@link extendToNow}).
540
+ * visible in the current window count (respecting {@link extendToStart} and
541
+ * {@link extendToNow}).
535
542
  * Default `false` (range fits the data only).
536
543
  */
537
544
  includeInRange?: boolean;
545
+ /**
546
+ * Series forms only: extend the threshold **flat before its first point to the
547
+ * visible window's left edge**, carrying the first known benchmark backward.
548
+ * Set `false` when the benchmark did not exist yet (for example, a break-even
549
+ * before the first trade) — left of the first point the stroke keeps its plain
550
+ * line color and the band / marker do not begin. Default `true`.
551
+ */
552
+ extendToStart?: boolean;
538
553
  /**
539
554
  * Series forms only: extend the threshold **flat past its last point to
540
555
  * "now"**, carrying the last known benchmark forward. Set `false` for a
@@ -2261,7 +2276,7 @@ export interface LiveChartCoreProps {
2261
2276
  transitions?: boolean | TransitionConfig;
2262
2277
  /**
2263
2278
  * Breathing-line loading shell. When this becomes `false`, the chart reveals
2264
- * only if there is data (≥2 line points or ≥2 committed candles).
2279
+ * only if there is data (≥1 line point or ≥1 committed candle).
2265
2280
  *
2266
2281
  * `true` shows the shell with the defaults; pass a {@link LoadingConfig} to
2267
2282
  * restyle it — `color` / `strokeWidth` for the squiggle + skeleton, `amplitude`
@@ -2330,8 +2345,8 @@ export interface LiveChartCoreProps {
2330
2345
  */
2331
2346
  yRangeScale?: SharedValue<number>;
2332
2347
  /**
2333
- * Label in the empty state when `loading` is false and there are fewer than
2334
- * two samples (line points or committed candles). Default `"No data"`.
2348
+ * Label in the empty state when `loading` is false and there are no line
2349
+ * points or committed candles. Default `"No data"`.
2335
2350
  */
2336
2351
  emptyText?: string;
2337
2352
  /** Custom formatter for value labels (axes, badge, tooltips). Default `v => v.toFixed(2)`. */