react-native-vroom-chart 0.6.0 → 0.8.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 (49) hide show
  1. package/cpp/VroomChartHostObject.cpp +281 -33
  2. package/cpp/_core_include/vroom/vroom_chart.h +231 -36
  3. package/cpp/_core_src/bollinger.h +1 -1
  4. package/cpp/_core_src/candles.cpp +138 -41
  5. package/cpp/_core_src/candles.h +11 -1
  6. package/cpp/_core_src/chart.cpp +91 -40
  7. package/cpp/_core_src/chart.h +69 -29
  8. package/cpp/_core_src/chart_facade.cpp +247 -69
  9. package/cpp/_core_src/drawings.cpp +147 -4
  10. package/cpp/_core_src/drawings.h +10 -5
  11. package/cpp/_core_src/gradient.cpp +46 -0
  12. package/cpp/_core_src/gradient.h +31 -0
  13. package/cpp/_core_src/labels.cpp +49 -16
  14. package/cpp/_core_src/labels.h +33 -4
  15. package/cpp/_core_src/liquidity.cpp +3 -37
  16. package/cpp/_core_src/ma_overlay.cpp +174 -12
  17. package/cpp/_core_src/ma_overlay.h +55 -3
  18. package/cpp/_core_src/macd.cpp +13 -42
  19. package/cpp/_core_src/macd.h +11 -8
  20. package/cpp/_core_src/macd_pane.cpp +57 -27
  21. package/cpp/_core_src/price_line_layout.h +1 -1
  22. package/cpp/_core_src/rsi.cpp +4 -18
  23. package/cpp/_core_src/rsi.h +6 -6
  24. package/cpp/_core_src/rsi_pane.cpp +34 -19
  25. package/cpp/_core_src/series_ma.cpp +64 -0
  26. package/cpp/_core_src/series_ma.h +30 -0
  27. package/cpp/_core_src/style_inherit.h +35 -0
  28. package/cpp/_core_src/theme.cpp +2 -1
  29. package/cpp/_core_src/viewport.cpp +36 -12
  30. package/cpp/_core_src/viewport.h +50 -0
  31. package/cpp/_core_src/volume.cpp +33 -8
  32. package/cpp/_core_src/volume.h +12 -1
  33. package/cpp/_core_src/volume_anim.cpp +32 -0
  34. package/cpp/_core_src/volume_anim.h +41 -0
  35. package/lib/index.d.mts +206 -31
  36. package/lib/index.d.ts +206 -31
  37. package/lib/index.js +324 -44
  38. package/lib/index.js.map +1 -1
  39. package/lib/index.mjs +329 -52
  40. package/lib/index.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/src/VroomChart.tsx +100 -17
  43. package/src/dataTransitions.ts +148 -0
  44. package/src/easing.ts +40 -0
  45. package/src/index.ts +9 -0
  46. package/src/jsi.d.ts +126 -19
  47. package/src/theme.ts +1 -0
  48. package/src/types.ts +3 -0
  49. package/src/useChartCore.ts +273 -30
package/lib/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/VroomChart.tsx
2
- import React, { useEffect as useEffect2, useRef as useRef2, useCallback, useState as useState2, useMemo } from "react";
2
+ import React, { useEffect as useEffect2, useRef as useRef2, useCallback as useCallback2, useState as useState2, useMemo } from "react";
3
3
  import { View } from "react-native";
4
4
  import { Canvas, Picture, Skia } from "@shopify/react-native-skia";
