react-native-vroom-chart 0.7.0 → 0.9.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.
@@ -0,0 +1,74 @@
1
+ // The pulsing ring at the line chart's tip — a phase in, a radius and two
2
+ // alphas out (VROOM_FLOAT_LINE_TIP_PULSE).
3
+ //
4
+ // Shape and timing follow TradingView's last-price animation, whose numbers are
5
+ // well-tuned: expand while the fill washes out and the edge sharpens, keep
6
+ // expanding while the edge fades, then rest. That rest is nearly half the period
7
+ // and it is what makes the ring read as a heartbeat instead of a strobe.
8
+ //
9
+ // Radii come out as multiples of the ring's start radius rather than pixels,
10
+ // because the tip dot scales with the line width and the ring has to scale with
11
+ // it (see draw_close_tip in ma_overlay.cpp).
12
+ //
13
+ // Skia-free and header-only so the unit tests can cover it; see
14
+ // tests/test_tip_pulse.cpp.
15
+
16
+ #pragma once
17
+
18
+ #include <cmath>
19
+
20
+ namespace vroom::tip_pulse {
21
+
22
+ // One full expand-and-rest cycle.
23
+ constexpr float kPeriodSeconds = 2.6f;
24
+
25
+ // The ring at one instant. Alphas already account for the rest stage, so a
26
+ // caller can paint unconditionally and simply draw nothing visible.
27
+ struct Frame {
28
+ float radius_mul; // multiple of the ring's start radius
29
+ float fill_alpha;
30
+ float stroke_alpha;
31
+ };
32
+
33
+ namespace detail {
34
+
35
+ struct Stage {
36
+ float end; // phase this stage runs until
37
+ float start_radius, end_radius;
38
+ float start_fill, end_fill;
39
+ float start_stroke, end_stroke;
40
+ };
41
+
42
+ // The fill fades as the ring grows, while the edge first *gains* alpha — that
43
+ // crossover is what gives the bloom its snap — before fading out in stage two.
44
+ constexpr Stage kStages[] = {
45
+ {0.25f, 1.0f, 2.5f, 0.25f, 0.f, 0.40f, 0.80f},
46
+ {0.525f, 2.5f, 3.5f, 0.f, 0.f, 0.80f, 0.f},
47
+ {1.0f, 3.5f, 3.5f, 0.f, 0.f, 0.f, 0.f},
48
+ };
49
+
50
+ inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
51
+
52
+ } // namespace detail
53
+
54
+ // Ring state at `phase`, in cycles. Values outside [0,1) wrap, so a caller can
55
+ // hand over raw elapsed time divided by the period without normalizing.
56
+ inline Frame at(float phase) {
57
+ const float p = phase - std::floor(phase);
58
+ float start = 0.f;
59
+ for (const detail::Stage& s : detail::kStages) {
60
+ // The last stage takes anything left over, which also catches a `p` that
61
+ // rounded up to 1 on the way in.
62
+ if (p < s.end || s.end >= 1.f) {
63
+ const float span = s.end - start;
64
+ const float t = span > 0.f ? (p - start) / span : 0.f;
65
+ return Frame{detail::lerp(s.start_radius, s.end_radius, t),
66
+ detail::lerp(s.start_fill, s.end_fill, t),
67
+ detail::lerp(s.start_stroke, s.end_stroke, t)};
68
+ }
69
+ start = s.end;
70
+ }
71
+ return Frame{1.f, 0.f, 0.f}; // unreachable: the table ends at 1
72
+ }
73
+
74
+ } // namespace vroom::tip_pulse
package/lib/index.d.mts CHANGED
@@ -107,6 +107,33 @@ type VroomTheme = {
107
107
  * the bottom of the price pane. Defaults to 0.28; set to 0 to disable the fill.
108
108
  */
109
109
  lineGradientOpacity?: number;
110
+ /**
111
+ * How much to round the line chart's corners, from 0 (straight segments
112
+ * between closes) to 1 (fully smooth). Defaults to 0.
113
+ *
114
+ * The curve is monotone-limited, so smoothing can never overshoot into a price
115
+ * that didn't trade: every peak and trough stays on an actual close, and the
116
+ * curve never leaves the range of the two closes it connects. Applies to the
117
+ * gradient fill beneath the line as well, so the two stay flush.
118
+ */
119
+ lineTension?: number;
120
+ /**
121
+ * Mark the line chart's newest end with a dot. Defaults to `true`.
122
+ *
123
+ * Takes its color from `line` and its radius from `lineWidth`, wrapped in a 2px
124
+ * ring of `background` that separates it from the line itself. Only drawn in
125
+ * line mode, and it crossfades along with the line during a candle↔line
126
+ * transition.
127
+ */
128
+ lineTipDot?: boolean;
129
+ /**
130
+ * Pulse a ring outward from the tip dot, once every 2.6s. Defaults to `false`.
131
+ *
132
+ * Ignored when `lineTipDot` is off, and suppressed when the OS asks for reduced
133
+ * motion. Because the ring never stops, enabling it keeps the chart repainting
134
+ * continuously — leave it off for charts that should be able to go idle.
135
+ */
136
+ lineTipPulse?: boolean;
110
137
  };
