react-native-vroom-chart 0.1.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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/android/README.md +7 -0
  4. package/cpp/README.md +11 -0
  5. package/cpp/VroomChartHostObject.cpp +458 -0
  6. package/cpp/VroomChartHostObject.h +36 -0
  7. package/cpp/VroomJsiInstaller.cpp +69 -0
  8. package/cpp/VroomJsiInstaller.h +10 -0
  9. package/cpp/VroomSkiaContext.cpp +25 -0
  10. package/cpp/VroomSkiaContext.h +30 -0
  11. package/cpp/_core_include/vroom/vroom_chart.h +195 -0
  12. package/cpp/_core_src/candles.cpp +74 -0
  13. package/cpp/_core_src/candles.h +34 -0
  14. package/cpp/_core_src/chart.cpp +317 -0
  15. package/cpp/_core_src/chart.h +167 -0
  16. package/cpp/_core_src/chart_facade.cpp +570 -0
  17. package/cpp/_core_src/chart_internal.h +30 -0
  18. package/cpp/_core_src/crosshair.cpp +65 -0
  19. package/cpp/_core_src/crosshair.h +32 -0
  20. package/cpp/_core_src/fonts.cpp +22 -0
  21. package/cpp/_core_src/fonts.h +25 -0
  22. package/cpp/_core_src/labels.cpp +340 -0
  23. package/cpp/_core_src/labels.h +85 -0
  24. package/cpp/_core_src/ma.cpp +53 -0
  25. package/cpp/_core_src/ma.h +39 -0
  26. package/cpp/_core_src/ma_overlay.cpp +73 -0
  27. package/cpp/_core_src/ma_overlay.h +42 -0
  28. package/cpp/_core_src/macd.cpp +68 -0
  29. package/cpp/_core_src/macd.h +29 -0
  30. package/cpp/_core_src/macd_pane.cpp +199 -0
  31. package/cpp/_core_src/macd_pane.h +41 -0
  32. package/cpp/_core_src/price_indicator.cpp +106 -0
  33. package/cpp/_core_src/price_indicator.h +29 -0
  34. package/cpp/_core_src/rsi.cpp +70 -0
  35. package/cpp/_core_src/rsi.h +32 -0
  36. package/cpp/_core_src/rsi_pane.cpp +167 -0
  37. package/cpp/_core_src/rsi_pane.h +39 -0
  38. package/cpp/_core_src/theme.cpp +41 -0
  39. package/cpp/_core_src/theme.h +23 -0
  40. package/cpp/_core_src/ticks.cpp +49 -0
  41. package/cpp/_core_src/ticks.h +30 -0
  42. package/cpp/_core_src/viewport.cpp +141 -0
  43. package/cpp/_core_src/viewport.h +100 -0
  44. package/cpp/_core_src/volume.cpp +70 -0
  45. package/cpp/_core_src/volume.h +34 -0
  46. package/cpp/_core_src/vwap.cpp +51 -0
  47. package/cpp/_core_src/vwap.h +27 -0
  48. package/ios/README.md +7 -0
  49. package/ios/VroomChartModule.h +11 -0
  50. package/ios/VroomChartModule.mm +44 -0
  51. package/lib/index.d.mts +185 -0
  52. package/lib/index.d.ts +185 -0
  53. package/lib/index.js +429 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/index.mjs +396 -0
  56. package/lib/index.mjs.map +1 -0
  57. package/package.json +75 -0
  58. package/react-native-vroom-chart.podspec +79 -0
  59. package/src/NativeVroomChart.ts +12 -0
  60. package/src/VroomChart.tsx +382 -0
  61. package/src/index.ts +14 -0
  62. package/src/jsi.d.ts +128 -0
  63. package/src/packCandles.ts +24 -0
  64. package/src/theme.ts +43 -0
  65. package/src/types.ts +27 -0
  66. package/src/useChartCore.ts +135 -0