5
5
  import {
@@ -7,15 +7,95 @@ import {
7
7
  GestureDetector,
8
8
  GestureHandlerRootView
9
9
  } from "react-native-gesture-handler";
10
- import { useSharedValue } from "react-native-reanimated";
10
+ import { useReducedMotion, useSharedValue } from "react-native-reanimated";
11
11
 
12
12
  // src/useChartCore.ts
13
- import { useEffect, useRef, useState } from "react";
13
+ import { useCallback, useEffect, useRef, useState } from "react";
14
14
 
15
15
  // src/NativeVroomChart.ts
16
16
  import { TurboModuleRegistry } from "react-native";
17
17
  var NativeVroomChart_default = TurboModuleRegistry.getEnforcing("VroomChartModule");
18
18
 
19
+ // src/dataTransitions.ts
20
+ var STEP_TOLERANCE = 0.01;
21
+ var MAX_SAME_ASSET_CLOSE_RATIO = 1.25;
22
+ var MAX_END_DRIFT_STEPS = 3;
23
+ var MAX_STREAM_ADVANCE_STEPS = 5;
24
+ function inferStepMs(candles) {
25
+ if (candles.length < 2) return null;
26
+ const k = Math.min(candles.length - 1, 8);
27
+ const diffs = [];
28
+ for (let i = 0; i < k; i++) diffs.push(candles[i + 1].timeMs - candles[i].timeMs);
29
+ diffs.sort((a, b) => a - b);
30
+ const median = diffs[Math.floor(diffs.length / 2)];
31
+ return median > 0 ? median : null;
32
+ }
33
+ function indexByTime(candles, t) {
34
+ let lo = 0;
35
+ let hi = candles.length - 1;
36
+ while (lo <= hi) {
37
+ const mid = lo + hi >>> 1;
38
+ const v = candles[mid].timeMs;
39
+ if (v === t) return mid;
40
+ if (v < t) lo = mid + 1;
41
+ else hi = mid - 1;
42
+ }
43
+ return -1;
44
+ }
45
+ function classifyTransition(prev, next, seriesKeyChanged) {
46
+ if (!prev || prev.length === 0) return "initial";
47
+ if (next.length === 0) return "stream";
48
+ if (seriesKeyChanged) return "reset";
49
+ const prevStep = inferStepMs(prev);
50
+ const nextStep = inferStepMs(next);
51
+ if (prevStep == null || nextStep == null) return "reset";
52
+ const prevLast = prev[prev.length - 1];
53
+ const nextLast = next[next.length - 1];
54
+ if (Math.abs(nextStep - prevStep) <= prevStep * STEP_TOLERANCE) {
55
+ const idx = indexByTime(next, prevLast.timeMs);
56
+ const aligned = idx >= 0;
57
+ const sharedBarRatio = aligned && next[idx].close > 0 && prevLast.close > 0 ? Math.max(next[idx].close / prevLast.close, prevLast.close / next[idx].close) : Infinity;
58
+ const advanced = nextLast.timeMs >= prevLast.timeMs && nextLast.timeMs - prevLast.timeMs <= MAX_STREAM_ADVANCE_STEPS * nextStep;
59
+ return sharedBarRatio <= MAX_SAME_ASSET_CLOSE_RATIO && advanced ? "stream" : "reset";
60
+ }
61
+ const closeRatio = prevLast.close > 0 && nextLast.close > 0 ? Math.max(nextLast.close / prevLast.close, prevLast.close / nextLast.close) : Infinity;
62
+ const prevEnd = prevLast.timeMs + prevStep;
63
+ const nextEnd = nextLast.timeMs + nextStep;
64
+ const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
65
+ return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
66
+ }
67
+ function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
68
+ const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
69
+ const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
70
+ const offsetSlots = Math.min(Math.max(offsetRaw, 0), slots * 0.75);
71
+ const endMs = Math.round(newLastMs + offsetSlots * newStepMs);
72
+ return { startMs: Math.round(endMs - slots * newStepMs), endMs };
73
+ }
74
+
75
+ // src/easing.ts
76
+ function ease(kind, p) {
77
+ switch (kind) {
78
+ case "linear":
79
+ return p;
80
+ case "ease-in":
81
+ return p * p;
82
+ case "ease-out":
83
+ return p * (2 - p);
84
+ default:
85
+ return p * p * (3 - 2 * p);
86
+ }
87
+ }
88
+ var EASINGS = [
89
+ "linear",
90
+ "ease-in",
91
+ "ease-out",
92
+ "ease-in-out"
93
+ ];
94
+ function easingIndex(kind) {
95
+ const i = kind ? EASINGS.indexOf(kind) : -1;
96
+ return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
97
+ }
98
+
19
99
  // src/packCandles.ts
20
100
  var BYTES_PER_CANDLE = 48;
