react-native-vroom-chart 0.13.0 → 0.14.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.
package/lib/index.d.ts CHANGED
@@ -187,6 +187,13 @@ type ChartType = 'candles' | 'line';
187
187
  * Defaults to `'ease-in-out'`.
188
188
  */
189
189
  type TransitionEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
190
+ /**
191
+ * How a same-asset interval switch animates. `'transform'` (default) lerps
192
+ * each visible column into its counterpart. `'fade'` fades the old scene out
193
+ * then the new one in — use when the two windows don’t share a 1:1 pairing
194
+ * (e.g. a fixed lookback).
195
+ */
196
+ type IntervalTransition = 'transform' | 'fade';
190
197
  /** Active drawing tool while in `draw` mode. `null` draws nothing. */
191
198
  type DrawTool = null | 'line' | 'box' | 'pencil' | 'path';
192
199
  /** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
@@ -200,9 +207,12 @@ type DrawPoint = {
200
207
  type DrawingBase = {
201
208
  /** Stable unique id (the chart generates one for drawings it creates). */
202
209
  id: string;
203
- /** Stroke color (hex string or packed ARGB number). Default solid blue. */
210
+ /**
211
+ * Stroke color (hex string or packed ARGB number). New drawings take
212
+ * `drawingStyle.color` when set; otherwise `#ff2962ff`.
213
+ */
204
214
  color?: VroomColor;
205
- /** Stroke width in px. Default 2. */
215
+ /** Stroke width in px. New drawings take `drawingStyle.width` when set; otherwise 2. */
206
216
  width?: number;
207
217
  /**
208
218
  * Protect the drawing from editing. A locked drawing still renders and can
@@ -295,6 +305,29 @@ type DrawingStyle = {
295
305
  fill?: VroomColor;
296
306
  locked?: boolean;
297
307
  };
308
+ /**
309
+ * Default appearance for drawings the user creates — the live draft and the
310
+ * object handed to `onDrawingComplete`. Paste copies the source drawing's
311
+ * style instead of this default.
312
+ *
313
+ * Omitted fields keep the library defaults: a 2px `#ff2962ff` stroke, and for
314
+ * boxes a 10% tint of the stroke as fill.
315
+ *
316
+ * Prefer 6-digit hex for `color` (`'#00FFFF'`). vroom treats that as opaque,
317
+ * and CSS swatches preview it correctly. 8-digit hex is `#aarrggbb`, not CSS
318
+ * `#rrggbbaa`.
319
+ */
320
+ type DefaultDrawingStyle = {
321
+ /** Stroke color, used for the live draft and stamped onto the committed drawing. */
322
+ color?: VroomColor;
323
+ /** Stroke width in px. Default 2. */
324
+ width?: number;
325
+ /**
326
+ * Box interior fill. Omitted, new boxes keep the default 10% stroke tint.
327
+ * Ignored by line, pencil, and path.
328
+ */
329
+ fill?: VroomColor;
330
+ };
298
331
  /**
299
332
  * Storage adapter for **managed** drawing persistence. Provide it via the
300
333
  * `drawingStore` prop and the chart owns the drawings array itself — loading and
@@ -720,16 +753,23 @@ type VroomChartCoreProps = {
720
753
  chartType?: ChartType;
721
754
  /**
722
755
  * Duration (ms) of the animated transitions: the candle↔line switch when
723
- * `chartType` changes, and the candle reshape when the `candles` array is
756
+ * `chartType` changes, and the interval switch when the `candles` array is
724
757
  * swapped for a different interval of the same asset. Default ~300. `0` snaps
725
- * instantly. Ignored (snaps) when the OS requests reduced motion, which
726
- * instead uses a plain cross-fade.
758
+ * instantly. Ignored (snaps) when the OS requests reduced motion.
727
759
  */
728
760
  transitionMs?: number;
729
761
  /**
730
762
  * Easing curve applied to those transitions. Default `'ease-in-out'`.
731
763
  */
732
764
  transitionEasing?: TransitionEasing;
