react-native-vroom-chart 0.6.0 → 0.8.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 +281 -33
- package/cpp/_core_include/vroom/vroom_chart.h +231 -36
- package/cpp/_core_src/bollinger.h +1 -1
- package/cpp/_core_src/candles.cpp +138 -41
- package/cpp/_core_src/candles.h +11 -1
- package/cpp/_core_src/chart.cpp +91 -40
- package/cpp/_core_src/chart.h +69 -29
- package/cpp/_core_src/chart_facade.cpp +247 -69
- package/cpp/_core_src/drawings.cpp +147 -4
- package/cpp/_core_src/drawings.h +10 -5
- package/cpp/_core_src/gradient.cpp +46 -0
- package/cpp/_core_src/gradient.h +31 -0
- package/cpp/_core_src/labels.cpp +49 -16
- package/cpp/_core_src/labels.h +33 -4
- package/cpp/_core_src/liquidity.cpp +3 -37
- package/cpp/_core_src/ma_overlay.cpp +174 -12
- package/cpp/_core_src/ma_overlay.h +55 -3
- package/cpp/_core_src/macd.cpp +13 -42
- package/cpp/_core_src/macd.h +11 -8
- package/cpp/_core_src/macd_pane.cpp +57 -27
- package/cpp/_core_src/price_line_layout.h +1 -1
- package/cpp/_core_src/rsi.cpp +4 -18
- package/cpp/_core_src/rsi.h +6 -6
- package/cpp/_core_src/rsi_pane.cpp +34 -19
- package/cpp/_core_src/series_ma.cpp +64 -0
- package/cpp/_core_src/series_ma.h +30 -0
- package/cpp/_core_src/style_inherit.h +35 -0
- package/cpp/_core_src/theme.cpp +2 -1
- package/cpp/_core_src/viewport.cpp +36 -12
- package/cpp/_core_src/viewport.h +50 -0
- package/cpp/_core_src/volume.cpp +33 -8
- package/cpp/_core_src/volume.h +12 -1
- package/cpp/_core_src/volume_anim.cpp +32 -0
- package/cpp/_core_src/volume_anim.h +41 -0
- package/lib/index.d.mts +206 -31
- package/lib/index.d.ts +206 -31
- package/lib/index.js +324 -44
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +329 -52
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +100 -17
- package/src/dataTransitions.ts +148 -0
- package/src/easing.ts +40 -0
- package/src/index.ts +9 -0
- package/src/jsi.d.ts +126 -19
- package/src/theme.ts +1 -0
- package/src/types.ts +3 -0
- package/src/useChartCore.ts +273 -30
|
@@ -63,6 +63,13 @@ int64_t damp_future_delta(int64_t delta_ms, int64_t cur_future,
|
|
|
63
63
|
void apply_default_framing(VroomChart* chart) {
|
|
64
64
|
if (chart->candles.empty()) return;
|
|
65
65
|
|
|
66
|
+
// A candle occupies the slot [time_ms, time_ms + duration) and draws centered
|
|
67
|
+
// in it, so the right edge of the view is the last slot's *end*. Pinning it to
|
|
68
|
+
// the last candle's time_ms instead puts that candle's center half a slot past
|
|
69
|
+
// the plot area, hiding the newest candle behind the y-axis strip.
|
|
70
|
+
const int64_t right_edge_ms =
|
|
71
|
+
chart->candles.back().time_ms + chart->candle_duration_ms;
|
|
72
|
+
|
|
66
73
|
// Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
|
|
67
74
|
// body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
|
|
68
75
|
// Needs a valid layout (width_px); until size is known it falls through to
|
|
@@ -81,7 +88,7 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
81
88
|
chart->candles.back().time_ms - chart->candles.front().time_ms;
|
|
82
89
|
window_ms = std::clamp<int64_t>(window_ms, chart->candle_duration_ms,
|
|
83
90
|
span > 0 ? span : window_ms);
|
|
84
|
-
chart->visible_end_ms =
|
|
91
|
+
chart->visible_end_ms = right_edge_ms;
|
|
85
92
|
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
86
93
|
return;
|
|
87
94
|
}
|
|
@@ -92,7 +99,7 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
92
99
|
? chart->candles.size() - kDefaultVisible
|
|
93
100
|
: 0;
|
|
94
101
|
chart->visible_start_ms = chart->candles[start_idx].time_ms;
|
|
95
|
-
chart->visible_end_ms =
|
|
102
|
+
chart->visible_end_ms = right_edge_ms;
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
// On the first manual-y gesture, adopt the on-screen auto-fit bounds so the
|
|
@@ -242,6 +249,10 @@ extern "C" void vroom_chart_reset_view(VroomChart* chart) {
|
|
|
242
249
|
chart->visible_start_ms = 0;
|
|
243
250
|
chart->visible_end_ms = 0;
|
|
244
251
|
chart->price_bounds_manual = false;
|
|
252
|
+
// An asset switch reframes wholesale, so a slot-paired morph is meaningless
|
|
253
|
+
// here — drop any capture rather than leaving it to reshape the wrong data.
|
|
254
|
+
chart->morph_from.clear();
|
|
255
|
+
chart->interval_morph_t = 1.f;
|
|
245
256
|
apply_default_framing(chart);
|
|
246
257
|
vroom::labels::recompute_axis_width(*chart);
|
|
247
258
|
if (chart->cb.on_viewport_changed) {
|
|
@@ -258,6 +269,104 @@ extern "C" void vroom_chart_reset_price_scale(VroomChart* chart) {
|
|
|
258
269
|
chart->mark_dirty();
|
|
259
270
|
}
|
|
260
271
|
|
|
272
|
+
extern "C" bool vroom_chart_get_visible_price_envelope(VroomChart* chart,
|
|
273
|
+
double* out_low,
|
|
274
|
+
double* out_high) {
|
|
275
|
+
if (!chart) return false;
|
|
276
|
+
const auto idx = vroom::visible_indices(
|
|
277
|
+
chart->candles.data(), chart->candles.size(),
|
|
278
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
279
|
+
if (idx.end <= idx.start) return false;
|
|
280
|
+
const auto env = vroom::price_bounds(chart->candles.data() + idx.start,
|
|
281
|
+
idx.end - idx.start);
|
|
282
|
+
if (out_low) *out_low = env.min;
|
|
283
|
+
if (out_high) *out_high = env.max;
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
extern "C" void vroom_chart_preserve_price_envelope(VroomChart* chart,
|
|
288
|
+
double prev_low,
|
|
289
|
+
double prev_high) {
|
|
290
|
+
// Auto mode already holds the envelope's pixel height constant (auto_price_
|
|
291
|
+
// bounds widens it by a fixed factor), so there is nothing to preserve.
|
|
292
|
+
if (!chart || !chart->price_bounds_manual) return;
|
|
293
|
+
|
|
294
|
+
double new_low = 0.0, new_high = 0.0;
|
|
295
|
+
const bool has_new =
|
|
296
|
+
vroom_chart_get_visible_price_envelope(chart, &new_low, &new_high);
|
|
297
|
+
if (!has_new || !(prev_high > prev_low) || !(new_high > new_low)) {
|
|
298
|
+
// Nothing to scale against — fall back to re-fitting the price scale.
|
|
299
|
+
vroom_chart_reset_price_scale(chart);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
chart->price_bounds = vroom::preserve_envelope_bounds(
|
|
304
|
+
chart->price_bounds, {prev_low, prev_high}, {new_low, new_high});
|
|
305
|
+
vroom::labels::recompute_axis_width(*chart);
|
|
306
|
+
chart->mark_dirty();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
extern "C" void vroom_chart_begin_interval_morph(VroomChart* chart) {
|
|
310
|
+
if (!chart) return;
|
|
311
|
+
chart->morph_from.clear();
|
|
312
|
+
chart->interval_morph_t = 1.f;
|
|
313
|
+
|
|
314
|
+
const auto lay = chart->layout();
|
|
315
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
316
|
+
const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
|
|
317
|
+
if (area_w <= 0.f || window_ms <= 0) return;
|
|
318
|
+
|
|
319
|
+
const auto idx = vroom::visible_indices(
|
|
320
|
+
chart->candles.data(), chart->candles.size(),
|
|
321
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
322
|
+
const std::size_t n = idx.end - idx.start;
|
|
323
|
+
if (n == 0) return;
|
|
324
|
+
const ::VroomCandle* visible = chart->candles.data() + idx.start;
|
|
325
|
+
|
|
326
|
+
const auto bounds = chart->price_bounds_manual
|
|
327
|
+
? chart->price_bounds
|
|
328
|
+
: vroom::auto_price_bounds(visible, n);
|
|
329
|
+
|
|
330
|
+
// Normalized so the capture is independent of both the price bounds (which
|
|
331
|
+
// the swap is about to replace) and the surface size (which may change
|
|
332
|
+
// mid-morph). Newest candle first, matching the slot indexing in
|
|
333
|
+
// candles::draw.
|
|
334
|
+
chart->morph_from.resize(n);
|
|
335
|
+
for (std::size_t k = 0; k < n; ++k) {
|
|
336
|
+
const auto& c = visible[n - 1 - k];
|
|
337
|
+
chart->morph_from[k] = vroom::CandleSnapshot{
|
|
338
|
+
vroom::candle_center_x(lay, c.time_ms, chart->candle_duration_ms,
|
|
339
|
+
chart->visible_start_ms, window_ms) / area_w,
|
|
340
|
+
static_cast<float>(vroom::price_fraction(bounds, c.open)),
|
|
341
|
+
static_cast<float>(vroom::price_fraction(bounds, c.high)),
|
|
342
|
+
static_cast<float>(vroom::price_fraction(bounds, c.low)),
|
|
343
|
+
static_cast<float>(vroom::price_fraction(bounds, c.close)),
|
|
344
|
+
c.close >= c.open,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The scale the capture was taken against — the axes render their outgoing
|
|
349
|
+
// ticks from it, and it's about to be replaced on the chart itself.
|
|
350
|
+
chart->morph_from_bounds = bounds;
|
|
351
|
+
chart->morph_from_start_ms = chart->visible_start_ms;
|
|
352
|
+
chart->morph_from_end_ms = chart->visible_end_ms;
|
|
353
|
+
|
|
354
|
+
// Open the morph at 0 rather than leaving it at 1: the caller still has to
|
|
355
|
+
// push the new candles, and any frame painted in between should show the
|
|
356
|
+
// captured geometry — which is what the pre-switch frame looked like.
|
|
357
|
+
chart->interval_morph_t = 0.f;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
extern "C" void vroom_chart_set_interval_morph(VroomChart* chart, float t) {
|
|
361
|
+
if (!chart) return;
|
|
362
|
+
chart->interval_morph_t = std::clamp(t, 0.f, 1.f);
|
|
363
|
+
if (chart->interval_morph_t >= 1.f) {
|
|
364
|
+
chart->morph_from.clear();
|
|
365
|
+
chart->morph_from.shrink_to_fit();
|
|
366
|
+
}
|
|
367
|
+
chart->mark_dirty();
|
|
368
|
+
}
|
|
369
|
+
|
|
261
370
|
extern "C" void vroom_chart_pan(VroomChart* chart, float dx_px, float /*dy_px*/) {
|
|
262
371
|
if (!chart || dx_px == 0.f || chart->candles.empty()) return;
|
|
263
372
|
|
|
@@ -481,7 +590,7 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
|
|
|
481
590
|
extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px) {
|
|
482
591
|
if (!chart || dy_px == 0.f) return;
|
|
483
592
|
|
|
484
|
-
const int pane_count = (chart->
|
|
593
|
+
const int pane_count = (chart->rsi.enabled ? 1 : 0) + (chart->macd.enabled ? 1 : 0);
|
|
485
594
|
if (pane_count == 0) return; // nothing below the chart to resize
|
|
486
595
|
|
|
487
596
|
const auto lay = chart->layout();
|
|
@@ -535,8 +644,8 @@ extern "C" void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px,
|
|
|
535
644
|
struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
|
|
536
645
|
ActivePane panes[2];
|
|
537
646
|
int count = 0;
|
|
538
|
-
if (chart->
|
|
539
|
-
if (chart->
|
|
647
|
+
if (chart->rsi.enabled) panes[count++] = {chart->rsi_order, 0};
|
|
648
|
+
if (chart->macd.enabled) panes[count++] = {chart->macd_order, 1};
|
|
540
649
|
if (count == 0) return;
|
|
541
650
|
if (count == 2 && panes[0].order > panes[1].order) {
|
|
542
651
|
const ActivePane tmp = panes[0];
|
|
@@ -774,8 +883,9 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
|
|
|
774
883
|
const VroomDrawing* drawings,
|
|
775
884
|
size_t count) {
|
|
776
885
|
if (!chart) return;
|
|
777
|
-
// Deep-copy into the owning form: a pencil's path
|
|
778
|
-
// caller's `points` buffer may be freed as soon as this
|
|
886
|
+
// Deep-copy into the owning form: a pencil stroke's or path's points live in
|
|
887
|
+
// the chart, so the caller's `points` buffer may be freed as soon as this
|
|
888
|
+
// returns.
|
|
779
889
|
chart->drawings.clear();
|
|
780
890
|
chart->drawings.reserve(count);
|
|
781
891
|
for (size_t i = 0; i < count; ++i) {
|
|
@@ -786,7 +896,7 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
|
|
|
786
896
|
d.color = src.color;
|
|
787
897
|
d.width = src.width;
|
|
788
898
|
d.kind = src.kind;
|
|
789
|
-
if (src.kind == 2 && src.points && src.point_count > 0) {
|
|
899
|
+
if ((src.kind == 2 || src.kind == 3) && src.points && src.point_count > 0) {
|
|
790
900
|
d.points.assign(src.points, src.points + src.point_count);
|
|
791
901
|
// Keep a/b mirroring the path ends so bounds/handle code is uniform.
|
|
792
902
|
d.a = d.points.front();
|
|
@@ -916,6 +1026,27 @@ extern "C" void vroom_chart_append_draft_point(VroomChart* chart, int64_t time_m
|
|
|
916
1026
|
chart->mark_dirty();
|
|
917
1027
|
}
|
|
918
1028
|
|
|
1029
|
+
extern "C" void vroom_chart_set_draft_path(VroomChart* chart,
|
|
1030
|
+
const VroomDrawPoint* points,
|
|
1031
|
+
int32_t count, bool has_cursor,
|
|
1032
|
+
int64_t cursor_time,
|
|
1033
|
+
double cursor_price, uint32_t color,
|
|
1034
|
+
float width) {
|
|
1035
|
+
if (!chart) return;
|
|
1036
|
+
chart->draft_active = true;
|
|
1037
|
+
chart->draft_kind = 3;
|
|
1038
|
+
chart->draft_guide = true;
|
|
1039
|
+
chart->draft_color = color;
|
|
1040
|
+
chart->draft_width = width;
|
|
1041
|
+
chart->draft_points.clear();
|
|
1042
|
+
if (points && count > 0) chart->draft_points.assign(points, points + count);
|
|
1043
|
+
chart->draft_has_b = has_cursor;
|
|
1044
|
+
chart->draft_b = VroomDrawPoint{cursor_time, cursor_price};
|
|
1045
|
+
// draft_a doubles as the first placed vertex for the shared node-dot path.
|
|
1046
|
+
if (!chart->draft_points.empty()) chart->draft_a = chart->draft_points.front();
|
|
1047
|
+
chart->mark_dirty();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
919
1050
|
extern "C" void vroom_chart_clear_draft(VroomChart* chart) {
|
|
920
1051
|
if (!chart) return;
|
|
921
1052
|
if (!chart->draft_active) return;
|
|
@@ -1042,6 +1173,21 @@ extern "C" void vroom_chart_move_drawing_endpoint(VroomChart* chart, int32_t ind
|
|
|
1042
1173
|
chart->mark_dirty();
|
|
1043
1174
|
}
|
|
1044
1175
|
|
|
1176
|
+
extern "C" void vroom_chart_move_drawing_vertex(VroomChart* chart, int32_t index,
|
|
1177
|
+
int32_t vertex, int64_t time_ms,
|
|
1178
|
+
double price) {
|
|
1179
|
+
if (!chart) return;
|
|
1180
|
+
if (index < 0 || index >= static_cast<int32_t>(chart->drawings.size())) return;
|
|
1181
|
+
auto& d = chart->drawings[index];
|
|
1182
|
+
if (d.kind != 3) return;
|
|
1183
|
+
if (vertex < 0 || vertex >= static_cast<int32_t>(d.points.size())) return;
|
|
1184
|
+
d.points[vertex] = VroomDrawPoint{time_ms, price};
|
|
1185
|
+
// a/b mirror the path ends, so moving a terminal vertex has to restate them.
|
|
1186
|
+
d.a = d.points.front();
|
|
1187
|
+
d.b = d.points.back();
|
|
1188
|
+
chart->mark_dirty();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1045
1191
|
extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
|
|
1046
1192
|
int64_t d_time_ms, double d_price) {
|
|
1047
1193
|
if (!chart) return;
|
|
@@ -1051,6 +1197,7 @@ extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
|
|
|
1051
1197
|
p.time_ms += d_time_ms;
|
|
1052
1198
|
p.price += d_price;
|
|
1053
1199
|
};
|
|
1200
|
+
// a/b mirror the path/stroke ends, so shifting everything keeps them in sync.
|
|
1054
1201
|
shift(d.a);
|
|
1055
1202
|
shift(d.b);
|
|
1056
1203
|
for (VroomDrawPoint& p : d.points) shift(p);
|
|
@@ -1059,61 +1206,66 @@ extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
|
|
|
1059
1206
|
|
|
1060
1207
|
// ---- Indicators -----------------------------------------------------------
|
|
1061
1208
|
|
|
1062
|
-
extern "C" void vroom_chart_set_rsi(VroomChart* chart,
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1209
|
+
extern "C" void vroom_chart_set_rsi(VroomChart* chart, const VroomRSI* cfg) {
|
|
1210
|
+
if (!chart || !cfg) return;
|
|
1211
|
+
VroomRSI next = *cfg;
|
|
1212
|
+
next.enabled = next.enabled ? 1 : 0;
|
|
1213
|
+
next.ma_visible = next.ma_visible ? 1 : 0;
|
|
1214
|
+
next.line_visible = next.line_visible ? 1 : 0;
|
|
1215
|
+
next.bands_visible = next.bands_visible ? 1 : 0;
|
|
1216
|
+
if (next.period < 2) next.period = 2;
|
|
1217
|
+
if (next.ma_period < 1) next.ma_period = 1;
|
|
1218
|
+
next.upper_band = std::clamp(next.upper_band, 0.0, 100.0);
|
|
1219
|
+
next.lower_band = std::clamp(next.lower_band, 0.0, 100.0);
|
|
1220
|
+
|
|
1221
|
+
// Only the series-affecting fields force a recompute; band levels, color,
|
|
1222
|
+
// width, and visibility changes are render-only. The trendline is one of
|
|
1223
|
+
// them: it isn't computed while hidden, so revealing it needs a recompute.
|
|
1224
|
+
const VroomRSI& cur = chart->rsi;
|
|
1225
|
+
const bool recompute = cur.enabled != next.enabled ||
|
|
1226
|
+
cur.period != next.period ||
|
|
1227
|
+
cur.ma_visible != next.ma_visible ||
|
|
1228
|
+
cur.ma_period != next.ma_period ||
|
|
1229
|
+
cur.ma_kind != next.ma_kind;
|
|
1080
1230
|
|
|
1081
1231
|
// Pane order: claim the next slot on an off->on transition, release on off.
|
|
1082
|
-
if (enabled && !
|
|
1083
|
-
else if (!enabled) chart->rsi_order = -1;
|
|
1084
|
-
|
|
1085
|
-
chart->
|
|
1086
|
-
chart->rsi_period = period;
|
|
1087
|
-
chart->rsi_upper = upper_band;
|
|
1088
|
-
chart->rsi_lower = lower_band;
|
|
1089
|
-
chart->rsi_ma_enabled = ma_enabled;
|
|
1090
|
-
chart->rsi_ma_period = ma_period;
|
|
1232
|
+
if (next.enabled && !cur.enabled) chart->rsi_order = chart->pane_seq++;
|
|
1233
|
+
else if (!next.enabled) chart->rsi_order = -1;
|
|
1234
|
+
|
|
1235
|
+
chart->rsi = next;
|
|
1091
1236
|
if (recompute) chart->rsi_dirty = true;
|
|
1092
1237
|
chart->mark_dirty();
|
|
1093
1238
|
}
|
|
1094
1239
|
|
|
1095
|
-
extern "C" void vroom_chart_set_macd(VroomChart* chart,
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1240
|
+
extern "C" void vroom_chart_set_macd(VroomChart* chart, const VroomMACD* cfg) {
|
|
1241
|
+
if (!chart || !cfg) return;
|
|
1242
|
+
VroomMACD next = *cfg;
|
|
1243
|
+
next.enabled = next.enabled ? 1 : 0;
|
|
1244
|
+
next.line_visible = next.line_visible ? 1 : 0;
|
|
1245
|
+
next.signal_visible = next.signal_visible ? 1 : 0;
|
|
1246
|
+
next.hist_visible = next.hist_visible ? 1 : 0;
|
|
1247
|
+
next.zero_visible = next.zero_visible ? 1 : 0;
|
|
1248
|
+
if (next.fast < 1) next.fast = 1;
|
|
1249
|
+
if (next.slow < 1) next.slow = 1;
|
|
1250
|
+
if (next.slow <= next.fast) next.slow = next.fast + 1;
|
|
1251
|
+
if (next.signal < 1) next.signal = 1;
|
|
1252
|
+
|
|
1253
|
+
// Only the series-affecting fields force a recompute; color, width, and
|
|
1254
|
+
// visibility changes are render-only.
|
|
1255
|
+
const VroomMACD& cur = chart->macd;
|
|
1256
|
+
const bool recompute = cur.enabled != next.enabled ||
|
|
1257
|
+
cur.fast != next.fast ||
|
|
1258
|
+
cur.slow != next.slow ||
|
|
1259
|
+
cur.signal != next.signal ||
|
|
1260
|
+
cur.source != next.source ||
|
|
1261
|
+
cur.ma_kind != next.ma_kind ||
|
|
1262
|
+
cur.signal_ma_kind != next.signal_ma_kind;
|
|
1263
|
+
|
|
1264
|
+
if (next.enabled && !cur.enabled) chart->macd_order = chart->pane_seq++;
|
|
1265
|
+
else if (!next.enabled) chart->macd_order = -1;
|
|
1266
|
+
|
|
1267
|
+
chart->macd = next;
|
|
1268
|
+
if (recompute) chart->macd_dirty = true;
|
|
1117
1269
|
chart->mark_dirty();
|
|
1118
1270
|
}
|
|
1119
1271
|
|
|
@@ -1126,19 +1278,21 @@ extern "C" void vroom_chart_set_overlays(VroomChart* chart,
|
|
|
1126
1278
|
chart->mark_dirty();
|
|
1127
1279
|
}
|
|
1128
1280
|
|
|
1129
|
-
extern "C" void vroom_chart_set_vwap(VroomChart* chart,
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1281
|
+
extern "C" void vroom_chart_set_vwap(VroomChart* chart, const VroomVWAP* cfg) {
|
|
1282
|
+
if (!chart || !cfg) return;
|
|
1283
|
+
VroomVWAP next = *cfg;
|
|
1284
|
+
next.enabled = next.enabled ? 1 : 0;
|
|
1133
1285
|
// Keep the offset within a day [0, 1440).
|
|
1134
|
-
|
|
1135
|
-
if (
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1286
|
+
next.reset_offset_min %= 1440;
|
|
1287
|
+
if (next.reset_offset_min < 0) next.reset_offset_min += 1440;
|
|
1288
|
+
|
|
1289
|
+
// Only the series-affecting fields force a recompute; color and width
|
|
1290
|
+
// changes are render-only.
|
|
1291
|
+
const VroomVWAP& cur = chart->vwap;
|
|
1292
|
+
const bool recompute = cur.enabled != next.enabled ||
|
|
1293
|
+
cur.reset_offset_min != next.reset_offset_min;
|
|
1294
|
+
|
|
1295
|
+
chart->vwap = next;
|
|
1142
1296
|
if (recompute) chart->vwap_dirty = true;
|
|
1143
1297
|
chart->mark_dirty();
|
|
1144
1298
|
}
|
|
@@ -1166,6 +1320,30 @@ extern "C" void vroom_chart_set_bollinger(VroomChart* chart,
|
|
|
1166
1320
|
chart->mark_dirty();
|
|
1167
1321
|
}
|
|
1168
1322
|
|
|
1323
|
+
extern "C" void vroom_chart_set_volume(VroomChart* chart,
|
|
1324
|
+
const VroomVolume* cfg) {
|
|
1325
|
+
if (!chart || !cfg) return;
|
|
1326
|
+
VroomVolume next = *cfg;
|
|
1327
|
+
next.enabled = next.enabled ? 1 : 0;
|
|
1328
|
+
// Negative means "inherit", so only clamp the ranges once a value is set.
|
|
1329
|
+
if (next.height_frac >= 0.f) next.height_frac = std::min(next.height_frac, 1.f);
|
|
1330
|
+
if (next.opacity >= 0.f) next.opacity = std::min(next.opacity, 1.f);
|
|
1331
|
+
chart->volume = next;
|
|
1332
|
+
// Snap the collapse to the new target, the way set_chart_type snaps the
|
|
1333
|
+
// candle↔line morph. A host that wants the toggle animated overrides this
|
|
1334
|
+
// from its frame loop before the next paint.
|
|
1335
|
+
chart->volume_collapse_t = next.enabled ? 0.f : 1.f;
|
|
1336
|
+
chart->mark_dirty();
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
extern "C" void vroom_chart_set_volume_collapse(VroomChart* chart, float t,
|
|
1340
|
+
int32_t easing) {
|
|
1341
|
+
if (!chart) return;
|
|
1342
|
+
chart->volume_collapse_t = std::clamp(t, 0.f, 1.f);
|
|
1343
|
+
chart->volume_collapse_easing = easing;
|
|
1344
|
+
chart->mark_dirty();
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1169
1347
|
// ---- Direct draw (used by hosts that don't need the SkPicture cache) ------
|
|
1170
1348
|
|
|
1171
1349
|
extern "C" void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas) {
|
|
@@ -115,6 +115,99 @@ void draw_path(SkCanvas* canvas, const VroomChart& chart, const Layout& lay,
|
|
|
115
115
|
canvas->drawPath(builder.detach(), paint);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// Strokes straight segments through `pts` (already in pixels). Unlike a pencil
|
|
119
|
+
// stroke a path's vertices were each placed deliberately, so the corners are the
|
|
120
|
+
// point — no smoothing. Round joins/caps keep the corners from spiking at the
|
|
121
|
+
// sharp angles a wave count tends to produce.
|
|
122
|
+
void draw_polyline(SkCanvas* canvas, const std::vector<SkPoint>& pts,
|
|
123
|
+
SkColor color, float width) {
|
|
124
|
+
if (pts.size() < 2) return;
|
|
125
|
+
SkPathBuilder builder;
|
|
126
|
+
builder.moveTo(pts[0]);
|
|
127
|
+
for (size_t i = 1; i < pts.size(); ++i) builder.lineTo(pts[i]);
|
|
128
|
+
|
|
129
|
+
SkPaint paint;
|
|
130
|
+
paint.setAntiAlias(true);
|
|
131
|
+
paint.setColor(color);
|
|
132
|
+
paint.setStyle(SkPaint::kStroke_Style);
|
|
133
|
+
paint.setStrokeWidth(width > 0.f ? width : 2.f);
|
|
134
|
+
paint.setStrokeCap(SkPaint::kRound_Cap);
|
|
135
|
+
paint.setStrokeJoin(SkPaint::kRound_Join);
|
|
136
|
+
canvas->drawPath(builder.detach(), paint);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Apex-to-base length of the arrowhead. Scales with the stroke so a thicker
|
|
140
|
+
// path gets a proportionally bigger head.
|
|
141
|
+
float arrow_head_len(float width) {
|
|
142
|
+
return 5.f + 2.5f * (width > 0.f ? width : 2.f);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Fills a small triangular arrowhead with its apex at `tip`, pointing away from
|
|
146
|
+
// `from`. Skipped when the segment is too short to seat one (otherwise the head
|
|
147
|
+
// would swallow the whole segment and flip direction on tiny jitters).
|
|
148
|
+
void draw_arrow_head(SkCanvas* canvas, SkPoint tip, SkPoint from, SkColor color,
|
|
149
|
+
float width) {
|
|
150
|
+
const float len = arrow_head_len(width);
|
|
151
|
+
const float half = len * 0.45f; // base half-width
|
|
152
|
+
const float dx = tip.fX - from.fX;
|
|
153
|
+
const float dy = tip.fY - from.fY;
|
|
154
|
+
const float dist = std::sqrt(dx * dx + dy * dy);
|
|
155
|
+
if (dist < len) return;
|
|
156
|
+
|
|
157
|
+
const float ux = dx / dist; // unit vector pointing at the tip
|
|
158
|
+
const float uy = dy / dist;
|
|
159
|
+
const SkPoint base{tip.fX - ux * len, tip.fY - uy * len};
|
|
160
|
+
|
|
161
|
+
SkPathBuilder head;
|
|
162
|
+
head.moveTo(tip);
|
|
163
|
+
head.lineTo(base.fX - uy * half, base.fY + ux * half); // base, one side
|
|
164
|
+
head.lineTo(base.fX + uy * half, base.fY - ux * half); // base, the other
|
|
165
|
+
head.close();
|
|
166
|
+
|
|
167
|
+
SkPaint paint;
|
|
168
|
+
paint.setAntiAlias(true);
|
|
169
|
+
paint.setColor(color);
|
|
170
|
+
paint.setStyle(SkPaint::kFill_Style);
|
|
171
|
+
canvas->drawPath(head.detach(), paint);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Projects every point of a path/stroke to pixels in one pass, so the render and
|
|
175
|
+
// arrowhead code can index them without re-projecting.
|
|
176
|
+
std::vector<SkPoint> project_all(const VroomChart& chart, const Layout& lay,
|
|
177
|
+
const PriceBounds& bounds, int64_t window_ms,
|
|
178
|
+
const std::vector<VroomDrawPoint>& pts) {
|
|
179
|
+
std::vector<SkPoint> out;
|
|
180
|
+
out.reserve(pts.size());
|
|
181
|
+
for (const auto& p : pts) out.push_back(to_px(chart, lay, bounds, window_ms, p));
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Draws a whole arrow-tipped path from its already-projected points: the
|
|
186
|
+
// segments, then the head on the last one.
|
|
187
|
+
void draw_arrow_path(SkCanvas* canvas, const std::vector<SkPoint>& pts,
|
|
188
|
+
SkColor color, float width) {
|
|
189
|
+
if (pts.size() < 2) return;
|
|
190
|
+
const SkPoint tip = pts.back();
|
|
191
|
+
const SkPoint prev = pts[pts.size() - 2];
|
|
192
|
+
const float len = arrow_head_len(width);
|
|
193
|
+
const float dx = tip.fX - prev.fX;
|
|
194
|
+
const float dy = tip.fY - prev.fY;
|
|
195
|
+
const float dist = std::sqrt(dx * dx + dy * dy);
|
|
196
|
+
if (dist < len) { // no room for a head — just the bare segments
|
|
197
|
+
draw_polyline(canvas, pts, color, width);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Stop the stroke at the head's base rather than running it to the apex:
|
|
202
|
+
// its round cap is a half-disc of radius width/2 centred on the endpoint, so
|
|
203
|
+
// at the apex it bulges past the point and blunts it. The base is wider than
|
|
204
|
+
// the stroke, so the cap ends up hidden inside the triangle.
|
|
205
|
+
std::vector<SkPoint> body(pts);
|
|
206
|
+
body.back() = SkPoint{tip.fX - dx / dist * len, tip.fY - dy / dist * len};
|
|
207
|
+
draw_polyline(canvas, body, color, width);
|
|
208
|
+
draw_arrow_head(canvas, tip, prev, color, width);
|
|
209
|
+
}
|
|
210
|
+
|
|
118
211
|
void draw_node(SkCanvas* canvas, SkPoint pt, float scale = 1.f) {
|
|
119
212
|
SkPaint fill;
|
|
120
213
|
fill.setAntiAlias(true);
|
|
@@ -232,6 +325,12 @@ void draw(SkCanvas* canvas,
|
|
|
232
325
|
static_cast<SkColor>(d.color), d.width);
|
|
233
326
|
continue;
|
|
234
327
|
}
|
|
328
|
+
if (d.kind == 3) {
|
|
329
|
+
draw_arrow_path(
|
|
330
|
+
canvas, project_all(chart, lay, bounds, window_ms, d.points),
|
|
331
|
+
static_cast<SkColor>(d.color), d.width);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
235
334
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
236
335
|
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
237
336
|
if (d.kind == 1) {
|
|
@@ -255,7 +354,16 @@ void draw(SkCanvas* canvas,
|
|
|
255
354
|
const auto& d = chart.drawings[chart.selected_drawing];
|
|
256
355
|
const SkPoint sa = to_px(chart, lay, bounds, window_ms, d.a);
|
|
257
356
|
const SkPoint sb = to_px(chart, lay, bounds, window_ms, d.b);
|
|
258
|
-
if (d.kind ==
|
|
357
|
+
if (d.kind == 3) {
|
|
358
|
+
// Path: every vertex was placed by hand, so every one is a grab
|
|
359
|
+
// handle. The one being dragged renders 50% larger, like a line's.
|
|
360
|
+
const auto pts = project_all(chart, lay, bounds, window_ms, d.points);
|
|
361
|
+
for (size_t i = 0; i < pts.size(); ++i) {
|
|
362
|
+
draw_node(canvas, pts[i],
|
|
363
|
+
chart.grabbed_endpoint == static_cast<int32_t>(i) ? 1.5f
|
|
364
|
+
: 1.f);
|
|
365
|
+
}
|
|
366
|
+
} else if (d.kind == 2) {
|
|
259
367
|
// Pencil: anchors on the first/last point are a visual cue that the
|
|
260
368
|
// stroke is movable — they aren't grab handles, so never enlarged.
|
|
261
369
|
draw_node(canvas, sa);
|
|
@@ -289,6 +397,28 @@ void draw(SkCanvas* canvas,
|
|
|
289
397
|
return;
|
|
290
398
|
}
|
|
291
399
|
|
|
400
|
+
// 1d. Path in progress: the vertices placed so far, plus a rubber-band
|
|
401
|
+
// segment to the cursor. The arrow rides the leading edge — on the
|
|
402
|
+
// cursor while drawing, and (once committed) on the final vertex — so
|
|
403
|
+
// the preview reads as the arrow it's about to become.
|
|
404
|
+
if (chart.draft_kind == 3) {
|
|
405
|
+
if (chart.draft_points.empty()) return;
|
|
406
|
+
auto pts = project_all(chart, lay, bounds, window_ms, chart.draft_points);
|
|
407
|
+
if (chart.draft_has_b) {
|
|
408
|
+
pts.push_back(to_px(chart, lay, bounds, window_ms, chart.draft_b));
|
|
409
|
+
}
|
|
410
|
+
canvas->save();
|
|
411
|
+
canvas->clipRect(clip);
|
|
412
|
+
draw_arrow_path(canvas, pts, static_cast<SkColor>(chart.draft_color),
|
|
413
|
+
chart.draft_width);
|
|
414
|
+
canvas->restore();
|
|
415
|
+
// Node dots on the placed vertices only (not the cursor) — unclipped, so
|
|
416
|
+
// a vertex on the very edge still renders whole.
|
|
417
|
+
const size_t placed = chart.draft_points.size();
|
|
418
|
+
for (size_t i = 0; i < placed; ++i) draw_node(canvas, pts[i]);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
292
422
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, chart.draft_a);
|
|
293
423
|
const bool has_b = chart.draft_has_b;
|
|
294
424
|
const SkPoint b =
|
|
@@ -339,7 +469,18 @@ HitResult hit_test(const VroomChart& chart,
|
|
|
339
469
|
const auto& d = chart.drawings[chart.selected_drawing];
|
|
340
470
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
341
471
|
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
342
|
-
if (d.kind ==
|
|
472
|
+
if (d.kind == 3) {
|
|
473
|
+
// Path: every vertex is a handle. Later vertices win an overlap, so
|
|
474
|
+
// stacked points stay individually reachable by peeling them off.
|
|
475
|
+
for (size_t i = d.points.size(); i-- > 0;) {
|
|
476
|
+
const SkPoint p = to_px(chart, lay, bounds, window_ms, d.points[i]);
|
|
477
|
+
if (std::hypot(x - p.fX, y - p.fY) <= kHandleHit) {
|
|
478
|
+
return HitResult{chart.selected_drawing,
|
|
479
|
+
VROOM_DRAW_PART_VERTEX + static_cast<int32_t>(i),
|
|
480
|
+
0.f};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} else if (d.kind == 2) {
|
|
343
484
|
// Pencil has no grab handles — its anchors translate like any other
|
|
344
485
|
// part of the stroke, so fall through to the body pass.
|
|
345
486
|
} else if (d.kind == 1) {
|
|
@@ -367,13 +508,15 @@ HitResult hit_test(const VroomChart& chart,
|
|
|
367
508
|
float best_t = 0.f;
|
|
368
509
|
for (size_t i = 0; i < chart.drawings.size(); ++i) {
|
|
369
510
|
const auto& d = chart.drawings[i];
|
|
370
|
-
|
|
511
|
+
// Pencil and path share the polyline distance — a path's straight
|
|
512
|
+
// segments are exactly what dist_to_path already measures against.
|
|
513
|
+
if (d.kind == 2 || d.kind == 3) {
|
|
371
514
|
const float dist = dist_to_path(x, y, chart, lay, bounds, window_ms,
|
|
372
515
|
d.points, kBodyHit);
|
|
373
516
|
if (dist <= best) { // <= so later (topmost) drawings win ties
|
|
374
517
|
best = dist;
|
|
375
518
|
best_i = static_cast<int32_t>(i);
|
|
376
|
-
best_part = 5;
|
|
519
|
+
best_part = d.kind == 3 ? 6 : 5;
|
|
377
520
|
best_t = 0.f;
|
|
378
521
|
}
|
|
379
522
|
continue;
|
package/cpp/_core_src/drawings.h
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Drawing annotations — user-placed line tools rendered on the price pane.
|
|
2
2
|
//
|
|
3
3
|
// Two kinds of geometry are drawn here:
|
|
4
|
-
// * Committed
|
|
5
|
-
//
|
|
4
|
+
// * Committed shapes (chart.drawings) — lines, boxes, freehand strokes and
|
|
5
|
+
// arrow-tipped paths, anchored in data space so they stay glued to the
|
|
6
|
+
// candles as the user pans/zooms.
|
|
6
7
|
// * The transient "draft" (chart.draft_*) — the in-progress node dots and the
|
|
7
|
-
// live guideline shown while the user is placing a
|
|
8
|
+
// live guideline shown while the user is placing a shape.
|
|
8
9
|
//
|
|
9
10
|
// Like ma_overlay / crosshair this is its own module so the chart orchestrator
|
|
10
11
|
// stays thin. Drawn after the overlays and before the axis backgrounds.
|
|
@@ -39,8 +40,12 @@ void draw(SkCanvas* canvas,
|
|
|
39
40
|
// * pencil — always 5 (stroke body). A freehand stroke has no grab handles:
|
|
40
41
|
// its end anchors are a visual cue only and translate the whole
|
|
41
42
|
// path like any other part of it.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
43
|
+
// * path — 6 (path body) or VROOM_DRAW_PART_VERTEX + i for vertex i. Unlike
|
|
44
|
+
// a pencil every vertex was placed deliberately, so every one is a
|
|
45
|
+
// grab handle; the offset keeps those indices clear of the small
|
|
46
|
+
// part numbers above.
|
|
47
|
+
// Corner/endpoint/vertex hits are only reported for the currently selected
|
|
48
|
+
// drawing (whose handles are visible).
|
|
44
49
|
struct HitResult {
|
|
45
50
|
int32_t index;
|
|
46
51
|
int32_t part;
|