react-native-vroom-chart 0.15.0 → 0.16.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 (42) hide show
  1. package/cpp/VroomChartHostObject.cpp +171 -1
  2. package/cpp/_core_include/vroom/vroom_chart.h +137 -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/chart.cpp +461 -172
  8. package/cpp/_core_src/chart.h +150 -1
  9. package/cpp/_core_src/chart_facade.cpp +209 -23
  10. package/cpp/_core_src/fair_value_gaps.cpp +76 -0
  11. package/cpp/_core_src/fair_value_gaps.h +54 -0
  12. package/cpp/_core_src/fvg_overlay.cpp +262 -0
  13. package/cpp/_core_src/fvg_overlay.h +43 -0
  14. package/cpp/_core_src/ichimoku.cpp +65 -0
  15. package/cpp/_core_src/ichimoku.h +45 -0
  16. package/cpp/_core_src/labels.cpp +3 -0
  17. package/cpp/_core_src/line_morph.h +159 -0
  18. package/cpp/_core_src/ma_overlay.cpp +259 -36
  19. package/cpp/_core_src/ma_overlay.h +57 -2
  20. package/cpp/_core_src/macd.cpp +24 -1
  21. package/cpp/_core_src/macd.h +16 -0
  22. package/cpp/_core_src/macd_pane.cpp +71 -62
  23. package/cpp/_core_src/macd_pane.h +13 -1
  24. package/cpp/_core_src/pane_series.h +169 -0
  25. package/cpp/_core_src/rsi.cpp +4 -0
  26. package/cpp/_core_src/rsi.h +7 -0
  27. package/cpp/_core_src/rsi_pane.cpp +85 -29
  28. package/cpp/_core_src/rsi_pane.h +11 -1
  29. package/cpp/_core_src/viewport.h +35 -0
  30. package/lib/index.d.mts +251 -3
  31. package/lib/index.d.ts +251 -3
  32. package/lib/index.js +224 -6
  33. package/lib/index.js.map +1 -1
  34. package/lib/index.mjs +224 -6
  35. package/lib/index.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/src/VroomChart.tsx +21 -3
  38. package/src/dataTransitions.ts +47 -0
  39. package/src/index.ts +5 -0
  40. package/src/jsi.d.ts +97 -1
  41. package/src/types.ts +5 -0
  42. package/src/useChartCore.ts +284 -5