111
138
  /** A time window over the candle data, as Unix epoch milliseconds. */
112
139
  type VisibleRange = {
@@ -134,7 +161,7 @@ type ChartType = 'candles' | 'line';
134
161
  */
135
162
  type TransitionEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
136
163
  /** Active drawing tool while in `draw` mode. `null` draws nothing. */
137
- type DrawTool = null | 'line' | 'box' | 'pencil';
164
+ type DrawTool = null | 'line' | 'box' | 'pencil' | 'path';
138
165
  /** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
139
166
  type DrawPoint = {
140
167
  /** Anchor time as Unix epoch milliseconds (not snapped to a candle slot). */
@@ -176,16 +203,27 @@ type PencilDrawing = DrawingBase & {
176
203
  /** The path's points in draw order (at least 2), in data space. */
177
204
  points: DrawPoint[];
178
205
  };
206
+ /**
207
+ * A multi-segment path: straight segments through `points`, in order, ending in
208
+ * an arrowhead on the last vertex. Like a pencil stroke it holds a variable
209
+ * number of points, but every one was placed deliberately (one click each), so
210
+ * each is an individually draggable handle once the path is committed.
211
+ */
212
+ type PathDrawing = DrawingBase & {
213
+ type: 'path';
214
+ /** The path's vertices in draw order (at least 2), in data space. */
215
+ points: DrawPoint[];
216
+ };
179
217
  /**
180
218
  * A committed drawing. Pass an array of these via the `drawings` prop to render
181
219
  * persisted annotations; the chart appends a new one (via `onDrawingComplete`)
182
220
  * each time the user finishes drawing.
183
221
  *
184
222
  * This is a discriminated union on `type` — narrow on it before reading
185
- * `points[1]`, since a `'pencil'` stroke has a variable-length array while
223
+ * `points[1]`, since `'pencil'` and `'path'` have variable-length arrays while
186
224
  * `'line'` and `'box'` are always exactly two points.
187
225
  */
188
- type Drawing = LineDrawing | BoxDrawing | PencilDrawing;
226
+ type Drawing = LineDrawing | BoxDrawing | PencilDrawing | PathDrawing;
189
227
  /**
190
228
  * Storage adapter for **managed** drawing persistence. Provide it via the
191
229
  * `drawingStore` prop and the chart owns the drawings array itself — loading and
@@ -783,4 +821,35 @@ declare global {
783
821
  */
784
822
  declare function VroomChart(props: VroomChartProps): React.JSX.Element;
785
823
 
786
- export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme };
824
+ /**
825
+ * How a new `candles` array relates to the one the chart already holds:
826
+ * `'initial'` is the first data, `'stream'` a live update to the same series,
827
+ * `'timeframe'` the same asset re-bucketed into a different interval, and
828
+ * `'reset'` a different series entirely.
829
+ */
830
+ type DataTransition = 'initial' | 'stream' | 'timeframe' | 'reset';
831
+ /**
832
+ * The candle period in ms, inferred as the median of the first few intervals
833
+ * (robust to a single gap). Null when there are fewer than two candles.
834
+ */
835
+ declare function inferStepMs(candles: Candle[]): number | null;
836
+ /**
837
+ * Classify a candles-prop change. `prev` is the previously rendered array
838
+ * (null on first render); `seriesKeyChanged` forces `reset` regardless of the
839
+ * data (the explicit escape hatch).
840
+ *
841
+ * Constraint: detection compares two immutable snapshots. An array mutated in
842
+ * place (same reference) never reaches this code — React props must change
843
+ * identity to re-render.
844
+ */
845
+ declare function classifyTransition(prev: Candle[] | null, next: Candle[], seriesKeyChanged: boolean): DataTransition;
846
+ /**
847
+ * The visible window to apply after a timeframe switch so each candle keeps
848
+ * the exact pixel width it had before: the visible slot count is preserved and
849
+ * the right edge re-anchors on the newest candle (any future-gap overshoot is
850
+ * carried over in slots, clamped to the core's 3/4-window cap). The new start
851
+ * may precede the first candle — that gap is intentional, width wins.
852
+ */
853
+ declare function timeframeWindow(oldWindow: VisibleRange, oldStepMs: number, oldLastMs: number, newStepMs: number, newLastMs: number): VisibleRange;
854
+
855
+ export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type DataTransition, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme, classifyTransition, inferStepMs, timeframeWindow };
package/lib/index.d.ts CHANGED
@@ -107,6 +107,33 @@ type VroomTheme = {
107
107
  * the bottom of the price pane. Defaults to 0.28; set to 0 to disable the fill.
108
108
  */
109
109
  lineGradientOpacity?: number;
110
+ /**
111
+ * How much to round the line chart's corners, from 0 (straight segments
112
+ * between closes) to 1 (fully smooth). Defaults to 0.
113
+ *
114
+ * The curve is monotone-limited, so smoothing can never overshoot into a price
115
+ * that didn't trade: every peak and trough stays on an actual close, and the
116
+ * curve never leaves the range of the two closes it connects. Applies to the
117
+ * gradient fill beneath the line as well, so the two stay flush.
118
+ */
119
+ lineTension?: number;
120
+ /**
121
+ * Mark the line chart's newest end with a dot. Defaults to `true`.
122
+ *
123
+ * Takes its color from `line` and its radius from `lineWidth`, wrapped in a 2px
124
+ * ring of `background` that separates it from the line itself. Only drawn in
125
+ * line mode, and it crossfades along with the line during a candle↔line
126
+ * transition.
127
+ */
128
+ lineTipDot?: boolean;
129
+ /**
130
+ * Pulse a ring outward from the tip dot, once every 2.6s. Defaults to `false`.
131
+ *
132
+ * Ignored when `lineTipDot` is off, and suppressed when the OS asks for reduced
133
+ * motion. Because the ring never stops, enabling it keeps the chart repainting
134
+ * continuously — leave it off for charts that should be able to go idle.
135
+ */
136
+ lineTipPulse?: boolean;
110
137
  };
