react-native-vroom-chart 0.14.0 → 0.16.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 (47) hide show
  1. package/cpp/VroomChartHostObject.cpp +289 -2
  2. package/cpp/_core_include/vroom/vroom_chart.h +221 -4
  3. package/cpp/_core_src/atr.cpp +67 -0
  4. package/cpp/_core_src/atr.h +44 -0
  5. package/cpp/_core_src/atr_pane.cpp +162 -0
  6. package/cpp/_core_src/atr_pane.h +48 -0
  7. package/cpp/_core_src/chart.cpp +477 -172
  8. package/cpp/_core_src/chart.h +173 -1
  9. package/cpp/_core_src/chart_facade.cpp +317 -23
  10. package/cpp/_core_src/fair_value_gaps.cpp +76 -0
  11. package/cpp/_core_src/fair_value_gaps.h +54 -0
  12. package/cpp/_core_src/footprints.cpp +226 -0
  13. package/cpp/_core_src/footprints.h +45 -0
  14. package/cpp/_core_src/footprints_layout.cpp +140 -0
  15. package/cpp/_core_src/footprints_layout.h +94 -0
  16. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  17. package/cpp/_core_src/fvg_overlay.h +43 -0
  18. package/cpp/_core_src/ichimoku.cpp +65 -0
  19. package/cpp/_core_src/ichimoku.h +45 -0
  20. package/cpp/_core_src/labels.cpp +3 -0
  21. package/cpp/_core_src/line_morph.h +159 -0
  22. package/cpp/_core_src/ma_overlay.cpp +259 -36
  23. package/cpp/_core_src/ma_overlay.h +57 -2
  24. package/cpp/_core_src/macd.cpp +24 -1
  25. package/cpp/_core_src/macd.h +16 -0
  26. package/cpp/_core_src/macd_pane.cpp +71 -62
  27. package/cpp/_core_src/macd_pane.h +13 -1
  28. package/cpp/_core_src/pane_series.h +169 -0
  29. package/cpp/_core_src/rsi.cpp +4 -0
  30. package/cpp/_core_src/rsi.h +7 -0
  31. package/cpp/_core_src/rsi_pane.cpp +85 -29
  32. package/cpp/_core_src/rsi_pane.h +11 -1
  33. package/cpp/_core_src/tip_pulse.h +4 -4
  34. package/cpp/_core_src/viewport.h +35 -0
  35. package/lib/index.d.mts +385 -3
  36. package/lib/index.d.ts +385 -3
  37. package/lib/index.js +305 -7
  38. package/lib/index.js.map +1 -1
  39. package/lib/index.mjs +305 -7
  40. package/lib/index.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/src/VroomChart.tsx +108 -7
  43. package/src/dataTransitions.ts +47 -0
  44. package/src/index.ts +10 -0
  45. package/src/jsi.d.ts +136 -1
  46. package/src/types.ts +10 -0
  47. package/src/useChartCore.ts +330 -5
