react-native-vroom-chart 0.2.0 → 0.5.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.
@@ -15,7 +15,7 @@ Pod::Spec.new do |s|
15
15
  s.name = "react-native-vroom-chart"
16
16
  s.version = package["version"]
17
17
  s.summary = package["description"]
18
- s.homepage = "https://github.com/darionwelch/vroom"
18
+ s.homepage = "https://github.com/keepitreal/vroom"
19
19
  s.license = { :type => "MIT" }
20
20
  s.authors = { "vroom" => "noreply@example.com" }
21
21
  s.platforms = { :ios => "14.0" }
@@ -41,11 +41,14 @@ export function VroomChart(props: VroomChartProps) {
41
41
  style,
42
42
  visibleRange,
43
43
  defaultCandleWidth,
44
+ chartType,
45
+ transitionMs,
44
46
  theme,
45
47
  rsi,
46
48
  macd,
47
49
  movingAverages,
48
50
  vwap,
51
+ bollingerBands,
49
52
  crosshairOffset = 40,
50
53
  onCrosshair,
51
54
  onViewportChange,
@@ -71,11 +74,13 @@ export function VroomChart(props: VroomChartProps) {
71
74
  { width, height },
72
75
  visibleRange,
73
76
  defaultCandleWidth,
77
+ chartType,
74
78
  theme,
75
79
  rsi,
76
80
  macd,
77
81
  movingAverages,
78
82
  vwap,
83
+ bollingerBands,
79
84
  );
80
85
 
81
86
  // RN-Skia's recorder reads this SharedValue on the UI/render runtime, a beat
@@ -147,6 +152,72 @@ export function VroomChart(props: VroomChartProps) {
147
152
  };
148
153
  }, []);
149
154
 
155
+ // Candle↔line morph. When `chartType` changes we drive the core per-frame with
156
+ // a (collapse, fade) blend and push a fresh picture into the SV each frame — the
157
+ // JS side owns the eased clock (see plan). A fresh handle snaps to the target;
158
+ // transitionMs=0 snaps. Mirrors the web driver in react/src/useChartCore.ts.
159
+ const morphRaf = useRef<number | null>(null);
160
+ const morphFade = useRef<number | null>(null);
161
+ const morphHandle = useRef<typeof handle>(null);
162
+ useEffect(() => {
163
+ if (!handle) return undefined;
164
+ const target = chartType === 'line' ? 1 : 0;
165
+
166
+ // Fresh handle (first load / recreate): snap, don't animate.
167
+ if (morphHandle.current !== handle || morphFade.current == null) {
168
+ morphHandle.current = handle;
169
+ morphFade.current = target;
170
+ handle.setChartType(target);
171
+ const p = handle.render();
172
+ if (p) pictureSV.value = p;
173
+ return undefined;
174
+ }
175
+ if (morphFade.current === target) return undefined;
176
+
177
+ if (morphRaf.current != null) {
178
+ cancelAnimationFrame(morphRaf.current);
179
+ morphRaf.current = null;
180
+ }
181
+ const dur = Math.max(0, transitionMs ?? 300);
182
+ if (dur === 0) {
183
+ morphFade.current = target;
184
+ handle.setChartType(target);
185
+ const p = handle.render();
186
+ if (p) pictureSV.value = p;
187
+ return undefined;
188
+ }
189
+
190
+ const from = morphFade.current;
191
+ let startTs: number | null = null;
192
+ const step = (now: number) => {
193
+ if (startTs == null) startTs = now;
194
+ const prog = Math.min(1, (now - startTs) / dur);
195
+ const e = prog * prog * (3 - 2 * prog); // smoothstep ease-in-out
196
+ const fade = from + (target - from) * e;
197
+ morphFade.current = fade;
198
+ handle.setMorph(fade, fade);
199
+ const p = handle.render();
200
+ if (p) pictureSV.value = p;
201
+ if (prog < 1) {
202
+ morphRaf.current = requestAnimationFrame(step);
203
+ } else {
204
+ morphRaf.current = null;
205
+ morphFade.current = target;
206
+ handle.setChartType(target); // lock the exact endpoint
207
+ const q = handle.render();
208
+ if (q) pictureSV.value = q;
209
+ }
210
+ };
211
+ morphRaf.current = requestAnimationFrame(step);
212
+
213
+ return () => {
214
+ if (morphRaf.current != null) {
215
+ cancelAnimationFrame(morphRaf.current);
216
+ morphRaf.current = null;
217
+ }
218
+ };
219
+ }, [handle, chartType, transitionMs, pictureSV]);
220
+
150
221
  // Classifies a touch point into the candle area vs. an axis strip. Axis
