react-native-vroom-chart 0.1.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/android/README.md +7 -0
  4. package/cpp/README.md +11 -0
  5. package/cpp/VroomChartHostObject.cpp +458 -0
  6. package/cpp/VroomChartHostObject.h +36 -0
  7. package/cpp/VroomJsiInstaller.cpp +69 -0
  8. package/cpp/VroomJsiInstaller.h +10 -0
  9. package/cpp/VroomSkiaContext.cpp +25 -0
  10. package/cpp/VroomSkiaContext.h +30 -0
  11. package/cpp/_core_include/vroom/vroom_chart.h +195 -0
  12. package/cpp/_core_src/candles.cpp +74 -0
  13. package/cpp/_core_src/candles.h +34 -0
  14. package/cpp/_core_src/chart.cpp +317 -0
  15. package/cpp/_core_src/chart.h +167 -0
  16. package/cpp/_core_src/chart_facade.cpp +570 -0
  17. package/cpp/_core_src/chart_internal.h +30 -0
  18. package/cpp/_core_src/crosshair.cpp +65 -0
  19. package/cpp/_core_src/crosshair.h +32 -0
  20. package/cpp/_core_src/fonts.cpp +22 -0
  21. package/cpp/_core_src/fonts.h +25 -0
  22. package/cpp/_core_src/labels.cpp +340 -0
  23. package/cpp/_core_src/labels.h +85 -0
  24. package/cpp/_core_src/ma.cpp +53 -0
  25. package/cpp/_core_src/ma.h +39 -0
  26. package/cpp/_core_src/ma_overlay.cpp +73 -0
  27. package/cpp/_core_src/ma_overlay.h +42 -0
  28. package/cpp/_core_src/macd.cpp +68 -0
  29. package/cpp/_core_src/macd.h +29 -0
  30. package/cpp/_core_src/macd_pane.cpp +199 -0
  31. package/cpp/_core_src/macd_pane.h +41 -0
  32. package/cpp/_core_src/price_indicator.cpp +106 -0
  33. package/cpp/_core_src/price_indicator.h +29 -0
  34. package/cpp/_core_src/rsi.cpp +70 -0
  35. package/cpp/_core_src/rsi.h +32 -0
  36. package/cpp/_core_src/rsi_pane.cpp +167 -0
  37. package/cpp/_core_src/rsi_pane.h +39 -0
  38. package/cpp/_core_src/theme.cpp +41 -0
  39. package/cpp/_core_src/theme.h +23 -0
  40. package/cpp/_core_src/ticks.cpp +49 -0
  41. package/cpp/_core_src/ticks.h +30 -0
  42. package/cpp/_core_src/viewport.cpp +141 -0
  43. package/cpp/_core_src/viewport.h +100 -0
  44. package/cpp/_core_src/volume.cpp +70 -0
  45. package/cpp/_core_src/volume.h +34 -0
  46. package/cpp/_core_src/vwap.cpp +51 -0
  47. package/cpp/_core_src/vwap.h +27 -0
  48. package/ios/README.md +7 -0
  49. package/ios/VroomChartModule.h +11 -0
  50. package/ios/VroomChartModule.mm +44 -0
  51. package/lib/index.d.mts +185 -0
  52. package/lib/index.d.ts +185 -0
  53. package/lib/index.js +429 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/index.mjs +396 -0
  56. package/lib/index.mjs.map +1 -0
  57. package/package.json +75 -0
  58. package/react-native-vroom-chart.podspec +79 -0
  59. package/src/NativeVroomChart.ts +12 -0
  60. package/src/VroomChart.tsx +382 -0
  61. package/src/index.ts +14 -0
  62. package/src/jsi.d.ts +128 -0
  63. package/src/packCandles.ts +24 -0
  64. package/src/theme.ts +43 -0
  65. package/src/types.ts +27 -0
  66. package/src/useChartCore.ts +135 -0