111
138
  /** A time window over the candle data, as Unix epoch milliseconds. */
112
139
  type VisibleRange = {
@@ -134,7 +161,7 @@ type ChartType = 'candles' | 'line';
134
161
  */
135
162
  type TransitionEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
136
163
  /** Active drawing tool while in `draw` mode. `null` draws nothing. */
137
- type DrawTool = null | 'line' | 'box' | 'pencil';
164
+ type DrawTool = null | 'line' | 'box' | 'pencil' | 'path';
138
165
  /** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
139
166
  type DrawPoint = {
140
167
  /** Anchor time as Unix epoch milliseconds (not snapped to a candle slot). */
@@ -176,16 +203,27 @@ type PencilDrawing = DrawingBase & {
176
203
  /** The path's points in draw order (at least 2), in data space. */
177
204
  points: DrawPoint[];
178
205
  };
206
+ /**
207
+ * A multi-segment path: straight segments through `points`, in order, ending in
208
+ * an arrowhead on the last vertex. Like a pencil stroke it holds a variable
209
+ * number of points, but every one was placed deliberately (one click each), so
210
+ * each is an individually draggable handle once the path is committed.
211
+ */
212
+ type PathDrawing = DrawingBase & {
213
+ type: 'path';
214
+ /** The path's vertices in draw order (at least 2), in data space. */
215
+ points: DrawPoint[];
216
+ };
179
217
  /**
180
218
  * A committed drawing. Pass an array of these via the `drawings` prop to render
181
219
  * persisted annotations; the chart appends a new one (via `onDrawingComplete`)
182
220
  * each time the user finishes drawing.
183
221
  *
184
222
  * This is a discriminated union on `type` — narrow on it before reading
185
- * `points[1]`, since a `'pencil'` stroke has a variable-length array while
223
+ * `points[1]`, since `'pencil'` and `'path'` have variable-length arrays while
186
224
  * `'line'` and `'box'` are always exactly two points.
187
225
  */
188
- type Drawing = LineDrawing | BoxDrawing | PencilDrawing;
226
+ type Drawing = LineDrawing | BoxDrawing | PencilDrawing | PathDrawing;
189
227
  /**
190
228
  * Storage adapter for **managed** drawing persistence. Provide it via the
191
229
  * `drawingStore` prop and the chart owns the drawings array itself — loading and
@@ -783,4 +821,35 @@ declare global {
783
821
  */
784
822
  declare function VroomChart(props: VroomChartProps): React.JSX.Element;
785
823
 
786
- export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme };
824
+ /**
825
+ * How a new `candles` array relates to the one the chart already holds:
826
+ * `'initial'` is the first data, `'stream'` a live update to the same series,
827
+ * `'timeframe'` the same asset re-bucketed into a different interval, and
828
+ * `'reset'` a different series entirely.
829
+ */
830
+ type DataTransition = 'initial' | 'stream' | 'timeframe' | 'reset';
831
+ /**
832
+ * The candle period in ms, inferred as the median of the first few intervals
833
+ * (robust to a single gap). Null when there are fewer than two candles.
834
+ */
835
+ declare function inferStepMs(candles: Candle[]): number | null;
836
+ /**
837
+ * Classify a candles-prop change. `prev` is the previously rendered array
838
+ * (null on first render); `seriesKeyChanged` forces `reset` regardless of the
839
+ * data (the explicit escape hatch).
840
+ *
841
+ * Constraint: detection compares two immutable snapshots. An array mutated in
842
+ * place (same reference) never reaches this code — React props must change
843
+ * identity to re-render.
844
+ */
845
+ declare function classifyTransition(prev: Candle[] | null, next: Candle[], seriesKeyChanged: boolean): DataTransition;
846
+ /**
847
+ * The visible window to apply after a timeframe switch so each candle keeps
848
+ * the exact pixel width it had before: the visible slot count is preserved and
849
+ * the right edge re-anchors on the newest candle (any future-gap overshoot is
850
+ * carried over in slots, clamped to the core's 3/4-window cap). The new start
851
+ * may precede the first candle — that gap is intentional, width wins.
852
+ */
853
+ declare function timeframeWindow(oldWindow: VisibleRange, oldStepMs: number, oldLastMs: number, newStepMs: number, newLastMs: number): VisibleRange;
854
+
855
+ export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type DataTransition, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme, classifyTransition, inferStepMs, timeframeWindow };
package/lib/index.js CHANGED
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- VroomChart: () => VroomChart
33
+ VroomChart: () => VroomChart,
34
+ classifyTransition: () => classifyTransition,
35
+ inferStepMs: () => inferStepMs,
36
+ timeframeWindow: () => timeframeWindow
34
37
  });
