react-native-vroom-chart 0.14.0 → 0.15.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 +118 -1
- package/cpp/_core_include/vroom/vroom_chart.h +84 -0
- package/cpp/_core_src/chart.cpp +16 -0
- package/cpp/_core_src/chart.h +23 -0
- package/cpp/_core_src/chart_facade.cpp +108 -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/tip_pulse.h +4 -4
- package/lib/index.d.mts +135 -1
- package/lib/index.d.ts +135 -1
- package/lib/index.js +83 -3
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +83 -3
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +87 -4
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +39 -0
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +49 -3
|
@@ -42,7 +42,7 @@ ChartHostObject::~ChartHostObject() {
|
|
|
42
42
|
std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
43
43
|
jsi::Runtime& rt) {
|
|
44
44
|
std::vector<jsi::PropNameID> out;
|
|
45
|
-
out.reserve(
|
|
45
|
+
out.reserve(42);
|
|
46
46
|
out.push_back(jsi::PropNameID::forAscii(rt, "setCandles"));
|
|
47
47
|
out.push_back(jsi::PropNameID::forAscii(rt, "setSize"));
|
|
48
48
|
out.push_back(jsi::PropNameID::forAscii(rt, "setColor"));
|
|
@@ -82,6 +82,9 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
82
82
|
out.push_back(jsi::PropNameID::forAscii(rt, "hitTestPriceLine"));
|
|
83
83
|
out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLineHover"));
|
|
84
84
|
out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLineDrag"));
|
|
85
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setFootprints"));
|
|
86
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "hitTestFootprint"));
|
|
87
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setFootprintHover"));
|
|
85
88
|
out.push_back(jsi::PropNameID::forAscii(rt, "render"));
|
|
86
89
|
return out;
|
|
87
90
|
}
|
|
@@ -1189,6 +1192,120 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
1189
1192
|
});
|
|
1190
1193
|
}
|
|
1191
1194
|
|
|
1195
|
+
if (name == "setFootprints") {
|
|
1196
|
+
// setFootprints({ prints: [{ timeMs, side }, ...], radiusPx, gapPx, marginPx,
|
|
1197
|
+
// hoverBoost }) — replaces the full set of executed-trade badges. The core
|
|
1198
|
+
// buckets them onto candles itself, so raw fill times are what to pass. No
|
|
1199
|
+
// render; the next render() picks it up.
|
|
1200
|
+
return jsi::Function::createFromHostFunction(
|
|
1201
|
+
rt,
|
|
1202
|
+
jsi::PropNameID::forAscii(rt, "setFootprints"),
|
|
1203
|
+
1,
|
|
1204
|
+
[this](jsi::Runtime& rt2,
|
|
1205
|
+
const jsi::Value& /*thisVal*/,
|
|
1206
|
+
const jsi::Value* args,
|
|
1207
|
+
size_t count) -> jsi::Value {
|
|
1208
|
+
if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
|
|
1209
|
+
auto cfg = args[0].asObject(rt2);
|
|
1210
|
+
auto prints_val = cfg.getProperty(rt2, "prints");
|
|
1211
|
+
if (!prints_val.isObject()) return jsi::Value::undefined();
|
|
1212
|
+
auto prints_obj = prints_val.asObject(rt2);
|
|
1213
|
+
if (!prints_obj.isArray(rt2)) return jsi::Value::undefined();
|
|
1214
|
+
auto arr = prints_obj.asArray(rt2);
|
|
1215
|
+
const size_t len = arr.size(rt2);
|
|
1216
|
+
std::vector<VroomFootprint> prints(len);
|
|
1217
|
+
for (size_t i = 0; i < len; ++i) {
|
|
1218
|
+
auto f = arr.getValueAtIndex(rt2, i).asObject(rt2);
|
|
1219
|
+
prints[i].time_ms =
|
|
1220
|
+
static_cast<int64_t>(f.getProperty(rt2, "timeMs").asNumber());
|
|
1221
|
+
prints[i].side =
|
|
1222
|
+
static_cast<int32_t>(f.getProperty(rt2, "side").asNumber());
|
|
1223
|
+
}
|
|
1224
|
+
VroomFootprintStyle style{};
|
|
1225
|
+
style.radius_px = static_cast<float>(
|
|
1226
|
+
cfg.getProperty(rt2, "radiusPx").asNumber());
|
|
1227
|
+
style.gap_px = static_cast<float>(
|
|
1228
|
+
cfg.getProperty(rt2, "gapPx").asNumber());
|
|
1229
|
+
style.margin_px = static_cast<float>(
|
|
1230
|
+
cfg.getProperty(rt2, "marginPx").asNumber());
|
|
1231
|
+
style.hover_boost = static_cast<float>(
|
|
1232
|
+
cfg.getProperty(rt2, "hoverBoost").asNumber());
|
|
1233
|
+
vroom_chart_set_footprints(chart_, prints.data(), prints.size(), &style);
|
|
1234
|
+
return jsi::Value::undefined();
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (name == "hitTestFootprint") {
|
|
1239
|
+
// hitTestFootprint(x, y) -> { side, candleTimeMs, x, y, radius, pane,
|
|
1240
|
+
// indices } | null. `indices` addresses the array last passed to
|
|
1241
|
+
// setFootprints and covers *both* sides of that candle, so one call fills a
|
|
1242
|
+
// tooltip; `pane` is the plot rect, for deciding which side of the badge that
|
|
1243
|
+
// tooltip fits on. No rendering.
|
|
1244
|
+
return jsi::Function::createFromHostFunction(
|
|
1245
|
+
rt,
|
|
1246
|
+
jsi::PropNameID::forAscii(rt, "hitTestFootprint"),
|
|
1247
|
+
2,
|
|
1248
|
+
[this](jsi::Runtime& rt2,
|
|
1249
|
+
const jsi::Value& /*thisVal*/,
|
|
1250
|
+
const jsi::Value* args,
|
|
1251
|
+
size_t count) -> jsi::Value {
|
|
1252
|
+
if (count < 2) return jsi::Value::null();
|
|
1253
|
+
VroomFootprintHit hit{};
|
|
1254
|
+
if (!vroom_chart_hit_test_footprint(
|
|
1255
|
+
chart_, static_cast<float>(args[0].asNumber()),
|
|
1256
|
+
static_cast<float>(args[1].asNumber()), &hit)) {
|
|
1257
|
+
return jsi::Value::null();
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const int32_t n =
|
|
1261
|
+
vroom_chart_footprints_at(chart_, hit.candle_time_ms, nullptr, 0);
|
|
1262
|
+
std::vector<int32_t> idx(static_cast<size_t>(n > 0 ? n : 0));
|
|
1263
|
+
if (n > 0) {
|
|
1264
|
+
vroom_chart_footprints_at(chart_, hit.candle_time_ms, idx.data(), n);
|
|
1265
|
+
}
|
|
1266
|
+
jsi::Array indices(rt2, idx.size());
|
|
1267
|
+
for (size_t i = 0; i < idx.size(); ++i) {
|
|
1268
|
+
indices.setValueAtIndex(rt2, i, jsi::Value(idx[i]));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
jsi::Object pane(rt2);
|
|
1272
|
+
pane.setProperty(rt2, "left", hit.pane_left);
|
|
1273
|
+
pane.setProperty(rt2, "top", hit.pane_top);
|
|
1274
|
+
pane.setProperty(rt2, "right", hit.pane_right);
|
|
1275
|
+
pane.setProperty(rt2, "bottom", hit.pane_bottom);
|
|
1276
|
+
|
|
1277
|
+
jsi::Object obj(rt2);
|
|
1278
|
+
obj.setProperty(rt2, "side", hit.side);
|
|
1279
|
+
obj.setProperty(rt2, "candleTimeMs",
|
|
1280
|
+
static_cast<double>(hit.candle_time_ms));
|
|
1281
|
+
obj.setProperty(rt2, "x", hit.center_x);
|
|
1282
|
+
obj.setProperty(rt2, "y", hit.center_y);
|
|
1283
|
+
obj.setProperty(rt2, "radius", hit.radius);
|
|
1284
|
+
obj.setProperty(rt2, "pane", std::move(pane));
|
|
1285
|
+
obj.setProperty(rt2, "indices", std::move(indices));
|
|
1286
|
+
return obj;
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (name == "setFootprintHover") {
|
|
1291
|
+
// setFootprintHover(candleTimeMs, side) — highlight one badge; side -1
|
|
1292
|
+
// clears. Touch has no hover, so on RN this tracks the tapped badge.
|
|
1293
|
+
return jsi::Function::createFromHostFunction(
|
|
1294
|
+
rt,
|
|
1295
|
+
jsi::PropNameID::forAscii(rt, "setFootprintHover"),
|
|
1296
|
+
2,
|
|
1297
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
1298
|
+
const jsi::Value& /*thisVal*/,
|
|
1299
|
+
const jsi::Value* args,
|
|
1300
|
+
size_t count) -> jsi::Value {
|
|
1301
|
+
if (count < 2) return jsi::Value::undefined();
|
|
1302
|
+
vroom_chart_set_footprint_hover(
|
|
1303
|
+
chart_, static_cast<int64_t>(args[0].asNumber()),
|
|
1304
|
+
static_cast<int32_t>(args[1].asNumber()));
|
|
1305
|
+
return jsi::Value::undefined();
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1192
1309
|
if (name == "render") {
|
|
1193
1310
|
return jsi::Function::createFromHostFunction(
|
|
1194
1311
|
rt,
|
|
@@ -257,6 +257,53 @@ typedef struct VroomPriceLineStyle {
|
|
|
257
257
|
float hover_boost; // brightness multiplier for the hovered segment (1 = flat)
|
|
258
258
|
} VroomPriceLineStyle;
|
|
259
259
|
|
|
260
|
+
// ---- Footprints (executed-trade badges) -----------------------------------
|
|
261
|
+
|
|
262
|
+
typedef enum {
|
|
263
|
+
VROOM_FOOTPRINT_BUY = 0, // position entry — "+" badge in the bull color
|
|
264
|
+
VROOM_FOOTPRINT_SELL = 1, // position exit — "-" badge in the bear color
|
|
265
|
+
} VroomFootprintSide;
|
|
266
|
+
|
|
267
|
+
// A single executed trade, drawn as a circular badge above the candle it fell in.
|
|
268
|
+
//
|
|
269
|
+
// `time_ms` is the raw execution time, *not* a bar-open time: the core buckets
|
|
270
|
+
// each footprint into whichever candle's window contains it, so one array renders
|
|
271
|
+
// correctly at every interval and re-groups by itself when the candles change.
|
|
272
|
+
// Order is irrelevant — the core sorts within each bucket.
|
|
273
|
+
//
|
|
274
|
+
// Only the fields the renderer needs live here; a host's per-trade payload (id,
|
|
275
|
+
// price, size) stays on the host side and is rejoined via the indices reported by
|
|
276
|
+
// vroom_chart_footprints_at.
|
|
277
|
+
typedef struct VroomFootprint {
|
|
278
|
+
int64_t time_ms;
|
|
279
|
+
int32_t side; // VroomFootprintSide
|
|
280
|
+
} VroomFootprint;
|
|
281
|
+
|
|
282
|
+
// Layout/style shared by every footprint badge.
|
|
283
|
+
typedef struct VroomFootprintStyle {
|
|
284
|
+
float radius_px; // badge radius; <= 0 falls back to 9
|
|
285
|
+
float gap_px; // vertical gap between the two stacked badges; < 0 => 4
|
|
286
|
+
float margin_px; // gap between the candle's high and the first badge; < 0 => 8
|
|
287
|
+
float hover_boost; // brightness multiplier for the hovered badge (1 = flat)
|
|
288
|
+
} VroomFootprintStyle;
|
|
289
|
+
|
|
290
|
+
// The badge under a pixel, as reported by vroom_chart_hit_test_footprint.
|
|
291
|
+
typedef struct VroomFootprintHit {
|
|
292
|
+
int64_t candle_time_ms; // bar-open time of the candle the badge sits on
|
|
293
|
+
int32_t side; // VroomFootprintSide — which of the candle's two badges
|
|
294
|
+
float center_x; // badge center, in the same px space as the hit test
|
|
295
|
+
float center_y;
|
|
296
|
+
float radius;
|
|
297
|
+
// The plot rect the badge was clipped to — same px space, axis strips
|
|
298
|
+
// excluded. Reported because a host placing its own tooltip has no other way
|
|
299
|
+
// to know it: the surface it measures includes the price and time axes, so
|
|
300
|
+
// sizing against that overstates the room next to a badge near the edge.
|
|
301
|
+
float pane_left;
|
|
302
|
+
float pane_top;
|
|
303
|
+
float pane_right;
|
|
304
|
+
float pane_bottom;
|
|
305
|
+
} VroomFootprintHit;
|
|
306
|
+
|
|
260
307
|
// A continuous data coordinate at a pixel position (no candle snapping). Used to
|
|
261
308
|
// translate a drawing-tool click into a data-space anchor.
|
|
262
309
|
typedef struct VroomCoord {
|
|
@@ -689,6 +736,43 @@ void vroom_chart_set_price_line_hover(VroomChart* chart, int32_t index,
|
|
|
689
736
|
void vroom_chart_set_price_line_drag(VroomChart* chart, int32_t index,
|
|
690
737
|
double price);
|
|
691
738
|
|
|
739
|
+
// Replaces the full set of footprints and their shared style. Pass count 0 to
|
|
740
|
+
// clear; `style` may be null when count is 0.
|
|
741
|
+
//
|
|
742
|
+
// Footprints are grouped onto candles by timestamp, so at most two badges render
|
|
743
|
+
// per candle: one for that bar's buys and one for its sells, however many trades
|
|
744
|
+
// went into each. When a candle has both, they stack upward with `gap_px` between
|
|
745
|
+
// them, the side whose latest trade came first sitting nearest the bar. Badges sit
|
|
746
|
+
// above the candle's high, clipped to the price pane, and re-group on their own
|
|
747
|
+
// whenever the candles change (a new interval, more history) — the host never
|
|
748
|
+
// re-buckets anything.
|
|
749
|
+
void vroom_chart_set_footprints(VroomChart* chart, const VroomFootprint* prints,
|
|
750
|
+
size_t count,
|
|
751
|
+
const VroomFootprintStyle* style);
|
|
752
|
+
|
|
753
|
+
// Hit-tests pixel (x_px, y_px) against the footprint badges. On a hit fills *out
|
|
754
|
+
// with the badge's candle, side and pixel geometry and returns true; returns false
|
|
755
|
+
// on a miss (out untouched). `out` may be null to test without reading the hit.
|
|
756
|
+
// When two badges overlap the nearest center wins.
|
|
757
|
+
bool vroom_chart_hit_test_footprint(VroomChart* chart, float x_px, float y_px,
|
|
758
|
+
VroomFootprintHit* out);
|
|
759
|
+
|
|
760
|
+
// Fills `out_indices` with the indices — into the array last passed to
|
|
761
|
+
// vroom_chart_set_footprints — of every footprint bucketed into the candle opening
|
|
762
|
+
// at `candle_time_ms`, *both* sides, ascending by time. Returns the total count,
|
|
763
|
+
// which may exceed `max` (only the first `max` are written), or 0 when that candle
|
|
764
|
+
// has no footprints. Pass a null `out_indices` with max 0 to size the buffer first.
|
|
765
|
+
//
|
|
766
|
+
// This is how a host rejoins a hit badge to its own trade objects: hit-test to get
|
|
767
|
+
// the candle, then map these indices back through the array it supplied.
|
|
768
|
+
int32_t vroom_chart_footprints_at(VroomChart* chart, int64_t candle_time_ms,
|
|
769
|
+
int32_t* out_indices, int32_t max);
|
|
770
|
+
|
|
771
|
+
// Marks one footprint badge as hovered so it renders highlighted. `side` -1
|
|
772
|
+
// clears the hover; `candle_time_ms` and `side` match vroom_chart_hit_test_footprint.
|
|
773
|
+
void vroom_chart_set_footprint_hover(VroomChart* chart, int64_t candle_time_ms,
|
|
774
|
+
int32_t side);
|
|
775
|
+
|
|
692
776
|
// Sets the transient in-progress "draft" the drawing tool shows while the user
|
|
693
777
|
// places points. Node A is always shown; when `has_b`, node B is shown too.
|
|
694
778
|
// `guide != 0` also draws the live preview (a guideline for a line, or a preview
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
#include "chart_internal.h"
|
|
26
26
|
#include "crosshair.h"
|
|
27
27
|
#include "drawings.h"
|
|
28
|
+
#include "footprints.h"
|
|
28
29
|
#include "labels.h"
|
|
29
30
|
#include "liquidity.h"
|
|
30
31
|
#include "ma.h"
|
|
@@ -125,6 +126,14 @@ void VroomChart::ensure_bollinger() {
|
|
|
125
126
|
bollinger_dirty = false;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
void VroomChart::ensure_footprint_buckets() {
|
|
130
|
+
if (!footprint_buckets_dirty) return;
|
|
131
|
+
footprint_buckets = vroom::footprints::build_buckets(
|
|
132
|
+
candles.data(), candles.size(), candle_duration_ms, footprints.data(),
|
|
133
|
+
footprints.size());
|
|
134
|
+
footprint_buckets_dirty = false;
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
129
138
|
begin_frame();
|
|
130
139
|
const auto lay = layout();
|
|
@@ -480,6 +489,13 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
480
489
|
// stays readable.
|
|
481
490
|
vroom::price_lines::draw(canvas, *this, lay, bounds, candle_right,
|
|
482
491
|
candle_area_h);
|
|
492
|
+
|
|
493
|
+
// 7.56. Footprint badges — data-anchored chrome that must not be hidden
|
|
494
|
+
// by candles or overlays, so it draws with the price lines rather
|
|
495
|
+
// than back at the drawings layer. Below the crosshair, which the
|
|
496
|
+
// user is actively pointing with.
|
|
497
|
+
ensure_footprint_buckets();
|
|
498
|
+
vroom::footprints::draw(canvas, *this, lay, bounds, window_ms);
|
|
483
499
|
}
|
|
484
500
|
|
|
485
501
|
// 7.6. Indicator panes stacked below the candles, ordered by enable
|
package/cpp/_core_src/chart.h
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
#include "include/core/SkRefCnt.h"
|
|
23
23
|
#pragma clang diagnostic pop
|
|
24
24
|
|
|
25
|
+
#include "footprints_layout.h"
|
|
25
26
|
#include "labels.h"
|
|
26
27
|
#include "price_format.h"
|
|
27
28
|
#include "theme.h"
|
|
@@ -280,6 +281,28 @@ struct VroomChart {
|
|
|
280
281
|
int32_t dragged_price_line = -1;
|
|
281
282
|
double dragged_price_line_price = 0.0;
|
|
282
283
|
|
|
284
|
+
// --- footprints (executed-trade badges) ---------------------------------
|
|
285
|
+
// Kept in the *host's* order so the indices reported by
|
|
286
|
+
// vroom_chart_footprints_at map straight back to its own trade objects.
|
|
287
|
+
std::vector<VroomFootprint> footprints;
|
|
288
|
+
VroomFootprintStyle footprint_style{};
|
|
289
|
+
|
|
290
|
+
// Footprints grouped onto candles, which is what the renderer and hit-test
|
|
291
|
+
// actually walk. Derived from `footprints` + `candles`, so it is rebuilt
|
|
292
|
+
// lazily (see ensure_footprint_buckets) whenever either changes — that
|
|
293
|
+
// rebuild is what re-groups badges onto the right bars after an interval
|
|
294
|
+
// switch, with no involvement from the host.
|
|
295
|
+
std::vector<vroom::footprints::Bucket> footprint_buckets;
|
|
296
|
+
bool footprint_buckets_dirty = true;
|
|
297
|
+
|
|
298
|
+
// Which badge renders highlighted, keyed the way the hit-test reports it.
|
|
299
|
+
// side -1 = none.
|
|
300
|
+
int64_t hovered_footprint_time_ms = 0;
|
|
301
|
+
int32_t hovered_footprint_side = -1;
|
|
302
|
+
|
|
303
|
+
// Rebuilds footprint_buckets if either input changed since the last call.
|
|
304
|
+
void ensure_footprint_buckets();
|
|
305
|
+
|
|
283
306
|
// --- theme --------------------------------------------------------------
|
|
284
307
|
vroom::Theme theme;
|
|
285
308
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
#include "chart.h"
|
|
15
15
|
#include "drawings.h"
|
|
16
|
+
#include "footprints.h"
|
|
16
17
|
#include "labels.h"
|
|
17
18
|
#include "price_lines.h"
|
|
18
19
|
#include "viewport.h"
|
|
@@ -186,6 +187,9 @@ extern "C" void vroom_chart_set_candles(VroomChart* chart, const VroomCandle* da
|
|
|
186
187
|
chart->overlays_dirty = true;
|
|
187
188
|
chart->vwap_dirty = true;
|
|
188
189
|
chart->bollinger_dirty = true;
|
|
190
|
+
// New bars (or a whole new interval) mean the footprint grouping no longer
|
|
191
|
+
// matches the data — regroup on next use.
|
|
192
|
+
chart->footprint_buckets_dirty = true;
|
|
189
193
|
|
|
190
194
|
// Infer the candle period from the first interval. Robust enough for
|
|
191
195
|
// uniform-duration series (the only kind we model today).
|
|
@@ -1023,6 +1027,110 @@ extern "C" void vroom_chart_set_price_line_drag(VroomChart* chart, int32_t index
|
|
|
1023
1027
|
chart->mark_dirty();
|
|
1024
1028
|
}
|
|
1025
1029
|
|
|
1030
|
+
// ---- Footprints ------------------------------------------------------------
|
|
1031
|
+
|
|
1032
|
+
extern "C" void vroom_chart_set_footprints(VroomChart* chart,
|
|
1033
|
+
const VroomFootprint* prints,
|
|
1034
|
+
size_t count,
|
|
1035
|
+
const VroomFootprintStyle* style) {
|
|
1036
|
+
if (!chart) return;
|
|
1037
|
+
// Stored in the caller's order: vroom_chart_footprints_at hands these indices
|
|
1038
|
+
// back, and they're only useful if they still address the caller's array.
|
|
1039
|
+
if (count > 0 && prints) {
|
|
1040
|
+
chart->footprints.assign(prints, prints + count);
|
|
1041
|
+
} else {
|
|
1042
|
+
chart->footprints.clear();
|
|
1043
|
+
}
|
|
1044
|
+
if (style) chart->footprint_style = *style;
|
|
1045
|
+
chart->footprint_buckets_dirty = true;
|
|
1046
|
+
// A badge that no longer exists must not keep its highlight.
|
|
1047
|
+
if (chart->footprints.empty()) chart->hovered_footprint_side = -1;
|
|
1048
|
+
chart->mark_dirty();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
extern "C" bool vroom_chart_hit_test_footprint(VroomChart* chart, float x_px,
|
|
1052
|
+
float y_px,
|
|
1053
|
+
VroomFootprintHit* out) {
|
|
1054
|
+
if (!chart || chart->footprints.empty() || chart->candles.empty()) return false;
|
|
1055
|
+
chart->ensure_footprint_buckets();
|
|
1056
|
+
if (chart->footprint_buckets.empty()) return false;
|
|
1057
|
+
|
|
1058
|
+
// Same bounds and pane geometry as draw_chart, so the badges hit where they
|
|
1059
|
+
// render.
|
|
1060
|
+
const auto lay = chart->layout();
|
|
1061
|
+
const auto range = vroom::visible_indices(
|
|
1062
|
+
chart->candles.data(), chart->candles.size(),
|
|
1063
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
1064
|
+
const size_t n = range.end - range.start;
|
|
1065
|
+
const auto bounds =
|
|
1066
|
+
chart->price_bounds_manual
|
|
1067
|
+
? chart->price_bounds
|
|
1068
|
+
: vroom::auto_price_bounds(chart->candles.data() + range.start, n);
|
|
1069
|
+
|
|
1070
|
+
const auto hit = vroom::footprints::hit_test(*chart, lay, bounds, x_px, y_px);
|
|
1071
|
+
if (hit.side < 0) return false;
|
|
1072
|
+
if (out) *out = hit;
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
extern "C" int32_t vroom_chart_footprints_at(VroomChart* chart,
|
|
1077
|
+
int64_t candle_time_ms,
|
|
1078
|
+
int32_t* out_indices, int32_t max) {
|
|
1079
|
+
if (!chart || chart->footprints.empty()) return 0;
|
|
1080
|
+
chart->ensure_footprint_buckets();
|
|
1081
|
+
|
|
1082
|
+
const auto& buckets = chart->footprint_buckets;
|
|
1083
|
+
const auto it = std::lower_bound(
|
|
1084
|
+
buckets.begin(), buckets.end(), candle_time_ms,
|
|
1085
|
+
[](const vroom::footprints::Bucket& b, int64_t t) {
|
|
1086
|
+
return b.candle_time_ms < t;
|
|
1087
|
+
});
|
|
1088
|
+
if (it == buckets.end() || it->candle_time_ms != candle_time_ms) return 0;
|
|
1089
|
+
|
|
1090
|
+
// Both sides, ascending by time — merge the two already-sorted lists rather
|
|
1091
|
+
// than re-sorting them.
|
|
1092
|
+
const auto& buys = it->buys;
|
|
1093
|
+
const auto& sells = it->sells;
|
|
1094
|
+
const int32_t total = static_cast<int32_t>(buys.size() + sells.size());
|
|
1095
|
+
if (!out_indices || max <= 0) return total;
|
|
1096
|
+
|
|
1097
|
+
const auto& fps = chart->footprints;
|
|
1098
|
+
int32_t written = 0;
|
|
1099
|
+
size_t bi = 0;
|
|
1100
|
+
size_t si = 0;
|
|
1101
|
+
while (written < max && (bi < buys.size() || si < sells.size())) {
|
|
1102
|
+
bool take_buy;
|
|
1103
|
+
if (bi >= buys.size()) {
|
|
1104
|
+
take_buy = false;
|
|
1105
|
+
} else if (si >= sells.size()) {
|
|
1106
|
+
take_buy = true;
|
|
1107
|
+
} else {
|
|
1108
|
+
const int64_t tb = fps[static_cast<size_t>(buys[bi])].time_ms;
|
|
1109
|
+
const int64_t ts = fps[static_cast<size_t>(sells[si])].time_ms;
|
|
1110
|
+
take_buy = tb <= ts;
|
|
1111
|
+
}
|
|
1112
|
+
out_indices[written++] = take_buy ? buys[bi++] : sells[si++];
|
|
1113
|
+
}
|
|
1114
|
+
return total;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
extern "C" void vroom_chart_set_footprint_hover(VroomChart* chart,
|
|
1118
|
+
int64_t candle_time_ms,
|
|
1119
|
+
int32_t side) {
|
|
1120
|
+
if (!chart) return;
|
|
1121
|
+
if (side != VROOM_FOOTPRINT_BUY && side != VROOM_FOOTPRINT_SELL) {
|
|
1122
|
+
side = -1;
|
|
1123
|
+
candle_time_ms = 0;
|
|
1124
|
+
}
|
|
1125
|
+
if (chart->hovered_footprint_side == side &&
|
|
1126
|
+
chart->hovered_footprint_time_ms == candle_time_ms) {
|
|
1127
|
+
return; // hover fires on every pointer move; don't redraw for nothing
|
|
1128
|
+
}
|
|
1129
|
+
chart->hovered_footprint_side = side;
|
|
1130
|
+
chart->hovered_footprint_time_ms = candle_time_ms;
|
|
1131
|
+
chart->mark_dirty();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1026
1134
|
extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
|
|
1027
1135
|
double a_price, bool has_b, int64_t b_time,
|
|
1028
1136
|
double b_price, bool guide, uint32_t color,
|
|
@@ -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
|