react-native-vroom-chart 0.16.0 → 0.17.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.
@@ -10,6 +10,7 @@ class SkCanvas;
10
10
  struct VroomChart;
11
11
 
12
12
  namespace vroom {
13
+ struct CandleSnapshot;
13
14
  struct Layout;
14
15
  struct PriceBounds;
15
16
  } // namespace vroom
@@ -19,11 +20,20 @@ namespace vroom::price_indicator {
19
20
  // Draws the line across [0, candle_right] at the latest close's y, plus the
20
21
  // price box in the y-axis strip. `candle_area_h` is the y of the x-axis
21
22
  // separator; the indicator is skipped if the close maps outside [0, candle_area_h].
23
+ //
24
+ // `morph_from` is the newest candle's outgoing capture (slot 0 of
25
+ // VroomChart::morph_from), or null to draw the settled close. When present the
26
+ // level eases across `morph_t` on the same clock as the candle it marks, so the
27
+ // badge tracks the bar instead of jumping ahead of it — see
28
+ // price_indicator_anim.h. Callers must only pass a capture that pairs with the
29
+ // newest candle (tip_anchor.h).
22
30
  void draw(SkCanvas* canvas,
23
31
  const VroomChart& chart,
24
32
  const Layout& lay,
25
33
  const PriceBounds& bounds,
26
34
  float candle_right,
27
- float candle_area_h);
35
+ float candle_area_h,
36
+ const CandleSnapshot* morph_from = nullptr,
37
+ float morph_t = 1.f);
28
38
 
29
39
  } // namespace vroom::price_indicator
@@ -0,0 +1,81 @@
1
+ // Where the current-price indicator sits mid-morph (see price_indicator.cpp).
2
+ //
3
+ // The indicator marks the latest close, so every tick moves it. Drawing it
4
+ // straight from that close makes it jump while the candle it belongs to eases
5
+ // into place, which reads as the badge coming loose from the chart. Instead it
6
+ // rides the same capture the candles do: morph_from[0] is the newest candle's
7
+ // outgoing geometry and interval_morph_t is the host's eased clock, so the
8
+ // indicator lands on the candle's close edge on every frame rather than only at
9
+ // the two ends.
10
+ //
11
+ // The y is deliberately the same expression close_vertex uses for slot 0
12
+ // (ma_overlay.cpp). Matching it by construction, rather than by giving the two
13
+ // the same duration, is what keeps the badge glued to the line chart's tip.
14
+ //
15
+ // Skia-free and header-only so the unit tests can cover it; see
16
+ // tests/test_price_indicator_anim.cpp.
17
+
18
+ #pragma once
19
+
20
+ #include <algorithm>
21
+
22
+ #include "viewport.h"
23
+
24
+ namespace vroom::price_indicator_anim {
25
+
26
+ // One frame of the indicator.
27
+ struct Level {
28
+ float y; // pixels, on the candle's close edge
29
+ double price; // what the badge should read
30
+ float bull_t; // 0 = the captured direction's color, 1 = the new one
31
+ };
32
+
33
+ namespace detail {
34
+ inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
35
+
36
+ // The price a captured band fraction stood for. The capture stores fractions so
37
+ // it survives a resize or a rescale, so recovering the price it came from needs
38
+ // the band it was measured against — which is why morph_from_bounds is kept.
39
+ inline double price_at_fraction(const PriceBounds& b, double frac) {
40
+ return b.min + frac * (b.max - b.min);
41
+ }
42
+ } // namespace detail
43
+
44
+ // `from` is the newest candle's capture, or null when nothing is morphing or
45
+ // the capture doesn't pair with the newest candle (panned into history — see
46
+ // tip_anchor.h, which the caller shares the pairing rule with). A null capture
47
+ // gives the settled values, which is what the indicator drew before it
48
+ // animated at all.
49
+ //
50
+ // `from_bounds` is the price band `from` was captured against
51
+ // (VroomChart::morph_from_bounds).
52
+ inline Level level_at(const Layout& lay,
53
+ const PriceBounds& bounds,
54
+ double close_new,
55
+ bool bull_new,
56
+ const CandleSnapshot* from,
57
+ const PriceBounds& from_bounds,
58
+ float morph_t) {
59
+ const float to_y = vroom::price_to_y(lay, bounds, close_new);
60
+ if (!from) return Level{to_y, close_new, 1.f};
61
+
62
+ const float t = std::clamp(morph_t, 0.f, 1.f);
63
+
64
+ // Two different spaces on purpose. The y interpolates in band fractions,
65
+ // because that is the space the capture is in and the space the candles
66
+ // move through — anything else would start the indicator off the pixel the
67
+ // close occupied last frame. The price interpolates in price space, so both
68
+ // ends read exactly the close they belong to even when this tick set a new
69
+ // extreme and rescaled the band underneath. The two agree whenever the band
70
+ // holds still, which is the overwhelmingly common case: y_at_fraction is
71
+ // affine, so a fraction lerp and a price lerp are then the same function.
72
+ const float y =
73
+ detail::lerp(vroom::y_at_fraction(lay, from->close), to_y, t);
74
+ const double close_old =
75
+ detail::price_at_fraction(from_bounds, from->close);
76
+
77
+ return Level{y, close_old + (close_new - close_old) * static_cast<double>(t),
78
+ from->bull == bull_new ? 1.f : t};
79
+ }
80
+
81
+ } // namespace vroom::price_indicator_anim
@@ -23,6 +23,11 @@ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
23
23
  0xff26a69a, // ACCENT_BULL — classic teal-green (price indicator, volume, MACD)