151
222
  // strips always own their gesture (scale price/time) and take priority over
152
223
  // the crosshair: an axis touch never opens, moves, or dismisses it.
package/src/index.ts CHANGED
@@ -11,4 +11,6 @@ export type {
11
11
  MASource,
12
12
  MovingAverageOverlay,
13
13
  VWAPConfig,
14
+ BollingerBandsConfig,
15
+ ChartType,
14
16
  } from './types';
package/src/jsi.d.ts CHANGED
@@ -23,6 +23,10 @@ export interface ChartHandle {
23
23
  * Affects initial framing only; an explicit setVisibleRange takes precedence.
24
24
  */
25
25
  setDefaultCandleWidth(px: number): void;
26
+ /** Render mode: 0 = candlesticks (default), 1 = line chart (close polyline). */
27
+ setChartType(mode: number): void;
28
+ /** Candle↔line morph blend: collapse folds candles to close, fade crossfades. */
29
+ setMorph(collapse: number, fade: number): void;
26
30
  /** Shifts the visible range by `dx`/`dy` pixels and returns a fresh picture. */
27
31
  pan(dx: number, dy: number): SkPicture | null;
28
32
  /**
@@ -138,6 +142,26 @@ export interface ChartHandle {
138
142
  color: number,
139
143
  width: number,
140
144
  ): void;
145
+ /**
146
+ * Configures the Bollinger Bands overlay (three price-pane lines + optional
147
+ * fill between the bands). source/basisKind mirror setOverlays' encodings;
148
+ * colors are packed 0xAARRGGBB; fillOpacity is 0..1.
149
+ */
150
+ setBollinger(spec: {
151
+ enabled: boolean;
152
+ period: number;
153
+ mult: number;
154
+ source: number;
155
+ basisKind: number;
156
+ upperColor: number;
157
+ upperWidth: number;
158
+ middleColor: number;
159
+ middleWidth: number;
160
+ lowerColor: number;
161
+ lowerWidth: number;
162
+ fillEnabled: boolean;
163
+ fillOpacity: number;
164
+ }): void;
141
165
  /** True while any axis-label fade is still in progress. Drives a RAF loop. */
142
166
  isAnimating(): boolean;
143
167
  render(): SkPicture | null;
package/src/theme.ts CHANGED
@@ -18,6 +18,7 @@ export const COLOR_KEYS: Partial<Record<keyof VroomTheme, number>> = {
18
18
  wickBear: 13, // VROOM_COLOR_WICK_BEAR
19
19
  accentBull: 14, // VROOM_COLOR_ACCENT_BULL
20
20
  accentBear: 15, // VROOM_COLOR_ACCENT_BEAR
21
+ lineColor: 16, // VROOM_COLOR_LINE
21
22
  };
22
23
 
23
24
  // Maps each numeric VroomTheme field to its VroomFloatKey index.
@@ -25,6 +26,7 @@ export const FLOAT_KEYS: Partial<Record<keyof VroomTheme, number>> = {
25
26
  wickWidth: 1, // VROOM_FLOAT_WICK_WIDTH_PX
26
27
  candleRadius: 8, // VROOM_FLOAT_CANDLE_RADIUS_PX
27
28
  volumeRadius: 10, // VROOM_FLOAT_VOLUME_RADIUS_PX
29
+ lineWidth: 11, // VROOM_FLOAT_LINE_WIDTH_PX
28
30
  };
29
31
 
30
32
  // Maps each boolean VroomTheme field to its VroomFloatKey index (pushed as 0/1).
package/src/types.ts CHANGED
@@ -14,7 +14,9 @@ export type {
14
14
  MASource,
15
15
  MovingAverageOverlay,
16
16
  VWAPConfig,
17
+ BollingerBandsConfig,
17
18
  MACDConfig,
19
+ ChartType,
18
20
  } from '@vroomchart/types';
19
21
 
20
22
  /**
@@ -6,7 +6,9 @@ import type { ChartHandle } from './jsi.d';
6
6
  import { packCandles } from './packCandles';
7
7
  import { applyTheme, parseColor } from './theme';
8
8
  import type {
9
+ BollingerBandsConfig,
9
10
  Candle,
11
+ ChartType,
10
12
  MACDConfig,
11
13
  MovingAverageOverlay,
12
14
  RSIConfig,
@@ -37,6 +39,35 @@ function overlayToNumeric(o: MovingAverageOverlay) {
37
39
  };
38
40
  }
39
41
 
42
+ // Bollinger defaults: blue bands / orange basis, matching the repo palette.
43
+ const DEFAULT_BB_BAND_COLOR = 0xff2962ff;
44
+ const DEFAULT_BB_BASIS_COLOR = 0xffff6d00;
45
+
46
+ function bollingerToSpec(cfg: BollingerBandsConfig | undefined) {
47
+ const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
48
+ return {
49
+ enabled: cfg?.enabled ?? false,
50
+ period: cfg?.period ?? 20,
51
+ mult: cfg?.stdDev ?? 2,
52
+ source: srcIdx < 0 ? 0 : srcIdx,
53
+ basisKind: cfg?.basis === 'ema' ? 1 : 0,
54
+ upperColor:
55
+ (cfg?.upperColor != null ? parseColor(cfg.upperColor) : null) ??
56
+ DEFAULT_BB_BAND_COLOR,
57
+ upperWidth: cfg?.upperWidth ?? 1,
58
+ middleColor:
59
+ (cfg?.middleColor != null ? parseColor(cfg.middleColor) : null) ??
60
+ DEFAULT_BB_BASIS_COLOR,
61
+ middleWidth: cfg?.middleWidth ?? 1,
62
+ lowerColor:
63
+ (cfg?.lowerColor != null ? parseColor(cfg.lowerColor) : null) ??
64
+ DEFAULT_BB_BAND_COLOR,
65
+ lowerWidth: cfg?.lowerWidth ?? 1,
66
+ fillEnabled: cfg?.fill ?? true,
67
+ fillOpacity: cfg?.fillOpacity ?? 0.1,
68
+ };
69
+ }
70
+
40
71
  let installed = false;
41
72
  function ensureInstalled(): void {
42
73
  if (installed) return;
@@ -63,11 +94,13 @@ export function useChartCore(
63
94
  size: { width: number; height: number; pxRatio?: number },
64
95
  visibleRange?: VisibleRange,
65
96
  defaultCandleWidth?: number,
97
+ chartType?: ChartType,
66
98
  theme?: VroomTheme,
67
99
  rsi?: RSIConfig,
68
100
  macd?: MACDConfig,
69
101
  movingAverages?: MovingAverageOverlay[],
70
102
  vwap?: VWAPConfig,
103
+ bollingerBands?: BollingerBandsConfig,
71
104
  ): ChartCoreState {
72
105
  const handleRef = useRef<ChartHandle | null>(null);
73
106
  // Push setDefaultCandleWidth only once (first load): setCandles re-runs on
@@ -96,6 +129,7 @@ export function useChartCore(
96
129
  const macdKey = macd ? JSON.stringify(macd) : '';
97
130
  const maKey = movingAverages ? JSON.stringify(movingAverages) : '';
98
131
  const vwapKey = vwap ? JSON.stringify(vwap) : '';
132
+ const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : '';
99
133
 
100
134
  useEffect(() => {
101
135
  const h = handleRef.current;
@@ -119,6 +153,8 @@ export function useChartCore(
119
153
  if (explicit) {
120
154
  h.setVisibleRange(startMs, endMs);
121
155
  }
156
+ // chartType / the candle↔line morph is driven separately (VroomChart owns the
157
+ // per-frame animation loop so it can update the picture SharedValue directly).
122
158
  if (theme) {
123
159
  applyTheme(h, theme);
124
160
  }
@@ -143,12 +179,14 @@ export function useChartCore(
143
179
  (vwap?.color != null ? parseColor(vwap.color) : null) ?? 0xff00bcd4,
144
180
  vwap?.width ?? 1.5,
145
181
  );
182
+ h.setBollinger(bollingerToSpec(bollingerBands));
146
183
  // TODO(rn-parity): mirror the web `liquidity` overlay here (setLiquidity +
147
184
  // the VroomBand structs in the JSI handle) — web-only for now.
148
185
  setPicture(h.render());
149
- // theme/rsi/macd/movingAverages/vwap are represented by their *Key deps.
186
+ // theme/rsi/macd/movingAverages/vwap/bollingerBands are represented by
187
+ // their *Key deps.
150
188
  // eslint-disable-next-line react-hooks/exhaustive-deps
151
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey]);
189
+ }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey]);
152
190
 
153
191
  return { handle: handleRef.current, picture };
154
192
  }