react-native-vroom-chart 0.15.0 → 0.16.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 +171 -1
- package/cpp/_core_include/vroom/vroom_chart.h +137 -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/chart.cpp +461 -172
- package/cpp/_core_src/chart.h +150 -1
- package/cpp/_core_src/chart_facade.cpp +209 -23
- 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/ma_overlay.cpp +259 -36
- package/cpp/_core_src/ma_overlay.h +57 -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/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/viewport.h +35 -0
- package/lib/index.d.mts +251 -3
- package/lib/index.d.ts +251 -3
- package/lib/index.js +224 -6
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +224 -6
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +21 -3
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +97 -1
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +284 -5
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
#include "chart.h"
|
|
22
22
|
#include "fonts.h"
|
|
23
|
+
#include "line_morph.h"
|
|
24
|
+
#include "macd.h"
|
|
25
|
+
#include "pane_series.h"
|
|
23
26
|
#include "style_inherit.h"
|
|
24
27
|
#include "theme.h"
|
|
25
28
|
#include "viewport.h"
|
|
@@ -34,7 +37,6 @@ constexpr SkColor kMacdLine = 0xff2962ff; // blue MACD line
|
|
|
34
37
|
constexpr SkColor kSignalLine = 0xffff6d00; // orange signal line
|
|
35
38
|
constexpr SkColor kZeroLine = 0xff484f58; // zero reference
|
|
36
39
|
constexpr SkColor kDivider = 0xff21262d; // pane separator
|
|
37
|
-
constexpr float kPadFrac = 0.85f; // keep curves off the band edges
|
|
38
40
|
constexpr float kLineWidth = 1.5f; // default stroke for both lines
|
|
39
41
|
constexpr float kFadedAlpha = 0.5f; // easing histogram bars
|
|
40
42
|
|
|
@@ -58,34 +60,42 @@ void draw(SkCanvas* canvas,
|
|
|
58
60
|
int64_t candle_duration_ms,
|
|
59
61
|
float candle_right,
|
|
60
62
|
float pane_top,
|
|
61
|
-
float pane_bottom
|
|
62
|
-
|
|
63
|
+
float pane_bottom,
|
|
64
|
+
const vroom::LineMorph* macd_from,
|
|
65
|
+
const vroom::LineMorph* signal_from,
|
|
66
|
+
const vroom::LineMorph* hist_from,
|
|
67
|
+
float morph_t) {
|
|
68
|
+
if (!canvas || candle_right <= 0.f) return;
|
|
63
69
|
const float band_h = pane_bottom - pane_top;
|
|
64
70
|
if (band_h <= 0.f) return;
|
|
71
|
+
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
72
|
+
// A fade's outgoing half has no new data at all, so the captures are the
|
|
73
|
+
// whole frame. Nothing on either side means nothing to paint, shell
|
|
74
|
+
// included.
|
|
75
|
+
const std::size_t hist_slots =
|
|
76
|
+
vroom::pane_series::slots(n, hist_from, morph_t);
|
|
77
|
+
if (n == 0 && hist_slots == 0 &&
|
|
78
|
+
vroom::morph_line_count(macd_from, morph_t) == 0 &&
|
|
79
|
+
vroom::morph_line_count(signal_from, morph_t) == 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
65
82
|
|
|
66
83
|
const VroomMACD& cfg = chart.macd;
|
|
67
84
|
const float mid = (pane_top + pane_bottom) * 0.5f;
|
|
68
|
-
// User y-zoom scales the amplitude about the zero line (mid). 1.0 = the
|
|
69
|
-
// default auto-fit; >1 zooms in, <1 zooms out.
|
|
70
|
-
const float half =
|
|
71
|
-
band_h * 0.5f * kPadFrac * static_cast<float>(chart.macd_y_scale);
|
|
72
85
|
|
|
73
86
|
// Auto-scale symmetric about zero across the finite values on show, so
|
|
74
|
-
// hiding a series lets the rest fill the pane.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
track(macd_visible, cfg.line_visible != 0);
|
|
83
|
-
track(signal_visible, cfg.signal_visible != 0);
|
|
84
|
-
track(hist_visible, cfg.hist_visible != 0);
|
|
87
|
+
// hiding a series lets the rest fill the pane. User y-zoom scales the
|
|
88
|
+
// amplitude about the zero line; 1.0 is the default auto-fit.
|
|
89
|
+
const double scale = vroom::macd::autoscale(
|
|
90
|
+
cfg.line_visible ? macd_visible : nullptr,
|
|
91
|
+
cfg.signal_visible ? signal_visible : nullptr,
|
|
92
|
+
cfg.hist_visible ? hist_visible : nullptr, n);
|
|
85
93
|
|
|
86
94
|
auto y_for = [&](double v) -> float {
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
return pane_bottom -
|
|
96
|
+
static_cast<float>(
|
|
97
|
+
vroom::macd::band_fraction(v, scale, chart.macd_y_scale)) *
|
|
98
|
+
band_h;
|
|
89
99
|
};
|
|
90
100
|
|
|
91
101
|
// Mask the band (candles can overflow below the shortened price pane).
|
|
@@ -115,7 +125,9 @@ void draw(SkCanvas* canvas,
|
|
|
115
125
|
// Histogram bars: from the zero line to y_for(hist), 4-color — above or
|
|
116
126
|
// below zero, each in a building and an easing shade. A bar is building
|
|
117
127
|
// while it grows away from zero and easing while it falls back toward it.
|
|
118
|
-
if (hist_visible && cfg.hist_visible) {
|
|
128
|
+
if ((hist_visible || hist_from) && cfg.hist_visible) {
|
|
129
|
+
// The new resolution's width from the first frame, matching how
|
|
130
|
+
// candles::draw widens its bodies while their positions still slide.
|
|
119
131
|
const float body_w =
|
|
120
132
|
vroom::candle_body_width(lay, window_ms, candle_duration_ms);
|
|
121
133
|
const float half_body = body_w * 0.5f;
|
|
@@ -126,68 +138,65 @@ void draw(SkCanvas* canvas,
|
|
|
126
138
|
color_or(cfg.hist_down_color, chart.theme.colors[VROOM_COLOR_ACCENT_BEAR]);
|
|
127
139
|
const SkColor up_easing = faded_or(cfg.hist_up_fading_color, up);
|
|
128
140
|
const SkColor down_easing = faded_or(cfg.hist_down_fading_color, down);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
const std::size_t hist_from_count =
|
|
142
|
+
vroom::morph_line_count(hist_from, morph_t);
|
|
143
|
+
// Bars read their sign and direction off the drawn geometry rather than
|
|
144
|
+
// the values: mid-morph a top is a blend of two resolutions' numbers,
|
|
145
|
+
// and only where it ended up says which side of zero it is on. y grows
|
|
146
|
+
// downward, so above zero is the smaller y.
|
|
147
|
+
MorphVertex prev;
|
|
148
|
+
for (std::size_t j = 0; j < hist_slots; ++j) {
|
|
149
|
+
const MorphVertex p = vroom::pane_series::vertex(
|
|
150
|
+
lay, visible, n, hist_visible, window_ms, visible_start_ms,
|
|
151
|
+
candle_duration_ms, pane_top, pane_bottom, hist_from,
|
|
152
|
+
hist_from_count, morph_t, hist_slots - 1 - j, y_for);
|
|
153
|
+
if (!p.valid) {
|
|
154
|
+
prev = MorphVertex{};
|
|
155
|
+
continue;
|
|
137
156
|
}
|
|
157
|
+
const bool above = p.y <= zero_y;
|
|
158
|
+
// Building while the bar grows away from zero, easing while it
|
|
159
|
+
// falls back toward it.
|
|
160
|
+
const bool building =
|
|
161
|
+
!prev.valid || (above ? p.y <= prev.y : p.y >= prev.y);
|
|
138
162
|
SkPaint bar;
|
|
139
163
|
bar.setAntiAlias(true);
|
|
140
|
-
if (
|
|
164
|
+
if (above) {
|
|
141
165
|
bar.setColor(building ? up : up_easing);
|
|
142
166
|
} else {
|
|
143
167
|
bar.setColor(building ? down : down_easing);
|
|
144
168
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
canvas->drawRect(
|
|
152
|
-
SkRect::MakeLTRB(cx - half_body, top, cx + half_body, bot), bar);
|
|
169
|
+
canvas->drawRect(SkRect::MakeLTRB(p.x - half_body,
|
|
170
|
+
std::min(zero_y, p.y),
|
|
171
|
+
p.x + half_body,
|
|
172
|
+
std::max(zero_y, p.y)),
|
|
173
|
+
bar);
|
|
174
|
+
prev = p;
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
|
|
156
178
|
// MACD + signal lines.
|
|
157
|
-
auto stroke_series = [&](const double* series,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
bool pen_down = false;
|
|
161
|
-
for (std::size_t i = 0; i < n; ++i) {
|
|
162
|
-
const double v = series[i];
|
|
163
|
-
if (!std::isfinite(v)) {
|
|
164
|
-
pen_down = false;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const float x = vroom::candle_center_x(
|
|
168
|
-
lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
|
|
169
|
-
window_ms);
|
|
170
|
-
const float y = y_for(v);
|
|
171
|
-
if (pen_down) {
|
|
172
|
-
path.lineTo(x, y);
|
|
173
|
-
} else {
|
|
174
|
-
path.moveTo(x, y);
|
|
175
|
-
pen_down = true;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
179
|
+
auto stroke_series = [&](const double* series, const vroom::LineMorph* from,
|
|
180
|
+
SkColor color, float width) {
|
|
181
|
+
if (!series && !from) return;
|
|
178
182
|
SkPaint line;
|
|
179
183
|
line.setAntiAlias(true);
|
|
180
184
|
line.setColor(color);
|
|
181
185
|
line.setStyle(SkPaint::kStroke_Style);
|
|
182
186
|
line.setStrokeWidth(width);
|
|
183
|
-
canvas->drawPath(
|
|
187
|
+
canvas->drawPath(
|
|
188
|
+
vroom::pane_series::build_path(
|
|
189
|
+
lay, visible, n, series, window_ms, visible_start_ms,
|
|
190
|
+
candle_duration_ms, pane_top, pane_bottom, from, morph_t, y_for),
|
|
191
|
+
line);
|
|
184
192
|
};
|
|
185
193
|
if (cfg.signal_visible) {
|
|
186
|
-
stroke_series(signal_visible,
|
|
194
|
+
stroke_series(signal_visible, signal_from,
|
|
195
|
+
color_or(cfg.signal_color, kSignalLine),
|
|
187
196
|
width_or(cfg.signal_width, kLineWidth));
|
|
188
197
|
}
|
|
189
198
|
if (cfg.line_visible) { // MACD over signal
|
|
190
|
-
stroke_series(macd_visible, color_or(cfg.line_color, kMacdLine),
|
|
199
|
+
stroke_series(macd_visible, macd_from, color_or(cfg.line_color, kMacdLine),
|
|
191
200
|
width_or(cfg.line_width, kLineWidth));
|
|
192
201
|
}
|
|
193
202
|
|
|
@@ -13,6 +13,7 @@ class SkCanvas;
|
|
|
13
13
|
|
|
14
14
|
namespace vroom {
|
|
15
15
|
struct Layout;
|
|
16
|
+
struct LineMorph;
|
|
16
17
|
} // namespace vroom
|
|
17
18
|
|
|
18
19
|
struct VroomChart;
|
|
@@ -23,6 +24,13 @@ namespace vroom::macd_pane {
|
|
|
23
24
|
// series are aligned with `visible` (NaN where undefined). The vertical scale
|
|
24
25
|
// auto-fits the visible values symmetrically about zero; shares the candles'
|
|
25
26
|
// horizontal mapping (candle_center_x) so it scrolls in lock-step.
|
|
27
|
+
//
|
|
28
|
+
// The three `*_from` captures plus `morph_t` are the interval morph: each holds
|
|
29
|
+
// its series' outgoing shape in fractions of the band, indexed from the right
|
|
30
|
+
// (slot 0 = newest), so the pane can re-fit across the switch without the shape
|
|
31
|
+
// moving. Histogram bars reshape the way candle bodies do — their tops slide
|
|
32
|
+
// while the zero line, which is fixed to the band center, stays put. A fade's
|
|
33
|
+
// outgoing half passes n = 0 with morph_t = 0, painting the captures alone.
|
|
26
34
|
void draw(SkCanvas* canvas,
|
|
27
35
|
const VroomChart& chart,
|
|
28
36
|
const Layout& lay,
|
|
@@ -36,6 +44,10 @@ void draw(SkCanvas* canvas,
|
|
|
36
44
|
int64_t candle_duration_ms,
|
|
37
45
|
float candle_right,
|
|
38
46
|
float pane_top,
|
|
39
|
-
float pane_bottom
|
|
47
|
+
float pane_bottom,
|
|
48
|
+
const LineMorph* macd_from = nullptr,
|
|
49
|
+
const LineMorph* signal_from = nullptr,
|
|
50
|
+
const LineMorph* hist_from = nullptr,
|
|
51
|
+
float morph_t = 1.f);
|
|
40
52
|
|
|
41
53
|
} // namespace vroom::macd_pane
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Polyline plotting shared by the indicator panes (RSI, MACD, ATR), including
|
|
2
|
+
// the interval morph.
|
|
3
|
+
//
|
|
4
|
+
// Each pane maps values into its own band with its own auto-fit, so it hands
|
|
5
|
+
// over its y mapping and everything from the slot pairing down is identical
|
|
6
|
+
// across the three. The capture holds band *fractions* rather than values,
|
|
7
|
+
// which is what lets a pane re-fit across a timeframe switch without its
|
|
8
|
+
// outgoing shape moving.
|
|
9
|
+
|
|
10
|
+
#pragma once
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <cmath>
|
|
14
|
+
#include <cstddef>
|
|
15
|
+
#include <cstdint>
|
|
16
|
+
|
|
17
|
+
#pragma clang diagnostic push
|
|
18
|
+
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
19
|
+
#include "include/core/SkPath.h"
|
|
20
|
+
#include "include/core/SkPathBuilder.h"
|
|
21
|
+
#pragma clang diagnostic pop
|
|
22
|
+
|
|
23
|
+
#include "line_morph.h"
|
|
24
|
+
#include "viewport.h"
|
|
25
|
+
#include "vroom/vroom_chart.h"
|
|
26
|
+
|
|
27
|
+
namespace vroom::pane_series {
|
|
28
|
+
|
|
29
|
+
// Slot `k` of a pane series in screen pixels, blended from the capture toward
|
|
30
|
+
// the new value. Slot 0 is the rightmost vertex on both sides — the pairing a
|
|
31
|
+
// timeframe switch preserves, the same one candles::draw uses.
|
|
32
|
+
template <typename YFor>
|
|
33
|
+
MorphVertex vertex(const Layout& lay,
|
|
34
|
+
const ::VroomCandle* visible,
|
|
35
|
+
std::size_t n,
|
|
36
|
+
const double* values,
|
|
37
|
+
int64_t window_ms,
|
|
38
|
+
int64_t visible_start_ms,
|
|
39
|
+
int64_t candle_duration_ms,
|
|
40
|
+
float pane_top,
|
|
41
|
+
float pane_bottom,
|
|
42
|
+
const LineMorph* from,
|
|
43
|
+
std::size_t from_count,
|
|
44
|
+
float morph_t,
|
|
45
|
+
std::size_t k,
|
|
46
|
+
YFor&& y_for) {
|
|
47
|
+
MorphVertex to;
|
|
48
|
+
if (values && k < n) {
|
|
49
|
+
const std::size_t i = n - 1 - k;
|
|
50
|
+
const double v = values[i];
|
|
51
|
+
if (std::isfinite(v)) {
|
|
52
|
+
to = MorphVertex{
|
|
53
|
+
vroom::candle_center_x(lay, visible[i].time_ms,
|
|
54
|
+
candle_duration_ms, visible_start_ms,
|
|
55
|
+
window_ms),
|
|
56
|
+
y_for(v), true};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
MorphVertex frm;
|
|
60
|
+
if (k < from_count) {
|
|
61
|
+
const LineSnapshot& s = from->pts[k];
|
|
62
|
+
if (s.valid) {
|
|
63
|
+
frm = MorphVertex{s.x * vroom::candle_area_width(lay),
|
|
64
|
+
pane_bottom - s.y * (pane_bottom - pane_top),
|
|
65
|
+
true};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return vroom::morph_vertex(to, frm, morph_t);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// How many vertices a morphing series spans: the new slice and the capture can
|
|
72
|
+
// disagree on length, and every slot either defines has to be walked.
|
|
73
|
+
inline std::size_t slots(std::size_t n, const LineMorph* from, float morph_t) {
|
|
74
|
+
return std::max(n, vroom::morph_line_count(from, morph_t));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The series as a polyline, walked left to right. A slot neither side defines
|
|
78
|
+
// lifts the pen, which is what leaves the warmup gap open.
|
|
79
|
+
template <typename YFor>
|
|
80
|
+
SkPath build_path(const Layout& lay,
|
|
81
|
+
const ::VroomCandle* visible,
|
|
82
|
+
std::size_t n,
|
|
83
|
+
const double* values,
|
|
84
|
+
int64_t window_ms,
|
|
85
|
+
int64_t visible_start_ms,
|
|
86
|
+
int64_t candle_duration_ms,
|
|
87
|
+
float pane_top,
|
|
88
|
+
float pane_bottom,
|
|
89
|
+
const LineMorph* from,
|
|
90
|
+
float morph_t,
|
|
91
|
+
YFor&& y_for) {
|
|
92
|
+
const std::size_t from_count = vroom::morph_line_count(from, morph_t);
|
|
93
|
+
const std::size_t count = std::max(n, from_count);
|
|
94
|
+
SkPathBuilder path;
|
|
95
|
+
bool pen_down = false;
|
|
96
|
+
for (std::size_t j = 0; j < count; ++j) {
|
|
97
|
+
const MorphVertex p =
|
|
98
|
+
vertex(lay, visible, n, values, window_ms, visible_start_ms,
|
|
99
|
+
candle_duration_ms, pane_top, pane_bottom, from, from_count,
|
|
100
|
+
morph_t, count - 1 - j, y_for);
|
|
101
|
+
if (!p.valid) {
|
|
102
|
+
pen_down = false;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (pen_down) {
|
|
106
|
+
path.lineTo(p.x, p.y);
|
|
107
|
+
} else {
|
|
108
|
+
path.moveTo(p.x, p.y);
|
|
109
|
+
pen_down = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return path.detach();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The region between the series and a horizontal rule at `y_band`, as the
|
|
116
|
+
// closed counterpart to build_path. Callers clip to one side of the rule to keep
|
|
117
|
+
// only the stretches that reach past it, which puts the crossings exactly where
|
|
118
|
+
// Skia cuts the geometry rather than anywhere this has to solve for.
|
|
119
|
+
//
|
|
120
|
+
// Each run of vertices gets its own contour, tied down to the rule at both
|
|
121
|
+
// ends. One contour spanning the whole series would close a warmup gap by
|
|
122
|
+
// running straight through it, filling a span the line never drew.
|
|
123
|
+
template <typename YFor>
|
|
124
|
+
SkPath build_band_area(const Layout& lay,
|
|
125
|
+
const ::VroomCandle* visible,
|
|
126
|
+
std::size_t n,
|
|
127
|
+
const double* values,
|
|
128
|
+
int64_t window_ms,
|
|
129
|
+
int64_t visible_start_ms,
|
|
130
|
+
int64_t candle_duration_ms,
|
|
131
|
+
float pane_top,
|
|
132
|
+
float pane_bottom,
|
|
133
|
+
const LineMorph* from,
|
|
134
|
+
float morph_t,
|
|
135
|
+
float y_band,
|
|
136
|
+
YFor&& y_for) {
|
|
137
|
+
const std::size_t from_count = vroom::morph_line_count(from, morph_t);
|
|
138
|
+
const std::size_t count = std::max(n, from_count);
|
|
139
|
+
SkPathBuilder path;
|
|
140
|
+
bool open = false;
|
|
141
|
+
float last_x = 0.f;
|
|
142
|
+
for (std::size_t j = 0; j < count; ++j) {
|
|
143
|
+
const MorphVertex p =
|
|
144
|
+
vertex(lay, visible, n, values, window_ms, visible_start_ms,
|
|
145
|
+
candle_duration_ms, pane_top, pane_bottom, from, from_count,
|
|
146
|
+
morph_t, count - 1 - j, y_for);
|
|
147
|
+
if (!p.valid) {
|
|
148
|
+
if (open) {
|
|
149
|
+
path.lineTo(last_x, y_band);
|
|
150
|
+
path.close();
|
|
151
|
+
open = false;
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!open) {
|
|
156
|
+
path.moveTo(p.x, y_band);
|
|
157
|
+
open = true;
|
|
158
|
+
}
|
|
159
|
+
path.lineTo(p.x, p.y);
|
|
160
|
+
last_x = p.x;
|
|
161
|
+
}
|
|
162
|
+
if (open) {
|
|
163
|
+
path.lineTo(last_x, y_band);
|
|
164
|
+
path.close();
|
|
165
|
+
}
|
|
166
|
+
return path.detach();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} // namespace vroom::pane_series
|
package/cpp/_core_src/rsi.cpp
CHANGED
|
@@ -53,4 +53,8 @@ void compute_ma(const std::vector<double>& rsi, int ma_period, int kind,
|
|
|
53
53
|
vroom::series_ma::smooth(rsi, kind, ma_period, out);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
double band_fraction(double v, double y_scale) {
|
|
57
|
+
return 0.5 + ((v - 50.0) / 100.0) * y_scale;
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
} // namespace vroom::rsi
|
package/cpp/_core_src/rsi.h
CHANGED
|
@@ -29,4 +29,11 @@ void compute(const ::VroomCandle* candles, std::size_t n, int period,
|
|
|
29
29
|
void compute_ma(const std::vector<double>& rsi, int ma_period, int kind,
|
|
30
30
|
std::vector<double>& out);
|
|
31
31
|
|
|
32
|
+
// Where an RSI value sits in the pane band, as a fraction of its height — 0 at
|
|
33
|
+
// the bottom edge, 1 at the top. The fixed 0..100 domain maps about the band
|
|
34
|
+
// center so the user's y-axis zoom (`y_scale`, 1 = the default fit) stretches
|
|
35
|
+
// symmetrically around 50. Split out of the renderer so the interval-morph
|
|
36
|
+
// capture maps its geometry through the same math.
|
|
37
|
+
double band_fraction(double v, double y_scale);
|
|
38
|
+
|
|
32
39
|
} // namespace vroom::rsi
|
|
@@ -9,17 +9,24 @@
|
|
|
9
9
|
#include "include/core/SkPaint.h"
|
|
10
10
|
#include "include/core/SkPath.h"
|
|
11
11
|
#include "include/core/SkPathBuilder.h"
|
|
12
|
+
#include "include/core/SkPoint.h"
|
|
12
13
|
#include "include/core/SkRect.h"
|
|
14
|
+
#include "include/core/SkShader.h"
|
|
13
15
|
#include "include/core/SkTypeface.h"
|
|
14
16
|
#include "include/effects/SkDashPathEffect.h"
|
|
15
17
|
#pragma clang diagnostic pop
|
|
16
18
|
|
|
19
|
+
#include <algorithm>
|
|
17
20
|
#include <cmath>
|
|
18
21
|
#include <cstdio>
|
|
19
22
|
#include <cstring>
|
|
20
23
|
|
|
21
24
|
#include "chart.h"
|
|
22
25
|
#include "fonts.h"
|
|
26
|
+
#include "gradient.h"
|
|
27
|
+
#include "line_morph.h"
|
|
28
|
+
#include "pane_series.h"
|
|
29
|
+
#include "rsi.h"
|
|
23
30
|
#include "style_inherit.h"
|
|
24
31
|
#include "theme.h"
|
|
25
32
|
#include "viewport.h"
|
|
@@ -36,6 +43,11 @@ constexpr SkColor kRefLine = 0xff30363d; // band reference lines
|
|
|
36
43
|
constexpr SkColor kDivider = 0xff21262d; // pane separator
|
|
37
44
|
constexpr SkScalar kDash[2] = {3.f, 3.f};
|
|
38
45
|
constexpr float kLineWidth = 1.5f; // default stroke for both lines
|
|
46
|
+
// Alpha the extreme shading reaches at the end of the scale. Only a reading
|
|
47
|
+
// pinned at 100 or 0 collects all of it, which is why this sits higher than the
|
|
48
|
+
// wash typically paints. Still short of opaque: the RSI line and the band rule
|
|
49
|
+
// sit on top of it and have to stay readable at its deepest.
|
|
50
|
+
constexpr float kExtremeFillAlpha = 0.7f;
|
|
39
51
|
} // namespace
|
|
40
52
|
|
|
41
53
|
void draw(SkCanvas* canvas,
|
|
@@ -50,19 +62,31 @@ void draw(SkCanvas* canvas,
|
|
|
50
62
|
int64_t candle_duration_ms,
|
|
51
63
|
float candle_right,
|
|
52
64
|
float pane_top,
|
|
53
|
-
float pane_bottom
|
|
54
|
-
|
|
65
|
+
float pane_bottom,
|
|
66
|
+
const vroom::LineMorph* rsi_from,
|
|
67
|
+
const vroom::LineMorph* rsi_ma_from,
|
|
68
|
+
float morph_t) {
|
|
69
|
+
if (!canvas || candle_right <= 0.f) return;
|
|
55
70
|
const float band_h = pane_bottom - pane_top;
|
|
56
71
|
if (band_h <= 0.f) return;
|
|
72
|
+
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
73
|
+
// A fade's outgoing half has no new data at all, so the captures are the
|
|
74
|
+
// whole frame. Nothing on either side means nothing to paint, shell
|
|
75
|
+
// included.
|
|
76
|
+
if (n == 0 && vroom::morph_line_count(rsi_from, morph_t) == 0 &&
|
|
77
|
+
vroom::morph_line_count(rsi_ma_from, morph_t) == 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
57
80
|
|
|
58
81
|
// Map 0..100 about the pane center so the user y-zoom scales symmetrically
|
|
59
82
|
// around 50. z == 1 reproduces the default fit (0 -> pane_bottom, 100 ->
|
|
60
83
|
// pane_top); z > 1 zooms in (taller), z < 1 zooms out (flatter).
|
|
61
84
|
const VroomRSI& cfg = chart.rsi;
|
|
62
|
-
const float center_y = (pane_top + pane_bottom) * 0.5f;
|
|
63
|
-
const float z = static_cast<float>(chart.rsi_y_scale);
|
|
64
85
|
auto y_for = [&](double v) -> float {
|
|
65
|
-
return
|
|
86
|
+
return pane_bottom -
|
|
87
|
+
static_cast<float>(
|
|
88
|
+
vroom::rsi::band_fraction(v, chart.rsi_y_scale)) *
|
|
89
|
+
band_h;
|
|
66
90
|
};
|
|
67
91
|
|
|
68
92
|
// Mask the band with the background so candles/volume that overflow below
|
|
@@ -86,6 +110,52 @@ void draw(SkCanvas* canvas,
|
|
|
86
110
|
// Configurable band reference lines (overbought / oversold).
|
|
87
111
|
const float y_upper = y_for(cfg.upper_band);
|
|
88
112
|
const float y_lower = y_for(cfg.lower_band);
|
|
113
|
+
|
|
114
|
+
// Shade the stretches that reach past a band, fading out at the rule itself
|
|
115
|
+
// and deepening toward the end of the scale — so how far the reading went
|
|
116
|
+
// past the threshold reads at a glance, not just that it did.
|
|
117
|
+
//
|
|
118
|
+
// Drawn before the rules and the lines so both stay legible on top. The
|
|
119
|
+
// region is built across the whole series and then clipped to the far side
|
|
120
|
+
// of the rule, which lands the crossings exactly without solving for them.
|
|
121
|
+
auto fill_extreme = [&](double band, double limit, SkColor base,
|
|
122
|
+
bool above) {
|
|
123
|
+
// The ramp spans band -> limit. Pinned together (upper at 100, lower at
|
|
124
|
+
// 0) there is nothing to ramp through, and Skia would clamp the
|
|
125
|
+
// zero-length gradient to its end color: a flat slab at full alpha.
|
|
126
|
+
if (band == limit) return;
|
|
127
|
+
const float y_band = y_for(band);
|
|
128
|
+
const SkPath area = vroom::pane_series::build_band_area(
|
|
129
|
+
lay, visible, n, rsi_visible, window_ms, visible_start_ms,
|
|
130
|
+
candle_duration_ms, pane_top, pane_bottom, rsi_from, morph_t,
|
|
131
|
+
y_band, y_for);
|
|
132
|
+
if (area.isEmpty()) return;
|
|
133
|
+
|
|
134
|
+
// Anchored to the RSI value rather than the pane edge, so the shading
|
|
135
|
+
// means the same thing at any y-zoom and a shallow excursion stays
|
|
136
|
+
// faint instead of saturating on a short pane.
|
|
137
|
+
const SkPoint pts[2] = {SkPoint::Make(0.f, y_band),
|
|
138
|
+
SkPoint::Make(0.f, y_for(limit))};
|
|
139
|
+
SkPaint fill;
|
|
140
|
+
fill.setAntiAlias(true);
|
|
141
|
+
fill.setStyle(SkPaint::kFill_Style);
|
|
142
|
+
fill.setShader(
|
|
143
|
+
vroom::linear_alpha_ramp(pts, base, 0.f, kExtremeFillAlpha));
|
|
144
|
+
|
|
145
|
+
canvas->save();
|
|
146
|
+
canvas->clipRect(
|
|
147
|
+
above ? SkRect::MakeLTRB(0.f, pane_top, candle_right, y_band)
|
|
148
|
+
: SkRect::MakeLTRB(0.f, y_band, candle_right, pane_bottom));
|
|
149
|
+
canvas->drawPath(area, fill);
|
|
150
|
+
canvas->restore();
|
|
151
|
+
};
|
|
152
|
+
if (cfg.extreme_fill) {
|
|
153
|
+
fill_extreme(cfg.upper_band, 100.0,
|
|
154
|
+
chart.theme.colors[VROOM_COLOR_ACCENT_BULL], true);
|
|
155
|
+
fill_extreme(cfg.lower_band, 0.0,
|
|
156
|
+
chart.theme.colors[VROOM_COLOR_ACCENT_BEAR], false);
|
|
157
|
+
}
|
|
158
|
+
|
|
89
159
|
if (cfg.bands_visible) {
|
|
90
160
|
SkPaint ref;
|
|
91
161
|
ref.setAntiAlias(true);
|
|
@@ -98,41 +168,27 @@ void draw(SkCanvas* canvas,
|
|
|
98
168
|
|
|
99
169
|
// Strokes a value series as a polyline; a NaN lifts the pen so undefined
|
|
100
170
|
// leading values (i < period) leave a gap.
|
|
101
|
-
auto stroke_series = [&](const double* series,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
bool pen_down = false;
|
|
105
|
-
for (std::size_t i = 0; i < n; ++i) {
|
|
106
|
-
const double v = series[i];
|
|
107
|
-
if (!std::isfinite(v)) {
|
|
108
|
-
pen_down = false;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const float x = vroom::candle_center_x(
|
|
112
|
-
lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
|
|
113
|
-
window_ms);
|
|
114
|
-
const float y = y_for(v);
|
|
115
|
-
if (pen_down) {
|
|
116
|
-
path.lineTo(x, y);
|
|
117
|
-
} else {
|
|
118
|
-
path.moveTo(x, y);
|
|
119
|
-
pen_down = true;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
171
|
+
auto stroke_series = [&](const double* series, const vroom::LineMorph* from,
|
|
172
|
+
SkColor color, float width) {
|
|
173
|
+
if (!series && !from) return;
|
|
122
174
|
SkPaint line;
|
|
123
175
|
line.setAntiAlias(true);
|
|
124
176
|
line.setColor(color);
|
|
125
177
|
line.setStyle(SkPaint::kStroke_Style);
|
|
126
178
|
line.setStrokeWidth(width);
|
|
127
|
-
canvas->drawPath(
|
|
179
|
+
canvas->drawPath(
|
|
180
|
+
vroom::pane_series::build_path(
|
|
181
|
+
lay, visible, n, series, window_ms, visible_start_ms,
|
|
182
|
+
candle_duration_ms, pane_top, pane_bottom, from, morph_t, y_for),
|
|
183
|
+
line);
|
|
128
184
|
};
|
|
129
185
|
if (cfg.line_visible) {
|
|
130
|
-
stroke_series(rsi_visible, color_or(cfg.line_color, kRsiLine),
|
|
186
|
+
stroke_series(rsi_visible, rsi_from, color_or(cfg.line_color, kRsiLine),
|
|
131
187
|
width_or(cfg.line_width, kLineWidth));
|
|
132
188
|
}
|
|
133
189
|
// Trendline on top. Its own visibility is handled upstream: a hidden
|
|
134
190
|
// trendline isn't computed, so the caller passes null.
|
|
135
|
-
stroke_series(rsi_ma_visible, color_or(cfg.ma_color, kRsiMaLine),
|
|
191
|
+
stroke_series(rsi_ma_visible, rsi_ma_from, color_or(cfg.ma_color, kRsiMaLine),
|
|
136
192
|
width_or(cfg.ma_width, kLineWidth));
|
|
137
193
|
|
|
138
194
|
// Caption, top-left of the pane.
|
package/cpp/_core_src/rsi_pane.h
CHANGED
|
@@ -13,6 +13,7 @@ class SkCanvas;
|
|
|
13
13
|
|
|
14
14
|
namespace vroom {
|
|
15
15
|
struct Layout;
|
|
16
|
+
struct LineMorph;
|
|
16
17
|
} // namespace vroom
|
|
17
18
|
|
|
18
19
|
struct VroomChart;
|
|
@@ -22,6 +23,12 @@ namespace vroom::rsi_pane {
|
|
|
22
23
|
// Draws the RSI pane spanning vertically [pane_top, pane_bottom]. `rsi_visible`
|
|
23
24
|
// is the cached RSI series aligned with `visible` (NaN where undefined). Shares
|
|
24
25
|
// the candles' horizontal mapping (candle_center_x) so it scrolls in lock-step.
|
|
26
|
+
//
|
|
27
|
+
// `rsi_from` / `rsi_ma_from` / `morph_t` are the interval morph: each capture
|
|
28
|
+
// holds its line's outgoing shape indexed from the right (slot 0 = newest) and
|
|
29
|
+
// every vertex slides from where it sat before the timeframe switch to where it
|
|
30
|
+
// sits now. A fade's outgoing half passes n = 0 with morph_t = 0, which paints
|
|
31
|
+
// the captures alone.
|
|
25
32
|
void draw(SkCanvas* canvas,
|
|
26
33
|
const VroomChart& chart,
|
|
27
34
|
const Layout& lay,
|
|
@@ -34,6 +41,9 @@ void draw(SkCanvas* canvas,
|
|
|
34
41
|
int64_t candle_duration_ms,
|
|
35
42
|
float candle_right,
|
|
36
43
|
float pane_top,
|
|
37
|
-
float pane_bottom
|
|
44
|
+
float pane_bottom,
|
|
45
|
+
const LineMorph* rsi_from = nullptr,
|
|
46
|
+
const LineMorph* rsi_ma_from = nullptr,
|
|
47
|
+
float morph_t = 1.f);
|
|
38
48
|
|
|
39
49
|
} // namespace vroom::rsi_pane
|
package/cpp/_core_src/viewport.h
CHANGED
|
@@ -108,6 +108,41 @@ inline std::size_t morph_from_count(const CandleSnapshot* from,
|
|
|
108
108
|
// anything else = fade the outgoing snapshot out then the new scene in.
|
|
109
109
|
inline bool interval_morph_is_fade(int32_t mode) { return mode != 0; }
|
|
110
110
|
|
|
111
|
+
// Rewrites a fresh capture so it starts from the shape currently on screen
|
|
112
|
+
// rather than from the data underneath it. Live ticks arrive faster than a
|
|
113
|
+
// morph lands, so every restart interrupts one; without this the bar would jump
|
|
114
|
+
// back to its un-morphed position on each tick, which is the snap the animation
|
|
115
|
+
// exists to remove.
|
|
116
|
+
//
|
|
117
|
+
// `interrupted` is the capture being replaced and `morph_t` the progress it had
|
|
118
|
+
// reached, so `dst` becomes exactly the frame that was being painted: the draw
|
|
119
|
+
// path interpolates in this same fraction space (see candles::draw), which is
|
|
120
|
+
// what makes a plain lerp here land pixel-exact.
|
|
121
|
+
//
|
|
122
|
+
// Slot 0 is the newest in both, so a differing visible count only drops the
|
|
123
|
+
// oldest slots — those keep the fresh capture, which is where they already are.
|
|
124
|
+
inline void blend_candle_snapshots(CandleSnapshot* dst, std::size_t dst_n,
|
|
125
|
+
const CandleSnapshot* interrupted,
|
|
126
|
+
std::size_t interrupted_n, float morph_t) {
|
|
127
|
+
if (!dst || !interrupted) return;
|
|
128
|
+
const std::size_t n = dst_n < interrupted_n ? dst_n : interrupted_n;
|
|
129
|
+
for (std::size_t k = 0; k < n; ++k) {
|
|
130
|
+
const CandleSnapshot& from = interrupted[k];
|
|
131
|
+
CandleSnapshot& to = dst[k];
|
|
132
|
+
const auto mix = [morph_t](float a, float b) {
|
|
133
|
+
return a + (b - a) * morph_t;
|
|
134
|
+
};
|
|
135
|
+
to.open = mix(from.open, to.open);
|
|
136
|
+
to.high = mix(from.high, to.high);
|
|
137
|
+
to.low = mix(from.low, to.low);
|
|
138
|
+
to.close = mix(from.close, to.close);
|
|
139
|
+
to.x = mix(from.x, to.x);
|
|
140
|
+
// `bull` stays the fresh one: the draw path colors a paired slot from
|
|
141
|
+
// the live candle and only reads the capture's flag for a slot the next
|
|
142
|
+
// update drops, where the newer direction is the better answer.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
111
146
|
// Returns the indices of candles whose time_ms falls in [start_ms, end_ms].
|
|
112
147
|
// When both are 0, returns the full range (Phase 1 default-everything behavior).
|
|
113
148
|
// Candles must be sorted ascending by time_ms (invariant of the public API).
|