react-native-vroom-chart 0.1.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/android/README.md +7 -0
  4. package/cpp/README.md +11 -0
  5. package/cpp/VroomChartHostObject.cpp +458 -0
  6. package/cpp/VroomChartHostObject.h +36 -0
  7. package/cpp/VroomJsiInstaller.cpp +69 -0
  8. package/cpp/VroomJsiInstaller.h +10 -0
  9. package/cpp/VroomSkiaContext.cpp +25 -0
  10. package/cpp/VroomSkiaContext.h +30 -0
  11. package/cpp/_core_include/vroom/vroom_chart.h +195 -0
  12. package/cpp/_core_src/candles.cpp +74 -0
  13. package/cpp/_core_src/candles.h +34 -0
  14. package/cpp/_core_src/chart.cpp +317 -0
  15. package/cpp/_core_src/chart.h +167 -0
  16. package/cpp/_core_src/chart_facade.cpp +570 -0
  17. package/cpp/_core_src/chart_internal.h +30 -0
  18. package/cpp/_core_src/crosshair.cpp +65 -0
  19. package/cpp/_core_src/crosshair.h +32 -0
  20. package/cpp/_core_src/fonts.cpp +22 -0
  21. package/cpp/_core_src/fonts.h +25 -0
  22. package/cpp/_core_src/labels.cpp +340 -0
  23. package/cpp/_core_src/labels.h +85 -0
  24. package/cpp/_core_src/ma.cpp +53 -0
  25. package/cpp/_core_src/ma.h +39 -0
  26. package/cpp/_core_src/ma_overlay.cpp +73 -0
  27. package/cpp/_core_src/ma_overlay.h +42 -0
  28. package/cpp/_core_src/macd.cpp +68 -0
  29. package/cpp/_core_src/macd.h +29 -0
  30. package/cpp/_core_src/macd_pane.cpp +199 -0
  31. package/cpp/_core_src/macd_pane.h +41 -0
  32. package/cpp/_core_src/price_indicator.cpp +106 -0
  33. package/cpp/_core_src/price_indicator.h +29 -0
  34. package/cpp/_core_src/rsi.cpp +70 -0
  35. package/cpp/_core_src/rsi.h +32 -0
  36. package/cpp/_core_src/rsi_pane.cpp +167 -0
  37. package/cpp/_core_src/rsi_pane.h +39 -0
  38. package/cpp/_core_src/theme.cpp +41 -0
  39. package/cpp/_core_src/theme.h +23 -0
  40. package/cpp/_core_src/ticks.cpp +49 -0
  41. package/cpp/_core_src/ticks.h +30 -0
  42. package/cpp/_core_src/viewport.cpp +141 -0
  43. package/cpp/_core_src/viewport.h +100 -0
  44. package/cpp/_core_src/volume.cpp +70 -0
  45. package/cpp/_core_src/volume.h +34 -0
  46. package/cpp/_core_src/vwap.cpp +51 -0
  47. package/cpp/_core_src/vwap.h +27 -0
  48. package/ios/README.md +7 -0
  49. package/ios/VroomChartModule.h +11 -0
  50. package/ios/VroomChartModule.mm +44 -0
  51. package/lib/index.d.mts +185 -0
  52. package/lib/index.d.ts +185 -0
  53. package/lib/index.js +429 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/index.mjs +396 -0
  56. package/lib/index.mjs.map +1 -0
  57. package/package.json +75 -0
  58. package/react-native-vroom-chart.podspec +79 -0
  59. package/src/NativeVroomChart.ts +12 -0
  60. package/src/VroomChart.tsx +382 -0
  61. package/src/index.ts +14 -0
  62. package/src/jsi.d.ts +128 -0
  63. package/src/packCandles.ts +24 -0
  64. package/src/theme.ts +43 -0
  65. package/src/types.ts +27 -0
  66. package/src/useChartCore.ts +135 -0
