react-native-vroom-chart 0.5.0 → 0.7.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 (58) hide show
  1. package/android/CMakeLists.txt +143 -0
  2. package/android/README.md +60 -5
  3. package/android/build.gradle +115 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/cpp/VroomChartJni.cpp +24 -0
  6. package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
  7. package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
  8. package/cpp/VroomChartHostObject.cpp +368 -33
  9. package/cpp/VroomJsiInstaller.cpp +11 -1
  10. package/cpp/VroomSkiaContext.cpp +15 -7
  11. package/cpp/_core_include/vroom/vroom_chart.h +250 -22
  12. package/cpp/_core_src/bollinger.h +1 -1
  13. package/cpp/_core_src/candles.cpp +138 -41
  14. package/cpp/_core_src/candles.h +11 -1
  15. package/cpp/_core_src/chart.cpp +98 -40
  16. package/cpp/_core_src/chart.h +82 -20
  17. package/cpp/_core_src/chart_facade.cpp +297 -66
  18. package/cpp/_core_src/gradient.cpp +46 -0
  19. package/cpp/_core_src/gradient.h +31 -0
  20. package/cpp/_core_src/labels.cpp +49 -16
  21. package/cpp/_core_src/labels.h +33 -4
  22. package/cpp/_core_src/liquidity.cpp +3 -37
  23. package/cpp/_core_src/ma_overlay.cpp +174 -12
  24. package/cpp/_core_src/ma_overlay.h +55 -3
  25. package/cpp/_core_src/macd.cpp +13 -42
  26. package/cpp/_core_src/macd.h +11 -8
  27. package/cpp/_core_src/macd_pane.cpp +57 -27
  28. package/cpp/_core_src/price_line_layout.cpp +87 -0
  29. package/cpp/_core_src/price_line_layout.h +80 -0
  30. package/cpp/_core_src/price_lines.cpp +428 -0
  31. package/cpp/_core_src/price_lines.h +56 -0
  32. package/cpp/_core_src/rsi.cpp +4 -18
  33. package/cpp/_core_src/rsi.h +6 -6
  34. package/cpp/_core_src/rsi_pane.cpp +34 -19
  35. package/cpp/_core_src/series_ma.cpp +64 -0
  36. package/cpp/_core_src/series_ma.h +30 -0
  37. package/cpp/_core_src/style_inherit.h +35 -0
  38. package/cpp/_core_src/theme.cpp +2 -1
  39. package/cpp/_core_src/viewport.cpp +36 -12
  40. package/cpp/_core_src/viewport.h +50 -0
  41. package/cpp/_core_src/volume.cpp +33 -8
  42. package/cpp/_core_src/volume.h +12 -1
  43. package/cpp/_core_src/volume_anim.cpp +32 -0
  44. package/cpp/_core_src/volume_anim.h +41 -0
  45. package/lib/index.d.mts +261 -25
  46. package/lib/index.d.ts +261 -25
  47. package/lib/index.js +252 -36
  48. package/lib/index.js.map +1 -1
  49. package/lib/index.mjs +252 -36
  50. package/lib/index.mjs.map +1 -1
  51. package/package.json +5 -4
  52. package/src/VroomChart.tsx +162 -11
  53. package/src/easing.ts +40 -0
  54. package/src/index.ts +5 -0
  55. package/src/jsi.d.ts +124 -20
  56. package/src/theme.ts +1 -0
  57. package/src/types.ts +5 -0
  58. package/src/useChartCore.ts +166 -28
@@ -14,6 +14,7 @@
14
14
  #include "chart.h"
15
15
  #include "drawings.h"
16
16
  #include "labels.h"
17
+ #include "price_lines.h"
17
18
  #include "viewport.h"
18
19
 
19
20
  class SkCanvas;
