react-native-vroom-chart 0.6.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 (46) hide show
  1. package/cpp/VroomChartHostObject.cpp +150 -32
  2. package/cpp/_core_include/vroom/vroom_chart.h +180 -22
  3. package/cpp/_core_src/bollinger.h +1 -1
  4. package/cpp/_core_src/candles.cpp +138 -41
  5. package/cpp/_core_src/candles.h +11 -1
  6. package/cpp/_core_src/chart.cpp +91 -40
  7. package/cpp/_core_src/chart.h +55 -20
  8. package/cpp/_core_src/chart_facade.cpp +206 -66
  9. package/cpp/_core_src/gradient.cpp +46 -0
  10. package/cpp/_core_src/gradient.h +31 -0
  11. package/cpp/_core_src/labels.cpp +49 -16
  12. package/cpp/_core_src/labels.h +33 -4
  13. package/cpp/_core_src/liquidity.cpp +3 -37
  14. package/cpp/_core_src/ma_overlay.cpp +174 -12
  15. package/cpp/_core_src/ma_overlay.h +55 -3
  16. package/cpp/_core_src/macd.cpp +13 -42
  17. package/cpp/_core_src/macd.h +11 -8
  18. package/cpp/_core_src/macd_pane.cpp +57 -27
  19. package/cpp/_core_src/price_line_layout.h +1 -1
  20. package/cpp/_core_src/rsi.cpp +4 -18
  21. package/cpp/_core_src/rsi.h +6 -6
  22. package/cpp/_core_src/rsi_pane.cpp +34 -19
  23. package/cpp/_core_src/series_ma.cpp +64 -0
  24. package/cpp/_core_src/series_ma.h +30 -0
  25. package/cpp/_core_src/style_inherit.h +35 -0
  26. package/cpp/_core_src/theme.cpp +2 -1
  27. package/cpp/_core_src/viewport.cpp +36 -12
  28. package/cpp/_core_src/viewport.h +50 -0
  29. package/cpp/_core_src/volume.cpp +33 -8
  30. package/cpp/_core_src/volume.h +12 -1
  31. package/cpp/_core_src/volume_anim.cpp +32 -0
  32. package/cpp/_core_src/volume_anim.h +41 -0
  33. package/lib/index.d.mts +161 -28
  34. package/lib/index.d.ts +161 -28
  35. package/lib/index.js +156 -31
  36. package/lib/index.js.map +1 -1
  37. package/lib/index.mjs +156 -31
  38. package/lib/index.mjs.map +1 -1
  39. package/package.json +3 -3
  40. package/src/VroomChart.tsx +69 -3
  41. package/src/easing.ts +40 -0
  42. package/src/index.ts +3 -0
  43. package/src/jsi.d.ts +76 -20
  44. package/src/theme.ts +1 -0
  45. package/src/types.ts +3 -0
  46. package/src/useChartCore.ts +103 -27