35
38
  module.exports = __toCommonJS(index_exports);
36
39
 
@@ -48,6 +51,86 @@ var import_react = require("react");
48
51
  var import_react_native = require("react-native");
49
52
  var NativeVroomChart_default = import_react_native.TurboModuleRegistry.getEnforcing("VroomChartModule");
50
53
 
54
+ // src/dataTransitions.ts
55
+ var STEP_TOLERANCE = 0.01;
56
+ var MAX_SAME_ASSET_CLOSE_RATIO = 1.25;
57
+ var MAX_END_DRIFT_STEPS = 3;
58
+ var MAX_STREAM_ADVANCE_STEPS = 5;
59
+ function inferStepMs(candles) {
60
+ if (candles.length < 2) return null;
61
+ const k = Math.min(candles.length - 1, 8);
62
+ const diffs = [];
63
+ for (let i = 0; i < k; i++) diffs.push(candles[i + 1].timeMs - candles[i].timeMs);
64
+ diffs.sort((a, b) => a - b);
65
+ const median = diffs[Math.floor(diffs.length / 2)];
66
+ return median > 0 ? median : null;
67
+ }
68
+ function indexByTime(candles, t) {
69
+ let lo = 0;
70
+ let hi = candles.length - 1;
71
+ while (lo <= hi) {
72
+ const mid = lo + hi >>> 1;
73
+ const v = candles[mid].timeMs;
74
+ if (v === t) return mid;
75
+ if (v < t) lo = mid + 1;
76
+ else hi = mid - 1;
77
+ }
78
+ return -1;
79
+ }
80
+ function classifyTransition(prev, next, seriesKeyChanged) {
81
+ if (!prev || prev.length === 0) return "initial";
82
+ if (next.length === 0) return "stream";
83
+ if (seriesKeyChanged) return "reset";
84
+ const prevStep = inferStepMs(prev);
85
+ const nextStep = inferStepMs(next);
86
+ if (prevStep == null || nextStep == null) return "reset";
87
+ const prevLast = prev[prev.length - 1];
88
+ const nextLast = next[next.length - 1];
89
+ if (Math.abs(nextStep - prevStep) <= prevStep * STEP_TOLERANCE) {
90
+ const idx = indexByTime(next, prevLast.timeMs);
91
+ const aligned = idx >= 0;
92
+ const sharedBarRatio = aligned && next[idx].close > 0 && prevLast.close > 0 ? Math.max(next[idx].close / prevLast.close, prevLast.close / next[idx].close) : Infinity;
93
+ const advanced = nextLast.timeMs >= prevLast.timeMs && nextLast.timeMs - prevLast.timeMs <= MAX_STREAM_ADVANCE_STEPS * nextStep;
94
+ return sharedBarRatio <= MAX_SAME_ASSET_CLOSE_RATIO && advanced ? "stream" : "reset";
95
+ }
96
+ const closeRatio = prevLast.close > 0 && nextLast.close > 0 ? Math.max(nextLast.close / prevLast.close, prevLast.close / nextLast.close) : Infinity;
97
+ const prevEnd = prevLast.timeMs + prevStep;
98
+ const nextEnd = nextLast.timeMs + nextStep;
99
+ const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
100
+ return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
101
+ }
102
+ function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
103
+ const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
104
+ const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
105
+ const offsetSlots = Math.min(Math.max(offsetRaw, 0), slots * 0.75);
106
+ const endMs = Math.round(newLastMs + offsetSlots * newStepMs);
107
+ return { startMs: Math.round(endMs - slots * newStepMs), endMs };
108
+ }
109
+
110
+ // src/easing.ts
111
+ function ease(kind, p) {
112
+ switch (kind) {
113
+ case "linear":
114
+ return p;
115
+ case "ease-in":
116
+ return p * p;
117
+ case "ease-out":
118
+ return p * (2 - p);
119
+ default:
120
+ return p * p * (3 - 2 * p);
121
+ }
122
+ }
123
+ var EASINGS = [
124
+ "linear",
125
+ "ease-in",
126
+ "ease-out",
127
+ "ease-in-out"
128
+ ];
129
+ function easingIndex(kind) {
130
+ const i = kind ? EASINGS.indexOf(kind) : -1;
131
+ return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
132
+ }
133
+
51
134
  // src/packCandles.ts