24
24
  0xffef5350, // ACCENT_BEAR — classic red
25
25
  0xff8957e5, // LINE — line-chart close polyline; violet, matching the RSI line
26
+ // SKELETON — transparent sentinel: inherit GRID, the way BORDER_BULL and
27
+ // the wick colors inherit their fills. The gridlines are already the
28
+ // chart's tone for structure rather than data, which is what the loading
29
+ // line is, so matching them keeps it from being read as a series.
30
+ 0x00000000,
26
31
  };
27
32
 
28
33
  constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
@@ -0,0 +1,55 @@
1
+ // How big the line chart's tip marker is, and how much gutter it needs.
2
+ //
3
+ // The dot marks the newest close, which on a view pinned to the latest bar sits
4
+ // half a slot from the right edge of the plot — a few pixels. The dot's own
5
+ // radius is larger than that, so it can only be drawn in full if it is allowed
6
+ // to spill into the gutter between the plot and the y-axis strip. That makes the
7
+ // marker's size a layout input, not just a paint detail, so the renderer
8
+ // (draw_close_tip in ma_overlay.cpp) and the layout (VroomChart::layout) read it
9
+ // from here rather than each carrying its own copy.
10
+ //
11
+ // The pulse ring is deliberately not part of this. At its widest it is 3.5x the
12
+ // border radius, and reserving that much gutter would eat the plot; it is
13
+ // allowed to clip against the axis.
14
+ //
15
+ // Skia-free and header-only so the unit tests can cover it; see
16
+ // tests/test_tip_geometry.cpp.
17
+
18
+ #pragma once
19
+
20
+ #include <algorithm>
21
+
22
+ namespace vroom::tip_geometry {
23
+
24
+ // Width of the background-colored ring that separates the tip dot from the line
25
+ // and from the pulse expanding out behind it.
26
+ constexpr float kBorderPx = 2.f;
27
+
28
+ // Gap left between the dot's outer edge and the y-axis strip. The dot touching
29
+ // the price labels reads as a rendering fault even when nothing is clipped.
30
+ constexpr float kClearPx = 4.f;
31
+
32
+ struct Geometry {
33
+ float dot_r; // the filled dot in the line's color
34
+ float border_r; // the dot plus its background-colored halo
35
+ };
36
+
37
+ // Scaling off the stroke keeps the marker proportionate at any line width; the
38
+ // floor stops a hairline chart from getting an invisible dot.
39
+ inline Geometry of(float line_width) {
40
+ const float w = line_width > 0.f ? line_width : 1.5f;
41
+ const float dot_r = std::max(2.f, w * 1.5f);
42
+ return Geometry{dot_r, dot_r + kBorderPx};
43
+ }
44
+
45
+ // Gutter width that lets the dot draw in full with `kClearPx` to spare.
46
+ //
47
+ // draw_close_tip drops the marker once its center passes the plot's right edge,
48
+ // so the center is at worst flush with that edge and the dot overhangs it by
49
+ // exactly `border_r`. A gutter of that plus the clearance therefore makes "the
50
+ // dot never touches the axis strip" true by construction, whatever the window.
51
+ inline float gutter_px(float line_width) {
52
+ return of(line_width).border_r + kClearPx;
53
+ }
54
+
55
+ } // namespace vroom::tip_geometry
@@ -92,6 +92,30 @@ struct CandleSnapshot {
92
92
  float x;
93
93
  float open, high, low, close;
94
94
  bool bull; // close >= open; selects the fill / wick / border color
95
+ // Set when the capture came from the loading skeleton. Such a slot starts
96
+ // out grey and semi-transparent rather than in its own bull/bear color, so
97
+ // the draw path has to blend from `skeleton_alpha`-scaled grey instead —
98
+ // that color blend is what makes the hand-off read as the placeholder
99
+ // *becoming* the data (see candles::draw).
100
+ bool skeleton = false;
101
+ // The wave's alpha for this bar at capture time. Carried so the morph's
102
+ // first frame matches the skeleton frame it replaced; without it every bar
103
+ // would jump to full opacity the instant data landed.
104
+ float skeleton_alpha = 1.f;
105
+ };
106
+
107
+ // One vertex of the loading line's morph, holding both ends of the animation
108
+ // so a frame is a plain lerp between them.
109
+ //
110
+ // The two y's are in different spaces on purpose. The sine knows nothing about
111
+ // prices, so it's a fraction of the pane; the candle centre is a fraction of
112
+ // the price band, the same space CandleSnapshot uses — which is what keeps the
113
+ // line sitting exactly where the bars emerge from, since both resolve through
114
+ // y_at_fraction.
115
+ struct LinePoint {
116
+ float x; // fraction of the candle-area width
117
+ float from_y; // fraction of the pane height — the sine, frozen at capture
118
+ float to_y; // fraction of the price band — the candle's vertical centre
95
119
  };
