react-native-vroom-chart 0.6.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 (46) hide show
  1. package/cpp/VroomChartHostObject.cpp +150 -32
  2. package/cpp/_core_include/vroom/vroom_chart.h +180 -22
  3. package/cpp/_core_src/bollinger.h +1 -1
  4. package/cpp/_core_src/candles.cpp +138 -41
  5. package/cpp/_core_src/candles.h +11 -1
  6. package/cpp/_core_src/chart.cpp +91 -40
  7. package/cpp/_core_src/chart.h +55 -20
  8. package/cpp/_core_src/chart_facade.cpp +206 -66
  9. package/cpp/_core_src/gradient.cpp +46 -0
  10. package/cpp/_core_src/gradient.h +31 -0
  11. package/cpp/_core_src/labels.cpp +49 -16
  12. package/cpp/_core_src/labels.h +33 -4
  13. package/cpp/_core_src/liquidity.cpp +3 -37
  14. package/cpp/_core_src/ma_overlay.cpp +174 -12
  15. package/cpp/_core_src/ma_overlay.h +55 -3
  16. package/cpp/_core_src/macd.cpp +13 -42
  17. package/cpp/_core_src/macd.h +11 -8
  18. package/cpp/_core_src/macd_pane.cpp +57 -27
  19. package/cpp/_core_src/price_line_layout.h +1 -1
  20. package/cpp/_core_src/rsi.cpp +4 -18
  21. package/cpp/_core_src/rsi.h +6 -6
  22. package/cpp/_core_src/rsi_pane.cpp +34 -19
  23. package/cpp/_core_src/series_ma.cpp +64 -0
  24. package/cpp/_core_src/series_ma.h +30 -0
  25. package/cpp/_core_src/style_inherit.h +35 -0
  26. package/cpp/_core_src/theme.cpp +2 -1
  27. package/cpp/_core_src/viewport.cpp +36 -12
  28. package/cpp/_core_src/viewport.h +50 -0
  29. package/cpp/_core_src/volume.cpp +33 -8
  30. package/cpp/_core_src/volume.h +12 -1
  31. package/cpp/_core_src/volume_anim.cpp +32 -0
  32. package/cpp/_core_src/volume_anim.h +41 -0
  33. package/lib/index.d.mts +161 -28
  34. package/lib/index.d.ts +161 -28
  35. package/lib/index.js +156 -31
  36. package/lib/index.js.map +1 -1
  37. package/lib/index.mjs +156 -31
  38. package/lib/index.mjs.map +1 -1
  39. package/package.json +3 -3
  40. package/src/VroomChart.tsx +69 -3
  41. package/src/easing.ts +40 -0
  42. package/src/index.ts +3 -0
  43. package/src/jsi.d.ts +76 -20
  44. package/src/theme.ts +1 -0
  45. package/src/types.ts +3 -0
  46. package/src/useChartCore.ts +103 -27