52
135
  var BYTES_PER_CANDLE = 48;
53
136
  function packCandles(candles) {
@@ -106,12 +189,18 @@ var FLOAT_KEYS = {
106
189
  // VROOM_FLOAT_VOLUME_RADIUS_PX
107
190
  lineWidth: 11,
108
191
  // VROOM_FLOAT_LINE_WIDTH_PX
109
- lineGradientOpacity: 12
192
+ lineGradientOpacity: 12,
110
193
  // VROOM_FLOAT_LINE_GRADIENT_OPACITY
194
+ lineTension: 13
195
+ // VROOM_FLOAT_LINE_TENSION
111
196
  };
197
+ var FLOAT_LINE_TIP_PULSE = 15;
112
198
  var BOOL_KEYS = {
113
- wickRoundCap: 9
199
+ wickRoundCap: 9,
114
200
  // VROOM_FLOAT_WICK_ROUND_CAP
201
+ lineTipDot: 14,
202
+ // VROOM_FLOAT_LINE_TIP_DOT
203
+ lineTipPulse: FLOAT_LINE_TIP_PULSE
115
204
  };
116
205
  function parseColor(value) {
117
206
  if (typeof value === "number") {
@@ -282,15 +371,53 @@ function ensureInstalled() {
282
371
  }
283
372
  installed = true;
284
373
  }
285
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines) {
374
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
286
375
  const handleRef = (0, import_react.useRef)(null);
287
376
  const defaultWidthAppliedRef = (0, import_react.useRef)(false);
288
377
  const volumeCollapseRef = (0, import_react.useRef)(null);
378
+ const prevDataRef = (0, import_react.useRef)(null);
379
+ const intervalMorphRaf = (0, import_react.useRef)(null);
289
380
  const [picture, setPicture] = (0, import_react.useState)(null);
290
381
  if (!handleRef.current && size.width > 0 && size.height > 0) {
291
382
  ensureInstalled();
292
383
  handleRef.current = globalThis.VroomChartJSI.create();
293
384
  }
385
+ const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false });
386
+ animRef.current = {
387
+ ms: Math.max(0, transition?.transitionMs ?? 300),
388
+ easing: transition?.transitionEasing,
389
+ reduceMotion: transition?.reduceMotion ?? false
390
+ };
391
+ const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
392
+ onFrameRef.current = transition?.onFrame;
393
+ const seriesKey = transition?.seriesKey;
394
+ const endIntervalMorph = (0, import_react.useCallback)(() => {
395
+ if (intervalMorphRaf.current != null) {
396
+ cancelAnimationFrame(intervalMorphRaf.current);
397
+ intervalMorphRaf.current = null;
398
+ }
399
+ handleRef.current?.setIntervalMorph(1);
400
+ }, []);
401
+ const startIntervalMorph = (0, import_react.useCallback)((h) => {
402
+ const { ms, easing } = animRef.current;
403
+ const start = performance.now();
404
+ const step = (now) => {
405
+ const p = Math.min(1, (now - start) / ms);
406
+ h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
407
+ const pic = h.render();
408
+ if (pic) onFrameRef.current?.(pic);
409
+ intervalMorphRaf.current = p < 1 ? requestAnimationFrame(step) : null;
410
+ };
411
+ intervalMorphRaf.current = requestAnimationFrame(step);
412
+ }, []);
413
+ (0, import_react.useEffect)(() => {
414
+ return () => {
415
+ if (intervalMorphRaf.current != null) {
416
+ cancelAnimationFrame(intervalMorphRaf.current);
417
+ intervalMorphRaf.current = null;
418
+ }
419
+ };
420
+ }, []);
294
421
  const explicit = visibleRange != null;
