react-native-vroom-chart 0.6.0 → 0.7.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 +150 -32
- package/cpp/_core_include/vroom/vroom_chart.h +180 -22
- 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 +55 -20
- package/cpp/_core_src/chart_facade.cpp +206 -66
- 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 +161 -28
- package/lib/index.d.ts +161 -28
- package/lib/index.js +156 -31
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +156 -31
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/VroomChart.tsx +69 -3
- package/src/easing.ts +40 -0
- package/src/index.ts +3 -0
- package/src/jsi.d.ts +76 -20
- package/src/theme.ts +1 -0
- package/src/types.ts +3 -0
- package/src/useChartCore.ts +103 -27
|
@@ -20,6 +20,31 @@ namespace {
|
|
|
20
20
|
inline uint32_t resolve_color(uint32_t color, uint32_t fill) {
|
|
21
21
|
return (color >> 24) == 0 ? fill : color;
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
// Channel-wise ARGB blend, for a candle that changes direction mid-morph.
|
|
25
|
+
inline uint32_t lerp_argb(uint32_t a, uint32_t b, float t) {
|
|
26
|
+
uint32_t out = 0;
|
|
27
|
+
for (int shift = 0; shift < 32; shift += 8) {
|
|
28
|
+
const float ca = static_cast<float>((a >> shift) & 0xFFu);
|
|
29
|
+
const float cb = static_cast<float>((b >> shift) & 0xFFu);
|
|
30
|
+
const auto v = static_cast<uint32_t>(ca + (cb - ca) * t + 0.5f);
|
|
31
|
+
out |= v << shift;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
inline uint32_t scale_alpha(uint32_t argb, float a) {
|
|
37
|
+
const auto alpha =
|
|
38
|
+
static_cast<uint32_t>(static_cast<float>((argb >> 24) & 0xFFu) * a + 0.5f);
|
|
39
|
+
return (alpha << 24) | (argb & 0x00FFFFFFu);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
43
|
+
|
|
44
|
+
// One slot's wick/body extents in pixels.
|
|
45
|
+
struct SlotGeom {
|
|
46
|
+
float x, open, high, low, close;
|
|
47
|
+
};
|
|
23
48
|
} // namespace
|
|
24
49
|
|
|
25
50
|
void draw(SkCanvas* canvas,
|
|
@@ -32,13 +57,23 @@ void draw(SkCanvas* canvas,
|
|
|
32
57
|
int64_t visible_start_ms,
|
|
33
58
|
int64_t candle_duration_ms,
|
|
34
59
|
float collapse,
|
|
35
|
-
float opacity
|
|
36
|
-
|
|
60
|
+
float opacity,
|
|
61
|
+
const CandleSnapshot* from,
|
|
62
|
+
std::size_t from_n,
|
|
63
|
+
float morph_t) {
|
|
64
|
+
if (!canvas) return;
|
|
37
65
|
|
|
38
66
|
collapse = std::clamp(collapse, 0.f, 1.f);
|
|
39
67
|
opacity = std::clamp(opacity, 0.f, 1.f);
|
|
68
|
+
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
40
69
|
if (opacity <= 0.f) return;
|
|
41
70
|
|
|
71
|
+
// A finished morph drops the capture, reducing the loop below to the plain
|
|
72
|
+
// candle path.
|
|
73
|
+
const std::size_t from_count = vroom::morph_from_count(from, from_n, morph_t);
|
|
74
|
+
const std::size_t slots = std::max(n, from_count);
|
|
75
|
+
if (slots == 0) return;
|
|
76
|
+
|
|
42
77
|
// During the candle→line morph, fade the entire candle layer as one unit so
|
|
43
78
|
// overlapping bars/wicks composite cleanly instead of stacking alpha.
|
|
44
79
|
const bool fade_layer = opacity < 0.999f;
|
|
@@ -60,13 +95,20 @@ void draw(SkCanvas* canvas,
|
|
|
60
95
|
const bool wick_round = theme.floats[VROOM_FLOAT_WICK_ROUND_CAP] > 0.5f;
|
|
61
96
|
|
|
62
97
|
constexpr float kBorderWidthPx = 1.f;
|
|
98
|
+
const uint32_t wick_color_bull =
|
|
99
|
+
resolve_color(theme.colors[VROOM_COLOR_WICK_BULL], fill_bull);
|
|
100
|
+
const uint32_t wick_color_bear =
|
|
101
|
+
resolve_color(theme.colors[VROOM_COLOR_WICK_BEAR], fill_bear);
|
|
102
|
+
const uint32_t border_color_bull =
|
|
103
|
+
resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull);
|
|
104
|
+
const uint32_t border_color_bear =
|
|
105
|
+
resolve_color(theme.colors[VROOM_COLOR_BORDER_BEAR], fill_bear);
|
|
106
|
+
|
|
63
107
|
// Draw the border only when it differs from the fill; an inherited/transparent
|
|
64
108
|
// border color (the default, and the "hide" path) renders nothing — so a hidden
|
|
65
109
|
// border costs no pixels and never affects candle width.
|
|
66
|
-
const bool draw_border_bull =
|
|
67
|
-
|
|
68
|
-
const bool draw_border_bear =
|
|
69
|
-
resolve_color(theme.colors[VROOM_COLOR_BORDER_BEAR], fill_bear) != fill_bear;
|
|
110
|
+
const bool draw_border_bull = border_color_bull != fill_bull;
|
|
111
|
+
const bool draw_border_bear = border_color_bear != fill_bear;
|
|
70
112
|
|
|
71
113
|
SkPaint bull_paint;
|
|
72
114
|
bull_paint.setAntiAlias(true);
|
|
@@ -78,15 +120,13 @@ void draw(SkCanvas* canvas,
|
|
|
78
120
|
|
|
79
121
|
SkPaint wick_bull;
|
|
80
122
|
wick_bull.setAntiAlias(true);
|
|
81
|
-
wick_bull.setColor(
|
|
82
|
-
resolve_color(theme.colors[VROOM_COLOR_WICK_BULL], fill_bull));
|
|
123
|
+
wick_bull.setColor(wick_color_bull);
|
|
83
124
|
wick_bull.setStrokeWidth(theme.floats[VROOM_FLOAT_WICK_WIDTH_PX]);
|
|
84
125
|
wick_bull.setStyle(SkPaint::kStroke_Style);
|
|
85
126
|
wick_bull.setStrokeCap(wick_round ? SkPaint::kRound_Cap : SkPaint::kButt_Cap);
|
|
86
127
|
|
|
87
128
|
SkPaint wick_bear = wick_bull;
|
|
88
|
-
wick_bear.setColor(
|
|
89
|
-
resolve_color(theme.colors[VROOM_COLOR_WICK_BEAR], fill_bear));
|
|
129
|
+
wick_bear.setColor(wick_color_bear);
|
|
90
130
|
|
|
91
131
|
// 1px body outlines, drawn *inside* the body (see the inset at draw time) so
|
|
92
132
|
// a contrasting border never widens the candle. Default sentinel resolves to
|
|
@@ -95,52 +135,110 @@ void draw(SkCanvas* canvas,
|
|
|
95
135
|
border_bull.setAntiAlias(true);
|
|
96
136
|
border_bull.setStyle(SkPaint::kStroke_Style);
|
|
97
137
|
border_bull.setStrokeWidth(kBorderWidthPx);
|
|
98
|
-
border_bull.setColor(
|
|
99
|
-
resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull));
|
|
138
|
+
border_bull.setColor(border_color_bull);
|
|
100
139
|
|
|
101
140
|
SkPaint border_bear = border_bull;
|
|
102
|
-
border_bear.setColor(
|
|
103
|
-
|
|
141
|
+
border_bear.setColor(border_color_bear);
|
|
142
|
+
|
|
143
|
+
// Scratch paints for the slots that need a per-slot color (see below).
|
|
144
|
+
SkPaint slot_wick, slot_fill, slot_border;
|
|
104
145
|
|
|
105
146
|
const float half_body = body_w * 0.5f;
|
|
147
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
106
148
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
// Iterate slots counting back from the right edge — the pairing a timeframe
|
|
150
|
+
// switch preserves — descending so the paint order stays oldest-first and
|
|
151
|
+
// touching wicks/borders composite exactly as they do without a morph.
|
|
152
|
+
for (std::size_t k = slots; k-- > 0;) {
|
|
153
|
+
const ::VroomCandle* to = (k < n) ? &visible[n - 1 - k] : nullptr;
|
|
154
|
+
const CandleSnapshot* frm = (k < from_count) ? &from[k] : nullptr;
|
|
155
|
+
|
|
156
|
+
SlotGeom g{};
|
|
157
|
+
bool bull;
|
|
158
|
+
float alpha = 1.f;
|
|
159
|
+
|
|
160
|
+
if (to) {
|
|
161
|
+
const SlotGeom t{
|
|
162
|
+
vroom::candle_center_x(lay, to->time_ms, candle_duration_ms,
|
|
163
|
+
visible_start_ms, window_ms),
|
|
164
|
+
vroom::price_to_y(lay, bounds, to->open),
|
|
165
|
+
vroom::price_to_y(lay, bounds, to->high),
|
|
166
|
+
vroom::price_to_y(lay, bounds, to->low),
|
|
167
|
+
vroom::price_to_y(lay, bounds, to->close),
|
|
168
|
+
};
|
|
169
|
+
bull = to->close >= to->open;
|
|
170
|
+
if (frm) {
|
|
171
|
+
// The snapshot is in band fractions, so it lands on the same
|
|
172
|
+
// pixels it occupied pre-switch even though the bounds changed.
|
|
173
|
+
g.x = lerp(frm->x * area_w, t.x, morph_t);
|
|
174
|
+
g.open = lerp(vroom::y_at_fraction(lay, frm->open), t.open, morph_t);
|
|
175
|
+
g.high = lerp(vroom::y_at_fraction(lay, frm->high), t.high, morph_t);
|
|
176
|
+
g.low = lerp(vroom::y_at_fraction(lay, frm->low), t.low, morph_t);
|
|
177
|
+
g.close =
|
|
178
|
+
lerp(vroom::y_at_fraction(lay, frm->close), t.close, morph_t);
|
|
179
|
+
} else {
|
|
180
|
+
g = t;
|
|
181
|
+
// A slot with no counterpart: fade in rather than pop.
|
|
182
|
+
if (from_count > 0) alpha = morph_t;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
g = {frm->x * area_w,
|
|
186
|
+
vroom::y_at_fraction(lay, frm->open),
|
|
187
|
+
vroom::y_at_fraction(lay, frm->high),
|
|
188
|
+
vroom::y_at_fraction(lay, frm->low),
|
|
189
|
+
vroom::y_at_fraction(lay, frm->close)};
|
|
190
|
+
bull = frm->bull;
|
|
191
|
+
alpha = 1.f - morph_t; // the new data dropped this slot
|
|
192
|
+
}
|
|
193
|
+
if (alpha <= 0.f) continue;
|
|
110
194
|
|
|
111
|
-
const float cx = vroom::candle_center_x(
|
|
112
|
-
lay, c.time_ms, candle_duration_ms,
|
|
113
|
-
visible_start_ms, window_ms);
|
|
114
|
-
const float y_close = vroom::price_to_y(lay, bounds, c.close);
|
|
115
195
|
// Collapse high/low/open toward the close so the candle folds into its
|
|
116
196
|
// close point (the line vertex) as `collapse` → 1.
|
|
117
|
-
const float
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const float
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
197
|
+
const float y_close = g.close;
|
|
198
|
+
const float y_high = g.high * (1.f - collapse) + y_close * collapse;
|
|
199
|
+
const float y_low = g.low * (1.f - collapse) + y_close * collapse;
|
|
200
|
+
const float y_open = g.open * (1.f - collapse) + y_close * collapse;
|
|
201
|
+
|
|
202
|
+
// Fast path: the prebuilt bull/bear paints. A faded slot and a candle
|
|
203
|
+
// that changes direction mid-morph both need per-slot colors.
|
|
204
|
+
const SkPaint* wick_p = bull ? &wick_bull : &wick_bear;
|
|
205
|
+
const SkPaint* fill_p = bull ? &bull_paint : &bear_paint;
|
|
206
|
+
const SkPaint* border_p = bull ? &border_bull : &border_bear;
|
|
207
|
+
bool draw_border = bull ? draw_border_bull : draw_border_bear;
|
|
208
|
+
|
|
209
|
+
const bool flip = frm && to && frm->bull != bull;
|
|
210
|
+
if (flip || alpha < 0.999f) {
|
|
211
|
+
const auto blend = [&](uint32_t c_bull, uint32_t c_bear) {
|
|
212
|
+
uint32_t c = bull ? c_bull : c_bear;
|
|
213
|
+
if (flip) c = lerp_argb(bull ? c_bear : c_bull, c, morph_t);
|
|
214
|
+
return scale_alpha(c, alpha);
|
|
215
|
+
};
|
|
216
|
+
slot_wick = *wick_p;
|
|
217
|
+
slot_wick.setColor(blend(wick_color_bull, wick_color_bear));
|
|
218
|
+
slot_fill = *fill_p;
|
|
219
|
+
slot_fill.setColor(blend(fill_bull, fill_bear));
|
|
220
|
+
slot_border = *border_p;
|
|
221
|
+
slot_border.setColor(blend(border_color_bull, border_color_bear));
|
|
222
|
+
wick_p = &slot_wick;
|
|
223
|
+
fill_p = &slot_fill;
|
|
224
|
+
border_p = &slot_border;
|
|
225
|
+
if (flip) draw_border = draw_border_bull || draw_border_bear;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
canvas->drawLine(g.x, y_high, g.x, y_low, *wick_p);
|
|
129
229
|
|
|
130
230
|
const float y_top = std::min(y_open, y_close);
|
|
131
231
|
const float y_bot = std::max(y_open, y_close);
|
|
132
232
|
const float h = std::max(1.f, y_bot - y_top);
|
|
133
|
-
const SkRect body = SkRect::MakeXYWH(
|
|
233
|
+
const SkRect body = SkRect::MakeXYWH(g.x - half_body, y_top, body_w, h);
|
|
134
234
|
// Corner radius clamped so thin candles / short bodies don't over-round.
|
|
135
235
|
const float r = std::min({body_r, body_w * 0.5f, h * 0.5f});
|
|
136
|
-
const SkPaint& fill = bull ? bull_paint : bear_paint;
|
|
137
236
|
if (r > 0.f) {
|
|
138
|
-
canvas->drawRoundRect(body, r, r,
|
|
237
|
+
canvas->drawRoundRect(body, r, r, *fill_p);
|
|
139
238
|
} else {
|
|
140
|
-
canvas->drawRect(body,
|
|
239
|
+
canvas->drawRect(body, *fill_p);
|
|
141
240
|
}
|
|
142
241
|
|
|
143
|
-
const bool draw_border = bull ? draw_border_bull : draw_border_bear;
|
|
144
242
|
// Inset by half the stroke so the centered stroke falls fully within the
|
|
145
243
|
// body: the border overlaps the fill's outer edge instead of straddling
|
|
146
244
|
// it, keeping the candle's footprint exactly body_w. Skip when the body is
|
|
@@ -148,13 +246,12 @@ void draw(SkCanvas* canvas,
|
|
|
148
246
|
if (draw_border && body_w > kBorderWidthPx && h > kBorderWidthPx) {
|
|
149
247
|
const SkRect inner =
|
|
150
248
|
body.makeInset(kBorderWidthPx * 0.5f, kBorderWidthPx * 0.5f);
|
|
151
|
-
const SkPaint& bp = bull ? border_bull : border_bear;
|
|
152
249
|
// Concentric radius so the inside border follows the rounded body.
|
|
153
250
|
const float ir = std::max(0.f, r - kBorderWidthPx * 0.5f);
|
|
154
251
|
if (ir > 0.f) {
|
|
155
|
-
canvas->drawRoundRect(inner, ir, ir,
|
|
252
|
+
canvas->drawRoundRect(inner, ir, ir, *border_p);
|
|
156
253
|
} else {
|
|
157
|
-
canvas->drawRect(inner,
|
|
254
|
+
canvas->drawRect(inner, *border_p);
|
|
158
255
|
}
|
|
159
256
|
}
|
|
160
257
|
}
|
package/cpp/_core_src/candles.h
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
class SkCanvas;
|
|
13
13
|
|
|
14
14
|
namespace vroom {
|
|
15
|
+
struct CandleSnapshot;
|
|
15
16
|
struct Layout;
|
|
16
17
|
struct PriceBounds;
|
|
17
18
|
struct Theme;
|
|
@@ -26,6 +27,12 @@ namespace vroom::candles {
|
|
|
26
27
|
// candle→line transition: 0 = normal candle, 1 = a flat point at the close (the
|
|
27
28
|
// body/wick heights and width shrink to the line). `opacity` (0..1) fades the
|
|
28
29
|
// whole candle layer out as the line fades in. Both default to a no-op.
|
|
30
|
+
//
|
|
31
|
+
// `from` / `from_n` is the outgoing geometry of an interval morph, indexed from
|
|
32
|
+
// the right of the visible slice (slot 0 = newest), and `morph_t` (0..1) is the
|
|
33
|
+
// eased progress toward `visible`. Each slot's wick and body interpolate between
|
|
34
|
+
// the two; slots present on only one side fade in or out. `morph_t == 1` (the
|
|
35
|
+
// default) draws `visible` alone.
|
|
29
36
|
void draw(SkCanvas* canvas,
|
|
30
37
|
const ::VroomCandle* visible,
|
|
31
38
|
std::size_t n,
|
|
@@ -36,6 +43,9 @@ void draw(SkCanvas* canvas,
|
|
|
36
43
|
int64_t visible_start_ms,
|
|
37
44
|
int64_t candle_duration_ms,
|
|
38
45
|
float collapse = 0.f,
|
|
39
|
-
float opacity = 1.f
|
|
46
|
+
float opacity = 1.f,
|
|
47
|
+
const CandleSnapshot* from = nullptr,
|
|
48
|
+
std::size_t from_n = 0,
|
|
49
|
+
float morph_t = 1.f);
|
|
40
50
|
|
|
41
51
|
} // namespace vroom::candles
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -33,11 +33,16 @@
|
|
|
33
33
|
#include "price_lines.h"
|
|
34
34
|
#include "rsi.h"
|
|
35
35
|
#include "rsi_pane.h"
|
|
36
|
+
#include "style_inherit.h"
|
|
36
37
|
#include "volume.h"
|
|
37
38
|
#include "vwap.h"
|
|
38
39
|
#include "theme.h"
|
|
39
40
|
#include "viewport.h"
|
|
40
41
|
|
|
42
|
+
namespace {
|
|
43
|
+
constexpr SkColor kVwapLine = 0xff00bcd4; // cyan
|
|
44
|
+
} // namespace
|
|
45
|
+
|
|
41
46
|
VroomChart::VroomChart() : theme(vroom::default_theme()) {}
|
|
42
47
|
|
|
43
48
|
void VroomChart::mark_dirty() {
|
|
@@ -49,7 +54,7 @@ vroom::Layout VroomChart::layout() const {
|
|
|
49
54
|
const float axis_w = axis_width_px > 0.f
|
|
50
55
|
? axis_width_px
|
|
51
56
|
: width_px * theme.floats[VROOM_FLOAT_Y_AXIS_WIDTH_RATIO];
|
|
52
|
-
const int pane_count = (
|
|
57
|
+
const int pane_count = (rsi.enabled ? 1 : 0) + (macd.enabled ? 1 : 0);
|
|
53
58
|
const float indicator_h = static_cast<float>(pane_count) * height_px *
|
|
54
59
|
theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
55
60
|
return vroom::Layout{
|
|
@@ -66,10 +71,11 @@ vroom::Layout VroomChart::layout() const {
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
void VroomChart::ensure_rsi() {
|
|
69
|
-
if (!
|
|
70
|
-
vroom::rsi::compute(candles.data(), candles.size(),
|
|
71
|
-
if (
|
|
72
|
-
vroom::rsi::compute_ma(rsi_cache,
|
|
74
|
+
if (!rsi.enabled || !rsi_dirty) return;
|
|
75
|
+
vroom::rsi::compute(candles.data(), candles.size(), rsi.period, rsi_cache);
|
|
76
|
+
if (rsi.ma_visible) {
|
|
77
|
+
vroom::rsi::compute_ma(rsi_cache, rsi.ma_period, rsi.ma_kind,
|
|
78
|
+
rsi_ma_cache);
|
|
73
79
|
} else {
|
|
74
80
|
rsi_ma_cache.clear();
|
|
75
81
|
}
|
|
@@ -77,9 +83,10 @@ void VroomChart::ensure_rsi() {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
void VroomChart::ensure_macd() {
|
|
80
|
-
if (!
|
|
81
|
-
vroom::macd::compute(candles.data(), candles.size(),
|
|
82
|
-
|
|
86
|
+
if (!macd.enabled || !macd_dirty) return;
|
|
87
|
+
vroom::macd::compute(candles.data(), candles.size(), macd.fast, macd.slow,
|
|
88
|
+
macd.signal, macd.source, macd.ma_kind,
|
|
89
|
+
macd.signal_ma_kind, macd_cache, macd_signal_cache,
|
|
83
90
|
macd_hist_cache);
|
|
84
91
|
macd_dirty = false;
|
|
85
92
|
}
|
|
@@ -96,8 +103,8 @@ void VroomChart::ensure_overlays() {
|
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
void VroomChart::ensure_vwap() {
|
|
99
|
-
if (!
|
|
100
|
-
vroom::vwap::compute(candles.data(), candles.size(),
|
|
106
|
+
if (!vwap.enabled || !vwap_dirty) return;
|
|
107
|
+
vroom::vwap::compute(candles.data(), candles.size(), vwap.reset_offset_min,
|
|
101
108
|
vwap_cache, vwap_breaks);
|
|
102
109
|
vwap_dirty = false;
|
|
103
110
|
}
|
|
@@ -112,6 +119,7 @@ void VroomChart::ensure_bollinger() {
|
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
122
|
+
begin_frame();
|
|
115
123
|
const auto lay = layout();
|
|
116
124
|
|
|
117
125
|
// 1. Background
|
|
@@ -140,19 +148,60 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
140
148
|
|
|
141
149
|
// 3. Update label fade state ONCE per frame — both gridlines and labels
|
|
142
150
|
// share these opacities so their animations stay in lockstep.
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
// While an interval morph fades the old ticks out, the axes are still laid
|
|
152
|
+
// out against the pre-switch scale and window so nothing appears to move;
|
|
153
|
+
// they adopt the new ones at the midpoint, invisible. Every axis call in
|
|
154
|
+
// the frame has to agree on which of the two it's using.
|
|
155
|
+
const auto axis_phase = vroom::labels::interval_phase(*this);
|
|
156
|
+
const auto& axis_bounds = axis_phase.outgoing ? morph_from_bounds : bounds;
|
|
157
|
+
const int64_t axis_start_ms =
|
|
158
|
+
axis_phase.outgoing ? morph_from_start_ms : visible_start_ms;
|
|
159
|
+
const int64_t axis_end_ms =
|
|
160
|
+
axis_phase.outgoing ? morph_from_end_ms : visible_end_ms;
|
|
161
|
+
vroom::labels::update_y_fades(*this, lay, axis_bounds);
|
|
162
|
+
vroom::labels::update_x_fades(*this, lay, axis_start_ms, axis_end_ms);
|
|
145
163
|
|
|
146
164
|
// 4. Gridlines — drawn before candles so candle bodies overlay them.
|
|
147
165
|
// Vertical (time) gridlines are intentionally disabled for now —
|
|
148
166
|
// re-enable by adding `vroom::labels::draw_x_gridlines(canvas, *this,
|
|
149
|
-
// candle_right, candle_area_h);` here.
|
|
150
|
-
vroom::labels::draw_y_gridlines(canvas, *this, lay,
|
|
167
|
+
// axis_start_ms, axis_end_ms, candle_right, candle_area_h);` here.
|
|
168
|
+
vroom::labels::draw_y_gridlines(canvas, *this, lay, axis_bounds,
|
|
151
169
|
candle_right, candle_area_h);
|
|
152
170
|
|
|
171
|
+
// 4.35. Price-series morph state, shared by the gradient fill below and the
|
|
172
|
+
// series itself (5). `morph_fade` crossfades candles→line and
|
|
173
|
+
// `morph_collapse` folds each candle toward its close (the line vertex);
|
|
174
|
+
// fade 0 = pure candles, fade 1 = pure line. An interval morph
|
|
175
|
+
// additionally reshapes each slot from the geometry it held before the
|
|
176
|
+
// timeframe switch (see `morph_from`) — candle bodies, line vertices and
|
|
177
|
+
// the fill alike, so every layer stays in step mid-crossfade.
|
|
178
|
+
const float fade = morph_fade;
|
|
179
|
+
const float collapse = morph_collapse;
|
|
180
|
+
const bool morphing = interval_morph_t < 1.f && !morph_from.empty();
|
|
181
|
+
const vroom::CandleSnapshot* morph_src = morphing ? morph_from.data() : nullptr;
|
|
182
|
+
const std::size_t morph_n = morphing ? morph_from.size() : 0;
|
|
183
|
+
const float morph_t = morphing ? interval_morph_t : 1.f;
|
|
184
|
+
|
|
185
|
+
// 4.4. Line-mode gradient fill — the wash under the close polyline. Behind the
|
|
186
|
+
// volume bars so they stay legible on top of it, which puts it at the
|
|
187
|
+
// back of the price pane's content.
|
|
188
|
+
if (fade > 0.f) {
|
|
189
|
+
vroom::ma_overlay::draw_close_gradient(
|
|
190
|
+
canvas, lay, bounds, visible, n, window_ms,
|
|
191
|
+
visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
|
|
192
|
+
theme.colors[VROOM_COLOR_LINE],
|
|
193
|
+
theme.floats[VROOM_FLOAT_LINE_GRADIENT_OPACITY],
|
|
194
|
+
fade, morph_src, morph_n, morph_t);
|
|
195
|
+
}
|
|
196
|
+
|
|
153
197
|
// 4.5. Volume bars — drawn under the candles so candles z-index above.
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
// A fully collapsed chart has no bars left to draw, which is also how
|
|
199
|
+
// "volume disabled" is represented (set_volume snaps the scalar).
|
|
200
|
+
if (volume_collapse_t < 1.f) {
|
|
201
|
+
vroom::volume::draw(canvas, visible, n, lay, theme, volume,
|
|
202
|
+
volume_collapse_t, volume_collapse_easing,
|
|
203
|
+
window_ms, visible_start_ms, candle_duration_ms);
|
|
204
|
+
}
|
|
156
205
|
|
|
157
206
|
// 4.6. Liquidity bands (resting-order depth) — behind the candles so the
|
|
158
207
|
// candle bodies paint over the volume-driven tint. Anchored in price
|
|
@@ -178,25 +227,19 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
178
227
|
}
|
|
179
228
|
|
|
180
229
|
// 5. Price series — candles, a close-price line, or a blend of the two during
|
|
181
|
-
// the candle↔line morph.
|
|
182
|
-
//
|
|
183
|
-
// fade 0 = pure candles, fade 1 = pure line. The line reuses the MA-overlay
|
|
184
|
-
// polyline routine, fed the visible closes and styled by theme.LINE.
|
|
185
|
-
const float fade = morph_fade;
|
|
186
|
-
const float collapse = morph_collapse;
|
|
230
|
+
// the candle↔line morph (state hoisted to 4.35 for the gradient fill). The
|
|
231
|
+
// line is styled by theme.LINE.
|
|
187
232
|
if (fade < 1.f) {
|
|
188
233
|
vroom::candles::draw(canvas, visible, n, lay, theme, bounds, window_ms,
|
|
189
234
|
visible_start_ms, candle_duration_ms, collapse,
|
|
190
|
-
1.f - fade);
|
|
235
|
+
1.f - fade, morph_src, morph_n, morph_t);
|
|
191
236
|
}
|
|
192
237
|
if (fade > 0.f) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
vroom::ma_overlay::draw(
|
|
196
|
-
canvas, lay, bounds, visible, n, closes.data(), window_ms,
|
|
238
|
+
vroom::ma_overlay::draw_close_line(
|
|
239
|
+
canvas, lay, bounds, visible, n, window_ms,
|
|
197
240
|
visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
|
|
198
241
|
theme.colors[VROOM_COLOR_LINE], theme.floats[VROOM_FLOAT_LINE_WIDTH_PX],
|
|
199
|
-
|
|
242
|
+
fade, morph_src, morph_n, morph_t);
|
|
200
243
|
}
|
|
201
244
|
|
|
202
245
|
// 5.5. Moving-average overlay lines (SMA/EMA) on the price pane, over the
|
|
@@ -216,7 +259,7 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
216
259
|
|
|
217
260
|
// 5.6. VWAP overlay (session, configurable reset) — a single price-pane line
|
|
218
261
|
// that breaks at each session reset (vwap_breaks).
|
|
219
|
-
if (
|
|
262
|
+
if (vwap.enabled) {
|
|
220
263
|
ensure_vwap();
|
|
221
264
|
if (vwap_cache.size() == candles.size()) {
|
|
222
265
|
const double* vis = vwap_cache.data() + range.start;
|
|
@@ -226,7 +269,10 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
226
269
|
vroom::ma_overlay::draw(canvas, lay, bounds, visible, n, vis,
|
|
227
270
|
window_ms, visible_start_ms,
|
|
228
271
|
candle_duration_ms, candle_right,
|
|
229
|
-
candle_area_h,
|
|
272
|
+
candle_area_h,
|
|
273
|
+
vroom::style::color_or(vwap.color, kVwapLine),
|
|
274
|
+
vroom::style::width_or(vwap.width, 1.5f),
|
|
275
|
+
brk);
|
|
230
276
|
}
|
|
231
277
|
}
|
|
232
278
|
|
|
@@ -272,8 +318,8 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
272
318
|
axis_bg);
|
|
273
319
|
|
|
274
320
|
// 7. Labels (read from y_fades / x_fades, no state mutation here)
|
|
275
|
-
vroom::labels::draw_y_labels(canvas, *this, lay,
|
|
276
|
-
vroom::labels::draw_x_labels(canvas, *this, lay);
|
|
321
|
+
vroom::labels::draw_y_labels(canvas, *this, lay, axis_bounds);
|
|
322
|
+
vroom::labels::draw_x_labels(canvas, *this, lay, axis_start_ms, axis_end_ms);
|
|
277
323
|
|
|
278
324
|
// 7.5. Current-price line + box — above labels so the box covers any label
|
|
279
325
|
// it overlaps; tracks the latest close as the price scale moves.
|
|
@@ -294,8 +340,8 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
294
340
|
struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
|
|
295
341
|
ActivePane panes[2];
|
|
296
342
|
int count = 0;
|
|
297
|
-
if (
|
|
298
|
-
if (
|
|
343
|
+
if (rsi.enabled) panes[count++] = {rsi_order, 0};
|
|
344
|
+
if (macd.enabled) panes[count++] = {macd_order, 1};
|
|
299
345
|
if (count == 2 && panes[0].order > panes[1].order) {
|
|
300
346
|
const ActivePane tmp = panes[0];
|
|
301
347
|
panes[0] = panes[1];
|
|
@@ -312,7 +358,7 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
312
358
|
const double* rsi_vis = rsi_cache.size() == candles.size()
|
|
313
359
|
? rsi_cache.data() + range.start : nullptr;
|
|
314
360
|
const double* rsi_ma_vis =
|
|
315
|
-
(
|
|
361
|
+
(rsi.ma_visible && rsi_ma_cache.size() == candles.size())
|
|
316
362
|
? rsi_ma_cache.data() + range.start : nullptr;
|
|
317
363
|
vroom::rsi_pane::draw(canvas, *this, lay, visible, n, rsi_vis,
|
|
318
364
|
rsi_ma_vis, window_ms, visible_start_ms,
|
|
@@ -361,19 +407,24 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
361
407
|
vroom::labels::gc_x_fades(*this);
|
|
362
408
|
}
|
|
363
409
|
|
|
364
|
-
void VroomChart::
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
|
|
410
|
+
void VroomChart::begin_frame() {
|
|
411
|
+
// dt for the fade animations. A large gap (resume from background, or an idle
|
|
412
|
+
// chart that a click just woke up) is clamped to a nominal frame rather than
|
|
413
|
+
// zeroed: dt == 0 means "snap" to the fade updaters, which would finish every
|
|
414
|
+
// fade in the first frame after any idle period.
|
|
415
|
+
constexpr float kNominalFrameSeconds = 1.f / 60.f;
|
|
416
|
+
const auto now = std::chrono::steady_clock::now();
|
|
368
417
|
float dt = 0.f;
|
|
369
418
|
if (anim_started) {
|
|
370
419
|
dt = std::chrono::duration<float>(now - last_anim_tick).count();
|
|
371
|
-
if (dt > 0.1f) dt =
|
|
420
|
+
if (dt > 0.1f) dt = kNominalFrameSeconds;
|
|
372
421
|
}
|
|
373
422
|
last_anim_tick = now;
|
|
374
423
|
anim_started = true;
|
|
375
424
|
last_dt_seconds = dt;
|
|
425
|
+
}
|
|
376
426
|
|
|
427
|
+
void VroomChart::rebuild_chart_picture() {
|
|
377
428
|
SkPictureRecorder recorder;
|
|
378
429
|
SkCanvas* canvas = recorder.beginRecording(SkRect::MakeWH(width_px, height_px));
|
|
379
430
|
draw_chart(canvas);
|