react-native-vroom-chart 0.7.0 → 0.9.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 +131 -1
- package/cpp/_core_include/vroom/vroom_chart.h +54 -14
- package/cpp/_core_src/chart.cpp +36 -2
- package/cpp/_core_src/chart.h +29 -12
- package/cpp/_core_src/chart_facade.cpp +72 -10
- package/cpp/_core_src/curve.h +67 -0
- package/cpp/_core_src/drawings.cpp +147 -4
- package/cpp/_core_src/drawings.h +10 -5
- package/cpp/_core_src/ma_overlay.cpp +200 -34
- package/cpp/_core_src/ma_overlay.h +41 -4
- package/cpp/_core_src/theme.cpp +3 -0
- package/cpp/_core_src/tip_pulse.h +74 -0
- package/lib/index.d.mts +73 -4
- package/lib/index.d.ts +73 -4
- package/lib/index.js +211 -46
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +216 -54
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/VroomChart.tsx +46 -23
- package/src/dataTransitions.ts +148 -0
- package/src/index.ts +6 -0
- package/src/jsi.d.ts +51 -0
- package/src/theme.ts +7 -0
- package/src/useChartCore.ts +179 -5
|
@@ -29,7 +29,7 @@ ChartHostObject::~ChartHostObject() {
|
|
|
29
29
|
std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
30
30
|
jsi::Runtime& rt) {
|
|
31
31
|
std::vector<jsi::PropNameID> out;
|
|
32
|
-
out.reserve(
|
|
32
|
+
out.reserve(39);
|
|
33
33
|
out.push_back(jsi::PropNameID::forAscii(rt, "setCandles"));
|
|
34
34
|
out.push_back(jsi::PropNameID::forAscii(rt, "setSize"));
|
|
35
35
|
out.push_back(jsi::PropNameID::forAscii(rt, "setColor"));
|
|
@@ -38,6 +38,13 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
38
38
|
out.push_back(jsi::PropNameID::forAscii(rt, "setDefaultCandleWidth"));
|
|
39
39
|
out.push_back(jsi::PropNameID::forAscii(rt, "setChartType"));
|
|
40
40
|
out.push_back(jsi::PropNameID::forAscii(rt, "setMorph"));
|
|
41
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "getVisibleRange"));
|
|
42
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "resetView"));
|
|
43
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "resetPriceScale"));
|
|
44
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "getVisiblePriceEnvelope"));
|
|
45
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "preservePriceEnvelope"));
|
|
46
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "beginIntervalMorph"));
|
|
47
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setIntervalMorph"));
|
|
41
48
|
out.push_back(jsi::PropNameID::forAscii(rt, "pan"));
|
|
42
49
|
out.push_back(jsi::PropNameID::forAscii(rt, "translate"));
|
|
43
50
|
out.push_back(jsi::PropNameID::forAscii(rt, "zoom"));
|
|
@@ -297,6 +304,129 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
297
304
|
});
|
|
298
305
|
}
|
|
299
306
|
|
|
307
|
+
if (name == "getVisibleRange") {
|
|
308
|
+
// getVisibleRange() -> { startMs, endMs }. Both 0 while uninitialized.
|
|
309
|
+
return jsi::Function::createFromHostFunction(
|
|
310
|
+
rt,
|
|
311
|
+
jsi::PropNameID::forAscii(rt, "getVisibleRange"),
|
|
312
|
+
0,
|
|
313
|
+
[this](jsi::Runtime& rt2,
|
|
314
|
+
const jsi::Value& /*thisVal*/,
|
|
315
|
+
const jsi::Value* /*args*/,
|
|
316
|
+
size_t /*count*/) -> jsi::Value {
|
|
317
|
+
int64_t s = 0, e = 0;
|
|
318
|
+
vroom_chart_get_visible_range(chart_, &s, &e);
|
|
319
|
+
jsi::Object obj(rt2);
|
|
320
|
+
obj.setProperty(rt2, "startMs", static_cast<double>(s));
|
|
321
|
+
obj.setProperty(rt2, "endMs", static_cast<double>(e));
|
|
322
|
+
return obj;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (name == "resetView") {
|
|
327
|
+
// resetView() — reframe the most recent candles and re-enable y auto-fit.
|
|
328
|
+
return jsi::Function::createFromHostFunction(
|
|
329
|
+
rt,
|
|
330
|
+
jsi::PropNameID::forAscii(rt, "resetView"),
|
|
331
|
+
0,
|
|
332
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
333
|
+
const jsi::Value& /*thisVal*/,
|
|
334
|
+
const jsi::Value* /*args*/,
|
|
335
|
+
size_t /*count*/) -> jsi::Value {
|
|
336
|
+
vroom_chart_reset_view(chart_);
|
|
337
|
+
return jsi::Value::undefined();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (name == "resetPriceScale") {
|
|
342
|
+
// resetPriceScale() — re-enable y auto-fit, leaving the time window alone.
|
|
343
|
+
return jsi::Function::createFromHostFunction(
|
|
344
|
+
rt,
|
|
345
|
+
jsi::PropNameID::forAscii(rt, "resetPriceScale"),
|
|
346
|
+
0,
|
|
347
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
348
|
+
const jsi::Value& /*thisVal*/,
|
|
349
|
+
const jsi::Value* /*args*/,
|
|
350
|
+
size_t /*count*/) -> jsi::Value {
|
|
351
|
+
vroom_chart_reset_price_scale(chart_);
|
|
352
|
+
return jsi::Value::undefined();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (name == "getVisiblePriceEnvelope") {
|
|
357
|
+
// getVisiblePriceEnvelope() -> { low, high } | null. The extent the visible
|
|
358
|
+
// candles occupy, not the (wider) axis range. No rendering.
|
|
359
|
+
return jsi::Function::createFromHostFunction(
|
|
360
|
+
rt,
|
|
361
|
+
jsi::PropNameID::forAscii(rt, "getVisiblePriceEnvelope"),
|
|
362
|
+
0,
|
|
363
|
+
[this](jsi::Runtime& rt2,
|
|
364
|
+
const jsi::Value& /*thisVal*/,
|
|
365
|
+
const jsi::Value* /*args*/,
|
|
366
|
+
size_t /*count*/) -> jsi::Value {
|
|
367
|
+
double low = 0.0, high = 0.0;
|
|
368
|
+
if (!vroom_chart_get_visible_price_envelope(chart_, &low, &high)) {
|
|
369
|
+
return jsi::Value::null();
|
|
370
|
+
}
|
|
371
|
+
jsi::Object obj(rt2);
|
|
372
|
+
obj.setProperty(rt2, "low", low);
|
|
373
|
+
obj.setProperty(rt2, "high", high);
|
|
374
|
+
return obj;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (name == "preservePriceEnvelope") {
|
|
379
|
+
// preservePriceEnvelope(prevLow, prevHigh) — scale-lock a manual price
|
|
380
|
+
// range across a timeframe switch. No-op in auto-y mode.
|
|
381
|
+
return jsi::Function::createFromHostFunction(
|
|
382
|
+
rt,
|
|
383
|
+
jsi::PropNameID::forAscii(rt, "preservePriceEnvelope"),
|
|
384
|
+
2,
|
|
385
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
386
|
+
const jsi::Value& /*thisVal*/,
|
|
387
|
+
const jsi::Value* args,
|
|
388
|
+
size_t count) -> jsi::Value {
|
|
389
|
+
if (count < 2) return jsi::Value::undefined();
|
|
390
|
+
vroom_chart_preserve_price_envelope(chart_, args[0].asNumber(),
|
|
391
|
+
args[1].asNumber());
|
|
392
|
+
return jsi::Value::undefined();
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (name == "beginIntervalMorph") {
|
|
397
|
+
// beginIntervalMorph() — capture the visible candle geometry so the next
|
|
398
|
+
// setCandles can be animated as a reshape. Call before setCandles.
|
|
399
|
+
return jsi::Function::createFromHostFunction(
|
|
400
|
+
rt,
|
|
401
|
+
jsi::PropNameID::forAscii(rt, "beginIntervalMorph"),
|
|
402
|
+
0,
|
|
403
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
404
|
+
const jsi::Value& /*thisVal*/,
|
|
405
|
+
const jsi::Value* /*args*/,
|
|
406
|
+
size_t /*count*/) -> jsi::Value {
|
|
407
|
+
vroom_chart_begin_interval_morph(chart_);
|
|
408
|
+
return jsi::Value::undefined();
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (name == "setIntervalMorph") {
|
|
413
|
+
// setIntervalMorph(t) — advance the capture toward the new candles. `t` is
|
|
414
|
+
// pre-eased progress: 0 = the captured frame, 1 = settled (capture freed).
|
|
415
|
+
return jsi::Function::createFromHostFunction(
|
|
416
|
+
rt,
|
|
417
|
+
jsi::PropNameID::forAscii(rt, "setIntervalMorph"),
|
|
418
|
+
1,
|
|
419
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
420
|
+
const jsi::Value& /*thisVal*/,
|
|
421
|
+
const jsi::Value* args,
|
|
422
|
+
size_t count) -> jsi::Value {
|
|
423
|
+
if (count < 1) return jsi::Value::undefined();
|
|
424
|
+
vroom_chart_set_interval_morph(
|
|
425
|
+
chart_, static_cast<float>(args[0].asNumber()));
|
|
426
|
+
return jsi::Value::undefined();
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
300
430
|
if (name == "pan") {
|
|
301
431
|
// pan(dx, dy) -> JsiSkPicture
|
|
302
432
|
// Mutates the visible range and renders in one JSI call so gesture
|
|
@@ -172,17 +172,20 @@ typedef struct VroomDrawPoint {
|
|
|
172
172
|
// and `b`; the other two corners (a.x,b.y) and (b.x,a.y) derive.
|
|
173
173
|
// 2 = pencil — a freehand path through `points` (in draw order). `a`/`b` mirror
|
|
174
174
|
// the first/last point so bounds and handle code stay uniform.
|
|
175
|
+
// 3 = path — straight segments through `points` (in draw order), ending in
|
|
176
|
+
// an arrowhead on the last one. Same storage as a pencil, but
|
|
177
|
+
// rendered unsmoothed and with every vertex a grab handle.
|
|
175
178
|
//
|
|
176
|
-
// `points`/`point_count` are only read for
|
|
177
|
-
// null/0. Like the rest of this API the points are copied internally, so
|
|
178
|
-
// caller may free them as soon as the call returns.
|
|
179
|
+
// `points`/`point_count` are only read for kinds 2 and 3; line and box leave
|
|
180
|
+
// them null/0. Like the rest of this API the points are copied internally, so
|
|
181
|
+
// the caller may free them as soon as the call returns.
|
|
179
182
|
typedef struct VroomDrawing {
|
|
180
183
|
VroomDrawPoint a;
|
|
181
184
|
VroomDrawPoint b;
|
|
182
185
|
uint32_t color; // 0xAARRGGBB
|
|
183
186
|
float width; // stroke width in px
|
|
184
|
-
int32_t kind; // 0 = line, 1 = box, 2 = pencil
|
|
185
|
-
const VroomDrawPoint* points; // pencil
|
|
187
|
+
int32_t kind; // 0 = line, 1 = box, 2 = pencil, 3 = path
|
|
188
|
+
const VroomDrawPoint* points; // pencil/path points (kind 2/3), else null
|
|
186
189
|
int32_t point_count; // number of `points`, else 0
|
|
187
190
|
} VroomDrawing;
|
|
188
191
|
|
|
@@ -291,6 +294,9 @@ typedef enum {
|
|
|
291
294
|
VROOM_FLOAT_VOLUME_RADIUS_PX, // volume bar top-corner radius px (0 = square)
|
|
292
295
|
VROOM_FLOAT_LINE_WIDTH_PX, // line-chart-mode polyline stroke width px
|
|
293
296
|
VROOM_FLOAT_LINE_GRADIENT_OPACITY, // fill under the line at its strongest (0 disables)
|
|
297
|
+
VROOM_FLOAT_LINE_TENSION, // 0..1 line-chart corner smoothing (0 = straight)
|
|
298
|
+
VROOM_FLOAT_LINE_TIP_DOT, // 0/1: dot at the line's newest end (default on)
|
|
299
|
+
VROOM_FLOAT_LINE_TIP_PULSE, // 0/1: expanding ring around that dot
|
|
294
300
|
VROOM_FLOAT_COUNT_
|
|
295
301
|
} VroomFloatKey;
|
|
296
302
|
|
|
@@ -551,18 +557,31 @@ void vroom_chart_set_drawings(VroomChart* chart, const VroomDrawing* drawings,
|
|
|
551
557
|
size_t count);
|
|
552
558
|
|
|
553
559
|
// Hit-tests pixel (x_px, y_px) against the committed drawings. On a hit, fills
|
|
554
|
-
// *out_index with the drawing index, *out_part with
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// reported for the currently selected drawing (whose handles are
|
|
558
|
-
// Returns false on a miss (out params untouched). Any out pointer may
|
|
560
|
+
// *out_index with the drawing index, *out_part with the sub-part that was hit,
|
|
561
|
+
// *out_t with the 0..1 grab position along the segment (A→B; 0/1 for handle
|
|
562
|
+
// hits, 0 for the shapes that have no meaningful t), and returns true. Handle
|
|
563
|
+
// hits are only reported for the currently selected drawing (whose handles are
|
|
564
|
+
// visible). Returns false on a miss (out params untouched). Any out pointer may
|
|
565
|
+
// be null.
|
|
566
|
+
//
|
|
567
|
+
// `out_part` is per-kind (see VroomDrawing.kind):
|
|
568
|
+
// line — 0 (endpoint A), 1 (endpoint B), 2 (body).
|
|
569
|
+
// box — 0..3 (corners, in the order a, (b.x,a.y), b, (a.x,b.y)), 4 (body).
|
|
570
|
+
// pencil — 5 (body; a stroke has no handles).
|
|
571
|
+
// path — 6 (body), or VROOM_DRAW_PART_VERTEX + i for vertex i. The offset
|
|
572
|
+
// keeps a vertex index from colliding with the small part numbers
|
|
573
|
+
// above, and is why a path is capped at VROOM_PATH_MAX_POINTS.
|
|
574
|
+
#define VROOM_DRAW_PART_VERTEX 100
|
|
575
|
+
#define VROOM_PATH_MAX_POINTS 64
|
|
559
576
|
bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px, float y_px,
|
|
560
577
|
int32_t* out_index, int32_t* out_part,
|
|
561
578
|
float* out_t);
|
|
562
579
|
|
|
563
|
-
// Selects a committed drawing (renders its
|
|
564
|
-
//
|
|
565
|
-
//
|
|
580
|
+
// Selects a committed drawing (renders its handles). `index` -1 clears the
|
|
581
|
+
// selection. `grabbed_endpoint` renders that handle 50% larger while it's being
|
|
582
|
+
// dragged (0/1 for a line, 0 for the box corner the host normalized to, the
|
|
583
|
+
// vertex index for a path); -1 for none. An out-of-range index clears the
|
|
584
|
+
// selection.
|
|
566
585
|
void vroom_chart_set_selected_drawing(VroomChart* chart, int32_t index,
|
|
567
586
|
int32_t grabbed_endpoint);
|
|
568
587
|
|
|
@@ -572,8 +591,16 @@ void vroom_chart_move_drawing_endpoint(VroomChart* chart, int32_t index,
|
|
|
572
591
|
int32_t endpoint, int64_t time_ms,
|
|
573
592
|
double price);
|
|
574
593
|
|
|
594
|
+
// Moves one vertex of a committed path (kind 3) to a new data-space anchor, for
|
|
595
|
+
// live handle dragging. `vertex` indexes `points`; a/b are re-mirrored onto the
|
|
596
|
+
// first/last point afterwards. No-op for an out-of-range drawing/vertex index or
|
|
597
|
+
// a drawing that isn't a path.
|
|
598
|
+
void vroom_chart_move_drawing_vertex(VroomChart* chart, int32_t index,
|
|
599
|
+
int32_t vertex, int64_t time_ms,
|
|
600
|
+
double price);
|
|
601
|
+
|
|
575
602
|
// Shifts a whole committed drawing by a *relative* data-space delta — `a`, `b`,
|
|
576
|
-
// and (for a pencil) every
|
|
603
|
+
// and (for a pencil or path) every point. Used for live body dragging of shapes
|
|
577
604
|
// whose points can't be restated cheaply. No-op for an out-of-range index.
|
|
578
605
|
void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
|
|
579
606
|
int64_t d_time_ms, double d_price);
|
|
@@ -642,6 +669,19 @@ void vroom_chart_start_draft_stroke(VroomChart* chart, uint32_t color,
|
|
|
642
669
|
void vroom_chart_append_draft_point(VroomChart* chart, int64_t time_ms,
|
|
643
670
|
double price);
|
|
644
671
|
|
|
672
|
+
// Restates the whole in-progress path (kind 3) draft: `count` vertices placed so
|
|
673
|
+
// far, plus — when `has_cursor` — a rubber-band segment from the last vertex to
|
|
674
|
+
// (cursor_time, cursor_price) carrying the arrow tip. Each placed vertex renders
|
|
675
|
+
// a node dot. A path is placed one deliberate click at a time (so `count` stays
|
|
676
|
+
// small, see VROOM_PATH_MAX_POINTS), which is why this restates rather than
|
|
677
|
+
// appending: the host stays the sole owner of the vertex list, and undoing a
|
|
678
|
+
// point is just a shorter array. Pass count 0 with has_cursor false to show
|
|
679
|
+
// nothing; the points are copied, so the caller may free them on return.
|
|
680
|
+
void vroom_chart_set_draft_path(VroomChart* chart, const VroomDrawPoint* points,
|
|
681
|
+
int32_t count, bool has_cursor,
|
|
682
|
+
int64_t cursor_time, double cursor_price,
|
|
683
|
+
uint32_t color, float width);
|
|
684
|
+
|
|
645
685
|
// Clears the draft (hides the in-progress node dots / guideline / stroke).
|
|
646
686
|
void vroom_chart_clear_draft(VroomChart* chart);
|
|
647
687
|
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
#include "chart.h"
|
|
11
11
|
|
|
12
|
+
#include <cmath>
|
|
13
|
+
|
|
12
14
|
#pragma clang diagnostic push
|
|
13
15
|
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
14
16
|
#include "include/core/SkCanvas.h"
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
#include "rsi.h"
|
|
35
37
|
#include "rsi_pane.h"
|
|
36
38
|
#include "style_inherit.h"
|
|
39
|
+
#include "tip_pulse.h"
|
|
37
40
|
#include "volume.h"
|
|
38
41
|
#include "vwap.h"
|
|
39
42
|
#include "theme.h"
|
|
@@ -191,7 +194,8 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
191
194
|
visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
|
|
192
195
|
theme.colors[VROOM_COLOR_LINE],
|
|
193
196
|
theme.floats[VROOM_FLOAT_LINE_GRADIENT_OPACITY],
|
|
194
|
-
fade, morph_src, morph_n, morph_t
|
|
197
|
+
fade, morph_src, morph_n, morph_t,
|
|
198
|
+
theme.floats[VROOM_FLOAT_LINE_TENSION]);
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
// 4.5. Volume bars — drawn under the candles so candles z-index above.
|
|
@@ -239,7 +243,8 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
239
243
|
canvas, lay, bounds, visible, n, window_ms,
|
|
240
244
|
visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
|
|
241
245
|
theme.colors[VROOM_COLOR_LINE], theme.floats[VROOM_FLOAT_LINE_WIDTH_PX],
|
|
242
|
-
fade, morph_src, morph_n, morph_t
|
|
246
|
+
fade, morph_src, morph_n, morph_t,
|
|
247
|
+
theme.floats[VROOM_FLOAT_LINE_TENSION]);
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
// 5.5. Moving-average overlay lines (SMA/EMA) on the price pane, over the
|
|
@@ -303,6 +308,21 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
303
308
|
vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
|
|
304
309
|
candle_area_h);
|
|
305
310
|
|
|
311
|
+
// 5.8. Line-mode tip marker — the dot (and optional pulse) at the newest
|
|
312
|
+
// close. Above the overlays so it stays the eye's anchor, but before the
|
|
313
|
+
// axis masks, which trim the ring at the price scale.
|
|
314
|
+
if (fade > 0.f && theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
|
|
315
|
+
vroom::ma_overlay::draw_close_tip(
|
|
316
|
+
canvas, lay, bounds, visible, n, window_ms, visible_start_ms,
|
|
317
|
+
candle_duration_ms, candle_right, candle_area_h,
|
|
318
|
+
theme.colors[VROOM_COLOR_LINE],
|
|
319
|
+
theme.colors[VROOM_COLOR_BACKGROUND],
|
|
320
|
+
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade,
|
|
321
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
322
|
+
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
|
|
323
|
+
morph_src, morph_n, morph_t);
|
|
324
|
+
}
|
|
325
|
+
|
|
306
326
|
// 6. Axis backgrounds (mask any candle overflow). The x-axis separator
|
|
307
327
|
// line is intentionally omitted for now. The bottom strip anchors at
|
|
308
328
|
// x_axis_top (below any indicator pane) so it never paints over it.
|
|
@@ -422,6 +442,17 @@ void VroomChart::begin_frame() {
|
|
|
422
442
|
last_anim_tick = now;
|
|
423
443
|
anim_started = true;
|
|
424
444
|
last_dt_seconds = dt;
|
|
445
|
+
|
|
446
|
+
// Wrapped rather than accumulated: the phase is the only thing anyone reads,
|
|
447
|
+
// and a chart left open for hours would otherwise lose float precision on it.
|
|
448
|
+
tip_pulse_elapsed_s =
|
|
449
|
+
std::fmod(tip_pulse_elapsed_s + dt, vroom::tip_pulse::kPeriodSeconds);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
bool VroomChart::tip_pulse_active() const {
|
|
453
|
+
return morph_fade > 0.f && !candles.empty() &&
|
|
454
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f &&
|
|
455
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f;
|
|
425
456
|
}
|
|
426
457
|
|
|
427
458
|
void VroomChart::rebuild_chart_picture() {
|
|
@@ -433,6 +464,9 @@ void VroomChart::rebuild_chart_picture() {
|
|
|
433
464
|
}
|
|
434
465
|
|
|
435
466
|
bool VroomChart::is_animating_now() const {
|
|
467
|
+
// The pulse never finishes on its own, so this is what keeps the host loops
|
|
468
|
+
// requeueing frames for it.
|
|
469
|
+
if (tip_pulse_active()) return true;
|
|
436
470
|
for (const auto& f : y_fades) {
|
|
437
471
|
if (f.opacity != f.target) return true;
|
|
438
472
|
}
|
package/cpp/_core_src/chart.h
CHANGED
|
@@ -64,6 +64,12 @@ struct VroomChart {
|
|
|
64
64
|
float morph_collapse = 0.f;
|
|
65
65
|
float morph_fade = 0.f;
|
|
66
66
|
|
|
67
|
+
// Phase of the line-tip pulse, in seconds into its cycle. Unlike every other
|
|
68
|
+
// animation here this one is core-driven: it loops with no endpoint for the
|
|
69
|
+
// host to ease toward, so begin_frame advances it from the frame clock and
|
|
70
|
+
// is_animating_now keeps the host's redraw loop alive (see tip_pulse.h).
|
|
71
|
+
float tip_pulse_elapsed_s = 0.f;
|
|
72
|
+
|
|
67
73
|
// Interval morph: the outgoing candle geometry captured when a timeframe
|
|
68
74
|
// switch begins, indexed from the right of the visible slice (slot 0 =
|
|
69
75
|
// newest) — the pairing the preserved slot grid guarantees. Stored as
|
|
@@ -182,9 +188,10 @@ struct VroomChart {
|
|
|
182
188
|
// Committed drawings, anchored in data space so they track the candles on
|
|
183
189
|
// pan/zoom. Drawn on the price pane above candles/overlays.
|
|
184
190
|
//
|
|
185
|
-
// Mirrors the public VroomDrawing but *owns* its points, so a pencil
|
|
186
|
-
// (kind 2) can carry a variable number of them. For
|
|
187
|
-
// empty and only a/b are used; for pencil a/b
|
|
191
|
+
// Mirrors the public VroomDrawing but *owns* its points, so a pencil stroke
|
|
192
|
+
// (kind 2) or path (kind 3) can carry a variable number of them. For
|
|
193
|
+
// line/box `points` is empty and only a/b are used; for pencil/path a/b
|
|
194
|
+
// mirror the first/last point.
|
|
188
195
|
struct StoredDrawing {
|
|
189
196
|
VroomDrawPoint a{};
|
|
190
197
|
VroomDrawPoint b{};
|
|
@@ -199,7 +206,9 @@ struct VroomChart {
|
|
|
199
206
|
// draft_a is always drawn (node dot); draft_b is drawn when draft_has_b.
|
|
200
207
|
// draft_guide draws the live guideline A->B; when false only node dots show
|
|
201
208
|
// (the committed segment renders via `drawings`). draft_color/draft_width
|
|
202
|
-
// style the guideline to match the eventual line.
|
|
209
|
+
// style the guideline to match the eventual line. For a path (draft_kind 3)
|
|
210
|
+
// draft_points holds the vertices placed so far and draft_b is the cursor
|
|
211
|
+
// the rubber-band segment runs to (when draft_has_b).
|
|
203
212
|
bool draft_active = false;
|
|
204
213
|
VroomDrawPoint draft_a{};
|
|
205
214
|
bool draft_has_b = false;
|
|
@@ -207,14 +216,16 @@ struct VroomChart {
|
|
|
207
216
|
bool draft_guide = false;
|
|
208
217
|
uint32_t draft_color = 0xff2962ff;
|
|
209
218
|
float draft_width = 2.f;
|
|
210
|
-
int32_t draft_kind = 0; // 0 = line, 1 = box, 2 = pencil
|
|
211
|
-
//
|
|
219
|
+
int32_t draft_kind = 0; // 0 = line, 1 = box, 2 = pencil, 3 = path
|
|
220
|
+
// Points of the in-progress stroke (draft_kind 2, grown one at a time) or
|
|
221
|
+
// path (draft_kind 3, restated per click/move).
|
|
212
222
|
std::vector<VroomDrawPoint> draft_points;
|
|
213
223
|
|
|
214
224
|
// Selection/editing state for committed drawings. selected_drawing indexes
|
|
215
|
-
// `drawings` (or -1); its
|
|
216
|
-
// 0 (A) or 1 (B)
|
|
217
|
-
//
|
|
225
|
+
// `drawings` (or -1); its handles render on top. grabbed_endpoint is the
|
|
226
|
+
// handle being dragged — 0 (A) or 1 (B) for a line, 0 for a box (the host
|
|
227
|
+
// normalizes the grabbed corner to endpoint 0), the vertex index for a path
|
|
228
|
+
// — rendered 50% larger; -1 when none.
|
|
218
229
|
int32_t selected_drawing = -1;
|
|
219
230
|
int32_t grabbed_endpoint = -1;
|
|
220
231
|
|
|
@@ -302,14 +313,20 @@ struct VroomChart {
|
|
|
302
313
|
void draw_chart(SkCanvas* canvas);
|
|
303
314
|
|
|
304
315
|
// Per-frame animation bookkeeping: computes `last_dt_seconds`, which paces
|
|
305
|
-
// the label fades
|
|
316
|
+
// the label fades, and advances `tip_pulse_elapsed_s`. Called at the top of
|
|
317
|
+
// draw_chart.
|
|
306
318
|
void begin_frame();
|
|
307
319
|
|
|
320
|
+
// True when the line tip's pulse ring is on screen and looping. Gated on
|
|
321
|
+
// line mode and on having data, so a chart that isn't showing the ring can
|
|
322
|
+
// still go idle.
|
|
323
|
+
bool tip_pulse_active() const;
|
|
324
|
+
|
|
308
325
|
// Re-records `chart_picture` by invoking draw_chart on a fresh
|
|
309
326
|
// SkPictureRecorder.
|
|
310
327
|
void rebuild_chart_picture();
|
|
311
328
|
|
|
312
|
-
// True if any axis label is mid-fade
|
|
313
|
-
// to know when to keep ticking.
|
|
329
|
+
// True if any axis label is mid-fade, or the tip pulse is running. Used by
|
|
330
|
+
// the JS-side animation loop to know when to keep ticking.
|
|
314
331
|
bool is_animating_now() const;
|
|
315
332
|
};
|
|
@@ -32,6 +32,17 @@ constexpr double kAxisDragSensitivity = 300.0;
|
|
|
32
32
|
constexpr double kMinCandleBodyPx = 1.5;
|
|
33
33
|
constexpr double kMaxCandleBodyPx = 32.0;
|
|
34
34
|
|
|
35
|
+
// Trailing gap the default framing leaves between the newest candle and the
|
|
36
|
+
// price axis. The newest bar flush against the axis reads as cramped, and in line
|
|
37
|
+
// mode the tip marker's pulse needs somewhere to expand into — the axis mask
|
|
38
|
+
// (draw_chart step 6) paints over the gutter, so clearance has to come from the
|
|
39
|
+
// time window rather than from right_padding_px.
|
|
40
|
+
//
|
|
41
|
+
// A gap this small is far under the 3/4-window cap, so it neither triggers the
|
|
42
|
+
// pan rubber-band nor gets clamped away; a timeframe switch carries it over in
|
|
43
|
+
// slots (see timeframeWindow in the hosts), which preserves it in pixels.
|
|
44
|
+
constexpr double kRightGapPx = 12.0;
|
|
45
|
+
|
|
35
46
|
// Hard cap on the future gap (visible_end - last_candle): 3/4 of the window so
|
|
36
47
|
// at least 25% of the chart still shows candles. Never forces an existing larger
|
|
37
48
|
// gap (e.g. from the x-axis drag zoom) to snap back — only limits new growth.
|
|
@@ -70,14 +81,29 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
70
81
|
const int64_t right_edge_ms =
|
|
71
82
|
chart->candles.back().time_ms + chart->candle_duration_ms;
|
|
72
83
|
|
|
84
|
+
const auto lay = chart->layout();
|
|
85
|
+
const double usable =
|
|
86
|
+
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
87
|
+
|
|
88
|
+
// Places a window of the given width with kRightGapPx of air past the newest
|
|
89
|
+
// candle. Shifts the window forward rather than widening it, so the framing
|
|
90
|
+
// keeps whatever candle width it just computed. Falls back to a flush right
|
|
91
|
+
// edge when the layout isn't measured yet and px can't be converted to time.
|
|
92
|
+
const auto frame = [&](int64_t window_ms) {
|
|
93
|
+
const int64_t gap_ms =
|
|
94
|
+
usable > 0.0 && window_ms > 0
|
|
95
|
+
? static_cast<int64_t>((kRightGapPx * static_cast<double>(window_ms)) /
|
|
96
|
+
usable)
|
|
97
|
+
: 0;
|
|
98
|
+
chart->visible_end_ms = right_edge_ms + gap_ms;
|
|
99
|
+
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
100
|
+
};
|
|
101
|
+
|
|
73
102
|
// Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
|
|
74
103
|
// body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
|
|
75
104
|
// Needs a valid layout (width_px); until size is known it falls through to
|
|
76
105
|
// the candle-count default below.
|
|
77
106
|
if (chart->default_candle_px > 0.f) {
|
|
78
|
-
const auto lay = chart->layout();
|
|
79
|
-
const double usable =
|
|
80
|
-
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
81
107
|
const double ratio = chart->theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO];
|
|
82
108
|
const double dur = static_cast<double>(chart->candle_duration_ms);
|
|
83
109
|
const double body = std::clamp(static_cast<double>(chart->default_candle_px),
|
|
@@ -88,8 +114,7 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
88
114
|
chart->candles.back().time_ms - chart->candles.front().time_ms;
|
|
89
115
|
window_ms = std::clamp<int64_t>(window_ms, chart->candle_duration_ms,
|
|
90
116
|
span > 0 ? span : window_ms);
|
|
91
|
-
|
|
92
|
-
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
117
|
+
frame(window_ms);
|
|
93
118
|
return;
|
|
94
119
|
}
|
|
95
120
|
}
|
|
@@ -98,8 +123,7 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
98
123
|
const size_t start_idx = chart->candles.size() > kDefaultVisible
|
|
99
124
|
? chart->candles.size() - kDefaultVisible
|
|
100
125
|
: 0;
|
|
101
|
-
|
|
102
|
-
chart->visible_end_ms = right_edge_ms;
|
|
126
|
+
frame(right_edge_ms - chart->candles[start_idx].time_ms);
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
// On the first manual-y gesture, adopt the on-screen auto-fit bounds so the
|
|
@@ -883,8 +907,9 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
|
|
|
883
907
|
const VroomDrawing* drawings,
|
|
884
908
|
size_t count) {
|
|
885
909
|
if (!chart) return;
|
|
886
|
-
// Deep-copy into the owning form: a pencil's path
|
|
887
|
-
// caller's `points` buffer may be freed as soon as this
|
|
910
|
+
// Deep-copy into the owning form: a pencil stroke's or path's points live in
|
|
911
|
+
// the chart, so the caller's `points` buffer may be freed as soon as this
|
|
912
|
+
// returns.
|
|
888
913
|
chart->drawings.clear();
|
|
889
914
|
chart->drawings.reserve(count);
|
|
890
915
|
for (size_t i = 0; i < count; ++i) {
|
|
@@ -895,7 +920,7 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
|
|
|
895
920
|
d.color = src.color;
|
|
896
921
|
d.width = src.width;
|
|
897
922
|
d.kind = src.kind;
|
|
898
|
-
if (src.kind == 2 && src.points && src.point_count > 0) {
|
|
923
|
+
if ((src.kind == 2 || src.kind == 3) && src.points && src.point_count > 0) {
|
|
899
924
|
d.points.assign(src.points, src.points + src.point_count);
|
|
900
925
|
// Keep a/b mirroring the path ends so bounds/handle code is uniform.
|
|
901
926
|
d.a = d.points.front();
|
|
@@ -1025,6 +1050,27 @@ extern "C" void vroom_chart_append_draft_point(VroomChart* chart, int64_t time_m
|
|
|
1025
1050
|
chart->mark_dirty();
|
|
1026
1051
|
}
|
|
1027
1052
|
|
|
1053
|
+
extern "C" void vroom_chart_set_draft_path(VroomChart* chart,
|
|
1054
|
+
const VroomDrawPoint* points,
|
|
1055
|
+
int32_t count, bool has_cursor,
|
|
1056
|
+
int64_t cursor_time,
|
|
1057
|
+
double cursor_price, uint32_t color,
|
|
1058
|
+
float width) {
|
|
1059
|
+
if (!chart) return;
|
|
1060
|
+
chart->draft_active = true;
|
|
1061
|
+
chart->draft_kind = 3;
|
|
1062
|
+
chart->draft_guide = true;
|
|
1063
|
+
chart->draft_color = color;
|
|
1064
|
+
chart->draft_width = width;
|
|
1065
|
+
chart->draft_points.clear();
|
|
1066
|
+
if (points && count > 0) chart->draft_points.assign(points, points + count);
|
|
1067
|
+
chart->draft_has_b = has_cursor;
|
|
1068
|
+
chart->draft_b = VroomDrawPoint{cursor_time, cursor_price};
|
|
1069
|
+
// draft_a doubles as the first placed vertex for the shared node-dot path.
|
|
1070
|
+
if (!chart->draft_points.empty()) chart->draft_a = chart->draft_points.front();
|
|
1071
|
+
chart->mark_dirty();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1028
1074
|
extern "C" void vroom_chart_clear_draft(VroomChart* chart) {
|
|
1029
1075
|
if (!chart) return;
|
|
1030
1076
|
if (!chart->draft_active) return;
|
|
@@ -1151,6 +1197,21 @@ extern "C" void vroom_chart_move_drawing_endpoint(VroomChart* chart, int32_t ind
|
|
|
1151
1197
|
chart->mark_dirty();
|
|
1152
1198
|
}
|
|
1153
1199
|
|
|
1200
|
+
extern "C" void vroom_chart_move_drawing_vertex(VroomChart* chart, int32_t index,
|
|
1201
|
+
int32_t vertex, int64_t time_ms,
|
|
1202
|
+
double price) {
|
|
1203
|
+
if (!chart) return;
|
|
1204
|
+
if (index < 0 || index >= static_cast<int32_t>(chart->drawings.size())) return;
|
|
1205
|
+
auto& d = chart->drawings[index];
|
|
1206
|
+
if (d.kind != 3) return;
|
|
1207
|
+
if (vertex < 0 || vertex >= static_cast<int32_t>(d.points.size())) return;
|
|
1208
|
+
d.points[vertex] = VroomDrawPoint{time_ms, price};
|
|
1209
|
+
// a/b mirror the path ends, so moving a terminal vertex has to restate them.
|
|
1210
|
+
d.a = d.points.front();
|
|
1211
|
+
d.b = d.points.back();
|
|
1212
|
+
chart->mark_dirty();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1154
1215
|
extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
|
|
1155
1216
|
int64_t d_time_ms, double d_price) {
|
|
1156
1217
|
if (!chart) return;
|
|
@@ -1160,6 +1221,7 @@ extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
|
|
|
1160
1221
|
p.time_ms += d_time_ms;
|
|
1161
1222
|
p.price += d_price;
|
|
1162
1223
|
};
|
|
1224
|
+
// a/b mirror the path/stroke ends, so shifting everything keeps them in sync.
|
|
1163
1225
|
shift(d.a);
|
|
1164
1226
|
shift(d.b);
|
|
1165
1227
|
for (VroomDrawPoint& p : d.points) shift(p);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Monotone cubic interpolation — the math behind the line chart's optional
|
|
2
|
+
// corner smoothing (VROOM_FLOAT_LINE_TENSION).
|
|
3
|
+
//
|
|
4
|
+
// Deliberately monotone (Fritsch-Carlson) rather than a plain Catmull-Rom or
|
|
5
|
+
// cardinal spline. An unconstrained spline through the closes overshoots on a
|
|
6
|
+
// sharp reversal, and on a price chart that isn't just ugly — the curve dips
|
|
7
|
+
// below the period's low and draws a price that never traded, next to candles
|
|
8
|
+
// that show the real one during the candle-to-line crossfade. Limiting the
|
|
9
|
+
// tangents keeps every local extreme on an actual data point.
|
|
10
|
+
//
|
|
11
|
+
// Skia-free and header-only so the unit tests can cover it (see
|
|
12
|
+
// tests/test_curve.cpp); ma_overlay.cpp owns the walk and the path emission.
|
|
13
|
+
|
|
14
|
+
#pragma once
|
|
15
|
+
|
|
16
|
+
#include <algorithm>
|
|
17
|
+
#include <cmath>
|
|
18
|
+
|
|
19
|
+
namespace vroom::curve {
|
|
20
|
+
|
|
21
|
+
// The two interior control points of a cubic Bezier segment, in whatever space
|
|
22
|
+
// the endpoints were given in (pixels, at every call site here).
|
|
23
|
+
struct Controls {
|
|
24
|
+
float c1x, c1y;
|
|
25
|
+
float c2x, c2y;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Slope of the straight line between two points. Non-increasing x yields 0,
|
|
29
|
+
// which keeps a degenerate span (duplicate timestamps, or the compressed
|
|
30
|
+
// spacing an interval morph can pass through) from producing infinities.
|
|
31
|
+
inline float secant(float x0, float y0, float x1, float y1) {
|
|
32
|
+
const float h = x1 - x0;
|
|
33
|
+
return h > 0.f ? (y1 - y0) / h : 0.f;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Tangent at a point with neighboring secants `d_prev` and `d_next`.
|
|
37
|
+
inline float monotone_tangent(float d_prev, float d_next) {
|
|
38
|
+
// Opposite signs (or a flat neighbor) mean this point is a local extreme.
|
|
39
|
+
// A zero tangent is what pins the curve to it instead of sailing past.
|
|
40
|
+
if (d_prev * d_next <= 0.f) return 0.f;
|
|
41
|
+
// Fritsch-Carlson: holding the tangent within three times the smaller
|
|
42
|
+
// neighboring secant is what makes each segment monotone, and is also why
|
|
43
|
+
// the control points below can never leave the endpoints' y range.
|
|
44
|
+
const float limit = 3.f * std::min(std::fabs(d_prev), std::fabs(d_next));
|
|
45
|
+
return std::clamp((d_prev + d_next) * 0.5f, -limit, limit);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Control points for the segment p0 -> p1, given the tangents chosen for each
|
|
49
|
+
// endpoint and how far to lean into them.
|
|
50
|
+
//
|
|
51
|
+
// `tension` 0 puts both tangents back on the segment's own secant, which lands
|
|
52
|
+
// the controls exactly on its thirds — the cubic *is* the straight line. So the
|
|
53
|
+
// knob is continuous at zero rather than snapping into a curve, and callers can
|
|
54
|
+
// interpolate it without a visible discontinuity.
|
|
55
|
+
inline Controls segment_controls(float x0, float y0,
|
|
56
|
+
float x1, float y1,
|
|
57
|
+
float m0, float m1,
|
|
58
|
+
float tension) {
|
|
59
|
+
const float d = secant(x0, y0, x1, y1);
|
|
60
|
+
const float a = d + (m0 - d) * tension;
|
|
61
|
+
const float b = d + (m1 - d) * tension;
|
|
62
|
+
const float third = (x1 - x0) / 3.f;
|
|
63
|
+
return Controls{x0 + third, y0 + a * third,
|
|
64
|
+
x1 - third, y1 - b * third};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} // namespace vroom::curve
|