295
422
  const startMs = visibleRange?.startMs ?? 0;
296
423
  const endMs = visibleRange?.endMs ?? 0;
@@ -310,8 +437,54 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
310
437
  h.setDefaultCandleWidth(defaultCandleWidth);
311
438
  defaultWidthAppliedRef.current = true;
312
439
  }
440
+ let morphing = false;
313
441
  if (candles.length > 0) {
314
- h.setCandles(packCandles(candles));
442
+ const prev = prevDataRef.current;
443
+ const freshHandle = prev == null || prev.handle !== h;
444
+ if (freshHandle || prev.candles !== candles || prev.seriesKey !== seriesKey) {
445
+ const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
446
+ let tfArgs = null;
447
+ let prevEnvelope = null;
448
+ if (transitionKind === "timeframe" && prev != null) {
449
+ const oldWindow = h.getVisibleRange();
450
+ const oldStepMs = inferStepMs(prev.candles);
451
+ if (oldWindow.endMs > oldWindow.startMs && oldStepMs != null) {
452
+ tfArgs = {
453
+ oldWindow,
454
+ oldStepMs,
455
+ oldLastMs: prev.candles[prev.candles.length - 1].timeMs
456
+ };
457
+ }
458
+ prevEnvelope = h.getVisiblePriceEnvelope();
459
+ morphing = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
460
+ if (morphing) {
461
+ endIntervalMorph();
462
+ h.beginIntervalMorph();
463
+ }
464
+ } else if (transitionKind === "initial" || transitionKind === "reset") {
465
+ endIntervalMorph();
466
+ }
467
+ h.setCandles(packCandles(candles));
468
+ if (transitionKind === "timeframe") {
469
+ const newStepMs = inferStepMs(candles);
470
+ if (tfArgs && newStepMs != null) {
471
+ const w = timeframeWindow(
472
+ tfArgs.oldWindow,
473
+ tfArgs.oldStepMs,
474
+ tfArgs.oldLastMs,
475
+ newStepMs,
476
+ candles[candles.length - 1].timeMs
477
+ );
478
+ h.setVisibleRange(w.startMs, w.endMs);
479
+ }
480
+ if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
481
+ else h.resetPriceScale();
482
+ if (morphing) startIntervalMorph(h);
483
+ } else if (transitionKind === "reset") {
484
+ h.resetView();
485
+ }
486
+ prevDataRef.current = { handle: h, candles, seriesKey };
487
+ }
315
488
  }
