@vroomchart/react 0.1.1 → 0.1.3

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/dist/index.d.ts CHANGED
@@ -54,10 +54,22 @@ type VroomColor = string | number;
54
54
  type VroomTheme = {
55
55
  /** Chart + axis-strip background. */
56
56
  background?: VroomColor;
57
- /** Up candles (also bull wicks, bull volume bars, rising price indicator). */
57
+ /** Up candle body fill. Wick and border default to this unless overridden. */
58
58
  bull?: VroomColor;
59
- /** Down candles (also bear wicks, bear volume bars, falling price indicator). */
59
+ /** Down candle body fill. Wick and border default to this unless overridden. */
60
60
  bear?: VroomColor;
61
+ /** Generic up color for the price indicator, volume bars, and MACD histogram. Defaults to teal-green; independent of `bull`. */
62
+ accentBull?: VroomColor;
63
+ /** Generic down color for the price indicator, volume bars, and MACD histogram. Defaults to red; independent of `bear`. */
64
+ accentBear?: VroomColor;
65
+ /** Up candle body 1px border. Defaults to the bull fill color. */
66
+ borderBull?: VroomColor;
67
+ /** Down candle body 1px border. Defaults to the bear fill color. */
68
+ borderBear?: VroomColor;
69
+ /** Up candle wick color. Defaults to the bull fill color. */
70
+ wickBull?: VroomColor;
71
+ /** Down candle wick color. Defaults to the bear fill color. */
72
+ wickBear?: VroomColor;
61
73
  /** Gridlines. */
62
74
  grid?: VroomColor;
63
75
  /** Axis label text (price + time). */
@@ -74,6 +86,38 @@ type VisibleRange = {
74
86
  /** Window end (inclusive), Unix epoch milliseconds. */
75
87
  endMs: number;
76
88
  };
89
+ /**
90
+ * Chart interaction mode.
91
+ * 'pan' — default: drag to pan, pinch/wheel to zoom, hover/long-press crosshair.
92
+ * 'draw' — left-clicks place drawing points; panning/zooming are suppressed.
93
+ */
94
+ type ChartMode = 'pan' | 'draw';
95
+ /** Active drawing tool while in `draw` mode. `null` draws nothing. */
96
+ type DrawTool = null | 'line';
97
+ /** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
98
+ type DrawPoint = {
99
+ /** Anchor time as Unix epoch milliseconds (not snapped to a candle slot). */
100
+ timeMs: number;
101
+ /** Anchor price. */
102
+ price: number;
103
+ };
104
+ /**
105
+ * A committed drawing. Pass an array of these via the `drawings` prop to render
106
+ * persisted annotations; the chart appends a new one (via `onDrawingComplete`)
107
+ * each time the user finishes drawing. For now only the `'line'` (two-point
108
+ * trendline) type exists.
109
+ */
110
+ type Drawing = {
111
+ /** Stable unique id (the chart generates one for drawings it creates). */
112
+ id: string;
113
+ type: 'line';
114
+ /** The two endpoints, in data space. */
115
+ points: [DrawPoint, DrawPoint];
116
+ /** Line color (hex string or packed ARGB number). Default solid blue. */
117
+ color?: VroomColor;
118
+ /** Stroke width in px. Default 2. */
119
+ width?: number;
120
+ };
77
121
  /** RSI indicator config. Rendered in a pane below the candles when enabled. */
