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
@@ -15,6 +15,7 @@
15
15
  #include <cstdio>
16
16
  #include <cstring>
17
17
  #include <ctime>
18
+ #include <vector>
18
19
 
19
20
  #include "chart.h"
20
21
  #include "fonts.h"
@@ -46,8 +47,25 @@ void advance(Fade& f, float step, float dt) {
46
47
  }
47
48
  }
48
49
 
50
+ // Drives a whole axis off the morph envelope instead of the per-label fades:
51
+ // everything in the phase's tick set shares one opacity, and anything outside it
52
+ // is dropped outright. At the midpoint the axis is transparent, so swapping the
53
+ // set there costs nothing visually.
54
+ template <typename Fade>
55
+ void apply_envelope(std::vector<Fade>& fades, float opacity) {
56
+ for (auto& f : fades) f.opacity = f.target > 0.f ? opacity : 0.f;
57
+ }
58
+
49
59
  } // namespace
50
60
 
61
+ IntervalPhase interval_phase(const VroomChart& chart) {
62
+ if (chart.morph_from.empty() || chart.interval_morph_t >= 1.f) return {};
63
+ const float t = chart.interval_morph_t;
64
+ constexpr float kMid = 0.5f;
65
+ if (t < kMid) return {true, true, 1.f - t / kMid};
66
+ return {true, false, (t - kMid) / (1.f - kMid)};
67
+ }
68
+
51
69
  // ----- Y-axis ---------------------------------------------------------------
52
70
 
53
71
  void update_y_fades(VroomChart& chart,
@@ -81,6 +99,11 @@ void update_y_fades(VroomChart& chart,
81
99
  }
82
100
  }
83
101
 
102
+ const auto phase = interval_phase(chart);
103
+ if (phase.active) {
104
+ apply_envelope(chart.y_fades, phase.opacity);
105
+ return;
106
+ }
84
107
  const float step = kFadeRate * chart.last_dt_seconds;
85
108
  for (auto& f : chart.y_fades) advance(f, step, chart.last_dt_seconds);
86
109
  }