@@ -0,0 +1,54 @@
1
+ // Fair Value Gaps over a candle series — pure, no Skia, so it builds into the
2
+ // unit-test target. Drawn as shaded price-pane boxes over three-candle
3
+ // imbalances.
4
+
5
+ #pragma once
6
+
7
+ #include <cstddef>
8
+ #include <cstdint>
9
+ #include <vector>
10
+
11
+ #include "vroom/vroom_chart.h" // ::VroomCandle
12
+
13
+ namespace vroom::fvg {
14
+
15
+ // One detected imbalance, anchored in absolute time and price so the renderer
16
+ // can place it without knowing which bar it came from.
17
+ struct Gap {
18
+ int64_t time_ms = 0; // open time of the middle bar
19
+ double top = 0.0; // upper price edge
20
+ double bottom = 0.0; // lower price edge
21
+ bool bullish = false;
22
+ int64_t filled_ms = 0; // open time of the bar that filled it; 0 = unfilled
23
+ // Open time of the bar that reclaimed the zone after it inverted; 0 while
24
+ // the inversion still stands. Only meaningful once filled_ms is set.
25
+ int64_t invalidated_ms = 0;
26
+ };
27
+
28
+ // Which price settles that a gap has been traded back through.
29
+ enum FillType : int {
30
+ kFillClose = 0, // a candle must close past the far edge
31
+ kFillWick = 1, // a high or low reaching through is enough
32
+ };
33
+
34
+ // Detects the gaps in the last `max_bars_back` bars of [candles, candles+n),
35
+ // oldest first, and resolves each one's fill.
36
+ //
37
+ // A gap sits around the middle bar of a three-candle run whose outer wicks miss
38
+ // each other: bullish when candles[i-1].high < candles[i+1].low (spanning that
39
+ // range), bearish when candles[i-1].low > candles[i+1].high. `wait_for_close`
40
+ // withholds a gap whose third candle is still the newest, live bar.
41
+ //
42
+ // The fill scan walks forward from i+2 and records the first bar to reach the
43
+ // far edge — the bottom of a bullish gap, the top of a bearish one.
44
+ //
45
+ // Closing through a gap inverts it: the band price just rejected becomes a
46
+ // zone of the opposite polarity, resistance overhead where a bullish gap was
47
+ // and support underfoot where a bearish one was. A second scan resumes after
48
+ // the fill and records the bar that reclaims the band the other way, ending
49
+ // the inversion. Both scans always run, so whether a filled or inverted box is
50
+ // drawn at all stays a drawing concern.
51
+ void compute(const ::VroomCandle* candles, std::size_t n, int max_bars_back,
52
+ bool wait_for_close, int fill_type, std::vector<Gap>& out);
53
+
54
+ } // namespace vroom::fvg
@@ -0,0 +1,262 @@
1
+ #include "fvg_overlay.h"
2
+
3
+ #include <algorithm>
4
+ #include <array>
5
+ #include <cstddef>
6
+ #include <string>
7
+
8
+ #pragma clang diagnostic push
9
+ #pragma clang diagnostic ignored "-Wdocumentation"
10
+ #include "include/core/SkCanvas.h"
11
+ #include "include/core/SkColor.h"
12
+ #include "include/core/SkFont.h"
13
+ #include "include/core/SkPaint.h"
14
+ #include "include/core/SkRect.h"
15
+ #include "include/effects/SkDashPathEffect.h"
16
+ #pragma clang diagnostic pop
17
+
18
+ #include "chart.h"
19
+ #include "fonts.h"
20
+
21
+ namespace vroom::fvg_overlay {
22
+
23
+ namespace {
24
+
25
+ constexpr SkScalar kDotted[2] = {2.f, 2.f};
26
+ constexpr SkScalar kDashed[2] = {6.f, 4.f};
27
+ constexpr float kLabelPadX = 4.f; // clearance between a label and a box edge
28
+
29
+ // Dash pattern for a VroomFairValueGaps::border_style. Null = solid.
30
+ sk_sp<SkPathEffect> dash_for(int32_t border_style) {
31
+ if (border_style == 1) return SkDashPathEffect::Make(kDotted, 0.f);
32
+ if (border_style == 2) return SkDashPathEffect::Make(kDashed, 0.f);
33
+ return nullptr;
34
+ }
35
+
36
+ // A stretch of time one box covers. A gap draws one of these while it is open
37
+ // and, under show_inverse, a second of the opposite polarity once it has been
38
+ // violated.
39
+ struct Span {
40
+ int64_t start_ms;
41
+ int64_t end_ms;
42
+ bool bullish; // polarity of THIS span, already flipped for an inverse
43
+ bool inverse;
44
+ bool open; // still running at the newest bar rather than cut short
45
+ };
46
+
47
+ // The spans `g` draws, written to `out` oldest first, returning how many. Zero
48
+ // when the gap is set to disappear on the fill and has no inversion to show.
49
+ std::size_t spans_for(const VroomChart& chart,
50
+ const vroom::fvg::Gap& g,
51
+ std::array<Span, 2>& out) {
52
+ const VroomFairValueGaps& cfg = chart.fvg;
53
+ const int64_t dur = chart.candle_duration_ms;
54
+ const int64_t newest_end = chart.candles.back().time_ms + dur;
55
+ const int64_t length = static_cast<int64_t>(cfg.box_length) * dur;
56
+ std::size_t n = 0;
57
+
58
+ // The original. It runs its configured length, or — when extended — to the
59
+ // end of the newest bar, and stops at the far side of the bar that filled
60
+ // it, so the shading covers exactly the span the gap was open for.
61
+ if (g.filled_ms == 0 || !cfg.delete_after_fill) {
62
+ int64_t end = cfg.extend_boxes ? newest_end : g.time_ms + length;
63
+ const bool open = g.filled_ms == 0;
64
+ if (!open) end = std::min(end, g.filled_ms + dur);
65
+ if (end > g.time_ms) out[n++] = Span{g.time_ms, end, g.bullish, false, open};
66
+ }
67
+
68
+ // The inversion, anchored past the close of the breaking bar so it picks up
69
+ // exactly where the original stops and the two never overlap. It measures
70
+ // its own length from there and ends where price reclaimed the band.
71
+ if (cfg.show_inverse && g.filled_ms != 0) {
72
+ const int64_t start = g.filled_ms + dur;
73
+ int64_t end = cfg.extend_boxes ? newest_end : start + length;
74
+ const bool open = g.invalidated_ms == 0;
75
+ if (!open) end = std::min(end, g.invalidated_ms + dur);
76
+ if (end > start) out[n++] = Span{start, end, !g.bullish, true, open};
77
+ }
78
+ return n;
79
+ }
80
+
81
+ // One span resolved to pixels, or false when it lands entirely off to one side.
82
+ bool box_for(const VroomChart& chart,
83
+ const vroom::Layout& lay,
84
+ const vroom::PriceBounds& bounds,
85
+ int64_t window_ms,
86
+ float candle_right,
87
+ const vroom::fvg::Gap& g,
88
+ const Span& s,
89
+ SkRect* out) {
90
+ const float left =
91
+ vroom::x_at_time(lay, chart.visible_start_ms, window_ms, s.start_ms);
92
+ const float right =
93
+ vroom::x_at_time(lay, chart.visible_start_ms, window_ms, s.end_ms);
94
+ if (right < 0.f || left > candle_right) return false;
95
+
96
+ *out = SkRect::MakeLTRB(left, vroom::price_to_y(lay, bounds, g.top), right,
97
+ vroom::price_to_y(lay, bounds, g.bottom));
98
+ return true;
99
+ }
100
+
101
+ // The fill color a span shades with, before opacity.
102
+ SkColor fill_color(const VroomFairValueGaps& cfg, const Span& s) {
103
+ if (s.inverse) {
104
+ return s.bullish ? cfg.inverse_bullish_color : cfg.inverse_bearish_color;
105
+ }
106
+ return s.bullish ? cfg.bullish_color : cfg.bearish_color;
107
+ }
108
+
109
+ // The color a span's border and label take. Inverse spans have no paired border
110
+ // color of their own, so they reuse their fill at full alpha — the same
111
+ // relationship bullishBorderColor already has with bullishColor by default.
112
+ SkColor line_color(const VroomFairValueGaps& cfg, const Span& s) {
113
+ if (s.inverse) return SkColorSetA(fill_color(cfg, s), 0xff);
114
+ return s.bullish ? cfg.bullish_border_color : cfg.bearish_border_color;
115
+ }
116
+
117
+ // Whether there is anything at all to draw, and the cache is safe to walk.
118
+ bool active(const VroomChart& chart, SkCanvas* canvas, float candle_right,
119
+ float candle_area_h) {
120
+ return canvas && candle_right > 0.f && candle_area_h > 0.f &&
121
+ chart.fvg.enabled && !chart.candles.empty() &&
122
+ !chart.fvg_cache.empty();
123
+ }
124
+
125
+ SkColor with_opacity(SkColor c, float opacity) {
126
+ return SkColorSetA(c, static_cast<U8CPU>(SkColorGetA(c) * opacity));
127
+ }
128
+
129
+ } // namespace
130
+
131
+ void draw_boxes(SkCanvas* canvas,
132
+ const VroomChart& chart,
133
+ const Layout& lay,
134
+ const PriceBounds& bounds,
135
+ int64_t window_ms,
136
+ float candle_right,
137
+ float candle_area_h) {
138
+ if (!active(chart, canvas, candle_right, candle_area_h)) return;
139
+ const VroomFairValueGaps& cfg = chart.fvg;
140
+
141
+ SkPaint fill;
142
+ fill.setAntiAlias(true);
143
+ fill.setStyle(SkPaint::kFill_Style);
144
+
145
+ SkPaint border;
146
+ border.setAntiAlias(true);
147
+ border.setStyle(SkPaint::kStroke_Style);
148
+ border.setStrokeWidth(cfg.border_width > 0.f ? cfg.border_width : 1.f);
149
+ border.setPathEffect(dash_for(cfg.border_style));
150
+
151
+ canvas->save();
152
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
153
+ std::array<Span, 2> spans;
154
+ for (const vroom::fvg::Gap& g : chart.fvg_cache) {
155
+ const std::size_t n = spans_for(chart, g, spans);
156
+ for (std::size_t i = 0; i < n; ++i) {
157
+ SkRect rect;
158
+ if (!box_for(chart, lay, bounds, window_ms, candle_right, g, spans[i],
159
+ &rect)) {
160
+ continue;
161
+ }
162
+
163
+ fill.setColor(with_opacity(fill_color(cfg, spans[i]), cfg.opacity));
164
+ canvas->drawRect(rect, fill);
165
+
166
+ if (cfg.border_enabled) {
167
+ border.setColor(line_color(cfg, spans[i]));
168
+ canvas->drawRect(rect, border);
169
+ }
170
+ }
171
+ }
172
+ canvas->restore();
173
+ }
174
+
175
+ void draw_labels(SkCanvas* canvas,
176
+ const VroomChart& chart,
177
+ const Layout& lay,
178
+ const PriceBounds& bounds,
179
+ int64_t window_ms,
180
+ float candle_right,
181
+ float candle_area_h) {
182
+ if (!active(chart, canvas, candle_right, candle_area_h)) return;
183
+ const VroomFairValueGaps& cfg = chart.fvg;
184
+ const bool any_inverse = cfg.show_inverse && !chart.fvg_inverse_label.empty();
185
+ if (!cfg.labels_enabled || (chart.fvg_label.empty() && !any_inverse)) return;
186
+
187
+ auto tf = vroom::axis_typeface();
188
+ if (!tf) return;
189
+ const float size = cfg.label_font_size > 0.f
190
+ ? cfg.label_font_size
191
+ : chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX];
192
+ SkFont font(tf, size);
193
+ font.setSubpixel(true);
194
+ font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
195
+
196
+ // Indexed by Span::inverse, so a span picks its own text and metrics.
197
+ struct Label {
198
+ const std::string* text;
199
+ float width;
200
+ SkRect bounds;
201
+ };
202
+ Label labels[2] = {{&chart.fvg_label, 0.f, {}},
203
+ {&chart.fvg_inverse_label, 0.f, {}}};
204
+ for (Label& l : labels) {
205
+ l.width = font.measureText(l.text->data(), l.text->size(),
206
+ SkTextEncoding::kUTF8, &l.bounds);
207
+ }
208
+
209
+ // Extended boxes all stop at the newest bar, so their labels go out into the
210
+ // empty slots past it rather than on top of the shading.
211
+ const float extend_x =
212
+ cfg.extend_boxes
213
+ ? vroom::x_at_time(lay, chart.visible_start_ms, window_ms,
214
+ chart.candles.back().time_ms +
215
+ static_cast<int64_t>(cfg.label_distance + 1) *
216
+ chart.candle_duration_ms)
217
+ : 0.f;
218
+
219
+ SkPaint paint;
220
+ paint.setAntiAlias(true);
221
+
222
+ canvas->save();
223
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
224
+ std::array<Span, 2> spans;
225
+ for (const vroom::fvg::Gap& g : chart.fvg_cache) {
226
+ const std::size_t n = spans_for(chart, g, spans);
227
+ for (std::size_t i = 0; i < n; ++i) {
228
+ const Span& s = spans[i];
229
+ const Label& l = labels[s.inverse ? 1 : 0];
230
+ if (l.text->empty()) continue;
231
+
232
+ SkRect rect;
233
+ if (!box_for(chart, lay, bounds, window_ms, candle_right, g, s,
234
+ &rect)) {
235
+ continue;
236
+ }
237
+
238
+ // A box that was cut short stops early even under extend, so it
239
+ // keeps its label inside; only boxes still running to the newest
240
+ // bar move theirs out — and only while the pane has the room, since
241
+ // the newest candle sits flush right until the user pans.
242
+ const bool outside = cfg.extend_boxes && s.open &&
243
+ extend_x + l.width <= candle_right;
244
+ // A box whose right end runs past the pane would have its label
245
+ // sliced mid-glyph by the clip, so hold the text inside the plot.
246
+ const float x =
247
+ std::min(outside ? extend_x : rect.right() - kLabelPadX - l.width,
248
+ candle_right - kLabelPadX - l.width);
249
+ if (x + l.width < 0.f) continue;
250
+
251
+ paint.setColor(SkColorGetA(cfg.label_color) != 0 ? cfg.label_color
252
+ : line_color(cfg, s));
253
+ canvas->drawString(
254
+ l.text->c_str(), x,
255
+ rect.centerY() - (l.bounds.fTop + l.bounds.fBottom) * 0.5f, font,
256
+ paint);
257
+ }
258
+ }
259
+ canvas->restore();
260
+ }
261
+
262
+ } // namespace vroom::fvg_overlay
@@ -0,0 +1,43 @@
1
+ // Fair Value Gap boxes — the drawing half of the indicator, kept out of the
2
+ // chart orchestrator the way liquidity / drawings are. Detection lives in the
3
+ // Skia-free fair_value_gaps.{h,cpp}; this reads the cache it fills.
4
+ //
5
+ // Each gap becomes an axis-aligned rectangle spanning its untouched price range
6
+ // and running right from the middle bar. Split across two passes because they
7
+ // sit at different depths: the boxes go BEHIND the candles so bodies paint over
8
+ // them, the labels go in FRONT so they stay readable.
9
+
10
+ #pragma once
11
+
12
+ #include <cstdint>
13
+
14
+ #include "viewport.h"
15
+
16
+ class SkCanvas;
17
+ struct VroomChart;
18
+
19
+ namespace vroom::fvg_overlay {
20
+
21
+ // Draws the box fills and their outlines. `candle_right` is the x of the
22
+ // price-axis strip and `candle_area_h` the price-pane bottom; geometry is
23
+ // clipped to that rectangle so a box never bleeds into the axis strips.
24
+ void draw_boxes(SkCanvas* canvas,
25
+ const VroomChart& chart,
26
+ const vroom::Layout& lay,
27
+ const vroom::PriceBounds& bounds,
28
+ int64_t window_ms,
29
+ float candle_right,
30
+ float candle_area_h);
31
+
32
+ // Draws each box's label: inside its right end normally, or out in the empty
33
+ // slots past the newest candle when the boxes are extended. No-op while no
34
+ // typeface is loaded.
35
+ void draw_labels(SkCanvas* canvas,
36
+ const VroomChart& chart,
37
+ const vroom::Layout& lay,
38
+ const vroom::PriceBounds& bounds,
39
+ int64_t window_ms,
40
+ float candle_right,
41
+ float candle_area_h);
42
+
43
+ } // namespace vroom::fvg_overlay
@@ -0,0 +1,65 @@
1
+ #include "ichimoku.h"
2
+
3
+ #include <algorithm> // std::max, std::min
4
+ #include <cmath> // std::nan
5
+
6
+ namespace vroom::ichimoku {
7
+ namespace {
8
+
9
+ // Midpoint of the high/low range over each trailing `period`-bar window — the
10
+ // shape all three of Ichimoku's averaged lines share. NaN over the warmup.
11
+ void midpoint_series(const ::VroomCandle* candles, std::size_t n, int period,
12
+ std::vector<double>& out) {
13
+ out.assign(n, std::nan(""));
14
+ if (!candles || period < 1) return;
15
+ const std::size_t P = static_cast<std::size_t>(period);
16
+ if (n < P) return;
17
+
18
+ for (std::size_t i = P - 1; i < n; ++i) {
19
+ const std::size_t s = i + 1 - P;
20
+ double hi = candles[s].high;
21
+ double lo = candles[s].low;
22
+ for (std::size_t j = s + 1; j <= i; ++j) {
23
+ hi = std::max(hi, candles[j].high);
24
+ lo = std::min(lo, candles[j].low);
25
+ }
26
+ out[i] = (hi + lo) * 0.5;
27
+ }
28
+ }
29
+
30
+ } // namespace
31
+
32
+ void compute(const ::VroomCandle* candles, std::size_t n, int tenkan_period,
33
+ int kijun_period, int senkou_b_period,
34
+ std::vector<double>& tenkan, std::vector<double>& kijun,
35
+ std::vector<double>& senkou_a, std::vector<double>& senkou_b,
36
+ std::vector<double>& chikou) {
37
+ midpoint_series(candles, n, tenkan_period, tenkan);
38
+ midpoint_series(candles, n, kijun_period, kijun);
39
+ midpoint_series(candles, n, senkou_b_period, senkou_b);
40
+
41
+ // NaN propagates through the average, so span A is only defined once both
42
+ // of its inputs are — no separate warmup bound to track.
43
+ senkou_a.assign(n, std::nan(""));
44
+ for (std::size_t i = 0; i < n; ++i) {
45
+ senkou_a[i] = (tenkan[i] + kijun[i]) * 0.5;
46
+ }
47
+
48
+ chikou.assign(n, std::nan(""));
49
+ if (!candles) return;
50
+ for (std::size_t i = 0; i < n; ++i) chikou[i] = candles[i].close;
51
+ }
52
+
53
+ IndexRange shifted_source_range(const ::VroomCandle* candles, std::size_t n,
54
+ int64_t start_ms, int64_t end_ms,
55
+ int64_t shift_ms) {
56
+ // 0/0 is visible_indices' "everything" sentinel (an unframed viewport).
57
+ // Shifting it would turn that into an empty range, so pass it through.
58
+ if (start_ms == 0 && end_ms == 0) {
59
+ return vroom::visible_indices(candles, n, 0, 0);
60
+ }
61
+ return vroom::visible_indices(candles, n, start_ms - shift_ms,
62
+ end_ms - shift_ms);
63
+ }
64
+
65
+ } // namespace vroom::ichimoku
@@ -0,0 +1,45 @@
1
+ // Ichimoku Kinko Hyo over a candle series — pure, no Skia, so it builds into
2
+ // the unit-test target. Drawn as five price-pane overlay lines plus the cloud
3
+ // shaded between the two leading spans.
4
+
5
+ #pragma once
6
+
7
+ #include <cstddef>
8
+ #include <cstdint>
9
+ #include <vector>
10
+
11
+ #include "viewport.h" // vroom::IndexRange
12
+ #include "vroom/vroom_chart.h" // ::VroomCandle
13
+
14
+ namespace vroom::ichimoku {
15
+
16
+ // Computes the five Ichimoku series over [candles, candles+n). Each period is
17
+ // clamped to >= 1.
18
+ //
19
+ // tenkan = midpoint of the high/low range over the trailing tenkan window
20
+ // kijun = same over the trailing kijun window
21
+ // senkou_a = (tenkan + kijun) / 2
22
+ // senkou_b = midpoint of the high/low range over the trailing senkou_b window
23
+ // chikou = close
24
+ //
25
+ // Every output is resized to n and indexed by the bar it was computed from —
26
+ // displacement is a drawing concern, so senkou_a/senkou_b/chikou are *not*
27
+ // shifted here. Values are NaN over each series' warmup and when n is shorter
28
+ // than the window a series needs.
29
+ void compute(const ::VroomCandle* candles, std::size_t n, int tenkan_period,
30
+ int kijun_period, int senkou_b_period,
31
+ std::vector<double>& tenkan, std::vector<double>& kijun,
32
+ std::vector<double>& senkou_a, std::vector<double>& senkou_b,
33
+ std::vector<double>& chikou);
34
+
35
+ // Source indices whose *shifted* plot time lands in [start_ms, end_ms], for a
36
+ // series drawn at `candles[i].time_ms + shift_ms`.
37
+ //
38
+ // Shifting the query window back by the same amount is what lets the leading
39
+ // spans run past the newest candle: the range is found among bars that exist,
40
+ // while the x each one draws at does not have to.
41
+ IndexRange shifted_source_range(const ::VroomCandle* candles, std::size_t n,
42
+ int64_t start_ms, int64_t end_ms,
43
+ int64_t shift_ms);
44
+
45
+ } // namespace vroom::ichimoku
@@ -74,6 +74,9 @@ void apply_envelope(std::vector<Fade>& fades, float opacity) {
74
74
 
75
75
  IntervalPhase interval_phase(const VroomChart& chart) {
76
76
  if (chart.morph_from.empty() || chart.interval_morph_t >= 1.f) return {};
77
+ // A live update keeps the same interval, so its ticks are still the right
78
+ // ones — they stay put and keep their own per-label fades.
79
+ if (chart.morph_is_stream) return {};
77
80
  const float t = chart.interval_morph_t;
78
81
  constexpr float kMid = 0.5f;
79
82
  if (t < kMid) return {true, true, 1.f - t / kMid};
@@ -0,0 +1,159 @@
1
+ // Interval-morph capture for indicator series — the line-shaped counterpart to
2
+ // viewport.h's CandleSnapshot. Pure geometry, no Skia, so it builds into the
3
+ // unit-test target.
4
+ //
5
+ // A timeframe switch replaces every indicator cache at once (see
6
+ // vroom_chart_set_candles), so an outgoing shape has to be captured before the
7
+ // swap if it is going to reshape into the new one. Captures normalize the same
8
+ // way the candle one does — x as a fraction of the candle-area width, y as a
9
+ // fraction of the band the series was drawn in — which keeps them correct
10
+ // across a mid-morph resize, across the y-axis rescale the switch brings, and,
11
+ // for the indicator panes, across a change in the pane's own auto-fit.
12
+
13
+ #pragma once
14
+
15
+ #include <cstddef>
16
+ #include <cstdint>
17
+ #include <vector>
18
+
19
+ #include "vroom/vroom_chart.h" // ::VroomOverlay
20
+
21
+ namespace vroom {
22
+
23
+ // Which series a capture belongs to. Pairing is by identity rather than
24
+ // position so an indicator toggled in the same commit as the timeframe can't
25
+ // reshape one line out of another's geometry.
26
+ enum class LineKind : uint8_t {
27
+ Overlay,
28
+ Vwap,
29
+ BollingerUpper,
30
+ BollingerMiddle,
31
+ BollingerLower,
32
+ IchimokuTenkan,
33
+ IchimokuKijun,
34
+ IchimokuSenkouA,
35
+ IchimokuSenkouB,
36
+ IchimokuChikou,
37
+ Rsi,
38
+ RsiMa,
39
+ Macd,
40
+ MacdSignal,
41
+ MacdHistogram,
42
+ Atr,
43
+ };
44
+
45
+ // Identifies one captured series. The moving-average overlays are a
46
+ // user-ordered vector rather than a fixed slot, so `index` alone would pair two
47
+ // different MAs if one were added or removed during the switch; `tag` carries
48
+ // the overlay's kind, period and source, so a mismatch finds no capture and the
49
+ // line simply snaps. Both are 0 for every other series.
50
+ struct LineKey {
51
+ LineKind kind;
52
+ int32_t index = 0;
53
+ int32_t tag = 0;
54
+ };
55
+
56
+ inline bool operator==(const LineKey& a, const LineKey& b) {
57
+ return a.kind == b.kind && a.index == b.index && a.tag == b.tag;
58
+ }
59
+
60
+ // The key for the moving-average overlay at `index`, packing its kind, source
61
+ // and period into the tag. Periods above 65535 alias, which no usable lookback
62
+ // reaches.
63
+ inline LineKey overlay_line_key(const ::VroomOverlay& ov, std::size_t index) {
64
+ const int32_t tag =
65
+ (ov.kind << 24) ^ (ov.source << 16) ^ (ov.period & 0xffff);
66
+ return LineKey{LineKind::Overlay, static_cast<int32_t>(index), tag};
67
+ }
68
+
69
+ // One captured vertex. `x` is a fraction of the candle-area width and `y` a
70
+ // fraction of the band the series was drawn in (0 = bottom edge, 1 = top).
71
+ // `valid` is false where the source value was NaN, which is how a warmup gap
72
+ // survives the capture.
73
+ struct LineSnapshot {
74
+ float x = 0.f;
75
+ float y = 0.f;
76
+ bool valid = false;
77
+ };
78
+
79
+ // One captured series. Slot 0 is the newest vertex, matching the right-edge
80
+ // indexing candles::draw pairs on. `scale` is the auto-fit its pane band was
81
+ // sized to at capture time, which the pane's axis label reads back mid-morph;
82
+ // 0 for series on the price scale, whose band is the price bounds.
83
+ struct LineMorph {
84
+ LineKey key{LineKind::Overlay};
85
+ double scale = 0.0;
86
+ std::vector<LineSnapshot> pts;
87
+ };
88
+
89
+ // The capture for `key`, or null when nothing matching was captured — which is
90
+ // what an indicator enabled mid-morph gets, so it draws its new shape directly
91
+ // instead of reshaping out of geometry that was never on screen.
92
+ inline const LineMorph* find_line_morph(const std::vector<LineMorph>& lines,
93
+ const LineKey& key) {
94
+ for (const LineMorph& line : lines) {
95
+ if (line.key == key) return &line;
96
+ }
97
+ return nullptr;
98
+ }
99
+
100
+ // How many captured slots still contribute to a frame, mirroring
101
+ // morph_from_count: 0 once the morph lands, which collapses the draw path back
102
+ // to the new series alone. Drawing routines take max(n, this) as their slot
103
+ // count, pairing the new vertex at slot k with the captured one at pts[k].
104
+ inline std::size_t morph_line_count(const LineMorph* from, float morph_t) {
105
+ return (from && morph_t < 1.f) ? from->pts.size() : 0;
106
+ }
107
+
108
+ // One slot resolved to screen pixels. `valid` false means neither side defined
109
+ // the slot, and the polyline lifts its pen there.
110
+ struct MorphVertex {
111
+ float x = 0.f;
112
+ float y = 0.f;
113
+ bool valid = false;
114
+ };
115
+
116
+ // Blends a slot's captured position toward its new one. Both sides arrive in
117
+ // pixels: the caller converts the capture's fractions through whichever band
118
+ // the series lives in, which is the only part that differs between a price-pane
119
+ // overlay and an indicator pane.
120
+ //
121
+ // A slot only one side defines holds that side's position rather than lifting
122
+ // the pen. The warmup gap at a line's left end spans a different amount of time
123
+ // at each resolution, and sliding that end reads far better than blinking it.
124
+ inline MorphVertex morph_vertex(const MorphVertex& to, const MorphVertex& from,
125
+ float morph_t) {
126
+ if (!to.valid) return from;
127
+ if (!from.valid) return to;
128
+ return MorphVertex{from.x + (to.x - from.x) * morph_t,
129
+ from.y + (to.y - from.y) * morph_t, true};
130
+ }
131
+
132
+ // The line counterpart to viewport.h's blend_candle_snapshots: rewrites a fresh
133
+ // capture to start from the shape on screen, so a live tick restarting the
134
+ // morph doesn't snap the indicators back off the candles they sit on.
135
+ //
136
+ // Series are matched by key, so one enabled or removed between two ticks simply
137
+ // finds no counterpart and keeps its fresh capture. A slot only one side
138
+ // defines keeps that side, mirroring morph_vertex — the warmup gap can move by
139
+ // a slot as bars arrive, and sliding that end reads better than blinking it.
140
+ inline void blend_line_morphs(std::vector<LineMorph>& dst,
141
+ const std::vector<LineMorph>& interrupted,
142
+ float morph_t) {
143
+ for (LineMorph& to : dst) {
144
+ const LineMorph* from = find_line_morph(interrupted, to.key);
145
+ if (!from) continue;
146
+ to.scale = from->scale + (to.scale - from->scale) * morph_t;
147
+ const std::size_t n =
148
+ to.pts.size() < from->pts.size() ? to.pts.size() : from->pts.size();
149
+ for (std::size_t k = 0; k < n; ++k) {
150
+ const LineSnapshot& a = from->pts[k];
151
+ LineSnapshot& b = to.pts[k];
152
+ if (!a.valid || !b.valid) continue;
153
+ b.x = a.x + (b.x - a.x) * morph_t;
154
+ b.y = a.y + (b.y - a.y) * morph_t;
155
+ }
156
+ }
157
+ }
158
+
159
+ } // namespace vroom