765
+ /**
766
+ * How a same-asset interval switch animates. `'transform'` (default) lerps
767
+ * each visible column into its counterpart. `'fade'` fades the old scene out
768
+ * then the new one in — use when the two windows don’t share a 1:1 pairing
769
+ * (e.g. a fixed lookback). Same duration/easing as `transitionMs`. Reduced
770
+ * motion still snaps.
771
+ */
772
+ intervalTransition?: IntervalTransition;
733
773
  theme?: VroomTheme;
734
774
  /** RSI indicator (pane below the candles). Omit/disable to hide it. */
735
775
  rsi?: RSIConfig;
@@ -802,6 +842,12 @@ type VroomChartCoreProps = {
802
842
  mode?: ChartMode;
803
843
  /** Active drawing tool while in `draw` mode. Default `null` (draws nothing). */
804
844
  tool?: DrawTool;
845
+ /**
846
+ * Default appearance for drawings the user creates (live draft + the object
847
+ * handed to `onDrawingComplete`). Paste copies the source drawing's style
848
+ * instead of this. Omitted fields keep the library defaults.
849
+ */
850
+ drawingStyle?: DefaultDrawingStyle;
805
851
  /**
806
852
  * Committed drawings to render, anchored to data so they track the candles on
807
853
  * pan/zoom. This is a controlled prop: append the value the chart hands you in
@@ -945,4 +991,4 @@ declare function classifyTransition(prev: Candle[] | null, next: Candle[], serie
945
991
  */
946
992
  declare function timeframeWindow(oldWindow: VisibleRange, oldStepMs: number, oldLastMs: number, newStepMs: number, newLastMs: number): VisibleRange;
947
993
 
948
- 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 };
994
+ export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type DataTransition, type DefaultDrawingStyle, type IntervalTransition, 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
@@ -384,11 +384,12 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
384
384
  ensureInstalled();
385
385
  handleRef.current = globalThis.VroomChartJSI.create();
386
386
  }
387
- const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false });
387
+ const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false, interval: "transform" });
388
388
  animRef.current = {
389
389
  ms: Math.max(0, transition?.transitionMs ?? 300),
390
390
  easing: transition?.transitionEasing,
391
- reduceMotion: transition?.reduceMotion ?? false
391
+ reduceMotion: transition?.reduceMotion ?? false,
392
+ interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
392
393
  };
393
394
  const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
394
395
  onFrameRef.current = transition?.onFrame;
@@ -461,7 +462,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
461
462
  morphing = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
462
463
  if (morphing) {
463
464
  endIntervalMorph();
464
- h.beginIntervalMorph();
465
+ h.beginIntervalMorph(animRef.current.interval);
465
466
  }
466
467
  } else if (transitionKind === "initial" || transitionKind === "reset") {
467
468
  endIntervalMorph();
@@ -514,6 +515,9 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
514
515
  }
515
516
 
516
517
  // src/VroomChart.tsx