@@ -0,0 +1,167 @@
1
+ #include "rsi_pane.h"
2
+
3
+ #pragma clang diagnostic push
4
+ #pragma clang diagnostic ignored "-Wdocumentation"
5
+ #include "include/core/SkCanvas.h"
6
+ #include "include/core/SkColor.h"
7
+ #include "include/core/SkFont.h"
8
+ #include "include/core/SkFontTypes.h"
9
+ #include "include/core/SkPaint.h"
10
+ #include "include/core/SkPath.h"
11
+ #include "include/core/SkPathBuilder.h"
12
+ #include "include/core/SkRect.h"
13
+ #include "include/core/SkTypeface.h"
14
+ #include "include/effects/SkDashPathEffect.h"
15
+ #pragma clang diagnostic pop
16
+
17
+ #include <cmath>
18
+ #include <cstdio>
19
+ #include <cstring>
20
+
21
+ #include "chart.h"
22
+ #include "fonts.h"
23
+ #include "theme.h"
24
+ #include "viewport.h"
25
+
26
+ namespace vroom::rsi_pane {
27
+
28
+ namespace {
29
+ constexpr SkColor kRsiLine = 0xff8957e5; // violet RSI line (themable later)
30
+ constexpr SkColor kRsiMaLine = 0xffd29922; // amber RSI moving-average trendline
31
+ constexpr SkColor kRefLine = 0xff30363d; // band reference lines
32
+ constexpr SkColor kDivider = 0xff21262d; // pane separator
33
+ constexpr SkScalar kDash[2] = {3.f, 3.f};
34
+ } // namespace
35
+
36
+ void draw(SkCanvas* canvas,
37
+ const VroomChart& chart,
38
+ const Layout& lay,
39
+ const ::VroomCandle* visible,
40
+ std::size_t n,
41
+ const double* rsi_visible,
42
+ const double* rsi_ma_visible,
43
+ int64_t window_ms,
44
+ int64_t visible_start_ms,
45
+ int64_t candle_duration_ms,
46
+ float candle_right,
47
+ float pane_top,
48
+ float pane_bottom) {
49
+ if (!canvas || n == 0 || candle_right <= 0.f) return;
50
+ const float band_h = pane_bottom - pane_top;
51
+ if (band_h <= 0.f) return;
52
+
53
+ auto y_for = [&](double v) -> float {
54
+ return pane_bottom - static_cast<float>(v / 100.0) * band_h;
55
+ };
56
+
57
+ // Mask the band with the background so candles/volume that overflow below
58
+ // the (shortened) price pane don't show through behind the RSI. The y-axis
59
+ // strip to the right is already masked full-height by draw_chart.
60
+ SkPaint bg;
61
+ bg.setColor(chart.theme.colors[VROOM_COLOR_BACKGROUND]);
62
+ canvas->drawRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom),
63
+ bg);
64
+
65
+ // Pane separator (top edge).
66
+ SkPaint divider;
67
+ divider.setColor(kDivider);
68
+ divider.setStrokeWidth(1.f);
69
+ canvas->drawLine(0.f, pane_top, candle_right, pane_top, divider);
70
+
71
+ // Keep the line + reference lines inside the pane / candle area.
72
+ canvas->save();
73
+ canvas->clipRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom));
74
+
75
+ // Configurable band reference lines (overbought / oversold).
76
+ SkPaint ref;
77
+ ref.setAntiAlias(true);
78
+ ref.setColor(kRefLine);
79
+ ref.setStrokeWidth(1.f);
80
+ ref.setPathEffect(SkDashPathEffect::Make(kDash, 0.f));
81
+ const float y_upper = y_for(chart.rsi_upper);
82
+ const float y_lower = y_for(chart.rsi_lower);
83
+ canvas->drawLine(0.f, y_upper, candle_right, y_upper, ref);
84
+ canvas->drawLine(0.f, y_lower, candle_right, y_lower, ref);
85
+
86
+ // Strokes a value series as a polyline; a NaN lifts the pen so undefined
87
+ // leading values (i < period) leave a gap.
88
+ auto stroke_series = [&](const double* series, SkColor color) {
89
+ if (!series) return;
90
+ SkPathBuilder path;
91
+ bool pen_down = false;
92
+ for (std::size_t i = 0; i < n; ++i) {
93
+ const double v = series[i];
94
+ if (!std::isfinite(v)) {
95
+ pen_down = false;
96
+ continue;
97
+ }
98
+ const float x = vroom::candle_center_x(
99
+ lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
100
+ window_ms);
101
+ const float y = y_for(v);
102
+ if (pen_down) {
103
+ path.lineTo(x, y);
104
+ } else {
105
+ path.moveTo(x, y);
106
+ pen_down = true;
107
+ }
108
+ }
109
+ SkPaint line;
110
+ line.setAntiAlias(true);
111
+ line.setColor(color);
112
+ line.setStyle(SkPaint::kStroke_Style);
113
+ line.setStrokeWidth(1.5f);
114
+ canvas->drawPath(path.detach(), line);
115
+ };
116
+ stroke_series(rsi_visible, kRsiLine);
117
+ stroke_series(rsi_ma_visible, kRsiMaLine); // trendline on top
118
+
119
+ // Caption, top-left of the pane.
120
+ auto tf = vroom::axis_typeface();
121
+ if (tf) {
122
+ SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
123
+ font.setSubpixel(true);
124
+ font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
125
+
126
+ char caption[24];
127
+ std::snprintf(caption, sizeof(caption), "RSI %d", chart.rsi_period);
128
+ SkRect cb;
129
+ font.measureText(caption, std::strlen(caption), SkTextEncoding::kUTF8,
130
+ &cb);
131
+ SkPaint cap_paint;
132
+ cap_paint.setAntiAlias(true);
133
+ cap_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
134
+ canvas->drawString(caption, 6.f, pane_top + 4.f - cb.fTop, font,
135
+ cap_paint);
136
+ }
137
+ canvas->restore();
138
+
139
+ // 70 / 30 labels in the y-axis strip (drawn after the clip; the strip's
140
+ // background was already masked by draw_chart before this call).
141
+ if (tf) {
142
+ SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
143
+ font.setSubpixel(true);
144
+ font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
145
+ SkPaint text_paint;
146
+ text_paint.setAntiAlias(true);
147
+ text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
148
+
149
+ const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
150
+ auto label = [&](const char* s, float y) {
151
+ SkRect tb;
152
+ const float tw = font.measureText(s, std::strlen(s),
153
+ SkTextEncoding::kUTF8, &tb);
154
+ const float text_x = axis_center_x - tw * 0.5f;
155
+ const float baseline_y = y - (tb.fTop + tb.fBottom) * 0.5f;
156
+ canvas->drawString(s, text_x, baseline_y, font, text_paint);
157
+ };
158
+ char ub[8];
159
+ char lb[8];
160
+ std::snprintf(ub, sizeof(ub), "%.0f", chart.rsi_upper);
161
+ std::snprintf(lb, sizeof(lb), "%.0f", chart.rsi_lower);
162
+ label(ub, y_upper);
163
+ label(lb, y_lower);
164
+ }
165
+ }
166
+
167
+ } // namespace vroom::rsi_pane
@@ -0,0 +1,39 @@
1
+ // RSI indicator pane — draws the RSI line, 70/30 reference lines, axis labels,
2
+ // and a caption in the band below the candles. Its own module alongside
3
+ // candles / volume / price_indicator so the chart orchestrator stays thin.
4
+
5
+ #pragma once
6
+
7
+ #include <cstddef>
8
+ #include <cstdint>
9
+
10
+ #include "vroom/vroom_chart.h" // ::VroomCandle
11
+
12
+ class SkCanvas;
13
+
14
+ namespace vroom {
15
+ struct Layout;
16
+ } // namespace vroom
17
+
18
+ struct VroomChart;
19
+
20
+ namespace vroom::rsi_pane {
21
+
22
+ // Draws the RSI pane spanning vertically [pane_top, pane_bottom]. `rsi_visible`
23
+ // is the cached RSI series aligned with `visible` (NaN where undefined). Shares
24
+ // the candles' horizontal mapping (candle_center_x) so it scrolls in lock-step.
25
+ void draw(SkCanvas* canvas,
26
+ const VroomChart& chart,
27
+ const Layout& lay,
28
+ const ::VroomCandle* visible,
29
+ std::size_t n,
30
+ const double* rsi_visible,
31
+ const double* rsi_ma_visible,
32
+ int64_t window_ms,
33
+ int64_t visible_start_ms,
34
+ int64_t candle_duration_ms,
35
+ float candle_right,
36
+ float pane_top,
37
+ float pane_bottom);
38
+
39
+ } // namespace vroom::rsi_pane
@@ -0,0 +1,41 @@
1
+ #include "theme.h"
2
+
3
+ namespace vroom {
4
+
5
+ namespace {
6
+
7
+ // GitHub-dark palette.
8
+ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
9
+ 0xff0d1117, // BACKGROUND
10
+ 0xff26a69a, // BULL
11
+ 0xffef5350, // BEAR
12
+ 0xffaaaaaa, // WICK
13
+ 0xff1a1e24, // GRID — ~20% darker than the crosshair so it recedes
14
+ 0xffc9d1d9, // AXIS_TEXT
15
+ 0xff303741, // CROSSHAIR — grid tone lightened ~44%; reads above GRID
16
+ 0xff161b22, // TOOLTIP_BG
17
+ 0xffc9d1d9, // TOOLTIP_TEXT
18
+ 0xff3e4855, // CROSSHAIR_TARGET — CROSSHAIR lightened 30% (prior derived ring)
19
+ };
20
+
21
+ constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
22
+ 0.68f, // CANDLE_WIDTH_RATIO — body fills 68% of slot, 32% gap
23
+ 1.0f, // WICK_WIDTH_PX
24
+ 6.f, // RIGHT_PADDING_PX — small gutter between candles and y-axis
25
+ 11.f, // AXIS_FONT_SIZE_PX
26
+ 0.18f, // Y_AXIS_WIDTH_RATIO — fallback before font is loaded
27
+ 22.f, // X_AXIS_HEIGHT_PX — bottom strip for time labels
28
+ 0.5f, // VOLUME_OPACITY — candle color at 50% transparency
29
+ 0.20f, // INDICATOR_HEIGHT_FRAC — below-chart indicator pane = 20% of height
30
+ };
31
+
32
+ } // namespace
33
+
34
+ Theme default_theme() {
35
+ Theme t;
36
+ for (int i = 0; i < VROOM_COLOR_COUNT_; ++i) t.colors[i] = kDefaultColors[i];
37
+ for (int i = 0; i < VROOM_FLOAT_COUNT_; ++i) t.floats[i] = kDefaultFloats[i];
38
+ return t;
39
+ }
40
+
41
+ } // namespace vroom
@@ -0,0 +1,23 @@
1
+ // Theme — colors and float-typed style parameters for a single chart.
2
+ //
3
+ // Layout: indexed arrays keyed by the public C-facade enums (VroomColorKey,
4
+ // VroomFloatKey) so we can stay ABI-stable while adding new keys. Wrapped in
5
+ // a struct so future expansion (booleans for visibility toggles, strings for
6
+ // label formats) lands as new sub-arrays without churning every field access.
7
+
8
+ #pragma once
9
+
10
+ #include "vroom/vroom_chart.h"
11
+
12
+ namespace vroom {
13
+
14
+ struct Theme {
15
+ uint32_t colors[VROOM_COLOR_COUNT_]{};
16
+ float floats[VROOM_FLOAT_COUNT_]{};
17
+ };
18
+
19
+ // Returns a Theme pre-filled with the default dark palette + sensible
20
+ // layout defaults. Used by `VroomChart`'s constructor.
21
+ Theme default_theme();
22
+
23
+ } // namespace vroom
@@ -0,0 +1,49 @@
1
+ #include "ticks.h"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+
6
+ namespace vroom {
7
+
8
+ int64_t pick_time_interval(int64_t window_ms, float candle_area_w) {
9
+ static constexpr int64_t kIntervals[] = {
10
+ 60'000LL, // 1m
11
+ 5 * 60'000LL, // 5m
12
+ 15 * 60'000LL, // 15m
13
+ 30 * 60'000LL, // 30m
14
+ 60 * 60'000LL, // 1h
15
+ 2 * 60 * 60'000LL, // 2h
16
+ 4 * 60 * 60'000LL, // 4h
17
+ 6 * 60 * 60'000LL, // 6h
18
+ 12 * 60 * 60'000LL, // 12h
19
+ 24 * 60 * 60'000LL, // 1d
20
+ 2 * 24 * 60 * 60'000LL, // 2d
21
+ 7 * 24 * 60 * 60'000LL, // 1w
22
+ };
23
+ if (window_ms <= 0 || candle_area_w <= 0.f) return kIntervals[0];
24
+ const double ms_per_px =
25
+ static_cast<double>(window_ms) / candle_area_w;
26
+ for (int64_t i : kIntervals) {
27
+ if (static_cast<double>(i) / ms_per_px >= kXLabelMinSpacing) {
28
+ return i;
29
+ }
30
+ }
31
+ return kIntervals[sizeof(kIntervals) / sizeof(kIntervals[0]) - 1];
32
+ }
33
+
34
+ double pick_price_interval(double range, float candle_area_h) {
35
+ if (range <= 0.0 || candle_area_h <= 0.f) return 1.0;
36
+ const int target_count =
37
+ std::max(2, static_cast<int>(candle_area_h / kYLabelTargetSpacing));
38
+ const double rough = range / target_count;
39
+ const double magnitude = std::pow(10.0, std::floor(std::log10(rough)));
40
+ const double normalized = rough / magnitude;
41
+ double nice;
42
+ if (normalized < 1.5) nice = 1.0;
43
+ else if (normalized < 3.5) nice = 2.0;
44
+ else if (normalized < 7.5) nice = 5.0;
45
+ else nice = 10.0;
46
+ return nice * magnitude;
47
+ }
48
+
49
+ } // namespace vroom
@@ -0,0 +1,30 @@
1
+ // Tick algorithms — pick a label interval based on current scale so labels
2
+ // land on canonical boundaries with reasonable spacing.
3
+ //
4
+ // Pure functions, no state. Used by the label subsystem; safe for future
5
+ // indicators that need their own axis grids to reuse.
6
+
7
+ #pragma once
8
+
9
+ #include <cstdint>
10
+
11
+ namespace vroom {
12
+
13
+ // Minimum spacing between adjacent x-axis labels in pixels. Smaller → more
14
+ // labels visible at every zoom level; larger → sparser.
15
+ inline constexpr float kXLabelMinSpacing = 50.f;
16
+
17
+ // Target spacing for y-axis labels — used to derive a target count.
18
+ inline constexpr float kYLabelTargetSpacing = 55.f;
19
+
20
+ // Selects the smallest time interval whose pixel-spacing meets the minimum.
21
+ // Returns milliseconds. Hierarchy: 1m → 5m → 15m → 30m → 1h → 2h → 4h → 6h →
22
+ // 12h → 1d → 2d → 1w.
23
+ int64_t pick_time_interval(int64_t window_ms, float candle_area_w);
24
+
25
+ // "Nice number" tick selection for the y-axis: snaps to 1, 2, or 5 × 10ⁿ
26
+ // based on the price range and target spacing. Returns the price interval
27
+ // in the same units as the data (e.g. dollars).
28
+ double pick_price_interval(double range, float candle_area_h);
29
+
30
+ } // namespace vroom
@@ -0,0 +1,141 @@
1
+ #include "viewport.h"
2
+
3
+ #include <algorithm>
4
+ #include <limits>
5
+
6
+ namespace vroom {
7
+
8
+ float candle_body_width(const Layout& layout,
9
+ int64_t window_ms,
10
+ int64_t candle_duration_ms) {
11
+ if (window_ms <= 0 || candle_duration_ms <= 0) return 0.f;
12
+ const float usable =
13
+ layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
14
+ const double slot = static_cast<double>(usable) *
15
+ (static_cast<double>(candle_duration_ms) /
16
+ static_cast<double>(window_ms));
17
+ return static_cast<float>(slot * layout.candle_width_ratio);
18
+ }
19
+
20
+ float candle_center_x(const Layout& layout,
21
+ int64_t time_ms,
22
+ int64_t candle_duration_ms,
23
+ int64_t visible_start_ms,
24
+ int64_t window_ms) {
25
+ if (window_ms <= 0) return 0.f;
26
+ const float usable =
27
+ layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
28
+ const double center_time =
29
+ static_cast<double>(time_ms) +
30
+ static_cast<double>(candle_duration_ms) * 0.5;
31
+ const double frac =
32
+ (center_time - static_cast<double>(visible_start_ms)) /
33
+ static_cast<double>(window_ms);
34
+ return static_cast<float>(static_cast<double>(usable) * frac);
35
+ }
36
+
37
+ size_t snap_index_to_candle(const Layout& layout,
38
+ const ::VroomCandle* candles,
39
+ size_t count,
40
+ int64_t candle_duration_ms,
41
+ int64_t visible_start_ms,
42
+ int64_t window_ms,
43
+ float x_px) {
44
+ const float usable =
45
+ layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
46
+ if (usable <= 0.f) return 0;
47
+
48
+ // Pixel x -> the period start time of the candle that would sit there.
49
+ const double target_center =
50
+ static_cast<double>(visible_start_ms) +
51
+ (static_cast<double>(x_px) / static_cast<double>(usable)) *
52
+ static_cast<double>(window_ms);
53
+ const int64_t key =
54
+ static_cast<int64_t>(target_center) - candle_duration_ms / 2;
55
+
56
+ // First candle with time_ms >= key, then pick whichever of it / its
57
+ // predecessor is closer in time.
58
+ auto* it = std::lower_bound(
59
+ candles, candles + count, key,
60
+ [](const ::VroomCandle& c, int64_t t) { return c.time_ms < t; });
61
+ if (it == candles) return 0;
62
+ if (it == candles + count) return count - 1;
63
+ const int64_t hi = it->time_ms;
64
+ const int64_t lo = (it - 1)->time_ms;
65
+ return (hi - key < key - lo) ? static_cast<size_t>(it - candles)
66
+ : static_cast<size_t>(it - 1 - candles);
67
+ }
68
+
69
+ float snap_x_to_candle(const Layout& layout,
70
+ const ::VroomCandle* candles,
71
+ size_t count,
72
+ int64_t candle_duration_ms,
73
+ int64_t visible_start_ms,
74
+ int64_t window_ms,
75
+ float x_px) {
76
+ if (count == 0 || window_ms <= 0) return x_px;
77
+
78
+ const float usable =
79
+ layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
80
+ if (usable <= 0.f) return x_px;
81
+
82
+ const size_t idx = snap_index_to_candle(
83
+ layout, candles, count, candle_duration_ms, visible_start_ms,
84
+ window_ms, x_px);
85
+ return candle_center_x(layout, candles[idx].time_ms, candle_duration_ms,
86
+ visible_start_ms, window_ms);
87
+ }
88
+
89
+ IndexRange visible_indices(const ::VroomCandle* candles,
90
+ size_t count,
91
+ int64_t start_ms,
92
+ int64_t end_ms) {
93
+ if (count == 0) return {0, 0};
94
+ if (start_ms == 0 && end_ms == 0) return {0, count};
95
+
96
+ // first index with time_ms >= start_ms
97
+ auto* first = std::lower_bound(
98
+ candles, candles + count, start_ms,
99
+ [](const ::VroomCandle& c, int64_t t) { return c.time_ms < t; });
100
+ // first index with time_ms > end_ms
101
+ auto* last = std::upper_bound(
102
+ candles, candles + count, end_ms,
103
+ [](int64_t t, const ::VroomCandle& c) { return t < c.time_ms; });
104
+ return {
105
+ static_cast<size_t>(first - candles),
106
+ static_cast<size_t>(last - candles),
107
+ };
108
+ }
109
+
110
+ PriceBounds price_bounds(const ::VroomCandle* candles, size_t count) {
111
+ PriceBounds b{
112
+ std::numeric_limits<double>::infinity(),
113
+ -std::numeric_limits<double>::infinity(),
114
+ };
115
+ for (size_t i = 0; i < count; ++i) {
116
+ b.min = std::min(b.min, candles[i].low);
117
+ b.max = std::max(b.max, candles[i].high);
118
+ }
119
+ if (count == 0) {
120
+ b.min = 0.0;
121
+ b.max = 1.0;
122
+ }
123
+ return b;
124
+ }
125
+
126
+ float price_to_y(const Layout& layout,
127
+ const PriceBounds& bounds,
128
+ double price) {
129
+ // The candle drawing area is the full height minus the x-axis strip and
130
+ // any below-chart indicator pane.
131
+ const float candle_area_h = price_pane_bottom(layout);
132
+ const float top = candle_area_h * layout.top_padding_frac;
133
+ const float bot = candle_area_h * (1.f - layout.bottom_padding_frac);
134
+ const float draw_h = bot - top;
135
+ const double range = bounds.max - bounds.min;
136
+ if (range <= 0.0) return (top + bot) * 0.5f;
137
+ const double t = (price - bounds.min) / range; // 0..1, 0 at min
138
+ return bot - static_cast<float>(t) * draw_h; // invert: high price → low y
139
+ }
140
+
141
+ } // namespace vroom
@@ -0,0 +1,100 @@
1
+ // Viewport math — pure functions, no Skia, no state.
2
+ // Translates between candle indices, time, price, and screen pixels.
3
+
4
+ #pragma once
5
+
6
+ #include <cstddef>
7
+ #include <cstdint>
8
+
9
+ #include "vroom/vroom_chart.h"
10
+
11
+ namespace vroom {
12
+
13
+ struct Layout {
14
+ float width_px; // total chart width
15
+ float height_px; // total chart height
16
+ float y_axis_width_px; // reserved on the right for price labels
17
+ float x_axis_height_px; // reserved at the bottom for time labels
18
+ float right_padding_px; // small gutter between candles and y-axis
19
+ float candle_width_ratio; // 0..1 of slot stride taken by the body
20
+ float top_padding_frac; // fraction of candle area; keeps prices off edges
21
+ float bottom_padding_frac;
22
+ float indicator_area_h; // height reserved for below-chart indicator panes
23
+ };
24
+
25
+ // Bottom of the price (candle) pane = top of the indicator band. Shrinks when
26
+ // an indicator pane is present so candles, volume, and price labels reflow.
27
+ inline float price_pane_bottom(const Layout& l) {
28
+ return l.height_px - l.x_axis_height_px - l.indicator_area_h;
29
+ }
30
+
31
+ // Top of the bottom time-axis strip. Stays anchored regardless of any indicator
32
+ // band (the strip is always the bottom x_axis_height_px of the chart).
33
+ inline float x_axis_top(const Layout& l) {
34
+ return l.height_px - l.x_axis_height_px;
35
+ }
36
+
37
+ struct PriceBounds {
38
+ double min;
39
+ double max;
40
+ };
41
+
42
+ // Half-open [start, end) range of candle indices.
43
+ struct IndexRange {
44
+ size_t start;
45
+ size_t end;
46
+ };
47
+
48
+ // Returns the indices of candles whose time_ms falls in [start_ms, end_ms].
49
+ // When both are 0, returns the full range (Phase 1 default-everything behavior).
50
+ // Candles must be sorted ascending by time_ms (invariant of the public API).
51
+ IndexRange visible_indices(const ::VroomCandle* candles,
52
+ size_t count,
53
+ int64_t start_ms,
54
+ int64_t end_ms);
55
+
56
+ // Width of one candle body in pixels, computed from the time slot a single
57
+ // candle occupies (candle_duration_ms / window_ms × candle area).
58
+ float candle_body_width(const Layout& layout,
59
+ int64_t window_ms,
60
+ int64_t candle_duration_ms);
61
+
62
+ // Center-x of a candle whose period starts at time_ms and lasts
63
+ // candle_duration_ms, given the current visible time window.
64
+ float candle_center_x(const Layout& layout,
65
+ int64_t time_ms,
66
+ int64_t candle_duration_ms,
67
+ int64_t visible_start_ms,
68
+ int64_t window_ms);
69
+
70
+ // Index of the candle whose center is nearest pixel x_px, searching
71
+ // [candles, candles+count). Clamps to the first/last candle. This is the
72
+ // integer counterpart of snap_x_to_candle — both share the same nearest-candle
73
+ // math. Precondition: count > 0 and window_ms > 0 (callers guard).
74
+ size_t snap_index_to_candle(const Layout& layout,
75
+ const ::VroomCandle* candles,
76
+ size_t count,
77
+ int64_t candle_duration_ms,
78
+ int64_t visible_start_ms,
79
+ int64_t window_ms,
80
+ float x_px);
81
+
82
+ // On-screen x of the candle whose center is nearest pixel x_px, searching
83
+ // [candles, candles+count). Clamps to the first/last candle. Returns x_px
84
+ // unchanged when there are no candles or the window is degenerate. Used to snap
85
+ // the crosshair to candles.
86
+ float snap_x_to_candle(const Layout& layout,
87
+ const ::VroomCandle* candles,
88
+ size_t count,
89
+ int64_t candle_duration_ms,
90
+ int64_t visible_start_ms,
91
+ int64_t window_ms,
92
+ float x_px);
93
+
94
+ // Min/max of (low..high) across the given range.
95
+ PriceBounds price_bounds(const ::VroomCandle* candles, size_t count);
96
+
97
+ // Map a price to y in pixels. y=0 is top of the chart.
98
+ float price_to_y(const Layout& layout, const PriceBounds& bounds, double price);
99
+
100
+ } // namespace vroom
@@ -0,0 +1,70 @@
1
+ #include "volume.h"
2
+
3
+ #pragma clang diagnostic push
4
+ #pragma clang diagnostic ignored "-Wdocumentation"
5
+ #include "include/core/SkCanvas.h"
6
+ #include "include/core/SkPaint.h"
7
+ #include "include/core/SkRect.h"
8
+ #pragma clang diagnostic pop
9
+
10
+ #include <algorithm>
11
+
12
+ #include "theme.h"
13
+ #include "viewport.h"
14
+
15
+ namespace vroom::volume {
16
+
17
+ namespace {
18
+ constexpr float kVolumeFrac = 0.20f; // tallest bar = this fraction of the area
19
+ } // namespace
20
+
21
+ void draw(SkCanvas* canvas,
22
+ const ::VroomCandle* visible,
23
+ std::size_t n,
24
+ const Layout& lay,
25
+ const Theme& theme,
26
+ int64_t window_ms,
27
+ int64_t visible_start_ms,
28
+ int64_t candle_duration_ms) {
29
+ if (!canvas || n == 0) return;
30
+
31
+ // Auto-fit bar heights to the loudest volume currently in view.
32
+ double max_vol = 0.0;
33
+ for (std::size_t i = 0; i < n; ++i) max_vol = std::max(max_vol, visible[i].volume);
34
+ if (max_vol <= 0.0) return;
35
+
36
+ const float candle_area_h = vroom::price_pane_bottom(lay);
37
+ const float region_h = candle_area_h * kVolumeFrac;
38
+
39
+ const float body_w = vroom::candle_body_width(
40
+ lay, window_ms, candle_duration_ms);
41
+ const float half_body = body_w * 0.5f;
42
+
43
+ const float opacity = theme.floats[VROOM_FLOAT_VOLUME_OPACITY];
44
+ SkPaint bull_paint;
45
+ bull_paint.setAntiAlias(true);
46
+ bull_paint.setColor(theme.colors[VROOM_COLOR_BULL]);
47
+ bull_paint.setAlphaf(opacity);
48
+
49
+ SkPaint bear_paint;
50
+ bear_paint.setAntiAlias(true);
51
+ bear_paint.setColor(theme.colors[VROOM_COLOR_BEAR]);
52
+ bear_paint.setAlphaf(opacity);
53
+
54
+ for (std::size_t i = 0; i < n; ++i) {
55
+ const auto& c = visible[i];
56
+ if (c.volume <= 0.0) continue;
57
+ const bool bull = c.close >= c.open;
58
+
59
+ const float cx = vroom::candle_center_x(
60
+ lay, c.time_ms, candle_duration_ms,
61
+ visible_start_ms, window_ms);
62
+ const float h = std::max(
63
+ 1.f, static_cast<float>(c.volume / max_vol) * region_h);
64
+ canvas->drawRect(
65
+ SkRect::MakeXYWH(cx - half_body, candle_area_h - h, body_w, h),
66
+ bull ? bull_paint : bear_paint);
67
+ }
68
+ }
69
+
70
+ } // namespace vroom::volume