@@ -64,6 +64,21 @@ struct VroomChart {
64
64
  float morph_collapse = 0.f;
65
65
  float morph_fade = 0.f;
66
66
 
67
+ // Interval morph: the outgoing candle geometry captured when a timeframe
68
+ // switch begins, indexed from the right of the visible slice (slot 0 =
69
+ // newest) — the pairing the preserved slot grid guarantees. Stored as
70
+ // normalized fractions so it survives the new bounds and a resize.
71
+ // Empty when not morphing.
72
+ std::vector<vroom::CandleSnapshot> morph_from;
73
+ float interval_morph_t = 1.f; // 1 = not morphing
74
+ // The pre-switch price scale and time window. The candle capture above is
75
+ // normalized against these, and the axes keep rendering their old ticks from
76
+ // them while fading out (see labels::interval_phase), so a label is never
77
+ // drawn at a position it didn't already occupy.
78
+ vroom::PriceBounds morph_from_bounds{0.0, 1.0};
79
+ int64_t morph_from_start_ms = 0;
80
+ int64_t morph_from_end_ms = 0;
81
+
67
82
  // Cached y-axis width in pixels, sized to fit the widest formatted price
68
83
  // label. 0 = uncomputed; layout() falls back to a width ratio.
69
84
  float axis_width_px = 0.f;
@@ -86,24 +101,27 @@ struct VroomChart {
86
101
  // RSI, drawn in a pane below the candles. rsi_cache is aligned to `candles`
87
102
  // (one value per candle, NaN where undefined) and recomputed lazily by
88
103
  // ensure_rsi() when rsi_dirty (set on any data or config change).
89
- bool rsi_enabled = false;
90
- int rsi_period = 14;
91
- double rsi_upper = 70.0; // overbought band
92
- double rsi_lower = 30.0; // oversold band
93
- bool rsi_ma_enabled = true; // RSI-based moving average (trendline)
94
- int rsi_ma_period = 14;
104
+ // Defaults: 14-period RSI with 70/30 bands and a 14-period SMA trendline,
105
+ // every style field on its inherit sentinel.
106
+ VroomRSI rsi{0, 14, 70.0, 30.0, 14, 0, 1,
107
+ 0u, -1.f, 1, // RSI line
108
+ 0u, -1.f, // trendline
109
+ 0u, 1}; // band rules
95
110
  std::vector<double> rsi_cache; // RSI per candle (NaN where undefined)
96
- std::vector<double> rsi_ma_cache; // SMA of rsi_cache (empty if MA off)
111
+ std::vector<double> rsi_ma_cache; // MA of rsi_cache (empty if MA off)
97
112
  bool rsi_dirty = true;
98
113
  // User y-axis zoom for the RSI pane (drag on its y-axis strip). 1.0 = the
99
114
  // default 0..100 fit; >1 zooms in (taller), <1 zooms out. Anchored at 50.
100
115
  double rsi_y_scale = 1.0;
101
116
 
102
117
  // MACD, drawn in its own pane. Caches aligned to `candles` (NaN warmup).
103
- bool macd_enabled = false;
104
- int macd_fast = 12;
105
- int macd_slow = 26;
106
- int macd_signal = 9;
118
+ // Defaults: 12/26/9 EMAs of close, every style field on its inherit
119
+ // sentinel so an untouched chart keeps the stock look.
120
+ VroomMACD macd{0, 12, 26, 9, 0, 1, 1,
121
+ 0u, -1.f, 1, // MACD line
122
+ 0u, -1.f, 1, // signal line
123
+ 1, 0u, 0u, 0u, 0u, // histogram
124
+ 0u, 1}; // zero line
107
125
  std::vector<double> macd_cache;
108
126
  std::vector<double> macd_signal_cache;
109
127
  std::vector<double> macd_hist_cache;
@@ -128,10 +146,8 @@ struct VroomChart {
128
146
 
129
147
  // VWAP overlay (session anchor, configurable reset time). Single line on the
130
148
  // price pane; vwap_breaks marks session resets so the line lifts the pen.
131
- bool vwap_enabled = false;
132
- int vwap_reset_offset_min = 0; // session boundary offset from UTC midnight
133
- uint32_t vwap_color = 0xff00bcd4; // cyan
134
- float vwap_width = 1.5f;
149
+ // Style fields sit on their inherit sentinel (cyan, 1.5px).
150
+ VroomVWAP vwap{0, 0, 0u, -1.f};
135
151
  std::vector<double> vwap_cache;
136
152
  std::vector<unsigned char> vwap_breaks;
137
153
  bool vwap_dirty = true;
@@ -150,6 +166,18 @@ struct VroomChart {
150
166
  std::vector<double> bb_lower_cache;
151
167
  bool bollinger_dirty = true;
152
168
 
169
+ // Volume bars (price pane, under the candles). On by default with every
170
+ // style field left on its inherit sentinel, so an untouched chart looks
171
+ // exactly as it did before the config existed. No cache — bar heights come
172
+ // straight off the visible candles' volume.
173
+ VroomVolume volume{1, -1.f, -1.f, -1.f, 0u, 0u};
174
+ // Staggered collapse of those bars, driven per-frame by the host animation
175
+ // loop: 0 = full height, 1 = all flat. Doubles as the visibility gate, since
176
+ // a fully collapsed chart draws nothing — set_volume snaps it to match
177
+ // `volume.enabled`, and the host overrides it while animating.
178
+ float volume_collapse_t = 0.f;
179
+ int32_t volume_collapse_easing = VROOM_EASING_IN_OUT;
180
+
153
181
  // --- drawings (annotations) --------------------------------------------
154
182
  // Committed drawings, anchored in data space so they track the candles on
155
183
  // pan/zoom. Drawn on the price pane above candles/overlays.
@@ -235,9 +263,9 @@ struct VroomChart {
235
263
  std::chrono::steady_clock::time_point last_anim_tick{};
236
264
  bool anim_started = false;
237
265
 
238
- // dt of the current rebuild — populated by rebuild_chart_picture and
239
- // consumed by the label-fade updaters. Public so the labels namespace
240
- // can read it without a getter.
266
+ // dt of the current frame — populated by begin_frame and consumed by the
267
+ // label-fade updaters. Public so the labels namespace can read it without a
268
+ // getter.
241
269
  float last_dt_seconds = 0.f;
242
270
 
243
271
  // --- methods ------------------------------------------------------------
@@ -267,11 +295,18 @@ struct VroomChart {
267
295
  // indicator is enabled.
268
296
  void ensure_bollinger();
269
297
 
270
- // The main drawing pass. Calls into the labels and candles modules.
298
+ // The main drawing pass. Calls into the labels and candles modules, and owns
299
+ // the per-frame animation clock (see begin_frame) so every host gets it —
300
+ // the web build draws straight through vroom_chart_draw rather than the
301
+ // SkPicture cache below.
271
302
  void draw_chart(SkCanvas* canvas);
272
303
 
304
+ // Per-frame animation bookkeeping: computes `last_dt_seconds`, which paces
305
+ // the label fades. Called at the top of draw_chart.
306
+ void begin_frame();
307
+
273
308
  // Re-records `chart_picture` by invoking draw_chart on a fresh
274
- // SkPictureRecorder. Also computes `last_dt_seconds` for fade animation.
309
+ // SkPictureRecorder.
275
310
  void rebuild_chart_picture();
276
311
 
277
312
  // True if any axis label is mid-fade. Used by the JS-side animation loop
@@ -63,6 +63,13 @@ int64_t damp_future_delta(int64_t delta_ms, int64_t cur_future,
63
63
  void apply_default_framing(VroomChart* chart) {
64
64
  if (chart->candles.empty()) return;
65
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
+
66
73
  // Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
67
74
  // body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
68
75
  // Needs a valid layout (width_px); until size is known it falls through to
@@ -81,7 +88,7 @@ void apply_default_framing(VroomChart* chart) {
81
88
  chart->candles.back().time_ms - chart->candles.front().time_ms;
82
89
  window_ms = std::clamp<int64_t>(window_ms, chart->candle_duration_ms,
83
90
  span > 0 ? span : window_ms);
84
- chart->visible_end_ms = chart->candles.back().time_ms;
91
+ chart->visible_end_ms = right_edge_ms;
85
92
  chart->visible_start_ms = chart->visible_end_ms - window_ms;
86
93
  return;
87
94
  }
@@ -92,7 +99,7 @@ void apply_default_framing(VroomChart* chart) {
92
99
  ? chart->candles.size() - kDefaultVisible
93
100
  : 0;
94
101
  chart->visible_start_ms = chart->candles[start_idx].time_ms;
95
- chart->visible_end_ms = chart->candles.back().time_ms;
102
+ chart->visible_end_ms = right_edge_ms;
96
103
  }
97
104
 
98
105
  // On the first manual-y gesture, adopt the on-screen auto-fit bounds so the
@@ -242,6 +249,10 @@ extern "C" void vroom_chart_reset_view(VroomChart* chart) {
242
249
  chart->visible_start_ms = 0;
243
250
  chart->visible_end_ms = 0;
244
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;
245
256
  apply_default_framing(chart);
246
257
  vroom::labels::recompute_axis_width(*chart);
247
258
  if (chart->cb.on_viewport_changed) {
@@ -258,6 +269,104 @@ extern "C" void vroom_chart_reset_price_scale(VroomChart* chart) {
258
269
  chart->mark_dirty();
259
270
  }
260
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
+
261
370
  extern "C" void vroom_chart_pan(VroomChart* chart, float dx_px, float /*dy_px*/) {
262
371
  if (!chart || dx_px == 0.f || chart->candles.empty()) return;
263
372
 
@@ -481,7 +590,7 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
481
590
  extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px) {
482
591
  if (!chart || dy_px == 0.f) return;
483
592
 
484
- 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);
485
594
  if (pane_count == 0) return; // nothing below the chart to resize
486
595
 
487
596
  const auto lay = chart->layout();
@@ -535,8 +644,8 @@ extern "C" void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px,
535
644
  struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
536
645
  ActivePane panes[2];
537
646
  int count = 0;
538
- if (chart->rsi_enabled) panes[count++] = {chart->rsi_order, 0};
539
- 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};
540
649
  if (count == 0) return;
541
650
  if (count == 2 && panes[0].order > panes[1].order) {
542
651
  const ActivePane tmp = panes[0];
@@ -1059,61 +1168,66 @@ extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
1059
1168
 
1060
1169
  // ---- Indicators -----------------------------------------------------------
1061
1170
 
1062
- extern "C" void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,
1063
- double upper_band, double lower_band,
1064
- bool ma_enabled, int ma_period) {
1065
- if (!chart) return;
1066
- if (period < 2) period = 2;
1067
- if (ma_period < 1) ma_period = 1;
1068
- upper_band = std::clamp(upper_band, 0.0, 100.0);
1069
- lower_band = std::clamp(lower_band, 0.0, 100.0);
1070
-
1071
- // Only the series-affecting fields force a recompute; band changes are
1072
- // render-only.
1073
- const bool recompute = chart->rsi_enabled != enabled ||
1074
- chart->rsi_period != period ||
1075
- chart->rsi_ma_enabled != ma_enabled ||
1076
- chart->rsi_ma_period != ma_period;
1077
- const bool changed = recompute || chart->rsi_upper != upper_band ||
1078
- chart->rsi_lower != lower_band;
1079
- 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;
1080
1192
 
1081
1193
  // Pane order: claim the next slot on an off->on transition, release on off.
1082
- if (enabled && !chart->rsi_enabled) chart->rsi_order = chart->pane_seq++;
1083
- else if (!enabled) chart->rsi_order = -1;
1084
-
1085
- chart->rsi_enabled = enabled;
1086
- chart->rsi_period = period;
1087
- chart->rsi_upper = upper_band;
1088
- chart->rsi_lower = lower_band;
1089
- chart->rsi_ma_enabled = ma_enabled;
1090
- 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;
1091
1198
  if (recompute) chart->rsi_dirty = true;
1092
1199
  chart->mark_dirty();
1093
1200
  }
1094
1201
 
1095
- extern "C" void vroom_chart_set_macd(VroomChart* chart, bool enabled, int fast,
1096
- int slow, int signal) {
1097
- if (!chart) return;
1098
- if (fast < 1) fast = 1;
1099
- if (slow < 1) slow = 1;
1100
- if (slow <= fast) slow = fast + 1;
1101
- if (signal < 1) signal = 1;
1102
-
1103
- const bool recompute = chart->macd_enabled != enabled ||
1104
- chart->macd_fast != fast ||
1105
- chart->macd_slow != slow ||
1106
- chart->macd_signal != signal;
1107
- if (!recompute) return;
1108
-
1109
- if (enabled && !chart->macd_enabled) chart->macd_order = chart->pane_seq++;
1110
- else if (!enabled) chart->macd_order = -1;
1111
-
1112
- chart->macd_enabled = enabled;
1113
- chart->macd_fast = fast;
1114
- chart->macd_slow = slow;
1115
- chart->macd_signal = signal;
1116
- 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;
1117
1231
  chart->mark_dirty();
1118
1232
  }
1119
1233
 
@@ -1126,19 +1240,21 @@ extern "C" void vroom_chart_set_overlays(VroomChart* chart,
1126
1240
  chart->mark_dirty();
1127
1241
  }
1128
1242
 
1129
- extern "C" void vroom_chart_set_vwap(VroomChart* chart, bool enabled,
1130
- int reset_offset_min, uint32_t color,
1131
- float width) {
1132
- 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;
1133
1247
  // Keep the offset within a day [0, 1440).
1134
- int off = reset_offset_min % 1440;
1135
- if (off < 0) off += 1440;
1136
- const bool recompute =
1137
- chart->vwap_enabled != enabled || chart->vwap_reset_offset_min != off;
1138
- chart->vwap_enabled = enabled;
1139
- chart->vwap_reset_offset_min = off;
1140
- chart->vwap_color = color;
1141
- 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;
1142
1258
  if (recompute) chart->vwap_dirty = true;
1143
1259
  chart->mark_dirty();
1144
1260
  }
@@ -1166,6 +1282,30 @@ extern "C" void vroom_chart_set_bollinger(VroomChart* chart,
1166
1282
  chart->mark_dirty();
1167
1283
  }
1168
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
+
1169
1309
  // ---- Direct draw (used by hosts that don't need the SkPicture cache) ------
1170
1310
 
1171
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