react-native-livechart 4.18.0 → 4.20.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 (70) hide show
  1. package/dist/components/CrosshairOverlay.d.ts +3 -1
  2. package/dist/components/CrosshairOverlay.d.ts.map +1 -1
  3. package/dist/components/DotOverlay.d.ts +4 -2
  4. package/dist/components/DotOverlay.d.ts.map +1 -1
  5. package/dist/components/LiveChart.d.ts +2 -2
  6. package/dist/components/LiveChart.d.ts.map +1 -1
  7. package/dist/components/LiveChartSeries.d.ts +2 -2
  8. package/dist/components/LiveChartSeries.d.ts.map +1 -1
  9. package/dist/components/MultiSeriesDots.d.ts +5 -3
  10. package/dist/components/MultiSeriesDots.d.ts.map +1 -1
  11. package/dist/constants.d.ts +6 -0
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/core/resolveConfig.d.ts +19 -3
  14. package/dist/core/resolveConfig.d.ts.map +1 -1
  15. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  16. package/dist/draw/candle.d.ts +21 -0
  17. package/dist/draw/candle.d.ts.map +1 -1
  18. package/dist/draw/grid.d.ts +1 -1
  19. package/dist/draw/grid.d.ts.map +1 -1
  20. package/dist/draw/line.d.ts +7 -1
  21. package/dist/draw/line.d.ts.map +1 -1
  22. package/dist/hooks/resolveChartLayout.d.ts +8 -0
  23. package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
  24. package/dist/hooks/useCandlePaths.d.ts +3 -1
  25. package/dist/hooks/useCandlePaths.d.ts.map +1 -1
  26. package/dist/hooks/useChartPaths.d.ts +3 -1
  27. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  28. package/dist/hooks/useDegen.d.ts +1 -3
  29. package/dist/hooks/useDegen.d.ts.map +1 -1
  30. package/dist/hooks/useMarkers.d.ts.map +1 -1
  31. package/dist/hooks/useMultiSeriesLinePaths.d.ts +3 -1
  32. package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
  33. package/dist/hooks/usePinchZoom.d.ts +7 -0
  34. package/dist/hooks/usePinchZoom.d.ts.map +1 -1
  35. package/dist/hooks/useSeriesIndicatorOpacity.d.ts +13 -0
  36. package/dist/hooks/useSeriesIndicatorOpacity.d.ts.map +1 -0
  37. package/dist/hooks/useTradeStream.d.ts.map +1 -1
  38. package/dist/hooks/useYAxis.d.ts +1 -1
  39. package/dist/hooks/useYAxis.d.ts.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/math/simplify.d.ts +20 -0
  43. package/dist/math/simplify.d.ts.map +1 -0
  44. package/dist/types.d.ts +101 -16
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/components/CrosshairOverlay.tsx +14 -9
  48. package/src/components/DotOverlay.tsx +17 -1
  49. package/src/components/LiveChart.tsx +227 -67
  50. package/src/components/LiveChartSeries.tsx +56 -16
  51. package/src/components/MultiSeriesDots.tsx +22 -3
  52. package/src/constants.ts +7 -0
  53. package/src/core/resolveConfig.ts +72 -14
  54. package/src/core/useLiveChartEngine.ts +6 -1
  55. package/src/draw/candle.ts +78 -0
  56. package/src/draw/grid.ts +34 -4
  57. package/src/draw/line.ts +20 -7
  58. package/src/hooks/resolveChartLayout.ts +30 -18
  59. package/src/hooks/useCandlePaths.ts +9 -1
  60. package/src/hooks/useChartPaths.ts +37 -7
  61. package/src/hooks/useDegen.ts +2 -6
  62. package/src/hooks/useMarkers.ts +6 -2
  63. package/src/hooks/useMultiSeriesLinePaths.ts +31 -4
  64. package/src/hooks/usePinchZoom.ts +21 -0
  65. package/src/hooks/useSeriesIndicatorOpacity.ts +56 -0
  66. package/src/hooks/useTradeStream.ts +6 -1
  67. package/src/hooks/useYAxis.ts +2 -0
  68. package/src/index.ts +2 -0
  69. package/src/math/simplify.ts +197 -0
  70. package/src/types.ts +103 -16
