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
@@ -30,13 +30,19 @@
30
30
  #include "macd.h"
31
31
  #include "macd_pane.h"
32
32
  #include "price_indicator.h"
33
+ #include "price_lines.h"
33
34
  #include "rsi.h"
34
35
  #include "rsi_pane.h"
36
+ #include "style_inherit.h"
35
37
  #include "volume.h"
36
38
  #include "vwap.h"
37
39
  #include "theme.h"
38
40
  #include "viewport.h"
39
41
 
42
+ namespace {
43
+ constexpr SkColor kVwapLine = 0xff00bcd4; // cyan
44
+ } // namespace
45
+
40
46
  VroomChart::VroomChart() : theme(vroom::default_theme()) {}
41
47
 
42
48
  void VroomChart::mark_dirty() {
@@ -48,7 +54,7 @@ vroom::Layout VroomChart::layout() const {
48
54
  const float axis_w = axis_width_px > 0.f
49
55
  ? axis_width_px
50
56
  : width_px * theme.floats[VROOM_FLOAT_Y_AXIS_WIDTH_RATIO];
51
- const int pane_count = (rsi_enabled ? 1 : 0) + (macd_enabled ? 1 : 0);
57
+ const int pane_count = (rsi.enabled ? 1 : 0) + (macd.enabled ? 1 : 0);
52
58
  const float indicator_h = static_cast<float>(pane_count) * height_px *
53
59
  theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
54
60
  return vroom::Layout{
@@ -65,10 +71,11 @@ vroom::Layout VroomChart::layout() const {
65
71
  }
66
72
 
67
73
  void VroomChart::ensure_rsi() {
68
- if (!rsi_enabled || !rsi_dirty) return;
69
- vroom::rsi::compute(candles.data(), candles.size(), rsi_period, rsi_cache);
70
- if (rsi_ma_enabled) {
71
- vroom::rsi::compute_ma(rsi_cache, rsi_ma_period, rsi_ma_cache);
74
+ if (!rsi.enabled || !rsi_dirty) return;
75
+ vroom::rsi::compute(candles.data(), candles.size(), rsi.period, rsi_cache);
76
+ if (rsi.ma_visible) {
77
+ vroom::rsi::compute_ma(rsi_cache, rsi.ma_period, rsi.ma_kind,
78
+ rsi_ma_cache);
72
79
  } else {
73
80
  rsi_ma_cache.clear();
74
81
  }
@@ -76,9 +83,10 @@ void VroomChart::ensure_rsi() {
76
83
  }
77
84
 
78
85
  void VroomChart::ensure_macd() {
79
- if (!macd_enabled || !macd_dirty) return;
80
- vroom::macd::compute(candles.data(), candles.size(), macd_fast, macd_slow,
81
- macd_signal, macd_cache, macd_signal_cache,
86
+ if (!macd.enabled || !macd_dirty) return;
87
+ vroom::macd::compute(candles.data(), candles.size(), macd.fast, macd.slow,
88
+ macd.signal, macd.source, macd.ma_kind,
89
+ macd.signal_ma_kind, macd_cache, macd_signal_cache,
82
90
  macd_hist_cache);
83
91
  macd_dirty = false;
84
92
  }
@@ -95,8 +103,8 @@ void VroomChart::ensure_overlays() {
95
103
  }
96
104
 
97
105
  void VroomChart::ensure_vwap() {
98
- if (!vwap_enabled || !vwap_dirty) return;
99
- vroom::vwap::compute(candles.data(), candles.size(), vwap_reset_offset_min,
106
+ if (!vwap.enabled || !vwap_dirty) return;
107
+ vroom::vwap::compute(candles.data(), candles.size(), vwap.reset_offset_min,
100
108
  vwap_cache, vwap_breaks);
101
109
  vwap_dirty = false;
102
110
  }
@@ -111,6 +119,7 @@ void VroomChart::ensure_bollinger() {
111
119
  }
112
120
 
113
121
  void VroomChart::draw_chart(SkCanvas* canvas) {
122
+ begin_frame();
114
123
  const auto lay = layout();
115
124
 
116
125
  // 1. Background
@@ -139,19 +148,60 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
139
148
 
140
149
  // 3. Update label fade state ONCE per frame — both gridlines and labels
141
150
  // share these opacities so their animations stay in lockstep.
142
- vroom::labels::update_y_fades(*this, lay, bounds);
143
- vroom::labels::update_x_fades(*this, lay);
151
+ // While an interval morph fades the old ticks out, the axes are still laid
152
+ // out against the pre-switch scale and window so nothing appears to move;
153
+ // they adopt the new ones at the midpoint, invisible. Every axis call in
154
+ // the frame has to agree on which of the two it's using.
155
+ const auto axis_phase = vroom::labels::interval_phase(*this);
156
+ const auto& axis_bounds = axis_phase.outgoing ? morph_from_bounds : bounds;
157
+ const int64_t axis_start_ms =
158
+ axis_phase.outgoing ? morph_from_start_ms : visible_start_ms;
159
+ const int64_t axis_end_ms =
160
+ axis_phase.outgoing ? morph_from_end_ms : visible_end_ms;
161
+ vroom::labels::update_y_fades(*this, lay, axis_bounds);
162
+ vroom::labels::update_x_fades(*this, lay, axis_start_ms, axis_end_ms);
144
163
 
145
164
  // 4. Gridlines — drawn before candles so candle bodies overlay them.
146
165
  // Vertical (time) gridlines are intentionally disabled for now —
147
166
  // re-enable by adding `vroom::labels::draw_x_gridlines(canvas, *this,
148
- // candle_right, candle_area_h);` here.
149
- vroom::labels::draw_y_gridlines(canvas, *this, lay, bounds,
167
+ // axis_start_ms, axis_end_ms, candle_right, candle_area_h);` here.
168
+ vroom::labels::draw_y_gridlines(canvas, *this, lay, axis_bounds,
150
169
  candle_right, candle_area_h);
151
170
 
171
+ // 4.35. Price-series morph state, shared by the gradient fill below and the
172
+ // series itself (5). `morph_fade` crossfades candles→line and
173
+ // `morph_collapse` folds each candle toward its close (the line vertex);
174
+ // fade 0 = pure candles, fade 1 = pure line. An interval morph
175
+ // additionally reshapes each slot from the geometry it held before the
176
+ // timeframe switch (see `morph_from`) — candle bodies, line vertices and
177
+ // the fill alike, so every layer stays in step mid-crossfade.
178
+ const float fade = morph_fade;
179
+ const float collapse = morph_collapse;
180
+ const bool morphing = interval_morph_t < 1.f && !morph_from.empty();
181
+ const vroom::CandleSnapshot* morph_src = morphing ? morph_from.data() : nullptr;
182
+ const std::size_t morph_n = morphing ? morph_from.size() : 0;
183
+ const float morph_t = morphing ? interval_morph_t : 1.f;
184
+
185
+ // 4.4. Line-mode gradient fill — the wash under the close polyline. Behind the
186
+ // volume bars so they stay legible on top of it, which puts it at the
187
+ // back of the price pane's content.
188
+ if (fade > 0.f) {
189
+ vroom::ma_overlay::draw_close_gradient(
190
+ canvas, lay, bounds, visible, n, window_ms,
191
+ visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
192
+ theme.colors[VROOM_COLOR_LINE],
193
+ theme.floats[VROOM_FLOAT_LINE_GRADIENT_OPACITY],
194
+ fade, morph_src, morph_n, morph_t);
195
+ }
196
+
152
197
  // 4.5. Volume bars — drawn under the candles so candles z-index above.
153
- vroom::volume::draw(canvas, visible, n, lay, theme,
154
- window_ms, visible_start_ms, candle_duration_ms);
198
+ // A fully collapsed chart has no bars left to draw, which is also how
199
+ // "volume disabled" is represented (set_volume snaps the scalar).
200
+ if (volume_collapse_t < 1.f) {
201
+ vroom::volume::draw(canvas, visible, n, lay, theme, volume,
202
+ volume_collapse_t, volume_collapse_easing,
203
+ window_ms, visible_start_ms, candle_duration_ms);
204
+ }
155
205
 
156
206
  // 4.6. Liquidity bands (resting-order depth) — behind the candles so the
157
207
  // candle bodies paint over the volume-driven tint. Anchored in price
@@ -177,25 +227,19 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
177
227
  }
178
228
 
179
229
  // 5. Price series — candles, a close-price line, or a blend of the two during
180
- // the candle↔line morph. `morph_fade` crossfades candles→line and
181
- // `morph_collapse` folds each candle toward its close (the line vertex).
182
- // fade 0 = pure candles, fade 1 = pure line. The line reuses the MA-overlay
183
- // polyline routine, fed the visible closes and styled by theme.LINE.
184
- const float fade = morph_fade;
185
- const float collapse = morph_collapse;
230
+ // the candle↔line morph (state hoisted to 4.35 for the gradient fill). The
231
+ // line is styled by theme.LINE.
186
232
  if (fade < 1.f) {
187
233
  vroom::candles::draw(canvas, visible, n, lay, theme, bounds, window_ms,
188
234
  visible_start_ms, candle_duration_ms, collapse,
189
- 1.f - fade);
235
+ 1.f - fade, morph_src, morph_n, morph_t);
190
236
  }
191
237
  if (fade > 0.f) {
192
- std::vector<double> closes(n);
193
- for (std::size_t i = 0; i < n; ++i) closes[i] = visible[i].close;
194
- vroom::ma_overlay::draw(
195
- canvas, lay, bounds, visible, n, closes.data(), window_ms,
238
+ vroom::ma_overlay::draw_close_line(
239
+ canvas, lay, bounds, visible, n, window_ms,
196
240
  visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
197
241
  theme.colors[VROOM_COLOR_LINE], theme.floats[VROOM_FLOAT_LINE_WIDTH_PX],
198
- nullptr, fade);
242
+ fade, morph_src, morph_n, morph_t);
199
243
  }
200
244
 
201
245
  // 5.5. Moving-average overlay lines (SMA/EMA) on the price pane, over the
@@ -215,7 +259,7 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
215
259
 
216
260
  // 5.6. VWAP overlay (session, configurable reset) — a single price-pane line
217
261
  // that breaks at each session reset (vwap_breaks).
218
- if (vwap_enabled) {
262
+ if (vwap.enabled) {
219
263
  ensure_vwap();
220
264
  if (vwap_cache.size() == candles.size()) {
221
265
  const double* vis = vwap_cache.data() + range.start;
@@ -225,7 +269,10 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
225
269
  vroom::ma_overlay::draw(canvas, lay, bounds, visible, n, vis,
226
270
  window_ms, visible_start_ms,
227
271
  candle_duration_ms, candle_right,
228
- candle_area_h, vwap_color, vwap_width, brk);
272
+ candle_area_h,
273
+ vroom::style::color_or(vwap.color, kVwapLine),
274
+ vroom::style::width_or(vwap.width, 1.5f),
275
+ brk);
229
276
  }
230
277
  }
231
278
 
@@ -271,14 +318,20 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
271
318
  axis_bg);
272
319
 
273
320
  // 7. Labels (read from y_fades / x_fades, no state mutation here)
274
- vroom::labels::draw_y_labels(canvas, *this, lay, bounds);
275
- vroom::labels::draw_x_labels(canvas, *this, lay);
321
+ vroom::labels::draw_y_labels(canvas, *this, lay, axis_bounds);
322
+ vroom::labels::draw_x_labels(canvas, *this, lay, axis_start_ms, axis_end_ms);
276
323
 
277
324
  // 7.5. Current-price line + box — above labels so the box covers any label
278
325
  // it overlaps; tracks the latest close as the price scale moves.
279
326
  vroom::price_indicator::draw(canvas, *this, lay, bounds,
280
327
  candle_right, candle_area_h);
281
328
 
329
+ // 7.55. Consumer-supplied price status lines — same tier as the current-price
330
+ // indicator (their badges must cover the labels underneath), but after
331
+ // it so a resting order at the last close stays readable.
332
+ vroom::price_lines::draw(canvas, *this, lay, bounds, candle_right,
333
+ candle_area_h);
334
+
282
335
  // 7.6. Indicator panes stacked below the candles, ordered by enable
283
336
  // sequence (most recently enabled at the bottom). Each pane is
284
337
  // INDICATOR_HEIGHT_FRAC of the height; the candle pane already shrank
@@ -287,8 +340,8 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
287
340
  struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
288
341
  ActivePane panes[2];
289
342
  int count = 0;
290
- if (rsi_enabled) panes[count++] = {rsi_order, 0};
291
- if (macd_enabled) panes[count++] = {macd_order, 1};
343
+ if (rsi.enabled) panes[count++] = {rsi_order, 0};
344
+ if (macd.enabled) panes[count++] = {macd_order, 1};
292
345
  if (count == 2 && panes[0].order > panes[1].order) {
293
346
  const ActivePane tmp = panes[0];
294
347
  panes[0] = panes[1];
@@ -305,7 +358,7 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
305
358
  const double* rsi_vis = rsi_cache.size() == candles.size()
306
359
  ? rsi_cache.data() + range.start : nullptr;
307
360
  const double* rsi_ma_vis =
308
- (rsi_ma_enabled && rsi_ma_cache.size() == candles.size())
361
+ (rsi.ma_visible && rsi_ma_cache.size() == candles.size())
309
362
  ? rsi_ma_cache.data() + range.start : nullptr;
310
363
  vroom::rsi_pane::draw(canvas, *this, lay, visible, n, rsi_vis,
311
364
  rsi_ma_vis, window_ms, visible_start_ms,
@@ -354,19 +407,24 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
354
407
  vroom::labels::gc_x_fades(*this);
355
408
  }
356
409
 
357
- void VroomChart::rebuild_chart_picture() {
358
- // Compute dt for fade animations. We cap large gaps (resume from
359
- // background, long idle) so we don't snap animations to completion.
360
- auto now = std::chrono::steady_clock::now();
410
+ void VroomChart::begin_frame() {
411
+ // dt for the fade animations. A large gap (resume from background, or an idle
412
+ // chart that a click just woke up) is clamped to a nominal frame rather than
413
+ // zeroed: dt == 0 means "snap" to the fade updaters, which would finish every
414
+ // fade in the first frame after any idle period.
415
+ constexpr float kNominalFrameSeconds = 1.f / 60.f;
416
+ const auto now = std::chrono::steady_clock::now();
361
417
  float dt = 0.f;
362
418
  if (anim_started) {
363
419
  dt = std::chrono::duration<float>(now - last_anim_tick).count();
364
- if (dt > 0.1f) dt = 0.f;
420
+ if (dt > 0.1f) dt = kNominalFrameSeconds;
365
421
  }
366
422
  last_anim_tick = now;
367
423
  anim_started = true;
368
424
  last_dt_seconds = dt;
425
+ }
369
426
 
427
+ void VroomChart::rebuild_chart_picture() {
370
428
  SkPictureRecorder recorder;
371
429
  SkCanvas* canvas = recorder.beginRecording(SkRect::MakeWH(width_px, height_px));
372
430
  draw_chart(canvas);
@@ -9,6 +9,7 @@
9
9
  #pragma once
10
10
 
11
11
  #include <chrono>
12
+ #include <string>
12
13
  #include <vector>
13
14
 
14
15
  #include "vroom/vroom_chart.h"
@@ -63,6 +64,21 @@ struct VroomChart {
63
64
  float morph_collapse = 0.f;
64
65
  float morph_fade = 0.f;
65
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
+
66
82
  // Cached y-axis width in pixels, sized to fit the widest formatted price
67
83
  // label. 0 = uncomputed; layout() falls back to a width ratio.
68
84
  float axis_width_px = 0.f;
@@ -85,24 +101,27 @@ struct VroomChart {
85
101
  // RSI, drawn in a pane below the candles. rsi_cache is aligned to `candles`
86
102
  // (one value per candle, NaN where undefined) and recomputed lazily by
87
103
  // ensure_rsi() when rsi_dirty (set on any data or config change).
88
- bool rsi_enabled = false;
89
- int rsi_period = 14;
90
- double rsi_upper = 70.0; // overbought band
91
- double rsi_lower = 30.0; // oversold band
92
- bool rsi_ma_enabled = true; // RSI-based moving average (trendline)
93
- 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
94
110
  std::vector<double> rsi_cache; // RSI per candle (NaN where undefined)
95
- 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)
96
112
  bool rsi_dirty = true;
97
113
  // User y-axis zoom for the RSI pane (drag on its y-axis strip). 1.0 = the
98
114
  // default 0..100 fit; >1 zooms in (taller), <1 zooms out. Anchored at 50.
99
115
  double rsi_y_scale = 1.0;
100
116
 
101
117
  // MACD, drawn in its own pane. Caches aligned to `candles` (NaN warmup).
102
- bool macd_enabled = false;
103
- int macd_fast = 12;
104
- int macd_slow = 26;
105
- 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
106
125
  std::vector<double> macd_cache;
107
126
  std::vector<double> macd_signal_cache;
108
127
  std::vector<double> macd_hist_cache;
@@ -127,10 +146,8 @@ struct VroomChart {
127
146
 
128
147
  // VWAP overlay (session anchor, configurable reset time). Single line on the
129
148
  // price pane; vwap_breaks marks session resets so the line lifts the pen.
130
- bool vwap_enabled = false;
131
- int vwap_reset_offset_min = 0; // session boundary offset from UTC midnight
132
- uint32_t vwap_color = 0xff00bcd4; // cyan
133
- float vwap_width = 1.5f;
149
+ // Style fields sit on their inherit sentinel (cyan, 1.5px).
150
+ VroomVWAP vwap{0, 0, 0u, -1.f};
134
151
  std::vector<double> vwap_cache;
135
152
  std::vector<unsigned char> vwap_breaks;
136
153
  bool vwap_dirty = true;
@@ -149,6 +166,18 @@ struct VroomChart {
149
166
  std::vector<double> bb_lower_cache;
150
167
  bool bollinger_dirty = true;
151
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
+
152
181
  // --- drawings (annotations) --------------------------------------------
153
182
  // Committed drawings, anchored in data space so they track the candles on
154
183
  // pan/zoom. Drawn on the price pane above candles/overlays.
@@ -195,6 +224,32 @@ struct VroomChart {
195
224
  std::vector<VroomBand> bands;
196
225
  VroomLiquidityStyle liquidity_style{};
197
226
 
227
+ // --- price status lines -------------------------------------------------
228
+ // Consumer-supplied horizontal lines at fixed prices (resting orders, TP/SL,
229
+ // liquidation levels), each with a label group and an optional close button.
230
+ //
231
+ // Mirrors the public VroomPriceLine but *owns* its label strings, so the
232
+ // caller may free theirs as soon as the setter returns.
233
+ struct StoredPriceLine {
234
+ double price = 0.0;
235
+ uint32_t color = 0xffef5350;
236
+ float width = 1.f;
237
+ int32_t line_style = 1; // dotted, matching the price indicator
238
+ std::string text;
239
+ std::string quantity;
240
+ int32_t flags = 0; // VroomPriceLineFlags
241
+ };
242
+ std::vector<StoredPriceLine> price_lines;
243
+ VroomPriceLineStyle price_line_style{};
244
+
245
+ // Interaction state, driven by the host's gesture layer. The hovered segment
246
+ // renders highlighted; while a line is dragged it renders at
247
+ // dragged_price_line_price with a ghost at its committed price. -1 = none.
248
+ int32_t hovered_price_line = -1;
249
+ int32_t hovered_price_line_part = -1;
250
+ int32_t dragged_price_line = -1;
251
+ double dragged_price_line_price = 0.0;
252
+
198
253
  // --- theme --------------------------------------------------------------
199
254
  vroom::Theme theme;
200
255
 
@@ -208,9 +263,9 @@ struct VroomChart {
208
263
  std::chrono::steady_clock::time_point last_anim_tick{};
209
264
  bool anim_started = false;
210
265
 
211
- // dt of the current rebuild — populated by rebuild_chart_picture and
212
- // consumed by the label-fade updaters. Public so the labels namespace
213
- // 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.
214
269
  float last_dt_seconds = 0.f;
215
270
 
216
271
  // --- methods ------------------------------------------------------------
@@ -240,11 +295,18 @@ struct VroomChart {
240
295
  // indicator is enabled.
241
296
  void ensure_bollinger();
242
297
 
243
- // 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.
244
302
  void draw_chart(SkCanvas* canvas);
245
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
+
246
308
  // Re-records `chart_picture` by invoking draw_chart on a fresh
247
- // SkPictureRecorder. Also computes `last_dt_seconds` for fade animation.
309
+ // SkPictureRecorder.
248
310
  void rebuild_chart_picture();
249
311
 
250
312
  // True if any axis label is mid-fade. Used by the JS-side animation loop