78
122
  type RSIConfig = {
79
123
  enabled?: boolean;
@@ -137,6 +181,14 @@ type MACDConfig = {
137
181
  type VroomChartCoreProps = {
138
182
  /** OHLCV bars to render. The only required prop. */
139
183
  candles: Candle[];
184
+ /**
185
+ * Identity of the data series (e.g. "BTC-USD"). When it changes between
186
+ * renders the chart resets to the default view (most recent candles + price
187
+ * auto-fit), regardless of what the data heuristics conclude — the escape
188
+ * hatch for ambiguous switches like two assets trading at similar prices.
189
+ * Omit to rely on automatic detection from the candle data alone.
190
+ */
191
+ seriesKey?: string;
140
192
  /**
141
193
  * Explicit size overrides in logical px. When omitted, the chart fills its
142
194
  * parent (measured at runtime). Prefer layout-driven sizing via the
@@ -161,6 +213,27 @@ type VroomChartCoreProps = {
161
213
  * touch x. Default 40.
162
214
  */
163
215
  crosshairOffset?: number;
216
+ /**
217
+ * Interaction mode. Default `'pan'`. Set to `'draw'` (with a `tool`) to let
218
+ * the user place drawing points; panning/zooming are suppressed while drawing.
219
+ */
220
+ mode?: ChartMode;
221
+ /** Active drawing tool while in `draw` mode. Default `null` (draws nothing). */
222
+ tool?: DrawTool;
223
+ /**
224
+ * Committed drawings to render, anchored to data so they track the candles on
225
+ * pan/zoom. This is a controlled prop: append the value the chart hands you in
226
+ * `onDrawingComplete` to persist it.
227
+ */
228
+ drawings?: Drawing[];
229
+ /** Fired with the finished drawing when the user completes one. */
230
+ onDrawingComplete?: (drawing: Drawing) => void;
231
+ /**
232
+ * Fired when the chart wants the mode changed — e.g. it requests `'pan'` after
233
+ * the user clicks away from a just-drawn line. Since `mode` is controlled, the
234
+ * host should apply the requested mode.
235
+ */
236
+ onModeChange?: (mode: ChartMode) => void;
164
237
  onCrosshair?: (e: CrosshairEvent) => void;
165
238
  onViewportChange?: (startMs: number, endMs: number) => void;
166
239
  };
@@ -190,4 +263,29 @@ type VroomChartProps = VroomChartCoreProps & {
190
263
  */
191
264
  declare function VroomChart(props: VroomChartProps): react_jsx_runtime.JSX.Element;
192
265
 
193
- export { type Candle, type CrosshairEvent, type MACDConfig, type MASource, type MovingAverageOverlay, type RSIConfig, type VWAPConfig, type VisibleRange, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme };
266
+ type DataTransition = 'initial' | 'stream' | 'timeframe' | 'reset';
267
+ /**
268
+ * The candle period in ms, inferred as the median of the first few intervals
269
+ * (robust to a single gap). Null when there are fewer than two candles.
270
+ */
271
+ declare function inferStepMs(candles: Candle[]): number | null;
272
+ /**
273
+ * Classify a candles-prop change. `prev` is the previously rendered array
274
+ * (null on first render); `seriesKeyChanged` forces `reset` regardless of the
275
+ * data (the explicit escape hatch).
276
+ *
277
+ * Constraint: detection compares two immutable snapshots. An array mutated in
278
+ * place (same reference) never reaches this code — React props must change
279
+ * identity to re-render.
280
+ */
281
+ declare function classifyTransition(prev: Candle[] | null, next: Candle[], seriesKeyChanged: boolean): DataTransition;
282
+ /**
283
+ * The visible window to apply after a timeframe switch so each candle keeps
284
+ * the exact pixel width it had before: the visible slot count is preserved and
285
+ * the right edge re-anchors on the newest candle (any future-gap overshoot is
286
+ * carried over in slots, clamped to the core's 3/4-window cap). The new start
287
+ * may precede the first candle — that gap is intentional, width wins.
288
+ */
289
+ declare function timeframeWindow(oldWindow: VisibleRange, oldStepMs: number, oldLastMs: number, newStepMs: number, newLastMs: number): VisibleRange;
290
+
291
+ export { type Candle, type ChartMode, type CrosshairEvent, type DataTransition, type DrawPoint, type DrawTool, type Drawing, type MACDConfig, type MASource, type MovingAverageOverlay, type RSIConfig, type VWAPConfig, type VisibleRange, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme, classifyTransition, inferStepMs, timeframeWindow };
package/dist/index.js CHANGED
@@ -6,6 +6,52 @@ import {
6
6
  applyTheme,
7
7
  parseColor
8
8
  } from "@vroomchart/core-wasm";
9
+
10
+ // src/dataTransitions.ts
11
+ var STEP_TOLERANCE = 0.01;
12
+ var MAX_SAME_ASSET_CLOSE_RATIO = 1.25;
13
+ var MAX_END_DRIFT_STEPS = 3;
14
+ var MAX_STREAM_ADVANCE_STEPS = 5;
15
+ function inferStepMs(candles) {
16
+ if (candles.length < 2) return null;
17
+ const k = Math.min(candles.length - 1, 8);
18
+ const diffs = [];
19
+ for (let i = 0; i < k; i++) diffs.push(candles[i + 1].timeMs - candles[i].timeMs);
20
+ diffs.sort((a, b) => a - b);
21
+ const median = diffs[Math.floor(diffs.length / 2)];
22
+ return median > 0 ? median : null;
23
+ }
24
+ function classifyTransition(prev, next, seriesKeyChanged) {
25
+ if (!prev || prev.length === 0) return "initial";
26
+ if (next.length === 0) return "stream";
27
+ if (seriesKeyChanged) return "reset";
28
+ const prevStep = inferStepMs(prev);
29
+ const nextStep = inferStepMs(next);
30
+ if (prevStep == null || nextStep == null) return "reset";
31
+ const prevLast = prev[prev.length - 1];
32
+ const nextLast = next[next.length - 1];
33
+ if (Math.abs(nextStep - prevStep) <= prevStep * STEP_TOLERANCE) {
34
+ const idx = Math.round((prevLast.timeMs - next[0].timeMs) / nextStep);
35
+ const aligned = idx >= 0 && idx < next.length && next[idx].timeMs === prevLast.timeMs;
36
+ const sharedBarRatio = aligned && next[idx].close > 0 && prevLast.close > 0 ? Math.max(next[idx].close / prevLast.close, prevLast.close / next[idx].close) : Infinity;
37
+ const advanced = nextLast.timeMs >= prevLast.timeMs && nextLast.timeMs - prevLast.timeMs <= MAX_STREAM_ADVANCE_STEPS * nextStep;
38
+ return sharedBarRatio <= MAX_SAME_ASSET_CLOSE_RATIO && advanced ? "stream" : "reset";
39
+ }
40
+ const closeRatio = prevLast.close > 0 && nextLast.close > 0 ? Math.max(nextLast.close / prevLast.close, prevLast.close / nextLast.close) : Infinity;
41
+ const prevEnd = prevLast.timeMs + prevStep;
42
+ const nextEnd = nextLast.timeMs + nextStep;
43
+ const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
44
+ return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
45
+ }
46
+ function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
47
+ const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
48
+ const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
49
+ const offsetSlots = Math.min(Math.max(offsetRaw, 0), slots * 0.75);
50
+ const endMs = Math.round(newLastMs + offsetSlots * newStepMs);
51
+ return { startMs: Math.round(endMs - slots * newStepMs), endMs };
52
+ }
53
+
54
+ // src/useChartCore.ts
9
55
  var MA_SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4"];
10
56
  function overlayToNumeric(o) {
11
57
  const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
@@ -17,9 +63,20 @@ function overlayToNumeric(o) {
17
63
  width: o.width ?? 1.5
18
64
  };
19
65
  }
66
+ function drawingToSpec(d) {
67
+ return {
68
+ aTime: d.points[0].timeMs,
69
+ aPrice: d.points[0].price,
70
+ bTime: d.points[1].timeMs,
71
+ bPrice: d.points[1].price,
72
+ color: (d.color != null ? parseColor(d.color) : null) ?? 4280902399,
73
+ width: d.width ?? 2
74
+ };
75
+ }
20
76
  function useChartCore(props, loadOpts) {
21
77
  const {
22
78
  candles,
79
+ seriesKey,
23
80
  width: widthProp,
24
81
  height: heightProp,
25
82
  visibleRange,
@@ -27,11 +84,13 @@ function useChartCore(props, loadOpts) {
27
84
  rsi,
28
85
  macd,
29
86
  movingAverages,
30
- vwap
87
+ vwap,
88
+ drawings
31
89
  } = props;
32
90
  const containerRef = useRef(null);
33
91
  const canvasRef = useRef(null);
34
92
  const handleRef = useRef(null);
93
+ const prevDataRef = useRef(null);
35
94
  const rafRef = useRef(null);
36
95
  const [ready, setReady] = useState(false);
37
96
  const [measured, setMeasured] = useState({ width: 0, height: 0 });
@@ -85,6 +144,7 @@ function useChartCore(props, loadOpts) {
85
144
  const macdKey = macd ? JSON.stringify(macd) : "";
86
145
  const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
87
146
  const vwapKey = vwap ? JSON.stringify(vwap) : "";
147
+ const drawingsKey = drawings ? JSON.stringify(drawings) : "";
88
148
  const explicit = visibleRange != null;
89
149
  const startMs = visibleRange?.startMs ?? 0;
90
150
  const endMs = visibleRange?.endMs ?? 0;
@@ -93,7 +153,39 @@ function useChartCore(props, loadOpts) {
93
153
  if (!h || width <= 0 || height <= 0) return;
94
154
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
95
155
  h.setSize(width, height, dpr);
96
- if (candles.length > 0) h.setCandles(packCandles(candles));
156
+ if (candles.length > 0) {
157
+ const prev = prevDataRef.current;
158
+ const freshHandle = prev == null || prev.handle !== h;
159
+ if (freshHandle || prev.candles !== candles || prev.seriesKey !== seriesKey) {
160
+ const transition = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
161
+ let tfArgs = null;
162
+ if (transition === "timeframe" && prev != null) {
163
+ const oldWindow = h.getVisibleRange();
164
+ const oldStepMs = inferStepMs(prev.candles);
165
+ if (oldWindow.endMs > oldWindow.startMs && oldStepMs != null) {
166
+ tfArgs = { oldWindow, oldStepMs, oldLastMs: prev.candles[prev.candles.length - 1].timeMs };
167
+ }
168
+ }
169
+ h.setCandles(packCandles(candles));
170
+ if (transition === "timeframe") {
171
+ const newStepMs = inferStepMs(candles);
172
+ if (tfArgs && newStepMs != null) {
173
+ const w = timeframeWindow(
174
+ tfArgs.oldWindow,
175
+ tfArgs.oldStepMs,
176
+ tfArgs.oldLastMs,
177
+ newStepMs,
178
+ candles[candles.length - 1].timeMs
179
+ );
180
+ h.setVisibleRange(w.startMs, w.endMs);
181
+ }
182
+ h.resetPriceScale();
183
+ } else if (transition === "reset") {
184
+ h.resetView();
185
+ }
186
+ prevDataRef.current = { handle: h, candles, seriesKey };
187
+ }
188
+ }
97
189
  if (explicit) h.setVisibleRange(startMs, endMs);
98
190
  if (theme) applyTheme(h, theme);
99
191
  h.setRSI(
@@ -112,8 +204,9 @@ function useChartCore(props, loadOpts) {
112
204
  (vwap?.color != null ? parseColor(vwap.color) : null) ?? 4278238420,
113
205
  vwap?.width ?? 1.5
114
206
  );
207
+ h.setDrawings((drawings ?? []).map(drawingToSpec));
115
208
  scheduleRender();
116
- }, [ready, width, height, candles, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey, scheduleRender]);
209
+ }, [ready, width, height, candles, seriesKey, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey, drawingsKey, scheduleRender]);
117
210
  return { containerRef, canvasRef, handleRef, scheduleRender, size: { width, height } };
118
211
  }
119
212
 
@@ -124,9 +217,42 @@ var AXIS_RATIO = 0.5;
124
217
  var LONG_PRESS_MS = 350;
125
218
  var MOVE_THRESH = 6;
126
219
  var WHEEL_K = 15e-4;
220
+ var SEP_HIT = 4;
221
+ var DRAW_COLOR = 4280902399;
222
+ var DRAW_WIDTH = 2;
223
+ var DRAW_HIT = 6;
224
+ function drawingId() {
225
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
226
+ return crypto.randomUUID();
227
+ }
228
+ return `draw-${Math.random().toString(36).slice(2)}`;
229
+ }
230
+ function distToSegment(px, py, ax, ay, bx, by) {
231
+ const dx = bx - ax;
232
+ const dy = by - ay;
233
+ const len2 = dx * dx + dy * dy;
234
+ let t = len2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0;
235
+ t = Math.max(0, Math.min(1, t));
236
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
237
+ }
127
238
  function useGestures(containerRef, handleRef, scheduleRender, opts) {
128
239
  const optsRef = useRef2(opts);
129
240
  optsRef.current = opts;
241
+ const drawAnchorRef = useRef2(null);
242
+ const drawAnchorPxRef = useRef2(null);
243
+ const drawSelectedPxRef = useRef2(null);
244
+ useEffect2(() => {
245
+ const active = opts.mode === "draw" && opts.tool === "line";
246
+ if (active) return;
247
+ drawAnchorRef.current = null;
248
+ drawAnchorPxRef.current = null;
249
+ drawSelectedPxRef.current = null;
250
+ const h = handleRef.current;
251
+ if (h) {
252
+ h.clearDraft();
253
+ scheduleRender();
254
+ }
255
+ }, [opts.mode, opts.tool, handleRef, scheduleRender]);
130
256
  useEffect2(() => {
131
257
  const el = containerRef.current;
132
258
  if (!el) return;
@@ -149,7 +275,17 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
149
275
  const r = el.getBoundingClientRect();
150
276
  if (!h) return "chart";
151
277
  const { yAxisWidth, xAxisHeight, indicatorHeight } = h.getAxisMetrics();
152
- if (x > r.width - yAxisWidth) return "price-axis";
278
+ if (x > r.width - yAxisWidth) {
279
+ const priceBottom = r.height - xAxisHeight - indicatorHeight;
280
+ if (indicatorHeight > 0 && y > priceBottom && y < r.height - xAxisHeight) {
281
+ return "indicator-axis";
282
+ }
283
+ return "price-axis";
284
+ }
285
+ const sepY = r.height - xAxisHeight - indicatorHeight;
286
+ if (indicatorHeight > 0 && x <= r.width - yAxisWidth && Math.abs(y - sepY) <= SEP_HIT) {
287
+ return "separator";
288
+ }
153
289
  if (y > r.height - xAxisHeight) return "time-axis";
154
290
  if (indicatorHeight > 0 && y > r.height - xAxisHeight - indicatorHeight) return "indicator";
155
291
  return "chart";
@@ -193,6 +329,56 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
193
329
  scheduleRender();
194
330
  reportCrosshair("hide");
195
331
  };
332
+ const drawActive = () => optsRef.current.mode === "draw" && optsRef.current.tool === "line";
333
+ const updateGuideline = (x, y) => {
334
+ const h = handleRef.current;
335
+ if (!h) return;
336
+ if (!drawAnchorRef.current || drawSelectedPxRef.current) return;
337
+ const c = h.coordAt(x, y);
338
+ if (!c) return;
339
+ const a = drawAnchorRef.current;
340
+ h.setDraft(a.timeMs, a.price, true, c.timeMs, c.price, true, DRAW_COLOR, DRAW_WIDTH);
341
+ scheduleRender();
342
+ };
343
+ const handleDrawClick = (x, y) => {
344
+ const h = handleRef.current;
345
+ if (!h) return;
346
+ const o = optsRef.current;
347
+ const sel = drawSelectedPxRef.current;
348
+ if (sel) {
349
+ if (distToSegment(x, y, sel.ax, sel.ay, sel.bx, sel.by) <= DRAW_HIT) return;
350
+ drawAnchorRef.current = null;
351
+ drawAnchorPxRef.current = null;
352
+ drawSelectedPxRef.current = null;
353
+ h.clearDraft();
354
+ scheduleRender();
355
+ el.style.cursor = "";
356
+ o.onRequestMode?.("pan");
357
+ return;
358
+ }
359
+ const coord = h.coordAt(x, y);
360
+ if (!coord) return;
361
+ if (!drawAnchorRef.current) {
362
+ drawAnchorRef.current = coord;
363
+ drawAnchorPxRef.current = { x, y };
364
+ h.setDraft(coord.timeMs, coord.price, false, 0, 0, true, DRAW_COLOR, DRAW_WIDTH);
365
+ scheduleRender();
366
+ } else {
367
+ const a = drawAnchorRef.current;
368
+ const apx = drawAnchorPxRef.current;
369
+ o.onDrawingComplete?.({
370
+ id: drawingId(),
371
+ type: "line",
372
+ points: [
373
+ { timeMs: a.timeMs, price: a.price },
374
+ { timeMs: coord.timeMs, price: coord.price }
375
+ ]
376
+ });
377
+ drawSelectedPxRef.current = { ax: apx.x, ay: apx.y, bx: x, by: y };
378
+ h.setDraft(a.timeMs, a.price, true, coord.timeMs, coord.price, false, DRAW_COLOR, DRAW_WIDTH);
379
+ scheduleRender();
380
+ }
381
+ };
196
382
  const onPointerDown = (e) => {
197
383
  const h = handleRef.current;
198
384
  if (!h) return;
@@ -219,7 +405,7 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
219
405
  downY = y;
220
406
  moved = false;
221
407
  panMode = regionAt(x, y);
222
- if (e.pointerType !== "mouse" && panMode === "chart") {
408
+ if (e.pointerType !== "mouse" && panMode === "chart" && !drawActive()) {
223
409
  longPressTimer = setTimeout(() => {
224
410
  longPressTimer = null;
225
411
  if (!moved) showCrosshair(downX, downY, "press", "show");
@@ -232,7 +418,14 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
232
418
  const { x, y } = rel(e);
233
419
  if (!pointers.has(e.pointerId)) {
234
420
  if (e.pointerType === "mouse" && pointers.size === 0) {
235
- if (regionAt(x, y) === "chart") {
421
+ if (drawActive()) {
422
+ el.style.cursor = "crosshair";
423
+ updateGuideline(x, y);
424
+ return;
425
+ }
426
+ const region = regionAt(x, y);
427
+ el.style.cursor = region === "separator" ? "row-resize" : region === "indicator-axis" ? "ns-resize" : "";
428
+ if (region === "chart") {
236
429
  showCrosshair(x, y, "hover", crosshairActive ? "move" : "show");
237
430
  } else {
238
431
  hideCrosshair();
@@ -244,7 +437,7 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
244
437
  const dx = x - prev.x;
245
438
  const dy = y - prev.y;
246
439
  pointers.set(e.pointerId, { x, y });
247
- if (pointers.size >= 2 && pinch.active) {
440
+ if (pointers.size >= 2 && pinch.active && !drawActive()) {
248
441
  const pts = [...pointers.values()];
249
442
  const focalX = (pts[0].x + pts[1].x) / 2;
250
443
  const focalY = (pts[0].y + pts[1].y) / 2;
@@ -271,12 +464,18 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
271
464
  clearLongPress();
272
465
  }
273
466
  if (!moved) return;
467
+ if (drawActive()) {
468
+ if (panMode === "chart") updateGuideline(x, y);
469
+ return;
470
+ }
274
471
  if (crosshairActive && crosshairSource === "press" && panMode === "chart") {
275
472
  showCrosshair(x, y, "press", "move");
276
473
  return;
277
474
  }
278
475
  if (panMode === "price-axis") h.scalePriceAxis(dy);
279
476
  else if (panMode === "time-axis") h.scaleTimeAxis(dx);
477
+ else if (panMode === "separator") h.resizeIndicatorPane(dy);
478
+ else if (panMode === "indicator-axis") h.scaleIndicatorAxis(downY, dy);
280
479
  else if (panMode === "indicator") h.pan(dx, 0);
281
480
  else h.translate(dx, dy);
282
481
  if (crosshairActive && crosshairSource === "hover") {
@@ -290,19 +489,27 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
290
489
  clearLongPress();
291
490
  if (pointers.size < 2) pinch.active = false;
292
491
  if (!had) return;
492
+ if (drawActive() && pointers.size === 0) {
493
+ if (!moved && panMode === "chart") handleDrawClick(downX, downY);
494
+ else el.style.cursor = "crosshair";
495
+ return;
496
+ }
293
497
  if (crosshairSource === "press" && pointers.size === 0) {
294
498
  hideCrosshair();
295
499
  } else if (moved && (panMode === "chart" || panMode === "indicator")) {
296
500
  optsRef.current.onViewportChange?.(0, 0);
297
501
  }
502
+ if (pointers.size === 0) el.style.cursor = "";
298
503
  };
299
504
  const onPointerLeave = () => {
300
505
  if (crosshairSource === "hover") hideCrosshair();
506
+ el.style.cursor = "";
301
507
  };
302
508
  const onWheel = (e) => {
303
509
  const h = handleRef.current;
304
510
  if (!h) return;
305
511
  e.preventDefault();
512
+ if (drawActive()) return;
306
513
  const { x, y } = rel(e);
307
514
  if (e.ctrlKey || e.metaKey) {
308
515
  const f = Math.exp(-e.deltaY * WHEEL_K);
@@ -346,15 +553,30 @@ var CANVAS_STYLE = {
346
553
  // we own pan/zoom; don't let the browser scroll/zoom
347
554
  };
348
555
  function VroomChart(props) {
349
- const { className, style, wasm, crosshairOffset = 40, onCrosshair, onViewportChange } = props;
556
+ const {
557
+ className,
558
+ style,
559
+ wasm,
560
+ crosshairOffset = 40,
561
+ mode,
562
+ tool,
563
+ onCrosshair,
564
+ onViewportChange,
565
+ onDrawingComplete,
566
+ onModeChange
567
+ } = props;
350
568
  const { containerRef, canvasRef, handleRef, scheduleRender } = useChartCore(
351
569
  props,
352
570
  wasm ? { wasm } : void 0
353
571
  );
354
572
  useGestures(containerRef, handleRef, scheduleRender, {
355
573
  crosshairOffset,
574
+ mode,
575
+ tool,
356
576
  onCrosshair,
357
- onViewportChange
577
+ onViewportChange,
578
+ onDrawingComplete,
579
+ onRequestMode: onModeChange
358
580
  });
359
581
  const rootStyle = {
360
582
  ...FILL,
@@ -365,6 +587,9 @@ function VroomChart(props) {
365
587
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: rootStyle, children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: CANVAS_STYLE }) });
366
588
  }
367
589
  export {
368
- VroomChart
590
+ VroomChart,
591
+ classifyTransition,
592
+ inferStepMs,
593
+ timeframeWindow
369
594
  };
370
595
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useChartCore.ts","../src/useGestures.ts","../src/VroomChart.tsx"],"sourcesContent":["// Owns one VroomChartHandle bound to a <canvas>: loads the core, measures the\n// container (ResizeObserver), pushes data/size/theme/indicators on change, and\n// drives a rAF-batched present() loop. The web analogue of\n// packages/react-native/src/useChartCore.ts — but it paints the canvas via\n// handle.present() instead of producing an SkPicture.\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n loadVroom,\n packCandles,\n applyTheme,\n parseColor,\n type LoadVroomOptions,\n type OverlaySpec,\n type VroomChartHandle,\n} from '@vroomchart/core-wasm';\nimport type { VroomChartCoreProps } from '@vroomchart/types';\n\n// Mirrors vroom::ma::Source order (packages/core/src/ma.h).\nconst MA_SOURCES = ['close', 'open', 'high', 'low', 'hl2', 'hlc3', 'ohlc4'] as const;\n\nfunction overlayToNumeric(\n o: NonNullable<VroomChartCoreProps['movingAverages']>[number],\n): OverlaySpec {\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\nexport type UseChartCore = {\n containerRef: React.RefObject<HTMLDivElement | null>;\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n handleRef: React.RefObject<VroomChartHandle | null>;\n /** Schedules a rAF-batched repaint (and keeps ticking while animating). */\n scheduleRender: () => void;\n /** Measured CSS size of the container (0 until first layout). */\n size: { width: number; height: number };\n};\n\nexport function useChartCore(\n props: VroomChartCoreProps,\n loadOpts?: LoadVroomOptions,\n): UseChartCore {\n const {\n candles,\n width: widthProp,\n height: heightProp,\n visibleRange,\n theme,\n rsi,\n macd,\n movingAverages,\n vwap,\n } = props;\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const handleRef = useRef<VroomChartHandle | null>(null);\n const rafRef = useRef<number | null>(null);\n const [ready, setReady] = useState(false);\n const [measured, setMeasured] = useState({ width: 0, height: 0 });\n\n // Captured at mount: which core to load (stub vs Skia-WASM). Changing it after\n // mount has no effect — the core is created once and shared process-wide.\n const loadOptsRef = useRef(loadOpts);\n loadOptsRef.current = loadOpts;\n\n const width = widthProp ?? measured.width;\n const height = heightProp ?? measured.height;\n\n const scheduleRender = useCallback(() => {\n if (rafRef.current != null) return;\n rafRef.current = requestAnimationFrame(() => {\n rafRef.current = null;\n const h = handleRef.current;\n if (!h) return;\n h.present();\n if (h.isAnimating()) scheduleRender();\n });\n }, []);\n\n // Create the handle once the canvas exists; destroy on unmount.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n let disposed = false;\n loadVroom(loadOptsRef.current).then((mod) => {\n if (disposed) return;\n handleRef.current = mod.create(canvas);\n setReady(true);\n });\n return () => {\n disposed = true;\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n handleRef.current?.destroy();\n handleRef.current = null;\n setReady(false);\n };\n }, []);\n\n // Measure the container (unless both dims are pinned via props).\n useEffect(() => {\n const el = containerRef.current;\n if (!el || (widthProp != null && heightProp != null)) return;\n const ro = new ResizeObserver((entries) => {\n const r = entries[0]?.contentRect;\n if (!r) return;\n const w = Math.round(r.width);\n const h = Math.round(r.height);\n setMeasured((prev) => (prev.width === w && prev.height === h ? prev : { width: w, height: h }));\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [widthProp, heightProp]);\n\n // Stable deps so inline object/array literals don't re-run every render.\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 const explicit = visibleRange != null;\n const startMs = visibleRange?.startMs ?? 0;\n const endMs = visibleRange?.endMs ?? 0;\n\n // Push everything into the core whenever data/size/config changes, then paint.\n useEffect(() => {\n const h = handleRef.current;\n if (!h || width <= 0 || height <= 0) return;\n const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;\n h.setSize(width, height, dpr);\n if (candles.length > 0) h.setCandles(packCandles(candles));\n if (explicit) h.setVisibleRange(startMs, endMs);\n if (theme) applyTheme(h, theme);\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(macd?.enabled ?? false, macd?.fast ?? 12, macd?.slow ?? 26, macd?.signal ?? 9);\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 scheduleRender();\n // theme/rsi/macd/movingAverages/vwap tracked via their *Key deps.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ready, width, height, candles, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey, scheduleRender]);\n\n return { containerRef, canvasRef, handleRef, scheduleRender, size: { width, height } };\n}\n","// Pointer/wheel gesture controller for the web chart. Maps input to the same\n// VroomChartHandle mutators the React Native gestures use (see\n// packages/react-native/src/VroomChart.tsx), so behavior matches across\n// platforms. Listeners are attached natively (not via React props) so wheel can\n// be non-passive and call preventDefault.\n\nimport { useEffect, useRef } from 'react';\nimport type { CrosshairEvent } from '@vroomchart/types';\nimport type { VroomChartHandle } from '@vroomchart/core-wasm';\n\ntype Region = 'chart' | 'price-axis' | 'time-axis' | 'indicator';\n\nexport type GestureOptions = {\n crosshairOffset: number;\n onCrosshair?: (e: CrosshairEvent) => void;\n onViewportChange?: (startMs: number, endMs: number) => void;\n};\n\nconst MIN_SPAN = 24; // px — minimum two-finger span for an axis to scale\nconst AXIS_RATIO = 0.5; // an axis scales only if its span ≥ this × the other's\nconst LONG_PRESS_MS = 350;\nconst MOVE_THRESH = 6; // px before a press becomes a drag\nconst WHEEL_K = 0.0015; // wheel delta → zoom factor exponent\n\nexport function useGestures(\n containerRef: React.RefObject<HTMLElement | null>,\n handleRef: React.RefObject<VroomChartHandle | null>,\n scheduleRender: () => void,\n opts: GestureOptions,\n): void {\n // Keep latest opts in a ref so the effect's listeners stay stable.\n const optsRef = useRef(opts);\n optsRef.current = opts;\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const pointers = new Map<number, { x: number; y: number }>();\n let panMode: Region = 'chart';\n let crosshairActive = false;\n let crosshairSource: 'press' | 'hover' | null = null;\n let lastCrosshairTime: number | null = null;\n let longPressTimer: ReturnType<typeof setTimeout> | null = null;\n let downX = 0;\n let downY = 0;\n let moved = false;\n const pinch = { spanX: 1, spanY: 1, ratioX: 1, ratioY: 1, enableX: false, enableY: false, active: false };\n\n const rel = (e: PointerEvent | WheelEvent) => {\n const r = el.getBoundingClientRect();\n return { x: e.clientX - r.left, y: e.clientY - r.top };\n };\n\n const regionAt = (x: number, y: number): Region => {\n const h = handleRef.current;\n const r = el.getBoundingClientRect();\n if (!h) return 'chart';\n const { yAxisWidth, xAxisHeight, indicatorHeight } = h.getAxisMetrics();\n if (x > r.width - yAxisWidth) return 'price-axis';\n if (y > r.height - xAxisHeight) return 'time-axis';\n if (indicatorHeight > 0 && y > r.height - xAxisHeight - indicatorHeight) return 'indicator';\n return 'chart';\n };\n\n const reportCrosshair = (reason: CrosshairEvent['reason']) => {\n const h = handleRef.current;\n if (!h) return;\n // Snapped slot — has a timeMs even in the empty space ahead of the last\n // candle, where `candle` is null.\n const info = h.getCrosshairInfo();\n const t = info?.timeMs ?? null;\n if (reason === 'move' && t === lastCrosshairTime) return;\n lastCrosshairTime = t;\n optsRef.current.onCrosshair?.({\n active: reason !== 'hide',\n candle: info?.candle ?? null,\n timeMs: t,\n reason,\n });\n };\n\n const clearLongPress = () => {\n if (longPressTimer != null) {\n clearTimeout(longPressTimer);\n longPressTimer = null;\n }\n };\n\n const showCrosshair = (x: number, y: number, source: 'press' | 'hover', reason: 'show' | 'move') => {\n const h = handleRef.current;\n if (!h) return;\n crosshairActive = true;\n crosshairSource = source;\n const lift = source === 'press' ? optsRef.current.crosshairOffset : 0;\n h.setCrosshair(x, y - lift);\n scheduleRender();\n reportCrosshair(reason);\n };\n\n const hideCrosshair = () => {\n const h = handleRef.current;\n if (!h || !crosshairActive) return;\n crosshairActive = false;\n crosshairSource = null;\n h.clearCrosshair();\n scheduleRender();\n reportCrosshair('hide');\n };\n\n const onPointerDown = (e: PointerEvent) => {\n const h = handleRef.current;\n if (!h) return;\n if (e.pointerType === 'mouse' && e.button !== 0) return; // left button only\n const { x, y } = rel(e);\n el.setPointerCapture(e.pointerId);\n pointers.set(e.pointerId, { x, y });\n\n if (pointers.size === 2) {\n clearLongPress();\n hideCrosshair();\n const pts = [...pointers.values()];\n const sx = Math.abs(pts[0]!.x - pts[1]!.x);\n const sy = Math.abs(pts[0]!.y - pts[1]!.y);\n pinch.spanX = sx;\n pinch.spanY = sy;\n pinch.ratioX = 1;\n pinch.ratioY = 1;\n pinch.enableX = sx >= MIN_SPAN && sx >= sy * AXIS_RATIO;\n pinch.enableY = sy >= MIN_SPAN && sy >= sx * AXIS_RATIO;\n pinch.active = true;\n return;\n }\n\n // Single pointer: classify region; arm long-press → crosshair (touch/pen).\n downX = x;\n downY = y;\n moved = false;\n panMode = regionAt(x, y);\n if (e.pointerType !== 'mouse' && panMode === 'chart') {\n longPressTimer = setTimeout(() => {\n longPressTimer = null;\n if (!moved) showCrosshair(downX, downY, 'press', 'show');\n }, LONG_PRESS_MS);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const h = handleRef.current;\n if (!h) return;\n const { x, y } = rel(e);\n\n // Hover (mouse, no button) → crosshair follows the cursor on the chart.\n if (!pointers.has(e.pointerId)) {\n if (e.pointerType === 'mouse' && pointers.size === 0) {\n if (regionAt(x, y) === 'chart') {\n showCrosshair(x, y, 'hover', crosshairActive ? 'move' : 'show');\n } else {\n hideCrosshair();\n }\n }\n return;\n }\n\n const prev = pointers.get(e.pointerId)!;\n const dx = x - prev.x;\n const dy = y - prev.y;\n pointers.set(e.pointerId, { x, y });\n\n // Two-finger directional pinch.\n if (pointers.size >= 2 && pinch.active) {\n const pts = [...pointers.values()];\n const focalX = (pts[0]!.x + pts[1]!.x) / 2;\n const focalY = (pts[0]!.y + pts[1]!.y) / 2;\n let frameX = 1;\n if (pinch.enableX) {\n const r = Math.max(Math.abs(pts[0]!.x - pts[1]!.x), MIN_SPAN) / pinch.spanX;\n frameX = r / pinch.ratioX;\n pinch.ratioX = r;\n }\n let frameY = 1;\n if (pinch.enableY) {\n const r = Math.max(Math.abs(pts[0]!.y - pts[1]!.y), MIN_SPAN) / pinch.spanY;\n frameY = r / pinch.ratioY;\n pinch.ratioY = r;\n }\n if (frameX !== 1 || frameY !== 1) {\n h.zoom(frameX, frameY, focalX, focalY);\n scheduleRender();\n }\n return;\n }\n\n if (!moved && Math.hypot(x - downX, y - downY) > MOVE_THRESH) {\n moved = true;\n clearLongPress();\n }\n if (!moved) return;\n\n // Chart-area drag with the (press) crosshair up moves the crosshair.\n if (crosshairActive && crosshairSource === 'press' && panMode === 'chart') {\n showCrosshair(x, y, 'press', 'move');\n return;\n }\n\n if (panMode === 'price-axis') h.scalePriceAxis(dy);\n else if (panMode === 'time-axis') h.scaleTimeAxis(dx);\n else if (panMode === 'indicator') h.pan(dx, 0);\n else h.translate(dx, dy);\n\n // Keep a hover crosshair glued to the cursor while dragging — the chart\n // pans under it, so re-snap to whatever candle is now beneath the cursor.\n if (crosshairActive && crosshairSource === 'hover') {\n h.setCrosshair(x, y);\n reportCrosshair('move');\n }\n scheduleRender();\n };\n\n const endPointer = (e: PointerEvent) => {\n const had = pointers.delete(e.pointerId);\n clearLongPress();\n if (pointers.size < 2) pinch.active = false;\n if (!had) return;\n\n // Press crosshair is active only while held — release dismisses it.\n if (crosshairSource === 'press' && pointers.size === 0) {\n hideCrosshair();\n } else if (moved && (panMode === 'chart' || panMode === 'indicator')) {\n optsRef.current.onViewportChange?.(0, 0);\n }\n };\n\n const onPointerLeave = () => {\n if (crosshairSource === 'hover') hideCrosshair();\n };\n\n const onWheel = (e: WheelEvent) => {\n const h = handleRef.current;\n if (!h) return;\n e.preventDefault();\n const { x, y } = rel(e);\n if (e.ctrlKey || e.metaKey) {\n // Trackpad pinch (sent as ctrl+wheel) / ctrl+wheel → zoom both axes.\n const f = Math.exp(-e.deltaY * WHEEL_K);\n h.zoom(f, f, x, y);\n } else if (e.shiftKey) {\n // Shift+wheel → horizontal pan (mouse-wheel-only users).\n h.pan(-(e.deltaX || e.deltaY), 0);\n } else if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {\n // Horizontal scroll → pan the time axis (scroll right = forward in time).\n h.pan(-e.deltaX, 0);\n } else {\n // Vertical scroll → zoom the time window around the cursor.\n const f = Math.exp(-e.deltaY * WHEEL_K);\n h.zoom(f, 1, x, y);\n }\n scheduleRender();\n };\n\n el.addEventListener('pointerdown', onPointerDown);\n el.addEventListener('pointermove', onPointerMove);\n el.addEventListener('pointerup', endPointer);\n el.addEventListener('pointercancel', endPointer);\n el.addEventListener('pointerleave', onPointerLeave);\n el.addEventListener('wheel', onWheel, { passive: false });\n\n return () => {\n clearLongPress();\n el.removeEventListener('pointerdown', onPointerDown);\n el.removeEventListener('pointermove', onPointerMove);\n el.removeEventListener('pointerup', endPointer);\n el.removeEventListener('pointercancel', endPointer);\n el.removeEventListener('pointerleave', onPointerLeave);\n el.removeEventListener('wheel', onWheel);\n };\n }, [containerRef, handleRef, scheduleRender]);\n}\n","// VroomChart (web) — DOM React component that renders the candlestick chart to\n// a <canvas> via @vroomchart/core-wasm and wires pointer/wheel gestures. API-matched\n// to the React Native component (same props from @vroomchart/types).\n\nimport React from 'react';\nimport type { CSSProperties } from 'react';\n\nimport { useChartCore } from './useChartCore';\nimport { useGestures } from './useGestures';\nimport type { VroomChartProps } from './types';\n\nconst FILL: CSSProperties = { position: 'relative', width: '100%', height: '100%' };\nconst CANVAS_STYLE: CSSProperties = {\n display: 'block',\n width: '100%',\n height: '100%',\n touchAction: 'none', // we own pan/zoom; don't let the browser scroll/zoom\n};\n\n/**\n * Skia-quality candlestick chart for the web. Pass OHLCV `candles` and size it\n * via `style`/`width`/`height` (it fills its parent by default). Drag to pan,\n * pinch or wheel to zoom, drag the price/time axes to rescale, hover (mouse) or\n * long-press (touch) for the crosshair. Optional indicators (`rsi`, `macd`,\n * `movingAverages`, `vwap`), colors (`theme`), and events (`onCrosshair`,\n * `onViewportChange`) are configured through props.\n *\n * @see {@link VroomChartProps} for the full prop reference.\n */\nexport function VroomChart(props: VroomChartProps) {\n const { className, style, wasm, crosshairOffset = 40, onCrosshair, onViewportChange } = props;\n const { containerRef, canvasRef, handleRef, scheduleRender } = useChartCore(\n props,\n wasm ? { wasm } : undefined,\n );\n\n useGestures(containerRef, handleRef, scheduleRender, {\n crosshairOffset,\n onCrosshair,\n onViewportChange,\n });\n\n const rootStyle: CSSProperties = {\n ...FILL,\n ...(props.width != null ? { width: props.width } : null),\n ...(props.height != null ? { height: props.height } : null),\n ...style,\n };\n\n return (\n <div ref={containerRef} className={className} style={rootStyle}>\n <canvas ref={canvasRef} style={CANVAS_STYLE} />\n </div>\n );\n}\n"],"mappings":"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","names":["useEffect","useRef"]}
1
+ {"version":3,"sources":["../src/useChartCore.ts","../src/dataTransitions.ts","../src/useGestures.ts","../src/VroomChart.tsx"],"sourcesContent":["// Owns one VroomChartHandle bound to a <canvas>: loads the core, measures the\n// container (ResizeObserver), pushes data/size/theme/indicators on change, and\n// drives a rAF-batched present() loop. The web analogue of\n// packages/react-native/src/useChartCore.ts — but it paints the canvas via\n// handle.present() instead of producing an SkPicture.\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n loadVroom,\n packCandles,\n applyTheme,\n parseColor,\n type LoadVroomOptions,\n type OverlaySpec,\n type DrawingSpec,\n type VroomChartHandle,\n} from '@vroomchart/core-wasm';\nimport type { VroomChartCoreProps } from '@vroomchart/types';\nimport {\n classifyTransition,\n inferStepMs,\n timeframeWindow,\n type DataTransition,\n} from './dataTransitions';\n\n// Mirrors vroom::ma::Source order (packages/core/src/ma.h).\nconst MA_SOURCES = ['close', 'open', 'high', 'low', 'hl2', 'hlc3', 'ohlc4'] as const;\n\nfunction overlayToNumeric(\n o: NonNullable<VroomChartCoreProps['movingAverages']>[number],\n): OverlaySpec {\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\nfunction drawingToSpec(\n d: NonNullable<VroomChartCoreProps['drawings']>[number],\n): DrawingSpec {\n return {\n aTime: d.points[0].timeMs,\n aPrice: d.points[0].price,\n bTime: d.points[1].timeMs,\n bPrice: d.points[1].price,\n color: (d.color != null ? parseColor(d.color) : null) ?? 0xff2962ff,\n width: d.width ?? 2,\n };\n}\n\nexport type UseChartCore = {\n containerRef: React.RefObject<HTMLDivElement | null>;\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n handleRef: React.RefObject<VroomChartHandle | null>;\n /** Schedules a rAF-batched repaint (and keeps ticking while animating). */\n scheduleRender: () => void;\n /** Measured CSS size of the container (0 until first layout). */\n size: { width: number; height: number };\n};\n\nexport function useChartCore(\n props: VroomChartCoreProps,\n loadOpts?: LoadVroomOptions,\n): UseChartCore {\n const {\n candles,\n seriesKey,\n width: widthProp,\n height: heightProp,\n visibleRange,\n theme,\n rsi,\n macd,\n movingAverages,\n vwap,\n drawings,\n } = props;\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const handleRef = useRef<VroomChartHandle | null>(null);\n // What the core currently holds, for classifying the next data change.\n // Keyed by handle so a recreated core is treated as a fresh initial load.\n const prevDataRef = useRef<{\n handle: VroomChartHandle;\n candles: VroomChartCoreProps['candles'];\n seriesKey?: string;\n } | null>(null);\n const rafRef = useRef<number | null>(null);\n const [ready, setReady] = useState(false);\n const [measured, setMeasured] = useState({ width: 0, height: 0 });\n\n // Captured at mount: which core to load (stub vs Skia-WASM). Changing it after\n // mount has no effect — the core is created once and shared process-wide.\n const loadOptsRef = useRef(loadOpts);\n loadOptsRef.current = loadOpts;\n\n const width = widthProp ?? measured.width;\n const height = heightProp ?? measured.height;\n\n const scheduleRender = useCallback(() => {\n if (rafRef.current != null) return;\n rafRef.current = requestAnimationFrame(() => {\n rafRef.current = null;\n const h = handleRef.current;\n if (!h) return;\n h.present();\n if (h.isAnimating()) scheduleRender();\n });\n }, []);\n\n // Create the handle once the canvas exists; destroy on unmount.\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n let disposed = false;\n loadVroom(loadOptsRef.current).then((mod) => {\n if (disposed) return;\n handleRef.current = mod.create(canvas);\n setReady(true);\n });\n return () => {\n disposed = true;\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n handleRef.current?.destroy();\n handleRef.current = null;\n setReady(false);\n };\n }, []);\n\n // Measure the container (unless both dims are pinned via props).\n useEffect(() => {\n const el = containerRef.current;\n if (!el || (widthProp != null && heightProp != null)) return;\n const ro = new ResizeObserver((entries) => {\n const r = entries[0]?.contentRect;\n if (!r) return;\n const w = Math.round(r.width);\n const h = Math.round(r.height);\n setMeasured((prev) => (prev.width === w && prev.height === h ? prev : { width: w, height: h }));\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, [widthProp, heightProp]);\n\n // Stable deps so inline object/array literals don't re-run every render.\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 const drawingsKey = drawings ? JSON.stringify(drawings) : '';\n const explicit = visibleRange != null;\n const startMs = visibleRange?.startMs ?? 0;\n const endMs = visibleRange?.endMs ?? 0;\n\n // Push everything into the core whenever data/size/config changes, then paint.\n useEffect(() => {\n const h = handleRef.current;\n if (!h || width <= 0 || height <= 0) return;\n const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;\n h.setSize(width, height, dpr);\n if (candles.length > 0) {\n const prev = prevDataRef.current;\n const freshHandle = prev == null || prev.handle !== h;\n if (freshHandle || prev.candles !== candles || prev.seriesKey !== seriesKey) {\n // A fresh core frames itself (its window starts at 0/0); an explicit\n // visibleRange prop overrides any auto behavior, so treat the change\n // like a stream and let the range application below win.\n const transition: DataTransition = freshHandle\n ? 'initial'\n : explicit\n ? 'stream'\n : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);\n\n // Capture the outgoing view before setCandles re-infers the candle\n // period from the new data.\n let tfArgs: { oldWindow: { startMs: number; endMs: number }; oldStepMs: number; oldLastMs: number } | null =\n null;\n if (transition === 'timeframe' && prev != null) {\n const oldWindow = h.getVisibleRange();\n const oldStepMs = inferStepMs(prev.candles);\n if (oldWindow.endMs > oldWindow.startMs && oldStepMs != null) {\n tfArgs = { oldWindow, oldStepMs, oldLastMs: prev.candles[prev.candles.length - 1].timeMs };\n }\n }\n\n h.setCandles(packCandles(candles));\n\n if (transition === 'timeframe') {\n const newStepMs = inferStepMs(candles);\n if (tfArgs && newStepMs != null) {\n const w = timeframeWindow(\n tfArgs.oldWindow,\n tfArgs.oldStepMs,\n tfArgs.oldLastMs,\n newStepMs,\n candles[candles.length - 1].timeMs,\n );\n h.setVisibleRange(w.startMs, w.endMs);\n }\n h.resetPriceScale();\n } else if (transition === 'reset') {\n h.resetView();\n }\n prevDataRef.current = { handle: h, candles, seriesKey };\n }\n }\n if (explicit) h.setVisibleRange(startMs, endMs);\n if (theme) applyTheme(h, theme);\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(macd?.enabled ?? false, macd?.fast ?? 12, macd?.slow ?? 26, macd?.signal ?? 9);\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 h.setDrawings((drawings ?? []).map(drawingToSpec));\n scheduleRender();\n // theme/rsi/macd/movingAverages/vwap/drawings tracked via their *Key deps.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ready, width, height, candles, seriesKey, explicit, startMs, endMs, themeKey, rsiKey, macdKey, maKey, vwapKey, drawingsKey, scheduleRender]);\n\n return { containerRef, canvasRef, handleRef, scheduleRender, size: { width, height } };\n}\n","// Classifies how a new `candles` prop relates to the previous one so the chart\n// can react appropriately: leave the viewport alone for streaming updates,\n// re-anchor the time window for a timeframe switch, or fully reset the view\n// for a different asset. Pure functions, no React — see useChartCore for the\n// orchestration.\n\nimport type { Candle, VisibleRange } from '@vroomchart/types';\n\nexport type DataTransition = 'initial' | 'stream' | 'timeframe' | 'reset';\n\n// A step change below this ratio is treated as the same timeframe. Real steps\n// are exact integer ms; the tolerance only absorbs rounding/DST quirks (the\n// smallest real timeframe jump, 1m -> 2m, is 100% apart).\nconst STEP_TOLERANCE = 0.01;\n\n// Same-asset check for a timeframe switch: both series end \"now\", so their\n// last closes must be close. No asset moves 25% between two consecutive prop\n// pushes; distinct assets within 25% of each other are what `seriesKey` is for.\nconst MAX_SAME_ASSET_CLOSE_RATIO = 1.25;\n\n// A coarser bucketing can shift the final bar's open by up to one coarse bar;\n// allow that plus an in-flight bar when checking the two series end together.\nconst MAX_END_DRIFT_STEPS = 3;\n\n// Streaming pushes may batch a few bars (e.g. a throttled background tab), but\n// a jump of more than this many steps means the data was re-fetched elsewhere.\nconst MAX_STREAM_ADVANCE_STEPS = 5;\n\n/**\n * The candle period in ms, inferred as the median of the first few intervals\n * (robust to a single gap). Null when there are fewer than two candles.\n */\nexport function inferStepMs(candles: Candle[]): number | null {\n if (candles.length < 2) return null;\n const k = Math.min(candles.length - 1, 8);\n const diffs: number[] = [];\n for (let i = 0; i < k; i++) diffs.push(candles[i + 1].timeMs - candles[i].timeMs);\n diffs.sort((a, b) => a - b);\n const median = diffs[Math.floor(diffs.length / 2)];\n return median > 0 ? median : null;\n}\n\n/**\n * Classify a candles-prop change. `prev` is the previously rendered array\n * (null on first render); `seriesKeyChanged` forces `reset` regardless of the\n * data (the explicit escape hatch).\n *\n * Constraint: detection compares two immutable snapshots. An array mutated in\n * place (same reference) never reaches this code — React props must change\n * identity to re-render.\n */\nexport function classifyTransition(\n prev: Candle[] | null,\n next: Candle[],\n seriesKeyChanged: boolean,\n): DataTransition {\n if (!prev || prev.length === 0) return 'initial';\n if (next.length === 0) return 'stream'; // nothing to reframe against\n if (seriesKeyChanged) return 'reset';\n\n const prevStep = inferStepMs(prev);\n const nextStep = inferStepMs(next);\n if (prevStep == null || nextStep == null) return 'reset'; // too little data to reason\n\n const prevLast = prev[prev.length - 1];\n const nextLast = next[next.length - 1];\n\n if (Math.abs(nextStep - prevStep) <= prevStep * STEP_TOLERANCE) {\n // Same step: streaming iff prev's last bar sits at its expected slot in\n // next (O(1) for a uniform series — covers append, update-last, and\n // rolling buffers that drop old bars from the front) and the series only\n // advanced by a few bars. Time alignment alone isn't enough: two assets\n // on the same exchange share the bar grid, so the bar at the shared\n // timestamp must also be (nearly) the same bar — update-last moves the\n // close, but never by the same-asset ratio.\n const idx = Math.round((prevLast.timeMs - next[0].timeMs) / nextStep);\n const aligned = idx >= 0 && idx < next.length && next[idx].timeMs === prevLast.timeMs;\n const sharedBarRatio =\n aligned && next[idx].close > 0 && prevLast.close > 0\n ? Math.max(next[idx].close / prevLast.close, prevLast.close / next[idx].close)\n : Infinity;\n const advanced =\n nextLast.timeMs >= prevLast.timeMs &&\n nextLast.timeMs - prevLast.timeMs <= MAX_STREAM_ADVANCE_STEPS * nextStep;\n return sharedBarRatio <= MAX_SAME_ASSET_CLOSE_RATIO && advanced ? 'stream' : 'reset';\n }\n\n // Step changed: a timeframe switch iff it still looks like the same asset —\n // last closes near each other and both series ending around the same time.\n const closeRatio =\n prevLast.close > 0 && nextLast.close > 0\n ? Math.max(nextLast.close / prevLast.close, prevLast.close / nextLast.close)\n : Infinity;\n const prevEnd = prevLast.timeMs + prevStep;\n const nextEnd = nextLast.timeMs + nextStep;\n const endsTogether =\n Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);\n return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? 'timeframe' : 'reset';\n}\n\n/**\n * The visible window to apply after a timeframe switch so each candle keeps\n * the exact pixel width it had before: the visible slot count is preserved and\n * the right edge re-anchors on the newest candle (any future-gap overshoot is\n * carried over in slots, clamped to the core's 3/4-window cap). The new start\n * may precede the first candle — that gap is intentional, width wins.\n */\nexport function timeframeWindow(\n oldWindow: VisibleRange,\n oldStepMs: number,\n oldLastMs: number,\n newStepMs: number,\n newLastMs: number,\n): VisibleRange {\n const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;\n const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;\n const offsetSlots = Math.min(Math.max(offsetRaw, 0), slots * 0.75);\n const endMs = Math.round(newLastMs + offsetSlots * newStepMs);\n return { startMs: Math.round(endMs - slots * newStepMs), endMs };\n}\n","// Pointer/wheel gesture controller for the web chart. Maps input to the same\n// VroomChartHandle mutators the React Native gestures use (see\n// packages/react-native/src/VroomChart.tsx), so behavior matches across\n// platforms. Listeners are attached natively (not via React props) so wheel can\n// be non-passive and call preventDefault.\n\nimport { useEffect, useRef } from 'react';\nimport type { ChartMode, CrosshairEvent, Drawing, DrawTool } from '@vroomchart/types';\nimport type { VroomChartHandle } from '@vroomchart/core-wasm';\n\ntype Region = 'chart' | 'price-axis' | 'time-axis' | 'indicator' | 'separator' | 'indicator-axis';\n\nexport type GestureOptions = {\n crosshairOffset: number;\n /** Interaction mode. In 'draw' mode panning/zooming/crosshair are suppressed. */\n mode?: ChartMode;\n /** Active drawing tool while in 'draw' mode. */\n tool?: DrawTool;\n onCrosshair?: (e: CrosshairEvent) => void;\n onViewportChange?: (startMs: number, endMs: number) => void;\n /** Fired with the finished line when the user places its second point. */\n onDrawingComplete?: (drawing: Drawing) => void;\n /** Fired when the chart wants the host to change mode (e.g. exit on click-away). */\n onRequestMode?: (mode: ChartMode) => void;\n};\n\nconst MIN_SPAN = 24; // px — minimum two-finger span for an axis to scale\nconst AXIS_RATIO = 0.5; // an axis scales only if its span ≥ this × the other's\nconst LONG_PRESS_MS = 350;\nconst MOVE_THRESH = 6; // px before a press becomes a drag\nconst WHEEL_K = 0.0015; // wheel delta → zoom factor exponent\nconst SEP_HIT = 4; // px band around the indicator separator for hit-testing\n\n// Drawing-tool styling: the guideline (and committed line) default to solid blue\n// at 2px, matching the core's default and useChartCore's drawing color.\nconst DRAW_COLOR = 0xff2962ff;\nconst DRAW_WIDTH = 2;\nconst DRAW_HIT = 6; // px tolerance for \"clicked on the selected line\" vs. away\n\n// Stable unique id for a freshly drawn line.\nfunction drawingId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return `draw-${Math.random().toString(36).slice(2)}`;\n}\n\n// Distance in px from point (px,py) to the segment (ax,ay)-(bx,by).\nfunction distToSegment(\n px: number,\n py: number,\n ax: number,\n ay: number,\n bx: number,\n by: number,\n): number {\n const dx = bx - ax;\n const dy = by - ay;\n const len2 = dx * dx + dy * dy;\n let t = len2 > 0 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0;\n t = Math.max(0, Math.min(1, t));\n return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));\n}\n\nexport function useGestures(\n containerRef: React.RefObject<HTMLElement | null>,\n handleRef: React.RefObject<VroomChartHandle | null>,\n scheduleRender: () => void,\n opts: GestureOptions,\n): void {\n // Keep latest opts in a ref so the effect's listeners stay stable.\n const optsRef = useRef(opts);\n optsRef.current = opts;\n\n // Drawing-tool state. Lives in refs (not effect-local) so the mode-change\n // effect below can reset it when the host leaves draw mode, and so it survives\n // the gesture effect's stable-listener lifecycle.\n // drawAnchor* — first point placed, awaiting the second (data + px coords).\n // drawSelectedPx — set once a line is committed: its px endpoints, used to\n // hit-test \"clicked on the line\" vs. \"clicked away to exit\".\n const drawAnchorRef = useRef<{ timeMs: number; price: number } | null>(null);\n const drawAnchorPxRef = useRef<{ x: number; y: number } | null>(null);\n const drawSelectedPxRef = useRef<{\n ax: number;\n ay: number;\n bx: number;\n by: number;\n } | null>(null);\n\n // Reset the in-progress draft whenever draw+line mode is not active (e.g. the\n // host toggled the tool off mid-draw), so re-entering draw mode starts clean.\n useEffect(() => {\n const active = opts.mode === 'draw' && opts.tool === 'line';\n if (active) return;\n drawAnchorRef.current = null;\n drawAnchorPxRef.current = null;\n drawSelectedPxRef.current = null;\n const h = handleRef.current;\n if (h) {\n h.clearDraft();\n scheduleRender();\n }\n }, [opts.mode, opts.tool, handleRef, scheduleRender]);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const pointers = new Map<number, { x: number; y: number }>();\n let panMode: Region = 'chart';\n let crosshairActive = false;\n let crosshairSource: 'press' | 'hover' | null = null;\n let lastCrosshairTime: number | null = null;\n let longPressTimer: ReturnType<typeof setTimeout> | null = null;\n let downX = 0;\n let downY = 0;\n let moved = false;\n const pinch = { spanX: 1, spanY: 1, ratioX: 1, ratioY: 1, enableX: false, enableY: false, active: false };\n\n const rel = (e: PointerEvent | WheelEvent) => {\n const r = el.getBoundingClientRect();\n return { x: e.clientX - r.left, y: e.clientY - r.top };\n };\n\n const regionAt = (x: number, y: number): Region => {\n const h = handleRef.current;\n const r = el.getBoundingClientRect();\n if (!h) return 'chart';\n const { yAxisWidth, xAxisHeight, indicatorHeight } = h.getAxisMetrics();\n if (x > r.width - yAxisWidth) {\n // The y-axis strip beside an indicator pane scales that pane's y-axis;\n // the rest of the strip scales the main price axis.\n const priceBottom = r.height - xAxisHeight - indicatorHeight;\n if (indicatorHeight > 0 && y > priceBottom && y < r.height - xAxisHeight) {\n return 'indicator-axis';\n }\n return 'price-axis';\n }\n // Separator sits at the top edge of the indicator band, within the candle\n // area width — a narrow grab band for resizing the panes.\n const sepY = r.height - xAxisHeight - indicatorHeight;\n if (indicatorHeight > 0 && x <= r.width - yAxisWidth && Math.abs(y - sepY) <= SEP_HIT) {\n return 'separator';\n }\n if (y > r.height - xAxisHeight) return 'time-axis';\n if (indicatorHeight > 0 && y > r.height - xAxisHeight - indicatorHeight) return 'indicator';\n return 'chart';\n };\n\n const reportCrosshair = (reason: CrosshairEvent['reason']) => {\n const h = handleRef.current;\n if (!h) return;\n // Snapped slot — has a timeMs even in the empty space ahead of the last\n // candle, where `candle` is null.\n const info = h.getCrosshairInfo();\n const t = info?.timeMs ?? null;\n if (reason === 'move' && t === lastCrosshairTime) return;\n lastCrosshairTime = t;\n optsRef.current.onCrosshair?.({\n active: reason !== 'hide',\n candle: info?.candle ?? null,\n timeMs: t,\n reason,\n });\n };\n\n const clearLongPress = () => {\n if (longPressTimer != null) {\n clearTimeout(longPressTimer);\n longPressTimer = null;\n }\n };\n\n const showCrosshair = (x: number, y: number, source: 'press' | 'hover', reason: 'show' | 'move') => {\n const h = handleRef.current;\n if (!h) return;\n crosshairActive = true;\n crosshairSource = source;\n const lift = source === 'press' ? optsRef.current.crosshairOffset : 0;\n h.setCrosshair(x, y - lift);\n scheduleRender();\n reportCrosshair(reason);\n };\n\n const hideCrosshair = () => {\n const h = handleRef.current;\n if (!h || !crosshairActive) return;\n crosshairActive = false;\n crosshairSource = null;\n h.clearCrosshair();\n scheduleRender();\n reportCrosshair('hide');\n };\n\n // True while the line tool should own input (suppress pan/zoom/crosshair).\n const drawActive = () =>\n optsRef.current.mode === 'draw' && optsRef.current.tool === 'line';\n\n // While placing the second point, keep the guideline glued to the cursor.\n const updateGuideline = (x: number, y: number) => {\n const h = handleRef.current;\n if (!h) return;\n if (!drawAnchorRef.current || drawSelectedPxRef.current) return;\n const c = h.coordAt(x, y);\n if (!c) return;\n const a = drawAnchorRef.current;\n h.setDraft(a.timeMs, a.price, true, c.timeMs, c.price, true, DRAW_COLOR, DRAW_WIDTH);\n scheduleRender();\n };\n\n // A tap in the chart area: place the next point, or exit on click-away.\n const handleDrawClick = (x: number, y: number) => {\n const h = handleRef.current;\n if (!h) return;\n const o = optsRef.current;\n\n // A committed line is selected: clicking on it keeps it; clicking away\n // hides the nodes and asks the host to return to pan mode.\n const sel = drawSelectedPxRef.current;\n if (sel) {\n if (distToSegment(x, y, sel.ax, sel.ay, sel.bx, sel.by) <= DRAW_HIT) return;\n drawAnchorRef.current = null;\n drawAnchorPxRef.current = null;\n drawSelectedPxRef.current = null;\n h.clearDraft();\n scheduleRender();\n el.style.cursor = '';\n o.onRequestMode?.('pan');\n return;\n }\n\n const coord = h.coordAt(x, y);\n if (!coord) return;\n\n if (!drawAnchorRef.current) {\n // First point: show node A; the guideline follows on the next move.\n drawAnchorRef.current = coord;\n drawAnchorPxRef.current = { x, y };\n h.setDraft(coord.timeMs, coord.price, false, 0, 0, true, DRAW_COLOR, DRAW_WIDTH);\n scheduleRender();\n } else {\n // Second point: commit the line and keep both nodes shown (selected).\n const a = drawAnchorRef.current;\n const apx = drawAnchorPxRef.current!;\n o.onDrawingComplete?.({\n id: drawingId(),\n type: 'line',\n points: [\n { timeMs: a.timeMs, price: a.price },\n { timeMs: coord.timeMs, price: coord.price },\n ],\n });\n drawSelectedPxRef.current = { ax: apx.x, ay: apx.y, bx: x, by: y };\n h.setDraft(a.timeMs, a.price, true, coord.timeMs, coord.price, false, DRAW_COLOR, DRAW_WIDTH);\n scheduleRender();\n }\n };\n\n const onPointerDown = (e: PointerEvent) => {\n const h = handleRef.current;\n if (!h) return;\n if (e.pointerType === 'mouse' && e.button !== 0) return; // left button only\n const { x, y } = rel(e);\n el.setPointerCapture(e.pointerId);\n pointers.set(e.pointerId, { x, y });\n\n if (pointers.size === 2) {\n clearLongPress();\n hideCrosshair();\n const pts = [...pointers.values()];\n const sx = Math.abs(pts[0]!.x - pts[1]!.x);\n const sy = Math.abs(pts[0]!.y - pts[1]!.y);\n pinch.spanX = sx;\n pinch.spanY = sy;\n pinch.ratioX = 1;\n pinch.ratioY = 1;\n pinch.enableX = sx >= MIN_SPAN && sx >= sy * AXIS_RATIO;\n pinch.enableY = sy >= MIN_SPAN && sy >= sx * AXIS_RATIO;\n pinch.active = true;\n return;\n }\n\n // Single pointer: classify region; arm long-press → crosshair (touch/pen).\n downX = x;\n downY = y;\n moved = false;\n panMode = regionAt(x, y);\n if (e.pointerType !== 'mouse' && panMode === 'chart' && !drawActive()) {\n longPressTimer = setTimeout(() => {\n longPressTimer = null;\n if (!moved) showCrosshair(downX, downY, 'press', 'show');\n }, LONG_PRESS_MS);\n }\n };\n\n const onPointerMove = (e: PointerEvent) => {\n const h = handleRef.current;\n if (!h) return;\n const { x, y } = rel(e);\n\n // Hover (mouse, no button) → crosshair follows the cursor on the chart.\n if (!pointers.has(e.pointerId)) {\n if (e.pointerType === 'mouse' && pointers.size === 0) {\n // Draw mode: no crosshair; the guideline tracks the cursor instead.\n if (drawActive()) {\n el.style.cursor = 'crosshair';\n updateGuideline(x, y);\n return;\n }\n const region = regionAt(x, y);\n el.style.cursor =\n region === 'separator'\n ? 'row-resize'\n : region === 'indicator-axis'\n ? 'ns-resize'\n : '';\n if (region === 'chart') {\n showCrosshair(x, y, 'hover', crosshairActive ? 'move' : 'show');\n } else {\n hideCrosshair();\n }\n }\n return;\n }\n\n const prev = pointers.get(e.pointerId)!;\n const dx = x - prev.x;\n const dy = y - prev.y;\n pointers.set(e.pointerId, { x, y });\n\n // Two-finger directional pinch (disabled while drawing).\n if (pointers.size >= 2 && pinch.active && !drawActive()) {\n const pts = [...pointers.values()];\n const focalX = (pts[0]!.x + pts[1]!.x) / 2;\n const focalY = (pts[0]!.y + pts[1]!.y) / 2;\n let frameX = 1;\n if (pinch.enableX) {\n const r = Math.max(Math.abs(pts[0]!.x - pts[1]!.x), MIN_SPAN) / pinch.spanX;\n frameX = r / pinch.ratioX;\n pinch.ratioX = r;\n }\n let frameY = 1;\n if (pinch.enableY) {\n const r = Math.max(Math.abs(pts[0]!.y - pts[1]!.y), MIN_SPAN) / pinch.spanY;\n frameY = r / pinch.ratioY;\n pinch.ratioY = r;\n }\n if (frameX !== 1 || frameY !== 1) {\n h.zoom(frameX, frameY, focalX, focalY);\n scheduleRender();\n }\n return;\n }\n\n if (!moved && Math.hypot(x - downX, y - downY) > MOVE_THRESH) {\n moved = true;\n clearLongPress();\n }\n if (!moved) return;\n\n // Draw mode: a press-drag neither pans nor moves a crosshair; if a point\n // is down, keep the guideline tracking the cursor so a drag still previews.\n if (drawActive()) {\n if (panMode === 'chart') updateGuideline(x, y);\n return;\n }\n\n // Chart-area drag with the (press) crosshair up moves the crosshair.\n if (crosshairActive && crosshairSource === 'press' && panMode === 'chart') {\n showCrosshair(x, y, 'press', 'move');\n return;\n }\n\n if (panMode === 'price-axis') h.scalePriceAxis(dy);\n else if (panMode === 'time-axis') h.scaleTimeAxis(dx);\n else if (panMode === 'separator') h.resizeIndicatorPane(dy);\n else if (panMode === 'indicator-axis') h.scaleIndicatorAxis(downY, dy);\n else if (panMode === 'indicator') h.pan(dx, 0);\n else h.translate(dx, dy);\n\n // Keep a hover crosshair glued to the cursor while dragging — the chart\n // pans under it, so re-snap to whatever candle is now beneath the cursor.\n if (crosshairActive && crosshairSource === 'hover') {\n h.setCrosshair(x, y);\n reportCrosshair('move');\n }\n scheduleRender();\n };\n\n const endPointer = (e: PointerEvent) => {\n const had = pointers.delete(e.pointerId);\n clearLongPress();\n if (pointers.size < 2) pinch.active = false;\n if (!had) return;\n\n // Draw mode: a stationary tap in the chart places a point (or exits on\n // click-away). Drags are ignored (no pan). Crosshair logic is skipped.\n if (drawActive() && pointers.size === 0) {\n if (!moved && panMode === 'chart') handleDrawClick(downX, downY);\n else el.style.cursor = 'crosshair';\n return;\n }\n\n // Press crosshair is active only while held — release dismisses it.\n if (crosshairSource === 'press' && pointers.size === 0) {\n hideCrosshair();\n } else if (moved && (panMode === 'chart' || panMode === 'indicator')) {\n optsRef.current.onViewportChange?.(0, 0);\n }\n if (pointers.size === 0) el.style.cursor = '';\n };\n\n const onPointerLeave = () => {\n if (crosshairSource === 'hover') hideCrosshair();\n el.style.cursor = '';\n };\n\n const onWheel = (e: WheelEvent) => {\n const h = handleRef.current;\n if (!h) return;\n e.preventDefault();\n // Draw mode: no pan/zoom from the wheel (keeps placed points stable).\n if (drawActive()) return;\n const { x, y } = rel(e);\n if (e.ctrlKey || e.metaKey) {\n // Trackpad pinch (sent as ctrl+wheel) / ctrl+wheel → zoom both axes.\n const f = Math.exp(-e.deltaY * WHEEL_K);\n h.zoom(f, f, x, y);\n } else if (e.shiftKey) {\n // Shift+wheel → horizontal pan (mouse-wheel-only users).\n h.pan(-(e.deltaX || e.deltaY), 0);\n } else if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {\n // Horizontal scroll → pan the time axis (scroll right = forward in time).\n h.pan(-e.deltaX, 0);\n } else {\n // Vertical scroll → zoom the time window around the cursor.\n const f = Math.exp(-e.deltaY * WHEEL_K);\n h.zoom(f, 1, x, y);\n }\n scheduleRender();\n };\n\n el.addEventListener('pointerdown', onPointerDown);\n el.addEventListener('pointermove', onPointerMove);\n el.addEventListener('pointerup', endPointer);\n el.addEventListener('pointercancel', endPointer);\n el.addEventListener('pointerleave', onPointerLeave);\n el.addEventListener('wheel', onWheel, { passive: false });\n\n return () => {\n clearLongPress();\n el.removeEventListener('pointerdown', onPointerDown);\n el.removeEventListener('pointermove', onPointerMove);\n el.removeEventListener('pointerup', endPointer);\n el.removeEventListener('pointercancel', endPointer);\n el.removeEventListener('pointerleave', onPointerLeave);\n el.removeEventListener('wheel', onWheel);\n };\n }, [containerRef, handleRef, scheduleRender]);\n}\n","// VroomChart (web) — DOM React component that renders the candlestick chart to\n// a <canvas> via @vroomchart/core-wasm and wires pointer/wheel gestures. API-matched\n// to the React Native component (same props from @vroomchart/types).\n\nimport React from 'react';\nimport type { CSSProperties } from 'react';\n\nimport { useChartCore } from './useChartCore';\nimport { useGestures } from './useGestures';\nimport type { VroomChartProps } from './types';\n\nconst FILL: CSSProperties = { position: 'relative', width: '100%', height: '100%' };\nconst CANVAS_STYLE: CSSProperties = {\n display: 'block',\n width: '100%',\n height: '100%',\n touchAction: 'none', // we own pan/zoom; don't let the browser scroll/zoom\n};\n\n/**\n * Skia-quality candlestick chart for the web. Pass OHLCV `candles` and size it\n * via `style`/`width`/`height` (it fills its parent by default). Drag to pan,\n * pinch or wheel to zoom, drag the price/time axes to rescale, hover (mouse) or\n * long-press (touch) for the crosshair. Optional indicators (`rsi`, `macd`,\n * `movingAverages`, `vwap`), colors (`theme`), and events (`onCrosshair`,\n * `onViewportChange`) are configured through props.\n *\n * @see {@link VroomChartProps} for the full prop reference.\n */\nexport function VroomChart(props: VroomChartProps) {\n const {\n className,\n style,\n wasm,\n crosshairOffset = 40,\n mode,\n tool,\n onCrosshair,\n onViewportChange,\n onDrawingComplete,\n onModeChange,\n } = props;\n const { containerRef, canvasRef, handleRef, scheduleRender } = useChartCore(\n props,\n wasm ? { wasm } : undefined,\n );\n\n useGestures(containerRef, handleRef, scheduleRender, {\n crosshairOffset,\n mode,\n tool,\n onCrosshair,\n onViewportChange,\n onDrawingComplete,\n onRequestMode: onModeChange,\n });\n\n const rootStyle: CSSProperties = {\n ...FILL,\n ...(props.width != null ? { width: props.width } : null),\n ...(props.height != null ? { height: props.height } : null),\n ...style,\n };\n\n return (\n <div ref={containerRef} className={className} style={rootStyle}>\n <canvas ref={canvasRef} style={CANVAS_STYLE} />\n </div>\n );\n}\n"],"mappings":"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","names":["useEffect","useRef"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vroomchart/react",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Skia-quality candlestick chart for the web (React DOM).",
5
5
  "license": "MIT",
6
6
  "author": "Darion Welch",
@@ -36,11 +36,17 @@
36
36
  "dist",
37
37
  "README.md"
38
38
  ],
39
+ "scripts": {
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run",
42
+ "build": "tsup",
43
+ "prepublishOnly": "tsup"
44
+ },
39
45
  "publishConfig": {
40
46
  "access": "public"
41
47
  },
42
48
  "dependencies": {
43
- "@vroomchart/core-wasm": "0.1.1"
49
+ "@vroomchart/core-wasm": "workspace:*"
44
50
  },
45
51
  "peerDependencies": {
46
52
  "react": ">=18",
@@ -49,14 +55,11 @@
49
55
  "devDependencies": {
50
56
  "@types/react": "~19.1.0",
51
57
  "@types/react-dom": "~19.1.0",
58
+ "@vroomchart/types": "workspace:*",
52
59
  "react": "19.1.0",
53
60
  "react-dom": "19.1.0",
54
61
  "tsup": "^8.5.1",
55
62
  "typescript": "~5.9.2",
56
- "@vroomchart/types": "0.0.1"
57
- },
58
- "scripts": {
59
- "typecheck": "tsc --noEmit",
60
- "build": "tsup"
63
+ "vitest": "^2.1.9"
61
64
  }
62
- }
65
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Darion Welch
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.