21
101
  function packCandles(candles) {
@@ -72,8 +152,10 @@ var FLOAT_KEYS = {
72
152
  // VROOM_FLOAT_CANDLE_RADIUS_PX
73
153
  volumeRadius: 10,
74
154
  // VROOM_FLOAT_VOLUME_RADIUS_PX
75
- lineWidth: 11
155
+ lineWidth: 11,
76
156
  // VROOM_FLOAT_LINE_WIDTH_PX
157
+ lineGradientOpacity: 12
158
+ // VROOM_FLOAT_LINE_GRADIENT_OPACITY
77
159
  };
78
160
  var BOOL_KEYS = {
79
161
  wickRoundCap: 9
@@ -119,16 +201,43 @@ var MA_SOURCES = [
119
201
  "hlc3",
120
202
  "ohlc4"
121
203
  ];
204
+ var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
122
205
  function overlayToNumeric(o) {
123
206
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
124
207
  return {
125
- kind: o.kind === "ema" ? 1 : 0,
126
- period: o.length,
208
+ kind: o.maType === "ema" ? 1 : 0,
209
+ period: o.period,
127
210
  source: srcIdx < 0 ? 0 : srcIdx,
128
211
  color: (o.color != null ? parseColor(o.color) : null) ?? 4280902399,
129
212
  width: o.width ?? 1.5
130
213
  };
131
214
  }
215
+ function rsiToSpec(cfg) {
216
+ return {
217
+ enabled: cfg?.enabled ?? false,
218
+ period: cfg?.period ?? 14,
219
+ upperBand: cfg?.upperBand ?? 70,
220
+ lowerBand: cfg?.lowerBand ?? 30,
221
+ maPeriod: cfg?.maPeriod ?? 14,
222
+ maKind: cfg?.maType === "ema" ? 1 : 0,
223
+ maVisible: cfg?.maVisible ?? true,
224
+ lineColor: inheritColor(cfg?.lineColor),
225
+ lineWidth: cfg?.lineWidth ?? -1,
226
+ lineVisible: cfg?.lineVisible ?? true,
227
+ maColor: inheritColor(cfg?.maColor),
228
+ maWidth: cfg?.maWidth ?? -1,
229
+ bandColor: inheritColor(cfg?.bandColor),
230
+ bandsVisible: cfg?.bandsVisible ?? true
231
+ };
232
+ }
233
+ function vwapToSpec(cfg) {
234
+ return {
235
+ enabled: cfg?.enabled ?? false,
236
+ resetOffsetMin: cfg?.resetMinutes ?? 0,
237
+ color: inheritColor(cfg?.color),
238
+ width: cfg?.width ?? -1
239
+ };
240
+ }
132
241
  var DEFAULT_BB_BAND_COLOR = 4280902399;
133
242
  var DEFAULT_BB_BASIS_COLOR = 4294929664;
134
243
  function bollingerToSpec(cfg) {
@@ -138,17 +247,52 @@ function bollingerToSpec(cfg) {
138
247
  period: cfg?.period ?? 20,
139
248
  mult: cfg?.stdDev ?? 2,
140
249
  source: srcIdx < 0 ? 0 : srcIdx,
141
- basisKind: cfg?.basis === "ema" ? 1 : 0,
250
+ basisKind: cfg?.maType === "ema" ? 1 : 0,
142
251
  upperColor: (cfg?.upperColor != null ? parseColor(cfg.upperColor) : null) ?? DEFAULT_BB_BAND_COLOR,
143
252
  upperWidth: cfg?.upperWidth ?? 1,
144
253
  middleColor: (cfg?.middleColor != null ? parseColor(cfg.middleColor) : null) ?? DEFAULT_BB_BASIS_COLOR,
145
254
  middleWidth: cfg?.middleWidth ?? 1,
146
255
  lowerColor: (cfg?.lowerColor != null ? parseColor(cfg.lowerColor) : null) ?? DEFAULT_BB_BAND_COLOR,
147
256
  lowerWidth: cfg?.lowerWidth ?? 1,
148
- fillEnabled: cfg?.fill ?? true,
257
+ fillEnabled: cfg?.fillVisible ?? true,
149
258
  fillOpacity: cfg?.fillOpacity ?? 0.1
150
259
  };
151
260
  }
261
+ function macdToSpec(cfg) {
262
+ const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
263
+ return {
264
+ enabled: cfg?.enabled ?? false,
265
+ fast: cfg?.fast ?? 12,
266
+ slow: cfg?.slow ?? 26,
267
+ signal: cfg?.signal ?? 9,
268
+ source: srcIdx < 0 ? 0 : srcIdx,
269
+ maKind: cfg?.maType === "sma" ? 0 : 1,
270
+ signalMaKind: cfg?.signalMaType === "sma" ? 0 : 1,
271
+ lineColor: inheritColor(cfg?.lineColor),
272
+ lineWidth: cfg?.lineWidth ?? -1,
273
+ lineVisible: cfg?.lineVisible ?? true,
274
+ signalColor: inheritColor(cfg?.signalColor),
275
+ signalWidth: cfg?.signalWidth ?? -1,
276
+ signalVisible: cfg?.signalVisible ?? true,
277
+ histVisible: cfg?.histogramVisible ?? true,
278
+ histUpColor: inheritColor(cfg?.histogramUpColor),
279
+ histUpFadingColor: inheritColor(cfg?.histogramUpFadingColor),
280
+ histDownColor: inheritColor(cfg?.histogramDownColor),
281
+ histDownFadingColor: inheritColor(cfg?.histogramDownFadingColor),
282
+ zeroColor: inheritColor(cfg?.zeroLineColor),
283
+ zeroVisible: cfg?.zeroLineVisible ?? true
284
+ };
285
+ }
286
+ function volumeToSpec(cfg) {
287
+ return {
288
+ enabled: cfg?.enabled ?? true,
289
+ heightFrac: cfg?.height ?? -1,
290
+ opacity: cfg?.opacity ?? -1,
291
+ radiusPx: cfg?.radius ?? -1,
292
+ upColor: (cfg?.upColor != null ? parseColor(cfg.upColor) : null) ?? 0,
293
+ downColor: (cfg?.downColor != null ? parseColor(cfg.downColor) : null) ?? 0
294
+ };
295
+ }
152
296
  var DEFAULT_PRICE_LINE_COLOR = 4293874512;
153
297
  var DEFAULT_PRICE_LINE_BODY_BG = 3642499368;
154
298
  var DEFAULT_PRICE_LINE_HOVER_BOOST = 1.25;
@@ -186,14 +330,53 @@ function ensureInstalled() {
186
330
  }
187
331
  installed = true;
188
332
  }
189
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, priceLines) {
333
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
190
334
  const handleRef = useRef(null);
191
335
  const defaultWidthAppliedRef = useRef(false);
336
+ const volumeCollapseRef = useRef(null);
337
+ const prevDataRef = useRef(null);
338
+ const intervalMorphRaf = useRef(null);
192
339
  const [picture, setPicture] = useState(null);
193
340
  if (!handleRef.current && size.width > 0 && size.height > 0) {
194
341
  ensureInstalled();
195
342
  handleRef.current = globalThis.VroomChartJSI.create();
196
343
  }
344
+ const animRef = useRef({ ms: 300, easing: void 0, reduceMotion: false });
345
+ animRef.current = {
346
+ ms: Math.max(0, transition?.transitionMs ?? 300),
347
+ easing: transition?.transitionEasing,
348
+ reduceMotion: transition?.reduceMotion ?? false
349
+ };
350
+ const onFrameRef = useRef(transition?.onFrame);
351
+ onFrameRef.current = transition?.onFrame;
352
+ const seriesKey = transition?.seriesKey;
353
+ const endIntervalMorph = useCallback(() => {
354
+ if (intervalMorphRaf.current != null) {
355
+ cancelAnimationFrame(intervalMorphRaf.current);
356
+ intervalMorphRaf.current = null;
357
+ }
358
+ handleRef.current?.setIntervalMorph(1);
359
+ }, []);
360
+ const startIntervalMorph = useCallback((h) => {
361
+ const { ms, easing } = animRef.current;
362
+ const start = performance.now();
363
+ const step = (now) => {
364
+ const p = Math.min(1, (now - start) / ms);
365
+ h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
366
+ const pic = h.render();
367
+ if (pic) onFrameRef.current?.(pic);
368
+ intervalMorphRaf.current = p < 1 ? requestAnimationFrame(step) : null;
369
+ };
370
+ intervalMorphRaf.current = requestAnimationFrame(step);
371
+ }, []);
372
+ useEffect(() => {
373
+ return () => {
374
+ if (intervalMorphRaf.current != null) {
375
+ cancelAnimationFrame(intervalMorphRaf.current);
376
+ intervalMorphRaf.current = null;
377
+ }
378
+ };
379
+ }, []);
197
380
  const explicit = visibleRange != null;
198
381
  const startMs = visibleRange?.startMs ?? 0;
199
382
  const endMs = visibleRange?.endMs ?? 0;
@@ -203,6 +386,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
203
386
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
204
387
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
205
388
  const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
389
+ const volumeKey = volume ? JSON.stringify(volume) : "";
206
390
  const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
207
391
  useEffect(() => {
208
392
  const h = handleRef.current;
@@ -212,8 +396,54 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
212
396
  h.setDefaultCandleWidth(defaultCandleWidth);
213
397
  defaultWidthAppliedRef.current = true;
214
398
  }
399
+ let morphing = false;
215
400
  if (candles.length > 0) {
216
- h.setCandles(packCandles(candles));
401
+ const prev = prevDataRef.current;
402
+ const freshHandle = prev == null || prev.handle !== h;
403
+ if (freshHandle || prev.candles !== candles || prev.seriesKey !== seriesKey) {
404
+ const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
405
+ let tfArgs = null;
406
+ let prevEnvelope = null;
407
+ if (transitionKind === "timeframe" && prev != null) {
408
+ const oldWindow = h.getVisibleRange();
409
+ const oldStepMs = inferStepMs(prev.candles);
410
+ if (oldWindow.endMs > oldWindow.startMs && oldStepMs != null) {
411
+ tfArgs = {
412
+ oldWindow,
413
+ oldStepMs,
414
+ oldLastMs: prev.candles[prev.candles.length - 1].timeMs
415
+ };
416
+ }
417
+ prevEnvelope = h.getVisiblePriceEnvelope();
418
+ morphing = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
419
+ if (morphing) {
420
+ endIntervalMorph();
421
+ h.beginIntervalMorph();
422
+ }
423
+ } else if (transitionKind === "initial" || transitionKind === "reset") {
424
+ endIntervalMorph();
425
+ }
426
+ h.setCandles(packCandles(candles));
427
+ if (transitionKind === "timeframe") {
428
+ const newStepMs = inferStepMs(candles);
429
+ if (tfArgs && newStepMs != null) {
430
+ const w = timeframeWindow(
431
+ tfArgs.oldWindow,
432
+ tfArgs.oldStepMs,
433
+ tfArgs.oldLastMs,
434
+ newStepMs,
435
+ candles[candles.length - 1].timeMs
436
+ );
437
+ h.setVisibleRange(w.startMs, w.endMs);
438
+ }
439
+ if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
440
+ else h.resetPriceScale();
441
+ if (morphing) startIntervalMorph(h);
442
+ } else if (transitionKind === "reset") {
443
+ h.resetView();
444
+ }
445
+ prevDataRef.current = { handle: h, candles, seriesKey };
446
+ }
217
447
  }
218
448
  if (explicit) {
219
449
  h.setVisibleRange(startMs, endMs);
@@ -221,40 +451,27 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
221
451
  if (theme) {
222
452
  applyTheme(h, theme);
223
453
  }
224
- h.setRSI(
225
- rsi?.enabled ?? false,
226
- rsi?.period ?? 14,
227
- rsi?.upperBand ?? 70,
228
- rsi?.lowerBand ?? 30,
229
- rsi?.maEnabled ?? true,
230
- rsi?.maPeriod ?? 14
231
- );
232
- h.setMACD(
233
- macd?.enabled ?? false,
234
- macd?.fast ?? 12,
235
- macd?.slow ?? 26,
236
- macd?.signal ?? 9
237
- );
454
+ h.setRSI(rsiToSpec(rsi));
455
+ h.setMACD(macdToSpec(macd));
238
456
  h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
239
- h.setVWAP(
240
- vwap?.enabled ?? false,
241
- vwap?.resetMinutes ?? 0,
242
- (vwap?.color != null ? parseColor(vwap.color) : null) ?? 4278238420,
243
- vwap?.width ?? 1.5
244
- );
457
+ h.setVWAP(vwapToSpec(vwap));
245
458
  h.setBollinger(bollingerToSpec(bollingerBands));
459
+ h.setVolume(volumeToSpec(volume));
460
+ const collapse = volumeCollapseRef.current;
461
+ if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
246
462
  h.setPriceLines(
247
463
  priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
248
464
  );
249
- setPicture(h.render());
250
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, priceLinesKey]);
251
- return { handle: handleRef.current, picture };
465
+ if (!morphing) setPicture(h.render());
466
+ }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
467
+ return { handle: handleRef.current, picture, volumeCollapseRef };
252
468
  }