316
489
  if (explicit) {
317
490
  h.setVisibleRange(startMs, endMs);
@@ -319,6 +492,9 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
319
492
  if (theme) {
320
493
  applyTheme(h, theme);
321
494
  }
495
+ if (animRef.current.reduceMotion) {
496
+ h.setFloat(FLOAT_LINE_TIP_PULSE, 0);
497
+ }
322
498
  h.setRSI(rsiToSpec(rsi));
323
499
  h.setMACD(macdToSpec(macd));
324
500
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
@@ -330,39 +506,16 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
330
506
  h.setPriceLines(
331
507
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
332
508
  );
333
- setPicture(h.render());
334
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey]);
509
+ if (!morphing) setPicture(h.render());
510
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
335
511
  return { handle: handleRef.current, picture, volumeCollapseRef };
336
512
  }
337
513
 
338
- // src/easing.ts
339
- function ease(kind, p) {
340
- switch (kind) {
341
- case "linear":
342
- return p;
343
- case "ease-in":
344
- return p * p;
345
- case "ease-out":
346
- return p * (2 - p);
347
- default:
348
- return p * p * (3 - 2 * p);
349
- }
350
- }
351
- var EASINGS = [
352
- "linear",
353
- "ease-in",
354
- "ease-out",
355
- "ease-in-out"
356
- ];
357
- function easingIndex(kind) {
358
- const i = kind ? EASINGS.indexOf(kind) : -1;
359
- return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
360
- }
361
-
362
514
  // src/VroomChart.tsx