@@ -62,6 +63,13 @@ int64_t damp_future_delta(int64_t delta_ms, int64_t cur_future,
62
63
  void apply_default_framing(VroomChart* chart) {
63
64
  if (chart->candles.empty()) return;
64
65
 
66
+ // A candle occupies the slot [time_ms, time_ms + duration) and draws centered
67
+ // in it, so the right edge of the view is the last slot's *end*. Pinning it to
68
+ // the last candle's time_ms instead puts that candle's center half a slot past
69
+ // the plot area, hiding the newest candle behind the y-axis strip.
70
+ const int64_t right_edge_ms =
71
+ chart->candles.back().time_ms + chart->candle_duration_ms;
72
+
65
73
  // Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
66
74
  // body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
67
75
  // Needs a valid layout (width_px); until size is known it falls through to
@@ -80,7 +88,7 @@ void apply_default_framing(VroomChart* chart) {
80
88
  chart->candles.back().time_ms - chart->candles.front().time_ms;
81
89
  window_ms = std::clamp<int64_t>(window_ms, chart->candle_duration_ms,
82
90
  span > 0 ? span : window_ms);
83
- chart->visible_end_ms = chart->candles.back().time_ms;
91
+ chart->visible_end_ms = right_edge_ms;
84
92
  chart->visible_start_ms = chart->visible_end_ms - window_ms;
85
93
  return;
86
94
  }
@@ -91,7 +99,7 @@ void apply_default_framing(VroomChart* chart) {
91
99
  ? chart->candles.size() - kDefaultVisible
92
100
  : 0;
93
101
  chart->visible_start_ms = chart->candles[start_idx].time_ms;
94
- chart->visible_end_ms = chart->candles.back().time_ms;
102
+ chart->visible_end_ms = right_edge_ms;
95
103
  }
96
104
 
97
105
  // On the first manual-y gesture, adopt the on-screen auto-fit bounds so the
@@ -241,6 +249,10 @@ extern "C" void vroom_chart_reset_view(VroomChart* chart) {
241
249
  chart->visible_start_ms = 0;
242
250
  chart->visible_end_ms = 0;
243
251
  chart->price_bounds_manual = false;
252
+ // An asset switch reframes wholesale, so a slot-paired morph is meaningless
253
+ // here — drop any capture rather than leaving it to reshape the wrong data.
254
+ chart->morph_from.clear();
255
+ chart->interval_morph_t = 1.f;
244
256
  apply_default_framing(chart);
245
257
  vroom::labels::recompute_axis_width(*chart);
246
258
  if (chart->cb.on_viewport_changed) {
@@ -257,6 +269,104 @@ extern "C" void vroom_chart_reset_price_scale(VroomChart* chart) {
257
269
  chart->mark_dirty();
258
270
  }
259
271
 
272
+ extern "C" bool vroom_chart_get_visible_price_envelope(VroomChart* chart,
273
+ double* out_low,
274
+ double* out_high) {
275
+ if (!chart) return false;
276
+ const auto idx = vroom::visible_indices(
277
+ chart->candles.data(), chart->candles.size(),
278
+ chart->visible_start_ms, chart->visible_end_ms);
279
+ if (idx.end <= idx.start) return false;
280
+ const auto env = vroom::price_bounds(chart->candles.data() + idx.start,
281
+ idx.end - idx.start);
282
+ if (out_low) *out_low = env.min;
283
+ if (out_high) *out_high = env.max;
284
+ return true;
285
+ }
286
+
287
+ extern "C" void vroom_chart_preserve_price_envelope(VroomChart* chart,
288
+ double prev_low,
289
+ double prev_high) {
290
+ // Auto mode already holds the envelope's pixel height constant (auto_price_
291
+ // bounds widens it by a fixed factor), so there is nothing to preserve.
292
+ if (!chart || !chart->price_bounds_manual) return;
293
+
294
+ double new_low = 0.0, new_high = 0.0;
295
+ const bool has_new =
296
+ vroom_chart_get_visible_price_envelope(chart, &new_low, &new_high);
297
+ if (!has_new || !(prev_high > prev_low) || !(new_high > new_low)) {
298
+ // Nothing to scale against — fall back to re-fitting the price scale.
299
+ vroom_chart_reset_price_scale(chart);
300
+ return;
301
+ }
302
+
303
+ chart->price_bounds = vroom::preserve_envelope_bounds(
304
+ chart->price_bounds, {prev_low, prev_high}, {new_low, new_high});
305
+ vroom::labels::recompute_axis_width(*chart);
306
+ chart->mark_dirty();
307
+ }
308
+
309
+ extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart) {
310
+ if (!chart) return;
311
+ chart->morph_from.clear();
312
+ chart->interval_morph_t = 1.f;
313
+
314
+ const auto lay = chart->layout();
315
+ const float area_w = vroom::candle_area_width(lay);
316
+ const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
317
+ if (area_w <= 0.f || window_ms <= 0) return;
318
+
319
+ const auto idx = vroom::visible_indices(
320
+ chart->candles.data(), chart->candles.size(),
321
+ chart->visible_start_ms, chart->visible_end_ms);
322
+ const std::size_t n = idx.end - idx.start;
323
+ if (n == 0) return;
324
+ const ::VroomCandle* visible = chart->candles.data() + idx.start;
325
+
326
+ const auto bounds = chart->price_bounds_manual
327
+ ? chart->price_bounds
328
+ : vroom::auto_price_bounds(visible, n);
329
+
330
+ // Normalized so the capture is independent of both the price bounds (which
331
+ // the swap is about to replace) and the surface size (which may change
332
+ // mid-morph). Newest candle first, matching the slot indexing in
333
+ // candles::draw.
334
+ chart->morph_from.resize(n);
335
+ for (std::size_t k = 0; k < n; ++k) {
336
+ const auto& c = visible[n - 1 - k];
337
+ chart->morph_from[k] = vroom::CandleSnapshot{
338
+ vroom::candle_center_x(lay, c.time_ms, chart->candle_duration_ms,
339
+ chart->visible_start_ms, window_ms) / area_w,
340
+ static_cast<float>(vroom::price_fraction(bounds, c.open)),
341
+ static_cast<float>(vroom::price_fraction(bounds, c.high)),
342
+ static_cast<float>(vroom::price_fraction(bounds, c.low)),
343
+ static_cast<float>(vroom::price_fraction(bounds, c.close)),
344
+ c.close >= c.open,
345
+ };
346
+ }
347
+
348
+ // The scale the capture was taken against — the axes render their outgoing
349
+ // ticks from it, and it's about to be replaced on the chart itself.
350
+ chart->morph_from_bounds = bounds;
351
+ chart->morph_from_start_ms = chart->visible_start_ms;
352
+ chart->morph_from_end_ms = chart->visible_end_ms;
353
+
354
+ // Open the morph at 0 rather than leaving it at 1: the caller still has to
355
+ // push the new candles, and any frame painted in between should show the
356
+ // captured geometry — which is what the pre-switch frame looked like.
357
+ chart->interval_morph_t = 0.f;
358
+ }
359
+
360
+ extern "C" void vroom_chart_set_interval_morph(VroomChart* chart, float t) {
361
+ if (!chart) return;
362
+ chart->interval_morph_t = std::clamp(t, 0.f, 1.f);
363
+ if (chart->interval_morph_t >= 1.f) {
364
+ chart->morph_from.clear();
365
+ chart->morph_from.shrink_to_fit();
366
+ }
367
+ chart->mark_dirty();
368
+ }
369
+
260
370
  extern "C" void vroom_chart_pan(VroomChart* chart, float dx_px, float /*dy_px*/) {
261
371
  if (!chart || dx_px == 0.f || chart->candles.empty()) return;
262
372
 
@@ -480,7 +590,7 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
480
590
  extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px) {
481
591
  if (!chart || dy_px == 0.f) return;
482
592
 
483
- const int pane_count = (chart->rsi_enabled ? 1 : 0) + (chart->macd_enabled ? 1 : 0);
593
+ const int pane_count = (chart->rsi.enabled ? 1 : 0) + (chart->macd.enabled ? 1 : 0);
484
594
  if (pane_count == 0) return; // nothing below the chart to resize
485
595
 
486
596
  const auto lay = chart->layout();
@@ -534,8 +644,8 @@ extern "C" void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px,
534
644
  struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
535
645
  ActivePane panes[2];
536
646
  int count = 0;
537
- if (chart->rsi_enabled) panes[count++] = {chart->rsi_order, 0};
538
- if (chart->macd_enabled) panes[count++] = {chart->macd_order, 1};
647
+ if (chart->rsi.enabled) panes[count++] = {chart->rsi_order, 0};
648
+ if (chart->macd.enabled) panes[count++] = {chart->macd_order, 1};
539
649
  if (count == 0) return;
540
650
  if (count == 2 && panes[0].order > panes[1].order) {
541
651
  const ActivePane tmp = panes[0];
@@ -812,6 +922,70 @@ extern "C" void vroom_chart_set_liquidity(VroomChart* chart,
812
922
  chart->mark_dirty();
813
923
  }
814
924
 
925
+ // ---- Price status lines ---------------------------------------------------
926
+
927
+ extern "C" void vroom_chart_set_price_lines(VroomChart* chart,
928
+ const VroomPriceLine* lines,
929
+ size_t count,
930
+ const VroomPriceLineStyle* style) {
931
+ if (!chart) return;
932
+ // Deep-copy into the owning form: the label strings live in the chart, so the
933
+ // caller's buffers may be freed as soon as this returns.
934
+ chart->price_lines.clear();
935
+ chart->price_lines.reserve(count);
936
+ for (size_t i = 0; i < count; ++i) {
937
+ const VroomPriceLine& src = lines[i];
938
+ VroomChart::StoredPriceLine pl;
939
+ pl.price = src.price;
940
+ pl.color = src.color;
941
+ pl.width = src.width;
942
+ pl.line_style = src.line_style;
943
+ if (src.text) pl.text = src.text;
944
+ if (src.quantity) pl.quantity = src.quantity;
945
+ pl.flags = src.flags;
946
+ chart->price_lines.push_back(std::move(pl));
947
+ }
948
+ if (style) chart->price_line_style = *style;
949
+ // Drop interaction state that no longer refers to a live line — otherwise a
950
+ // removed line would leave a stuck hover highlight or drag preview.
951
+ const auto n = static_cast<int32_t>(count);
952
+ if (chart->hovered_price_line >= n) {
953
+ chart->hovered_price_line = -1;
954
+ chart->hovered_price_line_part = -1;
955
+ }
956
+ if (chart->dragged_price_line >= n) chart->dragged_price_line = -1;
957
+ chart->mark_dirty();
958
+ }
959
+
960
+ extern "C" void vroom_chart_set_price_line_hover(VroomChart* chart, int32_t index,
961
+ int32_t part) {
962
+ if (!chart) return;
963
+ if (index < 0 || index >= static_cast<int32_t>(chart->price_lines.size())) {
964
+ index = -1;
965
+ part = -1;
966
+ }
967
+ if (chart->hovered_price_line == index && chart->hovered_price_line_part == part) {
968
+ return; // hover fires on every pointer move; don't redraw for nothing
969
+ }
970
+ chart->hovered_price_line = index;
971
+ chart->hovered_price_line_part = part;
972
+ chart->mark_dirty();
973
+ }
974
+
975
+ extern "C" void vroom_chart_set_price_line_drag(VroomChart* chart, int32_t index,
976
+ double price) {
977
+ if (!chart) return;
978
+ if (index < 0 || index >= static_cast<int32_t>(chart->price_lines.size())) {
979
+ if (chart->dragged_price_line == -1) return;
980
+ chart->dragged_price_line = -1;
981
+ chart->mark_dirty();
982
+ return;
983
+ }
984
+ chart->dragged_price_line = index;
985
+ chart->dragged_price_line_price = price;
986
+ chart->mark_dirty();
987
+ }
988
+
815
989
  extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
816
990
  double a_price, bool has_b, int64_t b_time,
817
991
  double b_price, bool guide, uint32_t color,
@@ -926,6 +1100,32 @@ extern "C" bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px,
926
1100
  return true;
927
1101
  }
928
1102
 
1103
+ extern "C" bool vroom_chart_hit_test_price_line(VroomChart* chart, float x_px,
1104
+ float y_px, int32_t* out_index,
1105
+ int32_t* out_part) {
1106
+ if (!chart || chart->price_lines.empty() || chart->candles.empty()) return false;
1107
+ const auto lay = chart->layout();
1108
+ // Same bounds and pane geometry as draw_chart, so the grab regions line up
1109
+ // with what's on screen.
1110
+ const auto range = vroom::visible_indices(
1111
+ chart->candles.data(), chart->candles.size(),
1112
+ chart->visible_start_ms, chart->visible_end_ms);
1113
+ const size_t n = range.end - range.start;
1114
+ const auto bounds =
1115
+ chart->price_bounds_manual
1116
+ ? chart->price_bounds
1117
+ : vroom::auto_price_bounds(chart->candles.data() + range.start, n);
1118
+ const float candle_area_h = vroom::price_pane_bottom(lay);
1119
+ const float candle_right =
1120
+ chart->width_px - lay.y_axis_width_px - lay.right_padding_px;
1121
+ const auto hit = vroom::price_lines::hit_test(*chart, lay, bounds, candle_right,
1122
+ candle_area_h, x_px, y_px);
1123
+ if (hit.index < 0) return false;
1124
+ if (out_index) *out_index = hit.index;
1125
+ if (out_part) *out_part = hit.part;
1126
+ return true;
1127
+ }
1128
+
929
1129
  extern "C" void vroom_chart_set_selected_drawing(VroomChart* chart,
930
1130
  int32_t index,
931
1131
  int32_t grabbed_endpoint) {
@@ -968,61 +1168,66 @@ extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
968
1168
 
969
1169
  // ---- Indicators -----------------------------------------------------------
970
1170
 
971
- extern "C" void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,
972
- double upper_band, double lower_band,
973
- bool ma_enabled, int ma_period) {
974
- if (!chart) return;
975
- if (period < 2) period = 2;
976
- if (ma_period < 1) ma_period = 1;
977
- upper_band = std::clamp(upper_band, 0.0, 100.0);
978
- lower_band = std::clamp(lower_band, 0.0, 100.0);
979
-
980
- // Only the series-affecting fields force a recompute; band changes are
981
- // render-only.
982
- const bool recompute = chart->rsi_enabled != enabled ||
983
- chart->rsi_period != period ||
984
- chart->rsi_ma_enabled != ma_enabled ||
985
- chart->rsi_ma_period != ma_period;
986
- const bool changed = recompute || chart->rsi_upper != upper_band ||
987
- chart->rsi_lower != lower_band;
988
- if (!changed) return;
1171
+ extern "C" void vroom_chart_set_rsi(VroomChart* chart, const VroomRSI* cfg) {
1172
+ if (!chart || !cfg) return;
1173
+ VroomRSI next = *cfg;
1174
+ next.enabled = next.enabled ? 1 : 0;
1175
+ next.ma_visible = next.ma_visible ? 1 : 0;
1176
+ next.line_visible = next.line_visible ? 1 : 0;
1177
+ next.bands_visible = next.bands_visible ? 1 : 0;
1178
+ if (next.period < 2) next.period = 2;
1179
+ if (next.ma_period < 1) next.ma_period = 1;
1180
+ next.upper_band = std::clamp(next.upper_band, 0.0, 100.0);
1181
+ next.lower_band = std::clamp(next.lower_band, 0.0, 100.0);
1182
+
1183
+ // Only the series-affecting fields force a recompute; band levels, color,
1184
+ // width, and visibility changes are render-only. The trendline is one of
1185
+ // them: it isn't computed while hidden, so revealing it needs a recompute.
1186
+ const VroomRSI& cur = chart->rsi;
1187
+ const bool recompute = cur.enabled != next.enabled ||
1188
+ cur.period != next.period ||
1189
+ cur.ma_visible != next.ma_visible ||
1190
+ cur.ma_period != next.ma_period ||
1191
+ cur.ma_kind != next.ma_kind;
989
1192
 
990
1193
  // Pane order: claim the next slot on an off->on transition, release on off.
991
- if (enabled && !chart->rsi_enabled) chart->rsi_order = chart->pane_seq++;
992
- else if (!enabled) chart->rsi_order = -1;
993
-
994
- chart->rsi_enabled = enabled;
995
- chart->rsi_period = period;
996
- chart->rsi_upper = upper_band;
997
- chart->rsi_lower = lower_band;
998
- chart->rsi_ma_enabled = ma_enabled;
999
- chart->rsi_ma_period = ma_period;
1194
+ if (next.enabled && !cur.enabled) chart->rsi_order = chart->pane_seq++;
1195
+ else if (!next.enabled) chart->rsi_order = -1;
1196
+
1197
+ chart->rsi = next;
1000
1198
  if (recompute) chart->rsi_dirty = true;
1001
1199
  chart->mark_dirty();
1002
1200
  }
1003
1201
 
1004
- extern "C" void vroom_chart_set_macd(VroomChart* chart, bool enabled, int fast,
1005
- int slow, int signal) {
1006
- if (!chart) return;
1007
- if (fast < 1) fast = 1;
1008
- if (slow < 1) slow = 1;
1009
- if (slow <= fast) slow = fast + 1;
1010
- if (signal < 1) signal = 1;
1011
-
1012
- const bool recompute = chart->macd_enabled != enabled ||
1013
- chart->macd_fast != fast ||
1014
- chart->macd_slow != slow ||
1015
- chart->macd_signal != signal;
1016
- if (!recompute) return;
1017
-
1018
- if (enabled && !chart->macd_enabled) chart->macd_order = chart->pane_seq++;
1019
- else if (!enabled) chart->macd_order = -1;
1020
-
1021
- chart->macd_enabled = enabled;
1022
- chart->macd_fast = fast;
1023
- chart->macd_slow = slow;
1024
- chart->macd_signal = signal;
1025
- chart->macd_dirty = true;
1202
+ extern "C" void vroom_chart_set_macd(VroomChart* chart, const VroomMACD* cfg) {
1203
+ if (!chart || !cfg) return;
1204
+ VroomMACD next = *cfg;
1205
+ next.enabled = next.enabled ? 1 : 0;
1206
+ next.line_visible = next.line_visible ? 1 : 0;
1207
+ next.signal_visible = next.signal_visible ? 1 : 0;
1208
+ next.hist_visible = next.hist_visible ? 1 : 0;
1209
+ next.zero_visible = next.zero_visible ? 1 : 0;
1210
+ if (next.fast < 1) next.fast = 1;
1211
+ if (next.slow < 1) next.slow = 1;
1212
+ if (next.slow <= next.fast) next.slow = next.fast + 1;
1213
+ if (next.signal < 1) next.signal = 1;
1214
+
1215
+ // Only the series-affecting fields force a recompute; color, width, and
1216
+ // visibility changes are render-only.
1217
+ const VroomMACD& cur = chart->macd;
1218
+ const bool recompute = cur.enabled != next.enabled ||
1219
+ cur.fast != next.fast ||
1220
+ cur.slow != next.slow ||
1221
+ cur.signal != next.signal ||
1222
+ cur.source != next.source ||
1223
+ cur.ma_kind != next.ma_kind ||
1224
+ cur.signal_ma_kind != next.signal_ma_kind;
1225
+
1226
+ if (next.enabled && !cur.enabled) chart->macd_order = chart->pane_seq++;
1227
+ else if (!next.enabled) chart->macd_order = -1;
1228
+
1229
+ chart->macd = next;
1230
+ if (recompute) chart->macd_dirty = true;
1026
1231
  chart->mark_dirty();
1027
1232
  }
1028
1233
 
@@ -1035,19 +1240,21 @@ extern "C" void vroom_chart_set_overlays(VroomChart* chart,
1035
1240
  chart->mark_dirty();
1036
1241
  }
1037
1242
 
1038
- extern "C" void vroom_chart_set_vwap(VroomChart* chart, bool enabled,
1039
- int reset_offset_min, uint32_t color,
1040
- float width) {
1041
- if (!chart) return;
1243
+ extern "C" void vroom_chart_set_vwap(VroomChart* chart, const VroomVWAP* cfg) {
1244
+ if (!chart || !cfg) return;
1245
+ VroomVWAP next = *cfg;
1246
+ next.enabled = next.enabled ? 1 : 0;
1042
1247
  // Keep the offset within a day [0, 1440).
1043
- int off = reset_offset_min % 1440;
1044
- if (off < 0) off += 1440;
1045
- const bool recompute =
1046
- chart->vwap_enabled != enabled || chart->vwap_reset_offset_min != off;
1047
- chart->vwap_enabled = enabled;
1048
- chart->vwap_reset_offset_min = off;
1049
- chart->vwap_color = color;
1050
- chart->vwap_width = width;
1248
+ next.reset_offset_min %= 1440;
1249
+ if (next.reset_offset_min < 0) next.reset_offset_min += 1440;
1250
+
1251
+ // Only the series-affecting fields force a recompute; color and width
1252
+ // changes are render-only.
1253
+ const VroomVWAP& cur = chart->vwap;
1254
+ const bool recompute = cur.enabled != next.enabled ||
1255
+ cur.reset_offset_min != next.reset_offset_min;
1256
+
1257
+ chart->vwap = next;
1051
1258
  if (recompute) chart->vwap_dirty = true;
1052
1259
  chart->mark_dirty();
1053
1260
  }
@@ -1075,6 +1282,30 @@ extern "C" void vroom_chart_set_bollinger(VroomChart* chart,
1075
1282
  chart->mark_dirty();
1076
1283
  }
1077
1284
 
1285
+ extern "C" void vroom_chart_set_volume(VroomChart* chart,
1286
+ const VroomVolume* cfg) {
1287
+ if (!chart || !cfg) return;
1288
+ VroomVolume next = *cfg;
1289
+ next.enabled = next.enabled ? 1 : 0;
1290
+ // Negative means "inherit", so only clamp the ranges once a value is set.
1291
+ if (next.height_frac >= 0.f) next.height_frac = std::min(next.height_frac, 1.f);
1292
+ if (next.opacity >= 0.f) next.opacity = std::min(next.opacity, 1.f);
1293
+ chart->volume = next;
1294
+ // Snap the collapse to the new target, the way set_chart_type snaps the
1295
+ // candle↔line morph. A host that wants the toggle animated overrides this
1296
+ // from its frame loop before the next paint.
1297
+ chart->volume_collapse_t = next.enabled ? 0.f : 1.f;
1298
+ chart->mark_dirty();
1299
+ }
1300
+
1301
+ extern "C" void vroom_chart_set_volume_collapse(VroomChart* chart, float t,
1302
+ int32_t easing) {
1303
+ if (!chart) return;
1304
+ chart->volume_collapse_t = std::clamp(t, 0.f, 1.f);
1305
+ chart->volume_collapse_easing = easing;
1306
+ chart->mark_dirty();
1307
+ }
1308
+
1078
1309
  // ---- Direct draw (used by hosts that don't need the SkPicture cache) ------
1079
1310
 
1080
1311
  extern "C" void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas) {
@@ -0,0 +1,46 @@
1
+ #include "gradient.h"
2
+
3
+ #pragma clang diagnostic push
4
+ #pragma clang diagnostic ignored "-Wdocumentation"
5
+ #include "include/core/SkPoint.h"
6
+ #include "include/core/SkShader.h"
7
+ #include "include/core/SkTileMode.h"
8
+ // See gradient.h: the two Skia versions we build against ship different
9
+ // linear-gradient headers, and neither ships both.
10
+ #if __has_include("include/effects/SkGradient.h")
11
+ # include "include/core/SkSpan.h"
12
+ # include "include/effects/SkGradient.h"
13
+ # define VROOM_SK_MODERN_GRADIENT 1
14
+ #else
15
+ # include "include/effects/SkGradientShader.h"
16
+ # define VROOM_SK_MODERN_GRADIENT 0
17
+ #endif
18
+ #pragma clang diagnostic pop
19
+
20
+ namespace vroom {
21
+
22
+ sk_sp<SkShader> linear_alpha_ramp(const SkPoint pts[2],
23
+ SkColor base,
24
+ float a0,
25
+ float a1) {
26
+ #if VROOM_SK_MODERN_GRADIENT
27
+ const float r = SkColorGetR(base) / 255.f;
28
+ const float g = SkColorGetG(base) / 255.f;
29
+ const float b = SkColorGetB(base) / 255.f;
30
+ const SkColor4f colors[2] = {SkColor4f{r, g, b, a0}, SkColor4f{r, g, b, a1}};
31
+ const SkGradient::Colors grad_colors(
32
+ SkSpan<const SkColor4f>(colors, 2), SkTileMode::kClamp);
33
+ const SkGradient grad(grad_colors, SkGradient::Interpolation{});
34
+ return SkShaders::LinearGradient(pts, grad);
35
+ #else
36
+ const auto to_u8 = [](float a) {
37
+ return static_cast<U8CPU>(a * 255.f + 0.5f);
38
+ };
39
+ const SkColor colors[2] = {SkColorSetA(base, to_u8(a0)),
40
+ SkColorSetA(base, to_u8(a1))};
41
+ return SkGradientShader::MakeLinear(pts, colors, nullptr, 2,
42
+ SkTileMode::kClamp);
43
+ #endif
44
+ }
45
+
46
+ } // namespace vroom
@@ -0,0 +1,31 @@
1
+ // Linear-gradient helper shared by the layers that fade a fill out.
2
+ //
3
+ // Skia's linear-gradient API differs across the Skia versions we build against:
4
+ // the WASM build uses a newer Skia (SkGradient.h + SkShaders::LinearGradient),
5
+ // while react-native-skia bundles an older one (SkGradientShader::MakeLinear).
6
+ // Neither ships both headers, so the implementation selects whichever is present
7
+ // and callers get one signature regardless of platform.
8
+
9
+ #pragma once
10
+
11
+ #pragma clang diagnostic push
12
+ #pragma clang diagnostic ignored "-Wdocumentation"
13
+ #include "include/core/SkColor.h"
14
+ #include "include/core/SkRefCnt.h"
15
+ #pragma clang diagnostic pop
16
+
17
+ struct SkPoint;
18
+ class SkShader;
19
+
20
+ namespace vroom {
21
+
22
+ // A gradient along pts[0] -> pts[1] holding `base`'s RGB while ramping alpha
23
+ // from `a0` to `a1`. RGB is held constant on purpose: interpolating toward a
24
+ // transparent black instead would darken the faded end into a grey smear.
25
+ // Clamps at both ends, so the shader can safely cover more than the fill does.
26
+ sk_sp<SkShader> linear_alpha_ramp(const SkPoint pts[2],
27
+ SkColor base,
28
+ float a0,
29
+ float a1);
30
+
31
+ } // namespace vroom