@@ -0,0 +1,32 @@
1
+ // Crosshair drawing — two perpendicular dashed lines with a hollow ring at
2
+ // their intersection, kept in its own module alongside candles / labels so the
3
+ // chart orchestrator stays thin.
4
+ //
5
+ // Non-interactive for now: the intersection is parked at the center of the
6
+ // candle area. Once pointer tracking lands, the position will come from
7
+ // chart.crosshair_x_px / crosshair_y_px (see chart.h).
8
+
9
+ #pragma once
10
+
11
+ #include "vroom/vroom_chart.h"
12
+
13
+ class SkCanvas;
14
+ struct VroomChart;
15
+
16
+ namespace vroom::crosshair {
17
+
18
+ // Draws the crosshair. `candle_right` is the x of the y-axis strip. The
19
+ // horizontal line and ring live in the price pane: the ring's y is clamped to
20
+ // `candle_area_h` (the price-pane bottom) and the horizontal line stops at
21
+ // `candle_right`. The vertical line spans from the top down to `vline_bottom`
22
+ // (the bottom of the indicator region, so it stays visible over any RSI/MACD
23
+ // panes); pass `candle_area_h` for both when there are no panes. `snap_x` is the
24
+ // candle-snapped x for the vertical line and ring.
25
+ void draw(SkCanvas* canvas,
26
+ const VroomChart& chart,
27
+ float candle_right,
28
+ float candle_area_h,
29
+ float vline_bottom,
30
+ float snap_x);
31
+
32
+ } // namespace vroom::crosshair
@@ -0,0 +1,22 @@
1
+ #include "fonts.h"
2
+
3
+ #pragma clang diagnostic push
4
+ #pragma clang diagnostic ignored "-Wdocumentation"
5
+ #include "include/core/SkTypeface.h"
6
+ #pragma clang diagnostic pop
7
+
8
+ #include <utility>
9
+
10
+ namespace vroom {
11
+
12
+ namespace {
13
+ sk_sp<SkTypeface> g_axis_typeface;
14
+ } // namespace
15
+
16
+ sk_sp<SkTypeface> axis_typeface() { return g_axis_typeface; }
17
+
18
+ void set_axis_typeface(sk_sp<SkTypeface> typeface) {
19
+ g_axis_typeface = std::move(typeface);
20
+ }
21
+
22
+ } // namespace vroom
@@ -0,0 +1,25 @@
1
+ // Global typeface for axis/label text rendering.
2
+ //
3
+ // Loaded once at startup by the bridge layer (which uses RN-Skia's platform-
4
+ // context font manager to grab the system default). Held as a process-global
5
+ // so all charts share it; per-chart font configuration is not modeled yet.
6
+
7
+ #pragma once
8
+
9
+ #pragma clang diagnostic push
10
+ #pragma clang diagnostic ignored "-Wdocumentation"
11
+ #include "include/core/SkRefCnt.h"
12
+ #pragma clang diagnostic pop
13
+
14
+ class SkTypeface;
15
+
16
+ namespace vroom {
17
+
18
+ // Returns the currently-installed axis typeface, or nullptr if none has been
19
+ // set yet. Drawing code should null-check.
20
+ sk_sp<SkTypeface> axis_typeface();
21
+
22
+ // Replaces the axis typeface. Charts pick it up on their next draw.
23
+ void set_axis_typeface(sk_sp<SkTypeface> typeface);
24
+
25
+ } // namespace vroom
@@ -0,0 +1,340 @@
1
+ #include "labels.h"
2
+
3
+ #pragma clang diagnostic push
4
+ #pragma clang diagnostic ignored "-Wdocumentation"
5
+ #include "include/core/SkCanvas.h"
6
+ #include "include/core/SkFont.h"
7
+ #include "include/core/SkFontMetrics.h"
8
+ #include "include/core/SkFontTypes.h"
9
+ #include "include/core/SkPaint.h"
10
+ #include "include/core/SkTypeface.h"
11
+ #pragma clang diagnostic pop
12
+
13
+ #include <algorithm>
14
+ #include <cmath>
15
+ #include <cstdio>
16
+ #include <cstring>
17
+ #include <ctime>
18
+
19
+ #include "chart.h"
20
+ #include "fonts.h"
21
+ #include "ticks.h"
22
+ #include "viewport.h"
23
+
24
+ namespace vroom::labels {
25
+
26
+ namespace {
27
+
28
+ // Fuzzy equality for price keys: a label like 65.000000001 should still match
29
+ // an existing fade entry at 65.0 after a few FP roundtrips.
30
+ bool price_matches(double a, double b) {
31
+ const double tol = std::max(std::abs(a), std::abs(b)) * 1e-9 + 1e-9;
32
+ return std::abs(a - b) < tol;
33
+ }
34
+
35
+ // Advance a label's opacity toward its target by `step`. On the first render
36
+ // of a chart `chart.last_dt_seconds == 0` (and we'd be stuck at opacity 0
37
+ // forever), so we snap to the target then; subsequent frames fade normally.
38
+ template <typename Fade>
39
+ void advance(Fade& f, float step, float dt) {
40
+ if (dt == 0.f) {
41
+ f.opacity = f.target;
42
+ } else if (f.opacity < f.target) {
43
+ f.opacity = std::min(f.target, f.opacity + step);
44
+ } else if (f.opacity > f.target) {
45
+ f.opacity = std::max(f.target, f.opacity - step);
46
+ }
47
+ }
48
+
49
+ } // namespace
50
+
51
+ // ----- Y-axis ---------------------------------------------------------------
52
+
53
+ void update_y_fades(VroomChart& chart,
54
+ const Layout& lay,
55
+ const PriceBounds& bounds) {
56
+ const double range = bounds.max - bounds.min;
57
+ if (range <= 0.0) return;
58
+
59
+ const float candle_area_h = vroom::price_pane_bottom(lay);
60
+ const double interval = vroom::pick_price_interval(range, candle_area_h);
61
+ if (interval <= 0.0) return;
62
+
63
+ for (auto& f : chart.y_fades) f.target = 0.f;
64
+
65
+ const double first = std::ceil(bounds.min / interval) * interval;
66
+ constexpr int kMaxLabels = 64;
67
+ int promoted = 0;
68
+ for (double price = first;
69
+ price <= bounds.max && promoted < kMaxLabels;
70
+ price += interval, ++promoted) {
71
+ bool found = false;
72
+ for (auto& f : chart.y_fades) {
73
+ if (price_matches(f.price, price)) {
74
+ f.target = 1.f;
75
+ found = true;
76
+ break;
77
+ }
78
+ }
79
+ if (!found) {
80
+ chart.y_fades.push_back(YLabelFade{price, 0.f, 1.f});
81
+ }
82
+ }
83
+
84
+ const float step = kFadeRate * chart.last_dt_seconds;
85
+ for (auto& f : chart.y_fades) advance(f, step, chart.last_dt_seconds);
86
+ }
87
+
88
+ void draw_y_gridlines(SkCanvas* canvas,
89
+ const VroomChart& chart,
90
+ const Layout& lay,
91
+ const PriceBounds& bounds,
92
+ float candle_right,
93
+ float candle_area_h) {
94
+ SkPaint grid;
95
+ grid.setColor(chart.theme.colors[VROOM_COLOR_GRID]);
96
+ grid.setStrokeWidth(1.f);
97
+ grid.setAntiAlias(true);
98
+ for (const auto& f : chart.y_fades) {
99
+ if (f.opacity <= 1e-3f) continue;
100
+ const float y = vroom::price_to_y(lay, bounds, f.price);
101
+ if (y < 0.f || y > candle_area_h) continue;
102
+ grid.setAlphaf(f.opacity);
103
+ canvas->drawLine(0.f, y, candle_right, y, grid);
104
+ }
105
+ }
106
+
107
+ void draw_y_labels(SkCanvas* canvas,
108
+ const VroomChart& chart,
109
+ const Layout& lay,
110
+ const PriceBounds& bounds) {
111
+ auto tf = vroom::axis_typeface();
112
+ if (!tf) return;
113
+
114
+ const float candle_area_h = vroom::price_pane_bottom(lay);
115
+
116
+ SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
117
+ font.setSubpixel(true);
118
+ font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
119
+
120
+ SkFontMetrics metrics;
121
+ font.getMetrics(&metrics);
122
+ const float cap_h = metrics.fCapHeight > 0
123
+ ? metrics.fCapHeight
124
+ : -metrics.fAscent * 0.7f;
125
+
126
+ SkPaint text_paint;
127
+ text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
128
+ text_paint.setAntiAlias(true);
129
+
130
+ // Horizontal center of the y-axis container ([width - y_axis_width, width]).
131
+ // Labels (and the price box) center on this so their text shares a column.
132
+ const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
133
+
134
+ for (const auto& f : chart.y_fades) {
135
+ if (f.opacity <= 1e-3f) continue;
136
+ const float y = vroom::price_to_y(lay, bounds, f.price);
137
+
138
+ char buf[32];
139
+ std::snprintf(buf, sizeof(buf), "%.2f", f.price);
140
+ const size_t len = std::strlen(buf);
141
+ const float text_w = font.measureText(
142
+ buf, len, SkTextEncoding::kUTF8);
143
+
144
+ const float text_x = axis_center_x - text_w * 0.5f;
145
+ const float baseline_y = y + cap_h * 0.5f;
146
+ if (baseline_y - cap_h < 0.f) continue;
147
+ if (baseline_y > candle_area_h) continue;
148
+
149
+ text_paint.setAlphaf(f.opacity);
150
+ canvas->drawString(buf, text_x, baseline_y, font, text_paint);
151
+ }
152
+ }
153
+
154
+ void gc_y_fades(VroomChart& chart) {
155
+ chart.y_fades.erase(
156
+ std::remove_if(chart.y_fades.begin(), chart.y_fades.end(),
157
+ [](const YLabelFade& f) {
158
+ return f.opacity <= 0.f && f.target <= 0.f;
159
+ }),
160
+ chart.y_fades.end());
161
+ }
162
+
163
+ // ----- X-axis ---------------------------------------------------------------
164
+
165
+ void update_x_fades(VroomChart& chart, const Layout& lay) {
166
+ if (chart.candles.empty()) return;
167
+ if (chart.visible_end_ms <= chart.visible_start_ms) return;
168
+
169
+ const float candle_area_w =
170
+ lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
171
+ if (candle_area_w <= 0.f) return;
172
+
173
+ const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
174
+ const int64_t interval = vroom::pick_time_interval(window_ms, candle_area_w);
175
+ if (interval <= 0) return;
176
+
177
+ for (auto& f : chart.x_fades) f.target = 0.f;
178
+
179
+ int64_t t = (chart.visible_start_ms / interval) * interval;
180
+ if (t < chart.visible_start_ms) t += interval;
181
+ constexpr int kMaxLabels = 64;
182
+ int promoted = 0;
183
+ for (; t <= chart.visible_end_ms && promoted < kMaxLabels;
184
+ t += interval, ++promoted) {
185
+ bool found = false;
186
+ for (auto& f : chart.x_fades) {
187
+ if (f.time_ms == t) {
188
+ f.target = 1.f;
189
+ found = true;
190
+ break;
191
+ }
192
+ }
193
+ if (!found) {
194
+ chart.x_fades.push_back(XLabelFade{t, 0.f, 1.f});
195
+ }
196
+ }
197
+
198
+ const float step = kFadeRate * chart.last_dt_seconds;
199
+ for (auto& f : chart.x_fades) advance(f, step, chart.last_dt_seconds);
200
+ }
201
+
202
+ void draw_x_gridlines(SkCanvas* canvas,
203
+ const VroomChart& chart,
204
+ float candle_area_w,
205
+ float candle_area_h) {
206
+ if (chart.visible_end_ms <= chart.visible_start_ms) return;
207
+ const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
208
+ if (window_ms <= 0 || candle_area_w <= 0.f) return;
209
+
210
+ SkPaint grid;
211
+ grid.setColor(chart.theme.colors[VROOM_COLOR_GRID]);
212
+ grid.setStrokeWidth(1.f);
213
+ grid.setAntiAlias(true);
214
+ for (const auto& f : chart.x_fades) {
215
+ if (f.opacity <= 1e-3f) continue;
216
+ const float frac =
217
+ static_cast<float>(f.time_ms - chart.visible_start_ms) /
218
+ static_cast<float>(window_ms);
219
+ const float x = frac * candle_area_w;
220
+ if (x < 0.f || x > candle_area_w) continue;
221
+ grid.setAlphaf(f.opacity);
222
+ canvas->drawLine(x, 0.f, x, candle_area_h, grid);
223
+ }
224
+ }
225
+
226
+ void draw_x_labels(SkCanvas* canvas,
227
+ const VroomChart& chart,
228
+ const Layout& lay) {
229
+ auto tf = vroom::axis_typeface();
230
+ if (!tf || chart.candles.empty()) return;
231
+ if (chart.visible_end_ms <= chart.visible_start_ms) return;
232
+
233
+ // X-axis labels live in the bottom strip, which stays anchored regardless
234
+ // of any indicator pane above it.
235
+ const float candle_area_h = vroom::x_axis_top(lay);
236
+ const float candle_area_w =
237
+ lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
238
+ if (candle_area_w <= 0.f) return;
239
+
240
+ const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
241
+ // Recompute interval here just to decide HH:MM vs MM/DD formatting.
242
+ const int64_t interval = vroom::pick_time_interval(window_ms, candle_area_w);
243
+
244
+ SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
245
+ font.setSubpixel(true);
246
+ font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
247
+
248
+ SkFontMetrics metrics;
249
+ font.getMetrics(&metrics);
250
+ const float cap_h = metrics.fCapHeight > 0
251
+ ? metrics.fCapHeight
252
+ : -metrics.fAscent * 0.7f;
253
+
254
+ SkPaint text_paint;
255
+ text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
256
+ text_paint.setAntiAlias(true);
257
+
258
+ const float baseline_y =
259
+ candle_area_h + (lay.x_axis_height_px + cap_h) * 0.5f;
260
+ const bool use_date = interval >= 24LL * 60 * 60 * 1000;
261
+ for (const auto& f : chart.x_fades) {
262
+ if (f.opacity <= 1e-3f) continue;
263
+ const float frac =
264
+ static_cast<float>(f.time_ms - chart.visible_start_ms) /
265
+ static_cast<float>(window_ms);
266
+ const float x_center = frac * candle_area_w;
267
+
268
+ char buf[16];
269
+ const time_t time_s = static_cast<time_t>(f.time_ms / 1000);
270
+ struct tm tm_buf;
271
+ localtime_r(&time_s, &tm_buf);
272
+ if (use_date) {
273
+ std::snprintf(buf, sizeof(buf), "%02d/%02d",
274
+ tm_buf.tm_mon + 1, tm_buf.tm_mday);
275
+ } else {
276
+ std::snprintf(buf, sizeof(buf), "%02d:%02d",
277
+ tm_buf.tm_hour, tm_buf.tm_min);
278
+ }
279
+
280
+ const size_t len = std::strlen(buf);
281
+ const float text_w = font.measureText(
282
+ buf, len, SkTextEncoding::kUTF8);
283
+ const float text_x = x_center - text_w * 0.5f;
284
+ if (text_x < 0.f) continue;
285
+ if (text_x + text_w > candle_area_w) continue;
286
+
287
+ text_paint.setAlphaf(f.opacity);
288
+ canvas->drawString(buf, text_x, baseline_y, font, text_paint);
289
+ }
290
+ }
291
+
292
+ void gc_x_fades(VroomChart& chart) {
293
+ chart.x_fades.erase(
294
+ std::remove_if(chart.x_fades.begin(), chart.x_fades.end(),
295
+ [](const XLabelFade& f) {
296
+ return f.opacity <= 0.f && f.target <= 0.f;
297
+ }),
298
+ chart.x_fades.end());
299
+ }
300
+
301
+ // ----- Axis-width sizing ----------------------------------------------------
302
+
303
+ void recompute_axis_width(VroomChart& chart) {
304
+ auto tf = vroom::axis_typeface();
305
+ if (!tf) {
306
+ chart.axis_width_px = 0.f;
307
+ return;
308
+ }
309
+ double hi, lo;
310
+ if (chart.price_bounds_initialized) {
311
+ hi = chart.price_bounds.max;
312
+ lo = chart.price_bounds.min;
313
+ } else if (!chart.candles.empty()) {
314
+ hi = chart.candles.front().high;
315
+ lo = chart.candles.front().low;
316
+ for (const auto& c : chart.candles) {
317
+ if (c.high > hi) hi = c.high;
318
+ if (c.low < lo) lo = c.low;
319
+ }
320
+ } else {
321
+ chart.axis_width_px = 0.f;
322
+ return;
323
+ }
324
+
325
+ SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
326
+ char buf_hi[32], buf_lo[32];
327
+ std::snprintf(buf_hi, sizeof(buf_hi), "%.2f", hi);
328
+ std::snprintf(buf_lo, sizeof(buf_lo), "%.2f", lo);
329
+ const float w_hi = font.measureText(
330
+ buf_hi, std::strlen(buf_hi), SkTextEncoding::kUTF8);
331
+ const float w_lo = font.measureText(
332
+ buf_lo, std::strlen(buf_lo), SkTextEncoding::kUTF8);
333
+ const float text_w = std::max(w_hi, w_lo);
334
+
335
+ constexpr float kPaddingLeft = 8.f; // separator ↔ text
336
+ constexpr float kPaddingRight = 6.f; // text ↔ screen edge
337
+ chart.axis_width_px = text_w + kPaddingLeft + kPaddingRight;
338
+ }
339
+
340
+ } // namespace vroom::labels
@@ -0,0 +1,85 @@
1
+ // Axis labels and their gridlines — both subsystems share a per-axis fade
2
+ // state so they animate in lockstep. When an interval swap happens (e.g.,
3
+ // 30m → 1h on x), the old labels and their gridlines fade out together, and
4
+ // the new ones fade in together.
5
+ //
6
+ // Free functions over `VroomChart&` rather than methods so the struct
7
+ // definition can stay in chart.h and the implementations can live here.
8
+
9
+ #pragma once
10
+
11
+ #include <cstdint>
12
+
13
+ class SkCanvas;
14
+ struct VroomChart;
15
+
16
+ namespace vroom {
17
+ struct Layout;
18
+ struct PriceBounds;
19
+ } // namespace vroom
20
+
21
+ namespace vroom::labels {
22
+
23
+ // Per-label fade state. Updated each frame: labels in the new active set get
24
+ // target=1 and fade in; labels falling out get target=0 and fade out.
25
+ struct YLabelFade {
26
+ double price;
27
+ float opacity = 0.f;
28
+ float target = 1.f;
29
+ };
30
+
31
+ struct XLabelFade {
32
+ int64_t time_ms;
33
+ float opacity = 0.f;
34
+ float target = 1.f;
35
+ };
36
+
37
+ // Opacity step per second (1 / 0.2s ≈ 200ms full fade).
38
+ inline constexpr float kFadeRate = 5.f;
39
+
40
+ // Y-axis (price) -----------------------------------------------------------
41
+
42
+ // Sets targets, walks the active price-interval set, advances opacities by
43
+ // `chart.last_dt_seconds`. Must be called once per frame before either
44
+ // `draw_y_gridlines` or `draw_y_labels`.
45
+ void update_y_fades(VroomChart& chart,
46
+ const Layout& lay,
47
+ const PriceBounds& bounds);
48
+
49
+ void draw_y_gridlines(SkCanvas* canvas,
50
+ const VroomChart& chart,
51
+ const Layout& lay,
52
+ const PriceBounds& bounds,
53
+ float candle_right,
54
+ float candle_area_h);
55
+
56
+ void draw_y_labels(SkCanvas* canvas,
57
+ const VroomChart& chart,
58
+ const Layout& lay,
59
+ const PriceBounds& bounds);
60
+
61
+ void gc_y_fades(VroomChart& chart);
62
+
63
+ // X-axis (time) ------------------------------------------------------------
64
+
65
+ void update_x_fades(VroomChart& chart, const Layout& lay);
66
+
67
+ void draw_x_gridlines(SkCanvas* canvas,
68
+ const VroomChart& chart,
69
+ float candle_area_w,
70
+ float candle_area_h);
71
+
72
+ void draw_x_labels(SkCanvas* canvas,
73
+ const VroomChart& chart,
74
+ const Layout& lay);
75
+
76
+ void gc_x_fades(VroomChart& chart);
77
+
78
+ // Y-axis width sizing ------------------------------------------------------
79
+
80
+ // Recomputes `chart.axis_width_px` to fit the widest formatted price label
81
+ // at the current bounds. No-op if the typeface isn't loaded yet — the layout
82
+ // then falls back to `VROOM_FLOAT_Y_AXIS_WIDTH_RATIO`.
83
+ void recompute_axis_width(VroomChart& chart);
84
+
85
+ } // namespace vroom::labels
@@ -0,0 +1,53 @@
1
+ #include "ma.h"
2
+
3
+ #include <cmath> // std::nan
4
+
5
+ namespace vroom::ma {
6
+
7
+ double source_value(const ::VroomCandle& c, int source) {
8
+ switch (source) {
9
+ case SOURCE_OPEN: return c.open;
10
+ case SOURCE_HIGH: return c.high;
11
+ case SOURCE_LOW: return c.low;
12
+ case SOURCE_HL2: return (c.high + c.low) / 2.0;
13
+ case SOURCE_HLC3: return (c.high + c.low + c.close) / 3.0;
14
+ case SOURCE_OHLC4: return (c.open + c.high + c.low + c.close) / 4.0;
15
+ case SOURCE_CLOSE:
16
+ default: return c.close;
17
+ }
18
+ }
19
+
20
+ void compute(const ::VroomCandle* candles, std::size_t n, int kind, int period,
21
+ int source, std::vector<double>& out) {
22
+ out.assign(n, std::nan(""));
23
+ if (!candles || period < 1) return;
24
+ const std::size_t P = static_cast<std::size_t>(period);
25
+ if (n < P) return;
26
+
27
+ std::vector<double> src(n);
28
+ for (std::size_t i = 0; i < n; ++i) src[i] = source_value(candles[i], source);
29
+
30
+ if (kind == KIND_EMA) {
31
+ // SMA-seeded EMA: seed at index P-1 with the SMA of the first P values.
32
+ double sum = 0.0;
33
+ for (std::size_t i = 0; i < P; ++i) sum += src[i];
34
+ double prev = sum / static_cast<double>(P);
35
+ out[P - 1] = prev;
36
+ const double alpha = 2.0 / (static_cast<double>(P) + 1.0);
37
+ for (std::size_t i = P; i < n; ++i) {
38
+ prev = alpha * src[i] + (1.0 - alpha) * prev;
39
+ out[i] = prev;
40
+ }
41
+ return;
42
+ }
43
+
44
+ // SMA via a rolling window sum (O(n)).
45
+ double sum = 0.0;
46
+ for (std::size_t i = 0; i < n; ++i) {
47
+ sum += src[i];
48
+ if (i >= P) sum -= src[i - P];
49
+ if (i >= P - 1) out[i] = sum / static_cast<double>(P);
50
+ }
51
+ }
52
+
53
+ } // namespace vroom::ma
@@ -0,0 +1,39 @@
1
+ // Moving averages (SMA / EMA) over a candle source series — pure, no Skia, so
2
+ // it builds into the unit-test target. Used by the price-pane overlay lines.
3
+
4
+ #pragma once
5
+
6
+ #include <cstddef>
7
+ #include <vector>
8
+
9
+ #include "vroom/vroom_chart.h" // ::VroomCandle
10
+
11
+ namespace vroom::ma {
12
+
13
+ // Source selector. Index order is mirrored on the TS side (MA_SOURCES).
14
+ enum Source {
15
+ SOURCE_CLOSE = 0,
16
+ SOURCE_OPEN,
17
+ SOURCE_HIGH,
18
+ SOURCE_LOW,
19
+ SOURCE_HL2, // (high + low) / 2
20
+ SOURCE_HLC3, // (high + low + close) / 3
21
+ SOURCE_OHLC4, // (open + high + low + close) / 4
22
+ };
23
+
24
+ // Kind selector (mirrored on the TS side).
25
+ enum Kind {
26
+ KIND_SMA = 0,
27
+ KIND_EMA = 1,
28
+ };
29
+
30
+ // The source price for a candle.
31
+ double source_value(const ::VroomCandle& c, int source);
32
+
33
+ // Computes the SMA (kind 0) or SMA-seeded EMA (kind 1) of the chosen source over
34
+ // [candles, candles+n). `period` is clamped to >= 1. Fills `out` (resized to n):
35
+ // out[i] is the average at candle i, or NaN for i < period-1 and when n < period.
36
+ void compute(const ::VroomCandle* candles, std::size_t n, int kind, int period,
37
+ int source, std::vector<double>& out);
38
+
39
+ } // namespace vroom::ma
@@ -0,0 +1,73 @@
1
+ #include "ma_overlay.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/SkPaint.h"
8
+ #include "include/core/SkPath.h"
9
+ #include "include/core/SkPathBuilder.h"
10
+ #include "include/core/SkRect.h"
11
+ #pragma clang diagnostic pop
12
+
13
+ #include <cmath>
14
+
15
+ #include "viewport.h"
16
+
17
+ namespace vroom::ma_overlay {
18
+
19
+ void draw(SkCanvas* canvas,
20
+ const Layout& lay,
21
+ const PriceBounds& bounds,
22
+ const ::VroomCandle* visible,
23
+ std::size_t n,
24
+ const double* values_visible,
25
+ int64_t window_ms,
26
+ int64_t visible_start_ms,
27
+ int64_t candle_duration_ms,
28
+ float candle_right,
29
+ float candle_area_h,
30
+ uint32_t color,
31
+ float width,
32
+ const unsigned char* break_before) {
33
+ if (!canvas || !values_visible || n == 0 || candle_right <= 0.f ||
34
+ candle_area_h <= 0.f) {
35
+ return;
36
+ }
37
+
38
+ // SkPathBuilder (not SkPath's edit methods, removed in newer Skia tips).
39
+ SkPathBuilder path;
40
+ bool pen_down = false;
41
+ for (std::size_t i = 0; i < n; ++i) {
42
+ const double v = values_visible[i];
43
+ if (!std::isfinite(v)) {
44
+ pen_down = false;
45
+ continue;
46
+ }
47
+ const float x = vroom::candle_center_x(
48
+ lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
49
+ window_ms);
50
+ const float y = vroom::price_to_y(lay, bounds, v);
51
+ if (pen_down && !(break_before && break_before[i])) {
52
+ path.lineTo(x, y);
53
+ } else {
54
+ path.moveTo(x, y);
55
+ pen_down = true;
56
+ }
57
+ }
58
+
59
+ SkPaint line;
60
+ line.setAntiAlias(true);
61
+ line.setColor(static_cast<SkColor>(color));
62
+ line.setStyle(SkPaint::kStroke_Style);
63
+ line.setStrokeWidth(width > 0.f ? width : 1.5f);
64
+
65
+ // Clip to the candle area so a line that runs off-range doesn't bleed into
66
+ // the axis strips or any indicator pane below.
67
+ canvas->save();
68
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
69
+ canvas->drawPath(path.detach(), line);
70
+ canvas->restore();
71
+ }
72
+
73
+ } // namespace vroom::ma_overlay