react-native-vroom-chart 0.4.0 → 0.6.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.
@@ -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"
@@ -135,6 +136,20 @@ struct VroomChart {
135
136
  std::vector<unsigned char> vwap_breaks;
136
137
  bool vwap_dirty = true;
137
138
 
139
+ // Bollinger Bands overlay (price pane; no pane reserved). Caches aligned to
140
+ // `candles` (NaN warmup), recomputed lazily by ensure_bollinger() when
141
+ // bollinger_dirty. Defaults: 20-period SMA of close, ±2σ, blue bands /
142
+ // orange basis, 10% fill.
143
+ VroomBollinger bollinger{0, 20, 2.f, 0, 0,
144
+ 0xff2962ff, 1.f, // upper: blue
145
+ 0xffff6d00, 1.f, // middle: orange
146
+ 0xff2962ff, 1.f, // lower: blue
147
+ 1, 0.1f};
148
+ std::vector<double> bb_middle_cache;
149
+ std::vector<double> bb_upper_cache;
150
+ std::vector<double> bb_lower_cache;
151
+ bool bollinger_dirty = true;
152
+
138
153
  // --- drawings (annotations) --------------------------------------------
139
154
  // Committed drawings, anchored in data space so they track the candles on
140
155
  // pan/zoom. Drawn on the price pane above candles/overlays.
@@ -181,6 +196,32 @@ struct VroomChart {
181
196
  std::vector<VroomBand> bands;
182
197
  VroomLiquidityStyle liquidity_style{};
183
198
 
199
+ // --- price status lines -------------------------------------------------
200
+ // Consumer-supplied horizontal lines at fixed prices (resting orders, TP/SL,
201
+ // liquidation levels), each with a label group and an optional close button.
202
+ //
203
+ // Mirrors the public VroomPriceLine but *owns* its label strings, so the
204
+ // caller may free theirs as soon as the setter returns.
205
+ struct StoredPriceLine {
206
+ double price = 0.0;
207
+ uint32_t color = 0xffef5350;
208
+ float width = 1.f;
209
+ int32_t line_style = 1; // dotted, matching the price indicator
210
+ std::string text;
211
+ std::string quantity;
212
+ int32_t flags = 0; // VroomPriceLineFlags
213
+ };
214
+ std::vector<StoredPriceLine> price_lines;
215
+ VroomPriceLineStyle price_line_style{};
216
+
217
+ // Interaction state, driven by the host's gesture layer. The hovered segment
218
+ // renders highlighted; while a line is dragged it renders at
219
+ // dragged_price_line_price with a ghost at its committed price. -1 = none.
220
+ int32_t hovered_price_line = -1;
221
+ int32_t hovered_price_line_part = -1;
222
+ int32_t dragged_price_line = -1;
223
+ double dragged_price_line_price = 0.0;
224
+
184
225
  // --- theme --------------------------------------------------------------
185
226
  vroom::Theme theme;
186
227
 
@@ -222,6 +263,10 @@ struct VroomChart {
222
263
  // Recomputes the VWAP cache when vwap_dirty and VWAP is enabled.
223
264
  void ensure_vwap();
224
265
 
266
+ // Recomputes the Bollinger Band caches when bollinger_dirty and the
267
+ // indicator is enabled.
268
+ void ensure_bollinger();
269
+
225
270
  // The main drawing pass. Calls into the labels and candles modules.
226
271
  void draw_chart(SkCanvas* canvas);
227
272
 
@@ -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;
@@ -131,6 +132,7 @@ extern "C" void vroom_chart_set_candles(VroomChart* chart, const VroomCandle* da
131
132
  chart->macd_dirty = true;
132
133
  chart->overlays_dirty = true;
133
134
  chart->vwap_dirty = true;
135
+ chart->bollinger_dirty = true;
134
136
 
135
137
  // Infer the candle period from the first interval. Robust enough for
136
138
  // uniform-duration series (the only kind we model today).
@@ -158,6 +160,7 @@ extern "C" void vroom_chart_append_candle(VroomChart* chart, const VroomCandle*
158
160
  chart->macd_dirty = true;
159
161
  chart->overlays_dirty = true;
160
162
  chart->vwap_dirty = true;
163
+ chart->bollinger_dirty = true;
161
164
  chart->mark_dirty();
162
165
  }
163
166
 
@@ -168,6 +171,7 @@ extern "C" void vroom_chart_update_last(VroomChart* chart, const VroomCandle* c)
168
171
  chart->macd_dirty = true;
169
172
  chart->overlays_dirty = true;
170
173
  chart->vwap_dirty = true;
174
+ chart->bollinger_dirty = true;
171
175
  chart->mark_dirty();
172
176
  }
173
177
 
@@ -809,6 +813,70 @@ extern "C" void vroom_chart_set_liquidity(VroomChart* chart,
809
813
  chart->mark_dirty();
810
814
  }
811
815
 
816
+ // ---- Price status lines ---------------------------------------------------
817
+
818
+ extern "C" void vroom_chart_set_price_lines(VroomChart* chart,
819
+ const VroomPriceLine* lines,
820
+ size_t count,
821
+ const VroomPriceLineStyle* style) {
822
+ if (!chart) return;
823
+ // Deep-copy into the owning form: the label strings live in the chart, so the
824
+ // caller's buffers may be freed as soon as this returns.
825
+ chart->price_lines.clear();
826
+ chart->price_lines.reserve(count);
827
+ for (size_t i = 0; i < count; ++i) {
828
+ const VroomPriceLine& src = lines[i];
829
+ VroomChart::StoredPriceLine pl;
830
+ pl.price = src.price;
831
+ pl.color = src.color;
832
+ pl.width = src.width;
833
+ pl.line_style = src.line_style;
834
+ if (src.text) pl.text = src.text;
835
+ if (src.quantity) pl.quantity = src.quantity;
836
+ pl.flags = src.flags;
837
+ chart->price_lines.push_back(std::move(pl));
838
+ }
839
+ if (style) chart->price_line_style = *style;
840
+ // Drop interaction state that no longer refers to a live line — otherwise a
841
+ // removed line would leave a stuck hover highlight or drag preview.
842
+ const auto n = static_cast<int32_t>(count);
843
+ if (chart->hovered_price_line >= n) {
844
+ chart->hovered_price_line = -1;
845
+ chart->hovered_price_line_part = -1;
846
+ }
847
+ if (chart->dragged_price_line >= n) chart->dragged_price_line = -1;
848
+ chart->mark_dirty();
849
+ }
850
+
851
+ extern "C" void vroom_chart_set_price_line_hover(VroomChart* chart, int32_t index,
852
+ int32_t part) {
853
+ if (!chart) return;
854
+ if (index < 0 || index >= static_cast<int32_t>(chart->price_lines.size())) {
855
+ index = -1;
856
+ part = -1;
857
+ }
858
+ if (chart->hovered_price_line == index && chart->hovered_price_line_part == part) {
859
+ return; // hover fires on every pointer move; don't redraw for nothing
860
+ }
861
+ chart->hovered_price_line = index;
862
+ chart->hovered_price_line_part = part;
863
+ chart->mark_dirty();
864
+ }
865
+
866
+ extern "C" void vroom_chart_set_price_line_drag(VroomChart* chart, int32_t index,
867
+ double price) {
868
+ if (!chart) return;
869
+ if (index < 0 || index >= static_cast<int32_t>(chart->price_lines.size())) {
870
+ if (chart->dragged_price_line == -1) return;
871
+ chart->dragged_price_line = -1;
872
+ chart->mark_dirty();
873
+ return;
874
+ }
875
+ chart->dragged_price_line = index;
876
+ chart->dragged_price_line_price = price;
877
+ chart->mark_dirty();
878
+ }
879
+
812
880
  extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
813
881
  double a_price, bool has_b, int64_t b_time,
814
882
  double b_price, bool guide, uint32_t color,
@@ -923,6 +991,32 @@ extern "C" bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px,
923
991
  return true;
924
992
  }
925
993
 
994
+ extern "C" bool vroom_chart_hit_test_price_line(VroomChart* chart, float x_px,
995
+ float y_px, int32_t* out_index,
996
+ int32_t* out_part) {
997
+ if (!chart || chart->price_lines.empty() || chart->candles.empty()) return false;
998
+ const auto lay = chart->layout();
999
+ // Same bounds and pane geometry as draw_chart, so the grab regions line up
1000
+ // with what's on screen.
1001
+ const auto range = vroom::visible_indices(
1002
+ chart->candles.data(), chart->candles.size(),
1003
+ chart->visible_start_ms, chart->visible_end_ms);
1004
+ const size_t n = range.end - range.start;
1005
+ const auto bounds =
1006
+ chart->price_bounds_manual
1007
+ ? chart->price_bounds
1008
+ : vroom::auto_price_bounds(chart->candles.data() + range.start, n);
1009
+ const float candle_area_h = vroom::price_pane_bottom(lay);
1010
+ const float candle_right =
1011
+ chart->width_px - lay.y_axis_width_px - lay.right_padding_px;
1012
+ const auto hit = vroom::price_lines::hit_test(*chart, lay, bounds, candle_right,
1013
+ candle_area_h, x_px, y_px);
1014
+ if (hit.index < 0) return false;
1015
+ if (out_index) *out_index = hit.index;
1016
+ if (out_part) *out_part = hit.part;
1017
+ return true;
1018
+ }
1019
+
926
1020
  extern "C" void vroom_chart_set_selected_drawing(VroomChart* chart,
927
1021
  int32_t index,
928
1022
  int32_t grabbed_endpoint) {
@@ -1049,6 +1143,29 @@ extern "C" void vroom_chart_set_vwap(VroomChart* chart, bool enabled,
1049
1143
  chart->mark_dirty();
1050
1144
  }
1051
1145
 
1146
+ extern "C" void vroom_chart_set_bollinger(VroomChart* chart,
1147
+ const VroomBollinger* cfg) {
1148
+ if (!chart || !cfg) return;
1149
+ VroomBollinger next = *cfg;
1150
+ next.enabled = next.enabled ? 1 : 0;
1151
+ next.fill_enabled = next.fill_enabled ? 1 : 0;
1152
+ if (next.period < 1) next.period = 1;
1153
+ if (!(next.mult >= 0.f)) next.mult = 0.f;
1154
+ next.fill_opacity = std::clamp(next.fill_opacity, 0.f, 1.f);
1155
+
1156
+ // Only the series-affecting fields force a recompute; style and fill
1157
+ // changes are render-only.
1158
+ const VroomBollinger& cur = chart->bollinger;
1159
+ const bool recompute = cur.enabled != next.enabled ||
1160
+ cur.period != next.period ||
1161
+ cur.mult != next.mult ||
1162
+ cur.source != next.source ||
1163
+ cur.basis_kind != next.basis_kind;
1164
+ chart->bollinger = next;
1165
+ if (recompute) chart->bollinger_dirty = true;
1166
+ chart->mark_dirty();
1167
+ }
1168
+
1052
1169
  // ---- Direct draw (used by hosts that don't need the SkPicture cache) ------
1053
1170
 
1054
1171
  extern "C" void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas) {
@@ -78,4 +78,71 @@ void draw(SkCanvas* canvas,
78
78
  canvas->restore();
79
79
  }
80
80
 
81
+ void fill_between(SkCanvas* canvas,
82
+ const Layout& lay,
83
+ const PriceBounds& bounds,
84
+ const ::VroomCandle* visible,
85
+ std::size_t n,
86
+ const double* upper_visible,
87
+ const double* lower_visible,
88
+ int64_t window_ms,
89
+ int64_t visible_start_ms,
90
+ int64_t candle_duration_ms,
91
+ float candle_right,
92
+ float candle_area_h,
93
+ uint32_t color,
94
+ float opacity) {
95
+ if (!canvas || !upper_visible || !lower_visible || n == 0 ||
96
+ candle_right <= 0.f || candle_area_h <= 0.f) {
97
+ return;
98
+ }
99
+ opacity = std::clamp(opacity, 0.f, 1.f);
100
+ if (opacity <= 0.f) return;
101
+
102
+ // One closed contour per maximal run where both series are finite; a
103
+ // single-point run has no area. Multiple runs (e.g. around a data gap)
104
+ // become multiple contours in one path.
105
+ SkPathBuilder path;
106
+ std::size_t i = 0;
107
+ while (i < n) {
108
+ if (!std::isfinite(upper_visible[i]) ||
109
+ !std::isfinite(lower_visible[i])) {
110
+ ++i;
111
+ continue;
112
+ }
113
+ std::size_t e = i;
114
+ while (e + 1 < n && std::isfinite(upper_visible[e + 1]) &&
115
+ std::isfinite(lower_visible[e + 1])) {
116
+ ++e;
117
+ }
118
+ if (e > i) {
119
+ const auto x_at = [&](std::size_t k) {
120
+ return vroom::candle_center_x(lay, visible[k].time_ms,
121
+ candle_duration_ms,
122
+ visible_start_ms, window_ms);
123
+ };
124
+ path.moveTo(x_at(i), vroom::price_to_y(lay, bounds, upper_visible[i]));
125
+ for (std::size_t k = i + 1; k <= e; ++k) {
126
+ path.lineTo(x_at(k), vroom::price_to_y(lay, bounds, upper_visible[k]));
127
+ }
128
+ for (std::size_t k = e + 1; k-- > i;) {
129
+ path.lineTo(x_at(k), vroom::price_to_y(lay, bounds, lower_visible[k]));
130
+ }
131
+ path.close();
132
+ }
133
+ i = e + 1;
134
+ }
135
+
136
+ SkPaint fill;
137
+ fill.setAntiAlias(true);
138
+ fill.setColor(static_cast<SkColor>(color));
139
+ fill.setAlphaf(fill.getAlphaf() * opacity);
140
+ fill.setStyle(SkPaint::kFill_Style);
141
+
142
+ canvas->save();
143
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
144
+ canvas->drawPath(path.detach(), fill);
145
+ canvas->restore();
146
+ }
147
+
81
148
  } // namespace vroom::ma_overlay
@@ -40,4 +40,25 @@ void draw(SkCanvas* canvas,
40
40
  const unsigned char* break_before = nullptr,
41
41
  float opacity = 1.f);
42
42
 
43
+ // Fills the closed region between two aligned series (NaN where undefined)
44
+ // with `color` at its alpha × `opacity` — used for the Bollinger Band fill.
45
+ // Runs where either series is NaN are skipped, so the fill never bridges the
46
+ // warmup gap. Plain-alpha SkPaint fill, no gradient shader (the Skia gradient
47
+ // APIs diverge across our pinned versions; see liquidity.cpp). Clipped to the
48
+ // candle area like draw().
49
+ void fill_between(SkCanvas* canvas,
50
+ const Layout& lay,
51
+ const PriceBounds& bounds,
52
+ const ::VroomCandle* visible,
53
+ std::size_t n,
54
+ const double* upper_visible,
55
+ const double* lower_visible,
56
+ int64_t window_ms,
57
+ int64_t visible_start_ms,
58
+ int64_t candle_duration_ms,
59
+ float candle_right,
60
+ float candle_area_h,
61
+ uint32_t color,
62
+ float opacity);
63
+
43
64
  } // namespace vroom::ma_overlay
@@ -0,0 +1,87 @@
1
+ #include "price_line_layout.h"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+
6
+ namespace vroom::price_lines {
7
+
8
+ namespace {
9
+ // A pill's width is its text plus symmetric padding. Zero-width text means the
10
+ // segment is absent, not a padding-only sliver.
11
+ float pill_width(float text_w) {
12
+ return text_w > 0.f ? text_w + 2.f * kPadH : 0.f;
13
+ }
14
+ } // namespace
15
+
16
+ bool contains(const Rect& r, float x, float y) {
17
+ if (r.empty()) return false;
18
+ return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
19
+ }
20
+
21
+ GroupLayout layout_group(const LabelMetrics& metrics,
22
+ float y,
23
+ float pane_right,
24
+ const ::VroomPriceLineStyle& style) {
25
+ GroupLayout out;
26
+
27
+ const float body_w = pill_width(metrics.text_w);
28
+ const float qty_w = pill_width(metrics.quantity_w);
29
+ // The close button is a square cell, so its icon stays centered whatever the
30
+ // font size works out to.
31
+ const float close_w = metrics.closable ? metrics.label_h : 0.f;
32
+ const float group_w = body_w + qty_w + close_w;
33
+
34
+ if (group_w <= 0.f || metrics.label_h <= 0.f || pane_right <= 0.f) {
35
+ // A bare line: no group, but still report a degenerate span at the
36
+ // anchor so callers can treat left/right uniformly.
37
+ out.left = out.right = pane_right;
38
+ return out;
39
+ }
40
+
41
+ float left = 0.f;
42
+ switch (style.align) {
43
+ case 0: // left
44
+ left = 0.f;
45
+ break;
46
+ case 1: // center
47
+ left = (pane_right - group_w) * 0.5f;
48
+ break;
49
+ default: { // right
50
+ const float frac = std::clamp(style.line_length_frac, 0.f, 1.f);
51
+ const float right = pane_right - frac * pane_right - kAxisGutter;
52
+ left = right - group_w;
53
+ break;
54
+ }
55
+ }
56
+ // Keep the group on-screen: never past the axis, never off the left edge.
57
+ left = std::min(left, pane_right - group_w);
58
+ left = std::max(left, 0.f);
59
+
60
+ const float top = y - metrics.label_h * 0.5f;
61
+ const float bottom = top + metrics.label_h;
62
+
63
+ float x = left;
64
+ if (body_w > 0.f) {
65
+ out.body = Rect{x, top, x + body_w, bottom};
66
+ x += body_w;
67
+ }
68
+ if (qty_w > 0.f) {
69
+ out.quantity = Rect{x, top, x + qty_w, bottom};
70
+ x += qty_w;
71
+ }
72
+ if (close_w > 0.f) {
73
+ out.close = Rect{x, top, x + close_w, bottom};
74
+ x += close_w;
75
+ }
76
+
77
+ out.left = left;
78
+ out.right = x;
79
+ return out;
80
+ }
81
+
82
+ bool hits_line(float line_y, float stroke_width, float y) {
83
+ const float band = std::max(stroke_width, 1.f) + kLineHitTolerance;
84
+ return std::fabs(y - line_y) <= band;
85
+ }
86
+
87
+ } // namespace vroom::price_lines
@@ -0,0 +1,80 @@
1
+ // Price-status-line geometry — pure functions, no Skia, no state.
2
+ //
3
+ // Split out from price_lines.cpp so the rendering pass and the hit-test pass
4
+ // derive their rectangles from the same arithmetic: a label the user can see but
5
+ // not click (or vice versa) is the classic failure mode for this widget. Being
6
+ // Skia-free it also unit-tests without a Skia checkout.
7
+
8
+ #pragma once
9
+
10
+ #include "vroom/vroom_chart.h"
11
+ namespace vroom::price_lines {
12
+
13
+ // Pill padding / corner radius, matching the current-price indicator so a price
14
+ // line and the price badge read as the same family of chrome.
15
+ constexpr float kPadH = 8.f; // left/right of the text
16
+ constexpr float kPadV = 4.f; // above/below the text
17
+ constexpr float kCorner = 6.f; // rounded-pill corner radius
18
+
19
+ // Extra grab band above and below the stroke, so a 1px line is still an easy
20
+ // target. Matches the tolerance TradingView's own chart libraries settled on.
21
+ constexpr float kLineHitTolerance = 7.f;
22
+
23
+ // Gap between the label group and the price axis when right-aligned. Wide enough
24
+ // that the line still shows through it — a couple of dashes reading as "this
25
+ // continues to the badge" rather than one orphaned dot.
26
+ constexpr float kAxisGutter = 12.f;
27
+
28
+ struct Rect {
29
+ float left = 0.f;
30
+ float top = 0.f;
31
+ float right = 0.f;
32
+ float bottom = 0.f;
33
+
34
+ // Empty rects stand in for absent segments (no quantity, no close button).
35
+ bool empty() const { return right <= left || bottom <= top; }
36
+ float width() const { return right - left; }
37
+ };
38
+
39
+ // True when (x, y) falls inside `r`. Always false for an empty rect.
40
+ bool contains(const Rect& r, float x, float y);
41
+
42
+ // The text extents a caller measured with the real font, plus which optional
43
+ // segments to reserve room for. `label_h` is the pill height (already including
44
+ // kPadV on both sides).
45
+ struct LabelMetrics {
46
+ float text_w = 0.f; // body text width; 0 hides the body pill
47
+ float quantity_w = 0.f; // quantity text width; 0 hides the quantity pill
48
+ float label_h = 0.f;
49
+ bool closable = false; // reserve the trailing square close-button cell
50
+ };
51
+
52
+ // The laid-out label group, vertically centered on the line. Any of the three
53
+ // segment rects may be empty. `left`/`right` bound whichever segments exist and
54
+ // are equal when the group is empty (a bare line).
55
+ struct GroupLayout {
56
+ Rect body;
57
+ Rect quantity;
58
+ Rect close;
59
+ float left = 0.f;
60
+ float right = 0.f;
61
+
62
+ bool empty() const { return right <= left; }
63
+ };
64
+
65
+ // Lays the group out on the line at `y`. `pane_right` is the inner edge of the
66
+ // price axis (the line's right end).
67
+ //
68
+ // `style.align` picks the anchor: 2 (right, the default) parks the group's right
69
+ // edge `style.line_length_frac` of the pane width in from the axis; 1 centers
70
+ // it; 0 pins it to the left edge. The group is clamped into [0, pane_right], so
71
+ // a label wider than the pane starts flush left rather than overflowing.
72
+ GroupLayout layout_group(const LabelMetrics& metrics,
73
+ float y,
74
+ float pane_right,
75
+ const ::VroomPriceLineStyle& style);
76
+
77
+ // True when `y` is within the grab band of a line drawn at `line_y`.
78
+ bool hits_line(float line_y, float stroke_width, float y);
79
+
80
+ } // namespace vroom::price_lines