react-native-vroom-chart 0.1.3 → 0.2.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.mts CHANGED
@@ -32,11 +32,19 @@ type CrosshairEvent = {
32
32
  * future candle-aligned slots past the last candle. Null when inactive.
33
33
  */
34
34
  timeMs: number | null;
35
+ /**
36
+ * Free price at the crosshair's horizontal line (what the price badge shows),
37
+ * in data space. Null when inactive. Pair with `timeMs` to mirror this
38
+ * crosshair onto another chart via its `crosshairOverride` prop.
39
+ */
40
+ price: number | null;
35
41
  /**
36
42
  * Why this event fired — lets the host react differently (e.g. haptics):
37
43
  * 'show' — long-press activated the crosshair
38
- * 'move' — the crosshair snapped to a *different* candle (one per candle
39
- * crossed; not per drag frame)
44
+ * 'move' — the crosshair moved: a different candle (time) or a vertical move
45
+ * within the same candle (price). Fires on any positional change so
46
+ * `price` stays current for cross-chart sync; for a per-candle
47
+ * signal (e.g. haptics), dedupe on `timeMs` yourself.
40
48
  * 'hide' — the crosshair was dismissed
41
49
  * The library never plays haptics itself; the host decides.
42
50
  */
@@ -60,14 +68,22 @@ type VroomTheme = {
60
68
  accentBull?: VroomColor;
61
69
  /** Generic down color for the price indicator, volume bars, and MACD histogram. Defaults to red; independent of `bear`. */
62
70
  accentBear?: VroomColor;
63
- /** Up candle body 1px border. Defaults to the bull fill color. */
71
+ /** Up candle body border (1px, drawn *inside* the body so it never changes candle width). Omit or set to the bull fill color to hide it. */
64
72
  borderBull?: VroomColor;
65
- /** Down candle body 1px border. Defaults to the bear fill color. */
73
+ /** Down candle body border (1px, drawn *inside* the body so it never changes candle width). Omit or set to the bear fill color to hide it. */
66
74
  borderBear?: VroomColor;
67
75
  /** Up candle wick color. Defaults to the bull fill color. */
68
76
  wickBull?: VroomColor;
69
77
  /** Down candle wick color. Defaults to the bear fill color. */
70
78
  wickBear?: VroomColor;
79
+ /** Wick stroke width in px (applies to both up and down wicks). Defaults to 1. */
80
+ wickWidth?: number;
81
+ /** Corner radius (px) of candle bodies. Defaults to 0 (square). */
82
+ candleRadius?: number;
83
+ /** Round the wick end caps. Defaults to false. */
84
+ wickRoundCap?: boolean;
85
+ /** Corner radius (px) of the *top* of volume bars. Defaults to 0 (square). */
86
+ volumeRadius?: number;
71
87
  /** Gridlines. */
72
88
  grid?: VroomColor;
73
89
  /** Axis label text (price + time). */
@@ -161,6 +177,51 @@ type VWAPConfig = {
161
177
  /** Stroke width in px. Default 1.5. */
162
178
  width?: number;
163
179
  };
180
+ /**
181
+ * A single resting-liquidity band: a price interval carrying a total order size
182
+ * on one side of the book. Consolidate raw L2 levels into these buckets before
183
+ * passing them. The vertical extent is defined in price space, so bands scale
184
+ * with the y-axis on zoom.
185
+ */
186
+ type LiquidityBand = {
187
+ /** Bottom of the price interval. */
188
+ minPrice: number;
189
+ /** Top of the price interval. A per-level order uses a thin interval. */
190
+ maxPrice: number;
191
+ /** Which side of the book — selects the buy vs sell color. */
192
+ side: 'buy' | 'sell';
193
+ /** Total resting size in this band; drives the band's opacity. */
194
+ volume: number;
195
+ };
196
+ /**
197
+ * Resting-order / order-book liquidity overlay. Each band is drawn as a
198
+ * horizontal rectangle anchored at the inner edge of the price axis, stretching
199
+ * left (~`widthPx`/`widthFrac` of the pane) and fading out with a gradient,
200
+ * colored by side and made more opaque with volume. Rendered behind the candles.
201
+ * Omit the prop (or pass no bands) to hide the overlay.
202
+ */
203
+ type LiquidityConfig = {
204
+ /** The bands to render. */
205
+ bands: LiquidityBand[];
206
+ /** Buy-side color (hex string or packed ARGB number). Default teal-green. */
207
+ buyColor?: VroomColor;
208
+ /** Sell-side color (hex string or packed ARGB number). Default red. */
209
+ sellColor?: VroomColor;
210
+ /**
211
+ * Volume mapped to the peak (`maxOpacity`) band. Omit to auto-scale to the
212
+ * largest band's volume each render.
213
+ */
214
+ maxVolume?: number;
215
+ /** Opacity floor for the smallest band. Default 0.05. */
216
+ minOpacity?: number;
217
+ /** Opacity at `maxVolume`. Default 0.8. */
218
+ maxOpacity?: number;
219
+ /** Leftward reach from the axis in px. Default 300. */
220
+ widthPx?: number;
221
+ /** Leftward reach as a fraction of pane width. Default 0.25. The smaller of
222
+ * `widthPx` and `widthFrac * paneWidth` is used. */
223
+ widthFrac?: number;
224
+ };
164
225
  /** MACD indicator config. Rendered in its own pane below the candles. */
165
226
  type MACDConfig = {
166
227
  enabled?: boolean;
@@ -196,6 +257,14 @@ type VroomChartCoreProps = {
196
257
  height?: number;
197
258
  /** Time window to render. Omit (or both 0) to show every candle. */
198
259
  visibleRange?: VisibleRange;
260
+ /**
261
+ * Target candle *body* width in logical px for the INITIAL framing. Larger →
262
+ * more zoomed in (fewer candles); smaller → more zoomed out. Only affects
263
+ * first load / reset; an explicit `visibleRange` takes precedence. Omit for
264
+ * the default (~80 candles). Useful to keep candles a consistent size across
265
+ * devices of different widths.
266
+ */
267
+ defaultCandleWidth?: number;
199
268
  theme?: VroomTheme;
200
269
  /** RSI indicator (pane below the candles). Omit/disable to hide it. */
201
270
  rsi?: RSIConfig;
@@ -205,12 +274,27 @@ type VroomChartCoreProps = {
205
274
  movingAverages?: MovingAverageOverlay[];
206
275
  /** VWAP overlay (session anchor, configurable reset). */
207
276
  vwap?: VWAPConfig;
277
+ /** Resting-order / order-book liquidity bands drawn behind the candles. */
278
+ liquidity?: LiquidityConfig;
208
279
  /**
209
280
  * Pixels the crosshair dot / horizontal line sit *above* the touch point so
210
281
  * they aren't hidden under the thumb. The vertical line stays centered on the
211
282
  * touch x. Default 40.
212
283
  */
213
284
  crosshairOffset?: number;
285
+ /**
286
+ * Controlled external crosshair, in data space (epoch ms + price). When
287
+ * non-null the chart shows a crosshair there — typically synced from another
288
+ * chart's `onCrosshair` — *without* emitting its own `onCrosshair`, so two
289
+ * charts can drive each other without a feedback loop. A local hover/press on
290
+ * this chart takes precedence; on release the override is restored. Set null
291
+ * to clear. Best when the synced charts share a comparable price scale (the
292
+ * horizontal line clamps to the pane edge when the price is off-scale).
293
+ */
294
+ crosshairOverride?: {
295
+ timeMs: number;
296
+ price: number;
297
+ } | null;
214
298
  /**
215
299
  * Interaction mode. Default `'pan'`. Set to `'draw'` (with a `tool`) to let
216
300
  * the user place drawing points; panning/zooming are suppressed while drawing.
package/lib/index.d.ts CHANGED
@@ -32,11 +32,19 @@ type CrosshairEvent = {
32
32
  * future candle-aligned slots past the last candle. Null when inactive.
33
33
  */
34
34
  timeMs: number | null;
35
+ /**
36
+ * Free price at the crosshair's horizontal line (what the price badge shows),
37
+ * in data space. Null when inactive. Pair with `timeMs` to mirror this
38
+ * crosshair onto another chart via its `crosshairOverride` prop.
39
+ */
40
+ price: number | null;
35
41
  /**
36
42
  * Why this event fired — lets the host react differently (e.g. haptics):
37
43
  * 'show' — long-press activated the crosshair
38
- * 'move' — the crosshair snapped to a *different* candle (one per candle
39
- * crossed; not per drag frame)
44
+ * 'move' — the crosshair moved: a different candle (time) or a vertical move
45
+ * within the same candle (price). Fires on any positional change so
46
+ * `price` stays current for cross-chart sync; for a per-candle
47
+ * signal (e.g. haptics), dedupe on `timeMs` yourself.
40
48
  * 'hide' — the crosshair was dismissed
41
49
  * The library never plays haptics itself; the host decides.
42
50
  */
@@ -60,14 +68,22 @@ type VroomTheme = {
60
68
  accentBull?: VroomColor;
61
69
  /** Generic down color for the price indicator, volume bars, and MACD histogram. Defaults to red; independent of `bear`. */
62
70
  accentBear?: VroomColor;
63
- /** Up candle body 1px border. Defaults to the bull fill color. */
71
+ /** Up candle body border (1px, drawn *inside* the body so it never changes candle width). Omit or set to the bull fill color to hide it. */
64
72
  borderBull?: VroomColor;
65
- /** Down candle body 1px border. Defaults to the bear fill color. */
73
+ /** Down candle body border (1px, drawn *inside* the body so it never changes candle width). Omit or set to the bear fill color to hide it. */
66
74
  borderBear?: VroomColor;
67
75
  /** Up candle wick color. Defaults to the bull fill color. */
68
76
  wickBull?: VroomColor;
69
77
  /** Down candle wick color. Defaults to the bear fill color. */
70
78
  wickBear?: VroomColor;
79
+ /** Wick stroke width in px (applies to both up and down wicks). Defaults to 1. */
80
+ wickWidth?: number;
81
+ /** Corner radius (px) of candle bodies. Defaults to 0 (square). */
82
+ candleRadius?: number;
83
+ /** Round the wick end caps. Defaults to false. */
84
+ wickRoundCap?: boolean;
85
+ /** Corner radius (px) of the *top* of volume bars. Defaults to 0 (square). */
86
+ volumeRadius?: number;
71
87
  /** Gridlines. */
72
88
  grid?: VroomColor;
73
89
  /** Axis label text (price + time). */
@@ -161,6 +177,51 @@ type VWAPConfig = {
161
177
  /** Stroke width in px. Default 1.5. */
162
178
  width?: number;
163
179
  };
180
+ /**
181
+ * A single resting-liquidity band: a price interval carrying a total order size
182
+ * on one side of the book. Consolidate raw L2 levels into these buckets before
183
+ * passing them. The vertical extent is defined in price space, so bands scale
184
+ * with the y-axis on zoom.
185
+ */
186
+ type LiquidityBand = {
187
+ /** Bottom of the price interval. */
188
+ minPrice: number;
189
+ /** Top of the price interval. A per-level order uses a thin interval. */
190
+ maxPrice: number;
191
+ /** Which side of the book — selects the buy vs sell color. */
192
+ side: 'buy' | 'sell';
193
+ /** Total resting size in this band; drives the band's opacity. */
194
+ volume: number;
195
+ };
196
+ /**
197
+ * Resting-order / order-book liquidity overlay. Each band is drawn as a
198
+ * horizontal rectangle anchored at the inner edge of the price axis, stretching
199
+ * left (~`widthPx`/`widthFrac` of the pane) and fading out with a gradient,
200
+ * colored by side and made more opaque with volume. Rendered behind the candles.
201
+ * Omit the prop (or pass no bands) to hide the overlay.
202
+ */
203
+ type LiquidityConfig = {
204
+ /** The bands to render. */
205
+ bands: LiquidityBand[];
206
+ /** Buy-side color (hex string or packed ARGB number). Default teal-green. */
207
+ buyColor?: VroomColor;
208
+ /** Sell-side color (hex string or packed ARGB number). Default red. */
209
+ sellColor?: VroomColor;
210
+ /**
211
+ * Volume mapped to the peak (`maxOpacity`) band. Omit to auto-scale to the
212
+ * largest band's volume each render.
213
+ */
214
+ maxVolume?: number;
215
+ /** Opacity floor for the smallest band. Default 0.05. */
216
+ minOpacity?: number;
217
+ /** Opacity at `maxVolume`. Default 0.8. */
218
+ maxOpacity?: number;
219
+ /** Leftward reach from the axis in px. Default 300. */
220
+ widthPx?: number;
221
+ /** Leftward reach as a fraction of pane width. Default 0.25. The smaller of
222
+ * `widthPx` and `widthFrac * paneWidth` is used. */
223
+ widthFrac?: number;
224
+ };
164
225
  /** MACD indicator config. Rendered in its own pane below the candles. */
165
226
  type MACDConfig = {
166
227
  enabled?: boolean;
@@ -196,6 +257,14 @@ type VroomChartCoreProps = {
196
257
  height?: number;
197
258
  /** Time window to render. Omit (or both 0) to show every candle. */
198
259
  visibleRange?: VisibleRange;
260
+ /**
261
+ * Target candle *body* width in logical px for the INITIAL framing. Larger →
262
+ * more zoomed in (fewer candles); smaller → more zoomed out. Only affects
263
+ * first load / reset; an explicit `visibleRange` takes precedence. Omit for
264
+ * the default (~80 candles). Useful to keep candles a consistent size across
265
+ * devices of different widths.
266
+ */
267
+ defaultCandleWidth?: number;
199
268
  theme?: VroomTheme;
200
269
  /** RSI indicator (pane below the candles). Omit/disable to hide it. */
201
270
  rsi?: RSIConfig;
@@ -205,12 +274,27 @@ type VroomChartCoreProps = {
205
274
  movingAverages?: MovingAverageOverlay[];
206
275
  /** VWAP overlay (session anchor, configurable reset). */
207
276
  vwap?: VWAPConfig;
277
+ /** Resting-order / order-book liquidity bands drawn behind the candles. */
278
+ liquidity?: LiquidityConfig;
208
279
  /**
209
280
  * Pixels the crosshair dot / horizontal line sit *above* the touch point so
210
281
  * they aren't hidden under the thumb. The vertical line stays centered on the
211
282
  * touch x. Default 40.
212
283
  */
213
284
  crosshairOffset?: number;
285
+ /**
286
+ * Controlled external crosshair, in data space (epoch ms + price). When
287
+ * non-null the chart shows a crosshair there — typically synced from another
288
+ * chart's `onCrosshair` — *without* emitting its own `onCrosshair`, so two
289
+ * charts can drive each other without a feedback loop. A local hover/press on
290
+ * this chart takes precedence; on release the override is restored. Set null
291
+ * to clear. Best when the synced charts share a comparable price scale (the
292
+ * horizontal line clamps to the pane edge when the price is off-scale).
293
+ */
294
+ crosshairOverride?: {
295
+ timeMs: number;
296
+ price: number;
297
+ } | null;
214
298
  /**
215
299
  * Interaction mode. Default `'pan'`. Set to `'draw'` (with a `tool`) to let
216
300
  * the user place drawing points; panning/zooming are suppressed while drawing.
package/lib/index.js CHANGED
@@ -95,6 +95,18 @@ var COLOR_KEYS = {
95
95
  accentBear: 15
96
96
  // VROOM_COLOR_ACCENT_BEAR
97
97
  };
98
+ var FLOAT_KEYS = {
99
+ wickWidth: 1,
100
+ // VROOM_FLOAT_WICK_WIDTH_PX
101
+ candleRadius: 8,
102
+ // VROOM_FLOAT_CANDLE_RADIUS_PX
103
+ volumeRadius: 10
104
+ // VROOM_FLOAT_VOLUME_RADIUS_PX
105
+ };
106
+ var BOOL_KEYS = {
107
+ wickRoundCap: 9
108
+ // VROOM_FLOAT_WICK_ROUND_CAP
109
+ };
98
110
  function parseColor(value) {
99
111
  if (typeof value === "number") {
100
112
  return Number.isFinite(value) ? value >>> 0 : null;
@@ -108,11 +120,21 @@ function parseColor(value) {
108
120
  function applyTheme(handle, theme) {
109
121
  Object.keys(COLOR_KEYS).forEach((field) => {
110
122
  const value = theme[field];
111
- if (value == null) return;
123
+ if (typeof value !== "string" && typeof value !== "number") return;
112
124
  const argb = parseColor(value);
113
125
  if (argb == null) return;
114
126
  handle.setColor(COLOR_KEYS[field], argb);
115
127
  });
128
+ Object.keys(FLOAT_KEYS).forEach((field) => {
129
+ const value = theme[field];
130
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
131
+ handle.setFloat(FLOAT_KEYS[field], value);
132
+ });
133
+ Object.keys(BOOL_KEYS).forEach((field) => {
134
+ const value = theme[field];
135
+ if (typeof value !== "boolean") return;
136
+ handle.setFloat(BOOL_KEYS[field], value ? 1 : 0);
137
+ });
116
138
  }
117
139
 
118
140
  // src/useChartCore.ts
@@ -145,8 +167,9 @@ function ensureInstalled() {
145
167
  }
146
168
  installed = true;
147
169
  }
148
- function useChartCore(candles, size, visibleRange, theme, rsi, macd, movingAverages, vwap) {
170
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, theme, rsi, macd, movingAverages, vwap) {
149
171
  const handleRef = (0, import_react.useRef)(null);
172
+ const defaultWidthAppliedRef = (0, import_react.useRef)(false);
150
173
  const [picture, setPicture] = (0, import_react.useState)(null);
151
174
  if (!handleRef.current && size.width > 0 && size.height > 0) {
152
175
  ensureInstalled();
@@ -164,6 +187,10 @@ function useChartCore(candles, size, visibleRange, theme, rsi, macd, movingAvera
164
187
  const h = handleRef.current;
165
188
  if (!h) return;
166
189
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
190
+ if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
191
+ h.setDefaultCandleWidth(defaultCandleWidth);
192
+ defaultWidthAppliedRef.current = true;
193
+ }
167
194
  if (candles.length > 0) {
168
195
  h.setCandles(packCandles(candles));
169
196
  }
@@ -195,7 +222,7 @@ function useChartCore(candles, size, visibleRange, theme, rsi, macd, movingAvera
195
222
  vwap?.width ?? 1.5
196
223
  );
197
224
  setPicture(h.render());
198
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey]);
225
+ }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey]);
199
226
  return { handle: handleRef.current, picture };
200
227
  }
201
228
 
@@ -207,6 +234,7 @@ function VroomChart(props) {
207
234
  height: heightProp,
208
235
  style,
209
236
  visibleRange,
237
+ defaultCandleWidth,
210
238
  theme,
211
239
  rsi,
212
240
  macd,
@@ -230,6 +258,7 @@ function VroomChart(props) {
230
258
  candles,
231
259
  { width, height },
232
260
  visibleRange,
261
+ defaultCandleWidth,
233
262
  theme,
234
263
  rsi,
235
264
  macd,
@@ -313,7 +342,7 @@ function VroomChart(props) {
313
342
  const t = info?.timeMs ?? null;
314
343
  if (t !== lastCrosshairTime.current) {
315
344
  lastCrosshairTime.current = t;
316
- onCrosshair?.({ active: true, candle: info?.candle ?? null, timeMs: t, reason: "move" });
345
+ onCrosshair?.({ active: true, candle: info?.candle ?? null, timeMs: t, price: null, reason: "move" });
317
346
  }
318
347
  return;
319
348
  } else {
@@ -409,6 +438,8 @@ function VroomChart(props) {
409
438
  active: true,
410
439
  candle: info?.candle ?? null,
411
440
  timeMs: info?.timeMs ?? null,
441
+ price: null,
442
+ // web-only for now (see @vroomchart/react)
412
443
  reason: "show"
413
444
  });
414
445
  });
@@ -419,7 +450,7 @@ function VroomChart(props) {
419
450
  const ch = handle.clearCrosshair();
420
451
  if (ch) pictureSV.value = ch;
421
452
  lastCrosshairTime.current = null;
422
- onCrosshair?.({ active: false, candle: null, timeMs: null, reason: "hide" });
453
+ onCrosshair?.({ active: false, candle: null, timeMs: null, price: null, reason: "hide" });
423
454
  });
424
455
  const gesture = import_react_native_gesture_handler.Gesture.Simultaneous(pan, pinch, longPress, tap);
425
456
  return /* @__PURE__ */ import_react2.default.createElement(
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/VroomChart.tsx","../src/useChartCore.ts","../src/NativeVroomChart.ts","../src/packCandles.ts","../src/theme.ts"],"sourcesContent":["export { VroomChart } from './VroomChart';\nexport type {\n VroomChartProps,\n Candle,\n CrosshairEvent,\n VroomTheme,\n VroomColor,\n VisibleRange,\n RSIConfig,\n MACDConfig,\n MASource,\n MovingAverageOverlay,\n VWAPConfig,\n} from './types';\n","// VroomChart — Phase 3.\n//\n// Owns a SharedValue<SkPicture> driven by:\n// - useChartCore's \"initial\" picture (when data/size/range change), AND\n// - Pan gesture callbacks that call handle.pan(dx, dy) → fresh picture.\n//\n// Reanimated 4 + RN-Skia 2 propagate SharedValue<SkPicture> changes to\n// <Picture> without a React re-render, so gesture-driven redraws are cheap.\n//\n// Gestures run on the JS thread for now (`runOnJS(true)`) — installing the\n// JSI bindings on the worklet runtime is a later perf optimization.\n\nimport React, { useEffect, useRef, useCallback, useState, useMemo } from 'react';\nimport { View, type LayoutChangeEvent } from 'react-native';\nimport { Canvas, Picture, Skia, type SkPicture } from '@shopify/react-native-skia';\nimport {\n Gesture,\n GestureDetector,\n GestureHandlerRootView,\n} from 'react-native-gesture-handler';\nimport { useSharedValue } from 'react-native-reanimated';\n\nimport { useChartCore } from './useChartCore';\nimport type { VroomChartProps } from './types';\nimport './jsi.d';\n\n/**\n * Skia-rendered candlestick chart. Pass OHLCV `candles` and size it via `style`\n * (it fills its parent by default). Pan to scroll, pinch to zoom, drag the\n * price/time axes to rescale, and long-press for the crosshair. Optional\n * indicators (`rsi`, `macd`, `movingAverages`, `vwap`), colors (`theme`), and\n * events (`onCrosshair`, `onViewportChange`) are configured through props.\n *\n * @see {@link VroomChartProps} for the full prop reference.\n */\nexport function VroomChart(props: VroomChartProps) {\n const {\n candles,\n width: widthProp,\n height: heightProp,\n style,\n visibleRange,\n theme,\n rsi,\n macd,\n movingAverages,\n vwap,\n crosshairOffset = 40,\n onCrosshair,\n onViewportChange,\n } = props;\n\n // Fill the parent by default: measure via onLayout. Explicit width/height\n // props (if given) win per-axis. Until the first layout, dims are 0 and we\n // render nothing (one frame).\n const [measured, setMeasured] = useState({ width: 0, height: 0 });\n const width = widthProp ?? measured.width;\n const height = heightProp ?? measured.height;\n\n const onLayout = useCallback((e: LayoutChangeEvent) => {\n const w = Math.round(e.nativeEvent.layout.width);\n const h = Math.round(e.nativeEvent.layout.height);\n setMeasured((prev) =>\n prev.width === w && prev.height === h ? prev : { width: w, height: h },\n );\n }, []);\n\n const { handle, picture } = useChartCore(\n candles,\n { width, height },\n visibleRange,\n theme,\n rsi,\n macd,\n movingAverages,\n vwap,\n );\n\n // RN-Skia's recorder reads this SharedValue on the UI/render runtime, a beat\n // behind JS-thread writes. If it ever reads null it throws (\"Invalid prop\n // value for SkTextBlob received\" — RN-Skia's mislabeled SkPicture error), so\n // we seed it with an empty picture and *never* assign null into it.\n const emptyPicture = useMemo(() => {\n const rec = Skia.PictureRecorder();\n rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));\n return rec.finishRecordingAsPicture();\n }, []);\n const pictureSV = useSharedValue<SkPicture>(emptyPicture);\n\n // When the crosshair is showing, pan moves it (instead of scrolling) and\n // pinch is disabled. A ref (not state) so gesture callbacks read it\n // synchronously without re-subscribing. Tap dismisses it.\n const crosshairActive = useRef(false);\n\n // timeMs of the candle last reported through onCrosshair, so a drag fires a\n // 'move' event only when it crosses into a *different* candle (one per\n // candle, not per frame). Null while the crosshair is hidden.\n const lastCrosshairTime = useRef<number | null>(null);\n\n // Sync the initial picture from useChartCore into the SV whenever it\n // refreshes (data load, size change, externally-controlled range change).\n // Only ever assign a non-null picture (see emptyPicture note above).\n useEffect(() => {\n if (picture) pictureSV.value = picture;\n }, [picture, pictureSV]);\n\n // Momentum scroll. After Pan ends with non-trivial velocity, we run a RAF\n // loop that calls handle.pan(dx, 0) each frame with an exponentially\n // decaying velocity. A new pan (or unmount) cancels the loop.\n const decayRaf = useRef<number | null>(null);\n const cancelDecay = useCallback(() => {\n if (decayRaf.current != null) {\n cancelAnimationFrame(decayRaf.current);\n decayRaf.current = null;\n }\n }, []);\n useEffect(() => cancelDecay, [cancelDecay]);\n\n // Axis-label fade animation loop. When a gesture changes which labels are\n // active, the C++ side starts ramping their opacities. We keep calling\n // render() on every frame until handle.isAnimating() returns false. The\n // loop is started by gesture callbacks (and the momentum tick) after they\n // update the picture, and self-stops when fades settle.\n const animRaf = useRef<number | null>(null);\n const animTick = useCallback(() => {\n animRaf.current = null;\n if (!handle) return;\n const next = handle.render();\n if (next) pictureSV.value = next;\n if (handle.isAnimating()) {\n animRaf.current = requestAnimationFrame(animTick);\n }\n }, [handle, pictureSV]);\n const maybeStartAnim = useCallback(() => {\n if (animRaf.current != null) return;\n if (!handle?.isAnimating()) return;\n animRaf.current = requestAnimationFrame(animTick);\n }, [handle, animTick]);\n useEffect(() => {\n return () => {\n if (animRaf.current != null) {\n cancelAnimationFrame(animRaf.current);\n animRaf.current = null;\n }\n };\n }, []);\n\n // Classifies a touch point into the candle area vs. an axis strip. Axis\n // strips always own their gesture (scale price/time) and take priority over\n // the crosshair: an axis touch never opens, moves, or dismisses it.\n const hitAxis = useCallback(\n (x: number, y: number): 'chart' | 'price-axis' | 'time-axis' | 'indicator' => {\n if (!handle) return 'chart';\n const { yAxisWidth, xAxisHeight, indicatorHeight } =\n handle.getAxisMetrics();\n if (x > width - yAxisWidth) return 'price-axis';\n if (y > height - xAxisHeight) return 'time-axis';\n // The indicator pane sits just above the time-axis strip. A drag here\n // scrolls the candles horizontally (no vertical price change).\n if (indicatorHeight > 0 && y > height - xAxisHeight - indicatorHeight) {\n return 'indicator';\n }\n return 'chart';\n },\n [handle, width, height],\n );\n\n // Pan routes to different C++ mutators depending on where it started: the\n // candle area (chart scroll / crosshair move), the y-axis strip (price\n // scale), the x-axis strip (time scale), or the indicator pane (horizontal\n // scroll only). We classify on onStart.\n const panMode = useRef<'chart' | 'price-axis' | 'time-axis' | 'indicator'>(\n 'chart',\n );\n\n const pan = Gesture.Pan()\n .runOnJS(true)\n .maxPointers(1) // don't fight Pinch's two-finger gesture\n .onStart((e) => {\n cancelDecay();\n // Always classify — an axis drag controls the axis even while the\n // crosshair is up. Only a chart-area drag interacts with the crosshair.\n panMode.current = hitAxis(e.x, e.y);\n })\n .onChange((e) => {\n if (!handle) return;\n let next: ReturnType<typeof handle.pan> = null;\n if (panMode.current === 'price-axis') {\n next = handle.scalePriceAxis(e.changeY);\n } else if (panMode.current === 'time-axis') {\n next = handle.scaleTimeAxis(e.changeX);\n } else if (panMode.current === 'indicator') {\n // Drag in an indicator pane scrolls the candles horizontally only —\n // no vertical price slide (the pane's scale is fixed).\n next = handle.pan(e.changeX, 0);\n } else if (crosshairActive.current) {\n // Chart area + crosshair up → the drag moves the crosshair instead of\n // scrolling. Vertical line tracks the finger x; the dot/horizontal line\n // stay lifted `crosshairOffset` px above the fingertip.\n const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);\n if (ch) pictureSV.value = ch;\n // The line follows the finger every frame (above), but only notify the\n // host when the snapped slot actually changes. The slot has a timeMs\n // even in the empty space ahead of the last candle, where candle=null.\n const info = handle.getCrosshairInfo();\n const t = info?.timeMs ?? null;\n if (t !== lastCrosshairTime.current) {\n lastCrosshairTime.current = t;\n onCrosshair?.({ active: true, candle: info?.candle ?? null, timeMs: t, reason: 'move' });\n }\n return;\n } else {\n // Chart area: 1-finger drag translates both axes. Horizontal\n // component scrolls time, vertical component slides price bounds\n // (axes follow). Diagonal works naturally.\n next = handle.translate(e.changeX, e.changeY);\n }\n if (next) pictureSV.value = next;\n maybeStartAnim();\n })\n .onEnd((e) => {\n if (!handle) return;\n // A chart-area drag with the crosshair up just moved the crosshair —\n // nothing about the viewport changed, and no momentum.\n if (panMode.current === 'chart' && crosshairActive.current) return;\n onViewportChange?.(0, 0);\n\n // Axis drags don't get momentum — they're a precise size adjustment.\n // Chart and indicator-pane drags both get horizontal fling momentum.\n if (panMode.current !== 'chart' && panMode.current !== 'indicator') return;\n\n let velocity = e.velocityX; // px/s\n const MIN_LAUNCH = 80; // ignore tiny flicks\n const MIN_STOP = 8; // px/s — stop threshold\n const HALF_LIFE_S = 0.35; // velocity halves every 0.35s\n if (Math.abs(velocity) < MIN_LAUNCH) return;\n\n let lastTime = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = (now - lastTime) / 1000;\n lastTime = now;\n\n // Frame-time-independent exponential decay.\n velocity *= Math.pow(0.5, dt / HALF_LIFE_S);\n const dx = velocity * dt;\n const next = handle.pan(dx, 0);\n if (next) pictureSV.value = next;\n maybeStartAnim();\n\n if (Math.abs(velocity) > MIN_STOP) {\n decayRaf.current = requestAnimationFrame(tick);\n } else {\n decayRaf.current = null;\n }\n };\n decayRaf.current = requestAnimationFrame(tick);\n });\n\n // Directional pinch. A single Pinch scale is uniform, so we read the two\n // touch points and track their horizontal/vertical spans independently: a\n // vertical pinch scales price (y), a horizontal pinch scales the time window\n // (x), and a diagonal pinch does both. An axis whose initial span is tiny\n // (fingers ~collinear on that axis) is left alone.\n // Lock the scalable axes at gesture start by orientation: an axis only\n // scales if its initial span is meaningful AND at least AXIS_RATIO of the\n // other axis. This keeps a vertical pinch from ever touching x (and vice\n // versa) — critical because during a vertical pinch the fingers' x-coords\n // drift and cross, sending spanX through ~0 and otherwise exploding frameX.\n const MIN_SPAN = 24; // px — minimum span for an axis to scale at all\n const AXIS_RATIO = 0.5; // axis scales only if its span ≥ this × the other's\n const pinchStart = useRef({\n spanX: 1,\n spanY: 1,\n ratioX: 1,\n ratioY: 1,\n enableX: false,\n enableY: false,\n });\n const pinch = Gesture.Pinch()\n .runOnJS(true)\n .onTouchesDown((e) => {\n if (e.numberOfTouches < 2) return;\n const [a, b] = e.allTouches;\n const spanX = Math.abs(a.x - b.x);\n const spanY = Math.abs(a.y - b.y);\n pinchStart.current = {\n spanX,\n spanY,\n ratioX: 1,\n ratioY: 1,\n enableX: spanX >= MIN_SPAN && spanX >= spanY * AXIS_RATIO,\n enableY: spanY >= MIN_SPAN && spanY >= spanX * AXIS_RATIO,\n };\n })\n .onTouchesMove((e) => {\n if (!handle || crosshairActive.current) return;\n if (e.numberOfTouches < 2) return;\n const [a, b] = e.allTouches;\n const start = pinchStart.current;\n const focalX = (a.x + b.x) * 0.5;\n const focalY = (a.y + b.y) * 0.5;\n\n // Per-frame factor = current cumulative ratio / previous. Floor the\n // current span at MIN_SPAN so a near-zero span (fingers crossing on that\n // axis) can't blow the ratio up.\n let frameX = 1;\n if (start.enableX) {\n const ratioX = Math.max(Math.abs(a.x - b.x), MIN_SPAN) / start.spanX;\n frameX = ratioX / start.ratioX;\n start.ratioX = ratioX;\n }\n let frameY = 1;\n if (start.enableY) {\n const ratioY = Math.max(Math.abs(a.y - b.y), MIN_SPAN) / start.spanY;\n frameY = ratioY / start.ratioY;\n start.ratioY = ratioY;\n }\n if (frameX === 1 && frameY === 1) return;\n\n const next = handle.zoom(frameX, frameY, focalX, focalY);\n if (next) pictureSV.value = next;\n maybeStartAnim();\n });\n\n // Long press shows the crosshair at the press point. A stationary hold never\n // activates `pan` (it needs movement first), so the chart won't scroll under\n // the hold. The dot/horizontal line are lifted above the fingertip.\n const longPress = Gesture.LongPress()\n .runOnJS(true)\n .onStart((e) => {\n if (!handle) return;\n // A long press on an axis strip controls the axis, never the crosshair.\n if (hitAxis(e.x, e.y) !== 'chart') return;\n cancelDecay();\n crosshairActive.current = true;\n const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);\n if (ch) pictureSV.value = ch;\n const info = handle.getCrosshairInfo();\n lastCrosshairTime.current = info?.timeMs ?? null;\n onCrosshair?.({\n active: true,\n candle: info?.candle ?? null,\n timeMs: info?.timeMs ?? null,\n reason: 'show',\n });\n });\n\n // A tap dismisses the crosshair while it's up; otherwise it's a no-op (so it\n // never interferes with normal pan/pinch).\n const tap = Gesture.Tap()\n .runOnJS(true)\n .onStart((e) => {\n if (!handle || !crosshairActive.current) return;\n // A tap on an axis strip controls the axis, never dismisses the crosshair.\n if (hitAxis(e.x, e.y) !== 'chart') return;\n crosshairActive.current = false;\n const ch = handle.clearCrosshair();\n if (ch) pictureSV.value = ch;\n lastCrosshairTime.current = null;\n onCrosshair?.({ active: false, candle: null, timeMs: null, reason: 'hide' });\n });\n\n const gesture = Gesture.Simultaneous(pan, pinch, longPress, tap);\n\n return (\n <GestureHandlerRootView\n onLayout={onLayout}\n style={[\n { width: widthProp, height: heightProp },\n widthProp == null && heightProp == null ? { flex: 1 } : null,\n style,\n ]}\n >\n <GestureDetector gesture={gesture}>\n <View style={{ flex: 1 }}>\n <Canvas style={{ flex: 1 }}>\n {width > 0 && height > 0 ? (\n // pictureSV is always a valid picture (seeded empty, never null),\n // so RN-Skia's UI-thread reader never sees null.\n <Picture picture={pictureSV} />\n ) : null}\n </Canvas>\n </View>\n </GestureDetector>\n </GestureHandlerRootView>\n );\n}\n","import { useEffect, useRef, useState } from 'react';\nimport type { SkPicture } from '@shopify/react-native-skia';\n\nimport NativeVroomChart from './NativeVroomChart';\nimport type { ChartHandle } from './jsi.d';\nimport { packCandles } from './packCandles';\nimport { applyTheme, parseColor } from './theme';\nimport type {\n Candle,\n MACDConfig,\n MovingAverageOverlay,\n RSIConfig,\n VisibleRange,\n VroomTheme,\n VWAPConfig,\n} from './types';\n\n// Mirrors vroom::ma::Source order in packages/core/src/ma.h.\nconst MA_SOURCES = [\n 'close',\n 'open',\n 'high',\n 'low',\n 'hl2',\n 'hlc3',\n 'ohlc4',\n] as const;\n\nfunction overlayToNumeric(o: MovingAverageOverlay) {\n const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;\n return {\n kind: o.kind === 'ema' ? 1 : 0,\n period: o.length,\n source: srcIdx < 0 ? 0 : srcIdx,\n color: (o.color != null ? parseColor(o.color) : null) ?? 0xff2962ff,\n width: o.width ?? 1.5,\n };\n}\n\nlet installed = false;\nfunction ensureInstalled(): void {\n if (installed) return;\n const ok = NativeVroomChart.install();\n if (!ok) throw new Error('VroomChartModule.install() returned false');\n if (typeof globalThis.VroomChartJSI === 'undefined') {\n throw new Error('global.VroomChartJSI undefined after install()');\n }\n installed = true;\n}\n\nexport type ChartCoreState = {\n handle: ChartHandle | null;\n /** Picture freshly rendered after the latest data/size/range push. */\n picture: SkPicture | null;\n};\n\n// Owns a ChartHandle and produces an \"initial\" picture whenever data, size,\n// or the externally-controlled visible range changes. Gesture-driven updates\n// happen outside this hook by calling handle.pan(...) directly and assigning\n// the result into a SharedValue.\nexport function useChartCore(\n candles: Candle[],\n size: { width: number; height: number; pxRatio?: number },\n visibleRange?: VisibleRange,\n theme?: VroomTheme,\n rsi?: RSIConfig,\n macd?: MACDConfig,\n movingAverages?: MovingAverageOverlay[],\n vwap?: VWAPConfig,\n): ChartCoreState {\n const handleRef = useRef<ChartHandle | null>(null);\n const [picture, setPicture] = useState<SkPicture | null>(null);\n\n if (!handleRef.current && size.width > 0 && size.height > 0) {\n ensureInstalled();\n handleRef.current = globalThis.VroomChartJSI!.create();\n }\n\n // When no visibleRange is provided, leave the range entirely to the C++\n // side (which defaults to a sensible recent window on first setCandles).\n // Only push setVisibleRange when the caller is actively controlling it,\n // so it doesn't clobber the default or fight gesture-driven pans.\n const explicit = visibleRange != null;\n const startMs = visibleRange?.startMs ?? 0;\n const endMs = visibleRange?.endMs ?? 0;\n\n // Stable deps so inline `theme={{...}}` / `rsi={{...}}` literals don't re-run\n // the effect every render — only when the actual values change.\n const themeKey = theme ? JSON.stringify(theme) : '';\n const rsiKey = rsi ? JSON.stringify(rsi) : '';\n const macdKey = macd ? JSON.stringify(macd) : '';\n const maKey = movingAverages ? JSON.stringify(movingAverages) : '';\n const vwapKey = vwap ? JSON.stringify(vwap) : '';\n\n useEffect(() => {\n const h = handleRef.current;\n if (!h) return;\n h.setSize(size.width, size.height, size.pxRatio ?? 1);\n if (candles.length > 0) {\n h.setCandles(packCandles(candles));\n }\n if (explicit) {\n h.setVisibleRange(startMs, endMs);\n }\n if (theme) {\n applyTheme(h, theme);\n }\n h.setRSI(\n rsi?.enabled ?? false,\n rsi?.period ?? 14,\n rsi?.upperBand ?? 70,\n rsi?.lowerBand ?? 30,\n rsi?.maEnabled ?? true,\n rsi?.maPeriod ?? 14,\n );\n h.setMACD(\n macd?.enabled ?? false,\n macd?.fast ?? 12,\n macd?.slow ?? 26,\n macd?.signal ?? 9,\n );\n h.setOverlays((movingAverages ?? []).map(overlayToNumeric));\n h.setVWAP(\n vwap?.enabled ?? false,\n vwap?.resetMinutes ?? 0,\n (vwap?.color != null ? parseColor(vwap.color) : null) ?? 0xff00bcd4,\n vwap?.width ?? 1.5,\n );\n setPicture(h.render());\n // theme/rsi/macd/movingAverages/vwap are represented by their *Key deps.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey]);\n\n return { handle: handleRef.current, picture };\n}\n","import type { TurboModule } from 'react-native';\nimport { TurboModuleRegistry } from 'react-native';\n\n// TurboModule spec consumed by codegen. The only method is `install`, which\n// the native side uses to install the `global.VroomChartJSI` host object the\n// first time it's called from JS. All real chart operations go through that\n// host object, not through the TurboModule itself.\nexport interface Spec extends TurboModule {\n install(): boolean;\n}\n\nexport default TurboModuleRegistry.getEnforcing<Spec>('VroomChartModule');\n","import type { Candle } from './types';\n\n// Wire format must match `VroomCandle` in packages/core/include/vroom/vroom_chart.h:\n// int64_t time_ms; double open, high, low, close, volume;\n// = 48 bytes per candle, 8-byte aligned, little-endian on iOS/Android.\nexport const BYTES_PER_CANDLE = 48;\n\n// Serializes candles into the packed little-endian buffer the C++ core expects.\n// Pure (no native/Skia deps) so it can be unit-tested in isolation.\nexport function packCandles(candles: Candle[]): ArrayBuffer {\n const buf = new ArrayBuffer(candles.length * BYTES_PER_CANDLE);\n const view = new DataView(buf);\n for (let i = 0; i < candles.length; i++) {\n const c = candles[i]!;\n const off = i * BYTES_PER_CANDLE;\n view.setBigInt64(off, BigInt(c.timeMs), true);\n view.setFloat64(off + 8, c.open, true);\n view.setFloat64(off + 16, c.high, true);\n view.setFloat64(off + 24, c.low, true);\n view.setFloat64(off + 32, c.close, true);\n view.setFloat64(off + 40, c.volume, true);\n }\n return buf;\n}\n","import type { ChartHandle } from './jsi.d';\nimport type { VroomColor, VroomTheme } from './types';\n\n// Maps each VroomTheme field to its VroomColorKey index in the C++ enum\n// (packages/core/include/vroom/vroom_chart.h). Keep in sync with that enum;\n// new keys are appended there so existing indices never shift.\nexport const COLOR_KEYS: Record<keyof VroomTheme, number> = {\n background: 0, // VROOM_COLOR_BACKGROUND\n bull: 1, // VROOM_COLOR_BULL\n bear: 2, // VROOM_COLOR_BEAR\n grid: 4, // VROOM_COLOR_GRID\n axisText: 5, // VROOM_COLOR_AXIS_TEXT\n crosshair: 6, // VROOM_COLOR_CROSSHAIR\n crosshairTarget: 9, // VROOM_COLOR_CROSSHAIR_TARGET\n borderBull: 10, // VROOM_COLOR_BORDER_BULL\n borderBear: 11, // VROOM_COLOR_BORDER_BEAR\n wickBull: 12, // VROOM_COLOR_WICK_BULL\n wickBear: 13, // VROOM_COLOR_WICK_BEAR\n accentBull: 14, // VROOM_COLOR_ACCENT_BULL\n accentBear: 15, // VROOM_COLOR_ACCENT_BEAR\n};\n\n// Parses a color into a packed 0xAARRGGBB integer (Skia's ARGB order).\n// - number → taken as already-packed ARGB\n// - '#rgb'-style 6-digit hex → opaque (alpha forced to ff)\n// - 8-digit hex → interpreted as AARRGGBB\n// Returns null for anything malformed so the caller can skip it.\nexport function parseColor(value: VroomColor): number | null {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value >>> 0 : null;\n }\n let s = value.trim();\n if (s.startsWith('#')) s = s.slice(1);\n if (s.length === 6) s = `ff${s}`; // assume opaque\n if (s.length !== 8 || !/^[0-9a-fA-F]{8}$/.test(s)) return null;\n return parseInt(s, 16) >>> 0;\n}\n\n// Pushes every provided theme color into the chart core via handle.setColor.\n// Unspecified or unparseable colors are skipped (they keep their default).\nexport function applyTheme(handle: ChartHandle, theme: VroomTheme): void {\n (Object.keys(COLOR_KEYS) as (keyof VroomTheme)[]).forEach((field) => {\n const value = theme[field];\n if (value == null) return;\n const argb = parseColor(value);\n if (argb == null) return;\n handle.setColor(COLOR_KEYS[field], argb);\n });\n}\n"],"mappings":"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","names":["import_react","import_react_native","React"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/VroomChart.tsx","../src/useChartCore.ts","../src/NativeVroomChart.ts","../src/packCandles.ts","../src/theme.ts"],"sourcesContent":["export { VroomChart } from './VroomChart';\nexport type {\n VroomChartProps,\n Candle,\n CrosshairEvent,\n VroomTheme,\n VroomColor,\n VisibleRange,\n RSIConfig,\n MACDConfig,\n MASource,\n MovingAverageOverlay,\n VWAPConfig,\n} from './types';\n","// VroomChart — Phase 3.\n//\n// Owns a SharedValue<SkPicture> driven by:\n// - useChartCore's \"initial\" picture (when data/size/range change), AND\n// - Pan gesture callbacks that call handle.pan(dx, dy) → fresh picture.\n//\n// Reanimated 4 + RN-Skia 2 propagate SharedValue<SkPicture> changes to\n// <Picture> without a React re-render, so gesture-driven redraws are cheap.\n//\n// Gestures run on the JS thread for now (`runOnJS(true)`) — installing the\n// JSI bindings on the worklet runtime is a later perf optimization.\n\nimport React, { useEffect, useRef, useCallback, useState, useMemo } from 'react';\nimport { View, type LayoutChangeEvent } from 'react-native';\nimport { Canvas, Picture, Skia, type SkPicture } from '@shopify/react-native-skia';\nimport {\n Gesture,\n GestureDetector,\n GestureHandlerRootView,\n} from 'react-native-gesture-handler';\nimport { useSharedValue } from 'react-native-reanimated';\n\nimport { useChartCore } from './useChartCore';\nimport type { VroomChartProps } from './types';\nimport './jsi.d';\n\n/**\n * Skia-rendered candlestick chart. Pass OHLCV `candles` and size it via `style`\n * (it fills its parent by default). Pan to scroll, pinch to zoom, drag the\n * price/time axes to rescale, and long-press for the crosshair. Optional\n * indicators (`rsi`, `macd`, `movingAverages`, `vwap`), colors (`theme`), and\n * events (`onCrosshair`, `onViewportChange`) are configured through props.\n *\n * @see {@link VroomChartProps} for the full prop reference.\n */\nexport function VroomChart(props: VroomChartProps) {\n const {\n candles,\n width: widthProp,\n height: heightProp,\n style,\n visibleRange,\n defaultCandleWidth,\n theme,\n rsi,\n macd,\n movingAverages,\n vwap,\n crosshairOffset = 40,\n onCrosshair,\n onViewportChange,\n } = props;\n\n // Fill the parent by default: measure via onLayout. Explicit width/height\n // props (if given) win per-axis. Until the first layout, dims are 0 and we\n // render nothing (one frame).\n const [measured, setMeasured] = useState({ width: 0, height: 0 });\n const width = widthProp ?? measured.width;\n const height = heightProp ?? measured.height;\n\n const onLayout = useCallback((e: LayoutChangeEvent) => {\n const w = Math.round(e.nativeEvent.layout.width);\n const h = Math.round(e.nativeEvent.layout.height);\n setMeasured((prev) =>\n prev.width === w && prev.height === h ? prev : { width: w, height: h },\n );\n }, []);\n\n const { handle, picture } = useChartCore(\n candles,\n { width, height },\n visibleRange,\n defaultCandleWidth,\n theme,\n rsi,\n macd,\n movingAverages,\n vwap,\n );\n\n // RN-Skia's recorder reads this SharedValue on the UI/render runtime, a beat\n // behind JS-thread writes. If it ever reads null it throws (\"Invalid prop\n // value for SkTextBlob received\" — RN-Skia's mislabeled SkPicture error), so\n // we seed it with an empty picture and *never* assign null into it.\n const emptyPicture = useMemo(() => {\n const rec = Skia.PictureRecorder();\n rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));\n return rec.finishRecordingAsPicture();\n }, []);\n const pictureSV = useSharedValue<SkPicture>(emptyPicture);\n\n // When the crosshair is showing, pan moves it (instead of scrolling) and\n // pinch is disabled. A ref (not state) so gesture callbacks read it\n // synchronously without re-subscribing. Tap dismisses it.\n const crosshairActive = useRef(false);\n\n // timeMs of the candle last reported through onCrosshair, so a drag fires a\n // 'move' event only when it crosses into a *different* candle (one per\n // candle, not per frame). Null while the crosshair is hidden.\n const lastCrosshairTime = useRef<number | null>(null);\n\n // Sync the initial picture from useChartCore into the SV whenever it\n // refreshes (data load, size change, externally-controlled range change).\n // Only ever assign a non-null picture (see emptyPicture note above).\n useEffect(() => {\n if (picture) pictureSV.value = picture;\n }, [picture, pictureSV]);\n\n // Momentum scroll. After Pan ends with non-trivial velocity, we run a RAF\n // loop that calls handle.pan(dx, 0) each frame with an exponentially\n // decaying velocity. A new pan (or unmount) cancels the loop.\n const decayRaf = useRef<number | null>(null);\n const cancelDecay = useCallback(() => {\n if (decayRaf.current != null) {\n cancelAnimationFrame(decayRaf.current);\n decayRaf.current = null;\n }\n }, []);\n useEffect(() => cancelDecay, [cancelDecay]);\n\n // Axis-label fade animation loop. When a gesture changes which labels are\n // active, the C++ side starts ramping their opacities. We keep calling\n // render() on every frame until handle.isAnimating() returns false. The\n // loop is started by gesture callbacks (and the momentum tick) after they\n // update the picture, and self-stops when fades settle.\n const animRaf = useRef<number | null>(null);\n const animTick = useCallback(() => {\n animRaf.current = null;\n if (!handle) return;\n const next = handle.render();\n if (next) pictureSV.value = next;\n if (handle.isAnimating()) {\n animRaf.current = requestAnimationFrame(animTick);\n }\n }, [handle, pictureSV]);\n const maybeStartAnim = useCallback(() => {\n if (animRaf.current != null) return;\n if (!handle?.isAnimating()) return;\n animRaf.current = requestAnimationFrame(animTick);\n }, [handle, animTick]);\n useEffect(() => {\n return () => {\n if (animRaf.current != null) {\n cancelAnimationFrame(animRaf.current);\n animRaf.current = null;\n }\n };\n }, []);\n\n // Classifies a touch point into the candle area vs. an axis strip. Axis\n // strips always own their gesture (scale price/time) and take priority over\n // the crosshair: an axis touch never opens, moves, or dismisses it.\n const hitAxis = useCallback(\n (x: number, y: number): 'chart' | 'price-axis' | 'time-axis' | 'indicator' => {\n if (!handle) return 'chart';\n const { yAxisWidth, xAxisHeight, indicatorHeight } =\n handle.getAxisMetrics();\n if (x > width - yAxisWidth) return 'price-axis';\n if (y > height - xAxisHeight) return 'time-axis';\n // The indicator pane sits just above the time-axis strip. A drag here\n // scrolls the candles horizontally (no vertical price change).\n if (indicatorHeight > 0 && y > height - xAxisHeight - indicatorHeight) {\n return 'indicator';\n }\n return 'chart';\n },\n [handle, width, height],\n );\n\n // Pan routes to different C++ mutators depending on where it started: the\n // candle area (chart scroll / crosshair move), the y-axis strip (price\n // scale), the x-axis strip (time scale), or the indicator pane (horizontal\n // scroll only). We classify on onStart.\n const panMode = useRef<'chart' | 'price-axis' | 'time-axis' | 'indicator'>(\n 'chart',\n );\n\n const pan = Gesture.Pan()\n .runOnJS(true)\n .maxPointers(1) // don't fight Pinch's two-finger gesture\n .onStart((e) => {\n cancelDecay();\n // Always classify — an axis drag controls the axis even while the\n // crosshair is up. Only a chart-area drag interacts with the crosshair.\n panMode.current = hitAxis(e.x, e.y);\n })\n .onChange((e) => {\n if (!handle) return;\n let next: ReturnType<typeof handle.pan> = null;\n if (panMode.current === 'price-axis') {\n next = handle.scalePriceAxis(e.changeY);\n } else if (panMode.current === 'time-axis') {\n next = handle.scaleTimeAxis(e.changeX);\n } else if (panMode.current === 'indicator') {\n // Drag in an indicator pane scrolls the candles horizontally only —\n // no vertical price slide (the pane's scale is fixed).\n next = handle.pan(e.changeX, 0);\n } else if (crosshairActive.current) {\n // Chart area + crosshair up → the drag moves the crosshair instead of\n // scrolling. Vertical line tracks the finger x; the dot/horizontal line\n // stay lifted `crosshairOffset` px above the fingertip.\n const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);\n if (ch) pictureSV.value = ch;\n // The line follows the finger every frame (above), but only notify the\n // host when the snapped slot actually changes. The slot has a timeMs\n // even in the empty space ahead of the last candle, where candle=null.\n const info = handle.getCrosshairInfo();\n const t = info?.timeMs ?? null;\n if (t !== lastCrosshairTime.current) {\n lastCrosshairTime.current = t;\n // price is web-only for now (see @vroomchart/react); RN reports null.\n onCrosshair?.({ active: true, candle: info?.candle ?? null, timeMs: t, price: null, reason: 'move' });\n }\n return;\n } else {\n // Chart area: 1-finger drag translates both axes. Horizontal\n // component scrolls time, vertical component slides price bounds\n // (axes follow). Diagonal works naturally.\n next = handle.translate(e.changeX, e.changeY);\n }\n if (next) pictureSV.value = next;\n maybeStartAnim();\n })\n .onEnd((e) => {\n if (!handle) return;\n // A chart-area drag with the crosshair up just moved the crosshair —\n // nothing about the viewport changed, and no momentum.\n if (panMode.current === 'chart' && crosshairActive.current) return;\n onViewportChange?.(0, 0);\n\n // Axis drags don't get momentum — they're a precise size adjustment.\n // Chart and indicator-pane drags both get horizontal fling momentum.\n if (panMode.current !== 'chart' && panMode.current !== 'indicator') return;\n\n let velocity = e.velocityX; // px/s\n const MIN_LAUNCH = 80; // ignore tiny flicks\n const MIN_STOP = 8; // px/s — stop threshold\n const HALF_LIFE_S = 0.35; // velocity halves every 0.35s\n if (Math.abs(velocity) < MIN_LAUNCH) return;\n\n let lastTime = performance.now();\n const tick = () => {\n const now = performance.now();\n const dt = (now - lastTime) / 1000;\n lastTime = now;\n\n // Frame-time-independent exponential decay.\n velocity *= Math.pow(0.5, dt / HALF_LIFE_S);\n const dx = velocity * dt;\n const next = handle.pan(dx, 0);\n if (next) pictureSV.value = next;\n maybeStartAnim();\n\n if (Math.abs(velocity) > MIN_STOP) {\n decayRaf.current = requestAnimationFrame(tick);\n } else {\n decayRaf.current = null;\n }\n };\n decayRaf.current = requestAnimationFrame(tick);\n });\n\n // Directional pinch. A single Pinch scale is uniform, so we read the two\n // touch points and track their horizontal/vertical spans independently: a\n // vertical pinch scales price (y), a horizontal pinch scales the time window\n // (x), and a diagonal pinch does both. An axis whose initial span is tiny\n // (fingers ~collinear on that axis) is left alone.\n // Lock the scalable axes at gesture start by orientation: an axis only\n // scales if its initial span is meaningful AND at least AXIS_RATIO of the\n // other axis. This keeps a vertical pinch from ever touching x (and vice\n // versa) — critical because during a vertical pinch the fingers' x-coords\n // drift and cross, sending spanX through ~0 and otherwise exploding frameX.\n const MIN_SPAN = 24; // px — minimum span for an axis to scale at all\n const AXIS_RATIO = 0.5; // axis scales only if its span ≥ this × the other's\n const pinchStart = useRef({\n spanX: 1,\n spanY: 1,\n ratioX: 1,\n ratioY: 1,\n enableX: false,\n enableY: false,\n });\n const pinch = Gesture.Pinch()\n .runOnJS(true)\n .onTouchesDown((e) => {\n if (e.numberOfTouches < 2) return;\n const [a, b] = e.allTouches;\n const spanX = Math.abs(a.x - b.x);\n const spanY = Math.abs(a.y - b.y);\n pinchStart.current = {\n spanX,\n spanY,\n ratioX: 1,\n ratioY: 1,\n enableX: spanX >= MIN_SPAN && spanX >= spanY * AXIS_RATIO,\n enableY: spanY >= MIN_SPAN && spanY >= spanX * AXIS_RATIO,\n };\n })\n .onTouchesMove((e) => {\n if (!handle || crosshairActive.current) return;\n if (e.numberOfTouches < 2) return;\n const [a, b] = e.allTouches;\n const start = pinchStart.current;\n const focalX = (a.x + b.x) * 0.5;\n const focalY = (a.y + b.y) * 0.5;\n\n // Per-frame factor = current cumulative ratio / previous. Floor the\n // current span at MIN_SPAN so a near-zero span (fingers crossing on that\n // axis) can't blow the ratio up.\n let frameX = 1;\n if (start.enableX) {\n const ratioX = Math.max(Math.abs(a.x - b.x), MIN_SPAN) / start.spanX;\n frameX = ratioX / start.ratioX;\n start.ratioX = ratioX;\n }\n let frameY = 1;\n if (start.enableY) {\n const ratioY = Math.max(Math.abs(a.y - b.y), MIN_SPAN) / start.spanY;\n frameY = ratioY / start.ratioY;\n start.ratioY = ratioY;\n }\n if (frameX === 1 && frameY === 1) return;\n\n const next = handle.zoom(frameX, frameY, focalX, focalY);\n if (next) pictureSV.value = next;\n maybeStartAnim();\n });\n\n // Long press shows the crosshair at the press point. A stationary hold never\n // activates `pan` (it needs movement first), so the chart won't scroll under\n // the hold. The dot/horizontal line are lifted above the fingertip.\n const longPress = Gesture.LongPress()\n .runOnJS(true)\n .onStart((e) => {\n if (!handle) return;\n // A long press on an axis strip controls the axis, never the crosshair.\n if (hitAxis(e.x, e.y) !== 'chart') return;\n cancelDecay();\n crosshairActive.current = true;\n const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);\n if (ch) pictureSV.value = ch;\n const info = handle.getCrosshairInfo();\n lastCrosshairTime.current = info?.timeMs ?? null;\n onCrosshair?.({\n active: true,\n candle: info?.candle ?? null,\n timeMs: info?.timeMs ?? null,\n price: null, // web-only for now (see @vroomchart/react)\n reason: 'show',\n });\n });\n\n // A tap dismisses the crosshair while it's up; otherwise it's a no-op (so it\n // never interferes with normal pan/pinch).\n const tap = Gesture.Tap()\n .runOnJS(true)\n .onStart((e) => {\n if (!handle || !crosshairActive.current) return;\n // A tap on an axis strip controls the axis, never dismisses the crosshair.\n if (hitAxis(e.x, e.y) !== 'chart') return;\n crosshairActive.current = false;\n const ch = handle.clearCrosshair();\n if (ch) pictureSV.value = ch;\n lastCrosshairTime.current = null;\n onCrosshair?.({ active: false, candle: null, timeMs: null, price: null, reason: 'hide' });\n });\n\n const gesture = Gesture.Simultaneous(pan, pinch, longPress, tap);\n\n return (\n <GestureHandlerRootView\n onLayout={onLayout}\n style={[\n { width: widthProp, height: heightProp },\n widthProp == null && heightProp == null ? { flex: 1 } : null,\n style,\n ]}\n >\n <GestureDetector gesture={gesture}>\n <View style={{ flex: 1 }}>\n <Canvas style={{ flex: 1 }}>\n {width > 0 && height > 0 ? (\n // pictureSV is always a valid picture (seeded empty, never null),\n // so RN-Skia's UI-thread reader never sees null.\n <Picture picture={pictureSV} />\n ) : null}\n </Canvas>\n </View>\n </GestureDetector>\n </GestureHandlerRootView>\n );\n}\n","import { useEffect, useRef, useState } from 'react';\nimport type { SkPicture } from '@shopify/react-native-skia';\n\nimport NativeVroomChart from './NativeVroomChart';\nimport type { ChartHandle } from './jsi.d';\nimport { packCandles } from './packCandles';\nimport { applyTheme, parseColor } from './theme';\nimport type {\n Candle,\n MACDConfig,\n MovingAverageOverlay,\n RSIConfig,\n VisibleRange,\n VroomTheme,\n VWAPConfig,\n} from './types';\n\n// Mirrors vroom::ma::Source order in packages/core/src/ma.h.\nconst MA_SOURCES = [\n 'close',\n 'open',\n 'high',\n 'low',\n 'hl2',\n 'hlc3',\n 'ohlc4',\n] as const;\n\nfunction overlayToNumeric(o: MovingAverageOverlay) {\n const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;\n return {\n kind: o.kind === 'ema' ? 1 : 0,\n period: o.length,\n source: srcIdx < 0 ? 0 : srcIdx,\n color: (o.color != null ? parseColor(o.color) : null) ?? 0xff2962ff,\n width: o.width ?? 1.5,\n };\n}\n\nlet installed = false;\nfunction ensureInstalled(): void {\n if (installed) return;\n const ok = NativeVroomChart.install();\n if (!ok) throw new Error('VroomChartModule.install() returned false');\n if (typeof globalThis.VroomChartJSI === 'undefined') {\n throw new Error('global.VroomChartJSI undefined after install()');\n }\n installed = true;\n}\n\nexport type ChartCoreState = {\n handle: ChartHandle | null;\n /** Picture freshly rendered after the latest data/size/range push. */\n picture: SkPicture | null;\n};\n\n// Owns a ChartHandle and produces an \"initial\" picture whenever data, size,\n// or the externally-controlled visible range changes. Gesture-driven updates\n// happen outside this hook by calling handle.pan(...) directly and assigning\n// the result into a SharedValue.\nexport function useChartCore(\n candles: Candle[],\n size: { width: number; height: number; pxRatio?: number },\n visibleRange?: VisibleRange,\n defaultCandleWidth?: number,\n theme?: VroomTheme,\n rsi?: RSIConfig,\n macd?: MACDConfig,\n movingAverages?: MovingAverageOverlay[],\n vwap?: VWAPConfig,\n): ChartCoreState {\n const handleRef = useRef<ChartHandle | null>(null);\n // Push setDefaultCandleWidth only once (first load): setCandles re-runs on\n // every data change, and the core setter re-frames when candles are present,\n // so re-pushing would snap the view away from the user's pan/zoom.\n const defaultWidthAppliedRef = useRef(false);\n const [picture, setPicture] = useState<SkPicture | null>(null);\n\n if (!handleRef.current && size.width > 0 && size.height > 0) {\n ensureInstalled();\n handleRef.current = globalThis.VroomChartJSI!.create();\n }\n\n // When no visibleRange is provided, leave the range entirely to the C++\n // side (which defaults to a sensible recent window on first setCandles).\n // Only push setVisibleRange when the caller is actively controlling it,\n // so it doesn't clobber the default or fight gesture-driven pans.\n const explicit = visibleRange != null;\n const startMs = visibleRange?.startMs ?? 0;\n const endMs = visibleRange?.endMs ?? 0;\n\n // Stable deps so inline `theme={{...}}` / `rsi={{...}}` literals don't re-run\n // the effect every render — only when the actual values change.\n const themeKey = theme ? JSON.stringify(theme) : '';\n const rsiKey = rsi ? JSON.stringify(rsi) : '';\n const macdKey = macd ? JSON.stringify(macd) : '';\n const maKey = movingAverages ? JSON.stringify(movingAverages) : '';\n const vwapKey = vwap ? JSON.stringify(vwap) : '';\n\n useEffect(() => {\n const h = handleRef.current;\n if (!h) return;\n h.setSize(size.width, size.height, size.pxRatio ?? 1);\n // Drive the initial zoom from a target candle width. Pushed once, before the\n // first setCandles (while the core window is still 0/0), and only when the\n // caller isn't explicitly controlling the range.\n if (\n !defaultWidthAppliedRef.current &&\n !explicit &&\n defaultCandleWidth != null &&\n defaultCandleWidth > 0\n ) {\n h.setDefaultCandleWidth(defaultCandleWidth);\n defaultWidthAppliedRef.current = true;\n }\n if (candles.length > 0) {\n h.setCandles(packCandles(candles));\n }\n if (explicit) {\n h.setVisibleRange(startMs, endMs);\n }\n if (theme) {\n applyTheme(h, theme);\n }\n h.setRSI(\n rsi?.enabled ?? false,\n rsi?.period ?? 14,\n rsi?.upperBand ?? 70,\n rsi?.lowerBand ?? 30,\n rsi?.maEnabled ?? true,\n rsi?.maPeriod ?? 14,\n );\n h.setMACD(\n macd?.enabled ?? false,\n macd?.fast ?? 12,\n macd?.slow ?? 26,\n macd?.signal ?? 9,\n );\n h.setOverlays((movingAverages ?? []).map(overlayToNumeric));\n h.setVWAP(\n vwap?.enabled ?? false,\n vwap?.resetMinutes ?? 0,\n (vwap?.color != null ? parseColor(vwap.color) : null) ?? 0xff00bcd4,\n vwap?.width ?? 1.5,\n );\n // TODO(rn-parity): mirror the web `liquidity` overlay here (setLiquidity +\n // the VroomBand structs in the JSI handle) — web-only for now.\n setPicture(h.render());\n // theme/rsi/macd/movingAverages/vwap are represented by their *Key deps.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey]);\n\n return { handle: handleRef.current, picture };\n}\n","import type { TurboModule } from 'react-native';\nimport { TurboModuleRegistry } from 'react-native';\n\n// TurboModule spec consumed by codegen. The only method is `install`, which\n// the native side uses to install the `global.VroomChartJSI` host object the\n// first time it's called from JS. All real chart operations go through that\n// host object, not through the TurboModule itself.\nexport interface Spec extends TurboModule {\n install(): boolean;\n}\n\nexport default TurboModuleRegistry.getEnforcing<Spec>('VroomChartModule');\n","import type { Candle } from './types';\n\n// Wire format must match `VroomCandle` in packages/core/include/vroom/vroom_chart.h:\n// int64_t time_ms; double open, high, low, close, volume;\n// = 48 bytes per candle, 8-byte aligned, little-endian on iOS/Android.\nexport const BYTES_PER_CANDLE = 48;\n\n// Serializes candles into the packed little-endian buffer the C++ core expects.\n// Pure (no native/Skia deps) so it can be unit-tested in isolation.\nexport function packCandles(candles: Candle[]): ArrayBuffer {\n const buf = new ArrayBuffer(candles.length * BYTES_PER_CANDLE);\n const view = new DataView(buf);\n for (let i = 0; i < candles.length; i++) {\n const c = candles[i]!;\n const off = i * BYTES_PER_CANDLE;\n view.setBigInt64(off, BigInt(c.timeMs), true);\n view.setFloat64(off + 8, c.open, true);\n view.setFloat64(off + 16, c.high, true);\n view.setFloat64(off + 24, c.low, true);\n view.setFloat64(off + 32, c.close, true);\n view.setFloat64(off + 40, c.volume, true);\n }\n return buf;\n}\n","import type { ChartHandle } from './jsi.d';\nimport type { VroomColor, VroomTheme } from './types';\n\n// Maps each color VroomTheme field to its VroomColorKey index in the C++ enum\n// (packages/core/include/vroom/vroom_chart.h). Keep in sync with that enum;\n// new keys are appended there so existing indices never shift.\nexport const COLOR_KEYS: Partial<Record<keyof VroomTheme, number>> = {\n background: 0, // VROOM_COLOR_BACKGROUND\n bull: 1, // VROOM_COLOR_BULL\n bear: 2, // VROOM_COLOR_BEAR\n grid: 4, // VROOM_COLOR_GRID\n axisText: 5, // VROOM_COLOR_AXIS_TEXT\n crosshair: 6, // VROOM_COLOR_CROSSHAIR\n crosshairTarget: 9, // VROOM_COLOR_CROSSHAIR_TARGET\n borderBull: 10, // VROOM_COLOR_BORDER_BULL\n borderBear: 11, // VROOM_COLOR_BORDER_BEAR\n wickBull: 12, // VROOM_COLOR_WICK_BULL\n wickBear: 13, // VROOM_COLOR_WICK_BEAR\n accentBull: 14, // VROOM_COLOR_ACCENT_BULL\n accentBear: 15, // VROOM_COLOR_ACCENT_BEAR\n};\n\n// Maps each numeric VroomTheme field to its VroomFloatKey index.\nexport const FLOAT_KEYS: Partial<Record<keyof VroomTheme, number>> = {\n wickWidth: 1, // VROOM_FLOAT_WICK_WIDTH_PX\n candleRadius: 8, // VROOM_FLOAT_CANDLE_RADIUS_PX\n volumeRadius: 10, // VROOM_FLOAT_VOLUME_RADIUS_PX\n};\n\n// Maps each boolean VroomTheme field to its VroomFloatKey index (pushed as 0/1).\nexport const BOOL_KEYS: Partial<Record<keyof VroomTheme, number>> = {\n wickRoundCap: 9, // VROOM_FLOAT_WICK_ROUND_CAP\n};\n\n// Parses a color into a packed 0xAARRGGBB integer (Skia's ARGB order).\n// - number → taken as already-packed ARGB\n// - '#rgb'-style 6-digit hex → opaque (alpha forced to ff)\n// - 8-digit hex → interpreted as AARRGGBB\n// Returns null for anything malformed so the caller can skip it.\nexport function parseColor(value: VroomColor): number | null {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value >>> 0 : null;\n }\n let s = value.trim();\n if (s.startsWith('#')) s = s.slice(1);\n if (s.length === 6) s = `ff${s}`; // assume opaque\n if (s.length !== 8 || !/^[0-9a-fA-F]{8}$/.test(s)) return null;\n return parseInt(s, 16) >>> 0;\n}\n\n// Pushes every provided theme color + numeric float into the chart core via\n// handle.setColor / handle.setFloat. Unspecified or unparseable values are\n// skipped (they keep their default).\nexport function applyTheme(handle: ChartHandle, theme: VroomTheme): void {\n (Object.keys(COLOR_KEYS) as (keyof VroomTheme)[]).forEach((field) => {\n const value = theme[field];\n if (typeof value !== 'string' && typeof value !== 'number') return;\n const argb = parseColor(value);\n if (argb == null) return;\n handle.setColor(COLOR_KEYS[field]!, argb);\n });\n (Object.keys(FLOAT_KEYS) as (keyof VroomTheme)[]).forEach((field) => {\n const value = theme[field];\n if (typeof value !== 'number' || !Number.isFinite(value)) return;\n handle.setFloat(FLOAT_KEYS[field]!, value);\n });\n (Object.keys(BOOL_KEYS) as (keyof VroomTheme)[]).forEach((field) => {\n const value = theme[field];\n if (typeof value !== 'boolean') return;\n handle.setFloat(BOOL_KEYS[field]!, value ? 1 : 0);\n });\n}\n"],"mappings":"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","names":["import_react","import_react_native","React"]}