518
+ function isSkImage(frame) {
519
+ return typeof frame.getImageInfo === "function";
520
+ }
517
521
  function VroomChart(props) {
518
522
  const {
519
523
  candles,
@@ -526,6 +530,7 @@ function VroomChart(props) {
526
530
  chartType,
527
531
  transitionMs,
528
532
  transitionEasing,
533
+ intervalTransition,
529
534
  theme,
530
535
  rsi,
531
536
  macd,
@@ -565,17 +570,38 @@ function VroomChart(props) {
565
570
  rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
566
571
  return rec.finishRecordingAsPicture();
567
572
  }, []);
573
+ const emptyImage = (0, import_react2.useMemo)(() => {
574
+ const data = import_react_native_skia.Skia.Data.fromBytes(new Uint8Array(4));
575
+ return import_react_native_skia.Skia.Image.MakeImage(
576
+ {
577
+ width: 1,
578
+ height: 1,
579
+ colorType: import_react_native_skia.ColorType.RGBA_8888,
580
+ alphaType: import_react_native_skia.AlphaType.Premul
581
+ },
582
+ data,
583
+ 4
584
+ );
585
+ }, []);
568
586
  const pictureSV = (0, import_react_native_reanimated.useSharedValue)(emptyPicture);
587
+ const imageSV = (0, import_react_native_reanimated.useSharedValue)(emptyImage);
588
+ const applyFrame = (0, import_react2.useCallback)(
589
+ (frame) => {
590
+ if (isSkImage(frame)) imageSV.value = frame;
591
+ else pictureSV.value = frame;
592
+ },
593
+ [imageSV, pictureSV]
594
+ );
569
595
  const reduceMotion = (0, import_react_native_reanimated.useReducedMotion)();
570
596
  const onFrame = (0, import_react2.useCallback)(
571
597
  (p) => {
572
- pictureSV.value = p;
598
+ applyFrame(p);
573
599
  },
574
- [pictureSV]
600
+ [applyFrame]
575
601
  );
576
602
  const { handle, picture, volumeCollapseRef } = useChartCore(
577
603
  candles,
578
- { width, height },
604
+ { width, height, pxRatio: import_react_native2.PixelRatio.get() },
579
605
  visibleRange,
580
606
  defaultCandleWidth,
581
607
  chartType,
@@ -587,7 +613,7 @@ function VroomChart(props) {
587
613
  bollingerBands,
588
614
  volume,
589
615
  priceLinesProp,
590
- { seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame }
616
+ { seriesKey, transitionMs, transitionEasing, intervalTransition, reduceMotion, onFrame }
591
617
  );
592
618
  const crosshairActive = (0, import_react2.useRef)(false);
593
619
  const lastCrosshairTime = (0, import_react2.useRef)(null);
@@ -604,11 +630,11 @@ function VroomChart(props) {
604
630
  animRaf.current = null;
605
631
  if (!handle) return;
606
632
  const next = handle.render();
607
- if (next) pictureSV.value = next;
633
+ if (next) applyFrame(next);
608
634
  if (handle.isAnimating()) {
609
635
  animRaf.current = requestAnimationFrame(animTick);
610
636
  }
611
- }, [handle, pictureSV]);
637
+ }, [handle, applyFrame]);
612
638
  const maybeStartAnim = (0, import_react2.useCallback)(() => {
613
639
  if (animRaf.current != null) return;
614
640
  if (!handle?.isAnimating()) return;
@@ -623,9 +649,9 @@ function VroomChart(props) {
623
649
  };
624
650
  }, []);
625
651
  (0, import_react2.useEffect)(() => {
626
- if (picture) pictureSV.value = picture;
652
+ if (picture) applyFrame(picture);
627
653
  maybeStartAnim();
628
- }, [picture, pictureSV, maybeStartAnim]);
654
+ }, [picture, applyFrame, maybeStartAnim]);
629
655
  const morphRaf = (0, import_react2.useRef)(null);
630
656
  const morphFade = (0, import_react2.useRef)(null);
631
657
  const morphHandle = (0, import_react2.useRef)(null);
@@ -639,7 +665,7 @@ function VroomChart(props) {
639
665
  morphFade.current = target;
640
666
  handle.setChartType(target);
641
667
  const p = handle.render();
642
- if (p) pictureSV.value = p;
668
+ if (p) applyFrame(p);
643
669
  maybeStartAnim();
644
670
  return void 0;
645
671
  }
@@ -656,7 +682,7 @@ function VroomChart(props) {
656
682
  morphFade.current = target;
657
683
  handle.setChartType(target);
658
684
  const p = handle.render();
659
- if (p) pictureSV.value = p;
685
+ if (p) applyFrame(p);
660
686
  maybeStartAnim();
661
687
  return void 0;
662
688
  }
@@ -669,7 +695,7 @@ function VroomChart(props) {
669
695
  morphFade.current = fade;
670
696
  handle.setMorph(reduceMotion ? 0 : fade, fade);
671
697
  const p = handle.render();
672
- if (p) pictureSV.value = p;
698
+ if (p) applyFrame(p);
673
699
  if (prog < 1) {
674
700
  morphRaf.current = requestAnimationFrame(step);
675
701
  } else {
@@ -677,7 +703,7 @@ function VroomChart(props) {
677
703
  morphFade.current = target;
678
704
  handle.setChartType(target);
679
705
  const q = handle.render();
680
- if (q) pictureSV.value = q;
706
+ if (q) applyFrame(q);
681
707
  maybeStartAnim();
682
708
  }
683
709
  };
@@ -688,7 +714,7 @@ function VroomChart(props) {
688
714
  morphRaf.current = null;
689
715
  }
690
716
  };
