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
@@ -18,6 +18,8 @@
18
18
 
19
19
  #include "curve.h"
20
20
  #include "gradient.h"
21
+ #include "line_morph.h"
22
+ #include "tip_geometry.h"
21
23
  #include "tip_pulse.h"
22
24
  #include "viewport.h"
23
25
 
@@ -51,9 +53,82 @@ void stroke_path(SkCanvas* canvas,
51
53
 
52
54
  inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
53
55
 
54
- // Width of the background-colored ring that separates the tip dot from the line
55
- // and from the pulse expanding out behind it.
56
- constexpr float kTipBorderPx = 2.f;
56
+ // Slot `k` of an indicator series in screen space, blended from the outgoing
57
+ // capture toward the new value. Slot 0 is the rightmost vertex on both sides —
58
+ // the pairing a timeframe switch preserves, same as close_vertex below.
59
+ //
60
+ // The capture is in fractions of the candle area and the price band, so it
61
+ // lands on the pixels it occupied pre-switch even though both the surface and
62
+ // the bounds may have changed under it.
63
+ MorphVertex series_vertex(const Layout& lay,
64
+ const PriceBounds& bounds,
65
+ const ::VroomCandle* visible,
66
+ std::size_t n,
67
+ const double* values,
68
+ int64_t window_ms,
69
+ int64_t visible_start_ms,
70
+ int64_t candle_duration_ms,
71
+ int64_t time_shift_ms,
72
+ const LineMorph* from,
73
+ std::size_t from_count,
74
+ float morph_t,
75
+ std::size_t k) {
76
+ MorphVertex to;
77
+ if (values && k < n) {
78
+ const std::size_t i = n - 1 - k;
79
+ const double v = values[i];
80
+ if (std::isfinite(v)) {
81
+ to = MorphVertex{
82
+ vroom::candle_center_x(lay, visible[i].time_ms + time_shift_ms,
83
+ candle_duration_ms, visible_start_ms,
84
+ window_ms),
85
+ vroom::price_to_y(lay, bounds, v), true};
86
+ }
87
+ }
88
+ MorphVertex frm;
89
+ if (k < from_count) {
90
+ const LineSnapshot& s = from->pts[k];
91
+ if (s.valid) {
92
+ frm = MorphVertex{s.x * vroom::candle_area_width(lay),
93
+ vroom::y_at_fraction(lay, s.y), true};
94
+ }
95
+ }
96
+ return vroom::morph_vertex(to, frm, morph_t);
97
+ }
98
+
99
+ // The same series walked left to right into `out`, for the two fills — they
100
+ // need to look ahead for crossovers and back along the opposite edge, so they
101
+ // can't stream the way the stroke does. out[j] is slot slots-1-j.
102
+ void resolve_series(const Layout& lay,
103
+ const PriceBounds& bounds,
104
+ const ::VroomCandle* visible,
105
+ std::size_t n,
106
+ const double* values,
107
+ int64_t window_ms,
108
+ int64_t visible_start_ms,
109
+ int64_t candle_duration_ms,
110
+ int64_t time_shift_ms,
111
+ const LineMorph* from,
112
+ float morph_t,
113
+ std::size_t slots,
114
+ std::vector<MorphVertex>& out) {
115
+ const std::size_t from_count = vroom::morph_line_count(from, morph_t);
116
+ out.clear();
117
+ out.reserve(slots);
118
+ for (std::size_t j = 0; j < slots; ++j) {
119
+ out.push_back(series_vertex(lay, bounds, visible, n, values, window_ms,
120
+ visible_start_ms, candle_duration_ms,
121
+ time_shift_ms, from, from_count, morph_t,
122
+ slots - 1 - j));
123
+ }
124
+ }
125
+
126
+ // How many vertices a morphing series spans: the new slice and the capture can
127
+ // disagree on length, and every slot either defines has to be walked.
128
+ inline std::size_t series_slots(std::size_t n, const LineMorph* from,
129
+ float morph_t) {
130
+ return std::max(n, vroom::morph_line_count(from, morph_t));
131
+ }
57
132
 
58
133
  // One vertex of the close polyline, in screen space. Slot `k` counts back from
59
134
  // the right edge (0 = the newest close), the pairing a timeframe switch
@@ -198,31 +273,43 @@ void draw(SkCanvas* canvas,
198
273
  uint32_t color,
199
274
  float width,
200
275
  const unsigned char* break_before,
201
- float opacity) {
202
- if (!canvas || !values_visible || n == 0 || candle_right <= 0.f ||
203
- candle_area_h <= 0.f) {
204
- return;
205
- }
276
+ float opacity,
277
+ int64_t time_shift_ms,
278
+ const LineMorph* from,
279
+ float morph_t) {
280
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
206
281
  opacity = std::clamp(opacity, 0.f, 1.f);
207
282
  if (opacity <= 0.f) return;
283
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
284
+
285
+ const std::size_t from_count = vroom::morph_line_count(from, morph_t);
286
+ const std::size_t slots = std::max(n, from_count);
287
+ if (slots == 0 || (!values_visible && from_count == 0)) return;
208
288
 
289
+ // Descending slots, which walks the line left to right. Settled (no
290
+ // capture) this visits exactly the source indices 0..n-1 in order, so the
291
+ // path is the one it has always emitted.
209
292
  // SkPathBuilder (not SkPath's edit methods, removed in newer Skia tips).
210
293
  SkPathBuilder path;
211
294
  bool pen_down = false;
212
- for (std::size_t i = 0; i < n; ++i) {
213
- const double v = values_visible[i];
214
- if (!std::isfinite(v)) {
295
+ for (std::size_t j = 0; j < slots; ++j) {
296
+ const std::size_t k = slots - 1 - j;
297
+ const MorphVertex p = series_vertex(
298
+ lay, bounds, visible, n, values_visible, window_ms,
299
+ visible_start_ms, candle_duration_ms, time_shift_ms, from,
300
+ from_count, morph_t, k);
301
+ if (!p.valid) {
215
302
  pen_down = false;
216
303
  continue;
217
304
  }
218
- const float x = vroom::candle_center_x(
219
- lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
220
- window_ms);
221
- const float y = vroom::price_to_y(lay, bounds, v);
222
- if (pen_down && !(break_before && break_before[i])) {
223
- path.lineTo(x, y);
305
+ // Session breaks are a property of the incoming data, so a slot the new
306
+ // series doesn't reach can't carry one.
307
+ const bool breaks =
308
+ break_before && k < n && break_before[n - 1 - k] != 0;
309
+ if (pen_down && !breaks) {
310
+ path.lineTo(p.x, p.y);
224
311
  } else {
225
- path.moveTo(x, y);
312
+ path.moveTo(p.x, p.y);
226
313
  pen_down = true;
227
314
  }
228
315
  }
@@ -329,6 +416,7 @@ void draw_close_tip(SkCanvas* canvas,
329
416
  int64_t visible_start_ms,
330
417
  int64_t candle_duration_ms,
331
418
  float candle_right,
419
+ float clip_right,
332
420
  float candle_area_h,
333
421
  uint32_t line_color,
334
422
  uint32_t bg_color,
@@ -357,14 +445,21 @@ void draw_close_tip(SkCanvas* canvas,
357
445
  if (tip.fY < 0.f || tip.fY > candle_area_h) return;
358
446
  if (tip.fX < 0.f || tip.fX > candle_right) return;
359
447
 
360
- // Scaling off the stroke keeps the marker proportionate at any line width;
361
- // the floor stops a hairline chart from getting an invisible dot.
362
- const float w = line_width > 0.f ? line_width : 1.5f;
363
- const float dot_r = std::max(2.f, w * 1.5f);
364
- const float border_r = dot_r + kTipBorderPx;
448
+ const auto geo = vroom::tip_geometry::of(line_width);
449
+ const float dot_r = geo.dot_r;
450
+ const float border_r = geo.border_r;
365
451
 
366
452
  canvas->save();
367
- canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
453
+ // Wider than the on-pane test above: the dot is anchored at the newest
454
+ // candle's center, which on a view pinned to the latest bar is closer to
455
+ // candle_right than the dot's own radius, so it has to reach into the gutter
456
+ // to draw in full. `clip_right` is the gutter's far side — the y-axis
457
+ // strip's edge — which the layout keeps wide enough for the dot (see
458
+ // tip_geometry::gutter_px). The pulse ring is much wider still and clips
459
+ // there; that is intended.
460
+ canvas->clipRect(
461
+ SkRect::MakeLTRB(0.f, 0.f, std::max(candle_right, clip_right),
462
+ candle_area_h));
368
463
 
369
464
  // Ring first: the border paints over its inner edge, so it reads as
370
465
  // expanding out from underneath the dot rather than around it.
@@ -419,42 +514,51 @@ void fill_between(SkCanvas* canvas,
419
514
  float candle_right,
420
515
  float candle_area_h,
421
516
  uint32_t color,
422
- float opacity) {
423
- if (!canvas || !upper_visible || !lower_visible || n == 0 ||
424
- candle_right <= 0.f || candle_area_h <= 0.f) {
425
- return;
426
- }
517
+ float opacity,
518
+ const LineMorph* upper_from,
519
+ const LineMorph* lower_from,
520
+ float morph_t) {
521
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
427
522
  opacity = std::clamp(opacity, 0.f, 1.f);
428
523
  if (opacity <= 0.f) return;
524
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
525
+
526
+ const std::size_t slots =
527
+ std::max(series_slots(n, upper_from, morph_t),
528
+ series_slots(n, lower_from, morph_t));
529
+ if (slots == 0) return;
429
530
 
430
- // One closed contour per maximal run where both series are finite; a
531
+ // Both edges resolved left to right first: mid-morph a vertex is a blend of
532
+ // the captured shape and the new one, and the fill has to be stitched from
533
+ // the same positions the two lines are stroked at.
534
+ static thread_local std::vector<MorphVertex> upper;
535
+ static thread_local std::vector<MorphVertex> lower;
536
+ resolve_series(lay, bounds, visible, n, upper_visible, window_ms,
537
+ visible_start_ms, candle_duration_ms, 0, upper_from, morph_t,
538
+ slots, upper);
539
+ resolve_series(lay, bounds, visible, n, lower_visible, window_ms,
540
+ visible_start_ms, candle_duration_ms, 0, lower_from, morph_t,
541
+ slots, lower);
542
+
543
+ // One closed contour per maximal run where both edges are defined; a
431
544
  // single-point run has no area. Multiple runs (e.g. around a data gap)
432
545
  // become multiple contours in one path.
433
546
  SkPathBuilder path;
434
547
  std::size_t i = 0;
435
- while (i < n) {
436
- if (!std::isfinite(upper_visible[i]) ||
437
- !std::isfinite(lower_visible[i])) {
548
+ while (i < slots) {
549
+ if (!upper[i].valid || !lower[i].valid) {
438
550
  ++i;
439
551
  continue;
440
552
  }
441
553
  std::size_t e = i;
442
- while (e + 1 < n && std::isfinite(upper_visible[e + 1]) &&
443
- std::isfinite(lower_visible[e + 1])) {
444
- ++e;
445
- }
554
+ while (e + 1 < slots && upper[e + 1].valid && lower[e + 1].valid) ++e;
446
555
  if (e > i) {
447
- const auto x_at = [&](std::size_t k) {
448
- return vroom::candle_center_x(lay, visible[k].time_ms,
449
- candle_duration_ms,
450
- visible_start_ms, window_ms);
451
- };
452
- path.moveTo(x_at(i), vroom::price_to_y(lay, bounds, upper_visible[i]));
556
+ path.moveTo(upper[i].x, upper[i].y);
453
557
  for (std::size_t k = i + 1; k <= e; ++k) {
454
- path.lineTo(x_at(k), vroom::price_to_y(lay, bounds, upper_visible[k]));
558
+ path.lineTo(upper[k].x, upper[k].y);
455
559
  }
456
560
  for (std::size_t k = e + 1; k-- > i;) {
457
- path.lineTo(x_at(k), vroom::price_to_y(lay, bounds, lower_visible[k]));
561
+ path.lineTo(lower[k].x, lower[k].y);
458
562
  }
459
563
  path.close();
460
564
  }
@@ -473,4 +577,128 @@ void fill_between(SkCanvas* canvas,
473
577
  canvas->restore();
474
578
  }
475
579
 
580
+ void fill_cloud(SkCanvas* canvas,
581
+ const Layout& lay,
582
+ const PriceBounds& bounds,
583
+ const ::VroomCandle* visible,
584
+ std::size_t n,
585
+ const double* a_visible,
586
+ const double* b_visible,
587
+ int64_t window_ms,
588
+ int64_t visible_start_ms,
589
+ int64_t candle_duration_ms,
590
+ float candle_right,
591
+ float candle_area_h,
592
+ uint32_t above_color,
593
+ uint32_t below_color,
594
+ float opacity,
595
+ int64_t time_shift_ms,
596
+ const LineMorph* a_from,
597
+ const LineMorph* b_from,
598
+ float morph_t) {
599
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
600
+ opacity = std::clamp(opacity, 0.f, 1.f);
601
+ if (opacity <= 0.f) return;
602
+ morph_t = std::clamp(morph_t, 0.f, 1.f);
603
+
604
+ const std::size_t slots = std::max(series_slots(n, a_from, morph_t),
605
+ series_slots(n, b_from, morph_t));
606
+ if (slots == 0) return;
607
+
608
+ static thread_local std::vector<MorphVertex> edge_a;
609
+ static thread_local std::vector<MorphVertex> edge_b;
610
+ resolve_series(lay, bounds, visible, n, a_visible, window_ms,
611
+ visible_start_ms, candle_duration_ms, time_shift_ms, a_from,
612
+ morph_t, slots, edge_a);
613
+ resolve_series(lay, bounds, visible, n, b_visible, window_ms,
614
+ visible_start_ms, candle_duration_ms, time_shift_ms, b_from,
615
+ morph_t, slots, edge_b);
616
+
617
+ // A vertex of the cloud: one x with the two edge heights there. At a
618
+ // crossover the two collapse onto the same y, which is what lets the
619
+ // neighboring contours meet on a point instead of overlapping.
620
+ struct Vertex {
621
+ float x, ya, yb;
622
+ };
623
+
624
+ SkPathBuilder above_path;
625
+ SkPathBuilder below_path;
626
+ std::vector<Vertex> contour;
627
+
628
+ // Closes the accumulated vertices into the path for whichever side is on
629
+ // top: forward along edge a, back along edge b.
630
+ const auto flush = [&](int sign) {
631
+ if (contour.size() >= 2) {
632
+ SkPathBuilder& into = sign >= 0 ? above_path : below_path;
633
+ into.moveTo(contour[0].x, contour[0].ya);
634
+ for (std::size_t k = 1; k < contour.size(); ++k) {
635
+ into.lineTo(contour[k].x, contour[k].ya);
636
+ }
637
+ for (std::size_t k = contour.size(); k-- > 0;) {
638
+ into.lineTo(contour[k].x, contour[k].yb);
639
+ }
640
+ into.close();
641
+ }
642
+ contour.clear();
643
+ };
644
+
645
+ // Which tone a run takes, read off the drawn geometry rather than the raw
646
+ // values: y grows downward, so a above b means ya is the smaller. Mid-morph
647
+ // the values belong to two different resolutions and only the interpolated
648
+ // positions say where the edges actually sit.
649
+ const auto separation = [&](std::size_t k) {
650
+ return static_cast<double>(edge_b[k].y - edge_a[k].y);
651
+ };
652
+
653
+ std::size_t i = 0;
654
+ while (i < slots) {
655
+ if (!edge_a[i].valid || !edge_b[i].valid) {
656
+ ++i;
657
+ continue;
658
+ }
659
+ std::size_t e = i;
660
+ while (e + 1 < slots && edge_a[e + 1].valid && edge_b[e + 1].valid) ++e;
661
+
662
+ int sign = 0;
663
+ Vertex prev{};
664
+ for (std::size_t k = i; k <= e; ++k) {
665
+ const Vertex v = Vertex{edge_a[k].x, edge_a[k].y, edge_b[k].y};
666
+ const double d = separation(k);
667
+ const int s = d > 0.0 ? 1 : (d < 0.0 ? -1 : 0);
668
+ if (sign != 0 && s != 0 && s != sign) {
669
+ // The spans swapped between the previous bar and this one. Meet
670
+ // them at the crossing so both tones end on the same point.
671
+ const double d0 = separation(k - 1);
672
+ const float t = static_cast<float>(d0 / (d0 - d));
673
+ const float xc = prev.x + (v.x - prev.x) * t;
674
+ const float yc = prev.ya + (v.ya - prev.ya) * t;
675
+ contour.push_back(Vertex{xc, yc, yc});
676
+ flush(sign);
677
+ contour.push_back(Vertex{xc, yc, yc});
678
+ sign = s;
679
+ } else if (sign == 0) {
680
+ sign = s;
681
+ }
682
+ contour.push_back(v);
683
+ prev = v;
684
+ }
685
+ flush(sign);
686
+ i = e + 1;
687
+ }
688
+
689
+ SkPaint fill;
690
+ fill.setAntiAlias(true);
691
+ fill.setStyle(SkPaint::kFill_Style);
692
+
693
+ canvas->save();
694
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
695
+ fill.setColor(static_cast<SkColor>(above_color));
696
+ fill.setAlphaf(fill.getAlphaf() * opacity);
697
+ canvas->drawPath(above_path.detach(), fill);
698
+ fill.setColor(static_cast<SkColor>(below_color));
699
+ fill.setAlphaf(fill.getAlphaf() * opacity);
700
+ canvas->drawPath(below_path.detach(), fill);
701
+ canvas->restore();
702
+ }
703
+
476
704
  } // namespace vroom::ma_overlay
@@ -13,6 +13,7 @@ class SkCanvas;
13
13
  namespace vroom {
14
14
  struct CandleSnapshot;
15
15
  struct Layout;
16
+ struct LineMorph;
16
17
  struct PriceBounds;
17
18
  } // namespace vroom
18
19
 
@@ -25,6 +26,18 @@ namespace vroom::ma_overlay {
25
26
  // `break_before` (optional, aligned with `visible`) forces a new subpath at any
26
27
  // index where it is non-zero — used by VWAP to break the line at session
27
28
  // resets. Pass nullptr for continuous lines (SMA/EMA).
29
+ //
30
+ // `time_shift_ms` offsets where each value lands on the time axis, for a series
31
+ // drawn away from the bar it was computed on (Ichimoku's leading and lagging
32
+ // spans). The shifted time need not be a bar that exists: projection is linear,
33
+ // so a leading span runs past the newest candle into empty time. `visible` is
34
+ // then the *source* slice — see ichimoku::shifted_source_range.
35
+ //
36
+ // `from` / `morph_t` are the interval morph, the indicator counterpart of what
37
+ // draw_close_line does for the price series: the capture holds this series'
38
+ // outgoing shape indexed from the right (slot 0 = newest) and each vertex
39
+ // slides from where it sat before the timeframe switch to where it sits now.
40
+ // A null capture (the default) draws `values_visible` alone.
28
41
  void draw(SkCanvas* canvas,
29
42
  const Layout& lay,
30
43
  const PriceBounds& bounds,
@@ -39,7 +52,10 @@ void draw(SkCanvas* canvas,
39
52
  uint32_t color,
40
53
  float width,
41
54
  const unsigned char* break_before = nullptr,
42
- float opacity = 1.f);
55
+ float opacity = 1.f,
56
+ int64_t time_shift_ms = 0,
57
+ const LineMorph* from = nullptr,
58
+ float morph_t = 1.f);
43
59
 
44
60
  // The close-price polyline of line-chart mode. Equivalent to draw() fed the
45
61
  // visible closes, plus the interval morph: `from` / `from_n` is the outgoing
@@ -115,6 +131,12 @@ void draw_close_gradient(SkCanvas* canvas,
115
131
  //
116
132
  // `pulse_phase` is in cycles and wraps, so the caller can hand over elapsed time
117
133
  // divided by tip_pulse::kPeriodSeconds. Ignored unless `pulse`.
134
+ //
135
+ // `candle_right` bounds where the marked candle may be for the marker to draw at
136
+ // all; `clip_right` bounds the paint, and is wider — the dot overhangs the plot
137
+ // when the newest candle is at the right edge, so it paints into the gutter the
138
+ // layout reserves for it. Callers must therefore draw the tip *after* the axis
139
+ // backgrounds, which mask that gutter.
118
140
  void draw_close_tip(SkCanvas* canvas,
119
141
  const Layout& lay,
120
142
  const PriceBounds& bounds,
@@ -124,6 +146,7 @@ void draw_close_tip(SkCanvas* canvas,
124
146
  int64_t visible_start_ms,
125
147
  int64_t candle_duration_ms,
126
148
  float candle_right,
149
+ float clip_right,
127
150
  float candle_area_h,
128
151
  uint32_t line_color,
129
152
  uint32_t bg_color,
@@ -140,6 +163,9 @@ void draw_close_tip(SkCanvas* canvas,
140
163
  // Runs where either series is NaN are skipped, so the fill never bridges the
141
164
  // warmup gap. Plain-alpha SkPaint fill, no gradient ramp (see gradient.h).
142
165
  // Clipped to the candle area like draw().
166
+ //
167
+ // Takes a capture per edge so the fill is built from the same interpolated
168
+ // vertices its two lines are, and can't detach from them mid-morph.
143
169
  void fill_between(SkCanvas* canvas,
144
170
  const Layout& lay,
145
171
  const PriceBounds& bounds,
@@ -153,6 +179,42 @@ void fill_between(SkCanvas* canvas,
153
179
  float candle_right,
154
180
  float candle_area_h,
155
181
  uint32_t color,
156
- float opacity);
182
+ float opacity,
183
+ const LineMorph* upper_from = nullptr,
184
+ const LineMorph* lower_from = nullptr,
185
+ float morph_t = 1.f);
186
+
187
+ // Ichimoku's cloud: the region between two aligned series, filled with
188
+ // `above_color` where `a` is over `b` and `below_color` where it is under, both
189
+ // at their alpha × `opacity`.
190
+ //
191
+ // Each run is split at every crossover, with the interpolated meeting point
192
+ // added to the contour on both sides, so the two tones abut exactly instead of
193
+ // overlapping or leaving a seam. NaN in either series breaks the run, as in
194
+ // fill_between. `time_shift_ms` shifts the whole cloud along the time axis the
195
+ // same way draw() does.
196
+ //
197
+ // Morphs per edge like fill_between. Which tone a run takes is read off the
198
+ // interpolated vertices rather than the raw values, so a crossover that only
199
+ // exists mid-morph still splits the cloud where the two edges actually meet.
200
+ void fill_cloud(SkCanvas* canvas,
201
+ const Layout& lay,
202
+ const PriceBounds& bounds,
203
+ const ::VroomCandle* visible,
204
+ std::size_t n,
205
+ const double* a_visible,
206
+ const double* b_visible,
207
+ int64_t window_ms,
208
+ int64_t visible_start_ms,
209
+ int64_t candle_duration_ms,
210
+ float candle_right,
211
+ float candle_area_h,
212
+ uint32_t above_color,
213
+ uint32_t below_color,
214
+ float opacity,
215
+ int64_t time_shift_ms = 0,
216
+ const LineMorph* a_from = nullptr,
217
+ const LineMorph* b_from = nullptr,
218
+ float morph_t = 1.f);
157
219
 
158
220
  } // namespace vroom::ma_overlay
@@ -1,6 +1,7 @@
1
1
  #include "macd.h"
2
2
 
3
- #include <cmath> // std::nan, std::isfinite
3
+ #include <algorithm> // std::max
4
+ #include <cmath> // std::nan, std::isfinite, std::abs
4
5
 
5
6
  #include "ma.h"
6
7
  #include "series_ma.h"
@@ -36,4 +37,26 @@ void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
36
37
  }
37
38
  }
38
39
 
40
+ double autoscale(const double* macd, const double* signal, const double* hist,
41
+ std::size_t n) {
42
+ double scale = 0.0;
43
+ const auto track = [&](const double* s) {
44
+ if (!s) return;
45
+ for (std::size_t i = 0; i < n; ++i) {
46
+ if (std::isfinite(s[i])) scale = std::max(scale, std::abs(s[i]));
47
+ }
48
+ };
49
+ track(macd);
50
+ track(signal);
51
+ track(hist);
52
+ return scale;
53
+ }
54
+
55
+ double band_fraction(double v, double scale, double y_scale) {
56
+ // Nothing on show yet — everything collapses onto the zero line rather than
57
+ // dividing by zero.
58
+ if (!(scale > 0.0)) return 0.5;
59
+ return 0.5 + (v / scale) * 0.5 * kBandPadFraction * y_scale;
60
+ }
61
+
39
62
  } // namespace vroom::macd
@@ -16,6 +16,10 @@
16
16
 
17
17
  namespace vroom::macd {
18
18
 
19
+ // How much of the pane's half-band an extreme is allowed to fill, so a peak
20
+ // stops short of the separator instead of touching it.
21
+ constexpr double kBandPadFraction = 0.85;
22
+
19
23
  // Computes MACD over [candles, candles+n). fast/slow/signal are clamped to >= 1
20
24
  // (callers pass 12/26/9 by default; slow should exceed fast). `source` selects
21
25
  // the price series (vroom::ma::Source); `ma_kind` picks the averaging for the
@@ -29,4 +33,16 @@ void compute(const ::VroomCandle* candles, std::size_t n, int fast, int slow,
29
33
  std::vector<double>& macd_out, std::vector<double>& signal_out,
30
34
  std::vector<double>& hist_out);
31
35
 
36
+ // The amplitude the pane band is fitted to, symmetric about zero: the largest
37
+ // finite magnitude across the series on show, or 0 when there is nothing to
38
+ // plot. Pass nullptr for a hidden series so the rest fill the band.
39
+ double autoscale(const double* macd, const double* signal, const double* hist,
40
+ std::size_t n);
41
+
42
+ // Where a value sits in the pane band, as a fraction of its height — 0 at the
43
+ // bottom edge, 1 at the top, 0.5 on the zero line. `y_scale` is the user's
44
+ // y-axis zoom (1 = the default fit). Split out of the renderer so the
45
+ // interval-morph capture maps its geometry through the same math.
46
+ double band_fraction(double v, double scale, double y_scale);
47
+
32
48
  } // namespace vroom::macd