@@ -20,17 +20,29 @@
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::macd_pane {
27
28
 
28
29
  namespace {
30
+ using vroom::style::color_or;
31
+ using vroom::style::width_or;
32
+
29
33
  constexpr SkColor kMacdLine = 0xff2962ff; // blue MACD line
30
34
  constexpr SkColor kSignalLine = 0xffff6d00; // orange signal line
31
35
  constexpr SkColor kZeroLine = 0xff484f58; // zero reference
32
36
  constexpr SkColor kDivider = 0xff21262d; // pane separator
33
37
  constexpr float kPadFrac = 0.85f; // keep curves off the band edges
38
+ constexpr float kLineWidth = 1.5f; // default stroke for both lines
39
+ constexpr float kFadedAlpha = 0.5f; // easing histogram bars
40
+
41
+ // The easing histogram bars default to their own base color dimmed, which is
42
+ // how the pane read before the colors were configurable.
43
+ SkColor faded_or(uint32_t configured, SkColor base) {
44
+ return vroom::style::faded_or(configured, base, kFadedAlpha);
45
+ }
34
46
  } // namespace
35
47
 
36
48
  void draw(SkCanvas* canvas,
@@ -51,23 +63,25 @@ void draw(SkCanvas* canvas,
51
63
  const float band_h = pane_bottom - pane_top;
52
64
  if (band_h <= 0.f) return;
53
65
 
66
+ const VroomMACD& cfg = chart.macd;
54
67
  const float mid = (pane_top + pane_bottom) * 0.5f;
55
68
  // User y-zoom scales the amplitude about the zero line (mid). 1.0 = the
56
69
  // default auto-fit; >1 zooms in, <1 zooms out.
57
70
  const float half =
58
71
  band_h * 0.5f * kPadFrac * static_cast<float>(chart.macd_y_scale);
59
72
 
60
- // Auto-scale symmetric about zero across all visible finite values.
73
+ // Auto-scale symmetric about zero across the finite values on show, so
74
+ // hiding a series lets the rest fill the pane.
61
75
  double scale = 0.0;
62
- auto track = [&](const double* s) {
63
- if (!s) return;
76
+ auto track = [&](const double* s, bool shown) {
77
+ if (!s || !shown) return;
64
78
  for (std::size_t i = 0; i < n; ++i) {
65
79
  if (std::isfinite(s[i])) scale = std::max(scale, std::abs(s[i]));
66
80
  }
67
81
  };
68
- track(macd_visible);
69
- track(signal_visible);
70
- track(hist_visible);
82
+ track(macd_visible, cfg.line_visible != 0);
83
+ track(signal_visible, cfg.signal_visible != 0);
84
+ track(hist_visible, cfg.hist_visible != 0);
71
85
 
72
86
  auto y_for = [&](double v) -> float {
73
87
  if (scale <= 0.0) return mid;
@@ -90,34 +104,44 @@ void draw(SkCanvas* canvas,
90
104
  canvas->clipRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom));
91
105
 
92
106
  // Zero line.
93
- SkPaint zero;
94
- zero.setAntiAlias(true);
95
- zero.setColor(kZeroLine);
96
- zero.setStrokeWidth(1.f);
97
- canvas->drawLine(0.f, mid, candle_right, mid, zero);
98
-
99
- // Histogram bars: from the zero line to y_for(hist), 4-color (green above /
100
- // red below, full alpha when accelerating, half when decelerating).
101
- if (hist_visible) {
107
+ if (cfg.zero_visible) {
108
+ SkPaint zero;
109
+ zero.setAntiAlias(true);
110
+ zero.setColor(color_or(cfg.zero_color, kZeroLine));
111
+ zero.setStrokeWidth(1.f);
112
+ canvas->drawLine(0.f, mid, candle_right, mid, zero);
113
+ }
114
+
115
+ // Histogram bars: from the zero line to y_for(hist), 4-color — above or
116
+ // below zero, each in a building and an easing shade. A bar is building
117
+ // while it grows away from zero and easing while it falls back toward it.
118
+ if (hist_visible && cfg.hist_visible) {
102
119
  const float body_w =
103
120
  vroom::candle_body_width(lay, window_ms, candle_duration_ms);
104
121
  const float half_body = body_w * 0.5f;
105
122
  const float zero_y = y_for(0.0);
106
- const SkColor bull = chart.theme.colors[VROOM_COLOR_ACCENT_BULL];
107
- const SkColor bear = chart.theme.colors[VROOM_COLOR_ACCENT_BEAR];
123
+ const SkColor up =
124
+ color_or(cfg.hist_up_color, chart.theme.colors[VROOM_COLOR_ACCENT_BULL]);
125
+ const SkColor down =
126
+ color_or(cfg.hist_down_color, chart.theme.colors[VROOM_COLOR_ACCENT_BEAR]);
127
+ const SkColor up_easing = faded_or(cfg.hist_up_fading_color, up);
128
+ const SkColor down_easing = faded_or(cfg.hist_down_fading_color, down);
108
129
  for (std::size_t i = 0; i < n; ++i) {
109
130
  const double h = hist_visible[i];
110
131
  if (!std::isfinite(h)) continue;
111
132
  const bool have_prev = i > 0 && std::isfinite(hist_visible[i - 1]);
112
133
  const double prev = have_prev ? hist_visible[i - 1] : h;
113
- bool accelerating = true;
134
+ bool building = true;
114
135
  if (have_prev) {
115
- accelerating = h >= 0.0 ? h >= prev : h <= prev;
136
+ building = h >= 0.0 ? h >= prev : h <= prev;
116
137
  }
117
138
  SkPaint bar;
118
139
  bar.setAntiAlias(true);
119
- bar.setColor(h >= 0.0 ? bull : bear);
120
- bar.setAlphaf(accelerating ? 1.0f : 0.5f);
140
+ if (h >= 0.0) {
141
+ bar.setColor(building ? up : up_easing);
142
+ } else {
143
+ bar.setColor(building ? down : down_easing);
144
+ }
121
145
  const float cx = vroom::candle_center_x(
122
146
  lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
123
147
  window_ms);
@@ -130,7 +154,7 @@ void draw(SkCanvas* canvas,
130
154
  }
131
155
 
132
156
  // MACD + signal lines.
133
- auto stroke_series = [&](const double* series, SkColor color) {
157
+ auto stroke_series = [&](const double* series, SkColor color, float width) {
134
158
  if (!series) return;
135
159
  SkPathBuilder path;
136
160
  bool pen_down = false;
@@ -155,11 +179,17 @@ void draw(SkCanvas* canvas,
155
179
  line.setAntiAlias(true);
156
180
  line.setColor(color);
157
181
  line.setStyle(SkPaint::kStroke_Style);
158
- line.setStrokeWidth(1.5f);
182
+ line.setStrokeWidth(width);
159
183
  canvas->drawPath(path.detach(), line);
160
184
  };
161
- stroke_series(signal_visible, kSignalLine);
162
- stroke_series(macd_visible, kMacdLine); // MACD over signal
185
+ if (cfg.signal_visible) {
186
+ stroke_series(signal_visible, color_or(cfg.signal_color, kSignalLine),
187
+ width_or(cfg.signal_width, kLineWidth));
188
+ }
189
+ if (cfg.line_visible) { // MACD over signal
190
+ stroke_series(macd_visible, color_or(cfg.line_color, kMacdLine),
191
+ width_or(cfg.line_width, kLineWidth));
192
+ }
163
193
 
164
194
  // Caption, top-left of the pane.
165
195
  auto tf = vroom::axis_typeface();
@@ -168,8 +198,8 @@ void draw(SkCanvas* canvas,
168
198
  font.setSubpixel(true);
169
199
  font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
170
200
  char caption[40];
171
- std::snprintf(caption, sizeof(caption), "MACD %d %d %d", chart.macd_fast,
172
- chart.macd_slow, chart.macd_signal);
201
+ std::snprintf(caption, sizeof(caption), "MACD %d %d %d", cfg.fast,
202
+ cfg.slow, cfg.signal);
173
203
  SkRect cb;
174
204
  font.measureText(caption, std::strlen(caption), SkTextEncoding::kUTF8,
175
205
  &cb);
@@ -17,7 +17,7 @@ constexpr float kPadV = 4.f; // above/below the text
17
17
  constexpr float kCorner = 6.f; // rounded-pill corner radius
18
18
 
19
19
  // Extra grab band above and below the stroke, so a 1px line is still an easy
20
- // target. Matches the tolerance TradingView's own chart libraries settled on.
20
+ // target. Matches the tolerance mainstream chart libraries settled on.
21
21
  constexpr float kLineHitTolerance = 7.f;
22
22
 
23
23
  // Gap between the label group and the price axis when right-aligned. Wide enough
@@ -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
  }
@@ -0,0 +1,64 @@
1
+ #include "series_ma.h"
2
+
3
+ #include <cmath> // std::nan, std::isfinite
4
+ #include <cstddef>
5
+
6
+ #include "ma.h"
7
+
8
+ namespace vroom::series_ma {
9
+
10
+ void ema_seeded(const std::vector<double>& src, int period,
11
+ std::vector<double>& out) {
12
+ const std::size_t n = src.size();
13
+ out.assign(n, std::nan(""));
14
+ if (period < 1) return;
15
+ const std::size_t P = static_cast<std::size_t>(period);
16
+
17
+ std::size_t f = 0;
18
+ while (f < n && !std::isfinite(src[f])) ++f;
19
+ if (f >= n || f + P > n) return; // not enough finite values to seed
20
+
21
+ const std::size_t seed = f + P - 1;
22
+ double sum = 0.0;
23
+ for (std::size_t k = f; k <= seed; ++k) sum += src[k];
24
+ double prev = sum / static_cast<double>(P);
25
+ out[seed] = prev;
26
+
27
+ const double alpha = 2.0 / (static_cast<double>(P) + 1.0);
28
+ for (std::size_t i = seed + 1; i < n; ++i) {
29
+ if (!std::isfinite(src[i])) break; // series are contiguous after f
30
+ prev = alpha * src[i] + (1.0 - alpha) * prev;
31
+ out[i] = prev;
32
+ }
33
+ }
34
+
35
+ void sma_seeded(const std::vector<double>& src, int period,
36
+ std::vector<double>& out) {
37
+ const std::size_t n = src.size();
38
+ out.assign(n, std::nan(""));
39
+ if (period < 1) return;
40
+ const std::size_t P = static_cast<std::size_t>(period);
41
+
42
+ std::size_t f = 0;
43
+ while (f < n && !std::isfinite(src[f])) ++f;
44
+ if (f >= n || f + P > n) return;
45
+
46
+ double sum = 0.0;
47
+ for (std::size_t i = f; i < n; ++i) {
48
+ if (!std::isfinite(src[i])) break; // series are contiguous after f
49
+ sum += src[i];
50
+ if (i >= f + P) sum -= src[i - P];
51
+ if (i >= f + P - 1) out[i] = sum / static_cast<double>(P);
52
+ }
53
+ }
54
+
55
+ void smooth(const std::vector<double>& src, int kind, int period,
56
+ std::vector<double>& out) {
57
+ if (kind == vroom::ma::KIND_SMA) {
58
+ sma_seeded(src, period, out);
59
+ } else {
60
+ ema_seeded(src, period, out);
61
+ }
62
+ }
63
+
64
+ } // namespace vroom::series_ma
@@ -0,0 +1,30 @@
1
+ // Moving averages over an already-computed series — pure computation, no Skia.
2
+ //
3
+ // Distinct from ma.h, which averages a *price* source off the candles. These
4
+ // smooth a derived series that may open with a run of NaN warmup (the MACD
5
+ // difference, an RSI line), so they skip that run and start counting from the
6
+ // first finite value. Both smoothers therefore produce their first value at the
7
+ // same index, which is what lets the MA kind be a config toggle.
8
+
9
+ #pragma once
10
+
11
+ #include <vector>
12
+
13
+ namespace vroom::series_ma {
14
+
15
+ // SMA-seeded EMA over `src`. Fills `out` (resized to src.size()): NaN until the
16
+ // seed index, the seed = SMA of the first `period` finite values, then
17
+ // ema = alpha*src + (1-alpha)*prev.
18
+ void ema_seeded(const std::vector<double>& src, int period,
19
+ std::vector<double>& out);
20
+
21
+ // Rolling SMA over `src`, skipping the same leading NaN run as ema_seeded.
22
+ void sma_seeded(const std::vector<double>& src, int period,
23
+ std::vector<double>& out);
24
+
25
+ // Dispatches on a vroom::ma KIND_* value: KIND_SMA picks sma_seeded, anything
26
+ // else ema_seeded.
27
+ void smooth(const std::vector<double>& src, int kind, int period,
28
+ std::vector<double>& out);
29
+
30
+ } // namespace vroom::series_ma
@@ -0,0 +1,35 @@
1
+ // Resolution of the indicator style "inherit sentinels".
2
+ //
3
+ // Every indicator style field is optional at the public API, so the marshalled
4
+ // struct needs a value that means "the host didn't set this". A fully
5
+ // transparent color and a non-positive width are those sentinels: they can't
6
+ // name anything drawable, so they're free to carry the meaning. The renderer
7
+ // swaps them for the theme value or the built-in default at draw time, which
8
+ // keeps the fallback in one place instead of freezing it into every bridge.
9
+
10
+ #pragma once
11
+
12
+ #pragma clang diagnostic push
13
+ #pragma clang diagnostic ignored "-Wdocumentation"
14
+ #include "include/core/SkColor.h"
15
+ #pragma clang diagnostic pop
16
+
17
+ namespace vroom::style {
18
+
19
+ inline SkColor color_or(uint32_t configured, SkColor fallback) {
20
+ return SkColorGetA(configured) == 0 ? fallback : configured;
21
+ }
22
+
23
+ // For an element whose default is another element's color dimmed — the easing
24
+ // MACD histogram bars, say.
25
+ inline SkColor faded_or(uint32_t configured, SkColor base, float alpha_frac) {
26
+ if (SkColorGetA(configured) != 0) return configured;
27
+ return SkColorSetA(base,
28
+ static_cast<U8CPU>(SkColorGetA(base) * alpha_frac));
29
+ }
30
+
31
+ inline float width_or(float configured, float fallback) {
32
+ return configured > 0.f ? configured : fallback;
33
+ }
34
+
35
+ } // namespace vroom::style
@@ -22,7 +22,7 @@ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
22
22
  0x00000000, // WICK_BEAR — transparent sentinel: inherit BEAR fill
23
23
  0xff26a69a, // ACCENT_BULL — classic teal-green (price indicator, volume, MACD)
24
24
  0xffef5350, // ACCENT_BEAR — classic red
25
- 0xffc9d1d9, // LINE — line-chart close polyline; neutral foreground (AXIS_TEXT tone)
25
+ 0xff8957e5, // LINE — line-chart close polyline; violet, matching the RSI line
26
26
  };
27
27
 
28
28
  constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
@@ -38,6 +38,7 @@ constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
38
38
  0.f, // WICK_ROUND_CAP — butt caps by default
39
39
  0.f, // VOLUME_RADIUS_PX — square by default
40
40
  1.5f, // LINE_WIDTH_PX — line-chart polyline stroke width
41
+ 0.28f, // LINE_GRADIENT_OPACITY — fill under the line, ramped to 0 at the pane bottom
41
42
  };
42
43
 
43
44
  } // namespace
@@ -10,8 +10,7 @@ float candle_body_width(const Layout& layout,
10
10
  int64_t window_ms,
11
11
  int64_t candle_duration_ms) {
12
12
  if (window_ms <= 0 || candle_duration_ms <= 0) return 0.f;
13
- const float usable =
14
- layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
13
+ const float usable = candle_area_width(layout);
15
14
  const double slot = static_cast<double>(usable) *
16
15
  (static_cast<double>(candle_duration_ms) /
17
16
  static_cast<double>(window_ms));
@@ -24,8 +23,7 @@ float candle_center_x(const Layout& layout,
24
23
  int64_t visible_start_ms,
25
24
  int64_t window_ms) {
26
25
  if (window_ms <= 0) return 0.f;
27
- const float usable =
28
- layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
26
+ const float usable = candle_area_width(layout);
29
27
  const double center_time =
30
28
  static_cast<double>(time_ms) +
31
29
  static_cast<double>(candle_duration_ms) * 0.5;
@@ -213,19 +211,45 @@ PriceBounds auto_price_bounds(const ::VroomCandle* candles, size_t count) {
213
211
  return {mid - half, mid + half};
214
212
  }
215
213
 
216
- float price_to_y(const Layout& layout,
217
- const PriceBounds& bounds,
218
- double price) {
214
+ PriceBounds preserve_envelope_bounds(const PriceBounds& old_axis,
215
+ const PriceBounds& old_env,
216
+ const PriceBounds& new_env) {
217
+ const double axis_range = old_axis.max - old_axis.min;
218
+ const double old_span = old_env.max - old_env.min;
219
+ const double new_span = new_env.max - new_env.min;
220
+ if (axis_range <= 0.0 || old_span <= 0.0 || new_span <= 0.0) return old_axis;
221
+
222
+ // Same span/range ratio => same pixel height for the envelope.
223
+ const double new_range = axis_range * (new_span / old_span);
224
+ // Fraction of the axis (from the bottom) where the old envelope's midpoint
225
+ // sat; putting the new midpoint at the same fraction pins the envelope box.
226
+ const double t =
227
+ ((old_env.min + old_env.max) * 0.5 - old_axis.min) / axis_range;
228
+ const double new_mid = (new_env.min + new_env.max) * 0.5;
229
+ return {new_mid - t * new_range, new_mid + (1.0 - t) * new_range};
230
+ }
231
+
232
+ double price_fraction(const PriceBounds& bounds, double price) {
233
+ const double range = bounds.max - bounds.min;
234
+ // Degenerate range: everything sits at the band midpoint, which keeps
235
+ // price_to_y's flat-series fallback intact.
236
+ if (range <= 0.0) return 0.5;
237
+ return (price - bounds.min) / range; // 0 at min, 1 at max
238
+ }
239
+
240
+ float y_at_fraction(const Layout& layout, double frac) {
219
241
  // The candle drawing area is the full height minus the x-axis strip and
220
242
  // any below-chart indicator pane.
221
243
  const float candle_area_h = price_pane_bottom(layout);
222
244
  const float top = candle_area_h * layout.top_padding_frac;
223
245
  const float bot = candle_area_h * (1.f - layout.bottom_padding_frac);
224
- const float draw_h = bot - top;
225
- const double range = bounds.max - bounds.min;
226
- if (range <= 0.0) return (top + bot) * 0.5f;
227
- const double t = (price - bounds.min) / range; // 0..1, 0 at min
228
- return bot - static_cast<float>(t) * draw_h; // invert: high price → low y
246
+ return bot - static_cast<float>(frac) * (bot - top); // invert: high → low y
247
+ }
248
+
249
+ float price_to_y(const Layout& layout,
250
+ const PriceBounds& bounds,
251
+ double price) {
252
+ return y_at_fraction(layout, price_fraction(bounds, price));
229
253
  }
230
254
 
231
255
  double y_to_price(const Layout& layout,
@@ -34,6 +34,12 @@ inline float x_axis_top(const Layout& l) {
34
34
  return l.height_px - l.x_axis_height_px;
35
35
  }
36
36
 
37
+ // Width available to candles: the full width minus the y-axis strip and the
38
+ // right-hand gutter.
39
+ inline float candle_area_width(const Layout& l) {
40
+ return l.width_px - l.y_axis_width_px - l.right_padding_px;
41
+ }
42
+
37
43
  struct PriceBounds {
38
44
  double min;
39
45
  double max;
@@ -45,6 +51,27 @@ struct IndexRange {
45
51
  size_t end;
46
52
  };
47
53
 
54
+ // One candle's geometry captured in normalized form for the interval morph:
55
+ // x as a fraction of the candle-area width, prices as fractions of the price
56
+ // band. Resolution-independent, so a resize mid-morph stays correct — and
57
+ // independent of the price bounds, so the captured geometry keeps its pixel
58
+ // position after a timeframe switch re-scales the y-axis.
59
+ struct CandleSnapshot {
60
+ float x;
61
+ float open, high, low, close;
62
+ bool bull; // close >= open; selects the fill / wick / border color
63
+ };
64
+
65
+ // How many captured slots still contribute to a frame — 0 once the morph is
66
+ // done, which releases the capture. Drawing routines take max(n, this) as their
67
+ // slot count, pairing the new candle at slot k (visible[n - 1 - k]) with the
68
+ // captured one (from[k]), both counting back from the right edge.
69
+ inline std::size_t morph_from_count(const CandleSnapshot* from,
70
+ std::size_t from_n,
71
+ float morph_t) {
72
+ return (from && morph_t < 1.f) ? from_n : 0;
73
+ }
74
+
48
75
  // Returns the indices of candles whose time_ms falls in [start_ms, end_ms].
49
76
  // When both are 0, returns the full range (Phase 1 default-everything behavior).
50
77
  // Candles must be sorted ascending by time_ms (invariant of the public API).
@@ -140,9 +167,32 @@ constexpr double kAutoYZoom = 1.5;
140
167
  // {0, 1} sentinel when count == 0 (callers keep their previous bounds).
141
168
  PriceBounds auto_price_bounds(const ::VroomCandle* candles, size_t count);
142
169
 
170
+ // Rescales an axis range so a data envelope keeps the pixel height *and* the
171
+ // pixel position it had before a data swap — the "scale lock" applied on a
172
+ // timeframe switch, where the same price action re-buckets into a smaller or
173
+ // larger high-low span.
174
+ //
175
+ // `old_axis` is the axis range that was in effect; `old_env` / `new_env` are the
176
+ // visible high-low envelopes (as returned by price_bounds) before and after the
177
+ // swap. Resolution-independent: both envelopes share the same draw band, so the
178
+ // band height cancels out. Returns `old_axis` unchanged when any span is
179
+ // degenerate.
180
+ PriceBounds preserve_envelope_bounds(const PriceBounds& old_axis,
181
+ const PriceBounds& old_env,
182
+ const PriceBounds& new_env);
183
+
143
184
  // Map a price to y in pixels. y=0 is top of the chart.
144
185
  float price_to_y(const Layout& layout, const PriceBounds& bounds, double price);
145
186
 
187
+ // Fraction of the price band a price sits at: 0 = bounds.min, 1 = bounds.max.
188
+ // Returns 0.5 for a degenerate range, matching price_to_y's midpoint fallback.
189
+ double price_fraction(const PriceBounds& bounds, double price);
190
+
191
+ // The y of a band fraction. Splits price_to_y in two so geometry can be stored
192
+ // independently of the price bounds (see CandleSnapshot):
193
+ // price_to_y(l, b, p) == y_at_fraction(l, price_fraction(b, p))
194
+ float y_at_fraction(const Layout& layout, double frac);
195
+
146
196
  // Inverse of price_to_y: map a pixel y in the price pane back to a price.
147
197
  // Returns bounds.min for a degenerate range or draw band.
148
198
  double y_to_price(const Layout& layout, const PriceBounds& bounds, float y);