363
515
  function VroomChart(props) {
364
516
  const {
365
517
  candles,
518
+ seriesKey,
366
519
  width: widthProp,
367
520
  height: heightProp,
368
521
  style,
@@ -405,6 +558,19 @@ function VroomChart(props) {
405
558
  } : void 0,
406
559
  [priceLines, priceLinesStyle, onPriceLineClose]
407
560
  );
561
+ const emptyPicture = (0, import_react2.useMemo)(() => {
562
+ const rec = import_react_native_skia.Skia.PictureRecorder();
563
+ rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
564
+ return rec.finishRecordingAsPicture();
565
+ }, []);
566
+ const pictureSV = (0, import_react_native_reanimated.useSharedValue)(emptyPicture);
567
+ const reduceMotion = (0, import_react_native_reanimated.useReducedMotion)();
568
+ const onFrame = (0, import_react2.useCallback)(
569
+ (p) => {
570
+ pictureSV.value = p;
571
+ },
572
+ [pictureSV]
573
+ );
408
574
  const { handle, picture, volumeCollapseRef } = useChartCore(
409
575
  candles,
410
576
  { width, height },
@@ -418,19 +584,11 @@ function VroomChart(props) {
418
584
  vwap,
419
585
  bollingerBands,
420
586
  volume,
421
- priceLinesProp
587
+ priceLinesProp,
588
+ { seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame }
422
589
  );
423
- const emptyPicture = (0, import_react2.useMemo)(() => {
424
- const rec = import_react_native_skia.Skia.PictureRecorder();
425
- rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
426
- return rec.finishRecordingAsPicture();
427
- }, []);
428
- const pictureSV = (0, import_react_native_reanimated.useSharedValue)(emptyPicture);
429
590
  const crosshairActive = (0, import_react2.useRef)(false);
430
591
  const lastCrosshairTime = (0, import_react2.useRef)(null);
431
- (0, import_react2.useEffect)(() => {
432
- if (picture) pictureSV.value = picture;
433
- }, [picture, pictureSV]);
434
592
  const decayRaf = (0, import_react2.useRef)(null);
435
593
  const cancelDecay = (0, import_react2.useCallback)(() => {
436
594
  if (decayRaf.current != null) {
@@ -462,6 +620,10 @@ function VroomChart(props) {
462
620
  }
463
621
  };
464
622
  }, []);
623
+ (0, import_react2.useEffect)(() => {
624
+ if (picture) pictureSV.value = picture;
625
+ maybeStartAnim();
626
+ }, [picture, pictureSV, maybeStartAnim]);
465
627
  const morphRaf = (0, import_react2.useRef)(null);
466
628
  const morphFade = (0, import_react2.useRef)(null);
467
629
  const morphHandle = (0, import_react2.useRef)(null);
@@ -498,7 +660,7 @@ function VroomChart(props) {
498
660
  const prog = Math.min(1, (now - startTs) / dur);
499
661
  const fade = from + (target - from) * ease(easingRef.current, prog);
500
662
  morphFade.current = fade;
501
- handle.setMorph(fade, fade);
663
+ handle.setMorph(reduceMotion ? 0 : fade, fade);
502
664
  const p = handle.render();
503
665
  if (p) pictureSV.value = p;
504
666
  if (prog < 1) {
@@ -518,7 +680,7 @@ function VroomChart(props) {
518
680
  morphRaf.current = null;
519
681
  }
520
682
  };
521
- }, [handle, chartType, transitionMs, pictureSV]);
683
+ }, [handle, chartType, transitionMs, reduceMotion, pictureSV]);
522
684
  const volumeRaf = (0, import_react2.useRef)(null);
523
685
  const volumeHandle = (0, import_react2.useRef)(null);
524
686
  (0, import_react2.useEffect)(() => {
@@ -536,7 +698,7 @@ function VroomChart(props) {
536
698
  volumeRaf.current = null;
537
699
  }
538
700
  const dur = Math.max(0, transitionMs ?? 300);
539
- if (dur === 0) {
701
+ if (dur === 0 || reduceMotion) {
540
702
  volumeCollapseRef.current = { t: target, easing };
541
703
  handle.setVolumeCollapse(target, easing);
542
704
  const p = handle.render();
@@ -563,7 +725,7 @@ function VroomChart(props) {
563
725
  volumeRaf.current = null;
564
726
  }
565
727
  };
566
- }, [handle, volume?.enabled, transitionMs, pictureSV, volumeCollapseRef]);
728
+ }, [handle, volume?.enabled, transitionMs, reduceMotion, pictureSV, volumeCollapseRef]);
567
729
  const hitAxis = (0, import_react2.useCallback)(
568
730
  (x, y) => {
569
731
  if (!handle) return "chart";
@@ -775,6 +937,9 @@ function VroomChart(props) {
775
937
  }
776
938
  // Annotate the CommonJS export names for ESM import in node:
777
939
  0 && (module.exports = {
778
- VroomChart
940
+ VroomChart,
941
+ classifyTransition,
942
+ inferStepMs,
943
+ timeframeWindow
779
944
  });
780
945
  //# sourceMappingURL=index.js.map