@@ -7,6 +7,10 @@ import type {
7
7
  SingleEngineState,
8
8
  } from "../core/useLiveChartEngine";
9
9
  import { buildLinePoints, type ChartPadding } from "../draw/line";
10
+ import {
11
+ makeLineSimplifyScratch,
12
+ simplifyLinePoints,
13
+ } from "../math/simplify";
10
14
  import { drawSpline, makeSplineScratch } from "../math/spline";
11
15
  import { sampleThresholdYAt, thresholdSampleSpanX } from "../math/threshold";
12
16
  import { blendPtsY, squigglifyPts } from "../math/squiggly";
@@ -52,6 +56,8 @@ export function useChartPaths(
52
56
  * `samples[]` (so band geometry matches the shader exactly). Takes precedence
53
57
  * over `thresholdY`, the constant (horizontal) case. */
54
58
  thresholdSamples?: SharedValue<number[]>,
59
+ /** Screen-space path simplification tolerance in pixels. `0` disables it. */
60
+ simplifyTolerance = 0,
55
61
  ) {
56
62
  const lineBuilder = usePathBuilder();
57
63
  const fillBuilder = usePathBuilder();
@@ -61,24 +67,28 @@ export function useChartPaths(
61
67
  emptyPath: SkPath;
62
68
  ptsA: number[];
63
69
  ptsB: number[];
70
+ rawPts: number[];
64
71
  ptsTick: boolean;
65
72
  squigglePts: number[];
66
73
  morphA: number[];
67
74
  morphB: number[];
68
75
  morphTick: boolean;
69
76
  scratch: ReturnType<typeof makeSplineScratch>;
77
+ simplifyScratch: ReturnType<typeof makeLineSimplifyScratch>;
70
78
  } | null>(null);
71
79
  if (cacheRef.current === null) {
72
80
  cacheRef.current = {
73
81
  emptyPath: Skia.Path.Make(),
74
82
  ptsA: [] as number[],
75
83
  ptsB: [] as number[],
84
+ rawPts: [] as number[],
76
85
  ptsTick: false,
77
86
  squigglePts: [] as number[],
78
87
  morphA: [] as number[],
79
88
  morphB: [] as number[],
80
89
  morphTick: false,
81
90
  scratch: makeSplineScratch(),
91
+ simplifyScratch: makeLineSimplifyScratch(),
82
92
  };
83
93
  }
84
94
 
@@ -94,32 +104,52 @@ export function useChartPaths(
94
104
  engine.edgeValue.get(),
95
105
  engine.viewEnd.get(),
96
106
  );
107
+ const simplify =
108
+ Number.isFinite(simplifyTolerance) && simplifyTolerance > 0
109
+ ? simplifyTolerance
110
+ : 0;
111
+ const now = engine.timestamp.get();
112
+ const windowSecs = engine.displayWindow.get();
113
+ const canvasWidth = engine.canvasWidth.get();
97
114
  const realPts = buildLinePoints(
98
115
  engine.data.get(),
99
116
  tipValue,
100
- engine.timestamp.get(),
101
- engine.displayWindow.get(),
117
+ now,
118
+ windowSecs,
102
119
  engine.displayMin.get(),
103
120
  engine.displayMax.get(),
104
- engine.canvasWidth.get(),
121
+ canvasWidth,
105
122
  engine.canvasHeight.get(),
106
123
  padding,
107
- buf,
124
+ simplify > 0 ? cache.rawPts : buf,
108
125
  // Only a live-following chart may extend the line to the right edge; a
109
126
  // parked (scrolled-back / overscrolled) window ends at its last real
110
127
  // point rather than fabricating a flat line into dataless space.
111
128
  engine.viewEnd.get() != null,
112
129
  );
130
+ const chartWidth = canvasWidth - padding.left - padding.right;
131
+ const absoluteXOffset =
132
+ windowSecs > 0 ? (now - windowSecs) * (chartWidth / windowSecs) : 0;
133
+ const renderPts =
134
+ simplify > 0
135
+ ? simplifyLinePoints(
136
+ realPts,
137
+ simplify,
138
+ buf,
139
+ cache.simplifyScratch,
140
+ absoluteXOffset,
141
+ )
142
+ : realPts;
113
143
 
114
144
  // Skip blending when fully revealed or no morphT provided
115
145
  const t = morphT?.get() ?? 1;
116
- if (t >= 1 || realPts.length === 0) return realPts;
146
+ if (t >= 1 || renderPts.length === 0) return renderPts;
117
147
 
118
148
  // Compute squiggly Y values at the same X positions as the real line
119
149
  const centerY =
120
150
  (engine.canvasHeight.get() - padding.bottom + padding.top) / 2;
121
151
  const squigglyPts = squigglifyPts(
122
- realPts,
152
+ renderPts,
123
153
  engine.timestamp.get(),
124
154
  centerY,
125
155
  squiggleAmplitude,
@@ -131,7 +161,7 @@ export function useChartPaths(
131
161
  cache.morphTick = !cache.morphTick;
132
162
  return blendPtsY(
133
163
  squigglyPts,
134
- realPts,
164
+ renderPts,
135
165
  t,
136
166
  padding,
137
167
  engine.canvasWidth.get(),
@@ -20,8 +20,6 @@ export function useDegen(
20
20
  momentumSV: SharedValue<Momentum>,
21
21
  cfg: ResolvedDegenConfig | null,
22
22
  onShake?: (payload: DegenShakePayload) => void,
23
- /** Static charts run no loops: register the frame callback without starting it. */
24
- isStatic = false,
25
23
  ): {
26
24
  pack: SharedValue<Float64Array<ArrayBuffer>>;
27
25
  packRevision: SharedValue<number>;
@@ -154,6 +152,8 @@ export function useDegen(
154
152
  speedMaxSV,
155
153
  ]);
156
154
 
155
+ // ChartWithDegen only mounts while the resolved effect is enabled; static
156
+ // charts force that config to null and unmount this callback entirely.
157
157
  useFrameCallback(
158
158
  /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
159
159
  "worklet";
@@ -250,10 +250,6 @@ export function useDegen(
250
250
  }
251
251
  prevActiveCount.set(activeCount);
252
252
  },
253
- // Static charts register the callback but never start it, so no degen frame
254
- // loop runs. Live charts keep the default (autostart) loop unchanged. The
255
- // controller also forces `cfg` to null in static, so degen is doubly off.
256
- !isStatic,
257
253
  );
258
254
 
259
255
  const shakeTransform = useDerivedValue(() => {
@@ -1,4 +1,4 @@
1
- import { useRef } from "react";
1
+ import { useEffect, useRef } from "react";
2
2
  import { Gesture } from "react-native-gesture-handler";
3
3
  import {
4
4
  useFrameCallback,
@@ -77,7 +77,7 @@ export function useMarkers(
77
77
  onMarkerPress?.(event);
78
78
  };
79
79
 
80
- useFrameCallback(
80
+ const markerFrameCallback = useFrameCallback(
81
81
  /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
82
82
  "worklet";
83
83
  const cache = cacheRef.current!;
@@ -111,6 +111,10 @@ export function useMarkers(
111
111
  },
112
112
  autostart,
113
113
  );
114
+ useEffect(() => {
115
+ // Reanimated does not reactively apply later `autostart` values.
116
+ markerFrameCallback.setActive(autostart);
117
+ }, [autostart, markerFrameCallback]);
114
118
 
115
119
  const tapGesture = Gesture.Tap().onEnd(
116
120
  /* istanbul ignore next -- gesture worklet runs on UI thread, not in Jest */ (
@@ -4,6 +4,10 @@ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
4
4
  import { MAX_MULTI_SERIES } from "../constants";
5
5
  import type { MultiEngineState } from "../core/useLiveChartEngine";
6
6
  import { buildLinePoints, type ChartPadding } from "../draw/line";
7
+ import {
8
+ makeLineSimplifyScratch,
9
+ simplifyLinePoints,
10
+ } from "../math/simplify";
7
11
  import { drawSpline, makeSplineScratch } from "../math/spline";
8
12
  import { usePathBuilders } from "./usePathBuilder";
9
13
 
@@ -21,25 +25,31 @@ export function useMultiSeriesLinePaths(
21
25
  engine: MultiEngineState,
22
26
  padding: ChartPadding,
23
27
  activeSeriesCount = MAX_MULTI_SERIES,
28
+ /** Shared screen-space simplification tolerance; a series can override it. */
29
+ simplifyTolerance = 0,
24
30
  ): SharedValue<SkPath[]> {
25
31
  const builders = usePathBuilders(MAX_MULTI_SERIES);
26
32
 
27
33
  const poolRef = useRef<{
28
34
  empty: SkPath;
29
35
  ptsBuf: number[];
36
+ simplifiedPts: number[];
30
37
  pathsA: SkPath[];
31
38
  pathsB: SkPath[];
32
39
  pathsTick: boolean;
33
40
  scratch: ReturnType<typeof makeSplineScratch>;
41
+ simplifyScratch: ReturnType<typeof makeLineSimplifyScratch>;
34
42
  } | null>(null);
35
43
  if (poolRef.current === null) {
36
44
  poolRef.current = {
37
45
  empty: Skia.Path.Make(),
38
46
  ptsBuf: [] as number[],
47
+ simplifiedPts: [] as number[],
39
48
  pathsA: [] as SkPath[],
40
49
  pathsB: [] as SkPath[],
41
50
  pathsTick: false,
42
51
  scratch: makeSplineScratch(),
52
+ simplifyScratch: makeLineSimplifyScratch(),
43
53
  };
44
54
  }
45
55
 
@@ -52,19 +62,36 @@ export function useMultiSeriesLinePaths(
52
62
  const out = pool.pathsTick ? pool.pathsA : pool.pathsB;
53
63
  out.length = 0;
54
64
  const count = Math.min(activeSeriesCount, s.length, MAX_MULTI_SERIES);
65
+ const now = engine.timestamp.get();
66
+ const windowSecs = engine.displayWindow.get();
67
+ const canvasWidth = engine.canvasWidth.get();
68
+ const chartWidth = canvasWidth - padding.left - padding.right;
69
+ const absoluteXOffset =
70
+ windowSecs > 0 ? (now - windowSecs) * (chartWidth / windowSecs) : 0;
55
71
  for (let i = 0; i < count; i++) {
56
- const pts = buildLinePoints(
72
+ const rawPts = buildLinePoints(
57
73
  s[i].data,
58
74
  displays[i] ?? s[i].value,
59
- engine.timestamp.get(),
60
- engine.displayWindow.get(),
75
+ now,
76
+ windowSecs,
61
77
  engine.displayMin.get(),
62
78
  engine.displayMax.get(),
63
- engine.canvasWidth.get(),
79
+ canvasWidth,
64
80
  engine.canvasHeight.get(),
65
81
  padding,
66
82
  pool.ptsBuf,
67
83
  );
84
+ const seriesTolerance = s[i].simplify ?? simplifyTolerance;
85
+ const pts =
86
+ Number.isFinite(seriesTolerance) && seriesTolerance > 0
87
+ ? simplifyLinePoints(
88
+ rawPts,
89
+ seriesTolerance,
90
+ pool.simplifiedPts,
91
+ pool.simplifyScratch,
92
+ absoluteXOffset,
93
+ )
94
+ : rawPts;
68
95
  const n = pts.length >> 1;
69
96
  if (n >= 2) {
70
97
  const b = slots[i];
@@ -26,6 +26,27 @@ export interface PinchZoomEngineRefs {
26
26
  canvasWidth: SharedValue<number>;
27
27
  }
28
28
 
29
+ /** The pinch state cleared by the public chart handle's `resetZoom()` method. */
30
+ export type PinchZoomResetRefs = Pick<
31
+ PinchZoomEngineRefs,
32
+ "viewWindow" | "viewEnd"
33
+ >;
34
+
35
+ /**
36
+ * Clear the focal-anchored pinch overrides so the engine follows its configured
37
+ * `timeWindow` and live edge again. Safe to call when the chart is already reset.
38
+ */
39
+ export function resetPinchZoom({
40
+ viewWindow,
41
+ viewEnd,
42
+ }: PinchZoomResetRefs): void {
43
+ "worklet";
44
+ cancelAnimation(viewWindow);
45
+ cancelAnimation(viewEnd);
46
+ viewWindow.set(null);
47
+ viewEnd.set(null);
48
+ }
49
+
29
50
  export interface UsePinchZoomOptions {
30
51
  engine: PinchZoomEngineRefs;
31
52
  padding: ChartPadding;
@@ -0,0 +1,56 @@
1
+ import {
2
+ Easing,
3
+ useAnimatedReaction,
4
+ useSharedValue,
5
+ withTiming,
6
+ type SharedValue,
7
+ } from "react-native-reanimated";
8
+
9
+ /** Short independent fade used when replacement data dims the plotted series. */
10
+ export const SERIES_INDICATOR_FADE_MS = 180;
11
+
12
+ /** Treat tiny animation residue at the top end as fully restored. */
13
+ const SERIES_OPACITY_VISIBLE_EPSILON = 0.001;
14
+
15
+ /** Worklet-safe target for live dots and their pulse rings. */
16
+ export function seriesIndicatorOpacityTarget(seriesOpacity: number): 0 | 1 {
17
+ "worklet";
18
+ return seriesOpacity >= 1 - SERIES_OPACITY_VISIBLE_EPSILON ? 1 : 0;
19
+ }
20
+
21
+ /**
22
+ * Fades live dots and pulse rings away while `seriesOpacity` dims replacement
23
+ * data, then restores them once the plotted series is fully opaque again.
24
+ * Keeping this as a separate opacity avoids compositing a translucent dot over
25
+ * its own line, where the stroke would otherwise shine through.
26
+ */
27
+ export function useSeriesIndicatorOpacity(
28
+ seriesOpacity: SharedValue<number>,
29
+ duration: number = SERIES_INDICATOR_FADE_MS,
30
+ ): SharedValue<number> {
31
+ // Fully visible is the common mount state. The reaction snaps an initially
32
+ // dimmed series to 0 on its first UI-thread run so it never flashes a dot.
33
+ const opacity = useSharedValue(1);
34
+
35
+ useAnimatedReaction(
36
+ () => seriesIndicatorOpacityTarget(seriesOpacity.get()),
37
+ (target, previous) => {
38
+ "worklet";
39
+ if (previous === null || previous === undefined || duration === 0) {
40
+ opacity.set(target);
41
+ return;
42
+ }
43
+ if (target !== previous) {
44
+ opacity.set(
45
+ withTiming(target, {
46
+ duration,
47
+ easing: Easing.inOut(Easing.ease),
48
+ }),
49
+ );
50
+ }
51
+ },
52
+ [seriesOpacity, duration],
53
+ );
54
+
55
+ return opacity;
56
+ }
@@ -1,3 +1,4 @@
1
+ import { useEffect } from "react";
1
2
  import {
2
3
  useFrameCallback,
3
4
  useSharedValue,
@@ -31,7 +32,7 @@ export function useTradeStream(
31
32
  const markers = useSharedValue<TradeMarker[]>([]);
32
33
  const state = useSharedValue<TradeStreamState>(createTradeStreamState());
33
34
 
34
- useFrameCallback(
35
+ const tradeFrameCallback = useFrameCallback(
35
36
  /* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (
36
37
  frameInfo,
37
38
  ) => {
@@ -62,6 +63,10 @@ export function useTradeStream(
62
63
  },
63
64
  autostart,
64
65
  );
66
+ useEffect(() => {
67
+ // Reanimated treats `autostart` as mount-time state; synchronize changes.
68
+ tradeFrameCallback.setActive(autostart);
69
+ }, [autostart, tradeFrameCallback]);
65
70
 
66
71
  return markers;
67
72
  }
@@ -20,6 +20,7 @@ export function useYAxis(
20
20
  minGap = 36,
21
21
  gridMetrics: GridMetrics = GRID_METRICS_DEFAULTS,
22
22
  count = 0,
23
+ intervalScale = 1,
23
24
  ) {
24
25
  const prevInterval = useSharedValue(0);
25
26
  const labelAlphas = useSharedValue<Record<number, number>>({});
@@ -41,6 +42,7 @@ export function useYAxis(
41
42
  minGap,
42
43
  gridMetrics,
43
44
  count,
45
+ intervalScale,
44
46
  );
45
47
 
46
48
  prevInterval.set(result.interval);
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ export type {
49
49
  ChartSegment,
50
50
  DegenOptions,
51
51
  DegenShakePayload,
52
+ DotGlowConfig,
52
53
  EmptyStateMetrics,
53
54
  FontConfig,
54
55
  FontWeight,
@@ -60,6 +61,7 @@ export type {
60
61
  LegendStyle,
61
62
  LineConfig,
62
63
  LiveChartCoreProps,
64
+ LiveChartHandle,
63
65
  LiveChartMetrics,
64
66
  LiveChartMetricsOverride,
65
67
  LiveChartPalette,
@@ -0,0 +1,197 @@
1
+ /** Reusable buffers for {@link simplifyLinePoints}. */
2
+ export interface LineSimplifyScratch {
3
+ keep: number[];
4
+ stack: number[];
5
+ }
6
+
7
+ /** Allocate simplification scratch once, outside the per-frame worklet. */
8
+ export function makeLineSimplifyScratch(): LineSimplifyScratch {
9
+ return { keep: [], stack: [] };
10
+ }
11
+
12
+ /**
13
+ * Maximum number of points simplified as one Ramer-Douglas-Peucker range.
14
+ *
15
+ * The visible line is already bounded to roughly two points per horizontal
16
+ * pixel. Splitting that bounded input into overlapping ranges also bounds the
17
+ * simplifier's worst-case work: a pathological zig-zag cannot turn a frame into
18
+ * an O(n²) scan. Range endpoints are retained, so chunking can only keep a few
19
+ * extra points; it cannot erase additional structure.
20
+ */
21
+ const MAX_RDP_RANGE_POINTS = 64;
22
+
23
+ /**
24
+ * Width of each RDP range on the stable, absolute screen-pixel grid.
25
+ *
26
+ * Dense line input contains at most two representatives per pixel bucket. A
27
+ * 30 px range therefore stays within the 64-point work bound even when the
28
+ * decimator and simplifier grids have different phases. Fixed-width ranges
29
+ * also keep their membership stable as the viewport advances.
30
+ */
31
+ const RDP_RANGE_WIDTH_PX = 30;
32
+
33
+ function pointSegmentDistanceSq(
34
+ px: number,
35
+ py: number,
36
+ ax: number,
37
+ ay: number,
38
+ bx: number,
39
+ by: number,
40
+ ): number {
41
+ "worklet";
42
+ const dx = bx - ax;
43
+ const dy = by - ay;
44
+ const lengthSq = dx * dx + dy * dy;
45
+ if (lengthSq === 0) {
46
+ const ex = px - ax;
47
+ const ey = py - ay;
48
+ return ex * ex + ey * ey;
49
+ }
50
+
51
+ let t = ((px - ax) * dx + (py - ay) * dy) / lengthSq;
52
+ if (t < 0) t = 0;
53
+ else if (t > 1) t = 1;
54
+ const ex = px - (ax + t * dx);
55
+ const ey = py - (ay + t * dy);
56
+ return ex * ex + ey * ey;
57
+ }
58
+
59
+ function simplifyRdpGroup(
60
+ points: number[],
61
+ keep: number[],
62
+ stack: number[],
63
+ toleranceSq: number,
64
+ rangeStart: number,
65
+ rangeEnd: number,
66
+ ): void {
67
+ "worklet";
68
+ if (rangeStart === rangeEnd) {
69
+ keep[rangeStart] = 1;
70
+ return;
71
+ }
72
+
73
+ // Oversized fixed-X groups are split into overlapping subranges. The group
74
+ // contents are themselves stable, so this point-count guard cannot move
75
+ // until that absolute group reaches a viewport edge.
76
+ for (let chunkStart = rangeStart; chunkStart < rangeEnd; ) {
77
+ const chunkEnd = Math.min(
78
+ chunkStart + MAX_RDP_RANGE_POINTS - 1,
79
+ rangeEnd,
80
+ );
81
+ keep[chunkStart] = 1;
82
+ keep[chunkEnd] = 1;
83
+ stack.push(chunkStart, chunkEnd);
84
+
85
+ while (stack.length >= 2) {
86
+ const end = stack.pop()!;
87
+ const start = stack.pop()!;
88
+ if (end <= start + 1) continue;
89
+
90
+ const ax = points[start * 2];
91
+ const ay = points[start * 2 + 1];
92
+ const bx = points[end * 2];
93
+ const by = points[end * 2 + 1];
94
+ let furthest = -1;
95
+ let maxDistanceSq = toleranceSq;
96
+
97
+ for (let i = start + 1; i < end; i++) {
98
+ const distanceSq = pointSegmentDistanceSq(
99
+ points[i * 2],
100
+ points[i * 2 + 1],
101
+ ax,
102
+ ay,
103
+ bx,
104
+ by,
105
+ );
106
+ if (distanceSq > maxDistanceSq) {
107
+ maxDistanceSq = distanceSq;
108
+ furthest = i;
109
+ }
110
+ }
111
+
112
+ if (furthest >= 0) {
113
+ keep[furthest] = 1;
114
+ stack.push(start, furthest, furthest, end);
115
+ }
116
+ }
117
+
118
+ chunkStart = chunkEnd;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Simplify flat screen-space points (`[x0, y0, x1, y1, ...]`) with a bounded
124
+ * Ramer-Douglas-Peucker pass.
125
+ *
126
+ * `tolerance` is the maximum removable deviation in pixels. The first and last
127
+ * point, larger peaks/valleys, and the original point order are retained. Pass
128
+ * distinct reusable `out` and `scratch` buffers to avoid per-frame allocation.
129
+ * `absoluteXOffset` maps the viewport-relative X coordinates back onto a fixed
130
+ * screen-pixel grid; live chart callers should pass `winStart * xScale` so
131
+ * historical range membership does not change as the window advances.
132
+ */
133
+ export function simplifyLinePoints(
134
+ points: number[],
135
+ tolerance: number,
136
+ out: number[],
137
+ scratch: LineSimplifyScratch,
138
+ absoluteXOffset = 0,
139
+ ): number[] {
140
+ "worklet";
141
+ if (out === points) return points;
142
+
143
+ out.length = 0;
144
+ const count = points.length >> 1;
145
+ if (!(tolerance > 0) || !Number.isFinite(tolerance) || count <= 2) {
146
+ for (let i = 0; i < count * 2; i++) out.push(points[i]);
147
+ return out;
148
+ }
149
+
150
+ const keep = scratch.keep;
151
+ keep.length = count;
152
+ for (let i = 0; i < count; i++) keep[i] = 0;
153
+
154
+ const stack = scratch.stack;
155
+ stack.length = 0;
156
+ const toleranceSq = tolerance * tolerance;
157
+ const stableXOffset = Number.isFinite(absoluteXOffset) ? absoluteXOffset : 0;
158
+
159
+ // Partition on a fixed absolute-X grid rather than by the first visible
160
+ // point's array index. Dropping a point from the left edge then affects only
161
+ // that edge range; fully visible historical ranges keep identical endpoints
162
+ // and RDP choices while translating across the viewport.
163
+ let groupStart = 0;
164
+ let groupKey = Math.floor(
165
+ (points[0] + stableXOffset) / RDP_RANGE_WIDTH_PX,
166
+ );
167
+ for (let i = 1; i < count; i++) {
168
+ const nextKey = Math.floor(
169
+ (points[i * 2] + stableXOffset) / RDP_RANGE_WIDTH_PX,
170
+ );
171
+ if (nextKey !== groupKey) {
172
+ simplifyRdpGroup(
173
+ points,
174
+ keep,
175
+ stack,
176
+ toleranceSq,
177
+ groupStart,
178
+ i - 1,
179
+ );
180
+ groupStart = i;
181
+ groupKey = nextKey;
182
+ }
183
+ }
184
+ simplifyRdpGroup(
185
+ points,
186
+ keep,
187
+ stack,
188
+ toleranceSq,
189
+ groupStart,
190
+ count - 1,
191
+ );
192
+
193
+ for (let i = 0; i < count; i++) {
194
+ if (keep[i] === 1) out.push(points[i * 2], points[i * 2 + 1]);
195
+ }
196
+ return out;
197
+ }