react-native-vroom-chart 0.8.0 → 0.9.1
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/_core_include/vroom/vroom_chart.h +5 -1
- package/cpp/_core_src/chart.cpp +36 -2
- package/cpp/_core_src/chart.h +15 -3
- package/cpp/_core_src/chart_facade.cpp +91 -61
- package/cpp/_core_src/crosshair.cpp +5 -1
- package/cpp/_core_src/curve.h +67 -0
- package/cpp/_core_src/labels.cpp +8 -3
- package/cpp/_core_src/ma_overlay.cpp +200 -34
- package/cpp/_core_src/ma_overlay.h +41 -4
- package/cpp/_core_src/price_indicator.cpp +4 -1
- package/cpp/_core_src/price_lines.cpp +7 -3
- package/cpp/_core_src/theme.cpp +3 -0
- package/cpp/_core_src/ticks.cpp +14 -0
- package/cpp/_core_src/ticks.h +8 -0
- package/cpp/_core_src/tip_pulse.h +74 -0
- package/cpp/_core_src/viewport.cpp +45 -0
- package/cpp/_core_src/viewport.h +30 -0
- package/lib/index.d.mts +29 -1
- package/lib/index.d.ts +29 -1
- package/lib/index.js +15 -5
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +15 -5
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +13 -7
- package/src/theme.ts +7 -0
- package/src/useChartCore.ts +8 -1
|
@@ -294,6 +294,9 @@ typedef enum {
|
|
|
294
294
|
VROOM_FLOAT_VOLUME_RADIUS_PX, // volume bar top-corner radius px (0 = square)
|
|
295
295
|
VROOM_FLOAT_LINE_WIDTH_PX, // line-chart-mode polyline stroke width px
|
|
296
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
|
|
297
300
|
VROOM_FLOAT_COUNT_
|
|
298
301
|
} VroomFloatKey;
|
|
299
302
|
|
|
@@ -342,7 +345,8 @@ void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_ms, int64_t
|
|
|
342
345
|
// Target candle *body* width in px for the default/reset framing. Larger = more
|
|
343
346
|
// zoomed in (fewer candles), smaller = more zoomed out. 0 restores the legacy
|
|
344
347
|
// "most recent ~80 candles" default. Affects initial framing only; an explicit
|
|
345
|
-
// set_visible_range still overrides it.
|
|
348
|
+
// set_visible_range still overrides it. A series shorter than the implied window
|
|
349
|
+
// is left-padded with empty slots so bars stay this wide instead of stretching.
|
|
346
350
|
void vroom_chart_set_default_candle_width(VroomChart* chart, float px);
|
|
347
351
|
|
|
348
352
|
// Chart render mode: 0 = candlesticks (default), 1 = line chart (a polyline
|
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
|
|
@@ -307,14 +313,20 @@ struct VroomChart {
|
|
|
307
313
|
void draw_chart(SkCanvas* canvas);
|
|
308
314
|
|
|
309
315
|
// Per-frame animation bookkeeping: computes `last_dt_seconds`, which paces
|
|
310
|
-
// the label fades
|
|
316
|
+
// the label fades, and advances `tip_pulse_elapsed_s`. Called at the top of
|
|
317
|
+
// draw_chart.
|
|
311
318
|
void begin_frame();
|
|
312
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
|
+
|
|
313
325
|
// Re-records `chart_picture` by invoking draw_chart on a fresh
|
|
314
326
|
// SkPictureRecorder.
|
|
315
327
|
void rebuild_chart_picture();
|
|
316
328
|
|
|
317
|
-
// True if any axis label is mid-fade
|
|
318
|
-
// 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.
|
|
319
331
|
bool is_animating_now() const;
|
|
320
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.
|
|
@@ -39,6 +50,21 @@ int64_t max_future_gap(int64_t window_ms, int64_t cur_future) {
|
|
|
39
50
|
return std::max<int64_t>((window_ms * 3) / 4, cur_future);
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
// Pan/translate future allowance. A window longer than the data has nowhere
|
|
54
|
+
// useful to scroll, so don't grow the future gap — growing it used to combine
|
|
55
|
+
// with a first-candle start pin and latch the view. Dense series keep the
|
|
56
|
+
// usual 3/4-window rubber-band.
|
|
57
|
+
int64_t pan_max_future(int64_t window_ms, int64_t cur_future,
|
|
58
|
+
int64_t first_time, int64_t last_time,
|
|
59
|
+
int64_t candle_duration_ms) {
|
|
60
|
+
const int64_t dur = candle_duration_ms > 0 ? candle_duration_ms : 0;
|
|
61
|
+
const int64_t data_extent = last_time + dur - first_time;
|
|
62
|
+
if (data_extent >= 0 && window_ms > data_extent) {
|
|
63
|
+
return std::max<int64_t>(0, cur_future);
|
|
64
|
+
}
|
|
65
|
+
return max_future_gap(window_ms, cur_future);
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
// Rubber-band a forward (into-future) pan delta: once the current future gap is
|
|
43
69
|
// past the soft cap (window/2), damp the delta quadratically toward zero as it
|
|
44
70
|
// nears the hard cap (3*window/4). Backward (into-past) deltas are unaffected.
|
|
@@ -70,27 +96,48 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
70
96
|
const int64_t right_edge_ms =
|
|
71
97
|
chart->candles.back().time_ms + chart->candle_duration_ms;
|
|
72
98
|
|
|
99
|
+
const auto lay = chart->layout();
|
|
100
|
+
const double usable =
|
|
101
|
+
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
102
|
+
|
|
103
|
+
// Places a window of the given width with kRightGapPx of air past the newest
|
|
104
|
+
// candle. Shifts the window forward rather than widening it, so the framing
|
|
105
|
+
// keeps whatever candle width it just computed. Falls back to a flush right
|
|
106
|
+
// edge when the layout isn't measured yet and px can't be converted to time.
|
|
107
|
+
const auto frame = [&](int64_t window_ms) {
|
|
108
|
+
const int64_t gap_ms =
|
|
109
|
+
usable > 0.0 && window_ms > 0
|
|
110
|
+
? static_cast<int64_t>((kRightGapPx * static_cast<double>(window_ms)) /
|
|
111
|
+
usable)
|
|
112
|
+
: 0;
|
|
113
|
+
chart->visible_end_ms = right_edge_ms + gap_ms;
|
|
114
|
+
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
115
|
+
};
|
|
116
|
+
|
|
73
117
|
// Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
|
|
74
118
|
// body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
|
|
75
119
|
// Needs a valid layout (width_px); until size is known it falls through to
|
|
76
120
|
// the candle-count default below.
|
|
77
121
|
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
122
|
const double ratio = chart->theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO];
|
|
82
123
|
const double dur = static_cast<double>(chart->candle_duration_ms);
|
|
83
124
|
const double body = std::clamp(static_cast<double>(chart->default_candle_px),
|
|
84
125
|
kMinCandleBodyPx, kMaxCandleBodyPx);
|
|
85
126
|
if (usable > 0.0 && ratio > 0.0 && dur > 0.0 && body > 0.0) {
|
|
86
|
-
int64_t window_ms =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
127
|
+
int64_t window_ms = vroom::window_for_body_width(
|
|
128
|
+
lay, chart->candle_duration_ms, body);
|
|
129
|
+
// Floor at one slot so the window is valid. Do NOT clamp to the
|
|
130
|
+
// data span: a short series would then stretch each bar to fill
|
|
131
|
+
// the plot, the opposite of a target body width. Empty space on
|
|
132
|
+
// the left is intentional — same rule as timeframeWindow (width
|
|
133
|
+
// wins).
|
|
134
|
+
if (window_ms > 0) {
|
|
135
|
+
if (window_ms < chart->candle_duration_ms) {
|
|
136
|
+
window_ms = chart->candle_duration_ms;
|
|
137
|
+
}
|
|
138
|
+
frame(window_ms);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
94
141
|
}
|
|
95
142
|
}
|
|
96
143
|
|
|
@@ -98,8 +145,7 @@ void apply_default_framing(VroomChart* chart) {
|
|
|
98
145
|
const size_t start_idx = chart->candles.size() > kDefaultVisible
|
|
99
146
|
? chart->candles.size() - kDefaultVisible
|
|
100
147
|
: 0;
|
|
101
|
-
|
|
102
|
-
chart->visible_end_ms = right_edge_ms;
|
|
148
|
+
frame(right_edge_ms - chart->candles[start_idx].time_ms);
|
|
103
149
|
}
|
|
104
150
|
|
|
105
151
|
// On the first manual-y gesture, adopt the on-screen auto-fit bounds so the
|
|
@@ -380,30 +426,23 @@ extern "C" void vroom_chart_pan(VroomChart* chart, float dx_px, float /*dy_px*/)
|
|
|
380
426
|
(-dx_px / usable_px) * static_cast<float>(window_ms));
|
|
381
427
|
if (delta_ms == 0) return;
|
|
382
428
|
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
// larger than the cap (it zooms in until ~one candle remains), so the cap
|
|
388
|
-
// never forces it back — it just shrinks naturally as the user scrolls.
|
|
389
|
-
// Left edge still hard-clamps at the first candle.
|
|
429
|
+
// Forward pans rubber-band between half and 3/4 of a window of empty
|
|
430
|
+
// future (see damp_future_delta / max_future_gap). Past clamping lives in
|
|
431
|
+
// clamp_shifted_time_window: dense series pin start at the first candle;
|
|
432
|
+
// a window longer than the data keeps empty past instead of latching.
|
|
390
433
|
const int64_t first_time = chart->candles.front().time_ms;
|
|
391
434
|
const int64_t last_time = chart->candles.back().time_ms;
|
|
392
435
|
const int64_t cur_future = chart->visible_end_ms - last_time;
|
|
393
436
|
const int64_t eff_delta = damp_future_delta(delta_ms, cur_future, window_ms);
|
|
394
437
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (new_start < first_time) {
|
|
404
|
-
new_start = first_time;
|
|
405
|
-
new_end = new_start + window_ms;
|
|
406
|
-
}
|
|
438
|
+
const auto clamped = vroom::clamp_shifted_time_window(
|
|
439
|
+
chart->visible_start_ms + eff_delta,
|
|
440
|
+
chart->visible_end_ms + eff_delta,
|
|
441
|
+
first_time, last_time, chart->candle_duration_ms,
|
|
442
|
+
pan_max_future(window_ms, cur_future, first_time, last_time,
|
|
443
|
+
chart->candle_duration_ms));
|
|
444
|
+
const int64_t new_start = clamped.start_ms;
|
|
445
|
+
const int64_t new_end = clamped.end_ms;
|
|
407
446
|
|
|
408
447
|
if (new_start == chart->visible_start_ms &&
|
|
409
448
|
new_end == chart->visible_end_ms) {
|
|
@@ -443,22 +482,16 @@ extern "C" void vroom_chart_translate(VroomChart* chart, float dx_px, float dy_p
|
|
|
443
482
|
const int64_t cur_future = chart->visible_end_ms - last_time;
|
|
444
483
|
const int64_t eff_delta =
|
|
445
484
|
damp_future_delta(delta_ms, cur_future, window_ms);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
new_end = new_start + window_ms;
|
|
457
|
-
}
|
|
458
|
-
if (new_start != chart->visible_start_ms ||
|
|
459
|
-
new_end != chart->visible_end_ms) {
|
|
460
|
-
chart->visible_start_ms = new_start;
|
|
461
|
-
chart->visible_end_ms = new_end;
|
|
485
|
+
const auto clamped = vroom::clamp_shifted_time_window(
|
|
486
|
+
chart->visible_start_ms + eff_delta,
|
|
487
|
+
chart->visible_end_ms + eff_delta,
|
|
488
|
+
first_time, last_time, chart->candle_duration_ms,
|
|
489
|
+
pan_max_future(window_ms, cur_future, first_time, last_time,
|
|
490
|
+
chart->candle_duration_ms));
|
|
491
|
+
if (clamped.start_ms != chart->visible_start_ms ||
|
|
492
|
+
clamped.end_ms != chart->visible_end_ms) {
|
|
493
|
+
chart->visible_start_ms = clamped.start_ms;
|
|
494
|
+
chart->visible_end_ms = clamped.end_ms;
|
|
462
495
|
changed = true;
|
|
463
496
|
}
|
|
464
497
|
}
|
|
@@ -573,9 +606,9 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
|
|
|
573
606
|
if (new_window_ms > max_window) new_window_ms = max_window;
|
|
574
607
|
|
|
575
608
|
// Pivot around the right edge — most-recent visible candle stays put.
|
|
609
|
+
// A window longer than the data may start before the first candle; that
|
|
610
|
+
// empty past is how defaultCandleWidth wins, so don't pin start to first.
|
|
576
611
|
int64_t new_start = chart->visible_end_ms - new_window_ms;
|
|
577
|
-
const int64_t first_time = chart->candles.front().time_ms;
|
|
578
|
-
if (new_start < first_time) new_start = first_time;
|
|
579
612
|
|
|
580
613
|
if (new_start == chart->visible_start_ms) return;
|
|
581
614
|
chart->visible_start_ms = new_start;
|
|
@@ -755,17 +788,14 @@ extern "C" void vroom_chart_zoom(VroomChart* chart, float scale_x, float scale_y
|
|
|
755
788
|
const int64_t last_time = chart->candles.back().time_ms;
|
|
756
789
|
// Cap the future gap at 3/4 of the (new) window so >= 25% of the
|
|
757
790
|
// chart still shows candles, without snapping back an existing
|
|
758
|
-
// larger gap (e.g. from the x-axis drag zoom).
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
new_start = first_time;
|
|
767
|
-
new_end = new_start + new_window;
|
|
768
|
-
}
|
|
791
|
+
// larger gap (e.g. from the x-axis drag zoom). Past clamping
|
|
792
|
+
// matches pan: a window longer than the data keeps empty left.
|
|
793
|
+
const auto clamped = vroom::clamp_shifted_time_window(
|
|
794
|
+
new_start, new_end, first_time, last_time,
|
|
795
|
+
chart->candle_duration_ms,
|
|
796
|
+
max_future_gap(new_window, chart->visible_end_ms - last_time));
|
|
797
|
+
new_start = clamped.start_ms;
|
|
798
|
+
new_end = clamped.end_ms;
|
|
769
799
|
if (new_start != chart->visible_start_ms ||
|
|
770
800
|
new_end != chart->visible_end_ms) {
|
|
771
801
|
chart->visible_start_ms = new_start;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
#include "chart.h"
|
|
22
22
|
#include "fonts.h"
|
|
23
23
|
#include "theme.h"
|
|
24
|
+
#include "ticks.h"
|
|
24
25
|
|
|
25
26
|
namespace vroom::crosshair {
|
|
26
27
|
|
|
@@ -146,7 +147,10 @@ void draw(SkCanvas* canvas,
|
|
|
146
147
|
if (lay.y_axis_width_px > 0.f) {
|
|
147
148
|
const double price = vroom::y_to_price(lay, bounds, cy);
|
|
148
149
|
char buf[32];
|
|
149
|
-
|
|
150
|
+
const int decimals = vroom::price_decimals(
|
|
151
|
+
vroom::pick_price_interval(bounds.max - bounds.min,
|
|
152
|
+
vroom::price_pane_bottom(lay)));
|
|
153
|
+
vroom::format_price(buf, sizeof(buf), price, decimals);
|
|
150
154
|
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
151
155
|
draw_badge(canvas, font, buf, axis_center_x, cy, badge_fill,
|
|
152
156
|
SK_ColorWHITE);
|
|
@@ -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
|
package/cpp/_core_src/labels.cpp
CHANGED
|
@@ -153,13 +153,15 @@ void draw_y_labels(SkCanvas* canvas,
|
|
|
153
153
|
// Horizontal center of the y-axis container ([width - y_axis_width, width]).
|
|
154
154
|
// Labels (and the price box) center on this so their text shares a column.
|
|
155
155
|
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
156
|
+
const int decimals = vroom::price_decimals(
|
|
157
|
+
vroom::pick_price_interval(bounds.max - bounds.min, candle_area_h));
|
|
156
158
|
|
|
157
159
|
for (const auto& f : chart.y_fades) {
|
|
158
160
|
if (f.opacity <= 1e-3f) continue;
|
|
159
161
|
const float y = vroom::price_to_y(lay, bounds, f.price);
|
|
160
162
|
|
|
161
163
|
char buf[32];
|
|
162
|
-
|
|
164
|
+
vroom::format_price(buf, sizeof(buf), f.price, decimals);
|
|
163
165
|
const size_t len = std::strlen(buf);
|
|
164
166
|
const float text_w = font.measureText(
|
|
165
167
|
buf, len, SkTextEncoding::kUTF8);
|
|
@@ -367,9 +369,12 @@ void recompute_axis_width(VroomChart& chart) {
|
|
|
367
369
|
}
|
|
368
370
|
|
|
369
371
|
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
372
|
+
const auto lay = chart.layout();
|
|
373
|
+
const int decimals = vroom::price_decimals(
|
|
374
|
+
vroom::pick_price_interval(hi - lo, vroom::price_pane_bottom(lay)));
|
|
370
375
|
char buf_hi[32], buf_lo[32];
|
|
371
|
-
|
|
372
|
-
|
|
376
|
+
vroom::format_price(buf_hi, sizeof(buf_hi), hi, decimals);
|
|
377
|
+
vroom::format_price(buf_lo, sizeof(buf_lo), lo, decimals);
|
|
373
378
|
const float w_hi = font.measureText(
|
|
374
379
|
buf_hi, std::strlen(buf_hi), SkTextEncoding::kUTF8);
|
|
375
380
|
const float w_lo = font.measureText(
|