691
- }, [handle, chartType, transitionMs, reduceMotion, pictureSV, maybeStartAnim]);
717
+ }, [handle, chartType, transitionMs, reduceMotion, applyFrame, maybeStartAnim]);
692
718
  const volumeRaf = (0, import_react2.useRef)(null);
693
719
  const volumeHandle = (0, import_react2.useRef)(null);
694
720
  (0, import_react2.useEffect)(() => {
@@ -714,7 +740,7 @@ function VroomChart(props) {
714
740
  volumeCollapseRef.current = { t: target, easing };
715
741
  handle.setVolumeCollapse(target, easing);
716
742
  const p = handle.render();
717
- if (p) pictureSV.value = p;
743
+ if (p) applyFrame(p);
718
744
  maybeStartAnim();
719
745
  return void 0;
720
746
  }
@@ -728,7 +754,7 @@ function VroomChart(props) {
728
754
  volumeCollapseRef.current = { t, easing: kind };
729
755
  handle.setVolumeCollapse(t, kind);
730
756
  const p = handle.render();
731
- if (p) pictureSV.value = p;
757
+ if (p) applyFrame(p);
732
758
  if (prog < 1) {
733
759
  volumeRaf.current = requestAnimationFrame(step);
734
760
  } else {
@@ -748,7 +774,7 @@ function VroomChart(props) {
748
774
  volume?.enabled,
749
775
  transitionMs,
750
776
  reduceMotion,
751
- pictureSV,
777
+ applyFrame,
752
778
  volumeCollapseRef,
753
779
  maybeStartAnim
754
780
  ]);
@@ -766,7 +792,7 @@ function VroomChart(props) {
766
792
  axisCollapse.current = { y: targetY, x: targetX };
767
793
  handle.setAxisCollapse(targetY, targetX);
768
794
  const p = handle.render();
769
- if (p) pictureSV.value = p;
795
+ if (p) applyFrame(p);
770
796
  maybeStartAnim();
771
797
  return void 0;
772
798
  }
@@ -785,7 +811,7 @@ function VroomChart(props) {
785
811
  axisCollapse.current = { y: targetY, x: targetX };
786
812
  handle.setAxisCollapse(targetY, targetX);
787
813
  const p = handle.render();
788
- if (p) pictureSV.value = p;
814
+ if (p) applyFrame(p);
789
815
  maybeStartAnim();
790
816
  return void 0;
791
817
  }
@@ -799,7 +825,7 @@ function VroomChart(props) {
799
825
  axisCollapse.current = { y, x };
800
826
  handle.setAxisCollapse(y, x);
801
827
  const p = handle.render();
802
- if (p) pictureSV.value = p;
828
+ if (p) applyFrame(p);
803
829
  if (prog < 1) {
804
830
  axisRaf.current = requestAnimationFrame(step);
805
831
  } else {
@@ -820,7 +846,7 @@ function VroomChart(props) {
820
846
  showXAxis,
821
847
  transitionMs,
822
848
  reduceMotion,
823
- pictureSV,
849
+ applyFrame,
824
850
  maybeStartAnim
825
851
  ]);
826
852
  const hitAxis = (0, import_react2.useCallback)(
@@ -860,7 +886,7 @@ function VroomChart(props) {
860
886
  priceDrag.current = { index: pl.index, id: pl.line.id, price: pl.line.price };
861
887
  handle.setPriceLineDrag(pl.index, pl.line.price);
862
888
  const p = handle.render();
863
- if (p) pictureSV.value = p;
889
+ if (p) applyFrame(p);
864
890
  }
865
891
  }
866
892
  }).onChange((e) => {
@@ -883,7 +909,7 @@ function VroomChart(props) {
883
909
  next = handle.render();
884
910
  } else if (crosshairActive.current) {
885
911
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
886
- if (ch) pictureSV.value = ch;
912
+ if (ch) applyFrame(ch);
887
913
  const info = handle.getCrosshairInfo();
888
914
  const t = info?.timeMs ?? null;
889
915
  if (t !== lastCrosshairTime.current) {
@@ -894,7 +920,7 @@ function VroomChart(props) {
894
920
  } else {
895
921
  next = handle.translate(e.changeX, e.changeY);
896
922
  }
897
- if (next) pictureSV.value = next;
923
+ if (next) applyFrame(next);
898
924
  maybeStartAnim();
899
925
  }).onEnd((e) => {
900
926
  if (!handle) return;
@@ -903,7 +929,7 @@ function VroomChart(props) {
903
929
  priceDrag.current = null;
904
930
  handle.setPriceLineDrag(-1, 0);
905
931
  const p = handle.render();
906
- if (p) pictureSV.value = p;
932
+ if (p) applyFrame(p);
907
933
  if (g) onPriceLineDragEnd?.(g.id, g.price);
908
934
  return;
909
935
  }
@@ -923,7 +949,7 @@ function VroomChart(props) {
923
949
  velocity *= Math.pow(0.5, dt / HALF_LIFE_S);
924
950
  const dx = velocity * dt;
925
951
  const next = handle.pan(dx, 0);
926
- if (next) pictureSV.value = next;
952
+ if (next) applyFrame(next);
927
953
  maybeStartAnim();
928
954
  if (Math.abs(velocity) > MIN_STOP) {
929
955
  decayRaf.current = requestAnimationFrame(tick);
@@ -977,7 +1003,7 @@ function VroomChart(props) {
977
1003
  }
978
1004
  if (frameX === 1 && frameY === 1) return;
979
1005
  const next = handle.zoom(frameX, frameY, focalX, focalY);
980
- if (next) pictureSV.value = next;
1006
+ if (next) applyFrame(next);
981
1007
  maybeStartAnim();
982
1008
  });
983
1009
  const longPress = import_react_native_gesture_handler.Gesture.LongPress().runOnJS(true).onStart((e) => {
@@ -987,7 +1013,7 @@ function VroomChart(props) {
987
1013
  cancelDecay();
988
1014
  crosshairActive.current = true;
989
1015
  const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
990
- if (ch) pictureSV.value = ch;
1016
+ if (ch) applyFrame(ch);
991
1017
  const info = handle.getCrosshairInfo();
992
1018
  lastCrosshairTime.current = info?.timeMs ?? null;
993
1019
  onCrosshair?.({
@@ -1010,7 +1036,7 @@ function VroomChart(props) {
1010
1036
  if (hitAxis(e.x, e.y) !== "chart") return;
1011
1037
  crosshairActive.current = false;
1012
1038
  const ch = handle.clearCrosshair();
1013
- if (ch) pictureSV.value = ch;
1039
+ if (ch) applyFrame(ch);
1014
1040
  lastCrosshairTime.current = null;
1015
1041
  onCrosshair?.({ active: false, candle: null, timeMs: null, price: null, reason: "hide" });
1016
1042
  });
@@ -1025,11 +1051,17 @@ function VroomChart(props) {
1025
1051
  style
1026
1052
  ]
1027
1053
  },
1028
- /* @__PURE__ */ import_react2.default.createElement(import_react_native_gesture_handler.GestureDetector, { gesture }, /* @__PURE__ */ import_react2.default.createElement(import_react_native2.View, { style: { flex: 1 } }, /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Canvas, { style: { flex: 1 } }, width > 0 && height > 0 ? (
1029
- // pictureSV is always a valid picture (seeded empty, never null),
1030
- // so RN-Skia's UI-thread reader never sees null.
1031
- /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Picture, { picture: pictureSV })
1032
- ) : null)))
1054
+ /* @__PURE__ */ import_react2.default.createElement(import_react_native_gesture_handler.GestureDetector, { gesture }, /* @__PURE__ */ import_react2.default.createElement(import_react_native2.View, { style: { flex: 1 } }, /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Canvas, { style: { flex: 1 } }, width > 0 && height > 0 ? /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(import_react_native_skia.Picture, { picture: pictureSV }), /* @__PURE__ */ import_react2.default.createElement(
1055
+ import_react_native_skia.Image,
1056
+ {
1057
+ image: imageSV,
1058
+ x: 0,
1059
+ y: 0,
1060
+ width,
1061
+ height,
1062
+ fit: "fill"
1063
+ }
1064
+ )) : null)))
1033
1065
  );
1034
1066
  }
1035
1067
  // Annotate the CommonJS export names for ESM import in node: