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.
Files changed (58) hide show
  1. package/android/CMakeLists.txt +143 -0
  2. package/android/README.md +60 -5
  3. package/android/build.gradle +115 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/cpp/VroomChartJni.cpp +24 -0
  6. package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
  7. package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
  8. package/cpp/VroomChartHostObject.cpp +368 -33
  9. package/cpp/VroomJsiInstaller.cpp +11 -1
  10. package/cpp/VroomSkiaContext.cpp +15 -7
  11. package/cpp/_core_include/vroom/vroom_chart.h +250 -22
  12. package/cpp/_core_src/bollinger.h +1 -1
  13. package/cpp/_core_src/candles.cpp +138 -41
  14. package/cpp/_core_src/candles.h +11 -1
  15. package/cpp/_core_src/chart.cpp +98 -40
  16. package/cpp/_core_src/chart.h +82 -20
  17. package/cpp/_core_src/chart_facade.cpp +297 -66
  18. package/cpp/_core_src/gradient.cpp +46 -0
  19. package/cpp/_core_src/gradient.h +31 -0
  20. package/cpp/_core_src/labels.cpp +49 -16
  21. package/cpp/_core_src/labels.h +33 -4
  22. package/cpp/_core_src/liquidity.cpp +3 -37
  23. package/cpp/_core_src/ma_overlay.cpp +174 -12
  24. package/cpp/_core_src/ma_overlay.h +55 -3
  25. package/cpp/_core_src/macd.cpp +13 -42
  26. package/cpp/_core_src/macd.h +11 -8
  27. package/cpp/_core_src/macd_pane.cpp +57 -27
  28. package/cpp/_core_src/price_line_layout.cpp +87 -0
  29. package/cpp/_core_src/price_line_layout.h +80 -0
  30. package/cpp/_core_src/price_lines.cpp +428 -0
  31. package/cpp/_core_src/price_lines.h +56 -0
  32. package/cpp/_core_src/rsi.cpp +4 -18
  33. package/cpp/_core_src/rsi.h +6 -6
  34. package/cpp/_core_src/rsi_pane.cpp +34 -19
  35. package/cpp/_core_src/series_ma.cpp +64 -0
  36. package/cpp/_core_src/series_ma.h +30 -0
  37. package/cpp/_core_src/style_inherit.h +35 -0
  38. package/cpp/_core_src/theme.cpp +2 -1
  39. package/cpp/_core_src/viewport.cpp +36 -12
  40. package/cpp/_core_src/viewport.h +50 -0
  41. package/cpp/_core_src/volume.cpp +33 -8
  42. package/cpp/_core_src/volume.h +12 -1
  43. package/cpp/_core_src/volume_anim.cpp +32 -0
  44. package/cpp/_core_src/volume_anim.h +41 -0
  45. package/lib/index.d.mts +261 -25
  46. package/lib/index.d.ts +261 -25
  47. package/lib/index.js +252 -36
  48. package/lib/index.js.map +1 -1
  49. package/lib/index.mjs +252 -36
  50. package/lib/index.mjs.map +1 -1
  51. package/package.json +5 -4
  52. package/src/VroomChart.tsx +162 -11
  53. package/src/easing.ts +40 -0
  54. package/src/index.ts +5 -0
  55. package/src/jsi.d.ts +124 -20
  56. package/src/theme.ts +1 -0
  57. package/src/types.ts +5 -0
  58. 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
@@ -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
- const std::size_t n = rsi.size();
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
@@ -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
- // Simple moving average of an RSI series (the "RSI-based MA" / trendline that
25
- // most RSI indicators overlay; crossovers of RSI vs. this line are the common
26
- // signal). `ma_period` is clamped to >= 1. Fills `out` (resized to rsi.size()):
27
- // out[i] is the mean of rsi[i-ma_period+1 .. i] when all those are finite, else
28
- // NaN (so it's only defined once `ma_period` valid RSI values exist).
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
- constexpr SkColor kRsiLine = 0xff8957e5; // violet RSI line (themable later)
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
- SkPaint ref;
82
- ref.setAntiAlias(true);
83
- ref.setColor(kRefLine);
84
- ref.setStrokeWidth(1.f);
85
- ref.setPathEffect(SkDashPathEffect::Make(kDash, 0.f));
86
- const float y_upper = y_for(chart.rsi_upper);
87
- const float y_lower = y_for(chart.rsi_lower);
88
- canvas->drawLine(0.f, y_upper, candle_right, y_upper, ref);
89
- canvas->drawLine(0.f, y_lower, candle_right, y_lower, ref);
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(1.5f);
126
+ line.setStrokeWidth(width);
119
127
  canvas->drawPath(path.detach(), line);
120
128
  };
121
- stroke_series(rsi_visible, kRsiLine);
122
- stroke_series(rsi_ma_visible, kRsiMaLine); // trendline on top
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", chart.rsi_period);
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
- if (tf) {
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", chart.rsi_upper);
166
- std::snprintf(lb, sizeof(lb), "%.0f", chart.rsi_lower);
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
  }