react-native-vroom-chart 0.1.4 → 0.4.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.mjs CHANGED
@@ -60,8 +60,24 @@ var COLOR_KEYS = {
60
60
  // VROOM_COLOR_WICK_BEAR
61
61
  accentBull: 14,
62
62
  // VROOM_COLOR_ACCENT_BULL
63
- accentBear: 15
63
+ accentBear: 15,
64
64
  // VROOM_COLOR_ACCENT_BEAR
65
+ lineColor: 16
66
+ // VROOM_COLOR_LINE
67
+ };
68
+ var FLOAT_KEYS = {
69
+ wickWidth: 1,
70
+ // VROOM_FLOAT_WICK_WIDTH_PX
71
+ candleRadius: 8,
72
+ // VROOM_FLOAT_CANDLE_RADIUS_PX
73
+ volumeRadius: 10,
74
+ // VROOM_FLOAT_VOLUME_RADIUS_PX
75
+ lineWidth: 11
76
+ // VROOM_FLOAT_LINE_WIDTH_PX
77
+ };
78
+ var BOOL_KEYS = {
79
+ wickRoundCap: 9
80
+ // VROOM_FLOAT_WICK_ROUND_CAP
65
81
  };
66
82
  function parseColor(value) {
67
83
  if (typeof value === "number") {
@@ -76,11 +92,21 @@ function parseColor(value) {
76
92
  function applyTheme(handle, theme) {
77
93
  Object.keys(COLOR_KEYS).forEach((field) => {
78
94
  const value = theme[field];
79
- if (value == null) return;
95
+ if (typeof value !== "string" && typeof value !== "number") return;
80
96
  const argb = parseColor(value);
81
97
  if (argb == null) return;
82
98
  handle.setColor(COLOR_KEYS[field], argb);
83
99
  });
100
+ Object.keys(FLOAT_KEYS).forEach((field) => {
101
+ const value = theme[field];
102
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
103
+ handle.setFloat(FLOAT_KEYS[field], value);
104
+ });
105
+ Object.keys(BOOL_KEYS).forEach((field) => {
106
+ const value = theme[field];
107
+ if (typeof value !== "boolean") return;
108
+ handle.setFloat(BOOL_KEYS[field], value ? 1 : 0);
109
+ });
84
110
  }
85
111
 
86
112
  // src/useChartCore.ts
@@ -113,8 +139,9 @@ function ensureInstalled() {
113
139
  }
114
140
  installed = true;
115
141
  }
116
- function useChartCore(candles, size, visibleRange, theme, rsi, macd, movingAverages, vwap) {
142
+ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap) {
117
143
  const handleRef = useRef(null);
144
+ const defaultWidthAppliedRef = useRef(false);
118
145
  const [picture, setPicture] = useState(null);
119
146
  if (!handleRef.current && size.width > 0 && size.height > 0) {
120
147
  ensureInstalled();
@@ -132,6 +159,10 @@ function useChartCore(candles, size, visibleRange, theme, rsi, macd, movingAvera
132
159
  const h = handleRef.current;
133
160
  if (!h) return;
134
161
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
162
+ if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
163
+ h.setDefaultCandleWidth(defaultCandleWidth);
164
+ defaultWidthAppliedRef.current = true;
165
+ }
135
166
  if (candles.length > 0) {
136
167
  h.setCandles(packCandles(candles));
137
168
  }
@@ -163,7 +194,7 @@ function useChartCore(candles, size, visibleRange, theme, rsi, macd, movingAvera
163
194
  vwap?.width ?? 1.5
164
195
  );
165
196
  setPicture(h.render());
166
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey]);
197
+ }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey]);
167
198
  return { handle: handleRef.current, picture };
168
199
  }
169
200
 
@@ -175,6 +206,9 @@ function VroomChart(props) {
175
206
  height: heightProp,
176
207
  style,
177
208
  visibleRange,
209
+ defaultCandleWidth,
210
+ chartType,
211
+ transitionMs,
178
212
  theme,
179
213
  rsi,
180
214
  macd,
@@ -198,6 +232,8 @@ function VroomChart(props) {
198
232
  candles,
199
233
  { width, height },
200
234
  visibleRange,
235
+ defaultCandleWidth,
236
+ chartType,
201
237
  theme,
202
238
  rsi,
203
239
  macd,
@@ -246,6 +282,62 @@ function VroomChart(props) {
246
282
  }
247
283
  };
248
284
  }, []);