253
469
 
254
470
  // src/VroomChart.tsx
255
471
  function VroomChart(props) {
256
472
  const {
257
473
  candles,
474
+ seriesKey,
258
475
  width: widthProp,
259
476
  height: heightProp,
260
477
  style,
@@ -262,12 +479,14 @@ function VroomChart(props) {
262
479
  defaultCandleWidth,
263
480
  chartType,
264
481
  transitionMs,
482
+ transitionEasing,
265
483
  theme,
266
484
  rsi,
267
485
  macd,
268
486
  movingAverages,
269
487
  vwap,
270
488
  bollingerBands,
489
+ volume,
271
490
  crosshairOffset = 40,
272
491
  onCrosshair,
273
492
  onViewportChange,
@@ -280,7 +499,7 @@ function VroomChart(props) {
280
499
  const [measured, setMeasured] = useState2({ width: 0, height: 0 });
281
500
  const width = widthProp ?? measured.width;
282
501
  const height = heightProp ?? measured.height;
283
- const onLayout = useCallback((e) => {
502
+ const onLayout = useCallback2((e) => {
284
503
  const w = Math.round(e.nativeEvent.layout.width);
285
504
  const h = Math.round(e.nativeEvent.layout.height);
286
505
  setMeasured(
@@ -295,7 +514,20 @@ function VroomChart(props) {
295
514
  } : void 0,
296
515
  [priceLines, priceLinesStyle, onPriceLineClose]
297
516
  );
298
- const { handle, picture } = useChartCore(
517
+ const emptyPicture = useMemo(() => {
518
+ const rec = Skia.PictureRecorder();
519
+ rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
520
+ return rec.finishRecordingAsPicture();
521
+ }, []);
522
+ const pictureSV = useSharedValue(emptyPicture);
523
+ const reduceMotion = useReducedMotion();
524
+ const onFrame = useCallback2(
525
+ (p) => {
526
+ pictureSV.value = p;
527
+ },
528
+ [pictureSV]
529
+ );
530
+ const { handle, picture, volumeCollapseRef } = useChartCore(
299
531
  candles,
300
532
  { width, height },
301
533
  visibleRange,
@@ -307,21 +539,17 @@ function VroomChart(props) {
307
539
  movingAverages,
308
540
  vwap,
309
541
  bollingerBands,
310
- priceLinesProp
542
+ volume,
543
+ priceLinesProp,
544
+ { seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame }
311
545
  );
312
- const emptyPicture = useMemo(() => {
313
- const rec = Skia.PictureRecorder();
314
- rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
315
- return rec.finishRecordingAsPicture();
316
- }, []);
317
- const pictureSV = useSharedValue(emptyPicture);
318
546
  const crosshairActive = useRef2(false);
319
547
  const lastCrosshairTime = useRef2(null);
320
548
  useEffect2(() => {
321
549
  if (picture) pictureSV.value = picture;
322
550
  }, [picture, pictureSV]);
323
551
  const decayRaf = useRef2(null);
324
- const cancelDecay = useCallback(() => {
552
+ const cancelDecay = useCallback2(() => {
325
553
  if (decayRaf.current != null) {
326
554
  cancelAnimationFrame(decayRaf.current);
327
555
  decayRaf.current = null;
@@ -329,7 +557,7 @@ function VroomChart(props) {
329
557
  }, []);
330
558
  useEffect2(() => cancelDecay, [cancelDecay]);
331
559
  const animRaf = useRef2(null);
332
- const animTick = useCallback(() => {
560
+ const animTick = useCallback2(() => {
333
561
  animRaf.current = null;
334
562
  if (!handle) return;
335
563
  const next = handle.render();
@@ -338,7 +566,7 @@ function VroomChart(props) {
338
566
  animRaf.current = requestAnimationFrame(animTick);
339
567
  }
340
568
  }, [handle, pictureSV]);
341
- const maybeStartAnim = useCallback(() => {
569
+ const maybeStartAnim = useCallback2(() => {
342
570
  if (animRaf.current != null) return;
343
571
  if (!handle?.isAnimating()) return;
344
572
  animRaf.current = requestAnimationFrame(animTick);
@@ -354,6 +582,8 @@ function VroomChart(props) {
354
582
  const morphRaf = useRef2(null);
355
583
  const morphFade = useRef2(null);
356
584
  const morphHandle = useRef2(null);
585
+ const easingRef = useRef2(transitionEasing);
586
+ easingRef.current = transitionEasing;
357
587
  useEffect2(() => {
358
588
  if (!handle) return void 0;
359
589
  const target = chartType === "line" ? 1 : 0;
@@ -383,10 +613,9 @@ function VroomChart(props) {
383
613
  const step = (now) => {
384
614
  if (startTs == null) startTs = now;
385
615
  const prog = Math.min(1, (now - startTs) / dur);
386
- const e = prog * prog * (3 - 2 * prog);
387
- const fade = from + (target - from) * e;
616
+ const fade = from + (target - from) * ease(easingRef.current, prog);
388
617
  morphFade.current = fade;
389
- handle.setMorph(fade, fade);
618
+ handle.setMorph(reduceMotion ? 0 : fade, fade);
390
619
  const p = handle.render();
391
620
  if (p) pictureSV.value = p;
392
621
  if (prog < 1) {
@@ -406,8 +635,53 @@ function VroomChart(props) {
406
635
  morphRaf.current = null;
407
636
  }
408
637
  };
409
- }, [handle, chartType, transitionMs, pictureSV]);
410
- const hitAxis = useCallback(
638
+ }, [handle, chartType, transitionMs, reduceMotion, pictureSV]);
639
+ const volumeRaf = useRef2(null);
640
+ const volumeHandle = useRef2(null);
641
+ useEffect2(() => {
642
+ if (!handle) return void 0;
643
+ const target = volume?.enabled ?? true ? 0 : 1;
644
+ const easing = easingIndex(easingRef.current);
645
+ if (volumeHandle.current !== handle || volumeCollapseRef.current == null) {
646
+ volumeHandle.current = handle;
647
+ volumeCollapseRef.current = { t: target, easing };
648
+ return void 0;
649
+ }
650
+ if (volumeCollapseRef.current.t === target) return void 0;
651
+ if (volumeRaf.current != null) {
652
+ cancelAnimationFrame(volumeRaf.current);
653
+ volumeRaf.current = null;
654
+ }
655
+ const dur = Math.max(0, transitionMs ?? 300);
656
+ if (dur === 0 || reduceMotion) {
657
+ volumeCollapseRef.current = { t: target, easing };
658
+ handle.setVolumeCollapse(target, easing);
659
+ const p = handle.render();
660
+ if (p) pictureSV.value = p;
661
+ return void 0;
662
+ }
663
+ const from = volumeCollapseRef.current.t;
664
+ let startTs = null;
665
+ const step = (now) => {
666
+ if (startTs == null) startTs = now;
667
+ const prog = Math.min(1, (now - startTs) / dur);
668
+ const t = prog < 1 ? from + (target - from) * prog : target;
669
+ const kind = easingIndex(easingRef.current);
670
+ volumeCollapseRef.current = { t, easing: kind };
671
+ handle.setVolumeCollapse(t, kind);
672
+ const p = handle.render();
673
+ if (p) pictureSV.value = p;
674
+ volumeRaf.current = prog < 1 ? requestAnimationFrame(step) : null;
675
+ };
676
+ volumeRaf.current = requestAnimationFrame(step);
677
+ return () => {
678
+ if (volumeRaf.current != null) {
679
+ cancelAnimationFrame(volumeRaf.current);
680
+ volumeRaf.current = null;
681
+ }
682
+ };
683
+ }, [handle, volume?.enabled, transitionMs, reduceMotion, pictureSV, volumeCollapseRef]);
684
+ const hitAxis = useCallback2(
411
685
  (x, y) => {
412
686
  if (!handle) return "chart";
413
687
  const { yAxisWidth, xAxisHeight, indicatorHeight } = handle.getAxisMetrics();
@@ -420,7 +694,7 @@ function VroomChart(props) {
420
694
  },
421
695
  [handle, width, height]
422
696
  );
423
- const hitPriceLine = useCallback(
697
+ const hitPriceLine = useCallback2(
424
698
  (x, y) => {
425
699
  if (!handle || !priceLines?.length) return null;
426
700
  const hit = handle.hitTestPriceLine(x, y);
@@ -617,6 +891,9 @@ function VroomChart(props) {
617
891
  );
618
892
  }
619
893
  export {
620
- VroomChart
894
+ VroomChart,
895
+ classifyTransition,
896
+ inferStepMs,
897
+ timeframeWindow
621
898
  };
622
899
  //# sourceMappingURL=index.mjs.map