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.
- package/cpp/VroomChartHostObject.cpp +289 -2
- package/cpp/_core_include/vroom/vroom_chart.h +221 -4
- package/cpp/_core_src/atr.cpp +67 -0
- package/cpp/_core_src/atr.h +44 -0
- package/cpp/_core_src/atr_pane.cpp +162 -0
- package/cpp/_core_src/atr_pane.h +48 -0
- package/cpp/_core_src/chart.cpp +477 -172
- package/cpp/_core_src/chart.h +173 -1
- package/cpp/_core_src/chart_facade.cpp +317 -23
- package/cpp/_core_src/fair_value_gaps.cpp +76 -0
- package/cpp/_core_src/fair_value_gaps.h +54 -0
- package/cpp/_core_src/footprints.cpp +226 -0
- package/cpp/_core_src/footprints.h +45 -0
- package/cpp/_core_src/footprints_layout.cpp +140 -0
- package/cpp/_core_src/footprints_layout.h +94 -0
- package/cpp/_core_src/fvg_overlay.cpp +262 -0
- package/cpp/_core_src/fvg_overlay.h +43 -0
- package/cpp/_core_src/ichimoku.cpp +65 -0
- package/cpp/_core_src/ichimoku.h +45 -0
- package/cpp/_core_src/labels.cpp +3 -0
- package/cpp/_core_src/line_morph.h +159 -0
- package/cpp/_core_src/ma_overlay.cpp +259 -36
- package/cpp/_core_src/ma_overlay.h +57 -2
- package/cpp/_core_src/macd.cpp +24 -1
- package/cpp/_core_src/macd.h +16 -0
- package/cpp/_core_src/macd_pane.cpp +71 -62
- package/cpp/_core_src/macd_pane.h +13 -1
- package/cpp/_core_src/pane_series.h +169 -0
- package/cpp/_core_src/rsi.cpp +4 -0
- package/cpp/_core_src/rsi.h +7 -0
- package/cpp/_core_src/rsi_pane.cpp +85 -29
- package/cpp/_core_src/rsi_pane.h +11 -1
- package/cpp/_core_src/tip_pulse.h +4 -4
- package/cpp/_core_src/viewport.h +35 -0
- package/lib/index.d.mts +385 -3
- package/lib/index.d.ts +385 -3
- package/lib/index.js +305 -7
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +305 -7
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +108 -7
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +10 -0
- package/src/jsi.d.ts +136 -1
- package/src/types.ts +10 -0
- 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
|