285
+ const morphRaf = useRef2(null);
286
+ const morphFade = useRef2(null);
287
+ const morphHandle = useRef2(null);
288
+ useEffect2(() => {
289
+ if (!handle) return void 0;
290
+ const target = chartType === "line" ? 1 : 0;
291
+ if (morphHandle.current !== handle || morphFade.current == null) {
292
+ morphHandle.current = handle;
293
+ morphFade.current = target;
294
+ handle.setChartType(target);
295
+ const p = handle.render();
296
+ if (p) pictureSV.value = p;
297
+ return void 0;
298
+ }
299
+ if (morphFade.current === target) return void 0;
300
+ if (morphRaf.current != null) {
301
+ cancelAnimationFrame(morphRaf.current);
302
+ morphRaf.current = null;
303
+ }
304
+ const dur = Math.max(0, transitionMs ?? 300);
305
+ if (dur === 0) {
306
+ morphFade.current = target;
307
+ handle.setChartType(target);
308
+ const p = handle.render();
309
+ if (p) pictureSV.value = p;
310
+ return void 0;
311
+ }
312
+ const from = morphFade.current;
313
+ let startTs = null;
314
+ const step = (now) => {
315
+ if (startTs == null) startTs = now;
316
+ const prog = Math.min(1, (now - startTs) / dur);
317
+ const e = prog * prog * (3 - 2 * prog);
318
+ const fade = from + (target - from) * e;
319
+ morphFade.current = fade;
320
+ handle.setMorph(fade, fade);
321
+ const p = handle.render();
322
+ if (p) pictureSV.value = p;
323
+ if (prog < 1) {
324
+ morphRaf.current = requestAnimationFrame(step);
325
+ } else {
326
+ morphRaf.current = null;
327
+ morphFade.current = target;
328
+ handle.setChartType(target);
329
+ const q = handle.render();
330
+ if (q) pictureSV.value = q;
331
+ }
332
+ };
333
+ morphRaf.current = requestAnimationFrame(step);
334
+ return () => {
335
+ if (morphRaf.current != null) {
336
+ cancelAnimationFrame(morphRaf.current);
337
+ morphRaf.current = null;
338
+ }
339
+ };
340
+ }, [handle, chartType, transitionMs, pictureSV]);
249
341
  const hitAxis = useCallback(
250
342
  (x, y) => {
251
343
  if (!handle) return "chart";
package/lib/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/VroomChart.tsx","../src/useChartCore.ts","../src/NativeVroomChart.ts","../src/packCandles.ts","../src/theme.ts"],"sourcesContent":["// 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 // 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 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":["useEffect","useRef","useState","useState","useRef","useEffect"]}
1
+ {"version":3,"sources":["../src/VroomChart.tsx","../src/useChartCore.ts","../src/NativeVroomChart.ts","../src/packCandles.ts","../src/theme.ts"],"sourcesContent":["// 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 chartType,\n transitionMs,\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 chartType,\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 // Candle↔line morph. When `chartType` changes we drive the core per-frame with\n // a (collapse, fade) blend and push a fresh picture into the SV each frame — the\n // JS side owns the eased clock (see plan). A fresh handle snaps to the target;\n // transitionMs=0 snaps. Mirrors the web driver in react/src/useChartCore.ts.\n const morphRaf = useRef<number | null>(null);\n const morphFade = useRef<number | null>(null);\n const morphHandle = useRef<typeof handle>(null);\n useEffect(() => {\n if (!handle) return undefined;\n const target = chartType === 'line' ? 1 : 0;\n\n // Fresh handle (first load / recreate): snap, don't animate.\n if (morphHandle.current !== handle || morphFade.current == null) {\n morphHandle.current = handle;\n morphFade.current = target;\n handle.setChartType(target);\n const p = handle.render();\n if (p) pictureSV.value = p;\n return undefined;\n }\n if (morphFade.current === target) return undefined;\n\n if (morphRaf.current != null) {\n cancelAnimationFrame(morphRaf.current);\n morphRaf.current = null;\n }\n const dur = Math.max(0, transitionMs ?? 300);\n if (dur === 0) {\n morphFade.current = target;\n handle.setChartType(target);\n const p = handle.render();\n if (p) pictureSV.value = p;\n return undefined;\n }\n\n const from = morphFade.current;\n let startTs: number | null = null;\n const step = (now: number) => {\n if (startTs == null) startTs = now;\n const prog = Math.min(1, (now - startTs) / dur);\n const e = prog * prog * (3 - 2 * prog); // smoothstep ease-in-out\n const fade = from + (target - from) * e;\n morphFade.current = fade;\n handle.setMorph(fade, fade);\n const p = handle.render();\n if (p) pictureSV.value = p;\n if (prog < 1) {\n morphRaf.current = requestAnimationFrame(step);\n } else {\n morphRaf.current = null;\n morphFade.current = target;\n handle.setChartType(target); // lock the exact endpoint\n const q = handle.render();\n if (q) pictureSV.value = q;\n }\n };\n morphRaf.current = requestAnimationFrame(step);\n\n return () => {\n if (morphRaf.current != null) {\n cancelAnimationFrame(morphRaf.current);\n morphRaf.current = null;\n }\n };\n }, [handle, chartType, transitionMs, pictureSV]);\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 ChartType,\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 chartType?: ChartType,\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 // chartType / the candle↔line morph is driven separately (VroomChart owns the\n // per-frame animation loop so it can update the picture SharedValue directly).\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 lineColor: 16, // VROOM_COLOR_LINE\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 lineWidth: 11, // VROOM_FLOAT_LINE_WIDTH_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":["useEffect","useRef","useState","useState","useRef","useEffect"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-vroom-chart",
3
- "version": "0.1.4",
3
+ "version": "0.4.0",
4
4
  "description": "Mobile-first Skia candlestick chart for React Native",
5
5
  "license": "MIT",
6
6
  "author": "Darion Welch",
@@ -40,6 +40,9 @@ export function VroomChart(props: VroomChartProps) {
40
40
  height: heightProp,
41
41
  style,
42
42
  visibleRange,
43
+ defaultCandleWidth,
44
+ chartType,
45
+ transitionMs,
43
46
  theme,
44
47
  rsi,
45
48
  macd,
@@ -69,6 +72,8 @@ export function VroomChart(props: VroomChartProps) {
69
72
  candles,
70
73
  { width, height },
71
74
  visibleRange,
75
+ defaultCandleWidth,
76
+ chartType,
72
77
  theme,
73
78
  rsi,
74
79
  macd,
@@ -145,6 +150,72 @@ export function VroomChart(props: VroomChartProps) {
145
150
  };
146
151
  }, []);
147
152
 
153
+ // Candle↔line morph. When `chartType` changes we drive the core per-frame with
154
+ // a (collapse, fade) blend and push a fresh picture into the SV each frame — the
155
+ // JS side owns the eased clock (see plan). A fresh handle snaps to the target;
156
+ // transitionMs=0 snaps. Mirrors the web driver in react/src/useChartCore.ts.
157
+ const morphRaf = useRef<number | null>(null);
158
+ const morphFade = useRef<number | null>(null);
159
+ const morphHandle = useRef<typeof handle>(null);
160
+ useEffect(() => {
161
+ if (!handle) return undefined;
162
+ const target = chartType === 'line' ? 1 : 0;
163
+
164
+ // Fresh handle (first load / recreate): snap, don't animate.
165
+ if (morphHandle.current !== handle || morphFade.current == null) {
166
+ morphHandle.current = handle;
167
+ morphFade.current = target;
168
+ handle.setChartType(target);
169
+ const p = handle.render();
170
+ if (p) pictureSV.value = p;
171
+ return undefined;
172
+ }
173
+ if (morphFade.current === target) return undefined;
174
+
175
+ if (morphRaf.current != null) {
176
+ cancelAnimationFrame(morphRaf.current);
177
+ morphRaf.current = null;
178
+ }
179
+ const dur = Math.max(0, transitionMs ?? 300);
180
+ if (dur === 0) {
181
+ morphFade.current = target;
182
+ handle.setChartType(target);
183
+ const p = handle.render();
184
+ if (p) pictureSV.value = p;
185
+ return undefined;
186
+ }
187
+
188
+ const from = morphFade.current;
189
+ let startTs: number | null = null;
190
+ const step = (now: number) => {
191
+ if (startTs == null) startTs = now;
192
+ const prog = Math.min(1, (now - startTs) / dur);
193
+ const e = prog * prog * (3 - 2 * prog); // smoothstep ease-in-out
194
+ const fade = from + (target - from) * e;
195
+ morphFade.current = fade;
196
+ handle.setMorph(fade, fade);
197
+ const p = handle.render();
198
+ if (p) pictureSV.value = p;
199
+ if (prog < 1) {
200
+ morphRaf.current = requestAnimationFrame(step);
201
+ } else {
202
+ morphRaf.current = null;
203
+ morphFade.current = target;
204
+ handle.setChartType(target); // lock the exact endpoint
205
+ const q = handle.render();
206
+ if (q) pictureSV.value = q;
207
+ }
208
+ };
209
+ morphRaf.current = requestAnimationFrame(step);
210
+
211
+ return () => {
212
+ if (morphRaf.current != null) {
213
+ cancelAnimationFrame(morphRaf.current);
214
+ morphRaf.current = null;
215
+ }
216
+ };
217
+ }, [handle, chartType, transitionMs, pictureSV]);
218
+
148
219
  // Classifies a touch point into the candle area vs. an axis strip. Axis
149
220
  // strips always own their gesture (scale price/time) and take priority over
150
221
  // the crosshair: an axis touch never opens, moves, or dismisses it.
package/src/index.ts CHANGED
@@ -11,4 +11,5 @@ export type {
11
11
  MASource,
12
12
  MovingAverageOverlay,
13
13
  VWAPConfig,
14
+ ChartType,
14
15
  } from './types';
package/src/jsi.d.ts CHANGED
@@ -10,8 +10,23 @@ export interface ChartHandle {
10
10
  * COLOR_KEYS in theme.ts); `argb` is a packed 0xAARRGGBB integer.
11
11
  */
12
12
  setColor(key: number, argb: number): void;
13
+ /**
14
+ * Overrides a single theme float. `key` is a VroomFloatKey index (see
15
+ * FLOAT_KEYS in theme.ts); e.g. wick width.
16
+ */
17
+ setFloat(key: number, value: number): void;
13
18
  /** Pass 0, 0 to show all candles. */
14
19
  setVisibleRange(startMs: number, endMs: number): void;
20
+ /**
21
+ * Target candle body width in px for the initial/reset framing. Larger = more
22
+ * zoomed in; smaller = more zoomed out. 0 restores the default (~80 candles).
23
+ * Affects initial framing only; an explicit setVisibleRange takes precedence.
24
+ */
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;
15
30
  /** Shifts the visible range by `dx`/`dy` pixels and returns a fresh picture. */
16
31
  pan(dx: number, dy: number): SkPicture | null;
17
32
  /**
package/src/theme.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { ChartHandle } from './jsi.d';
2
2
  import type { VroomColor, VroomTheme } from './types';
3
3
 
4
- // Maps each VroomTheme field to its VroomColorKey index in the C++ enum
4
+ // Maps each color VroomTheme field to its VroomColorKey index in the C++ enum
5
5
  // (packages/core/include/vroom/vroom_chart.h). Keep in sync with that enum;
6
6
  // new keys are appended there so existing indices never shift.
7
- export const COLOR_KEYS: Record<keyof VroomTheme, number> = {
7
+ export const COLOR_KEYS: Partial<Record<keyof VroomTheme, number>> = {
8
8
  background: 0, // VROOM_COLOR_BACKGROUND
9
9
  bull: 1, // VROOM_COLOR_BULL
10
10
  bear: 2, // VROOM_COLOR_BEAR
@@ -18,6 +18,20 @@ export const COLOR_KEYS: 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
22
+ };
23
+
24
+ // Maps each numeric VroomTheme field to its VroomFloatKey index.
25
+ export const FLOAT_KEYS: Partial<Record<keyof VroomTheme, number>> = {
26
+ wickWidth: 1, // VROOM_FLOAT_WICK_WIDTH_PX
27
+ candleRadius: 8, // VROOM_FLOAT_CANDLE_RADIUS_PX
28
+ volumeRadius: 10, // VROOM_FLOAT_VOLUME_RADIUS_PX
29
+ lineWidth: 11, // VROOM_FLOAT_LINE_WIDTH_PX
30
+ };
31
+
32
+ // Maps each boolean VroomTheme field to its VroomFloatKey index (pushed as 0/1).
33
+ export const BOOL_KEYS: Partial<Record<keyof VroomTheme, number>> = {
34
+ wickRoundCap: 9, // VROOM_FLOAT_WICK_ROUND_CAP
21
35
  };
22
36
 
23
37
  // Parses a color into a packed 0xAARRGGBB integer (Skia's ARGB order).
@@ -36,14 +50,25 @@ export function parseColor(value: VroomColor): number | null {
36
50
  return parseInt(s, 16) >>> 0;
37
51
  }
38
52
 
39
- // Pushes every provided theme color into the chart core via handle.setColor.
40
- // Unspecified or unparseable colors are skipped (they keep their default).
53
+ // Pushes every provided theme color + numeric float into the chart core via
54
+ // handle.setColor / handle.setFloat. Unspecified or unparseable values are
55
+ // skipped (they keep their default).
41
56
  export function applyTheme(handle: ChartHandle, theme: VroomTheme): void {
42
57
  (Object.keys(COLOR_KEYS) as (keyof VroomTheme)[]).forEach((field) => {
43
58
  const value = theme[field];
44
- if (value == null) return;
59
+ if (typeof value !== 'string' && typeof value !== 'number') return;
45
60
  const argb = parseColor(value);
46
61
  if (argb == null) return;
47
- handle.setColor(COLOR_KEYS[field], argb);
62
+ handle.setColor(COLOR_KEYS[field]!, argb);
63
+ });
64
+ (Object.keys(FLOAT_KEYS) as (keyof VroomTheme)[]).forEach((field) => {
65
+ const value = theme[field];
66
+ if (typeof value !== 'number' || !Number.isFinite(value)) return;
67
+ handle.setFloat(FLOAT_KEYS[field]!, value);
68
+ });
69
+ (Object.keys(BOOL_KEYS) as (keyof VroomTheme)[]).forEach((field) => {
70
+ const value = theme[field];
71
+ if (typeof value !== 'boolean') return;
72
+ handle.setFloat(BOOL_KEYS[field]!, value ? 1 : 0);
48
73
  });
49
74
  }
package/src/types.ts CHANGED
@@ -15,6 +15,7 @@ export type {
15
15
  MovingAverageOverlay,
16
16
  VWAPConfig,
17
17
  MACDConfig,
18
+ ChartType,
18
19
  } from '@vroomchart/types';
19
20
 
20
21
  /**
@@ -7,6 +7,7 @@ import { packCandles } from './packCandles';
7
7
  import { applyTheme, parseColor } from './theme';
8
8
  import type {
9
9
  Candle,
10
+ ChartType,
10
11
  MACDConfig,
11
12
  MovingAverageOverlay,
12
13
  RSIConfig,
@@ -62,6 +63,8 @@ export function useChartCore(
62
63
  candles: Candle[],
63
64
  size: { width: number; height: number; pxRatio?: number },
64
65
  visibleRange?: VisibleRange,
66
+ defaultCandleWidth?: number,
67
+ chartType?: ChartType,
65
68
  theme?: VroomTheme,
66
69
  rsi?: RSIConfig,
67
70
  macd?: MACDConfig,
@@ -69,6 +72,10 @@ export function useChartCore(
69
72
  vwap?: VWAPConfig,
70
73
  ): ChartCoreState {
71
74
  const handleRef = useRef<ChartHandle | null>(null);
75
+ // Push setDefaultCandleWidth only once (first load): setCandles re-runs on
76
+ // every data change, and the core setter re-frames when candles are present,
77
+ // so re-pushing would snap the view away from the user's pan/zoom.
78
+ const defaultWidthAppliedRef = useRef(false);
72
79
  const [picture, setPicture] = useState<SkPicture | null>(null);
73
80
 
74
81
  if (!handleRef.current && size.width > 0 && size.height > 0) {
@@ -96,12 +103,26 @@ export function useChartCore(
96
103
  const h = handleRef.current;
97
104
  if (!h) return;
98
105
  h.setSize(size.width, size.height, size.pxRatio ?? 1);
106
+ // Drive the initial zoom from a target candle width. Pushed once, before the
107
+ // first setCandles (while the core window is still 0/0), and only when the
108
+ // caller isn't explicitly controlling the range.
109
+ if (
110
+ !defaultWidthAppliedRef.current &&
111
+ !explicit &&
112
+ defaultCandleWidth != null &&
113
+ defaultCandleWidth > 0
114
+ ) {
115
+ h.setDefaultCandleWidth(defaultCandleWidth);
116
+ defaultWidthAppliedRef.current = true;
117
+ }
99
118
  if (candles.length > 0) {
100
119
  h.setCandles(packCandles(candles));
101
120
  }
102
121
  if (explicit) {
103
122
  h.setVisibleRange(startMs, endMs);
104
123
  }
124
+ // chartType / the candle↔line morph is driven separately (VroomChart owns the
125
+ // per-frame animation loop so it can update the picture SharedValue directly).
105
126
  if (theme) {
106
127
  applyTheme(h, theme);
107
128
  }
@@ -126,10 +147,12 @@ export function useChartCore(
126
147
  (vwap?.color != null ? parseColor(vwap.color) : null) ?? 0xff00bcd4,
127
148
  vwap?.width ?? 1.5,
128
149
  );
150
+ // TODO(rn-parity): mirror the web `liquidity` overlay here (setLiquidity +
151
+ // the VroomBand structs in the JSI handle) — web-only for now.
129
152
  setPicture(h.render());
130
153
  // theme/rsi/macd/movingAverages/vwap are represented by their *Key deps.
131
154
  // eslint-disable-next-line react-hooks/exhaustive-deps
132
- }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey]);
155
+ }, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey]);
133
156
 
134
157
  return { handle: handleRef.current, picture };
135
158
  }