react-native-vroom-chart 0.15.0 → 0.17.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 (53) hide show
  1. package/cpp/VroomChartHostObject.cpp +252 -1
  2. package/cpp/_core_include/vroom/vroom_chart.h +178 -4
  3. package/cpp/_core_src/atr.cpp +67 -0
  4. package/cpp/_core_src/atr.h +44 -0
  5. package/cpp/_core_src/atr_pane.cpp +162 -0
  6. package/cpp/_core_src/atr_pane.h +48 -0
  7. package/cpp/_core_src/candles.cpp +15 -14
  8. package/cpp/_core_src/chart.cpp +758 -200
  9. package/cpp/_core_src/chart.h +221 -3
  10. package/cpp/_core_src/chart_facade.cpp +236 -23
  11. package/cpp/_core_src/color_lerp.h +28 -0
  12. package/cpp/_core_src/fair_value_gaps.cpp +76 -0
  13. package/cpp/_core_src/fair_value_gaps.h +54 -0
  14. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  15. package/cpp/_core_src/fvg_overlay.h +43 -0
  16. package/cpp/_core_src/ichimoku.cpp +65 -0
  17. package/cpp/_core_src/ichimoku.h +45 -0
  18. package/cpp/_core_src/labels.cpp +3 -0
  19. package/cpp/_core_src/line_morph.h +159 -0
  20. package/cpp/_core_src/loading_line.cpp +183 -0
  21. package/cpp/_core_src/loading_line.h +38 -0
  22. package/cpp/_core_src/loading_wave.h +140 -0
  23. package/cpp/_core_src/ma_overlay.cpp +273 -45
  24. package/cpp/_core_src/ma_overlay.h +64 -2
  25. package/cpp/_core_src/macd.cpp +24 -1
  26. package/cpp/_core_src/macd.h +16 -0
  27. package/cpp/_core_src/macd_pane.cpp +71 -62
  28. package/cpp/_core_src/macd_pane.h +13 -1
  29. package/cpp/_core_src/pane_series.h +169 -0
  30. package/cpp/_core_src/price_indicator.cpp +21 -7
  31. package/cpp/_core_src/price_indicator.h +11 -1
  32. package/cpp/_core_src/price_indicator_anim.h +81 -0
  33. package/cpp/_core_src/rsi.cpp +4 -0
  34. package/cpp/_core_src/rsi.h +7 -0
  35. package/cpp/_core_src/rsi_pane.cpp +85 -29
  36. package/cpp/_core_src/rsi_pane.h +11 -1
  37. package/cpp/_core_src/theme.cpp +5 -0
  38. package/cpp/_core_src/tip_geometry.h +55 -0
  39. package/cpp/_core_src/viewport.h +68 -0
  40. package/lib/index.d.mts +281 -3
  41. package/lib/index.d.ts +281 -3
  42. package/lib/index.js +292 -14
  43. package/lib/index.js.map +1 -1
  44. package/lib/index.mjs +292 -14
  45. package/lib/index.mjs.map +1 -1
  46. package/package.json +1 -1
  47. package/src/VroomChart.tsx +33 -3
  48. package/src/dataTransitions.ts +47 -0
  49. package/src/index.ts +5 -0
  50. package/src/jsi.d.ts +139 -1
  51. package/src/theme.ts +1 -0
  52. package/src/types.ts +5 -0
  53. package/src/useChartCore.ts +402 -8
@@ -20,6 +20,9 @@
20
20
 
21
21
  #include "chart.h"
22
22
  #include "fonts.h"
23
+ #include "line_morph.h"
24
+ #include "macd.h"
25
+ #include "pane_series.h"
23
26
  #include "style_inherit.h"
24
27
  #include "theme.h"
25
28
  #include "viewport.h"
@@ -34,7 +37,6 @@ constexpr SkColor kMacdLine = 0xff2962ff; // blue MACD line
34
37
  constexpr SkColor kSignalLine = 0xffff6d00; // orange signal line
