react-native-vroom-chart 0.15.0 → 0.17.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 +252 -1
- package/cpp/_core_include/vroom/vroom_chart.h +178 -4
- package/cpp/_core_src/atr.cpp +67 -0
- package/cpp/_core_src/atr.h +44 -0
- package/cpp/_core_src/atr_pane.cpp +162 -0
- package/cpp/_core_src/atr_pane.h +48 -0
- package/cpp/_core_src/candles.cpp +15 -14
- package/cpp/_core_src/chart.cpp +758 -200
- package/cpp/_core_src/chart.h +221 -3
- package/cpp/_core_src/chart_facade.cpp +236 -23
- package/cpp/_core_src/color_lerp.h +28 -0
- package/cpp/_core_src/fair_value_gaps.cpp +76 -0
- package/cpp/_core_src/fair_value_gaps.h +54 -0
- package/cpp/_core_src/fvg_overlay.cpp +262 -0
- package/cpp/_core_src/fvg_overlay.h +43 -0
- package/cpp/_core_src/ichimoku.cpp +65 -0
- package/cpp/_core_src/ichimoku.h +45 -0
- package/cpp/_core_src/labels.cpp +3 -0
- package/cpp/_core_src/line_morph.h +159 -0
- package/cpp/_core_src/loading_line.cpp +183 -0
- package/cpp/_core_src/loading_line.h +38 -0
- package/cpp/_core_src/loading_wave.h +140 -0
- package/cpp/_core_src/ma_overlay.cpp +273 -45
- package/cpp/_core_src/ma_overlay.h +64 -2
- package/cpp/_core_src/macd.cpp +24 -1
- package/cpp/_core_src/macd.h +16 -0
- package/cpp/_core_src/macd_pane.cpp +71 -62
- package/cpp/_core_src/macd_pane.h +13 -1
- package/cpp/_core_src/pane_series.h +169 -0
- package/cpp/_core_src/price_indicator.cpp +21 -7
- package/cpp/_core_src/price_indicator.h +11 -1
- package/cpp/_core_src/price_indicator_anim.h +81 -0
- package/cpp/_core_src/rsi.cpp +4 -0
- package/cpp/_core_src/rsi.h +7 -0
- package/cpp/_core_src/rsi_pane.cpp +85 -29
- package/cpp/_core_src/rsi_pane.h +11 -1
- package/cpp/_core_src/theme.cpp +5 -0
- package/cpp/_core_src/tip_geometry.h +55 -0
- package/cpp/_core_src/viewport.h +68 -0
- package/lib/index.d.mts +281 -3
- package/lib/index.d.ts +281 -3
- package/lib/index.js +292 -14
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +292 -14
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +33 -3
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +139 -1
- package/src/theme.ts +1 -0
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +402 -8
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
#include "chart.h"
|
|
11
11
|
|
|
12
|
+
#include <algorithm>
|
|
12
13
|
#include <cmath>
|
|
13
14
|
|
|
14
15
|
#pragma clang diagnostic push
|
|
@@ -20,14 +21,20 @@
|
|
|
20
21
|
#include "include/core/SkRect.h"
|
|
21
22
|
#pragma clang diagnostic pop
|
|
22
23
|
|
|
24
|
+
#include "atr.h"
|
|
25
|
+
#include "atr_pane.h"
|
|
23
26
|
#include "bollinger.h"
|
|
24
27
|
#include "candles.h"
|
|
25
28
|
#include "chart_internal.h"
|
|
26
29
|
#include "crosshair.h"
|
|
27
30
|
#include "drawings.h"
|
|
28
31
|
#include "footprints.h"
|
|
32
|
+
#include "fvg_overlay.h"
|
|
33
|
+
#include "ichimoku.h"
|
|
29
34
|
#include "labels.h"
|
|
30
35
|
#include "liquidity.h"
|
|
36
|
+
#include "loading_line.h"
|
|
37
|
+
#include "loading_wave.h"
|
|
31
38
|
#include "ma.h"
|
|
32
39
|
#include "ma_overlay.h"
|
|
33
40
|
#include "macd.h"
|
|
@@ -38,6 +45,7 @@
|
|
|
38
45
|
#include "rsi_pane.h"
|
|
39
46
|
#include "style_inherit.h"
|
|
40
47
|
#include "tip_anchor.h"
|
|
48
|
+
#include "tip_geometry.h"
|
|
41
49
|
#include "tip_pulse.h"
|
|
42
50
|
#include "volume.h"
|
|
43
51
|
#include "vwap.h"
|
|
@@ -59,16 +67,34 @@ vroom::Layout VroomChart::layout() const {
|
|
|
59
67
|
const float axis_w = axis_width_px > 0.f
|
|
60
68
|
? axis_width_px
|
|
61
69
|
: width_px * theme.floats[VROOM_FLOAT_Y_AXIS_WIDTH_RATIO];
|
|
62
|
-
const int pane_count =
|
|
70
|
+
const int pane_count =
|
|
71
|
+
(rsi.enabled ? 1 : 0) + (macd.enabled ? 1 : 0) + (atr.enabled ? 1 : 0);
|
|
63
72
|
const float indicator_h = static_cast<float>(pane_count) * height_px *
|
|
64
73
|
theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
74
|
+
|
|
75
|
+
// The line chart's tip dot is anchored to the newest close and overhangs the
|
|
76
|
+
// plot's right edge by its own radius, so the gutter has to be at least that
|
|
77
|
+
// wide or the dot lands under the y-axis strip (see tip_geometry.h). Only
|
|
78
|
+
// line mode needs it, and interpolating on morph_fade rather than switching
|
|
79
|
+
// on chart_type lets the few pixels of reflow ride the candles→line
|
|
80
|
+
// crossfade instead of stepping the instant the mode changes.
|
|
81
|
+
const float base_pad = theme.floats[VROOM_FLOAT_RIGHT_PADDING_PX];
|
|
82
|
+
float right_pad = base_pad;
|
|
83
|
+
if (theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
|
|
84
|
+
const float wanted = std::max(
|
|
85
|
+
base_pad, vroom::tip_geometry::gutter_px(
|
|
86
|
+
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX]));
|
|
87
|
+
right_pad = base_pad + (wanted - base_pad) *
|
|
88
|
+
std::clamp(morph_fade, 0.f, 1.f);
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
return vroom::Layout{
|
|
66
92
|
width_px,
|
|
67
93
|
height_px,
|
|
68
94
|
vroom::axis_extent(axis_w, y_axis_collapse_t),
|
|
69
95
|
vroom::axis_extent(theme.floats[VROOM_FLOAT_X_AXIS_HEIGHT_PX],
|
|
70
96
|
x_axis_collapse_t),
|
|
71
|
-
|
|
97
|
+
right_pad,
|
|
72
98
|
theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO],
|
|
73
99
|
0.05f,
|
|
74
100
|
0.05f,
|
|
@@ -99,6 +125,90 @@ void VroomChart::ensure_macd() {
|
|
|
99
125
|
macd_dirty = false;
|
|
100
126
|
}
|
|
101
127
|
|
|
128
|
+
void VroomChart::ensure_atr() {
|
|
129
|
+
if (!atr.enabled || !atr_dirty) return;
|
|
130
|
+
vroom::atr::compute(candles.data(), candles.size(), atr.period,
|
|
131
|
+
atr.smoothing, atr_cache);
|
|
132
|
+
atr_dirty = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
int VroomChart::indicator_panes(vroom::IndicatorPane out[vroom::kMaxPanes]) const {
|
|
136
|
+
int count = 0;
|
|
137
|
+
if (rsi.enabled) out[count++] = {rsi_order, vroom::PaneKind::Rsi};
|
|
138
|
+
if (macd.enabled) out[count++] = {macd_order, vroom::PaneKind::Macd};
|
|
139
|
+
if (atr.enabled) out[count++] = {atr_order, vroom::PaneKind::Atr};
|
|
140
|
+
// Insertion sort: at most three entries, already nearly ordered.
|
|
141
|
+
for (int i = 1; i < count; ++i) {
|
|
142
|
+
const vroom::IndicatorPane key = out[i];
|
|
143
|
+
int j = i - 1;
|
|
144
|
+
for (; j >= 0 && out[j].order > key.order; --j) out[j + 1] = out[j];
|
|
145
|
+
out[j + 1] = key;
|
|
146
|
+
}
|
|
147
|
+
return count;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void VroomChart::draw_indicator_panes(SkCanvas* canvas,
|
|
151
|
+
const vroom::Layout& lay,
|
|
152
|
+
const VroomCandle* visible,
|
|
153
|
+
std::size_t n, std::size_t first,
|
|
154
|
+
int64_t window_ms, float candle_right,
|
|
155
|
+
float pane_top, bool use_capture,
|
|
156
|
+
float morph_t) {
|
|
157
|
+
vroom::IndicatorPane panes[vroom::kMaxPanes];
|
|
158
|
+
const int count = indicator_panes(panes);
|
|
159
|
+
const float pane_h =
|
|
160
|
+
height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
161
|
+
|
|
162
|
+
// A cache only lines up with the candles once its ensure_* has run against
|
|
163
|
+
// the current series; until then there is nothing to plot.
|
|
164
|
+
const auto visible_slice = [&](const std::vector<double>& cache) -> const double* {
|
|
165
|
+
return cache.size() == candles.size() ? cache.data() + first : nullptr;
|
|
166
|
+
};
|
|
167
|
+
const auto capture = [&](vroom::LineKey key) -> const vroom::LineMorph* {
|
|
168
|
+
return use_capture ? vroom::find_line_morph(morph_lines, key) : nullptr;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (int i = 0; i < count; ++i) {
|
|
172
|
+
const float pane_bottom = pane_top + pane_h;
|
|
173
|
+
switch (panes[i].kind) {
|
|
174
|
+
case vroom::PaneKind::Rsi: {
|
|
175
|
+
ensure_rsi();
|
|
176
|
+
vroom::rsi_pane::draw(
|
|
177
|
+
canvas, *this, lay, visible, n, visible_slice(rsi_cache),
|
|
178
|
+
rsi.ma_visible ? visible_slice(rsi_ma_cache) : nullptr,
|
|
179
|
+
window_ms, visible_start_ms, candle_duration_ms,
|
|
180
|
+
candle_right, pane_top, pane_bottom,
|
|
181
|
+
capture({vroom::LineKind::Rsi}),
|
|
182
|
+
rsi.ma_visible ? capture({vroom::LineKind::RsiMa}) : nullptr,
|
|
183
|
+
morph_t);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case vroom::PaneKind::Macd: {
|
|
187
|
+
ensure_macd();
|
|
188
|
+
vroom::macd_pane::draw(
|
|
189
|
+
canvas, *this, lay, visible, n, visible_slice(macd_cache),
|
|
190
|
+
visible_slice(macd_signal_cache),
|
|
191
|
+
visible_slice(macd_hist_cache), window_ms, visible_start_ms,
|
|
192
|
+
candle_duration_ms, candle_right, pane_top, pane_bottom,
|
|
193
|
+
capture({vroom::LineKind::Macd}),
|
|
194
|
+
capture({vroom::LineKind::MacdSignal}),
|
|
195
|
+
capture({vroom::LineKind::MacdHistogram}), morph_t);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case vroom::PaneKind::Atr: {
|
|
199
|
+
ensure_atr();
|
|
200
|
+
vroom::atr_pane::draw(canvas, *this, lay, visible, n,
|
|
201
|
+
visible_slice(atr_cache), window_ms,
|
|
202
|
+
visible_start_ms, candle_duration_ms,
|
|
203
|
+
candle_right, pane_top, pane_bottom,
|
|
204
|
+
capture({vroom::LineKind::Atr}), morph_t);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
pane_top = pane_bottom;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
102
212
|
void VroomChart::ensure_overlays() {
|
|
103
213
|
if (!overlays_dirty) return;
|
|
104
214
|
overlay_caches.resize(overlays.size());
|
|
@@ -126,6 +236,23 @@ void VroomChart::ensure_bollinger() {
|
|
|
126
236
|
bollinger_dirty = false;
|
|
127
237
|
}
|
|
128
238
|
|
|
239
|
+
void VroomChart::ensure_ichimoku() {
|
|
240
|
+
if (!ichimoku.enabled || !ichimoku_dirty) return;
|
|
241
|
+
vroom::ichimoku::compute(candles.data(), candles.size(),
|
|
242
|
+
ichimoku.tenkan_period, ichimoku.kijun_period,
|
|
243
|
+
ichimoku.senkou_b_period, ich_tenkan_cache,
|
|
244
|
+
ich_kijun_cache, ich_senkou_a_cache,
|
|
245
|
+
ich_senkou_b_cache, ich_chikou_cache);
|
|
246
|
+
ichimoku_dirty = false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
void VroomChart::ensure_fvg() {
|
|
250
|
+
if (!fvg.enabled || !fvg_dirty) return;
|
|
251
|
+
vroom::fvg::compute(candles.data(), candles.size(), fvg.max_bars_back,
|
|
252
|
+
fvg.wait_for_close != 0, fvg.fill_type, fvg_cache);
|
|
253
|
+
fvg_dirty = false;
|
|
254
|
+
}
|
|
255
|
+
|
|
129
256
|
void VroomChart::ensure_footprint_buckets() {
|
|
130
257
|
if (!footprint_buckets_dirty) return;
|
|
131
258
|
footprint_buckets = vroom::footprints::build_buckets(
|
|
@@ -134,6 +261,301 @@ void VroomChart::ensure_footprint_buckets() {
|
|
|
134
261
|
footprint_buckets_dirty = false;
|
|
135
262
|
}
|
|
136
263
|
|
|
264
|
+
vroom::IndexRange VroomChart::ichimoku_source_range(int64_t shift_ms) const {
|
|
265
|
+
return vroom::ichimoku::shifted_source_range(
|
|
266
|
+
candles.data(), candles.size(), visible_start_ms - candle_duration_ms,
|
|
267
|
+
visible_end_ms + candle_duration_ms, shift_ms);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
void VroomChart::draw_overlay_fills(SkCanvas* canvas, const vroom::Layout& lay,
|
|
271
|
+
const vroom::PriceBounds& bounds,
|
|
272
|
+
const VroomCandle* visible, std::size_t n,
|
|
273
|
+
std::size_t first, int64_t window_ms,
|
|
274
|
+
float candle_right, float candle_area_h,
|
|
275
|
+
bool use_capture, float morph_t) {
|
|
276
|
+
const std::size_t sz = candles.size();
|
|
277
|
+
const auto capture = [&](vroom::LineKey key) -> const vroom::LineMorph* {
|
|
278
|
+
return use_capture ? vroom::find_line_morph(morph_lines, key) : nullptr;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (bollinger.enabled) {
|
|
282
|
+
ensure_bollinger();
|
|
283
|
+
if (bollinger.fill_enabled && bb_upper_cache.size() == sz &&
|
|
284
|
+
bb_lower_cache.size() == sz) {
|
|
285
|
+
vroom::ma_overlay::fill_between(
|
|
286
|
+
canvas, lay, bounds, visible, n, bb_upper_cache.data() + first,
|
|
287
|
+
bb_lower_cache.data() + first, window_ms, visible_start_ms,
|
|
288
|
+
candle_duration_ms, candle_right, candle_area_h,
|
|
289
|
+
bollinger.upper_color, bollinger.fill_opacity,
|
|
290
|
+
capture({vroom::LineKind::BollingerUpper}),
|
|
291
|
+
capture({vroom::LineKind::BollingerLower}), morph_t);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (ichimoku.enabled && ichimoku.cloud_enabled) {
|
|
296
|
+
ensure_ichimoku();
|
|
297
|
+
if (ich_senkou_a_cache.size() == sz && ich_senkou_b_cache.size() == sz) {
|
|
298
|
+
const int64_t shift_ms =
|
|
299
|
+
static_cast<int64_t>(ichimoku.displacement) * candle_duration_ms;
|
|
300
|
+
const auto r = ichimoku_source_range(shift_ms);
|
|
301
|
+
// A fade's outgoing half has no new data to slice, but its capture
|
|
302
|
+
// still has to paint.
|
|
303
|
+
const bool has_new = n > 0 && r.end > r.start;
|
|
304
|
+
vroom::ma_overlay::fill_cloud(
|
|
305
|
+
canvas, lay, bounds,
|
|
306
|
+
has_new ? candles.data() + r.start : candles.data(),
|
|
307
|
+
has_new ? r.end - r.start : 0,
|
|
308
|
+
has_new ? ich_senkou_a_cache.data() + r.start : nullptr,
|
|
309
|
+
has_new ? ich_senkou_b_cache.data() + r.start : nullptr,
|
|
310
|
+
window_ms, visible_start_ms, candle_duration_ms, candle_right,
|
|
311
|
+
candle_area_h, ichimoku.bullish_cloud_color,
|
|
312
|
+
ichimoku.bearish_cloud_color, ichimoku.cloud_opacity, shift_ms,
|
|
313
|
+
capture({vroom::LineKind::IchimokuSenkouA}),
|
|
314
|
+
capture({vroom::LineKind::IchimokuSenkouB}), morph_t);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
void VroomChart::draw_overlay_lines(SkCanvas* canvas, const vroom::Layout& lay,
|
|
320
|
+
const vroom::PriceBounds& bounds,
|
|
321
|
+
const VroomCandle* visible, std::size_t n,
|
|
322
|
+
std::size_t first, int64_t window_ms,
|
|
323
|
+
float candle_right, float candle_area_h,
|
|
324
|
+
bool use_capture, float morph_t) {
|
|
325
|
+
const std::size_t sz = candles.size();
|
|
326
|
+
const auto capture = [&](vroom::LineKey key) -> const vroom::LineMorph* {
|
|
327
|
+
return use_capture ? vroom::find_line_morph(morph_lines, key) : nullptr;
|
|
328
|
+
};
|
|
329
|
+
// One price-pane line against the visible slice. `n == 0` (a fade's
|
|
330
|
+
// outgoing half) leaves only the capture to draw.
|
|
331
|
+
const auto stroke = [&](vroom::LineKey key, const std::vector<double>& cache,
|
|
332
|
+
uint32_t color, float width,
|
|
333
|
+
const unsigned char* breaks) {
|
|
334
|
+
if (cache.size() != sz) return;
|
|
335
|
+
vroom::ma_overlay::draw(canvas, lay, bounds, visible, n,
|
|
336
|
+
cache.data() + first, window_ms,
|
|
337
|
+
visible_start_ms, candle_duration_ms,
|
|
338
|
+
candle_right, candle_area_h, color, width,
|
|
339
|
+
breaks, 1.f, 0, capture(key), morph_t);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
if (!overlays.empty()) {
|
|
343
|
+
ensure_overlays();
|
|
344
|
+
for (std::size_t k = 0; k < overlays.size(); ++k) {
|
|
345
|
+
stroke(vroom::overlay_line_key(overlays[k], k), overlay_caches[k],
|
|
346
|
+
overlays[k].color, overlays[k].width, nullptr);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (vwap.enabled) {
|
|
351
|
+
ensure_vwap();
|
|
352
|
+
stroke({vroom::LineKind::Vwap}, vwap_cache,
|
|
353
|
+
vroom::style::color_or(vwap.color, kVwapLine),
|
|
354
|
+
vroom::style::width_or(vwap.width, 1.5f),
|
|
355
|
+
vwap_breaks.size() == sz ? vwap_breaks.data() + first : nullptr);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (bollinger.enabled) {
|
|
359
|
+
ensure_bollinger();
|
|
360
|
+
stroke({vroom::LineKind::BollingerUpper}, bb_upper_cache,
|
|
361
|
+
bollinger.upper_color, bollinger.upper_width, nullptr);
|
|
362
|
+
stroke({vroom::LineKind::BollingerLower}, bb_lower_cache,
|
|
363
|
+
bollinger.lower_color, bollinger.lower_width, nullptr);
|
|
364
|
+
stroke({vroom::LineKind::BollingerMiddle}, bb_middle_cache,
|
|
365
|
+
bollinger.middle_color, bollinger.middle_width, nullptr);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (ichimoku.enabled) {
|
|
369
|
+
ensure_ichimoku();
|
|
370
|
+
const int64_t shift_ms =
|
|
371
|
+
static_cast<int64_t>(ichimoku.displacement) * candle_duration_ms;
|
|
372
|
+
// The displaced spans read from their own source slice, so they can't
|
|
373
|
+
// go through `stroke`: the query window shifts back by however far the
|
|
374
|
+
// drawing shifts forward.
|
|
375
|
+
const auto span = [&](vroom::LineKey key,
|
|
376
|
+
const std::vector<double>& cache, uint32_t color,
|
|
377
|
+
float width, int64_t line_shift_ms) {
|
|
378
|
+
if (cache.size() != sz) return;
|
|
379
|
+
const auto r = ichimoku_source_range(line_shift_ms);
|
|
380
|
+
const bool has_new = n > 0 && r.end > r.start;
|
|
381
|
+
vroom::ma_overlay::draw(
|
|
382
|
+
canvas, lay, bounds,
|
|
383
|
+
has_new ? candles.data() + r.start : candles.data(),
|
|
384
|
+
has_new ? r.end - r.start : 0,
|
|
385
|
+
has_new ? cache.data() + r.start : nullptr, window_ms,
|
|
386
|
+
visible_start_ms, candle_duration_ms, candle_right,
|
|
387
|
+
candle_area_h, color, width, nullptr, 1.f, line_shift_ms,
|
|
388
|
+
capture(key), morph_t);
|
|
389
|
+
};
|
|
390
|
+
// Span B under span A where the two edges touch, then the unshifted
|
|
391
|
+
// signal lines, then chikou on top of everything.
|
|
392
|
+
if (ichimoku.senkou_b_enabled) {
|
|
393
|
+
span({vroom::LineKind::IchimokuSenkouB}, ich_senkou_b_cache,
|
|
394
|
+
ichimoku.senkou_b_color, ichimoku.senkou_b_width, shift_ms);
|
|
395
|
+
}
|
|
396
|
+
if (ichimoku.senkou_a_enabled) {
|
|
397
|
+
span({vroom::LineKind::IchimokuSenkouA}, ich_senkou_a_cache,
|
|
398
|
+
ichimoku.senkou_a_color, ichimoku.senkou_a_width, shift_ms);
|
|
399
|
+
}
|
|
400
|
+
if (ichimoku.kijun_enabled) {
|
|
401
|
+
span({vroom::LineKind::IchimokuKijun}, ich_kijun_cache,
|
|
402
|
+
ichimoku.kijun_color, ichimoku.kijun_width, 0);
|
|
403
|
+
}
|
|
404
|
+
if (ichimoku.tenkan_enabled) {
|
|
405
|
+
span({vroom::LineKind::IchimokuTenkan}, ich_tenkan_cache,
|
|
406
|
+
ichimoku.tenkan_color, ichimoku.tenkan_width, 0);
|
|
407
|
+
}
|
|
408
|
+
if (ichimoku.chikou_enabled) {
|
|
409
|
+
span({vroom::LineKind::IchimokuChikou}, ich_chikou_cache,
|
|
410
|
+
ichimoku.chikou_color, ichimoku.chikou_width, -shift_ms);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
void VroomChart::capture_morph_lines(const vroom::Layout& lay,
|
|
416
|
+
const vroom::PriceBounds& bounds,
|
|
417
|
+
vroom::IndexRange range,
|
|
418
|
+
int64_t window_ms) {
|
|
419
|
+
morph_lines.clear();
|
|
420
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
421
|
+
const std::size_t n = range.end - range.start;
|
|
422
|
+
if (area_w <= 0.f || window_ms <= 0 || n == 0) return;
|
|
423
|
+
const ::VroomCandle* visible = candles.data() + range.start;
|
|
424
|
+
const std::size_t sz = candles.size();
|
|
425
|
+
|
|
426
|
+
// Captures one series newest-first, matching the slot indexing every morph
|
|
427
|
+
// draw pairs on. `src` is the slice the values were drawn against — the
|
|
428
|
+
// visible one for everything except Ichimoku's displaced spans, which plot
|
|
429
|
+
// at `time_ms + shift_ms` from a window of their own. `frac` maps a value
|
|
430
|
+
// into its band; it's the only part that differs between the price pane and
|
|
431
|
+
// an indicator pane.
|
|
432
|
+
const auto capture = [&](vroom::LineKey key, const ::VroomCandle* src,
|
|
433
|
+
std::size_t src_n, const double* values,
|
|
434
|
+
int64_t shift_ms, double scale, auto&& frac) {
|
|
435
|
+
if (!values || src_n == 0) return;
|
|
436
|
+
vroom::LineMorph line;
|
|
437
|
+
line.key = key;
|
|
438
|
+
line.scale = scale;
|
|
439
|
+
line.pts.resize(src_n);
|
|
440
|
+
for (std::size_t k = 0; k < src_n; ++k) {
|
|
441
|
+
const std::size_t i = src_n - 1 - k;
|
|
442
|
+
const double v = values[i];
|
|
443
|
+
const bool defined = std::isfinite(v);
|
|
444
|
+
line.pts[k] = vroom::LineSnapshot{
|
|
445
|
+
vroom::candle_center_x(lay, src[i].time_ms + shift_ms,
|
|
446
|
+
candle_duration_ms, visible_start_ms,
|
|
447
|
+
window_ms) /
|
|
448
|
+
area_w,
|
|
449
|
+
defined ? static_cast<float>(frac(v)) : 0.f,
|
|
450
|
+
defined,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
morph_lines.push_back(std::move(line));
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Price-pane series share the band the candle capture was taken against, so
|
|
457
|
+
// they keep their position relative to the candles through the switch.
|
|
458
|
+
const auto price_frac = [&](double v) {
|
|
459
|
+
return vroom::price_fraction(bounds, v);
|
|
460
|
+
};
|
|
461
|
+
const auto price_series = [&](vroom::LineKey key,
|
|
462
|
+
const std::vector<double>& cache) {
|
|
463
|
+
if (cache.size() != sz) return;
|
|
464
|
+
capture(key, visible, n, cache.data() + range.start, 0, 0.0, price_frac);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
if (!overlays.empty()) {
|
|
468
|
+
ensure_overlays();
|
|
469
|
+
for (std::size_t k = 0; k < overlays.size(); ++k) {
|
|
470
|
+
price_series(vroom::overlay_line_key(overlays[k], k),
|
|
471
|
+
overlay_caches[k]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (vwap.enabled) {
|
|
476
|
+
ensure_vwap();
|
|
477
|
+
price_series({vroom::LineKind::Vwap}, vwap_cache);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (bollinger.enabled) {
|
|
481
|
+
ensure_bollinger();
|
|
482
|
+
price_series({vroom::LineKind::BollingerUpper}, bb_upper_cache);
|
|
483
|
+
price_series({vroom::LineKind::BollingerMiddle}, bb_middle_cache);
|
|
484
|
+
price_series({vroom::LineKind::BollingerLower}, bb_lower_cache);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (ichimoku.enabled) {
|
|
488
|
+
ensure_ichimoku();
|
|
489
|
+
const int64_t shift_ms =
|
|
490
|
+
static_cast<int64_t>(ichimoku.displacement) * candle_duration_ms;
|
|
491
|
+
// Same source windows draw_chart uses, so slot 0 is the same rightmost
|
|
492
|
+
// vertex on both sides of the switch even for a span running past the
|
|
493
|
+
// newest candle into empty time.
|
|
494
|
+
const auto span = [&](vroom::LineKey key,
|
|
495
|
+
const std::vector<double>& cache,
|
|
496
|
+
int64_t line_shift_ms) {
|
|
497
|
+
if (cache.size() != sz) return;
|
|
498
|
+
const auto r = ichimoku_source_range(line_shift_ms);
|
|
499
|
+
if (r.end <= r.start) return;
|
|
500
|
+
capture(key, candles.data() + r.start, r.end - r.start,
|
|
501
|
+
cache.data() + r.start, line_shift_ms, 0.0, price_frac);
|
|
502
|
+
};
|
|
503
|
+
span({vroom::LineKind::IchimokuTenkan}, ich_tenkan_cache, 0);
|
|
504
|
+
span({vroom::LineKind::IchimokuKijun}, ich_kijun_cache, 0);
|
|
505
|
+
span({vroom::LineKind::IchimokuSenkouA}, ich_senkou_a_cache, shift_ms);
|
|
506
|
+
span({vroom::LineKind::IchimokuSenkouB}, ich_senkou_b_cache, shift_ms);
|
|
507
|
+
span({vroom::LineKind::IchimokuChikou}, ich_chikou_cache, -shift_ms);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Pane series normalize against their pane's own band rather than the price
|
|
511
|
+
// one, so each pane's auto-fit is free to change across the switch without
|
|
512
|
+
// the capture moving.
|
|
513
|
+
const auto pane_slice = [&](const std::vector<double>& cache) -> const double* {
|
|
514
|
+
return cache.size() == sz ? cache.data() + range.start : nullptr;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
if (rsi.enabled) {
|
|
518
|
+
ensure_rsi();
|
|
519
|
+
const auto frac = [&](double v) {
|
|
520
|
+
return vroom::rsi::band_fraction(v, rsi_y_scale);
|
|
521
|
+
};
|
|
522
|
+
capture({vroom::LineKind::Rsi}, visible, n, pane_slice(rsi_cache), 0, 0.0,
|
|
523
|
+
frac);
|
|
524
|
+
if (rsi.ma_visible) {
|
|
525
|
+
capture({vroom::LineKind::RsiMa}, visible, n,
|
|
526
|
+
pane_slice(rsi_ma_cache), 0, 0.0, frac);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (macd.enabled) {
|
|
531
|
+
ensure_macd();
|
|
532
|
+
const double* line = pane_slice(macd_cache);
|
|
533
|
+
const double* signal = pane_slice(macd_signal_cache);
|
|
534
|
+
const double* hist = pane_slice(macd_hist_cache);
|
|
535
|
+
const double scale = vroom::macd::autoscale(
|
|
536
|
+
macd.line_visible ? line : nullptr,
|
|
537
|
+
macd.signal_visible ? signal : nullptr,
|
|
538
|
+
macd.hist_visible ? hist : nullptr, n);
|
|
539
|
+
const auto frac = [&](double v) {
|
|
540
|
+
return vroom::macd::band_fraction(v, scale, macd_y_scale);
|
|
541
|
+
};
|
|
542
|
+
capture({vroom::LineKind::Macd}, visible, n, line, 0, scale, frac);
|
|
543
|
+
capture({vroom::LineKind::MacdSignal}, visible, n, signal, 0, scale, frac);
|
|
544
|
+
capture({vroom::LineKind::MacdHistogram}, visible, n, hist, 0, scale,
|
|
545
|
+
frac);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (atr.enabled) {
|
|
549
|
+
ensure_atr();
|
|
550
|
+
const double* line = pane_slice(atr_cache);
|
|
551
|
+
const double scale = vroom::atr::autoscale(line, n);
|
|
552
|
+
capture({vroom::LineKind::Atr}, visible, n, line, 0, scale,
|
|
553
|
+
[&](double v) {
|
|
554
|
+
return vroom::atr::band_fraction(v, scale, atr_y_scale);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
137
559
|
void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
138
560
|
begin_frame();
|
|
139
561
|
const auto lay = layout();
|
|
@@ -143,6 +565,23 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
143
565
|
bg.setColor(theme.colors[VROOM_COLOR_BACKGROUND]);
|
|
144
566
|
canvas->drawRect(SkRect::MakeWH(width_px, height_px), bg);
|
|
145
567
|
|
|
568
|
+
// 1b. Loading line, in place of the scene (stages 1 and 2). Above the
|
|
569
|
+
// `candles.empty()` return because `loading` is authoritative: the host
|
|
570
|
+
// may still be holding the previous asset's bars while the new one
|
|
571
|
+
// loads, and the line has to win over them. Returning here is also what
|
|
572
|
+
// suppresses the axis text, price badge, crosshair and indicator panes
|
|
573
|
+
// until stage 3 releases the flag.
|
|
574
|
+
if (loading) {
|
|
575
|
+
if (loading_fade_in < 1.f) {
|
|
576
|
+
canvas->saveLayerAlphaf(nullptr, loading_fade_in);
|
|
577
|
+
vroom::loading_line::draw(canvas, *this, lay);
|
|
578
|
+
canvas->restore();
|
|
579
|
+
} else {
|
|
580
|
+
vroom::loading_line::draw(canvas, *this, lay);
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
146
585
|
if (candles.empty()) return;
|
|
147
586
|
|
|
148
587
|
// 2. Visible slice + bounds + window_ms + geometry
|
|
@@ -161,6 +600,10 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
161
600
|
const float candle_area_h = vroom::price_pane_bottom(lay);
|
|
162
601
|
const float candle_right =
|
|
163
602
|
width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
603
|
+
// The line tip is the one plot layer allowed past candle_right: its dot
|
|
604
|
+
// overhangs the newest candle's center, which the pinned-to-latest framing
|
|
605
|
+
// puts within a few pixels of that edge. It stops at the y-axis strip.
|
|
606
|
+
const float tip_clip_right = width_px - lay.y_axis_width_px;
|
|
164
607
|
|
|
165
608
|
// 3. Update label fade state ONCE per frame — both gridlines and labels
|
|
166
609
|
// share these opacities so their animations stay in lockstep.
|
|
@@ -205,13 +648,38 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
205
648
|
const std::size_t morph_n_draw = reshape ? morph_n : 0;
|
|
206
649
|
const float morph_t = reshape ? interval_morph_t : 1.f;
|
|
207
650
|
|
|
651
|
+
// Line-mode tip marker, resolved by whichever branch below draws the price
|
|
652
|
+
// series and painted at step 6.5. It is the only plot layer that has to
|
|
653
|
+
// outlive the axis backgrounds, so it can't be drawn in place like the rest.
|
|
654
|
+
float tip_opacity = 0.f;
|
|
655
|
+
std::size_t tip_slots = 0;
|
|
656
|
+
const vroom::CandleSnapshot* tip_from = nullptr;
|
|
657
|
+
std::size_t tip_from_n = 0;
|
|
658
|
+
float tip_morph_t = 1.f;
|
|
659
|
+
|
|
208
660
|
if (fade_out) {
|
|
209
|
-
// First half: only
|
|
210
|
-
// Layers with no snapshot (volume,
|
|
211
|
-
//
|
|
212
|
-
//
|
|
661
|
+
// First half: only what was captured, at the axis envelope opacity.
|
|
662
|
+
// Layers with no snapshot (volume, liquidity, FVG, drawings) would
|
|
663
|
+
// already be the new data, so they stay hidden until the incoming half.
|
|
664
|
+
// morph_t = 0 draws every capture pixel-identically.
|
|
213
665
|
const float layer = axis_phase.opacity;
|
|
666
|
+
// Groups the captured indicators into one layer rather than threading
|
|
667
|
+
// the envelope through as a per-paint alpha, so overlapping strokes
|
|
668
|
+
// composite once and the panes keep their signatures. The price series
|
|
669
|
+
// below fades per-paint, as it always has.
|
|
670
|
+
const auto push_fade_layer = [&]() -> bool {
|
|
671
|
+
if (layer >= 0.999f) return false;
|
|
672
|
+
canvas->saveLayerAlpha(nullptr,
|
|
673
|
+
static_cast<U8CPU>(layer * 255.f + 0.5f));
|
|
674
|
+
return true;
|
|
675
|
+
};
|
|
214
676
|
if (layer > 0.f) {
|
|
677
|
+
// Behind the candles, as in the settled z-order.
|
|
678
|
+
const bool fills_wrapped = push_fade_layer();
|
|
679
|
+
draw_overlay_fills(canvas, lay, bounds, visible, 0, range.start,
|
|
680
|
+
window_ms, candle_right, candle_area_h, true, 0.f);
|
|
681
|
+
if (fills_wrapped) canvas->restore();
|
|
682
|
+
|
|
215
683
|
if (fade > 0.f) {
|
|
216
684
|
vroom::ma_overlay::draw_close_gradient(
|
|
217
685
|
canvas, lay, bounds, visible, 0, window_ms,
|
|
@@ -236,18 +704,24 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
236
704
|
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade * layer,
|
|
237
705
|
morph_from.data(), morph_n, 0.f,
|
|
238
706
|
theme.floats[VROOM_FLOAT_LINE_TENSION]);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
theme.colors[VROOM_COLOR_BACKGROUND],
|
|
245
|
-
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade * layer,
|
|
246
|
-
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
247
|
-
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
|
|
248
|
-
morph_from.data(), morph_n, 0.f);
|
|
249
|
-
}
|
|
707
|
+
tip_opacity = fade * layer;
|
|
708
|
+
tip_slots = 0;
|
|
709
|
+
tip_from = morph_from.data();
|
|
710
|
+
tip_from_n = morph_n;
|
|
711
|
+
tip_morph_t = 0.f;
|
|
250
712
|
}
|
|
713
|
+
|
|
714
|
+
// Over the candles, and the panes below them. One layer for both:
|
|
715
|
+
// they occupy disjoint bands, so nothing overlaps across the two.
|
|
716
|
+
const bool lines_wrapped = push_fade_layer();
|
|
717
|
+
draw_overlay_lines(canvas, lay, bounds, visible, 0, range.start,
|
|
718
|
+
window_ms, candle_right, candle_area_h, true, 0.f);
|
|
719
|
+
if (lay.indicator_area_h > 0.f) {
|
|
720
|
+
draw_indicator_panes(canvas, lay, visible, 0, range.start,
|
|
721
|
+
window_ms, candle_right, candle_area_h,
|
|
722
|
+
true, 0.f);
|
|
723
|
+
}
|
|
724
|
+
if (lines_wrapped) canvas->restore();
|
|
251
725
|
}
|
|
252
726
|
} else {
|
|
253
727
|
// Incoming fade: the new scene at envelope opacity, no slot lerp
|
|
@@ -273,9 +747,19 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
273
747
|
}
|
|
274
748
|
|
|
275
749
|
// 4.5. Volume bars — drawn under the candles so candles z-index above.
|
|
276
|
-
|
|
750
|
+
// Through a loading reveal they grow up off the axis on the
|
|
751
|
+
// same clock as the candles, borrowing the collapse they
|
|
752
|
+
// already have. They carry no morph snapshot of their own, so
|
|
753
|
+
// without this they'd snap in at full height while the price
|
|
754
|
+
// series is still a thread lying on the line. A render-time
|
|
755
|
+
// override, not a state change: the host still owns the
|
|
756
|
+
// collapse for the volume toggle.
|
|
757
|
+
const float vol_t = loading_line_revealing
|
|
758
|
+
? std::max(volume_collapse_t, 1.f - interval_morph_t)
|
|
759
|
+
: volume_collapse_t;
|
|
760
|
+
if (vol_t < 1.f) {
|
|
277
761
|
vroom::volume::draw(canvas, visible, n, lay, theme, volume,
|
|
278
|
-
|
|
762
|
+
vol_t, volume_collapse_easing,
|
|
279
763
|
window_ms, visible_start_ms,
|
|
280
764
|
candle_duration_ms);
|
|
281
765
|
}
|
|
@@ -284,22 +768,20 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
284
768
|
vroom::liquidity::draw(canvas, *this, lay, bounds, candle_right,
|
|
285
769
|
candle_area_h);
|
|
286
770
|
|
|
287
|
-
// 4.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
canvas, lay, bounds, visible, n,
|
|
295
|
-
bb_upper_cache.data() + range.start,
|
|
296
|
-
bb_lower_cache.data() + range.start, window_ms,
|
|
297
|
-
visible_start_ms, candle_duration_ms, candle_right,
|
|
298
|
-
candle_area_h, bollinger.upper_color,
|
|
299
|
-
bollinger.fill_opacity);
|
|
300
|
-
}
|
|
771
|
+
// 4.65. Fair Value Gap boxes — behind the candles, so the bars that
|
|
772
|
+
// formed each imbalance still read over their own shading.
|
|
773
|
+
if (fvg.enabled) {
|
|
774
|
+
ensure_fvg();
|
|
775
|
+
vroom::fvg_overlay::draw_boxes(canvas, *this, lay, bounds,
|
|
776
|
+
window_ms, candle_right,
|
|
777
|
+
candle_area_h);
|
|
301
778
|
}
|
|
302
779
|
|
|
780
|
+
// 4.7 / 4.75. Bollinger fill and Ichimoku cloud, behind the candles.
|
|
781
|
+
draw_overlay_fills(canvas, lay, bounds, visible, n, range.start,
|
|
782
|
+
window_ms, candle_right, candle_area_h, reshape,
|
|
783
|
+
morph_t);
|
|
784
|
+
|
|
303
785
|
// 5. Price series — candles, a close-price line, or a blend.
|
|
304
786
|
if (fade < 1.f) {
|
|
305
787
|
vroom::candles::draw(canvas, visible, n, lay, theme, bounds,
|
|
@@ -317,146 +799,49 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
317
799
|
theme.floats[VROOM_FLOAT_LINE_TENSION]);
|
|
318
800
|
}
|
|
319
801
|
|
|
320
|
-
// 5.5. Moving
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (overlay_caches[k].size() != candles.size()) continue;
|
|
325
|
-
const double* vis = overlay_caches[k].data() + range.start;
|
|
326
|
-
vroom::ma_overlay::draw(
|
|
327
|
-
canvas, lay, bounds, visible, n, vis, window_ms,
|
|
328
|
-
visible_start_ms, candle_duration_ms, candle_right,
|
|
329
|
-
candle_area_h, overlays[k].color, overlays[k].width);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// 5.6. VWAP overlay.
|
|
334
|
-
if (vwap.enabled) {
|
|
335
|
-
ensure_vwap();
|
|
336
|
-
if (vwap_cache.size() == candles.size()) {
|
|
337
|
-
const double* vis = vwap_cache.data() + range.start;
|
|
338
|
-
const unsigned char* brk =
|
|
339
|
-
vwap_breaks.size() == candles.size()
|
|
340
|
-
? vwap_breaks.data() + range.start
|
|
341
|
-
: nullptr;
|
|
342
|
-
vroom::ma_overlay::draw(
|
|
343
|
-
canvas, lay, bounds, visible, n, vis, window_ms,
|
|
344
|
-
visible_start_ms, candle_duration_ms, candle_right,
|
|
345
|
-
candle_area_h,
|
|
346
|
-
vroom::style::color_or(vwap.color, kVwapLine),
|
|
347
|
-
vroom::style::width_or(vwap.width, 1.5f), brk);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// 5.65. Bollinger Band lines.
|
|
352
|
-
if (bollinger.enabled) {
|
|
353
|
-
ensure_bollinger();
|
|
354
|
-
const std::size_t sz = candles.size();
|
|
355
|
-
if (bb_upper_cache.size() == sz && bb_lower_cache.size() == sz &&
|
|
356
|
-
bb_middle_cache.size() == sz) {
|
|
357
|
-
const auto stroke = [&](const std::vector<double>& cache,
|
|
358
|
-
uint32_t color, float width) {
|
|
359
|
-
vroom::ma_overlay::draw(
|
|
360
|
-
canvas, lay, bounds, visible, n,
|
|
361
|
-
cache.data() + range.start, window_ms,
|
|
362
|
-
visible_start_ms, candle_duration_ms, candle_right,
|
|
363
|
-
candle_area_h, color, width);
|
|
364
|
-
};
|
|
365
|
-
stroke(bb_upper_cache, bollinger.upper_color,
|
|
366
|
-
bollinger.upper_width);
|
|
367
|
-
stroke(bb_lower_cache, bollinger.lower_color,
|
|
368
|
-
bollinger.lower_width);
|
|
369
|
-
stroke(bb_middle_cache, bollinger.middle_color,
|
|
370
|
-
bollinger.middle_width);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
802
|
+
// 5.5 - 5.67. Moving averages, VWAP, Bollinger and Ichimoku lines.
|
|
803
|
+
draw_overlay_lines(canvas, lay, bounds, visible, n, range.start,
|
|
804
|
+
window_ms, candle_right, candle_area_h, reshape,
|
|
805
|
+
morph_t);
|
|
373
806
|
|
|
374
807
|
// 5.7. Drawing annotations.
|
|
375
808
|
vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
|
|
376
809
|
candle_area_h);
|
|
377
810
|
|
|
378
|
-
// 5.8. Line-mode tip marker.
|
|
379
|
-
|
|
811
|
+
// 5.8. Line-mode tip marker. The saveLayer this branch may be
|
|
812
|
+
// wrapped in doesn't reach step 6.5, so fold its alpha in.
|
|
813
|
+
if (fade > 0.f) {
|
|
380
814
|
const auto anchor = vroom::tip_anchor::at(
|
|
381
815
|
range.start, range.end, candles.size(), reshape);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade,
|
|
388
|
-
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
389
|
-
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
|
|
390
|
-
anchor.use_morph ? morph_src : nullptr,
|
|
391
|
-
anchor.use_morph ? morph_n_draw : 0, morph_t);
|
|
816
|
+
tip_opacity = fade * (wrap ? axis_phase.opacity : 1.f);
|
|
817
|
+
tip_slots = anchor.slot_count;
|
|
818
|
+
tip_from = anchor.use_morph ? morph_src : nullptr;
|
|
819
|
+
tip_from_n = anchor.use_morph ? morph_n_draw : 0;
|
|
820
|
+
tip_morph_t = morph_t;
|
|
392
821
|
}
|
|
393
822
|
|
|
394
823
|
// Incoming fade: indicator panes share the scene opacity (and
|
|
395
824
|
// paint before axis masks so overflow still clips). Transforms
|
|
396
825
|
// keep the settled z-order after the price indicator.
|
|
397
826
|
if (fade_in && lay.indicator_area_h > 0.f) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
};
|
|
402
|
-
ActivePane panes[2];
|
|
403
|
-
int count = 0;
|
|
404
|
-
if (rsi.enabled) panes[count++] = {rsi_order, 0};
|
|
405
|
-
if (macd.enabled) panes[count++] = {macd_order, 1};
|
|
406
|
-
if (count == 2 && panes[0].order > panes[1].order) {
|
|
407
|
-
const ActivePane tmp = panes[0];
|
|
408
|
-
panes[0] = panes[1];
|
|
409
|
-
panes[1] = tmp;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const float pane_h =
|
|
413
|
-
height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
414
|
-
float pane_top = candle_area_h;
|
|
415
|
-
for (int i = 0; i < count; ++i) {
|
|
416
|
-
const float pane_bottom = pane_top + pane_h;
|
|
417
|
-
if (panes[i].type == 0) {
|
|
418
|
-
ensure_rsi();
|
|
419
|
-
const double* rsi_vis =
|
|
420
|
-
rsi_cache.size() == candles.size()
|
|
421
|
-
? rsi_cache.data() + range.start
|
|
422
|
-
: nullptr;
|
|
423
|
-
const double* rsi_ma_vis =
|
|
424
|
-
(rsi.ma_visible &&
|
|
425
|
-
rsi_ma_cache.size() == candles.size())
|
|
426
|
-
? rsi_ma_cache.data() + range.start
|
|
427
|
-
: nullptr;
|
|
428
|
-
vroom::rsi_pane::draw(
|
|
429
|
-
canvas, *this, lay, visible, n, rsi_vis, rsi_ma_vis,
|
|
430
|
-
window_ms, visible_start_ms, candle_duration_ms,
|
|
431
|
-
candle_right, pane_top, pane_bottom);
|
|
432
|
-
} else {
|
|
433
|
-
ensure_macd();
|
|
434
|
-
const double* macd_vis =
|
|
435
|
-
macd_cache.size() == candles.size()
|
|
436
|
-
? macd_cache.data() + range.start
|
|
437
|
-
: nullptr;
|
|
438
|
-
const double* sig_vis =
|
|
439
|
-
macd_signal_cache.size() == candles.size()
|
|
440
|
-
? macd_signal_cache.data() + range.start
|
|
441
|
-
: nullptr;
|
|
442
|
-
const double* hist_vis =
|
|
443
|
-
macd_hist_cache.size() == candles.size()
|
|
444
|
-
? macd_hist_cache.data() + range.start
|
|
445
|
-
: nullptr;
|
|
446
|
-
vroom::macd_pane::draw(
|
|
447
|
-
canvas, *this, lay, visible, n, macd_vis, sig_vis,
|
|
448
|
-
hist_vis, window_ms, visible_start_ms,
|
|
449
|
-
candle_duration_ms, candle_right, pane_top,
|
|
450
|
-
pane_bottom);
|
|
451
|
-
}
|
|
452
|
-
pane_top = pane_bottom;
|
|
453
|
-
}
|
|
827
|
+
draw_indicator_panes(canvas, lay, visible, n, range.start,
|
|
828
|
+
window_ms, candle_right, candle_area_h,
|
|
829
|
+
false, 1.f);
|
|
454
830
|
}
|
|
455
831
|
}
|
|
456
832
|
|
|
457
833
|
if (wrap) canvas->restore();
|
|
458
834
|
}
|
|
459
835
|
|
|
836
|
+
// 5.9. Loading line, stage 3. Over the series it handed off to, on the same
|
|
837
|
+
// clock, so it reads as the bars peeling away from the line rather than
|
|
838
|
+
// two things dissolving independently. Before the axis masks below,
|
|
839
|
+
// since it belongs inside the plot.
|
|
840
|
+
if (loading_line_revealing) {
|
|
841
|
+
vroom::loading_line::draw_fading(canvas, *this, lay,
|
|
842
|
+
1.f - interval_morph_t);
|
|
843
|
+
}
|
|
844
|
+
|
|
460
845
|
// 6. Axis backgrounds (mask any candle overflow). The x-axis separator
|
|
461
846
|
// line is intentionally omitted for now. The bottom strip anchors at
|
|
462
847
|
// x_axis_top (below any indicator pane) so it never paints over it.
|
|
@@ -471,6 +856,24 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
471
856
|
SkRect::MakeXYWH(candle_right, 0, axis_block_w, height_px),
|
|
472
857
|
axis_bg);
|
|
473
858
|
|
|
859
|
+
// 6.5. Line-mode tip marker, resolved at 5.8. After the masks because the
|
|
860
|
+
// dot sits on the newest close, which a view following the latest bar
|
|
861
|
+
// parks within a few pixels of candle_right — closer than the dot's own
|
|
862
|
+
// radius — so it paints into the gutter the layout widened for it. The
|
|
863
|
+
// clip still stops at the y-axis strip, so nothing here can reach the
|
|
864
|
+
// price labels; the pulse ring simply ends there.
|
|
865
|
+
if (tip_opacity > 0.f && theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
|
|
866
|
+
vroom::ma_overlay::draw_close_tip(
|
|
867
|
+
canvas, lay, bounds, visible, tip_slots, window_ms,
|
|
868
|
+
visible_start_ms, candle_duration_ms, candle_right, tip_clip_right,
|
|
869
|
+
candle_area_h, theme.colors[VROOM_COLOR_LINE],
|
|
870
|
+
theme.colors[VROOM_COLOR_BACKGROUND],
|
|
871
|
+
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], tip_opacity,
|
|
872
|
+
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
873
|
+
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds, tip_from,
|
|
874
|
+
tip_from_n, tip_morph_t);
|
|
875
|
+
}
|
|
876
|
+
|
|
474
877
|
// 7. Labels (read from y_fades / x_fades, no state mutation here)
|
|
475
878
|
vroom::labels::draw_y_labels(canvas, *this, lay, axis_bounds);
|
|
476
879
|
vroom::labels::draw_x_labels(canvas, *this, lay, axis_start_ms, axis_end_ms);
|
|
@@ -480,8 +883,24 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
480
883
|
// Hidden during a fade's outgoing half: the close is already the new
|
|
481
884
|
// series and there is no snapshot to fade.
|
|
482
885
|
if (!fade_out) {
|
|
483
|
-
|
|
484
|
-
|
|
886
|
+
// Eased off the same capture the candles reshape through, so the badge
|
|
887
|
+
// rides the bar's close edge instead of landing on the new price a
|
|
888
|
+
// whole animation early. The pairing rule is the tip marker's: the
|
|
889
|
+
// capture only stands for the newest candle while that candle is the
|
|
890
|
+
// one at the right edge.
|
|
891
|
+
const auto price_anchor = vroom::tip_anchor::at(
|
|
892
|
+
range.start, range.end, candles.size(), reshape);
|
|
893
|
+
const vroom::CandleSnapshot* price_from =
|
|
894
|
+
(price_anchor.use_morph && morph_src && morph_n_draw > 0)
|
|
895
|
+
? morph_src
|
|
896
|
+
: nullptr;
|
|
897
|
+
vroom::price_indicator::draw(canvas, *this, lay, bounds, candle_right,
|
|
898
|
+
candle_area_h, price_from, morph_t);
|
|
899
|
+
|
|
900
|
+
// 7.54. Fair Value Gap labels — the boxes themselves are back behind
|
|
901
|
+
// the candles, but their text has to clear the bars it sits over.
|
|
902
|
+
vroom::fvg_overlay::draw_labels(canvas, *this, lay, bounds, window_ms,
|
|
903
|
+
candle_right, candle_area_h);
|
|
485
904
|
|
|
486
905
|
// 7.55. Consumer-supplied price status lines — same tier as the
|
|
487
906
|
// current-price indicator (their badges must cover the labels
|
|
@@ -503,50 +922,9 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
503
922
|
// INDICATOR_HEIGHT_FRAC of the height; the candle pane already shrank
|
|
504
923
|
// to fit them (see layout()). A fade already drew them with the scene.
|
|
505
924
|
if (!fade_swap && lay.indicator_area_h > 0.f) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (rsi.enabled) panes[count++] = {rsi_order, 0};
|
|
510
|
-
if (macd.enabled) panes[count++] = {macd_order, 1};
|
|
511
|
-
if (count == 2 && panes[0].order > panes[1].order) {
|
|
512
|
-
const ActivePane tmp = panes[0];
|
|
513
|
-
panes[0] = panes[1];
|
|
514
|
-
panes[1] = tmp;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const float pane_h =
|
|
518
|
-
height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
|
|
519
|
-
float pane_top = candle_area_h; // == price_pane_bottom(lay)
|
|
520
|
-
for (int i = 0; i < count; ++i) {
|
|
521
|
-
const float pane_bottom = pane_top + pane_h;
|
|
522
|
-
if (panes[i].type == 0) {
|
|
523
|
-
ensure_rsi();
|
|
524
|
-
const double* rsi_vis = rsi_cache.size() == candles.size()
|
|
525
|
-
? rsi_cache.data() + range.start : nullptr;
|
|
526
|
-
const double* rsi_ma_vis =
|
|
527
|
-
(rsi.ma_visible && rsi_ma_cache.size() == candles.size())
|
|
528
|
-
? rsi_ma_cache.data() + range.start : nullptr;
|
|
529
|
-
vroom::rsi_pane::draw(canvas, *this, lay, visible, n, rsi_vis,
|
|
530
|
-
rsi_ma_vis, window_ms, visible_start_ms,
|
|
531
|
-
candle_duration_ms, candle_right, pane_top,
|
|
532
|
-
pane_bottom);
|
|
533
|
-
} else {
|
|
534
|
-
ensure_macd();
|
|
535
|
-
const double* macd_vis = macd_cache.size() == candles.size()
|
|
536
|
-
? macd_cache.data() + range.start : nullptr;
|
|
537
|
-
const double* sig_vis =
|
|
538
|
-
macd_signal_cache.size() == candles.size()
|
|
539
|
-
? macd_signal_cache.data() + range.start : nullptr;
|
|
540
|
-
const double* hist_vis =
|
|
541
|
-
macd_hist_cache.size() == candles.size()
|
|
542
|
-
? macd_hist_cache.data() + range.start : nullptr;
|
|
543
|
-
vroom::macd_pane::draw(canvas, *this, lay, visible, n, macd_vis,
|
|
544
|
-
sig_vis, hist_vis, window_ms,
|
|
545
|
-
visible_start_ms, candle_duration_ms,
|
|
546
|
-
candle_right, pane_top, pane_bottom);
|
|
547
|
-
}
|
|
548
|
-
pane_top = pane_bottom;
|
|
549
|
-
}
|
|
925
|
+
// candle_area_h == price_pane_bottom(lay), the top of the band.
|
|
926
|
+
draw_indicator_panes(canvas, lay, visible, n, range.start, window_ms,
|
|
927
|
+
candle_right, candle_area_h, reshape, morph_t);
|
|
550
928
|
}
|
|
551
929
|
|
|
552
930
|
// 7.7. Crosshair — drawn last so it sits on top of everything, including the
|
|
@@ -593,6 +971,182 @@ void VroomChart::begin_frame() {
|
|
|
593
971
|
// and a chart left open for hours would otherwise lose float precision on it.
|
|
594
972
|
tip_pulse_elapsed_s =
|
|
595
973
|
std::fmod(tip_pulse_elapsed_s + dt, vroom::tip_pulse::kPeriodSeconds);
|
|
974
|
+
|
|
975
|
+
if (loading) {
|
|
976
|
+
// Frozen once a capture exists: stage 2 morphs away from the phase the
|
|
977
|
+
// sine held when the data landed, and advancing it underneath would
|
|
978
|
+
// drag the morph's own start point around.
|
|
979
|
+
if (loading_animate && loading_line.empty()) {
|
|
980
|
+
loading_elapsed_s = std::fmod(loading_elapsed_s + dt,
|
|
981
|
+
vroom::loading_wave::kPeriodSeconds);
|
|
982
|
+
}
|
|
983
|
+
// Ramps regardless of `loading_animate`: reduced motion suppresses the
|
|
984
|
+
// wave's movement, not the appearance of the line itself, which would
|
|
985
|
+
// otherwise pop in at full strength.
|
|
986
|
+
constexpr float kFadeInSeconds = 0.3f;
|
|
987
|
+
loading_fade_in = std::min(1.f, loading_fade_in + dt / kFadeInSeconds);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
void VroomChart::set_loading(bool on, bool animate) {
|
|
992
|
+
loading_animate = animate;
|
|
993
|
+
if (on == loading) return; // a re-push of the same state isn't a restart
|
|
994
|
+
loading = on;
|
|
995
|
+
|
|
996
|
+
// Either direction abandons any hand-off in flight. Going false without
|
|
997
|
+
// passing through the two stages is the cancel path (fetch failed, asset
|
|
998
|
+
// switched again mid-load), and it should leave nothing behind.
|
|
999
|
+
loading_line.clear();
|
|
1000
|
+
loading_line_t = 1.f;
|
|
1001
|
+
loading_line_revealing = false;
|
|
1002
|
+
if (!on) {
|
|
1003
|
+
mark_dirty();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
loading_elapsed_s = 0.f;
|
|
1008
|
+
loading_fade_in = 0.f;
|
|
1009
|
+
mark_dirty();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Visible slice + price bounds the two hand-off stages both need. Mirrors what
|
|
1013
|
+
// draw_chart computes in step 2; `n == 0` means there is nothing to hand off to.
|
|
1014
|
+
VroomChart::LoadingTarget VroomChart::loading_target() const {
|
|
1015
|
+
LoadingTarget t{};
|
|
1016
|
+
if (candles.empty()) return t;
|
|
1017
|
+
const auto range = vroom::visible_indices(candles.data(), candles.size(),
|
|
1018
|
+
visible_start_ms, visible_end_ms);
|
|
1019
|
+
const std::size_t n = range.end - range.start;
|
|
1020
|
+
if (n == 0) return t;
|
|
1021
|
+
t.visible = candles.data() + range.start;
|
|
1022
|
+
t.n = n;
|
|
1023
|
+
t.bounds = price_bounds_manual
|
|
1024
|
+
? price_bounds
|
|
1025
|
+
: vroom::auto_price_bounds(t.visible, n);
|
|
1026
|
+
return t;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
void VroomChart::begin_loading_morph() {
|
|
1030
|
+
if (!loading) return;
|
|
1031
|
+
|
|
1032
|
+
const auto lay = layout();
|
|
1033
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
1034
|
+
const auto target = loading_target();
|
|
1035
|
+
if (target.n == 0 || area_w <= 0.f) return;
|
|
1036
|
+
|
|
1037
|
+
const int64_t window_ms = visible_end_ms - visible_start_ms;
|
|
1038
|
+
const float elapsed = loading_animate ? loading_elapsed_s : 0.f;
|
|
1039
|
+
|
|
1040
|
+
const auto push = [&](float xf, float to_y) {
|
|
1041
|
+
vroom::LinePoint p{};
|
|
1042
|
+
p.x = xf;
|
|
1043
|
+
// Sampling the sine at this vertex's own x is what makes the freeze
|
|
1044
|
+
// invisible: the curve the user was watching passes through this point
|
|
1045
|
+
// already, so stage 2 starts on the pixels stage 1 ended on.
|
|
1046
|
+
p.from_y = vroom::loading_wave::y_frac(elapsed,
|
|
1047
|
+
std::clamp(xf, 0.f, 1.f));
|
|
1048
|
+
p.to_y = to_y;
|
|
1049
|
+
loading_line.push_back(p);
|
|
1050
|
+
};
|
|
1051
|
+
// Mid-range, not mid-body: the bars grow outward symmetrically from this
|
|
1052
|
+
// line, so anchoring on the body would leave one wick reaching further
|
|
1053
|
+
// than the other.
|
|
1054
|
+
const auto center_frac = [&](const ::VroomCandle& c) {
|
|
1055
|
+
return static_cast<float>(
|
|
1056
|
+
vroom::price_fraction(target.bounds, (c.high + c.low) * 0.5));
|
|
1057
|
+
};
|
|
1058
|
+
const auto center_x_frac = [&](const ::VroomCandle& c) {
|
|
1059
|
+
return vroom::candle_center_x(lay, c.time_ms, candle_duration_ms,
|
|
1060
|
+
visible_start_ms, window_ms) / area_w;
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// Left to right, the order a polyline wants. (The reveal below indexes from
|
|
1064
|
+
// the right instead, because that's what the morph machinery expects.)
|
|
1065
|
+
loading_line.clear();
|
|
1066
|
+
loading_line.reserve(target.n + 2);
|
|
1067
|
+
|
|
1068
|
+
// Spans the data doesn't reach — a short series in a wider window — are
|
|
1069
|
+
// sampled at the idle curve's own resolution and flattened to the nearest
|
|
1070
|
+
// candle's level. So the line still begins as the curve across the *whole*
|
|
1071
|
+
// plot and eases into a level run leading into the series; leaving those
|
|
1072
|
+
// spans out instead would snap the line's length on the first frame.
|
|
1073
|
+
const int across = vroom::loading_wave::sample_count(area_w);
|
|
1074
|
+
const float step = 1.f / static_cast<float>(across);
|
|
1075
|
+
const float first_xf = center_x_frac(target.visible[0]);
|
|
1076
|
+
const float last_xf = center_x_frac(target.visible[target.n - 1]);
|
|
1077
|
+
|
|
1078
|
+
for (int i = 0; static_cast<float>(i) * step < first_xf; ++i) {
|
|
1079
|
+
push(static_cast<float>(i) * step, center_frac(target.visible[0]));
|
|
1080
|
+
}
|
|
1081
|
+
for (std::size_t i = 0; i < target.n; ++i) {
|
|
1082
|
+
push(center_x_frac(target.visible[i]), center_frac(target.visible[i]));
|
|
1083
|
+
}
|
|
1084
|
+
for (int i = static_cast<int>(std::ceil(last_xf / step)); i <= across; ++i) {
|
|
1085
|
+
push(static_cast<float>(i) * step,
|
|
1086
|
+
center_frac(target.visible[target.n - 1]));
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
loading_line_t = 0.f;
|
|
1090
|
+
// `loading` stays set: the axes, price badge and panes have no business
|
|
1091
|
+
// appearing until the candles themselves do, in stage 3.
|
|
1092
|
+
mark_dirty();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
void VroomChart::begin_loading_reveal() {
|
|
1096
|
+
if (!loading) return;
|
|
1097
|
+
loading = false;
|
|
1098
|
+
|
|
1099
|
+
const auto lay = layout();
|
|
1100
|
+
const float area_w = vroom::candle_area_width(lay);
|
|
1101
|
+
const auto target = loading_target();
|
|
1102
|
+
if (target.n == 0 || area_w <= 0.f) {
|
|
1103
|
+
loading_line.clear();
|
|
1104
|
+
mark_dirty();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const int64_t window_ms = visible_end_ms - visible_start_ms;
|
|
1109
|
+
|
|
1110
|
+
// Right-to-left, matching how the morph machinery indexes slots (slot 0 =
|
|
1111
|
+
// newest bar at the right edge).
|
|
1112
|
+
morph_from.clear();
|
|
1113
|
+
morph_from.reserve(target.n);
|
|
1114
|
+
for (std::size_t slot = 0; slot < target.n; ++slot) {
|
|
1115
|
+
const ::VroomCandle& c = target.visible[target.n - 1 - slot];
|
|
1116
|
+
const float center = static_cast<float>(
|
|
1117
|
+
vroom::price_fraction(target.bounds, (c.high + c.low) * 0.5));
|
|
1118
|
+
|
|
1119
|
+
vroom::CandleSnapshot s{};
|
|
1120
|
+
s.x = vroom::candle_center_x(lay, c.time_ms, candle_duration_ms,
|
|
1121
|
+
visible_start_ms, window_ms) / area_w;
|
|
1122
|
+
// Every price collapsed onto the centre, so the slot starts as a
|
|
1123
|
+
// zero-height sliver sitting on the line and the existing lerp is what
|
|
1124
|
+
// grows it out to full open/high/low/close.
|
|
1125
|
+
s.open = s.high = s.low = s.close = center;
|
|
1126
|
+
s.bull = c.close >= c.open;
|
|
1127
|
+
// Starts the colour blend from fully transparent rather than from the
|
|
1128
|
+
// skeleton grey: the bar emerges from the line, so a grey body would
|
|
1129
|
+
// just smear the line thicker on the first frames.
|
|
1130
|
+
s.skeleton = true;
|
|
1131
|
+
s.skeleton_alpha = 0.f;
|
|
1132
|
+
morph_from.push_back(s);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
morph_from_bounds = target.bounds;
|
|
1136
|
+
interval_morph_t = 0.f;
|
|
1137
|
+
// Slot-lerp, not crossfade: each bar has a counterpart to grow out of, so
|
|
1138
|
+
// pairing column to column is what makes the line *become* the data instead
|
|
1139
|
+
// of one scene dissolving into another.
|
|
1140
|
+
interval_morph_fade = false;
|
|
1141
|
+
// Keeps the axes out of the interval envelope (see labels::interval_phase),
|
|
1142
|
+
// which is what the flag actually selects. A timeframe switch runs the
|
|
1143
|
+
// outgoing ticks out and the new ones in; here there are no outgoing ticks
|
|
1144
|
+
// — stages 1 and 2 draw no axis at all — so that envelope would hold the
|
|
1145
|
+
// real labels back for the first half of the reveal. Per-label fades bring
|
|
1146
|
+
// them up from nothing instead, in step with the bars.
|
|
1147
|
+
morph_is_stream = true;
|
|
1148
|
+
loading_line_revealing = true;
|
|
1149
|
+
mark_dirty();
|
|
596
1150
|
}
|
|
597
1151
|
|
|
598
1152
|
bool VroomChart::tip_pulse_active() const {
|
|
@@ -613,6 +1167,10 @@ bool VroomChart::is_animating_now() const {
|
|
|
613
1167
|
// The pulse never finishes on its own, so this is what keeps the host loops
|
|
614
1168
|
// requeueing frames for it.
|
|
615
1169
|
if (tip_pulse_active()) return true;
|
|
1170
|
+
// Same deal for the loading line's wave, except a still line (reduced
|
|
1171
|
+
// motion, once faded in) has nothing left to redraw and may go idle. The
|
|
1172
|
+
// two hand-off stages are host-driven, so they need nothing here.
|
|
1173
|
+
if (loading && (loading_animate || loading_fade_in < 1.f)) return true;
|
|
616
1174
|
for (const auto& f : y_fades) {
|
|
617
1175
|
if (f.opacity != f.target) return true;
|
|
618
1176
|
}
|