@@ -162,24 +185,27 @@ void gc_y_fades(VroomChart& chart) {
162
185
 
163
186
  // ----- X-axis ---------------------------------------------------------------
164
187
 
165
- void update_x_fades(VroomChart& chart, const Layout& lay) {
188
+ void update_x_fades(VroomChart& chart,
189
+ const Layout& lay,
190
+ int64_t start_ms,
191
+ int64_t end_ms) {
166
192
  if (chart.candles.empty()) return;
167
- if (chart.visible_end_ms <= chart.visible_start_ms) return;
193
+ if (end_ms <= start_ms) return;
168
194
 
169
195
  const float candle_area_w =
170
196
  lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
171
197
  if (candle_area_w <= 0.f) return;
172
198
 
173
- const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
199
+ const int64_t window_ms = end_ms - start_ms;
174
200
  const vroom::TimeTick tick =
175
201
  vroom::pick_time_tick(window_ms, candle_area_w);
176
202
 
177
203
  for (auto& f : chart.x_fades) f.target = 0.f;
178
204
 
179
- int64_t t = vroom::first_tick_at_or_after(chart.visible_start_ms, tick);
205
+ int64_t t = vroom::first_tick_at_or_after(start_ms, tick);
180
206
  constexpr int kMaxLabels = 64;
181
207
  int promoted = 0;
182
- for (; t <= chart.visible_end_ms && promoted < kMaxLabels;
208
+ for (; t <= end_ms && promoted < kMaxLabels;
183
209
  t = vroom::next_tick(t, tick), ++promoted) {
184
210
  bool found = false;
185
211
  for (auto& f : chart.x_fades) {
@@ -194,16 +220,23 @@ void update_x_fades(VroomChart& chart, const Layout& lay) {
194
220
  }
195
221
  }
196
222
 
223
+ const auto phase = interval_phase(chart);
224
+ if (phase.active) {
225
+ apply_envelope(chart.x_fades, phase.opacity);
226
+ return;
227
+ }
197
228
  const float step = kFadeRate * chart.last_dt_seconds;
198
229
  for (auto& f : chart.x_fades) advance(f, step, chart.last_dt_seconds);
199
230
  }
200
231
 
201
232
  void draw_x_gridlines(SkCanvas* canvas,
202
233
  const VroomChart& chart,
234
+ int64_t start_ms,
235
+ int64_t end_ms,
203
236
  float candle_area_w,
204
237
  float candle_area_h) {
205
- if (chart.visible_end_ms <= chart.visible_start_ms) return;
206
- const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
238
+ if (end_ms <= start_ms) return;
239
+ const int64_t window_ms = end_ms - start_ms;
207
240
  if (window_ms <= 0 || candle_area_w <= 0.f) return;
208
241
 
209
242
  SkPaint grid;
@@ -212,9 +245,8 @@ void draw_x_gridlines(SkCanvas* canvas,
212
245
  grid.setAntiAlias(true);
213
246
  for (const auto& f : chart.x_fades) {
214
247
  if (f.opacity <= 1e-3f) continue;
215
- const float frac =
216
- static_cast<float>(f.time_ms - chart.visible_start_ms) /
217
- static_cast<float>(window_ms);
248
+ const float frac = static_cast<float>(f.time_ms - start_ms) /
249
+ static_cast<float>(window_ms);
218
250
  const float x = frac * candle_area_w;
219
251
  if (x < 0.f || x > candle_area_w) continue;
220
252
  grid.setAlphaf(f.opacity);
@@ -224,10 +256,12 @@ void draw_x_gridlines(SkCanvas* canvas,
224
256
 
225
257
  void draw_x_labels(SkCanvas* canvas,
226
258
  const VroomChart& chart,
227
- const Layout& lay) {
259
+ const Layout& lay,
260
+ int64_t start_ms,
261
+ int64_t end_ms) {
228
262
  auto tf = vroom::axis_typeface();
229
263
  if (!tf || chart.candles.empty()) return;
230
- if (chart.visible_end_ms <= chart.visible_start_ms) return;
264
+ if (end_ms <= start_ms) return;
231
265
 
232
266
  // X-axis labels live in the bottom strip, which stays anchored regardless
233
267
  // of any indicator pane above it.
@@ -236,7 +270,7 @@ void draw_x_labels(SkCanvas* canvas,
236
270
  lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
237
271
  if (candle_area_w <= 0.f) return;
238
272
 
239
- const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
273
+ const int64_t window_ms = end_ms - start_ms;
240
274
  // Recompute the cadence here just to decide label formatting.
241
275
  const vroom::TimeTick tick =
242
276
  vroom::pick_time_tick(window_ms, candle_area_w);
@@ -262,9 +296,8 @@ void draw_x_labels(SkCanvas* canvas,
262
296
  tick.step_ms >= 24LL * 60 * 60 * 1000;
263
297
  for (const auto& f : chart.x_fades) {
264
298
  if (f.opacity <= 1e-3f) continue;
265
- const float frac =
266
- static_cast<float>(f.time_ms - chart.visible_start_ms) /
267
- static_cast<float>(window_ms);
299
+ const float frac = static_cast<float>(f.time_ms - start_ms) /
300
+ static_cast<float>(window_ms);
268
301
  const float x_center = frac * candle_area_w;
269
302
 
270
303
  char buf[16];
@@ -34,14 +34,33 @@ struct XLabelFade {
34
34
  float target = 1.f;
35
35
  };
36
36
 
37
- // Opacity step per second (1 / 0.2s ≈ 200ms full fade).
37
+ // Opacity step per second (1 / 0.2s ≈ 200ms full fade). Paces the incremental
38
+ // per-label fades; an interval morph overrides them with the envelope below.
38
39
  inline constexpr float kFadeRate = 5.f;
39
40
 
41
+ // How an axis behaves during an interval morph: the pre-switch ticks fade out
42
+ // over the first half, the new ones fade in over the second. Nothing translates
43
+ // — the tick set is swapped at the midpoint, while the axis is fully
44
+ // transparent, so a label is never seen moving between two positions.
45
+ //
46
+ // Both halves are clocked off the morph's own eased progress, so the axes take
47
+ // `transitionMs`, follow `transitionEasing`, and land with the candles.
48
+ struct IntervalPhase {
49
+ bool active = false; // false: fade per-label at kFadeRate as usual
50
+ bool outgoing = false; // true: first half — pre-switch ticks and scale
51
+ float opacity = 1.f; // whole-axis multiplier, 1 → 0 → 1
52
+ };
53
+
54
+ // Derived purely from the chart's morph state, so callers can ask for it freely.
55
+ IntervalPhase interval_phase(const VroomChart& chart);
56
+
40
57
  // Y-axis (price) -----------------------------------------------------------
41
58
 
42
59
  // Sets targets, walks the active price-interval set, advances opacities by
43
60
  // `chart.last_dt_seconds`. Must be called once per frame before either
44
- // `draw_y_gridlines` or `draw_y_labels`.
61
+ // `draw_y_gridlines` or `draw_y_labels`. `bounds` is the scale to lay ticks out
62
+ // against, which during an interval morph's outgoing half is the pre-switch one
63
+ // (`chart.morph_from_bounds`) — pass the same value to the draw calls.
45
64
  void update_y_fades(VroomChart& chart,
46
65
  const Layout& lay,
47
66
  const PriceBounds& bounds);
@@ -62,16 +81,26 @@ void gc_y_fades(VroomChart& chart);
62
81
 
63
82
  // X-axis (time) ------------------------------------------------------------
64
83
 
65
- void update_x_fades(VroomChart& chart, const Layout& lay);
84
+ // `start_ms`/`end_ms` are the window to lay ticks out against — the pre-switch
85
+ // one during an interval morph's outgoing half, `chart.visible_*` otherwise.
86
+ // Every x-axis call in a frame must be given the same window.
87
+ void update_x_fades(VroomChart& chart,
88
+ const Layout& lay,
89
+ int64_t start_ms,
90
+ int64_t end_ms);
66
91
 
67
92
  void draw_x_gridlines(SkCanvas* canvas,
68
93
  const VroomChart& chart,
94
+ int64_t start_ms,
95
+ int64_t end_ms,
69
96
  float candle_area_w,
70
97
  float candle_area_h);
71
98
 
72
99
  void draw_x_labels(SkCanvas* canvas,
73
100
  const VroomChart& chart,
74
- const Layout& lay);
101
+ const Layout& lay,
102
+ int64_t start_ms,
103
+ int64_t end_ms);
75
104
 
76
105
  void gc_x_fades(VroomChart& chart);
77
106
 
@@ -11,47 +11,12 @@
11
11
  #include "include/core/SkPoint.h"
12
12
  #include "include/core/SkRect.h"
13
13
  #include "include/core/SkShader.h"
14
- #include "include/core/SkTileMode.h"
15
- // Skia's linear-gradient API differs across the Skia versions we build against:
16
- // the WASM build uses a newer Skia (SkGradient.h + SkShaders::LinearGradient),
17
- // while react-native-skia bundles an older one (SkGradientShader::MakeLinear).
18
- // Neither ships both headers, so select whichever is present.
19
- #if __has_include("include/effects/SkGradient.h")
20
- # include "include/core/SkSpan.h"
21
- # include "include/effects/SkGradient.h"
22
- # define VROOM_SK_MODERN_GRADIENT 1
23
- #else
24
- # include "include/effects/SkGradientShader.h"
25
- # define VROOM_SK_MODERN_GRADIENT 0
26
- #endif
27
14
  #pragma clang diagnostic pop
28
15
 
29
16
  #include "chart.h"
17
+ #include "gradient.h"
30
18
 
31
19
  namespace vroom::liquidity {
32
- namespace {
33
-
34
- // A horizontal shader fading from transparent at the left of `pts` to `base` at
35
- // opacity `alpha` on the right, keeping RGB constant so the faded edge doesn't
36
- // darken toward black. Abstracts over the two Skia gradient APIs above.
37
- sk_sp<SkShader> left_fade_shader(const SkPoint pts[2], SkColor base, float alpha) {
38
- #if VROOM_SK_MODERN_GRADIENT
39
- const float r = SkColorGetR(base) / 255.f;
40
- const float g = SkColorGetG(base) / 255.f;
41
- const float b = SkColorGetB(base) / 255.f;
42
- const SkColor4f colors[2] = {SkColor4f{r, g, b, 0.f}, SkColor4f{r, g, b, alpha}};
43
- const SkGradient::Colors grad_colors(
44
- SkSpan<const SkColor4f>(colors, 2), SkTileMode::kClamp);
45
- const SkGradient grad(grad_colors, SkGradient::Interpolation{});
46
- return SkShaders::LinearGradient(pts, grad);
47
- #else
48
- const auto a = static_cast<U8CPU>(alpha * 255.f + 0.5f);
49
- const SkColor colors[2] = {SkColorSetA(base, 0), SkColorSetA(base, a)};
50
- return SkGradientShader::MakeLinear(pts, colors, nullptr, 2, SkTileMode::kClamp);
51
- #endif
52
- }
53
-
54
- } // namespace
55
20
 
56
21
  void draw(SkCanvas* canvas,
57
22
  const VroomChart& chart,
@@ -102,7 +67,8 @@ void draw(SkCanvas* canvas,
102
67
 
103
68
  SkPaint paint;
104
69
  paint.setAntiAlias(true);
105
- paint.setShader(left_fade_shader(pts, base, alpha));
70
+ // Transparent at the left of the band's reach, full `alpha` at the axis.
71
+ paint.setShader(vroom::linear_alpha_ramp(pts, base, 0.f, alpha));
106
72
  canvas->drawRect(SkRect::MakeLTRB(left_x, y_top, candle_right, y_bot), paint);
107
73
  }
108
74
 
@@ -7,16 +7,107 @@
7
7
  #include "include/core/SkPaint.h"
8
8
  #include "include/core/SkPath.h"
9
9
  #include "include/core/SkPathBuilder.h"
10
+ #include "include/core/SkPoint.h"
10
11
  #include "include/core/SkRect.h"
12
+ #include "include/core/SkShader.h"
11
13
  #pragma clang diagnostic pop
12
14
 
13
15
  #include <algorithm>
14
16
  #include <cmath>
15
17
 
18
+ #include "gradient.h"
16
19
  #include "viewport.h"
17
20
 
18
21
  namespace vroom::ma_overlay {
19
22
 
23
+ namespace {
24
+ // Strokes a built polyline, clipped to the candle area so a line that runs off
25
+ // range doesn't bleed into the axis strips or any indicator pane below.
26
+ void stroke_path(SkCanvas* canvas,
27
+ const SkPath& path,
28
+ float candle_right,
29
+ float candle_area_h,
30
+ uint32_t color,
31
+ float width,
32
+ float opacity) {
33
+ SkPaint line;
34
+ line.setAntiAlias(true);
35
+ line.setColor(static_cast<SkColor>(color));
36
+ // Fade the line in during the candle→line morph (multiplies the color alpha).
37
+ if (opacity < 1.f) {
38
+ line.setAlphaf(line.getAlphaf() * opacity);
39
+ }
40
+ line.setStyle(SkPaint::kStroke_Style);
41
+ line.setStrokeWidth(width > 0.f ? width : 1.5f);
42
+
43
+ canvas->save();
44
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
45
+ canvas->drawPath(path, line);
46
+ canvas->restore();
47
+ }
48
+
49
+ inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
50
+
51
+ // The close-price polyline, walked left to right. Shared by the stroke and the
52
+ // gradient fill so both derive from identical geometry, mid-morph included.
53
+ //
54
+ // Descending slots — position counting back from the right edge, the pairing a
55
+ // timeframe switch preserves — which walks the line left to right, so the vertex
56
+ // order matches draw()'s at morph_t == 1. Every candle has a close, so there are
57
+ // no gaps to break the subpath on.
58
+ SkPath build_close_path(const Layout& lay,
59
+ const PriceBounds& bounds,
60
+ const ::VroomCandle* visible,
61
+ std::size_t n,
62
+ int64_t window_ms,
63
+ int64_t visible_start_ms,
64
+ int64_t candle_duration_ms,
65
+ const CandleSnapshot* from,
66
+ std::size_t from_n,
67
+ float morph_t) {
68
+ const std::size_t from_count = vroom::morph_from_count(from, from_n, morph_t);
69
+ const std::size_t slots = std::max(n, from_count);
70
+ if (slots == 0) return SkPath();
71
+ const float area_w = vroom::candle_area_width(lay);
72
+
73
+ SkPathBuilder path;
74
+ bool pen_down = false;
75
+ for (std::size_t k = slots; k-- > 0;) {
76
+ const ::VroomCandle* to = (k < n) ? &visible[n - 1 - k] : nullptr;
77
+ const CandleSnapshot* frm = (k < from_count) ? &from[k] : nullptr;
78
+
79
+ float x, y;
80
+ if (to) {
81
+ const float tx = vroom::candle_center_x(
82
+ lay, to->time_ms, candle_duration_ms, visible_start_ms, window_ms);
83
+ const float ty = vroom::price_to_y(lay, bounds, to->close);
84
+ if (frm) {
85
+ // The capture is in band fractions, so it lands on the same pixels
86
+ // it occupied pre-switch even though the bounds changed.
87
+ x = lerp(frm->x * area_w, tx, morph_t);
88
+ y = lerp(vroom::y_at_fraction(lay, frm->close), ty, morph_t);
89
+ } else {
90
+ x = tx;
91
+ y = ty;
92
+ }
93
+ } else {
94
+ // A slot only the outgoing data had. Counts match in practice, so this
95
+ // just keeps gappy or short-history data from breaking the line.
96
+ x = frm->x * area_w;
97
+ y = vroom::y_at_fraction(lay, frm->close);
98
+ }
99
+
100
+ if (pen_down) {
101
+ path.lineTo(x, y);
102
+ } else {
103
+ path.moveTo(x, y);
104
+ pen_down = true;
105
+ }
106
+ }
107
+ return path.detach();
108
+ }
109
+ } // namespace
110
+
20
111
  void draw(SkCanvas* canvas,
21
112
  const Layout& lay,
22
113
  const PriceBounds& bounds,
@@ -60,21 +151,92 @@ void draw(SkCanvas* canvas,
60
151
  }
61
152
  }
62
153
 
63
- SkPaint line;
64
- line.setAntiAlias(true);
65
- line.setColor(static_cast<SkColor>(color));
66
- // Fade the line in during the candle→line morph (multiplies the color alpha).
67
- if (opacity < 1.f) {
68
- line.setAlphaf(line.getAlphaf() * opacity);
69
- }
70
- line.setStyle(SkPaint::kStroke_Style);
71
- line.setStrokeWidth(width > 0.f ? width : 1.5f);
154
+ stroke_path(canvas, path.detach(), candle_right, candle_area_h, color, width,
155
+ opacity);
156
+ }
157
+
158
+ void draw_close_line(SkCanvas* canvas,
159
+ const Layout& lay,
160
+ const PriceBounds& bounds,
161
+ const ::VroomCandle* visible,
162
+ std::size_t n,
163
+ int64_t window_ms,
164
+ int64_t visible_start_ms,
165
+ int64_t candle_duration_ms,
166
+ float candle_right,
167
+ float candle_area_h,
168
+ uint32_t color,
169
+ float width,
170
+ float opacity,
171
+ const CandleSnapshot* from,
172
+ std::size_t from_n,
173
+ float morph_t) {
174
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
175
+ opacity = std::clamp(opacity, 0.f, 1.f);
176
+ if (opacity <= 0.f) return;
177
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
178
+
179
+ const SkPath path = build_close_path(lay, bounds, visible, n, window_ms,
180
+ visible_start_ms, candle_duration_ms,
181
+ from, from_n, morph_t);
182
+ if (path.isEmpty()) return;
183
+
184
+ stroke_path(canvas, path, candle_right, candle_area_h, color, width, opacity);
185
+ }
186
+
187
+ void draw_close_gradient(SkCanvas* canvas,
188
+ const Layout& lay,
189
+ const PriceBounds& bounds,
190
+ const ::VroomCandle* visible,
191
+ std::size_t n,
192
+ int64_t window_ms,
193
+ int64_t visible_start_ms,
194
+ int64_t candle_duration_ms,
195
+ float candle_right,
196
+ float candle_area_h,
197
+ uint32_t color,
198
+ float gradient_opacity,
199
+ float opacity,
200
+ const CandleSnapshot* from,
201
+ std::size_t from_n,
202
+ float morph_t) {
203
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
204
+ gradient_opacity = std::clamp(gradient_opacity, 0.f, 1.f);
205
+ opacity = std::clamp(opacity, 0.f, 1.f);
206
+ const float alpha = gradient_opacity * opacity;
207
+ if (alpha <= 0.f) return;
208
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
209
+
210
+ const SkPath line = build_close_path(lay, bounds, visible, n, window_ms,
211
+ visible_start_ms, candle_duration_ms,
212
+ from, from_n, morph_t);
213
+ if (line.isEmpty()) return;
214
+
215
+ // The polyline runs strictly left to right, so its bounds give the first and
216
+ // last vertex x plus the peak the ramp starts from. A peak above the pane
217
+ // (line zoomed off the top) would stretch the ramp, so anchor at the pane.
218
+ const SkRect b = line.getBounds();
219
+ const float top = std::max(b.top(), 0.f);
220
+ if (top >= candle_area_h) return;
221
+
222
+ // Drop from the last vertex to the pane bottom and back along it, turning the
223
+ // polyline into the closed region beneath the line.
224
+ SkPathBuilder area(line);
225
+ area.lineTo(b.right(), candle_area_h);
226
+ area.lineTo(b.left(), candle_area_h);
227
+ area.close();
228
+
229
+ const SkPoint pts[2] = {SkPoint::Make(0.f, top),
230
+ SkPoint::Make(0.f, candle_area_h)};
231
+ SkPaint fill;
232
+ fill.setAntiAlias(true);
233
+ fill.setStyle(SkPaint::kFill_Style);
234
+ fill.setShader(
235
+ vroom::linear_alpha_ramp(pts, static_cast<SkColor>(color), alpha, 0.f));
72
236
 
73
- // Clip to the candle area so a line that runs off-range doesn't bleed into
74
- // the axis strips or any indicator pane below.
75
237
  canvas->save();
76
238
  canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
77
- canvas->drawPath(path.detach(), line);
239
+ canvas->drawPath(area.detach(), fill);
78
240
  canvas->restore();
79
241
  }
80
242
 
@@ -11,6 +11,7 @@
11
11
  class SkCanvas;
12
12
 
13
13
  namespace vroom {
14
+ struct CandleSnapshot;
14
15
  struct Layout;
15
16
  struct PriceBounds;
16
17
  } // namespace vroom
@@ -40,12 +41,63 @@ void draw(SkCanvas* canvas,
40
41
  const unsigned char* break_before = nullptr,
41
42
  float opacity = 1.f);
42
43
 
44
+ // The close-price polyline of line-chart mode. Equivalent to draw() fed the
45
+ // visible closes, plus the interval morph: `from` / `from_n` is the outgoing
46
+ // geometry indexed from the right of the visible slice (slot 0 = newest) and
47
+ // `morph_t` (0..1) is the eased progress, so each vertex slides from the close it
48
+ // had before the timeframe switch to its new one. `morph_t == 1` (the default)
49
+ // draws `visible` alone.
50
+ //
51
+ // Separate from draw() because the capture holds candle closes — it can't stand
52
+ // in for an indicator's values.
53
+ void draw_close_line(SkCanvas* canvas,
54
+ const Layout& lay,
55
+ const PriceBounds& bounds,
56
+ const ::VroomCandle* visible,
57
+ std::size_t n,
58
+ int64_t window_ms,
59
+ int64_t visible_start_ms,
60
+ int64_t candle_duration_ms,
61
+ float candle_right,
62
+ float candle_area_h,
63
+ uint32_t color,
64
+ float width,
65
+ float opacity = 1.f,
66
+ const CandleSnapshot* from = nullptr,
67
+ std::size_t from_n = 0,
68
+ float morph_t = 1.f);
69
+
70
+ // The area beneath the close-price polyline, filled with `color` ramping from
71
+ // `gradient_opacity` at the line's peak to fully transparent at the pane bottom.
72
+ // Takes the same geometry and morph arguments as draw_close_line and builds the
73
+ // identical polyline, so the fill tracks the line through a timeframe switch.
74
+ //
75
+ // `opacity` multiplies the ramp, which is what fades the fill in alongside the
76
+ // line during the candle→line morph. Draws nothing when either opacity is 0.
77
+ // Belongs behind the volume bars, so it's a separate call rather than part of
78
+ // draw_close_line.
79
+ void draw_close_gradient(SkCanvas* canvas,
80
+ const Layout& lay,
81
+ const PriceBounds& bounds,
82
+ const ::VroomCandle* visible,
83
+ std::size_t n,
84
+ int64_t window_ms,
85
+ int64_t visible_start_ms,
86
+ int64_t candle_duration_ms,
87
+ float candle_right,
88
+ float candle_area_h,
89
+ uint32_t color,
90
+ float gradient_opacity,
91
+ float opacity = 1.f,
92
+ const CandleSnapshot* from = nullptr,
93
+ std::size_t from_n = 0,
94
+ float morph_t = 1.f);
95
+
43
96
  // Fills the closed region between two aligned series (NaN where undefined)
44
97
  // with `color` at its alpha × `opacity` — used for the Bollinger Band fill.
45
98
  // Runs where either series is NaN are skipped, so the fill never bridges the
46
- // warmup gap. Plain-alpha SkPaint fill, no gradient shader (the Skia gradient
47
- // APIs diverge across our pinned versions; see liquidity.cpp). Clipped to the
48
- // candle area like draw().
99
+ // warmup gap. Plain-alpha SkPaint fill, no gradient ramp (see gradient.h).
100
+ // Clipped to the candle area like draw().
49
101
  void fill_between(SkCanvas* canvas,
50
102
  const Layout& lay,
51
103
  const PriceBounds& bounds,
@@ -2,61 +2,32 @@
2
2
 
3
3
  #include <cmath> // std::nan, std::isfinite
4
4
 
5
- namespace vroom::macd {
6
-
7
- namespace {
8
- // SMA-seeded EMA over `src` (which may have a leading NaN run). Fills `out`
9
- // (resized to src.size()): NaN until the seed index, the seed = SMA of the
10
- // first `period` finite values, then ema = alpha*src + (1-alpha)*prev.
11
- void ema_seeded(const std::vector<double>& src, int period,
12
- std::vector<double>& out) {
13
- const std::size_t n = src.size();
14
- out.assign(n, std::nan(""));
15
- if (period < 1) return;
16
- const std::size_t P = static_cast<std::size_t>(period);
17
-
18
- std::size_t f = 0;
19
- while (f < n && !std::isfinite(src[f])) ++f;
20
- if (f >= n || f + P > n) return; // not enough finite values to seed
21
-
22
- const std::size_t seed = f + P - 1;
23
- double sum = 0.0;
24
- for (std::size_t k = f; k <= seed; ++k) sum += src[k];
25
- double prev = sum / static_cast<double>(P);
26
- out[seed] = prev;
5
+ #include "ma.h"
6
+ #include "series_ma.h"
27
7
 
28
- const double alpha = 2.0 / (static_cast<double>(P) + 1.0);
29
- for (std::size_t i = seed + 1; i < n; ++i) {
30
- if (!std::isfinite(src[i])) break; // series are contiguous after f
31
- prev = alpha * src[i] + (1.0 - alpha) * prev;
32
- out[i] = prev;
33
- }
34
- }
35
- } // namespace
8
+ namespace vroom::macd {
36
9
 
37
10
  void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
38
- int signal, std::vector<double>& macd_out,
39
- std::vector<double>& signal_out, std::vector<double>& hist_out) {
11
+ int signal, int source, int ma_kind, int signal_ma_kind,
12
+ std::vector<double>& macd_out, std::vector<double>& signal_out,
13
+ std::vector<double>& hist_out) {
40
14
  macd_out.assign(n, std::nan(""));
41
15
  signal_out.assign(n, std::nan(""));
42
16
  hist_out.assign(n, std::nan(""));
43
17
  if (!candles || fast < 1 || slow < 1 || signal < 1) return;
44
18
 
45
- std::vector<double> closes(n);
46
- for (std::size_t i = 0; i < n; ++i) closes[i] = candles[i].close;
47
-
48
- std::vector<double> ema_fast;
49
- std::vector<double> ema_slow;
50
- ema_seeded(closes, fast, ema_fast);
51
- ema_seeded(closes, slow, ema_slow);
19
+ std::vector<double> ma_fast;
20
+ std::vector<double> ma_slow;
21
+ vroom::ma::compute(candles, n, ma_kind, fast, source, ma_fast);
22
+ vroom::ma::compute(candles, n, ma_kind, slow, source, ma_slow);
52
23
 
53
24
  for (std::size_t i = 0; i < n; ++i) {
54
- if (std::isfinite(ema_fast[i]) && std::isfinite(ema_slow[i])) {
55
- macd_out[i] = ema_fast[i] - ema_slow[i];
25
+ if (std::isfinite(ma_fast[i]) && std::isfinite(ma_slow[i])) {
26
+ macd_out[i] = ma_fast[i] - ma_slow[i];
56
27
  }
57
28
  }
58
29
 
59
- ema_seeded(macd_out, signal, signal_out);
30
+ vroom::series_ma::smooth(macd_out, signal_ma_kind, signal, signal_out);
60
31
 
61
32
  for (std::size_t i = 0; i < n; ++i) {
62
33
  if (std::isfinite(macd_out[i]) && std::isfinite(signal_out[i])) {
@@ -1,8 +1,8 @@
1
1
  // MACD — pure computation, no Skia. Kept Skia-free so it builds into the
2
2
  // unit-test target. Mirrors rsi.cpp's NaN-warmup convention.
3
3
  //
4
- // MACD line = EMA(fast) - EMA(slow) of close
5
- // Signal line = EMA(signal) of the MACD line
4
+ // MACD line = MA(fast) - MA(slow) of the chosen price source
5
+ // Signal line = MA(signal) of the MACD line
6
6
  // Histogram = MACD - Signal
7
7
  // EMAs are SMA-seeded: NaN until the first `period` values exist (the seed is
8
8
  // their simple average), then ema = alpha*src + (1-alpha)*prev, alpha=2/(p+1).
@@ -16,14 +16,17 @@
16
16
 
17
17
  namespace vroom::macd {
18
18
 
19
- // Computes MACD over the closes of [candles, candles+n). fast/slow/signal are
20
- // clamped to >= 1 (callers pass 12/26/9 by default; slow should exceed fast).
19
+ // Computes MACD over [candles, candles+n). fast/slow/signal are clamped to >= 1
20
+ // (callers pass 12/26/9 by default; slow should exceed fast). `source` selects
21
+ // the price series (vroom::ma::Source); `ma_kind` picks the averaging for the
22
+ // fast/slow legs and `signal_ma_kind` for the signal line (vroom::ma::Kind).
21
23
  // Fills three series (each resized to n, NaN where undefined):
22
- // macd_out — EMA(fast) - EMA(slow), defined from index slow-1
23
- // signal_out — EMA(signal) of macd_out, defined from (slow-1)+(signal-1)
24
+ // macd_out — MA(fast) - MA(slow), defined from index slow-1
25
+ // signal_out — MA(signal) of macd_out, defined from (slow-1)+(signal-1)
24
26
  // hist_out — macd_out - signal_out, defined where both are
25
27
  void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
26
- int signal, std::vector<double>& macd_out,
27
- std::vector<double>& signal_out, std::vector<double>& hist_out);
28
+ int signal, int source, int ma_kind, int signal_ma_kind,
29
+ std::vector<double>& macd_out, std::vector<double>& signal_out,
30
+ std::vector<double>& hist_out);
28
31
 
29
32
  } // namespace vroom::macd