35
38
  constexpr SkColor kZeroLine = 0xff484f58; // zero reference
36
39
  constexpr SkColor kDivider = 0xff21262d; // pane separator
37
- constexpr float kPadFrac = 0.85f; // keep curves off the band edges
38
40
  constexpr float kLineWidth = 1.5f; // default stroke for both lines
39
41
  constexpr float kFadedAlpha = 0.5f; // easing histogram bars
40
42
 
@@ -58,34 +60,42 @@ void draw(SkCanvas* canvas,
58
60
  int64_t candle_duration_ms,
59
61
  float candle_right,
60
62
  float pane_top,
61
- float pane_bottom) {
62
- if (!canvas || n == 0 || candle_right <= 0.f) return;
63
+ float pane_bottom,
64
+ const vroom::LineMorph* macd_from,
65
+ const vroom::LineMorph* signal_from,
66
+ const vroom::LineMorph* hist_from,
67
+ float morph_t) {
68
+ if (!canvas || candle_right <= 0.f) return;
63
69
  const float band_h = pane_bottom - pane_top;
64
70
  if (band_h <= 0.f) return;
71
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
72
+ // A fade's outgoing half has no new data at all, so the captures are the
73
+ // whole frame. Nothing on either side means nothing to paint, shell
74
+ // included.
75
+ const std::size_t hist_slots =
76
+ vroom::pane_series::slots(n, hist_from, morph_t);
77
+ if (n == 0 && hist_slots == 0 &&
78
+ vroom::morph_line_count(macd_from, morph_t) == 0 &&
79
+ vroom::morph_line_count(signal_from, morph_t) == 0) {
80
+ return;
81
+ }
65
82
 
66
83
  const VroomMACD& cfg = chart.macd;
67
84
  const float mid = (pane_top + pane_bottom) * 0.5f;
68
- // User y-zoom scales the amplitude about the zero line (mid). 1.0 = the
69
- // default auto-fit; >1 zooms in, <1 zooms out.
70
- const float half =
71
- band_h * 0.5f * kPadFrac * static_cast<float>(chart.macd_y_scale);
72
85
 
73
86
  // Auto-scale symmetric about zero across the finite values on show, so
74
- // hiding a series lets the rest fill the pane.
75
- double scale = 0.0;
76
- auto track = [&](const double* s, bool shown) {
77
- if (!s || !shown) return;
78
- for (std::size_t i = 0; i < n; ++i) {
79
- if (std::isfinite(s[i])) scale = std::max(scale, std::abs(s[i]));
80
- }
81
- };
82
- track(macd_visible, cfg.line_visible != 0);
83
- track(signal_visible, cfg.signal_visible != 0);
84
- track(hist_visible, cfg.hist_visible != 0);
87
+ // hiding a series lets the rest fill the pane. User y-zoom scales the
88
+ // amplitude about the zero line; 1.0 is the default auto-fit.
89
+ const double scale = vroom::macd::autoscale(
90
+ cfg.line_visible ? macd_visible : nullptr,
91
+ cfg.signal_visible ? signal_visible : nullptr,
92
+ cfg.hist_visible ? hist_visible : nullptr, n);
85
93
 
86
94
  auto y_for = [&](double v) -> float {
87
- if (scale <= 0.0) return mid;
88
- return mid - static_cast<float>(v / scale) * half;
95
+ return pane_bottom -
96
+ static_cast<float>(
97
+ vroom::macd::band_fraction(v, scale, chart.macd_y_scale)) *
98
+ band_h;
89
99
  };
90
100
 
91
101
  // Mask the band (candles can overflow below the shortened price pane).
@@ -115,7 +125,9 @@ void draw(SkCanvas* canvas,
115
125
  // Histogram bars: from the zero line to y_for(hist), 4-color — above or
116
126
  // below zero, each in a building and an easing shade. A bar is building
117
127
  // while it grows away from zero and easing while it falls back toward it.
118
- if (hist_visible && cfg.hist_visible) {
128
+ if ((hist_visible || hist_from) && cfg.hist_visible) {
129
+ // The new resolution's width from the first frame, matching how
130
+ // candles::draw widens its bodies while their positions still slide.
119
131
  const float body_w =
120
132
  vroom::candle_body_width(lay, window_ms, candle_duration_ms);
121
133
  const float half_body = body_w * 0.5f;
@@ -126,68 +138,65 @@ void draw(SkCanvas* canvas,
126
138
  color_or(cfg.hist_down_color, chart.theme.colors[VROOM_COLOR_ACCENT_BEAR]);
127
139
  const SkColor up_easing = faded_or(cfg.hist_up_fading_color, up);
128
140
  const SkColor down_easing = faded_or(cfg.hist_down_fading_color, down);
129
- for (std::size_t i = 0; i < n; ++i) {
130
- const double h = hist_visible[i];
131
- if (!std::isfinite(h)) continue;
132
- const bool have_prev = i > 0 && std::isfinite(hist_visible[i - 1]);
133
- const double prev = have_prev ? hist_visible[i - 1] : h;
134
- bool building = true;
135
- if (have_prev) {
136
- building = h >= 0.0 ? h >= prev : h <= prev;
141
+ const std::size_t hist_from_count =
142
+ vroom::morph_line_count(hist_from, morph_t);
143
+ // Bars read their sign and direction off the drawn geometry rather than
144
+ // the values: mid-morph a top is a blend of two resolutions' numbers,
145
+ // and only where it ended up says which side of zero it is on. y grows
146
+ // downward, so above zero is the smaller y.
147
+ MorphVertex prev;
148
+ for (std::size_t j = 0; j < hist_slots; ++j) {
149
+ const MorphVertex p = vroom::pane_series::vertex(
150
+ lay, visible, n, hist_visible, window_ms, visible_start_ms,
151
+ candle_duration_ms, pane_top, pane_bottom, hist_from,
152
+ hist_from_count, morph_t, hist_slots - 1 - j, y_for);
153
+ if (!p.valid) {
154
+ prev = MorphVertex{};
155
+ continue;
137
156
  }
157
+ const bool above = p.y <= zero_y;
158
+ // Building while the bar grows away from zero, easing while it
159
+ // falls back toward it.
160
+ const bool building =
161
+ !prev.valid || (above ? p.y <= prev.y : p.y >= prev.y);
138
162
  SkPaint bar;
139
163
  bar.setAntiAlias(true);
140
- if (h >= 0.0) {
164
+ if (above) {
141
165
  bar.setColor(building ? up : up_easing);
142
166
  } else {
143
167
  bar.setColor(building ? down : down_easing);
144
168
  }
145
- const float cx = vroom::candle_center_x(
146
- lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
147
- window_ms);
148
- const float vy = y_for(h);
149
- const float top = std::min(zero_y, vy);
150
- const float bot = std::max(zero_y, vy);
151
- canvas->drawRect(
152
- SkRect::MakeLTRB(cx - half_body, top, cx + half_body, bot), bar);
169
+ canvas->drawRect(SkRect::MakeLTRB(p.x - half_body,
170
+ std::min(zero_y, p.y),
171
+ p.x + half_body,
172
+ std::max(zero_y, p.y)),
173
+ bar);
174
+ prev = p;
153
175
  }
154
176
  }
155
177
 
156
178
  // MACD + signal lines.
157
- auto stroke_series = [&](const double* series, SkColor color, float width) {
158
- if (!series) return;
159
- SkPathBuilder path;
160
- bool pen_down = false;
161
- for (std::size_t i = 0; i < n; ++i) {
162
- const double v = series[i];
163
- if (!std::isfinite(v)) {
164
- pen_down = false;
165
- continue;
166
- }
167
- const float x = vroom::candle_center_x(
168
- lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
169
- window_ms);
170
- const float y = y_for(v);
171
- if (pen_down) {
172
- path.lineTo(x, y);
173
- } else {
174
- path.moveTo(x, y);
175
- pen_down = true;
176
- }
177
- }
179
+ auto stroke_series = [&](const double* series, const vroom::LineMorph* from,
180
+ SkColor color, float width) {
181
+ if (!series && !from) return;
178
182
  SkPaint line;
179
183
  line.setAntiAlias(true);
180
184
  line.setColor(color);
181
185
  line.setStyle(SkPaint::kStroke_Style);
182
186
  line.setStrokeWidth(width);
183
- canvas->drawPath(path.detach(), line);
187
+ canvas->drawPath(
188
+ vroom::pane_series::build_path(
189
+ lay, visible, n, series, window_ms, visible_start_ms,
190
+ candle_duration_ms, pane_top, pane_bottom, from, morph_t, y_for),
191
+ line);
184
192
  };
185
193
  if (cfg.signal_visible) {
186
- stroke_series(signal_visible, color_or(cfg.signal_color, kSignalLine),
194
+ stroke_series(signal_visible, signal_from,
195
+ color_or(cfg.signal_color, kSignalLine),
187
196
  width_or(cfg.signal_width, kLineWidth));
188
197
  }
189
198
  if (cfg.line_visible) { // MACD over signal
190
- stroke_series(macd_visible, color_or(cfg.line_color, kMacdLine),
199
+ stroke_series(macd_visible, macd_from, color_or(cfg.line_color, kMacdLine),
191
200
  width_or(cfg.line_width, kLineWidth));
192
201
  }
193
202
 
@@ -13,6 +13,7 @@ class SkCanvas;
13
13
 
14
14
  namespace vroom {
15
15
  struct Layout;
16
+ struct LineMorph;
16
17
  } // namespace vroom
17
18
 
18
19
  struct VroomChart;
@@ -23,6 +24,13 @@ namespace vroom::macd_pane {
23
24
  // series are aligned with `visible` (NaN where undefined). The vertical scale
24
25
  // auto-fits the visible values symmetrically about zero; shares the candles'
25
26
  // horizontal mapping (candle_center_x) so it scrolls in lock-step.
27
+ //
28
+ // The three `*_from` captures plus `morph_t` are the interval morph: each holds
29
+ // its series' outgoing shape in fractions of the band, indexed from the right
30
+ // (slot 0 = newest), so the pane can re-fit across the switch without the shape
31
+ // moving. Histogram bars reshape the way candle bodies do — their tops slide
32
+ // while the zero line, which is fixed to the band center, stays put. A fade's
33
+ // outgoing half passes n = 0 with morph_t = 0, painting the captures alone.
26
34
  void draw(SkCanvas* canvas,
27
35
  const VroomChart& chart,
28
36
  const Layout& lay,
@@ -36,6 +44,10 @@ void draw(SkCanvas* canvas,
36
44
  int64_t candle_duration_ms,
37
45
  float candle_right,
38
46
  float pane_top,
39
- float pane_bottom);
47
+ float pane_bottom,
48
+ const LineMorph* macd_from = nullptr,
49
+ const LineMorph* signal_from = nullptr,
50
+ const LineMorph* hist_from = nullptr,
51
+ float morph_t = 1.f);
40
52
 
41
53
  } // namespace vroom::macd_pane
@@ -0,0 +1,169 @@
1
+ // Polyline plotting shared by the indicator panes (RSI, MACD, ATR), including
2
+ // the interval morph.
3
+ //
4
+ // Each pane maps values into its own band with its own auto-fit, so it hands
5
+ // over its y mapping and everything from the slot pairing down is identical
6
+ // across the three. The capture holds band *fractions* rather than values,
7
+ // which is what lets a pane re-fit across a timeframe switch without its
8
+ // outgoing shape moving.
9
+
10
+ #pragma once
11
+
12
+ #include <algorithm>
13
+ #include <cmath>
14
+ #include <cstddef>
15
+ #include <cstdint>
16
+
17
+ #pragma clang diagnostic push
18
+ #pragma clang diagnostic ignored "-Wdocumentation"
19
+ #include "include/core/SkPath.h"
20
+ #include "include/core/SkPathBuilder.h"
21
+ #pragma clang diagnostic pop
22
+
23
+ #include "line_morph.h"
24
+ #include "viewport.h"
25
+ #include "vroom/vroom_chart.h"
26
+
27
+ namespace vroom::pane_series {
28
+
29
+ // Slot `k` of a pane series in screen pixels, blended from the capture toward
30
+ // the new value. Slot 0 is the rightmost vertex on both sides — the pairing a
31
+ // timeframe switch preserves, the same one candles::draw uses.
32
+ template <typename YFor>
33
+ MorphVertex vertex(const Layout& lay,
34
+ const ::VroomCandle* visible,
35
+ std::size_t n,
36
+ const double* values,
37
+ int64_t window_ms,
38
+ int64_t visible_start_ms,
39
+ int64_t candle_duration_ms,
40
+ float pane_top,
41
+ float pane_bottom,
42
+ const LineMorph* from,
43
+ std::size_t from_count,
44
+ float morph_t,
45
+ std::size_t k,
46
+ YFor&& y_for) {
47
+ MorphVertex to;
48
+ if (values && k < n) {
49
+ const std::size_t i = n - 1 - k;
50
+ const double v = values[i];
51
+ if (std::isfinite(v)) {
52
+ to = MorphVertex{
53
+ vroom::candle_center_x(lay, visible[i].time_ms,
54
+ candle_duration_ms, visible_start_ms,
55
+ window_ms),
56
+ y_for(v), true};
57
+ }
58
+ }
59
+ MorphVertex frm;
60
+ if (k < from_count) {
61
+ const LineSnapshot& s = from->pts[k];
62
+ if (s.valid) {
63
+ frm = MorphVertex{s.x * vroom::candle_area_width(lay),
64
+ pane_bottom - s.y * (pane_bottom - pane_top),
65
+ true};
66
+ }
67
+ }
68
+ return vroom::morph_vertex(to, frm, morph_t);
69
+ }
70
+
71
+ // How many vertices a morphing series spans: the new slice and the capture can
72
+ // disagree on length, and every slot either defines has to be walked.
73
+ inline std::size_t slots(std::size_t n, const LineMorph* from, float morph_t) {
74
+ return std::max(n, vroom::morph_line_count(from, morph_t));
75
+ }
76
+
77
+ // The series as a polyline, walked left to right. A slot neither side defines
78
+ // lifts the pen, which is what leaves the warmup gap open.
79
+ template <typename YFor>
80
+ SkPath build_path(const Layout& lay,
81
+ const ::VroomCandle* visible,
82
+ std::size_t n,
83
+ const double* values,
84
+ int64_t window_ms,
85
+ int64_t visible_start_ms,
86
+ int64_t candle_duration_ms,
87
+ float pane_top,
88
+ float pane_bottom,
89
+ const LineMorph* from,
90
+ float morph_t,
91
+ YFor&& y_for) {
92
+ const std::size_t from_count = vroom::morph_line_count(from, morph_t);
93
+ const std::size_t count = std::max(n, from_count);
94
+ SkPathBuilder path;
95
+ bool pen_down = false;
96
+ for (std::size_t j = 0; j < count; ++j) {
97
+ const MorphVertex p =
98
+ vertex(lay, visible, n, values, window_ms, visible_start_ms,
99
+ candle_duration_ms, pane_top, pane_bottom, from, from_count,
100
+ morph_t, count - 1 - j, y_for);
101
+ if (!p.valid) {
102
+ pen_down = false;
103
+ continue;
104
+ }
105
+ if (pen_down) {
106
+ path.lineTo(p.x, p.y);
107
+ } else {
108
+ path.moveTo(p.x, p.y);
109
+ pen_down = true;
110
+ }
111
+ }
112
+ return path.detach();
113
+ }
114
+
115
+ // The region between the series and a horizontal rule at `y_band`, as the
116
+ // closed counterpart to build_path. Callers clip to one side of the rule to keep
117
+ // only the stretches that reach past it, which puts the crossings exactly where
118
+ // Skia cuts the geometry rather than anywhere this has to solve for.
119
+ //
120
+ // Each run of vertices gets its own contour, tied down to the rule at both
121
+ // ends. One contour spanning the whole series would close a warmup gap by
122
+ // running straight through it, filling a span the line never drew.
123
+ template <typename YFor>
124
+ SkPath build_band_area(const Layout& lay,
125
+ const ::VroomCandle* visible,
126
+ std::size_t n,
127
+ const double* values,
128
+ int64_t window_ms,
129
+ int64_t visible_start_ms,
130
+ int64_t candle_duration_ms,
131
+ float pane_top,
132
+ float pane_bottom,
133
+ const LineMorph* from,
134
+ float morph_t,
135
+ float y_band,
136
+ YFor&& y_for) {
137
+ const std::size_t from_count = vroom::morph_line_count(from, morph_t);
138
+ const std::size_t count = std::max(n, from_count);
139
+ SkPathBuilder path;
140
+ bool open = false;
141
+ float last_x = 0.f;
142
+ for (std::size_t j = 0; j < count; ++j) {
143
+ const MorphVertex p =
144
+ vertex(lay, visible, n, values, window_ms, visible_start_ms,
145
+ candle_duration_ms, pane_top, pane_bottom, from, from_count,
146
+ morph_t, count - 1 - j, y_for);
147
+ if (!p.valid) {
148
+ if (open) {
149
+ path.lineTo(last_x, y_band);
150
+ path.close();
151
+ open = false;
152
+ }
153
+ continue;
154
+ }
155
+ if (!open) {
156
+ path.moveTo(p.x, y_band);
157
+ open = true;
158
+ }
159
+ path.lineTo(p.x, p.y);
160
+ last_x = p.x;
161
+ }
162
+ if (open) {
163
+ path.lineTo(last_x, y_band);
164
+ path.close();
165
+ }
166
+ return path.detach();
167
+ }
168
+
169
+ } // namespace vroom::pane_series
@@ -17,8 +17,10 @@
17
17
  #include <cstring>
18
18
 
19
19
  #include "chart.h"
20
+ #include "color_lerp.h"
20
21
  #include "fonts.h"
21
22
  #include "price_format.h"
23
+ #include "price_indicator_anim.h"
22
24
  #include "theme.h"
23
25
  #include "ticks.h"
24
26
  #include "viewport.h"
@@ -37,17 +39,29 @@ void draw(SkCanvas* canvas,
37
39
  const Layout& lay,
38
40
  const PriceBounds& bounds,
39
41
  float candle_right,
40
- float candle_area_h) {
42
+ float candle_area_h,
43
+ const vroom::CandleSnapshot* morph_from,
44
+ float morph_t) {
41
45
  if (!canvas || chart.candles.empty()) return;
42
46
 
43
47
  // The "current price" is the latest period's close, regardless of whether
44
- // that candle is horizontally in view.
48
+ // that candle is horizontally in view. Mid-morph it is wherever that close
49
+ // has eased to, so the badge stays on the candle's close edge.
45
50
  const ::VroomCandle& last = chart.candles.back();
46
51
  const bool bull = last.close >= last.open;
47
- const SkColor color =
48
- chart.theme.colors[bull ? VROOM_COLOR_ACCENT_BULL : VROOM_COLOR_ACCENT_BEAR];
49
-
50
- const float y = vroom::price_to_y(lay, bounds, last.close);
52
+ const auto level = vroom::price_indicator_anim::level_at(
53
+ lay, bounds, last.close, bull, morph_from, chart.morph_from_bounds,
54
+ morph_t);
55
+
56
+ // Blended rather than switched, so a candle that changes direction under a
57
+ // tick carries the indicator's color with it instead of snapping — the same
58
+ // cross-fade the body itself does.
59
+ const SkColor color = static_cast<SkColor>(vroom::lerp_argb(
60
+ chart.theme.colors[bull ? VROOM_COLOR_ACCENT_BEAR : VROOM_COLOR_ACCENT_BULL],
61
+ chart.theme.colors[bull ? VROOM_COLOR_ACCENT_BULL : VROOM_COLOR_ACCENT_BEAR],
62
+ level.bull_t));
63
+
64
+ const float y = level.y;
51
65
  if (y < 0.f || y > candle_area_h) return; // price scrolled off-range
52
66
 
53
67
  // Dotted line from the left edge to the y-axis separator.
@@ -73,7 +87,7 @@ void draw(SkCanvas* canvas,
73
87
  const vroom::PriceFormat fmt = vroom::with_tick_guard(
74
88
  chart.price_fmt,
75
89
  vroom::pick_price_interval(bounds.max - bounds.min, candle_area_h));
76
- vroom::format_price(buf, sizeof(buf), last.close, fmt);
90
+ vroom::format_price(buf, sizeof(buf), level.price, fmt);
77
91
  const size_t len = std::strlen(buf);
78
92
 
79
93
  // Measure the tight glyph bounds (origin at the baseline) so we can center
@@ -10,6 +10,7 @@ class SkCanvas;
10
10
  struct VroomChart;
11
11
 
12
12
  namespace vroom {
13
+ struct CandleSnapshot;
13
14
  struct Layout;
14
15
  struct PriceBounds;
15
16
  } // namespace vroom
@@ -19,11 +20,20 @@ namespace vroom::price_indicator {
19
20
  // Draws the line across [0, candle_right] at the latest close's y, plus the
20
21
  // price box in the y-axis strip. `candle_area_h` is the y of the x-axis
21
22
  // separator; the indicator is skipped if the close maps outside [0, candle_area_h].
23
+ //
24
+ // `morph_from` is the newest candle's outgoing capture (slot 0 of
25
+ // VroomChart::morph_from), or null to draw the settled close. When present the
26
+ // level eases across `morph_t` on the same clock as the candle it marks, so the
27
+ // badge tracks the bar instead of jumping ahead of it — see
28
+ // price_indicator_anim.h. Callers must only pass a capture that pairs with the
29
+ // newest candle (tip_anchor.h).
22
30
  void draw(SkCanvas* canvas,
23
31
  const VroomChart& chart,
24
32
  const Layout& lay,
25
33
  const PriceBounds& bounds,
26
34
  float candle_right,
27
- float candle_area_h);
35
+ float candle_area_h,
36
+ const CandleSnapshot* morph_from = nullptr,
37
+ float morph_t = 1.f);
28
38
 
29
39
  } // namespace vroom::price_indicator
@@ -0,0 +1,81 @@
1
+ // Where the current-price indicator sits mid-morph (see price_indicator.cpp).
2
+ //
3
+ // The indicator marks the latest close, so every tick moves it. Drawing it
4
+ // straight from that close makes it jump while the candle it belongs to eases
5
+ // into place, which reads as the badge coming loose from the chart. Instead it
6
+ // rides the same capture the candles do: morph_from[0] is the newest candle's
7
+ // outgoing geometry and interval_morph_t is the host's eased clock, so the
8
+ // indicator lands on the candle's close edge on every frame rather than only at
9
+ // the two ends.
10
+ //
11
+ // The y is deliberately the same expression close_vertex uses for slot 0
12
+ // (ma_overlay.cpp). Matching it by construction, rather than by giving the two
13
+ // the same duration, is what keeps the badge glued to the line chart's tip.
14
+ //
15
+ // Skia-free and header-only so the unit tests can cover it; see
16
+ // tests/test_price_indicator_anim.cpp.
17
+
18
+ #pragma once
19
+
20
+ #include <algorithm>
21
+
22
+ #include "viewport.h"
23
+
24
+ namespace vroom::price_indicator_anim {
25
+
26
+ // One frame of the indicator.
27
+ struct Level {
28
+ float y; // pixels, on the candle's close edge
29
+ double price; // what the badge should read
30
+ float bull_t; // 0 = the captured direction's color, 1 = the new one
31
+ };
32
+
33
+ namespace detail {
34
+ inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
35
+
36
+ // The price a captured band fraction stood for. The capture stores fractions so
37
+ // it survives a resize or a rescale, so recovering the price it came from needs
38
+ // the band it was measured against — which is why morph_from_bounds is kept.
39
+ inline double price_at_fraction(const PriceBounds& b, double frac) {
40
+ return b.min + frac * (b.max - b.min);
41
+ }
42
+ } // namespace detail
43
+
44
+ // `from` is the newest candle's capture, or null when nothing is morphing or
45
+ // the capture doesn't pair with the newest candle (panned into history — see
46
+ // tip_anchor.h, which the caller shares the pairing rule with). A null capture
47
+ // gives the settled values, which is what the indicator drew before it
48
+ // animated at all.
49
+ //
50
+ // `from_bounds` is the price band `from` was captured against
51
+ // (VroomChart::morph_from_bounds).
52
+ inline Level level_at(const Layout& lay,
53
+ const PriceBounds& bounds,
54
+ double close_new,
55
+ bool bull_new,
56
+ const CandleSnapshot* from,
57
+ const PriceBounds& from_bounds,
58
+ float morph_t) {
59
+ const float to_y = vroom::price_to_y(lay, bounds, close_new);
60
+ if (!from) return Level{to_y, close_new, 1.f};
61
+
62
+ const float t = std::clamp(morph_t, 0.f, 1.f);
63
+
64
+ // Two different spaces on purpose. The y interpolates in band fractions,
65
+ // because that is the space the capture is in and the space the candles
66
+ // move through — anything else would start the indicator off the pixel the
67
+ // close occupied last frame. The price interpolates in price space, so both
68
+ // ends read exactly the close they belong to even when this tick set a new
69
+ // extreme and rescaled the band underneath. The two agree whenever the band
70
+ // holds still, which is the overwhelmingly common case: y_at_fraction is
71
+ // affine, so a fraction lerp and a price lerp are then the same function.
72
+ const float y =
73
+ detail::lerp(vroom::y_at_fraction(lay, from->close), to_y, t);
74
+ const double close_old =
75
+ detail::price_at_fraction(from_bounds, from->close);
76
+
77
+ return Level{y, close_old + (close_new - close_old) * static_cast<double>(t),
78
+ from->bull == bull_new ? 1.f : t};
79
+ }
80
+
81
+ } // namespace vroom::price_indicator_anim
@@ -53,4 +53,8 @@ void compute_ma(const std::vector<double>& rsi, int ma_period, int kind,
53
53
  vroom::series_ma::smooth(rsi, kind, ma_period, out);
54
54
  }
55
55
 
56
+ double band_fraction(double v, double y_scale) {
57
+ return 0.5 + ((v - 50.0) / 100.0) * y_scale;
58
+ }
59
+
56
60
  } // namespace vroom::rsi
@@ -29,4 +29,11 @@ void compute(const ::VroomCandle* candles, std::size_t n, int period,
29
29
  void compute_ma(const std::vector<double>& rsi, int ma_period, int kind,
30
30
  std::vector<double>& out);
31
31
 
32
+ // Where an RSI value sits in the pane band, as a fraction of its height — 0 at
33
+ // the bottom edge, 1 at the top. The fixed 0..100 domain maps about the band
34
+ // center so the user's y-axis zoom (`y_scale`, 1 = the default fit) stretches
35
+ // symmetrically around 50. Split out of the renderer so the interval-morph
36
+ // capture maps its geometry through the same math.
37
+ double band_fraction(double v, double y_scale);
38
+
32
39
  } // namespace vroom::rsi