96
120
 
97
121
  // How many captured slots still contribute to a frame — 0 once the morph is
@@ -140,6 +164,15 @@ inline void blend_candle_snapshots(CandleSnapshot* dst, std::size_t dst_n,
140
164
  // `bull` stays the fresh one: the draw path colors a paired slot from
141
165
  // the live candle and only reads the capture's flag for a slot the next
142
166
  // update drops, where the newer direction is the better answer.
167
+ //
168
+ // The skeleton state, by contrast, carries over from the interrupted
169
+ // capture: a tick landing mid-hand-off must not abandon the grey blend
170
+ // partway, or the bar would snap to full color while its geometry is
171
+ // still moving. Its alpha follows the same lerp as the geometry.
172
+ if (from.skeleton) {
173
+ to.skeleton = true;
174
+ to.skeleton_alpha = mix(from.skeleton_alpha, 1.f);
175
+ }
143
176
  }
144
177
  }
145
178
 
package/lib/index.d.mts CHANGED
@@ -106,6 +106,19 @@ type VroomTheme = {
106
106
  crosshairTarget?: VroomColor;
107
107
  /** Line-chart-mode close polyline color. Defaults to violet, matching the RSI line. */
108
108
  lineColor?: VroomColor;
109
+ /**
110
+ * The line drawn across the plot while loading (see the `loading` prop).
111
+ * Defaults to inheriting `grid`: the gridlines are already the chart's tone
112
+ * for structure rather than data, which is what the line is, so matching
113
+ * them keeps it from being read as a series.
114
+ *
115
+ * The line breathes between roughly 60% and 100% of whatever color it ends
116
+ * up with — a pulse, not a dimmer, so a recessive color stays legible. Any
117
+ * alpha given here multiplies into that, so an opaque color is the usual
118
+ * choice. Pass `lineColor` to make the line read as the chart's own series
119
+ * warming up instead.
120
+ */
121
+ skeleton?: VroomColor;
109
122
  /** Line-chart-mode polyline stroke width in px. Defaults to 1.5. */
110
123
  lineWidth?: number;
111
124
  /**
@@ -1059,6 +1072,23 @@ type ATRConfig = {
1059
1072
  type VroomChartCoreProps = {
1060
1073
  /** OHLCV bars to render. The only required prop. */
1061
1074
  candles: Candle[];
1075
+ /**
1076
+ * Whether the series is still being fetched. While this is true *and*
1077
+ * `candles` is empty, the chart draws a single grey line across the plot,
1078
+ * drifting in a slow sine wave, in place of the scene. Gestures, the
1079
+ * crosshair, the price badge, the axis labels and any indicator panes are all
1080
+ * suppressed for the duration.
1081
+ *
1082
+ * When the data arrives the line doesn't cut away — it becomes the chart, in
1083
+ * two steps that split `transitionMs`: it reshapes to pass through the
1084
+ * vertical centre of every candle about to be drawn, then fades out while
1085
+ * those candles grow outward from it and their color fades up.
1086
+ *
1087
+ * Passing `candles` alongside `loading` leaves the real chart up, so a
1088
+ * background refresh of an already-loaded series won't blank out. Default
1089
+ * false.
1090
+ */
1091
+ loading?: boolean;
1062
1092
  /**
1063
1093
  * Identity of the data series (e.g. "BTC-USD"). When it changes between
1064
1094
  * renders the chart resets to the default view (most recent candles + price
package/lib/index.d.ts CHANGED
@@ -106,6 +106,19 @@ type VroomTheme = {
106
106
  crosshairTarget?: VroomColor;
107
107
  /** Line-chart-mode close polyline color. Defaults to violet, matching the RSI line. */
108
108
  lineColor?: VroomColor;
109
+ /**
110
+ * The line drawn across the plot while loading (see the `loading` prop).
111
+ * Defaults to inheriting `grid`: the gridlines are already the chart's tone
112
+ * for structure rather than data, which is what the line is, so matching
113
+ * them keeps it from being read as a series.
114
+ *
115
+ * The line breathes between roughly 60% and 100% of whatever color it ends
116
+ * up with — a pulse, not a dimmer, so a recessive color stays legible. Any
117
+ * alpha given here multiplies into that, so an opaque color is the usual
118
+ * choice. Pass `lineColor` to make the line read as the chart's own series
119
+ * warming up instead.
120
+ */
121
+ skeleton?: VroomColor;
109
122
  /** Line-chart-mode polyline stroke width in px. Defaults to 1.5. */
110
123
  lineWidth?: number;
111
124
  /**
@@ -1059,6 +1072,23 @@ type ATRConfig = {
1059
1072
  type VroomChartCoreProps = {
1060
1073
  /** OHLCV bars to render. The only required prop. */
1061
1074
  candles: Candle[];
1075
+ /**
1076
+ * Whether the series is still being fetched. While this is true *and*
1077
+ * `candles` is empty, the chart draws a single grey line across the plot,
1078
+ * drifting in a slow sine wave, in place of the scene. Gestures, the
1079
+ * crosshair, the price badge, the axis labels and any indicator panes are all
1080
+ * suppressed for the duration.
1081
+ *
1082
+ * When the data arrives the line doesn't cut away — it becomes the chart, in
1083
+ * two steps that split `transitionMs`: it reshapes to pass through the
1084
+ * vertical centre of every candle about to be drawn, then fades out while
1085
+ * those candles grow outward from it and their color fades up.
1086
+ *
1087
+ * Passing `candles` alongside `loading` leaves the real chart up, so a
1088
+ * background refresh of an already-loaded series won't blank out. Default
1089
+ * false.
1090
+ */
1091
+ loading?: boolean;
1062
1092
  /**
1063
1093
  * Identity of the data series (e.g. "BTC-USD"). When it changes between
1064
1094
  * renders the chart resets to the default view (most recent candles + price
package/lib/index.js CHANGED
@@ -186,8 +186,10 @@ var COLOR_KEYS = {
186
186
  // VROOM_COLOR_ACCENT_BULL
187
187
  accentBear: 15,
188
188
  // VROOM_COLOR_ACCENT_BEAR
189
- lineColor: 16
189
+ lineColor: 16,
190
190
  // VROOM_COLOR_LINE
191
+ skeleton: 17
192
+ // VROOM_COLOR_SKELETON
191
193
  };
192
194
  var FLOAT_KEYS = {
193
195
  wickWidth: 1,
@@ -487,7 +489,7 @@ function ensureInstalled() {
487
489
  }
488
490
  installed = true;
489
491
  }
490
- function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition) {
492
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition, loading) {
491
493
  const handleRef = (0, import_react.useRef)(null);
492
494
  const defaultWidthAppliedRef = (0, import_react.useRef)(false);
493
495
  const volumeCollapseRef = (0, import_react.useRef)(null);
@@ -522,18 +524,26 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
522
524
  const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
523
525
  onFrameRef.current = transition?.onFrame;
524
526
  const seriesKey = transition?.seriesKey;
527
+ const loadingHandoffRef = (0, import_react.useRef)(false);
525
528
  const endIntervalMorph = (0, import_react.useCallback)(() => {
526
529
  if (intervalMorphRaf.current != null) {
527
530
  cancelAnimationFrame(intervalMorphRaf.current);
528
531
  intervalMorphRaf.current = null;
529
532
  }
530
- handleRef.current?.setIntervalMorph(1);
533
+ const h = handleRef.current;
534
+ if (loadingHandoffRef.current) {
535
+ loadingHandoffRef.current = false;
536
+ h?.setLoadingMorph(1);
537
+ h?.beginLoadingReveal();
538
+ }
539
+ h?.setIntervalMorph(1);
531
540
  }, []);
532
- const startIntervalMorph = (0, import_react.useCallback)((h) => {
541
+ const startIntervalMorph = (0, import_react.useCallback)((h, durationMs) => {
533
542
  const { ms, easing } = animRef.current;
543
+ const dur = durationMs ?? ms;
534
544
  const start = performance.now();
535
545
  const step = (now) => {
536
- const p = Math.min(1, (now - start) / ms);
546
+ const p = Math.min(1, (now - start) / dur);
537
547
  h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
538
548
  const pic = h.render();
539
549
  if (pic) onFrameRef.current?.(pic);
@@ -541,6 +551,31 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
541
551
  };
542
552
  intervalMorphRaf.current = requestAnimationFrame(step);
543
553
  }, []);
554
+ const startLoadingHandoff = (0, import_react.useCallback)(
555
+ (h) => {
556
+ const { ms, easing } = animRef.current;
557
+ const half = ms / 2;
558
+ loadingHandoffRef.current = true;
559
+ h.beginLoadingMorph();
560
+ const start = performance.now();
561
+ const step = (now) => {
562
+ const p = Math.min(1, (now - start) / half);
563
+ h.setLoadingMorph(p < 1 ? ease(easing, p) : 1);
564
+ const pic = h.render();
565
+ if (pic) onFrameRef.current?.(pic);
566
+ if (p < 1) {
567
+ intervalMorphRaf.current = requestAnimationFrame(step);
568
+ return;
569
+ }
570
+ intervalMorphRaf.current = null;
571
+ loadingHandoffRef.current = false;
572
+ h.beginLoadingReveal();
573
+ startIntervalMorph(h, half);
574
+ };
575
+ intervalMorphRaf.current = requestAnimationFrame(step);
576
+ },
577
+ [startIntervalMorph]
578
+ );
544
579
  const settleStream = (0, import_react.useCallback)((keepMorph = false) => {
545
580
  if (streamRaf.current != null) {
546
581
  cancelAnimationFrame(streamRaf.current);
@@ -610,6 +645,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
610
645
  const explicit = visibleRange != null;
611
646
  const startMs = visibleRange?.startMs ?? 0;
612
647
  const endMs = visibleRange?.endMs ?? 0;
648
+ const showLoadingLine = loading === true && candles.length === 0;
649
+ const lineUpRef = (0, import_react.useRef)(false);
613
650
  const themeKey = theme ? JSON.stringify(theme) : "";
614
651
  const rsiKey = rsi ? JSON.stringify(rsi) : "";
615
652
  const macdKey = macd ? JSON.stringify(macd) : "";
@@ -632,6 +669,19 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
632
669
  defaultWidthAppliedRef.current = true;
633
670
  }
634
671
  let morphing = false;
672
+ if (showLoadingLine) {
673
+ if (!lineUpRef.current) {
674
+ endIntervalMorph();
675
+ settleStream();
676
+ h.setCandles(packCandles([]));
677
+ prevDataRef.current = null;
678
+ }
679
+ h.setLoading(true, !animRef.current.reduceMotion);
680
+ lineUpRef.current = true;
681
+ } else if (lineUpRef.current && candles.length === 0) {
682
+ h.setLoading(false, true);
683
+ lineUpRef.current = false;
684
+ }
635
685
  if (candles.length > 0) {
636
686
  const prev = prevDataRef.current;
637
687
  const freshHandle = prev == null || prev.handle !== h;
@@ -639,6 +689,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
639
689
  const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
640
690
  let tfArgs = null;
641
691
  let prevEnvelope = null;
692
+ let handOff = false;
642
693
  let stream = null;
643
694
  if (transitionKind === "stream" && prev != null && !explicit) {
644
695
  const { stream: mode, streamMs, reduceMotion } = animRef.current;
@@ -688,6 +739,11 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
688
739
  } else if (transitionKind === "initial" || transitionKind === "reset") {
689
740
  endIntervalMorph();
690
741
  }
742
+ if (lineUpRef.current) {
743
+ lineUpRef.current = false;
744
+ handOff = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
745
+ if (!handOff) h.setLoading(false, true);
746
+ }
691
747
  h.setCandles(packCandles(candles));
692
748
  if (transitionKind === "timeframe") {
693
749
  const newStepMs = inferStepMs(candles);
@@ -709,6 +765,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
709
765
  } else if (transitionKind === "reset") {
710
766
  h.resetView();
711
767
  }
768
+ if (handOff) startLoadingHandoff(h);
712
769
  prevDataRef.current = { handle: h, candles, seriesKey };
713
770
  }
714
771
  }
@@ -738,7 +795,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
738
795
  footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
739
796
  );
740
797
  if (!morphing) setPicture(h.render());
741
- }, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph, startStreamAnim, settleStream]);
798
+ }, [candles, showLoadingLine, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, startLoadingHandoff, endIntervalMorph, startStreamAnim, settleStream]);
742
799
  return { handle: handleRef.current, picture, volumeCollapseRef };
743
800
  }
744
801
 
@@ -750,6 +807,7 @@ function isSkImage(frame) {
750
807
  function VroomChart(props) {
751
808
  const {
752
809
  candles,
810
+ loading,
753
811
  seriesKey,
754
812
  width: widthProp,
755
813
  height: heightProp,
@@ -867,8 +925,10 @@ function VroomChart(props) {
867
925
  streamTransitionMs,
868
926
  reduceMotion,
869
927
  onFrame
870
- }
928
+ },
929
+ loading
871
930
  );
931
+ const showLoadingLine = loading === true && candles.length === 0;
872
932
  const crosshairActive = (0, import_react2.useRef)(false);
873
933
  const footprintActive = (0, import_react2.useRef)(false);
874
934
  const lastCrosshairTime = (0, import_react2.useRef)(null);
@@ -1148,7 +1208,7 @@ function VroomChart(props) {
1148
1208
  pane: null
1149
1209
  });
1150
1210
  };
1151
- const pan = import_react_native_gesture_handler.Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
1211
+ const pan = import_react_native_gesture_handler.Gesture.Pan().enabled(!showLoadingLine).runOnJS(true).maxPointers(1).onStart((e) => {
1152
1212
  cancelDecay();
1153
1213
  panMode.current = hitAxis(e.x, e.y);
1154
1214
  priceDrag.current = null;
@@ -1243,7 +1303,7 @@ function VroomChart(props) {
1243
1303
  enableX: false,
1244
1304
  enableY: false
1245
1305
  });
1246
- const pinch = import_react_native_gesture_handler.Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
1306
+ const pinch = import_react_native_gesture_handler.Gesture.Pinch().enabled(!showLoadingLine).runOnJS(true).onTouchesDown((e) => {
1247
1307
  if (e.numberOfTouches < 2) return;
1248
1308
  dismissFootprint();
1249
1309
  const [a, b] = e.allTouches;
@@ -1281,7 +1341,7 @@ function VroomChart(props) {
1281
1341
  if (next) applyFrame(next);
1282
1342
  maybeStartAnim();
1283
1343
  });
1284
- const longPress = import_react_native_gesture_handler.Gesture.LongPress().runOnJS(true).onStart((e) => {
1344
+ const longPress = import_react_native_gesture_handler.Gesture.LongPress().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
1285
1345
  if (!handle) return;
1286
1346
  if (hitAxis(e.x, e.y) !== "chart") return;
1287
1347
  if (hitPriceLine(e.x, e.y)) return;
@@ -1301,7 +1361,7 @@ function VroomChart(props) {
1301
1361
  reason: "show"
1302
1362
  });
1303
1363
  });
1304
- const tap = import_react_native_gesture_handler.Gesture.Tap().runOnJS(true).onStart((e) => {
1364
+ const tap = import_react_native_gesture_handler.Gesture.Tap().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
1305
1365
  if (!handle) return;
1306
1366
  const prints = footprints ?? [];
1307
1367
  const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;