@@ -0,0 +1,570 @@
1
+ // Public C facade — the surface declared in `vroom/vroom_chart.h`.
2
+ //
3
+ // Most functions are thin wrappers over mutators on `VroomChart` plus
4
+ // `mark_dirty`. The actual rendering and animation logic lives in
5
+ // chart.cpp / labels.cpp / candles.cpp.
6
+
7
+ #include "vroom/vroom_chart.h"
8
+
9
+ #include <algorithm>
10
+ #include <cstddef>
11
+ #include <cstdint>
12
+
13
+ #include "chart.h"
14
+ #include "labels.h"
15
+ #include "viewport.h"
16
+
17
+ class SkCanvas;
18
+
19
+ // ---- Sensitivity / clamping constants -------------------------------------
20
+
21
+ namespace {
22
+
23
+ // Pixels per 2× scale factor on axis-strip drags. Smaller = more aggressive.
24
+ constexpr double kAxisDragSensitivity = 300.0;
25
+
26
+ // Bounds for x-axis-drag clamp: minimum / maximum candle body width in pixels.
27
+ // The clamp converts these to corresponding time-window bounds based on the
28
+ // current candle area, so candles stay visually reasonable across zoom.
29
+ constexpr double kMinCandleBodyPx = 1.5;
30
+ constexpr double kMaxCandleBodyPx = 32.0;
31
+
32
+ } // namespace
33
+
34
+ // ---- Lifecycle -------------------------------------------------------------
35
+
36
+ extern "C" VroomChart* vroom_chart_create(const VroomCallbacks* cb, void* user_ctx) {
37
+ auto* c = new VroomChart();
38
+ if (cb) c->cb = *cb;
39
+ c->user_ctx = user_ctx;
40
+ return c;
41
+ }
42
+
43
+ extern "C" void vroom_chart_destroy(VroomChart* chart) { delete chart; }
44
+
45
+ // ---- Data ------------------------------------------------------------------
46
+
47
+ extern "C" void vroom_chart_set_candles(VroomChart* chart, const VroomCandle* data, size_t count) {
48
+ if (!chart) return;
49
+ chart->candles.assign(data, data + count);
50
+ chart->rsi_dirty = true;
51
+ chart->macd_dirty = true;
52
+ chart->overlays_dirty = true;
53
+ chart->vwap_dirty = true;
54
+
55
+ // Infer the candle period from the first interval. Robust enough for
56
+ // uniform-duration series (the only kind we model today).
57
+ if (chart->candles.size() >= 2) {
58
+ const int64_t d = chart->candles[1].time_ms - chart->candles[0].time_ms;
59
+ if (d > 0) chart->candle_duration_ms = d;
60
+ }
61
+ vroom::labels::recompute_axis_width(*chart);
62
+
63
+ // Default the visible window to the most recent ~80 candles when the
64
+ // consumer hasn't set one — a narrower x-window so candles read wider.
65
+ if (chart->visible_start_ms == 0 && chart->visible_end_ms == 0 &&
66
+ !chart->candles.empty()) {
67
+ constexpr size_t kDefaultVisible = 80;
68
+ const size_t start_idx = chart->candles.size() > kDefaultVisible
69
+ ? chart->candles.size() - kDefaultVisible
70
+ : 0;
71
+ chart->visible_start_ms = chart->candles[start_idx].time_ms;
72
+ chart->visible_end_ms = chart->candles.back().time_ms;
73
+ }
74
+
75
+ // Snap the price (y-axis) bounds to whatever's visible right now. After
76
+ // this, only zoom / axis-drags change them; panning preserves them.
77
+ if (!chart->candles.empty()) {
78
+ const auto idx = vroom::visible_indices(
79
+ chart->candles.data(), chart->candles.size(),
80
+ chart->visible_start_ms, chart->visible_end_ms);
81
+ if (idx.end > idx.start) {
82
+ auto b = vroom::price_bounds(
83
+ chart->candles.data() + idx.start, idx.end - idx.start);
84
+ // Widen the default y-window beyond the data's min/max so candles
85
+ // fill less vertical space (shorter candles, more headroom). 1.0 =
86
+ // snug; larger = wider. Only the default — zoom/axis-drag override.
87
+ constexpr double kDefaultYZoom = 1.5;
88
+ const double mid = (b.min + b.max) * 0.5;
89
+ const double half = (b.max - b.min) * 0.5 * kDefaultYZoom;
90
+ b.min = mid - half;
91
+ b.max = mid + half;
92
+ chart->price_bounds = b;
93
+ chart->price_bounds_initialized = true;
94
+ }
95
+ }
96
+
97
+ chart->mark_dirty();
98
+ }
99
+
100
+ extern "C" void vroom_chart_append_candle(VroomChart* chart, const VroomCandle* c) {
101
+ if (!chart || !c) return;
102
+ chart->candles.push_back(*c);
103
+ chart->rsi_dirty = true;
104
+ chart->macd_dirty = true;
105
+ chart->overlays_dirty = true;
106
+ chart->vwap_dirty = true;
107
+ chart->mark_dirty();
108
+ }
109
+
110
+ extern "C" void vroom_chart_update_last(VroomChart* chart, const VroomCandle* c) {
111
+ if (!chart || !c || chart->candles.empty()) return;
112
+ chart->candles.back() = *c;
113
+ chart->rsi_dirty = true;
114
+ chart->macd_dirty = true;
115
+ chart->overlays_dirty = true;
116
+ chart->vwap_dirty = true;
117
+ chart->mark_dirty();
118
+ }
119
+
120
+ // ---- Layout ----------------------------------------------------------------
121
+
122
+ extern "C" void vroom_chart_set_size(VroomChart* chart, float w, float h, float ratio) {
123
+ if (!chart) return;
124
+ if (chart->width_px == w && chart->height_px == h && chart->px_ratio == ratio) return;
125
+ chart->width_px = w;
126
+ chart->height_px = h;
127
+ chart->px_ratio = ratio;
128
+ chart->mark_dirty();
129
+ }
130
+
131
+ // ---- Viewport / gestures ---------------------------------------------------
132
+
133
+ extern "C" void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_ms, int64_t end_ms) {
134
+ if (!chart) return;
135
+ if (chart->visible_start_ms == start_ms && chart->visible_end_ms == end_ms) return;
136
+ chart->visible_start_ms = start_ms;
137
+ chart->visible_end_ms = end_ms;
138
+ if (chart->cb.on_viewport_changed) {
139
+ chart->cb.on_viewport_changed(chart->user_ctx, start_ms, end_ms);
140
+ }
141
+ chart->mark_dirty();
142
+ }
143
+
144
+ extern "C" void vroom_chart_pan(VroomChart* chart, float dx_px, float /*dy_px*/) {
145
+ if (!chart || dx_px == 0.f || chart->candles.empty()) return;
146
+
147
+ const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
148
+ const float usable_px =
149
+ chart->width_px - chart->theme.floats[VROOM_FLOAT_RIGHT_PADDING_PX];
150
+ if (window_ms <= 0 || usable_px <= 0.f) return;
151
+
152
+ // Finger right (dx > 0) → content moves right → see earlier data → window shifts back.
153
+ const int64_t delta_ms = static_cast<int64_t>(
154
+ (-dx_px / usable_px) * static_cast<float>(window_ms));
155
+ if (delta_ms == 0) return;
156
+
157
+ int64_t new_start = chart->visible_start_ms + delta_ms;
158
+ int64_t new_end = chart->visible_end_ms + delta_ms;
159
+
160
+ // Right edge can overshoot the last candle by up to half a window so the
161
+ // user can scroll into empty "future" space (e.g., to view right-anchored
162
+ // indicators). Left edge still hard-clamps at the first candle.
163
+ const int64_t first_time = chart->candles.front().time_ms;
164
+ const int64_t last_time = chart->candles.back().time_ms;
165
+ const int64_t max_future = window_ms / 2;
166
+ if (new_end > last_time + max_future) {
167
+ new_end = last_time + max_future;
168
+ new_start = new_end - window_ms;
169
+ }
170
+ if (new_start < first_time) {
171
+ new_start = first_time;
172
+ new_end = new_start + window_ms;
173
+ }
174
+
175
+ if (new_start == chart->visible_start_ms &&
176
+ new_end == chart->visible_end_ms) {
177
+ return; // fully clamped, nothing changed
178
+ }
179
+
180
+ chart->visible_start_ms = new_start;
181
+ chart->visible_end_ms = new_end;
182
+
183
+ if (chart->cb.on_viewport_changed) {
184
+ chart->cb.on_viewport_changed(
185
+ chart->user_ctx, chart->visible_start_ms, chart->visible_end_ms);
186
+ }
187
+ chart->mark_dirty();
188
+ }
189
+
190
+ extern "C" void vroom_chart_translate(VroomChart* chart, float dx_px, float dy_px) {
191
+ if (!chart || chart->candles.empty()) return;
192
+ bool changed = false;
193
+
194
+ // Horizontal: shift the time window. Same math + clamping as pan, but
195
+ // we don't fire the redraw notification mid-call — we batch it below.
196
+ if (dx_px != 0.f) {
197
+ const int64_t window_ms =
198
+ chart->visible_end_ms - chart->visible_start_ms;
199
+ const float usable_px =
200
+ chart->width_px - chart->theme.floats[VROOM_FLOAT_RIGHT_PADDING_PX];
201
+ if (window_ms > 0 && usable_px > 0.f) {
202
+ const int64_t delta_ms = static_cast<int64_t>(
203
+ (-dx_px / usable_px) * static_cast<float>(window_ms));
204
+ if (delta_ms != 0) {
205
+ int64_t new_start = chart->visible_start_ms + delta_ms;
206
+ int64_t new_end = chart->visible_end_ms + delta_ms;
207
+ const int64_t first_time = chart->candles.front().time_ms;
208
+ const int64_t last_time = chart->candles.back().time_ms;
209
+ const int64_t max_future = window_ms / 2;
210
+ if (new_end > last_time + max_future) {
211
+ new_end = last_time + max_future;
212
+ new_start = new_end - window_ms;
213
+ }
214
+ if (new_start < first_time) {
215
+ new_start = first_time;
216
+ new_end = new_start + window_ms;
217
+ }
218
+ if (new_start != chart->visible_start_ms ||
219
+ new_end != chart->visible_end_ms) {
220
+ chart->visible_start_ms = new_start;
221
+ chart->visible_end_ms = new_end;
222
+ changed = true;
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ // Vertical: shift price bounds by the price-equivalent of dy_px. Range
229
+ // stays constant. We divide by draw_h (the 90% slice of candle area
230
+ // that's actually used for price → y) so translation feels 1:1.
231
+ if (dy_px != 0.f && chart->price_bounds_initialized) {
232
+ const float candle_area_h = vroom::price_pane_bottom(chart->layout());
233
+ const double draw_h = static_cast<double>(candle_area_h) * 0.9;
234
+ if (draw_h > 0.0) {
235
+ const double range =
236
+ chart->price_bounds.max - chart->price_bounds.min;
237
+ const double dprice =
238
+ (static_cast<double>(dy_px) / draw_h) * range;
239
+ chart->price_bounds.min += dprice;
240
+ chart->price_bounds.max += dprice;
241
+ vroom::labels::recompute_axis_width(*chart);
242
+ changed = true;
243
+ }
244
+ }
245
+
246
+ if (changed) {
247
+ if (chart->cb.on_viewport_changed) {
248
+ chart->cb.on_viewport_changed(
249
+ chart->user_ctx, chart->visible_start_ms,
250
+ chart->visible_end_ms);
251
+ }
252
+ chart->mark_dirty();
253
+ }
254
+ }
255
+
256
+ extern "C" void vroom_chart_scale_price_axis(VroomChart* chart, float dy_px) {
257
+ if (!chart || dy_px == 0.f) return;
258
+ if (!chart->price_bounds_initialized) return;
259
+
260
+ const double range = chart->price_bounds.max - chart->price_bounds.min;
261
+ if (range <= 0.0) return;
262
+
263
+ // Drag down (dy > 0) → scale > 1 → wider price range → candles shrink.
264
+ double scale = 1.0 + static_cast<double>(dy_px) / kAxisDragSensitivity;
265
+ if (scale < 0.05) scale = 0.05; // never collapse or flip
266
+
267
+ const double center = (chart->price_bounds.max + chart->price_bounds.min) * 0.5;
268
+ const double new_range = range * scale;
269
+ chart->price_bounds.max = center + new_range * 0.5;
270
+ chart->price_bounds.min = center - new_range * 0.5;
271
+ vroom::labels::recompute_axis_width(*chart);
272
+
273
+ chart->mark_dirty();
274
+ }
275
+
276
+ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
277
+ if (!chart || dx_px == 0.f || chart->candles.empty()) return;
278
+
279
+ const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
280
+ if (window_ms <= 0) return;
281
+
282
+ // Drag right (dx > 0) → scale > 1 → wider time window → candles thin.
283
+ double scale = 1.0 + static_cast<double>(dx_px) / kAxisDragSensitivity;
284
+ if (scale < 0.05) scale = 0.05;
285
+
286
+ int64_t new_window_ms = static_cast<int64_t>(
287
+ static_cast<double>(window_ms) * scale);
288
+
289
+ // Clamp window so candle body width stays in [kMinCandleBodyPx,
290
+ // kMaxCandleBodyPx]. body_w = usable × (dur / window) × ratio →
291
+ // window for a given body_w = usable × dur × ratio / body_w.
292
+ const auto lay = chart->layout();
293
+ const double usable =
294
+ lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
295
+ const double ratio = chart->theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO];
296
+ const double dur = static_cast<double>(chart->candle_duration_ms);
297
+ if (usable > 0.0 && ratio > 0.0 && dur > 0.0) {
298
+ const int64_t min_window = static_cast<int64_t>(
299
+ (usable * dur * ratio) / kMaxCandleBodyPx);
300
+ const int64_t max_window = static_cast<int64_t>(
301
+ (usable * dur * ratio) / kMinCandleBodyPx);
302
+ if (new_window_ms < min_window) new_window_ms = min_window;
303
+ if (new_window_ms > max_window) new_window_ms = max_window;
304
+ }
305
+
306
+ // Pivot around the right edge — most-recent visible candle stays put.
307
+ int64_t new_start = chart->visible_end_ms - new_window_ms;
308
+ const int64_t first_time = chart->candles.front().time_ms;
309
+ if (new_start < first_time) new_start = first_time;
310
+
311
+ if (new_start == chart->visible_start_ms) return;
312
+ chart->visible_start_ms = new_start;
313
+
314
+ if (chart->cb.on_viewport_changed) {
315
+ chart->cb.on_viewport_changed(
316
+ chart->user_ctx, chart->visible_start_ms, chart->visible_end_ms);
317
+ }
318
+ chart->mark_dirty();
319
+ }
320
+
321
+ extern "C" void vroom_chart_get_axis_metrics(VroomChart* chart,
322
+ float* out_y_axis_width_px,
323
+ float* out_x_axis_height_px,
324
+ float* out_indicator_height_px) {
325
+ if (!chart) return;
326
+ const auto lay = chart->layout();
327
+ if (out_y_axis_width_px) *out_y_axis_width_px = lay.y_axis_width_px;
328
+ if (out_x_axis_height_px) *out_x_axis_height_px = lay.x_axis_height_px;
329
+ if (out_indicator_height_px) *out_indicator_height_px = lay.indicator_area_h;
330
+ }
331
+
332
+ extern "C" void vroom_chart_zoom(VroomChart* chart, float scale_x, float scale_y,
333
+ float fx, float fy) {
334
+ if (!chart) return;
335
+ bool price_changed = false;
336
+ bool window_changed = false;
337
+
338
+ // --- Y (price) zoom around fy ------------------------------------------
339
+ // scale_y > 1 (vertical pinch out) narrows the price range → taller candles.
340
+ if (scale_y > 0.f && scale_y != 1.f && chart->price_bounds_initialized) {
341
+ const float candle_area_h = vroom::price_pane_bottom(chart->layout());
342
+ const double range = chart->price_bounds.max - chart->price_bounds.min;
343
+ if (candle_area_h > 0.f && range > 0.0) {
344
+ const float frac = std::clamp(fy / candle_area_h, 0.f, 1.f);
345
+ const double focal_price = chart->price_bounds.max - frac * range;
346
+ const double new_range = range / static_cast<double>(scale_y);
347
+ chart->price_bounds.max = focal_price + frac * new_range;
348
+ chart->price_bounds.min = chart->price_bounds.max - new_range;
349
+ price_changed = true;
350
+ }
351
+ }
352
+
353
+ // --- X (time) zoom around fx -------------------------------------------
354
+ // scale_x > 1 (horizontal pinch out) narrows the time window → wider candles.
355
+ if (scale_x > 0.f && scale_x != 1.f && !chart->candles.empty()) {
356
+ const int64_t window_ms =
357
+ chart->visible_end_ms - chart->visible_start_ms;
358
+ const auto lay = chart->layout();
359
+ const double usable =
360
+ lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
361
+ if (window_ms > 0 && usable > 0.0) {
362
+ const double fracX =
363
+ std::clamp(static_cast<double>(fx) / usable, 0.0, 1.0);
364
+ const int64_t focal_time = chart->visible_start_ms +
365
+ static_cast<int64_t>(fracX * static_cast<double>(window_ms));
366
+
367
+ int64_t new_window = static_cast<int64_t>(
368
+ static_cast<double>(window_ms) / static_cast<double>(scale_x));
369
+
370
+ // Keep candle body width within [min, max] (same bounds as the
371
+ // x-axis drag): window = usable × dur × ratio / body_w.
372
+ const double ratio =
373
+ chart->theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO];
374
+ const double dur = static_cast<double>(chart->candle_duration_ms);
375
+ if (ratio > 0.0 && dur > 0.0) {
376
+ const int64_t min_window = static_cast<int64_t>(
377
+ (usable * dur * ratio) / kMaxCandleBodyPx);
378
+ const int64_t max_window = static_cast<int64_t>(
379
+ (usable * dur * ratio) / kMinCandleBodyPx);
380
+ if (new_window < min_window) new_window = min_window;
381
+ if (new_window > max_window) new_window = max_window;
382
+ }
383
+
384
+ if (new_window > 0) {
385
+ int64_t new_start = focal_time -
386
+ static_cast<int64_t>(fracX * static_cast<double>(new_window));
387
+ int64_t new_end = new_start + new_window;
388
+ const int64_t first_time = chart->candles.front().time_ms;
389
+ const int64_t last_time = chart->candles.back().time_ms;
390
+ const int64_t max_future = new_window / 2;
391
+ if (new_end > last_time + max_future) {
392
+ new_end = last_time + max_future;
393
+ new_start = new_end - new_window;
394
+ }
395
+ if (new_start < first_time) {
396
+ new_start = first_time;
397
+ new_end = new_start + new_window;
398
+ }
399
+ if (new_start != chart->visible_start_ms ||
400
+ new_end != chart->visible_end_ms) {
401
+ chart->visible_start_ms = new_start;
402
+ chart->visible_end_ms = new_end;
403
+ window_changed = true;
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+ if (price_changed) vroom::labels::recompute_axis_width(*chart);
410
+ if (window_changed && chart->cb.on_viewport_changed) {
411
+ chart->cb.on_viewport_changed(
412
+ chart->user_ctx, chart->visible_start_ms, chart->visible_end_ms);
413
+ }
414
+ if (price_changed || window_changed) chart->mark_dirty();
415
+ }
416
+
417
+ // ---- Crosshair (state only; rendering not yet implemented) ----------------
418
+
419
+ extern "C" void vroom_chart_set_crosshair(VroomChart* chart, float x, float y) {
420
+ if (!chart) return;
421
+ chart->crosshair_active = true;
422
+ chart->crosshair_x_px = x;
423
+ chart->crosshair_y_px = y;
424
+ chart->mark_dirty();
425
+ }
426
+
427
+ extern "C" void vroom_chart_clear_crosshair(VroomChart* chart) {
428
+ if (!chart) return;
429
+ chart->crosshair_active = false;
430
+ chart->mark_dirty();
431
+ }
432
+
433
+ extern "C" bool vroom_chart_get_crosshair_candle(VroomChart* chart,
434
+ VroomCandle* out) {
435
+ if (!chart || !out || !chart->crosshair_active || chart->candles.empty()) {
436
+ return false;
437
+ }
438
+ // Mirror draw_chart's visible-slice derivation so the queried candle is
439
+ // exactly the one the crosshair snaps to on screen.
440
+ const auto lay = chart->layout();
441
+ const auto range = vroom::visible_indices(
442
+ chart->candles.data(), chart->candles.size(),
443
+ chart->visible_start_ms, chart->visible_end_ms);
444
+ const size_t n = range.end - range.start;
445
+ const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
446
+ if (n == 0 || window_ms <= 0) return false;
447
+
448
+ const ::VroomCandle* visible = chart->candles.data() + range.start;
449
+ const size_t idx = vroom::snap_index_to_candle(
450
+ lay, visible, n, chart->candle_duration_ms,
451
+ chart->visible_start_ms, window_ms, chart->crosshair_x_px);
452
+ *out = visible[idx];
453
+ return true;
454
+ }
455
+
456
+ // ---- Indicators -----------------------------------------------------------
457
+
458
+ extern "C" void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,
459
+ double upper_band, double lower_band,
460
+ bool ma_enabled, int ma_period) {
461
+ if (!chart) return;
462
+ if (period < 2) period = 2;
463
+ if (ma_period < 1) ma_period = 1;
464
+ upper_band = std::clamp(upper_band, 0.0, 100.0);
465
+ lower_band = std::clamp(lower_band, 0.0, 100.0);
466
+
467
+ // Only the series-affecting fields force a recompute; band changes are
468
+ // render-only.
469
+ const bool recompute = chart->rsi_enabled != enabled ||
470
+ chart->rsi_period != period ||
471
+ chart->rsi_ma_enabled != ma_enabled ||
472
+ chart->rsi_ma_period != ma_period;
473
+ const bool changed = recompute || chart->rsi_upper != upper_band ||
474
+ chart->rsi_lower != lower_band;
475
+ if (!changed) return;
476
+
477
+ // Pane order: claim the next slot on an off->on transition, release on off.
478
+ if (enabled && !chart->rsi_enabled) chart->rsi_order = chart->pane_seq++;
479
+ else if (!enabled) chart->rsi_order = -1;
480
+
481
+ chart->rsi_enabled = enabled;
482
+ chart->rsi_period = period;
483
+ chart->rsi_upper = upper_band;
484
+ chart->rsi_lower = lower_band;
485
+ chart->rsi_ma_enabled = ma_enabled;
486
+ chart->rsi_ma_period = ma_period;
487
+ if (recompute) chart->rsi_dirty = true;
488
+ chart->mark_dirty();
489
+ }
490
+
491
+ extern "C" void vroom_chart_set_macd(VroomChart* chart, bool enabled, int fast,
492
+ int slow, int signal) {
493
+ if (!chart) return;
494
+ if (fast < 1) fast = 1;
495
+ if (slow < 1) slow = 1;
496
+ if (slow <= fast) slow = fast + 1;
497
+ if (signal < 1) signal = 1;
498
+
499
+ const bool recompute = chart->macd_enabled != enabled ||
500
+ chart->macd_fast != fast ||
501
+ chart->macd_slow != slow ||
502
+ chart->macd_signal != signal;
503
+ if (!recompute) return;
504
+
505
+ if (enabled && !chart->macd_enabled) chart->macd_order = chart->pane_seq++;
506
+ else if (!enabled) chart->macd_order = -1;
507
+
508
+ chart->macd_enabled = enabled;
509
+ chart->macd_fast = fast;
510
+ chart->macd_slow = slow;
511
+ chart->macd_signal = signal;
512
+ chart->macd_dirty = true;
513
+ chart->mark_dirty();
514
+ }
515
+
516
+ extern "C" void vroom_chart_set_overlays(VroomChart* chart,
517
+ const VroomOverlay* overlays,
518
+ size_t count) {
519
+ if (!chart) return;
520
+ chart->overlays.assign(overlays, overlays + count);
521
+ chart->overlays_dirty = true;
522
+ chart->mark_dirty();
523
+ }
524
+
525
+ extern "C" void vroom_chart_set_vwap(VroomChart* chart, bool enabled,
526
+ int reset_offset_min, uint32_t color,
527
+ float width) {
528
+ if (!chart) return;
529
+ // Keep the offset within a day [0, 1440).
530
+ int off = reset_offset_min % 1440;
531
+ if (off < 0) off += 1440;
532
+ const bool recompute =
533
+ chart->vwap_enabled != enabled || chart->vwap_reset_offset_min != off;
534
+ chart->vwap_enabled = enabled;
535
+ chart->vwap_reset_offset_min = off;
536
+ chart->vwap_color = color;
537
+ chart->vwap_width = width;
538
+ if (recompute) chart->vwap_dirty = true;
539
+ chart->mark_dirty();
540
+ }
541
+
542
+ // ---- Direct draw (used by hosts that don't need the SkPicture cache) ------
543
+
544
+ extern "C" void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas) {
545
+ if (!chart || !canvas) return;
546
+ chart->draw_chart(canvas);
547
+ }
548
+
549
+ extern "C" bool vroom_chart_is_animating(VroomChart* chart) {
550
+ return chart ? chart->is_animating_now() : false;
551
+ }
552
+
553
+ // ---- Theme setters ---------------------------------------------------------
554
+
555
+ extern "C" void vroom_chart_set_color(VroomChart* chart, VroomColorKey key, uint32_t argb) {
556
+ if (!chart || key < 0 || key >= VROOM_COLOR_COUNT_) return;
557
+ if (chart->theme.colors[key] == argb) return;
558
+ chart->theme.colors[key] = argb;
559
+ chart->mark_dirty();
560
+ }
561
+
562
+ extern "C" void vroom_chart_set_float(VroomChart* chart, VroomFloatKey key, float value) {
563
+ if (!chart || key < 0 || key >= VROOM_FLOAT_COUNT_) return;
564
+ if (chart->theme.floats[key] == value) return;
565
+ chart->theme.floats[key] = value;
566
+ if (key == VROOM_FLOAT_AXIS_FONT_SIZE_PX) {
567
+ vroom::labels::recompute_axis_width(*chart);
568
+ }
569
+ chart->mark_dirty();
570
+ }
@@ -0,0 +1,30 @@
1
+ // Internal C++ helpers — not part of the public C facade.
2
+ // Consumers: VroomPictureBridge / VroomChartHostObject in the RN package.
3
+
4
+ #pragma once
5
+
6
+ #pragma clang diagnostic push
7
+ #pragma clang diagnostic ignored "-Wdocumentation"
8
+ #include "include/core/SkRefCnt.h"
9
+ #pragma clang diagnostic pop
10
+
11
+ class SkPicture;
12
+ class SkTypeface;
13
+ struct VroomChart;
14
+
15
+ namespace vroom {
16
+
17
+ // Returns the cached chart picture, rebuilding it if dirty. Caller must hold a
18
+ // ref or assign into something that does (e.g. sk_sp = ...).
19
+ sk_sp<SkPicture> render_chart_picture(VroomChart* chart);
20
+
21
+ // True if any axis label is mid-fade. JS uses this to know whether to keep
22
+ // re-rendering on a RAF loop until things settle.
23
+ bool is_animating(VroomChart* chart);
24
+
25
+ // Sets the typeface used for axis labels. Bridge layer calls this once at
26
+ // startup with a system typeface loaded via RN-Skia's font manager. Labels
27
+ // are skipped if no typeface is set.
28
+ void set_axis_typeface(sk_sp<SkTypeface> typeface);
29
+
30
+ } // namespace vroom
@@ -0,0 +1,65 @@
1
+ #include "crosshair.h"
2
+
3
+ #pragma clang diagnostic push
4
+ #pragma clang diagnostic ignored "-Wdocumentation"
5
+ #include "include/core/SkCanvas.h"
6
+ #include "include/core/SkColor.h"
7
+ #include "include/core/SkPaint.h"
8
+ #include "include/effects/SkDashPathEffect.h"
9
+ #pragma clang diagnostic pop
10
+
11
+ #include <algorithm>
12
+
13
+ #include "chart.h"
14
+ #include "theme.h"
15
+
16
+ namespace vroom::crosshair {
17
+
18
+ namespace {
19
+ constexpr float kRingRadius = 3.5f; // hollow dot at the intersection
20
+ constexpr SkScalar kDash[2] = {2.f, 2.f};
21
+ } // namespace
22
+
23
+ void draw(SkCanvas* canvas,
24
+ const VroomChart& chart,
25
+ float candle_right,
26
+ float candle_area_h,
27
+ float vline_bottom,
28
+ float snap_x) {
29
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
30
+
31
+ // Vertical line + ring snap to the nearest candle's center x; the horizontal
32
+ // line and the ring's y follow the (lifted) touch y. Clamp into the candle
33
+ // area so nothing bleeds into the axis strips.
34
+ const float cx = std::clamp(snap_x, 0.f, candle_right);
35
+ const float cy = std::clamp(chart.crosshair_y_px, 0.f, candle_area_h);
36
+
37
+ const SkColor color = chart.theme.colors[VROOM_COLOR_CROSSHAIR];
38
+
39
+ // Dashed perpendicular lines. The vertical line runs the full height of the
40
+ // candle + indicator region (down to vline_bottom) so it stays visible over
41
+ // any below-chart panes; the horizontal line spans from the left edge to the
42
+ // y-axis strip at candle_right.
43
+ SkPaint dash;
44
+ dash.setAntiAlias(true);
45
+ dash.setColor(color);
46
+ dash.setStrokeWidth(1.f);
47
+ dash.setPathEffect(SkDashPathEffect::Make(kDash, 0.f));
48
+ canvas->drawLine(cx, 0.f, cx, vline_bottom, dash);
49
+ canvas->drawLine(0.f, cy, candle_right, cy, dash);
50
+
51
+ // Punch the dashes out from under the ring so its center reads as hollow.
52
+ SkPaint hole;
53
+ hole.setAntiAlias(true);
54
+ hole.setColor(chart.theme.colors[VROOM_COLOR_BACKGROUND]);
55
+ canvas->drawCircle(cx, cy, kRingRadius, hole);
56
+
57
+ SkPaint ring;
58
+ ring.setAntiAlias(true);
59
+ ring.setColor(chart.theme.colors[VROOM_COLOR_CROSSHAIR_TARGET]);
60
+ ring.setStyle(SkPaint::kStroke_Style);
61
+ ring.setStrokeWidth(2.f); // thicker border so the dot reads clearly
62
+ canvas->drawCircle(cx, cy, kRingRadius, ring);
63
+ }
64
+
65
+ } // namespace vroom::crosshair