@@ -0,0 +1,226 @@
1
+ #include "footprints.h"
2
+
3
+ #pragma clang diagnostic push
4
+ #pragma clang diagnostic ignored "-Wdocumentation"
5
+ #include "include/core/SkCanvas.h"
6
+ #include "include/core/SkColor.h"
7
+ #include "include/core/SkPaint.h"
8
+ #include "include/core/SkRect.h"
9
+ #pragma clang diagnostic pop
10
+
11
+ #include <algorithm>
12
+ #include <cmath>
13
+
14
+ #include "chart.h"
15
+ #include "footprints_layout.h"
16
+ #include "theme.h"
17
+ #include "viewport.h"
18
+
19
+ namespace vroom::footprints {
20
+
21
+ namespace {
22
+
23
+ // The glyph is a bar (minus) or a crossed pair of bars (plus) sized as a fraction
24
+ // of the badge, so restyling the radius scales the whole mark.
25
+ constexpr float kGlyphFrac = 0.52f; // glyph length / diameter
26
+ constexpr float kStrokeFrac = 0.20f; // stroke width / radius
27
+
28
+ // The hovered badge gets a soft ring outside its edge — the "lit up" state in the
29
+ // reference, and the only affordance that reads at a 9px radius.
30
+ constexpr float kHaloWidthFrac = 0.42f; // ring stroke width / radius
31
+ constexpr float kHaloGapFrac = 0.30f; // clear space between badge and ring
32
+ constexpr U8CPU kHaloAlpha = 0x66;
33
+
34
+ // Brightens `c`'s channels by `mul` (clamped at white), leaving alpha alone.
35
+ // Matches price_lines::boost so hover feels the same across both widgets.
36
+ SkColor boost(SkColor c, float mul) {
37
+ if (mul <= 1.f) return c;
38
+ const auto up = [mul](U8CPU v) {
39
+ return static_cast<U8CPU>(
40
+ std::min(255.f, static_cast<float>(v) * mul + 0.5f));
41
+ };
42
+ return SkColorSetARGB(SkColorGetA(c), up(SkColorGetR(c)), up(SkColorGetG(c)),
43
+ up(SkColorGetB(c)));
44
+ }
45
+
46
+ // One laid-out badge, in pixels.
47
+ struct Badge {
48
+ float cx = 0.f;
49
+ float cy = 0.f;
50
+ float radius = 0.f;
51
+ int32_t side = -1;
52
+ int64_t candle_time_ms = 0;
53
+ };
54
+
55
+ // Walks every badge that could be on screen, in draw order, handing each to `fn`.
56
+ // Both passes go through here: the badge the user sees and the badge they can
57
+ // hover are the same object by construction.
58
+ template <typename Fn>
59
+ void for_each_badge(const VroomChart& chart, const Layout& lay,
60
+ const PriceBounds& bounds, int64_t window_ms, Fn&& fn) {
61
+ if (chart.footprint_buckets.empty() || chart.candles.empty()) return;
62
+ if (window_ms <= 0) return;
63
+
64
+ const Metrics m = metrics_from(&chart.footprint_style);
65
+ const float pane_bottom = vroom::price_pane_bottom(lay);
66
+ const float pane_right = vroom::candle_area_width(lay);
67
+
68
+ const ::VroomCandle* candles = chart.candles.data();
69
+ const size_t candle_count = chart.candles.size();
70
+
71
+ // Only the buckets whose candles are in (or just off) the window matter. One
72
+ // bar of slack each side keeps a badge from popping in at the edge.
73
+ const int64_t slack = chart.candle_duration_ms;
74
+ const auto& buckets = chart.footprint_buckets;
75
+ auto it = std::lower_bound(buckets.begin(), buckets.end(),
76
+ chart.visible_start_ms - slack,
77
+ [](const Bucket& b, int64_t t) {
78
+ return b.candle_time_ms < t;
79
+ });
80
+
81
+ for (; it != buckets.end(); ++it) {
82
+ if (it->candle_time_ms > chart.visible_end_ms + slack) break;
83
+
84
+ // The bar this bucket belongs to, for its high.
85
+ const ::VroomCandle* c =
86
+ std::lower_bound(candles, candles + candle_count, it->candle_time_ms,
87
+ [](const ::VroomCandle& a, int64_t t) {
88
+ return a.time_ms < t;
89
+ });
90
+ if (c == candles + candle_count || c->time_ms != it->candle_time_ms) {
91
+ continue; // the bar went away; the bucket is about to be rebuilt
92
+ }
93
+
94
+ const float cx = vroom::candle_center_x(lay, it->candle_time_ms,
95
+ chart.candle_duration_ms,
96
+ chart.visible_start_ms, window_ms);
97
+ // Cheap reject before laying the stack out.
98
+ if (cx < -m.radius || cx > pane_right + m.radius) continue;
99
+
100
+ const float high_y = vroom::price_to_y(lay, bounds, c->high);
101
+ const Stack stack = layout_stack(*it, high_y, 0.f, m.radius, m.gap, m.margin);
102
+
103
+ for (int32_t i = 0; i < stack.count; ++i) {
104
+ // A bar whose high is below the pane (scrolled off the bottom) parks
105
+ // its stack out of sight; skip rather than draw over the x-axis.
106
+ if (stack.y[i] - m.radius > pane_bottom) continue;
107
+ Badge b;
108
+ b.cx = cx;
109
+ b.cy = stack.y[i];
110
+ b.radius = m.radius;
111
+ b.side = stack.sides[i];
112
+ b.candle_time_ms = it->candle_time_ms;
113
+ fn(b, m);
114
+ }
115
+ }
116
+ }
117
+
118
+ // Buy = "+", sell = "-": one horizontal bar, plus a vertical one for an entry.
119
+ void draw_glyph(SkCanvas* canvas, const Badge& b, SkColor color) {
120
+ SkPaint p;
121
+ p.setAntiAlias(true);
122
+ p.setColor(color);
123
+ p.setStyle(SkPaint::kStroke_Style);
124
+ p.setStrokeWidth(std::max(1.f, b.radius * kStrokeFrac));
125
+ p.setStrokeCap(SkPaint::kRound_Cap);
126
+
127
+ const float arm = b.radius * kGlyphFrac;
128
+ canvas->drawLine(b.cx - arm, b.cy, b.cx + arm, b.cy, p);
129
+ if (b.side == VROOM_FOOTPRINT_BUY) {
130
+ canvas->drawLine(b.cx, b.cy - arm, b.cx, b.cy + arm, p);
131
+ }
132
+ }
133
+
134
+ } // namespace
135
+
136
+ void draw(SkCanvas* canvas,
137
+ const VroomChart& chart,
138
+ const Layout& lay,
139
+ const PriceBounds& bounds,
140
+ int64_t window_ms) {
141
+ if (!canvas || chart.footprints.empty()) return;
142
+
143
+ const float pane_bottom = vroom::price_pane_bottom(lay);
144
+ const float pane_right = vroom::candle_area_width(lay);
145
+ if (pane_right <= 0.f || pane_bottom <= 0.f) return;
146
+
147
+ const SkColor bull = chart.theme.colors[VROOM_COLOR_BULL];
148
+ const SkColor bear = chart.theme.colors[VROOM_COLOR_BEAR];
149
+ const SkColor glyph = chart.theme.colors[VROOM_COLOR_BADGE_TEXT];
150
+
151
+ // Badges belong to the plot, not the axis strips: a bar at the right edge
152
+ // must not spill its badge over the price labels.
153
+ canvas->save();
154
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, pane_right, pane_bottom));
155
+
156
+ for_each_badge(chart, lay, bounds, window_ms,
157
+ [&](const Badge& b, const Metrics& m) {
158
+ const bool hovered = chart.hovered_footprint_side == b.side &&
159
+ chart.hovered_footprint_time_ms == b.candle_time_ms;
160
+ const SkColor base = b.side == VROOM_FOOTPRINT_BUY ? bull : bear;
161
+ const SkColor fill = hovered ? boost(base, m.hover_boost) : base;
162
+
163
+ if (hovered) {
164
+ SkPaint halo;
165
+ halo.setAntiAlias(true);
166
+ halo.setStyle(SkPaint::kStroke_Style);
167
+ halo.setStrokeWidth(b.radius * kHaloWidthFrac);
168
+ halo.setColor(SkColorSetA(fill, kHaloAlpha));
169
+ const float r =
170
+ b.radius * (1.f + kHaloGapFrac + kHaloWidthFrac * 0.5f);
171
+ canvas->drawCircle(b.cx, b.cy, r, halo);
172
+ }
173
+
174
+ SkPaint body;
175
+ body.setAntiAlias(true);
176
+ body.setColor(fill);
177
+ canvas->drawCircle(b.cx, b.cy, b.radius, body);
178
+
179
+ draw_glyph(canvas, b, glyph);
180
+ });
181
+
182
+ canvas->restore();
183
+ }
184
+
185
+ ::VroomFootprintHit hit_test(const VroomChart& chart,
186
+ const Layout& lay,
187
+ const PriceBounds& bounds,
188
+ float x,
189
+ float y) {
190
+ ::VroomFootprintHit best{};
191
+ best.side = -1;
192
+
193
+ // The rect the badges are clipped to. Reported on every hit so the host can
194
+ // place a tooltip against the real plot edge rather than the element's.
195
+ const float pane_right = vroom::candle_area_width(lay);
196
+ const float pane_bottom = vroom::price_pane_bottom(lay);
197
+ best.pane_left = 0.f;
198
+ best.pane_top = 0.f;
199
+ best.pane_right = pane_right;
200
+ best.pane_bottom = pane_bottom;
201
+
202
+ const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
203
+ // Outside the plot there is nothing to hit, matching the draw-time clip.
204
+ if (x < 0.f || x > pane_right) return best;
205
+ if (y < 0.f || y > pane_bottom) return best;
206
+
207
+ float best_d2 = 0.f;
208
+ for_each_badge(chart, lay, bounds, window_ms, [&](const Badge& b, const Metrics&) {
209
+ if (!hits_badge(b.cx, b.cy, b.radius, x, y)) return;
210
+ // Stacked badges are drawn with a gap, but a generous hit tolerance can
211
+ // still overlap; the nearest center is the one the user meant.
212
+ const float dx = x - b.cx;
213
+ const float dy = y - b.cy;
214
+ const float d2 = dx * dx + dy * dy;
215
+ if (best.side >= 0 && d2 >= best_d2) return;
216
+ best_d2 = d2;
217
+ best.candle_time_ms = b.candle_time_ms;
218
+ best.side = b.side;
219
+ best.center_x = b.cx;
220
+ best.center_y = b.cy;
221
+ best.radius = b.radius;
222
+ });
223
+ return best;
224
+ }
225
+
226
+ } // namespace vroom::footprints
@@ -0,0 +1,45 @@
1
+ // Footprints — circular badges marking where a trader entered and exited, drawn
2
+ // above the candle each trade fell in.
3
+ //
4
+ // A buy is a "+" in the bull color, a sell a "-" in the bear color, both glyphs in
5
+ // VROOM_COLOR_BADGE_TEXT. Only one badge per side per candle renders however many
6
+ // fills went into it; the host learns the full set through
7
+ // vroom_chart_footprints_at and draws its own tooltip.
8
+ //
9
+ // Bucketing and badge geometry live in footprints_layout.h so the draw and
10
+ // hit-test passes agree by construction.
11
+
12
+ #pragma once
13
+
14
+ #include <cstdint>
15
+
16
+ #include "vroom/vroom_chart.h"
17
+
18
+ class SkCanvas;
19
+ struct VroomChart;
20
+
21
+ namespace vroom {
22
+ struct Layout;
23
+ struct PriceBounds;
24
+ } // namespace vroom
25
+
26
+ namespace vroom::footprints {
27
+
28
+ // Draws every visible footprint badge, clipped to the price pane. `window_ms` is
29
+ // the visible time span, used to place each badge over its candle's slot center.
30
+ void draw(SkCanvas* canvas,
31
+ const VroomChart& chart,
32
+ const Layout& lay,
33
+ const PriceBounds& bounds,
34
+ int64_t window_ms);
35
+
36
+ // Hit-tests pixel (x, y) against the badges. The returned hit's `side` is -1 on a
37
+ // miss; when two badges overlap the nearest center wins. Derives geometry from the
38
+ // same layout functions as draw, so what the user sees is what they can hover.
39
+ ::VroomFootprintHit hit_test(const VroomChart& chart,
40
+ const Layout& lay,
41
+ const PriceBounds& bounds,
42
+ float x,
43
+ float y);
44
+
45
+ } // namespace vroom::footprints
@@ -0,0 +1,140 @@
1
+ #include "footprints_layout.h"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+
6
+ namespace vroom::footprints {
7
+
8
+ int32_t bucket_index(const ::VroomCandle* candles, size_t count,
9
+ int64_t duration_ms, int64_t time_ms) {
10
+ if (!candles || count == 0 || duration_ms <= 0) return -1;
11
+
12
+ // Last candle that opened at or before the trade.
13
+ const ::VroomCandle* it =
14
+ std::upper_bound(candles, candles + count, time_ms,
15
+ [](int64_t t, const ::VroomCandle& c) { return t < c.time_ms; });
16
+ if (it == candles) return -1; // before the first bar
17
+ const int32_t i = static_cast<int32_t>((it - 1) - candles);
18
+
19
+ // Inside that bar's window, or in a gap where no bar exists.
20
+ if (time_ms >= candles[i].time_ms + duration_ms) return -1;
21
+ return i;
22
+ }
23
+
24
+ std::vector<Bucket> build_buckets(const ::VroomCandle* candles, size_t count,
25
+ int64_t duration_ms,
26
+ const ::VroomFootprint* prints, size_t print_count) {
27
+ std::vector<Bucket> out;
28
+ if (!candles || count == 0 || !prints || print_count == 0) return out;
29
+
30
+ // Bucket per candle index, then compact. Candle counts stay in the low
31
+ // thousands, so a flat scratch vector beats a map on both sides of the ledger.
32
+ std::vector<int32_t> slot(count, -1);
33
+
34
+ for (size_t i = 0; i < print_count; ++i) {
35
+ const ::VroomFootprint& p = prints[i];
36
+ if (p.side != VROOM_FOOTPRINT_BUY && p.side != VROOM_FOOTPRINT_SELL) continue;
37
+
38
+ const int32_t ci = bucket_index(candles, count, duration_ms, p.time_ms);
39
+ if (ci < 0) continue;
40
+
41
+ if (slot[static_cast<size_t>(ci)] < 0) {
42
+ slot[static_cast<size_t>(ci)] = static_cast<int32_t>(out.size());
43
+ Bucket b;
44
+ b.candle_time_ms = candles[ci].time_ms;
45
+ out.push_back(std::move(b));
46
+ }
47
+ Bucket& b = out[static_cast<size_t>(slot[static_cast<size_t>(ci)])];
48
+ if (p.side == VROOM_FOOTPRINT_BUY) {
49
+ b.buys.push_back(static_cast<int32_t>(i));
50
+ } else {
51
+ b.sells.push_back(static_cast<int32_t>(i));
52
+ }
53
+ }
54
+
55
+ // The host's array can arrive in any order, so sort each side by time and read
56
+ // the latest off the end.
57
+ const auto by_time = [prints](int32_t a, int32_t b) {
58
+ const int64_t ta = prints[static_cast<size_t>(a)].time_ms;
59
+ const int64_t tb = prints[static_cast<size_t>(b)].time_ms;
60
+ // Fall back to the host's order so equal timestamps stay stable.
61
+ return ta != tb ? ta < tb : a < b;
62
+ };
63
+ for (Bucket& b : out) {
64
+ std::sort(b.buys.begin(), b.buys.end(), by_time);
65
+ std::sort(b.sells.begin(), b.sells.end(), by_time);
66
+ if (!b.buys.empty()) {
67
+ b.buy_latest_ms = prints[static_cast<size_t>(b.buys.back())].time_ms;
68
+ }
69
+ if (!b.sells.empty()) {
70
+ b.sell_latest_ms = prints[static_cast<size_t>(b.sells.back())].time_ms;
71
+ }
72
+ }
73
+
74
+ // Buckets were created in first-seen order; callers want them by candle.
75
+ std::sort(out.begin(), out.end(), [](const Bucket& a, const Bucket& b) {
76
+ return a.candle_time_ms < b.candle_time_ms;
77
+ });
78
+ return out;
79
+ }
80
+
81
+ Metrics metrics_from(const ::VroomFootprintStyle* style) {
82
+ Metrics m;
83
+ if (!style) return m;
84
+ if (style->radius_px > 0.f) m.radius = style->radius_px;
85
+ if (style->gap_px > 0.f) m.gap = style->gap_px;
86
+ if (style->margin_px > 0.f) m.margin = style->margin_px;
87
+ if (style->hover_boost > 0.f) m.hover_boost = style->hover_boost;
88
+ return m;
89
+ }
90
+
91
+ Stack layout_stack(const Bucket& bucket, float high_y, float pane_top,
92
+ float radius, float gap, float margin) {
93
+ Stack out;
94
+ if (radius <= 0.f) radius = kRadius;
95
+ if (gap <= 0.f) gap = kGap;
96
+ if (margin <= 0.f) margin = kMargin;
97
+
98
+ const bool buys = bucket.has_buys();
99
+ const bool sells = bucket.has_sells();
100
+ if (!buys && !sells) return out;
101
+
102
+ if (buys && sells) {
103
+ // Bottom-to-top replays the order the trades happened in. Ties go to the
104
+ // buy, so an entry and exit stamped the same ms still lay out predictably.
105
+ const bool buy_first = bucket.buy_latest_ms <= bucket.sell_latest_ms;
106
+ out.sides[0] = buy_first ? VROOM_FOOTPRINT_BUY : VROOM_FOOTPRINT_SELL;
107
+ out.sides[1] = buy_first ? VROOM_FOOTPRINT_SELL : VROOM_FOOTPRINT_BUY;
108
+ out.count = 2;
109
+ } else {
110
+ out.sides[0] = buys ? VROOM_FOOTPRINT_BUY : VROOM_FOOTPRINT_SELL;
111
+ out.count = 1;
112
+ }
113
+
114
+ // Slot 0 clears the high by `margin`; each further slot clears the one below
115
+ // it by `gap`, which is what keeps them separately hoverable.
116
+ const float pitch = 2.f * radius + gap;
117
+ for (int32_t i = 0; i < out.count; ++i) {
118
+ out.y[i] = high_y - margin - radius - static_cast<float>(i) * pitch;
119
+ }
120
+
121
+ // A bar near the top of the view would push its stack off-screen; slide the
122
+ // whole stack down so the topmost badge stays inside the pane. Moving both
123
+ // together preserves the order the stack encodes.
124
+ const float top_edge = out.y[out.count - 1] - radius;
125
+ if (top_edge < pane_top) {
126
+ const float shift = pane_top - top_edge;
127
+ for (int32_t i = 0; i < out.count; ++i) out.y[i] += shift;
128
+ }
129
+ return out;
130
+ }
131
+
132
+ bool hits_badge(float cx, float cy, float radius, float x, float y) {
133
+ if (radius <= 0.f) return false;
134
+ const float dx = x - cx;
135
+ const float dy = y - cy;
136
+ const float r = radius + kHitTolerance;
137
+ return dx * dx + dy * dy <= r * r;
138
+ }
139
+
140
+ } // namespace vroom::footprints
@@ -0,0 +1,94 @@
1
+ // Footprint bucketing + badge geometry — pure functions, no Skia, no state.
2
+ //
3
+ // Split out from footprints.cpp for the same reason as price_line_layout.h: the
4
+ // render pass and the hit-test pass must agree on where every badge sits, or the
5
+ // user ends up hovering nothing where a badge clearly is. Being Skia-free it also
6
+ // unit-tests without a Skia checkout.
7
+
8
+ #pragma once
9
+
10
+ #include <cstddef>
11
+ #include <cstdint>
12
+ #include <vector>
13
+
14
+ #include "vroom/vroom_chart.h"
15
+
16
+ namespace vroom::footprints {
17
+
18
+ // Badge radius, the gap between two stacked badges, and the gap between the
19
+ // candle's high and the first badge. Defaults for a zero/negative style field.
20
+ constexpr float kRadius = 9.f;
21
+ constexpr float kGap = 4.f;
22
+ constexpr float kMargin = 8.f;
23
+ constexpr float kHoverBoost = 1.25f;
24
+
25
+ // Slop around the circle so a 9px badge is still a comfortable target, on a
26
+ // mouse and under a thumb alike.
27
+ constexpr float kHitTolerance = 3.f;
28
+
29
+ // One candle's footprints, split by side. Both lists hold indices into the array
30
+ // the host passed to vroom_chart_set_footprints — its order, so a caller can map
31
+ // them straight back to its own trade objects — sorted ascending by time.
32
+ struct Bucket {
33
+ int64_t candle_time_ms = 0;
34
+ std::vector<int32_t> buys;
35
+ std::vector<int32_t> sells;
36
+ // Time of the last trade on each side, which decides the stack order. Only
37
+ // meaningful when the matching list is non-empty.
38
+ int64_t buy_latest_ms = 0;
39
+ int64_t sell_latest_ms = 0;
40
+
41
+ bool has_buys() const { return !buys.empty(); }
42
+ bool has_sells() const { return !sells.empty(); }
43
+ // Every candle that made it into a bucket has at least one badge to draw.
44
+ int32_t badge_count() const { return (has_buys() ? 1 : 0) + (has_sells() ? 1 : 0); }
45
+ };
46
+
47
+ // Index of the candle whose [time_ms, time_ms + duration_ms) window contains
48
+ // `time_ms`, or -1 when it falls in no window — before the first candle, or in a
49
+ // gap (a weekend, a halt) where no bar exists. Candles must be ascending.
50
+ int32_t bucket_index(const ::VroomCandle* candles, size_t count,
51
+ int64_t duration_ms, int64_t time_ms);
52
+
53
+ // Groups `prints` onto candles. The result is ascending by candle time and holds
54
+ // only candles that got at least one footprint, so it is also the dedupe: however
55
+ // many trades a bar collected, it comes back as one bucket with at most two sides.
56
+ // Footprints with an unknown side, or that land in no candle's window, are dropped.
57
+ std::vector<Bucket> build_buckets(const ::VroomCandle* candles, size_t count,
58
+ int64_t duration_ms,
59
+ const ::VroomFootprint* prints, size_t print_count);
60
+
61
+ // The laid-out badge stack for one candle: one or two centers, going up from the
62
+ // candle's high. Slot 0 is the one nearest the bar.
63
+ struct Stack {
64
+ int32_t count = 0;
65
+ int32_t sides[2]{}; // VroomFootprintSide per slot
66
+ float y[2]{}; // center y per slot
67
+ };
68
+
69
+ // Stacks `bucket`'s badges above `high_y` (the pixel y of the candle's high).
70
+ //
71
+ // The side whose latest trade came *first* takes the lower slot, so reading the
72
+ // stack bottom-to-top replays the order the trades happened in. The whole stack
73
+ // shifts down if it would poke out the top of the pane, keeping badges on a bar
74
+ // near the high of the view visible and hoverable rather than clipped away.
75
+ //
76
+ // Style fields at or below zero fall back to kRadius / kGap / kMargin.
77
+ Stack layout_stack(const Bucket& bucket, float high_y, float pane_top,
78
+ float radius, float gap, float margin);
79
+
80
+ // True when (x, y) is within a badge of `radius` centered at (cx, cy), plus
81
+ // kHitTolerance.
82
+ bool hits_badge(float cx, float cy, float radius, float x, float y);
83
+
84
+ // Resolves the style struct's raw fields to the values the layout should use.
85
+ // A null `style`, or any field left at zero, takes the default.
86
+ struct Metrics {
87
+ float radius = kRadius;
88
+ float gap = kGap;
89
+ float margin = kMargin;
90
+ float hover_boost = kHoverBoost;
91
+ };
92
+ Metrics metrics_from(const ::VroomFootprintStyle* style);
93
+
94
+ } // namespace vroom::footprints