react-native-vroom-chart 0.5.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/android/CMakeLists.txt +143 -0
- package/android/README.md +60 -5
- package/android/build.gradle +115 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/VroomChartJni.cpp +24 -0
- package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
- package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
- package/cpp/VroomChartHostObject.cpp +368 -33
- package/cpp/VroomJsiInstaller.cpp +11 -1
- package/cpp/VroomSkiaContext.cpp +15 -7
- package/cpp/_core_include/vroom/vroom_chart.h +250 -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 +98 -40
- package/cpp/_core_src/chart.h +82 -20
- package/cpp/_core_src/chart_facade.cpp +297 -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.cpp +87 -0
- package/cpp/_core_src/price_line_layout.h +80 -0
- package/cpp/_core_src/price_lines.cpp +428 -0
- package/cpp/_core_src/price_lines.h +56 -0
- 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 +261 -25
- package/lib/index.d.ts +261 -25
- package/lib/index.js +252 -36
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +252 -36
- package/lib/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/VroomChart.tsx +162 -11
- package/src/easing.ts +40 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +124 -20
- package/src/theme.ts +1 -0
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +166 -28
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#include "price_lines.h"
|
|
2
|
+
|
|
3
|
+
#pragma clang diagnostic push
|
|
4
|
+
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
5
|
+
#include "include/core/SkCanvas.h"
|
|
6
|
+
#include "include/core/SkColor.h"
|
|
7
|
+
#include "include/core/SkFont.h"
|
|
8
|
+
#include "include/core/SkFontTypes.h"
|
|
9
|
+
#include "include/core/SkPaint.h"
|
|
10
|
+
#include "include/core/SkPathEffect.h"
|
|
11
|
+
#include "include/core/SkRRect.h"
|
|
12
|
+
#include "include/core/SkRect.h"
|
|
13
|
+
#include "include/core/SkTypeface.h"
|
|
14
|
+
#include "include/effects/SkDashPathEffect.h"
|
|
15
|
+
#pragma clang diagnostic pop
|
|
16
|
+
|
|
17
|
+
#include <algorithm>
|
|
18
|
+
#include <cmath>
|
|
19
|
+
#include <cstdio>
|
|
20
|
+
#include <cstring>
|
|
21
|
+
|
|
22
|
+
#include "chart.h"
|
|
23
|
+
#include "fonts.h"
|
|
24
|
+
#include "price_line_layout.h"
|
|
25
|
+
#include "theme.h"
|
|
26
|
+
#include "viewport.h"
|
|
27
|
+
|
|
28
|
+
namespace vroom::price_lines {
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
constexpr SkScalar kDotted[2] = {2.f, 2.f};
|
|
33
|
+
constexpr SkScalar kDashed[2] = {6.f, 4.f};
|
|
34
|
+
constexpr float kBorderWidth = 1.f; // pill outline
|
|
35
|
+
constexpr float kGhostAlpha = 0.35f; // pre-drag position marker
|
|
36
|
+
constexpr float kCloseInset = 6.f; // X glyph inset within its square cell
|
|
37
|
+
constexpr float kCloseStroke = 1.5f;
|
|
38
|
+
constexpr float kDividerAlpha = 0.5f;
|
|
39
|
+
|
|
40
|
+
// The reference glyph whose tight bounds set every pill's height, so a group's
|
|
41
|
+
// segments stay the same height whether or not their text has descenders — and
|
|
42
|
+
// match the price badge, which measures digits.
|
|
43
|
+
constexpr char kHeightRef[] = "0";
|
|
44
|
+
|
|
45
|
+
// Dash pattern for a VroomPriceLine::line_style. Null = solid.
|
|
46
|
+
sk_sp<SkPathEffect> dash_for(int32_t line_style) {
|
|
47
|
+
if (line_style == 1) return SkDashPathEffect::Make(kDotted, 0.f);
|
|
48
|
+
if (line_style == 2) return SkDashPathEffect::Make(kDashed, 0.f);
|
|
49
|
+
return nullptr;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Brightens `c`'s channels by `mul` (clamped at white), leaving alpha alone.
|
|
53
|
+
// Used for the hovered segment so a line lifts under the cursor without
|
|
54
|
+
// changing size or position.
|
|
55
|
+
SkColor boost(SkColor c, float mul) {
|
|
56
|
+
if (mul <= 1.f) return c;
|
|
57
|
+
const auto up = [mul](U8CPU v) {
|
|
58
|
+
return static_cast<U8CPU>(
|
|
59
|
+
std::min(255.f, static_cast<float>(v) * mul + 0.5f));
|
|
60
|
+
};
|
|
61
|
+
return SkColorSetARGB(SkColorGetA(c), up(SkColorGetR(c)), up(SkColorGetG(c)),
|
|
62
|
+
up(SkColorGetB(c)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The price a line renders at: the live drag position while it's being dragged,
|
|
66
|
+
// otherwise its committed price. The committed value is never mutated — the host
|
|
67
|
+
// applies or rejects the drag by restating its lines.
|
|
68
|
+
double render_price(const VroomChart& chart, size_t i) {
|
|
69
|
+
if (chart.dragged_price_line == static_cast<int32_t>(i)) {
|
|
70
|
+
return chart.dragged_price_line_price;
|
|
71
|
+
}
|
|
72
|
+
return chart.price_lines[i].price;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The label font: the axis typeface, emboldened (these labels are chrome that
|
|
76
|
+
// must stay legible over candles). Returns false when no typeface is loaded yet,
|
|
77
|
+
// in which case callers fall back to lines without labels.
|
|
78
|
+
bool label_font(const VroomChart& chart, SkFont* out) {
|
|
79
|
+
auto tf = vroom::axis_typeface();
|
|
80
|
+
if (!tf) return false;
|
|
81
|
+
const float size = chart.price_line_style.font_size_px > 0.f
|
|
82
|
+
? chart.price_line_style.font_size_px
|
|
83
|
+
: chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX];
|
|
84
|
+
*out = SkFont(tf, size);
|
|
85
|
+
out->setSubpixel(true);
|
|
86
|
+
out->setEmbolden(true);
|
|
87
|
+
out->setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
float text_width(const SkFont& font, const std::string& s) {
|
|
92
|
+
if (s.empty()) return 0.f;
|
|
93
|
+
return font.measureText(s.data(), s.size(), SkTextEncoding::kUTF8);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Measures one line's label so both passes lay it out identically.
|
|
97
|
+
LabelMetrics metrics_for(const SkFont& font,
|
|
98
|
+
const VroomChart::StoredPriceLine& pl) {
|
|
99
|
+
SkRect ref;
|
|
100
|
+
font.measureText(kHeightRef, std::strlen(kHeightRef), SkTextEncoding::kUTF8,
|
|
101
|
+
&ref);
|
|
102
|
+
|
|
103
|
+
LabelMetrics m;
|
|
104
|
+
m.text_w = text_width(font, pl.text);
|
|
105
|
+
m.quantity_w = text_width(font, pl.quantity);
|
|
106
|
+
m.label_h = ref.height() + 2.f * kPadV;
|
|
107
|
+
m.closable = (pl.flags & VROOM_PRICE_LINE_CLOSABLE) != 0;
|
|
108
|
+
return m;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Rounds only the corners on the group's outer edges, so butted segments read as
|
|
112
|
+
// one continuous pill with dividers rather than a row of separate lozenges.
|
|
113
|
+
SkRRect segment_rrect(const Rect& r, bool round_left, bool round_right) {
|
|
114
|
+
const SkRect rect = SkRect::MakeLTRB(r.left, r.top, r.right, r.bottom);
|
|
115
|
+
const float l = round_left ? kCorner : 0.f;
|
|
116
|
+
const float rr = round_right ? kCorner : 0.f;
|
|
117
|
+
// setRectRadii takes corners in UL, UR, LR, LL order.
|
|
118
|
+
const SkVector radii[4] = {{l, l}, {rr, rr}, {rr, rr}, {l, l}};
|
|
119
|
+
SkRRect out;
|
|
120
|
+
out.setRectRadii(rect, radii);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Draws `s` horizontally centered in `cell` and vertically centered on `cy`,
|
|
125
|
+
// shifting the baseline so the glyphs' own midpoint lands on the line.
|
|
126
|
+
void draw_centered_text(SkCanvas* canvas,
|
|
127
|
+
const SkFont& font,
|
|
128
|
+
const std::string& s,
|
|
129
|
+
const Rect& cell,
|
|
130
|
+
float cy,
|
|
131
|
+
SkColor color) {
|
|
132
|
+
if (s.empty()) return;
|
|
133
|
+
SkRect tb;
|
|
134
|
+
const float w =
|
|
135
|
+
font.measureText(s.data(), s.size(), SkTextEncoding::kUTF8, &tb);
|
|
136
|
+
|
|
137
|
+
SkPaint paint;
|
|
138
|
+
paint.setAntiAlias(true);
|
|
139
|
+
paint.setColor(color);
|
|
140
|
+
const float cx = (cell.left + cell.right) * 0.5f;
|
|
141
|
+
canvas->drawString(s.c_str(), cx - w * 0.5f,
|
|
142
|
+
cy - (tb.fTop + tb.fBottom) * 0.5f, font, paint);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Fills a segment and strokes its 1px outline.
|
|
146
|
+
void draw_pill(SkCanvas* canvas,
|
|
147
|
+
const Rect& r,
|
|
148
|
+
bool round_left,
|
|
149
|
+
bool round_right,
|
|
150
|
+
SkColor fill,
|
|
151
|
+
SkColor border) {
|
|
152
|
+
const SkRRect rr = segment_rrect(r, round_left, round_right);
|
|
153
|
+
|
|
154
|
+
SkPaint bg;
|
|
155
|
+
bg.setAntiAlias(true);
|
|
156
|
+
bg.setColor(fill);
|
|
157
|
+
canvas->drawRRect(rr, bg);
|
|
158
|
+
|
|
159
|
+
if (SkColorGetA(border) == 0) return;
|
|
160
|
+
SkPaint outline;
|
|
161
|
+
outline.setAntiAlias(true);
|
|
162
|
+
outline.setColor(border);
|
|
163
|
+
outline.setStyle(SkPaint::kStroke_Style);
|
|
164
|
+
outline.setStrokeWidth(kBorderWidth);
|
|
165
|
+
// Inset by half the stroke so the outline sits inside the segment and butted
|
|
166
|
+
// neighbours don't double up along their shared edge.
|
|
167
|
+
canvas->drawRRect(segment_rrect(
|
|
168
|
+
Rect{r.left + kBorderWidth * 0.5f,
|
|
169
|
+
r.top + kBorderWidth * 0.5f,
|
|
170
|
+
r.right - kBorderWidth * 0.5f,
|
|
171
|
+
r.bottom - kBorderWidth * 0.5f},
|
|
172
|
+
round_left, round_right),
|
|
173
|
+
outline);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The close affordance: an X stroked inside its square cell.
|
|
177
|
+
void draw_close_icon(SkCanvas* canvas, const Rect& cell, SkColor color) {
|
|
178
|
+
SkPaint paint;
|
|
179
|
+
paint.setAntiAlias(true);
|
|
180
|
+
paint.setColor(color);
|
|
181
|
+
paint.setStyle(SkPaint::kStroke_Style);
|
|
182
|
+
paint.setStrokeWidth(kCloseStroke);
|
|
183
|
+
paint.setStrokeCap(SkPaint::kRound_Cap);
|
|
184
|
+
|
|
185
|
+
// Keep the glyph square even if the cell isn't, and never let a tiny cell
|
|
186
|
+
// invert the inset.
|
|
187
|
+
const float inset =
|
|
188
|
+
std::min(kCloseInset, std::min(cell.width(), cell.bottom - cell.top) * 0.5f);
|
|
189
|
+
const float l = cell.left + inset;
|
|
190
|
+
const float r = cell.right - inset;
|
|
191
|
+
const float t = cell.top + inset;
|
|
192
|
+
const float b = cell.bottom - inset;
|
|
193
|
+
canvas->drawLine(l, t, r, b, paint);
|
|
194
|
+
canvas->drawLine(r, t, l, b, paint);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A 1px vertical rule on a segment's left edge, separating butted segments.
|
|
198
|
+
void draw_divider(SkCanvas* canvas, const Rect& seg, SkColor color) {
|
|
199
|
+
SkPaint paint;
|
|
200
|
+
paint.setAntiAlias(false);
|
|
201
|
+
paint.setColor(SkColorSetA(
|
|
202
|
+
color, static_cast<U8CPU>(SkColorGetA(color) * kDividerAlpha)));
|
|
203
|
+
paint.setStrokeWidth(1.f);
|
|
204
|
+
canvas->drawLine(seg.left, seg.top + kBorderWidth, seg.left,
|
|
205
|
+
seg.bottom - kBorderWidth, paint);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The price badge in the y-axis strip, matching the current-price indicator's
|
|
209
|
+
// geometry so the two read as the same chrome.
|
|
210
|
+
void draw_axis_badge(SkCanvas* canvas,
|
|
211
|
+
const SkFont& font,
|
|
212
|
+
const Layout& lay,
|
|
213
|
+
double price,
|
|
214
|
+
float y,
|
|
215
|
+
SkColor fill) {
|
|
216
|
+
if (lay.y_axis_width_px <= 0.f) return;
|
|
217
|
+
|
|
218
|
+
char buf[32];
|
|
219
|
+
std::snprintf(buf, sizeof(buf), "%.2f", price);
|
|
220
|
+
const size_t len = std::strlen(buf);
|
|
221
|
+
|
|
222
|
+
SkRect tb;
|
|
223
|
+
const float text_w = font.measureText(buf, len, SkTextEncoding::kUTF8, &tb);
|
|
224
|
+
const float box_w = text_w + 2.f * kPadH;
|
|
225
|
+
const float box_h = tb.height() + 2.f * kPadV;
|
|
226
|
+
const float cx = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
227
|
+
const SkRect rect =
|
|
228
|
+
SkRect::MakeXYWH(cx - box_w * 0.5f, y - box_h * 0.5f, box_w, box_h);
|
|
229
|
+
|
|
230
|
+
SkPaint box;
|
|
231
|
+
box.setAntiAlias(true);
|
|
232
|
+
box.setColor(fill);
|
|
233
|
+
canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
|
|
234
|
+
|
|
235
|
+
SkPaint text;
|
|
236
|
+
text.setAntiAlias(true);
|
|
237
|
+
text.setColor(SK_ColorWHITE);
|
|
238
|
+
canvas->drawString(buf, cx - text_w * 0.5f,
|
|
239
|
+
y - (tb.fTop + tb.fBottom) * 0.5f, font, text);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
} // namespace
|
|
243
|
+
|
|
244
|
+
void draw(SkCanvas* canvas,
|
|
245
|
+
const VroomChart& chart,
|
|
246
|
+
const Layout& lay,
|
|
247
|
+
const PriceBounds& bounds,
|
|
248
|
+
float candle_right,
|
|
249
|
+
float candle_area_h) {
|
|
250
|
+
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
251
|
+
if (chart.price_lines.empty()) return;
|
|
252
|
+
|
|
253
|
+
const VroomPriceLineStyle& style = chart.price_line_style;
|
|
254
|
+
SkFont font;
|
|
255
|
+
const bool has_font = label_font(chart, &font);
|
|
256
|
+
|
|
257
|
+
for (size_t i = 0; i < chart.price_lines.size(); ++i) {
|
|
258
|
+
const VroomChart::StoredPriceLine& pl = chart.price_lines[i];
|
|
259
|
+
const bool dragging = chart.dragged_price_line == static_cast<int32_t>(i);
|
|
260
|
+
const bool hovered = chart.hovered_price_line == static_cast<int32_t>(i);
|
|
261
|
+
|
|
262
|
+
const float y = vroom::price_to_y(lay, bounds, render_price(chart, i));
|
|
263
|
+
if (y < 0.f || y > candle_area_h) continue; // off-pane: cull, don't clamp
|
|
264
|
+
|
|
265
|
+
const SkColor color = static_cast<SkColor>(pl.color);
|
|
266
|
+
const float stroke = pl.width > 0.f ? pl.width : 1.f;
|
|
267
|
+
// Hovering the line body lifts the whole line; hovering just the close
|
|
268
|
+
// button lifts only that button (handled below).
|
|
269
|
+
const bool line_hot = hovered && chart.hovered_price_line_part == 0;
|
|
270
|
+
const SkColor line_color =
|
|
271
|
+
line_hot || dragging ? boost(color, style.hover_boost) : color;
|
|
272
|
+
|
|
273
|
+
SkPaint line;
|
|
274
|
+
line.setAntiAlias(true);
|
|
275
|
+
line.setColor(line_color);
|
|
276
|
+
line.setStrokeWidth(stroke);
|
|
277
|
+
line.setPathEffect(dash_for(pl.line_style));
|
|
278
|
+
|
|
279
|
+
// A faint marker at the pre-drag price, so the user can see how far
|
|
280
|
+
// they've moved and where releasing outside will snap back to.
|
|
281
|
+
if (dragging) {
|
|
282
|
+
const float ghost_y = vroom::price_to_y(lay, bounds, pl.price);
|
|
283
|
+
if (ghost_y >= 0.f && ghost_y <= candle_area_h) {
|
|
284
|
+
SkPaint ghost = line;
|
|
285
|
+
ghost.setColor(SkColorSetA(
|
|
286
|
+
color, static_cast<U8CPU>(SkColorGetA(color) * kGhostAlpha)));
|
|
287
|
+
canvas->drawLine(0.f, ghost_y, candle_right, ghost_y, ghost);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const bool extend_left = (pl.flags & VROOM_PRICE_LINE_EXTEND_LEFT) != 0;
|
|
292
|
+
|
|
293
|
+
LabelMetrics metrics;
|
|
294
|
+
if (has_font) metrics = metrics_for(font, pl);
|
|
295
|
+
const GroupLayout group = layout_group(metrics, y, candle_right, style);
|
|
296
|
+
|
|
297
|
+
if (group.empty()) {
|
|
298
|
+
// Nothing to anchor a partial span to, so a bare line always spans
|
|
299
|
+
// the pane.
|
|
300
|
+
canvas->drawLine(0.f, y, candle_right, y, line);
|
|
301
|
+
} else {
|
|
302
|
+
// Two segments with the label group punched out between them, so the
|
|
303
|
+
// dashes don't show through the translucent pills.
|
|
304
|
+
if (extend_left && group.left > 0.f) {
|
|
305
|
+
canvas->drawLine(0.f, y, group.left, y, line);
|
|
306
|
+
}
|
|
307
|
+
if (group.right < candle_right) {
|
|
308
|
+
canvas->drawLine(group.right, y, candle_right, y, line);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!group.empty()) {
|
|
313
|
+
const SkColor body_bg = static_cast<SkColor>(style.body_bg);
|
|
314
|
+
const bool close_hot = hovered && chart.hovered_price_line_part == 1;
|
|
315
|
+
|
|
316
|
+
// Only the group's outermost corners are rounded.
|
|
317
|
+
const Rect* first = !group.body.empty()
|
|
318
|
+
? &group.body
|
|
319
|
+
: (!group.quantity.empty() ? &group.quantity
|
|
320
|
+
: &group.close);
|
|
321
|
+
const Rect* last = !group.close.empty()
|
|
322
|
+
? &group.close
|
|
323
|
+
: (!group.quantity.empty() ? &group.quantity
|
|
324
|
+
: &group.body);
|
|
325
|
+
|
|
326
|
+
if (!group.body.empty()) {
|
|
327
|
+
draw_pill(canvas, group.body, first == &group.body,
|
|
328
|
+
last == &group.body, body_bg, line_color);
|
|
329
|
+
draw_centered_text(canvas, font, pl.text, group.body, y,
|
|
330
|
+
line_color);
|
|
331
|
+
}
|
|
332
|
+
if (!group.quantity.empty()) {
|
|
333
|
+
// Solid fill + white text, so size reads at a glance against the
|
|
334
|
+
// translucent body.
|
|
335
|
+
draw_pill(canvas, group.quantity, first == &group.quantity,
|
|
336
|
+
last == &group.quantity, line_color, SK_ColorTRANSPARENT);
|
|
337
|
+
draw_centered_text(canvas, font, pl.quantity, group.quantity, y,
|
|
338
|
+
SK_ColorWHITE);
|
|
339
|
+
}
|
|
340
|
+
if (!group.close.empty()) {
|
|
341
|
+
const SkColor close_color =
|
|
342
|
+
close_hot ? boost(color, style.hover_boost) : line_color;
|
|
343
|
+
draw_pill(canvas, group.close, first == &group.close,
|
|
344
|
+
last == &group.close, body_bg, close_color);
|
|
345
|
+
if (!group.quantity.empty()) {
|
|
346
|
+
draw_divider(canvas, group.close, close_color);
|
|
347
|
+
}
|
|
348
|
+
draw_close_icon(canvas, group.close, close_color);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (has_font && (pl.flags & VROOM_PRICE_LINE_AXIS_LABEL) != 0) {
|
|
353
|
+
draw_axis_badge(canvas, font, lay, render_price(chart, i), y,
|
|
354
|
+
line_color);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
HitResult hit_test(const VroomChart& chart,
|
|
360
|
+
const Layout& lay,
|
|
361
|
+
const PriceBounds& bounds,
|
|
362
|
+
float candle_right,
|
|
363
|
+
float candle_area_h,
|
|
364
|
+
float x,
|
|
365
|
+
float y) {
|
|
366
|
+
HitResult miss{-1, -1};
|
|
367
|
+
if (chart.price_lines.empty()) return miss;
|
|
368
|
+
if (candle_right <= 0.f || candle_area_h <= 0.f) return miss;
|
|
369
|
+
|
|
370
|
+
const VroomPriceLineStyle& style = chart.price_line_style;
|
|
371
|
+
SkFont font;
|
|
372
|
+
const bool has_font = label_font(chart, &font);
|
|
373
|
+
|
|
374
|
+
// Close buttons first — they sit on top of the line, so a press there must
|
|
375
|
+
// never be read as the start of a drag.
|
|
376
|
+
int32_t best = -1;
|
|
377
|
+
float best_dist = 0.f;
|
|
378
|
+
for (size_t i = 0; i < chart.price_lines.size(); ++i) {
|
|
379
|
+
const VroomChart::StoredPriceLine& pl = chart.price_lines[i];
|
|
380
|
+
if ((pl.flags & VROOM_PRICE_LINE_CLOSABLE) == 0) continue;
|
|
381
|
+
if (!has_font) continue; // no font, no rendered button to press
|
|
382
|
+
|
|
383
|
+
const float ly = vroom::price_to_y(lay, bounds, render_price(chart, i));
|
|
384
|
+
if (ly < 0.f || ly > candle_area_h) continue;
|
|
385
|
+
|
|
386
|
+
const GroupLayout group =
|
|
387
|
+
layout_group(metrics_for(font, pl), ly, candle_right, style);
|
|
388
|
+
if (!contains(group.close, x, y)) continue;
|
|
389
|
+
|
|
390
|
+
const float dist = std::fabs(y - ly);
|
|
391
|
+
if (best < 0 || dist < best_dist) {
|
|
392
|
+
best = static_cast<int32_t>(i);
|
|
393
|
+
best_dist = dist;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (best >= 0) return HitResult{best, 1};
|
|
397
|
+
|
|
398
|
+
// Then draggable lines, nearest in y.
|
|
399
|
+
for (size_t i = 0; i < chart.price_lines.size(); ++i) {
|
|
400
|
+
const VroomChart::StoredPriceLine& pl = chart.price_lines[i];
|
|
401
|
+
if ((pl.flags & VROOM_PRICE_LINE_DRAGGABLE) == 0) continue;
|
|
402
|
+
|
|
403
|
+
const float ly = vroom::price_to_y(lay, bounds, render_price(chart, i));
|
|
404
|
+
if (ly < 0.f || ly > candle_area_h) continue;
|
|
405
|
+
if (!hits_line(ly, pl.width, y)) continue;
|
|
406
|
+
|
|
407
|
+
// Don't grab where the line isn't drawn: without extend_left only the
|
|
408
|
+
// label group and the run to the axis are live.
|
|
409
|
+
if ((pl.flags & VROOM_PRICE_LINE_EXTEND_LEFT) == 0) {
|
|
410
|
+
const GroupLayout group = layout_group(
|
|
411
|
+
has_font ? metrics_for(font, pl) : LabelMetrics{}, ly,
|
|
412
|
+
candle_right, style);
|
|
413
|
+
if (!group.empty() && x < group.left) continue;
|
|
414
|
+
}
|
|
415
|
+
if (x < 0.f || x > candle_right) continue;
|
|
416
|
+
|
|
417
|
+
const float dist = std::fabs(y - ly);
|
|
418
|
+
if (best < 0 || dist < best_dist) {
|
|
419
|
+
best = static_cast<int32_t>(i);
|
|
420
|
+
best_dist = dist;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (best >= 0) return HitResult{best, 0};
|
|
424
|
+
|
|
425
|
+
return miss;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
} // namespace vroom::price_lines
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Consumer-supplied horizontal price status lines — the primitive behind resting
|
|
2
|
+
// limit orders, take-profits and liquidation levels.
|
|
3
|
+
//
|
|
4
|
+
// Each line is a styled horizontal rule across the price pane ending in a label
|
|
5
|
+
// group (body pill + optional quantity pill + optional close button) and an
|
|
6
|
+
// optional price badge in the y-axis strip, closely modeled on the current-price
|
|
7
|
+
// indicator it sits beside. Lines can be dragged vertically and closed; the
|
|
8
|
+
// interaction state (hover, live drag price) lives on the chart and is driven by
|
|
9
|
+
// the host's gesture layer through the public facade.
|
|
10
|
+
//
|
|
11
|
+
// Geometry is computed in price_line_layout.h so the draw and hit-test passes
|
|
12
|
+
// agree by construction.
|
|
13
|
+
|
|
14
|
+
#pragma once
|
|
15
|
+
|
|
16
|
+
#include <cstdint>
|
|
17
|
+
|
|
18
|
+
class SkCanvas;
|
|
19
|
+
struct VroomChart;
|
|
20
|
+
|
|
21
|
+
namespace vroom {
|
|
22
|
+
struct Layout;
|
|
23
|
+
struct PriceBounds;
|
|
24
|
+
} // namespace vroom
|
|
25
|
+
|
|
26
|
+
namespace vroom::price_lines {
|
|
27
|
+
|
|
28
|
+
// Draws every price line. `candle_right` is the inner edge of the y-axis strip
|
|
29
|
+
// and `candle_area_h` the y of the x-axis separator; a line whose price maps
|
|
30
|
+
// outside [0, candle_area_h] is skipped rather than clamped.
|
|
31
|
+
void draw(SkCanvas* canvas,
|
|
32
|
+
const VroomChart& chart,
|
|
33
|
+
const Layout& lay,
|
|
34
|
+
const PriceBounds& bounds,
|
|
35
|
+
float candle_right,
|
|
36
|
+
float candle_area_h);
|
|
37
|
+
|
|
38
|
+
// A hit against a price line. `index` is -1 for a miss; `part` is 0 for the line
|
|
39
|
+
// or its label body (the drag target) and 1 for the close button.
|
|
40
|
+
struct HitResult {
|
|
41
|
+
int32_t index;
|
|
42
|
+
int32_t part;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Hit-tests pixel (x, y). Close buttons win over lines, and among several
|
|
46
|
+
// candidates the nearest in y wins. Only draggable lines report part 0 and only
|
|
47
|
+
// closable lines report part 1.
|
|
48
|
+
HitResult hit_test(const VroomChart& chart,
|
|
49
|
+
const Layout& lay,
|
|
50
|
+
const PriceBounds& bounds,
|
|
51
|
+
float candle_right,
|
|
52
|
+
float candle_area_h,
|
|
53
|
+
float x,
|
|
54
|
+
float y);
|
|
55
|
+
|
|
56
|
+
} // namespace vroom::price_lines
|
package/cpp/_core_src/rsi.cpp
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
#include <cmath> // std::nan, std::isfinite
|
|
4
4
|
|
|
5
|
+
#include "series_ma.h"
|
|
6
|
+
|
|
5
7
|
namespace vroom::rsi {
|
|
6
8
|
|
|
7
9
|
namespace {
|
|
@@ -46,25 +48,9 @@ void compute(const ::VroomCandle* candles, std::size_t n, int period,
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
void compute_ma(const std::vector<double>& rsi, int ma_period,
|
|
51
|
+
void compute_ma(const std::vector<double>& rsi, int ma_period, int kind,
|
|
50
52
|
std::vector<double>& out) {
|
|
51
|
-
|
|
52
|
-
out.assign(n, std::nan(""));
|
|
53
|
-
if (ma_period < 1) return;
|
|
54
|
-
const std::size_t P = static_cast<std::size_t>(ma_period);
|
|
55
|
-
if (n < P) return;
|
|
56
|
-
for (std::size_t i = P - 1; i < n; ++i) {
|
|
57
|
-
double sum = 0.0;
|
|
58
|
-
bool ok = true;
|
|
59
|
-
for (std::size_t k = i + 1 - P; k <= i; ++k) {
|
|
60
|
-
if (!std::isfinite(rsi[k])) {
|
|
61
|
-
ok = false;
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
sum += rsi[k];
|
|
65
|
-
}
|
|
66
|
-
if (ok) out[i] = sum / static_cast<double>(P);
|
|
67
|
-
}
|
|
53
|
+
vroom::series_ma::smooth(rsi, kind, ma_period, out);
|
|
68
54
|
}
|
|
69
55
|
|
|
70
56
|
} // namespace vroom::rsi
|
package/cpp/_core_src/rsi.h
CHANGED
|
@@ -21,12 +21,12 @@ namespace vroom::rsi {
|
|
|
21
21
|
void compute(const ::VroomCandle* candles, std::size_t n, int period,
|
|
22
22
|
std::vector<double>& out);
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// out
|
|
28
|
-
//
|
|
29
|
-
void compute_ma(const std::vector<double>& rsi, int ma_period,
|
|
24
|
+
// Moving average of an RSI series (the "RSI-based MA" / trendline that most RSI
|
|
25
|
+
// indicators overlay; crossovers of RSI vs. this line are the common signal).
|
|
26
|
+
// `kind` is a vroom::ma KIND_* value. `ma_period` is clamped to >= 1. Fills
|
|
27
|
+
// `out` (resized to rsi.size()), NaN until `ma_period` valid RSI values exist —
|
|
28
|
+
// either kind produces its first value at the same index.
|
|
29
|
+
void compute_ma(const std::vector<double>& rsi, int ma_period, int kind,
|
|
30
30
|
std::vector<double>& out);
|
|
31
31
|
|
|
32
32
|
} // namespace vroom::rsi
|
|
@@ -20,17 +20,22 @@
|
|
|
20
20
|
|
|
21
21
|
#include "chart.h"
|
|
22
22
|
#include "fonts.h"
|
|
23
|
+
#include "style_inherit.h"
|
|
23
24
|
#include "theme.h"
|
|
24
25
|
#include "viewport.h"
|
|
25
26
|
|
|
26
27
|
namespace vroom::rsi_pane {
|
|
27
28
|
|
|
28
29
|
namespace {
|
|
29
|
-
|
|
30
|
+
using vroom::style::color_or;
|
|
31
|
+
using vroom::style::width_or;
|
|
32
|
+
|
|
33
|
+
constexpr SkColor kRsiLine = 0xff8957e5; // violet RSI line
|
|
30
34
|
constexpr SkColor kRsiMaLine = 0xffd29922; // amber RSI moving-average trendline
|
|
31
35
|
constexpr SkColor kRefLine = 0xff30363d; // band reference lines
|
|
32
36
|
constexpr SkColor kDivider = 0xff21262d; // pane separator
|
|
33
37
|
constexpr SkScalar kDash[2] = {3.f, 3.f};
|
|
38
|
+
constexpr float kLineWidth = 1.5f; // default stroke for both lines
|
|
34
39
|
} // namespace
|
|
35
40
|
|
|
36
41
|
void draw(SkCanvas* canvas,
|
|
@@ -53,6 +58,7 @@ void draw(SkCanvas* canvas,
|
|
|
53
58
|
// Map 0..100 about the pane center so the user y-zoom scales symmetrically
|
|
54
59
|
// around 50. z == 1 reproduces the default fit (0 -> pane_bottom, 100 ->
|
|
55
60
|
// pane_top); z > 1 zooms in (taller), z < 1 zooms out (flatter).
|
|
61
|
+
const VroomRSI& cfg = chart.rsi;
|
|
56
62
|
const float center_y = (pane_top + pane_bottom) * 0.5f;
|
|
57
63
|
const float z = static_cast<float>(chart.rsi_y_scale);
|
|
58
64
|
auto y_for = [&](double v) -> float {
|
|
@@ -78,19 +84,21 @@ void draw(SkCanvas* canvas,
|
|
|
78
84
|
canvas->clipRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom));
|
|
79
85
|
|
|
80
86
|
// Configurable band reference lines (overbought / oversold).
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const float y_upper = y_for(cfg.upper_band);
|
|
88
|
+
const float y_lower = y_for(cfg.lower_band);
|
|
89
|
+
if (cfg.bands_visible) {
|
|
90
|
+
SkPaint ref;
|
|
91
|
+
ref.setAntiAlias(true);
|
|
92
|
+
ref.setColor(color_or(cfg.band_color, kRefLine));
|
|
93
|
+
ref.setStrokeWidth(1.f);
|
|
94
|
+
ref.setPathEffect(SkDashPathEffect::Make(kDash, 0.f));
|
|
95
|
+
canvas->drawLine(0.f, y_upper, candle_right, y_upper, ref);
|
|
96
|
+
canvas->drawLine(0.f, y_lower, candle_right, y_lower, ref);
|
|
97
|
+
}
|
|
90
98
|
|
|
91
99
|
// Strokes a value series as a polyline; a NaN lifts the pen so undefined
|
|
92
100
|
// leading values (i < period) leave a gap.
|
|
93
|
-
auto stroke_series = [&](const double* series, SkColor color) {
|
|
101
|
+
auto stroke_series = [&](const double* series, SkColor color, float width) {
|
|
94
102
|
if (!series) return;
|
|
95
103
|
SkPathBuilder path;
|
|
96
104
|
bool pen_down = false;
|
|
@@ -115,11 +123,17 @@ void draw(SkCanvas* canvas,
|
|
|
115
123
|
line.setAntiAlias(true);
|
|
116
124
|
line.setColor(color);
|
|
117
125
|
line.setStyle(SkPaint::kStroke_Style);
|
|
118
|
-
line.setStrokeWidth(
|
|
126
|
+
line.setStrokeWidth(width);
|
|
119
127
|
canvas->drawPath(path.detach(), line);
|
|
120
128
|
};
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
if (cfg.line_visible) {
|
|
130
|
+
stroke_series(rsi_visible, color_or(cfg.line_color, kRsiLine),
|
|
131
|
+
width_or(cfg.line_width, kLineWidth));
|
|
132
|
+
}
|
|
133
|
+
// Trendline on top. Its own visibility is handled upstream: a hidden
|
|
134
|
+
// trendline isn't computed, so the caller passes null.
|
|
135
|
+
stroke_series(rsi_ma_visible, color_or(cfg.ma_color, kRsiMaLine),
|
|
136
|
+
width_or(cfg.ma_width, kLineWidth));
|
|
123
137
|
|
|
124
138
|
// Caption, top-left of the pane.
|
|
125
139
|
auto tf = vroom::axis_typeface();
|
|
@@ -129,7 +143,7 @@ void draw(SkCanvas* canvas,
|
|
|
129
143
|
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
130
144
|
|
|
131
145
|
char caption[24];
|
|
132
|
-
std::snprintf(caption, sizeof(caption), "RSI %d",
|
|
146
|
+
std::snprintf(caption, sizeof(caption), "RSI %d", cfg.period);
|
|
133
147
|
SkRect cb;
|
|
134
148
|
font.measureText(caption, std::strlen(caption), SkTextEncoding::kUTF8,
|
|
135
149
|
&cb);
|
|
@@ -142,8 +156,9 @@ void draw(SkCanvas* canvas,
|
|
|
142
156
|
canvas->restore();
|
|
143
157
|
|
|
144
158
|
// 70 / 30 labels in the y-axis strip (drawn after the clip; the strip's
|
|
145
|
-
// background was already masked by draw_chart before this call).
|
|
146
|
-
|
|
159
|
+
// background was already masked by draw_chart before this call). They label
|
|
160
|
+
// the band rules, so they go with them.
|
|
161
|
+
if (tf && cfg.bands_visible) {
|
|
147
162
|
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
148
163
|
font.setSubpixel(true);
|
|
149
164
|
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
@@ -162,8 +177,8 @@ void draw(SkCanvas* canvas,
|
|
|
162
177
|
};
|
|
163
178
|
char ub[8];
|
|
164
179
|
char lb[8];
|
|
165
|
-
std::snprintf(ub, sizeof(ub), "%.0f",
|
|
166
|
-
std::snprintf(lb, sizeof(lb), "%.0f",
|
|
180
|
+
std::snprintf(ub, sizeof(ub), "%.0f", cfg.upper_band);
|
|
181
|
+
std::snprintf(lb, sizeof(lb), "%.0f", cfg.lower_band);
|
|
167
182
|
label(ub, y_upper);
|
|
168
183
|
label(lb, y_lower);
|
|
169
184
|
}
|