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,30 @@
1
+ // Helpers for reaching into RN-Skia's installed JSI bindings to grab its
2
+ // RNSkPlatformContext — which we need so we can use JsiSkFontMgrFactory to
3
+ // get the system font manager cross-platform.
4
+ //
5
+ // JsiSkHostObject::getContext() is protected, so we use a "friend helper"
6
+ // subclass that exposes it via `using`. We never construct an instance —
7
+ // we just static_cast existing host objects to the accessor type.
8
+
9
+ #pragma once
10
+
11
+ #include <memory>
12
+
13
+ #include <jsi/jsi.h>
14
+
15
+ #include "JsiSkHostObjects.h" // RNSkia::JsiSkHostObject
16
+ #include "RNSkPlatformContext.h" // RNSkia::RNSkPlatformContext
17
+
18
+ namespace vroom {
19
+
20
+ class HostObjectAccessor : public RNSkia::JsiSkHostObject {
21
+ public:
22
+ using RNSkia::JsiSkHostObject::getContext;
23
+ };
24
+
25
+ // Returns the RN-Skia platform context by reading global.SkiaApi.
26
+ // Returns nullptr if SkiaApi isn't on the global yet (RN-Skia not installed).
27
+ std::shared_ptr<RNSkia::RNSkPlatformContext> getRNSkContext(
28
+ facebook::jsi::Runtime& runtime);
29
+
30
+ } // namespace vroom
@@ -0,0 +1,195 @@
1
+ // vroom_chart.h — public C facade for the vroom chart core.
2
+ //
3
+ // Threading: All calls must happen on a single thread (the render thread the
4
+ // host picked). The core does no internal locking.
5
+ //
6
+ // Rendering model: push. The core fires `on_redraw_requested` when something
7
+ // visual changed. The host marks dirty and calls `vroom_chart_draw` on the
8
+ // next vsync — never re-enter draw from the callback.
9
+ //
10
+ // Gestures: host-driven. The host wires its native gesture recognizers
11
+ // (UIPanGestureRecognizer / Android GestureDetector / RNGH) and calls
12
+ // `vroom_chart_pan`, `vroom_chart_zoom`, `vroom_chart_set_crosshair`,
13
+ // `vroom_chart_clear_crosshair`. The core has no gesture state machine.
14
+
15
+ #ifndef VROOM_CHART_H
16
+ #define VROOM_CHART_H
17
+
18
+ #include <stdbool.h>
19
+ #include <stddef.h>
20
+ #include <stdint.h>
21
+
22
+ #ifdef __cplusplus
23
+ extern "C" {
24
+ #endif
25
+
26
+ // Forward-declare Skia types as opaque so this header has no Skia dependency.
27
+ // The implementation casts back to SkCanvas* internally.
28
+ typedef struct SkCanvas SkCanvas;
29
+
30
+ typedef struct VroomChart VroomChart;
31
+
32
+ // ---- Data types -----------------------------------------------------------
33
+
34
+ typedef struct VroomCandle {
35
+ int64_t time_ms; // epoch milliseconds, ascending, non-overlapping
36
+ double open;
37
+ double high;
38
+ double low;
39
+ double close;
40
+ double volume;
41
+ } VroomCandle;
42
+
43
+ // A moving-average overlay line drawn on the price pane.
44
+ typedef struct VroomOverlay {
45
+ int32_t kind; // 0 = SMA, 1 = EMA
46
+ int32_t period; // lookback in candles (>= 1)
47
+ int32_t source; // 0=close,1=open,2=high,3=low,4=hl2,5=hlc3,6=ohlc4
48
+ uint32_t color; // 0xAARRGGBB
49
+ float width; // stroke width in px
50
+ } VroomOverlay;
51
+
52
+ // ---- Styling keys ---------------------------------------------------------
53
+
54
+ typedef enum {
55
+ VROOM_COLOR_BACKGROUND = 0,
56
+ VROOM_COLOR_BULL,
57
+ VROOM_COLOR_BEAR,
58
+ VROOM_COLOR_WICK,
59
+ VROOM_COLOR_GRID,
60
+ VROOM_COLOR_AXIS_TEXT,
61
+ VROOM_COLOR_CROSSHAIR,
62
+ VROOM_COLOR_TOOLTIP_BG,
63
+ VROOM_COLOR_TOOLTIP_TEXT,
64
+ VROOM_COLOR_CROSSHAIR_TARGET, // the hollow ring/dot at the intersection
65
+ VROOM_COLOR_COUNT_
66
+ } VroomColorKey;
67
+
68
+ typedef enum {
69
+ VROOM_FLOAT_CANDLE_WIDTH_RATIO = 0, // 0..1 of slot width
70
+ VROOM_FLOAT_WICK_WIDTH_PX,
71
+ VROOM_FLOAT_RIGHT_PADDING_PX, // small gutter between candles and y-axis
72
+ VROOM_FLOAT_AXIS_FONT_SIZE_PX,
73
+ VROOM_FLOAT_Y_AXIS_WIDTH_RATIO, // fallback y-axis width when no typeface
74
+ VROOM_FLOAT_X_AXIS_HEIGHT_PX, // bottom strip reserved for time labels
75
+ VROOM_FLOAT_VOLUME_OPACITY, // volume bar opacity (1=opaque)
76
+ VROOM_FLOAT_INDICATOR_HEIGHT_FRAC, // below-chart indicator pane, fraction of height
77
+ VROOM_FLOAT_COUNT_
78
+ } VroomFloatKey;
79
+
80
+ // ---- Callbacks ------------------------------------------------------------
81
+
82
+ typedef struct {
83
+ void (*on_redraw_requested)(void* ctx);
84
+ void (*on_viewport_changed)(void* ctx, int64_t start_ms, int64_t end_ms);
85
+ void (*on_crosshair_changed)(void* ctx, bool active, int64_t ms, double price);
86
+ } VroomCallbacks;
87
+
88
+ // ---- Lifecycle ------------------------------------------------------------
89
+
90
+ VroomChart* vroom_chart_create(const VroomCallbacks* cb, void* user_ctx);
91
+ void vroom_chart_destroy(VroomChart* chart);
92
+
93
+ // ---- Data (copied internally; caller may free immediately) ----------------
94
+
95
+ void vroom_chart_set_candles(VroomChart* chart, const VroomCandle* data, size_t count);
96
+ void vroom_chart_append_candle(VroomChart* chart, const VroomCandle* c);
97
+ void vroom_chart_update_last(VroomChart* chart, const VroomCandle* c);
98
+
99
+ // ---- Sizing ---------------------------------------------------------------
100
+
101
+ void vroom_chart_set_size(VroomChart* chart, float width_px, float height_px, float px_ratio);
102
+
103
+ // ---- Viewport -------------------------------------------------------------
104
+
105
+ void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_ms, int64_t end_ms);
106
+ void vroom_chart_pan(VroomChart* chart, float dx_px, float dy_px);
107
+ // Directional zoom. scale_x scales the time window around focus_x_px (>1 =
108
+ // narrower window, wider candles); scale_y scales the price range around
109
+ // focus_y_px (>1 = taller candles). Pass 1.0 for an axis to leave it untouched.
110
+ void vroom_chart_zoom(VroomChart* chart, float scale_x, float scale_y,
111
+ float focus_x_px, float focus_y_px);
112
+
113
+ // Two-finger translation. Shifts the time window like pan (dx) and shifts
114
+ // the price bounds vertically (dy) without changing their range — so the
115
+ // chart slides without rescaling. dy > 0 (drag down) moves content down
116
+ // (price labels move down to higher numbers).
117
+ void vroom_chart_translate(VroomChart* chart, float dx_px, float dy_px);
118
+
119
+ // Axis-drag controls — used by JS gesture handlers that detect a drag
120
+ // started on the y-axis or x-axis strip. Scaling pivots around the natural
121
+ // anchor (price center for y, right edge for x).
122
+ //
123
+ // Sign conventions:
124
+ // scale_price_axis: dy > 0 (drag down) widens the price range → candles shrink
125
+ // scale_time_axis: dx > 0 (drag right) widens the time range → candles thin
126
+ void vroom_chart_scale_price_axis(VroomChart* chart, float dy_px);
127
+ void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px);
128
+
129
+ // Reads the current y-axis width, x-axis height, and below-chart indicator pane
130
+ // height in pixels so callers can hit-test gestures against each region on the
131
+ // JS side. out_indicator_height_px is 0 when no indicator pane is shown.
132
+ void vroom_chart_get_axis_metrics(VroomChart* chart,
133
+ float* out_y_axis_width_px,
134
+ float* out_x_axis_height_px,
135
+ float* out_indicator_height_px);
136
+
137
+ // ---- Crosshair ------------------------------------------------------------
138
+
139
+ void vroom_chart_set_crosshair(VroomChart* chart, float x_px, float y_px);
140
+ void vroom_chart_clear_crosshair(VroomChart* chart);
141
+
142
+ // Fills *out with the OHLCV of the candle the crosshair currently snaps to and
143
+ // returns true. Returns false (leaving *out untouched) when the crosshair is
144
+ // inactive or there are no visible candles. Stateless — recomputes the snap
145
+ // from the current crosshair x and visible window, matching what's rendered.
146
+ bool vroom_chart_get_crosshair_candle(VroomChart* chart, VroomCandle* out);
147
+
148
+ // ---- Indicators -----------------------------------------------------------
149
+
150
+ // Configures the RSI indicator (rendered in a pane below the candles). `period`
151
+ // is the RSI lookback in candle counts (clamped >= 2). `upper_band`/`lower_band`
152
+ // are the overbought/oversold reference levels (0..100; default 70/30).
153
+ // `ma_enabled` toggles the RSI-based moving-average trendline and `ma_period`
154
+ // is its length (clamped >= 1). When enabled, the candle pane shrinks by
155
+ // VROOM_FLOAT_INDICATOR_HEIGHT_FRAC.
156
+ void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,
157
+ double upper_band, double lower_band,
158
+ bool ma_enabled, int ma_period);
159
+
160
+ // Configures the MACD indicator (its own pane below the candles). `fast`/`slow`
161
+ // are the EMA lengths (clamped >= 1, slow forced > fast; default 12/26) and
162
+ // `signal` is the signal-line EMA length (clamped >= 1; default 9). Panes stack
163
+ // in enable order, most recently enabled at the bottom.
164
+ void vroom_chart_set_macd(VroomChart* chart, bool enabled, int fast, int slow,
165
+ int signal);
166
+
167
+ // Replaces the full set of moving-average overlay lines (SMA/EMA) drawn on the
168
+ // price pane. Pass count 0 to clear them. Overlays don't reserve a pane.
169
+ void vroom_chart_set_overlays(VroomChart* chart, const VroomOverlay* overlays,
170
+ size_t count);
171
+
172
+ // Configures the session VWAP overlay (a single price-pane line). The session
173
+ // resets each UTC day shifted by `reset_offset_min` minutes (the configurable
174
+ // reset time). `color` is 0xAARRGGBB; `width` is the stroke px.
175
+ void vroom_chart_set_vwap(VroomChart* chart, bool enabled, int reset_offset_min,
176
+ uint32_t color, float width);
177
+
178
+ // ---- Rendering ------------------------------------------------------------
179
+
180
+ void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas);
181
+
182
+ // True while any axis-label fade is still animating. Hosts that drive their own
183
+ // redraw loop (e.g. the web WASM build) poll this to know when to keep ticking.
184
+ bool vroom_chart_is_animating(VroomChart* chart);
185
+
186
+ // ---- Styling --------------------------------------------------------------
187
+
188
+ void vroom_chart_set_color(VroomChart* chart, VroomColorKey key, uint32_t argb);
189
+ void vroom_chart_set_float(VroomChart* chart, VroomFloatKey key, float value);
190
+
191
+ #ifdef __cplusplus
192
+ } // extern "C"
193
+ #endif
194
+
195
+ #endif // VROOM_CHART_H
@@ -0,0 +1,74 @@
1
+ #include "candles.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::candles {
16
+
17
+ void draw(SkCanvas* canvas,
18
+ const ::VroomCandle* visible,
19
+ std::size_t n,
20
+ const Layout& lay,
21
+ const Theme& theme,
22
+ const PriceBounds& bounds,
23
+ int64_t window_ms,
24
+ int64_t visible_start_ms,
25
+ int64_t candle_duration_ms) {
26
+ if (!canvas || n == 0) return;
27
+
28
+ const float body_w = vroom::candle_body_width(
29
+ lay, window_ms, candle_duration_ms);
30
+
31
+ SkPaint bull_paint;
32
+ bull_paint.setAntiAlias(true);
33
+ bull_paint.setColor(theme.colors[VROOM_COLOR_BULL]);
34
+
35
+ SkPaint bear_paint;
36
+ bear_paint.setAntiAlias(true);
37
+ bear_paint.setColor(theme.colors[VROOM_COLOR_BEAR]);
38
+
39
+ SkPaint wick_bull;
40
+ wick_bull.setAntiAlias(true);
41
+ wick_bull.setColor(theme.colors[VROOM_COLOR_BULL]);
42
+ wick_bull.setStrokeWidth(theme.floats[VROOM_FLOAT_WICK_WIDTH_PX]);
43
+ wick_bull.setStyle(SkPaint::kStroke_Style);
44
+
45
+ SkPaint wick_bear = wick_bull;
46
+ wick_bear.setColor(theme.colors[VROOM_COLOR_BEAR]);
47
+
48
+ const float half_body = body_w * 0.5f;
49
+
50
+ for (std::size_t i = 0; i < n; ++i) {
51
+ const auto& c = visible[i];
52
+ const bool bull = c.close >= c.open;
53
+
54
+ const float cx = vroom::candle_center_x(
55
+ lay, c.time_ms, candle_duration_ms,
56
+ visible_start_ms, window_ms);
57
+ const float y_high = vroom::price_to_y(lay, bounds, c.high);
58
+ const float y_low = vroom::price_to_y(lay, bounds, c.low);
59
+ const float y_open = vroom::price_to_y(lay, bounds, c.open);
60
+ const float y_close = vroom::price_to_y(lay, bounds, c.close);
61
+
62
+ canvas->drawLine(cx, y_high, cx, y_low,
63
+ bull ? wick_bull : wick_bear);
64
+
65
+ const float y_top = std::min(y_open, y_close);
66
+ const float y_bot = std::max(y_open, y_close);
67
+ const float h = std::max(1.f, y_bot - y_top);
68
+ canvas->drawRect(
69
+ SkRect::MakeXYWH(cx - half_body, y_top, body_w, h),
70
+ bull ? bull_paint : bear_paint);
71
+ }
72
+ }
73
+
74
+ } // namespace vroom::candles
@@ -0,0 +1,34 @@
1
+ // Candle drawing — the wick+body loop, extracted so it can sit alongside
2
+ // future drawing modules (volume bars, indicator overlays, etc.) without
3
+ // growing the chart orchestrator.
4
+
5
+ #pragma once
6
+
7
+ #include <cstddef>
8
+ #include <cstdint>
9
+
10
+ #include "vroom/vroom_chart.h"
11
+
12
+ class SkCanvas;
13
+
14
+ namespace vroom {
15
+ struct Layout;
16
+ struct PriceBounds;
17
+ struct Theme;
18
+ } // namespace vroom
19
+
20
+ namespace vroom::candles {
21
+
22
+ // Draws every candle in [visible, visible + n). The visible slice should
23
+ // already be filtered by `visible_indices` in viewport.h.
24
+ void draw(SkCanvas* canvas,
25
+ const ::VroomCandle* visible,
26
+ std::size_t n,
27
+ const Layout& lay,
28
+ const Theme& theme,
29
+ const PriceBounds& bounds,
30
+ int64_t window_ms,
31
+ int64_t visible_start_ms,
32
+ int64_t candle_duration_ms);
33
+
34
+ } // namespace vroom::candles
@@ -0,0 +1,317 @@
1
+ // VroomChart engine — orchestration only.
2
+ //
3
+ // The struct definition lives in chart.h; per-subsystem rendering and the
4
+ // public C facade live in candles.cpp, labels.cpp, and chart_facade.cpp.
5
+ // This file keeps just the constructor, the small helpers used everywhere
6
+ // (mark_dirty, layout, is_animating_now), the picture cache lifecycle
7
+ // (rebuild_chart_picture), and the top-level draw_chart that composes all
8
+ // the layers in z-order.
9
+
10
+ #include "chart.h"
11
+
12
+ #pragma clang diagnostic push
13
+ #pragma clang diagnostic ignored "-Wdocumentation"
14
+ #include "include/core/SkCanvas.h"
15
+ #include "include/core/SkPaint.h"
16
+ #include "include/core/SkPicture.h"
17
+ #include "include/core/SkPictureRecorder.h"
18
+ #include "include/core/SkRect.h"
19
+ #pragma clang diagnostic pop
20
+
21
+ #include "candles.h"
22
+ #include "chart_internal.h"
23
+ #include "crosshair.h"
24
+ #include "labels.h"
25
+ #include "ma.h"
26
+ #include "ma_overlay.h"
27
+ #include "macd.h"
28
+ #include "macd_pane.h"
29
+ #include "price_indicator.h"
30
+ #include "rsi.h"
31
+ #include "rsi_pane.h"
32
+ #include "volume.h"
33
+ #include "vwap.h"
34
+ #include "theme.h"
35
+ #include "viewport.h"
36
+
37
+ VroomChart::VroomChart() : theme(vroom::default_theme()) {}
38
+
39
+ void VroomChart::mark_dirty() {
40
+ chart_dirty = true;
41
+ if (cb.on_redraw_requested) cb.on_redraw_requested(user_ctx);
42
+ }
43
+
44
+ vroom::Layout VroomChart::layout() const {
45
+ const float axis_w = axis_width_px > 0.f
46
+ ? axis_width_px
47
+ : width_px * theme.floats[VROOM_FLOAT_Y_AXIS_WIDTH_RATIO];
48
+ const int pane_count = (rsi_enabled ? 1 : 0) + (macd_enabled ? 1 : 0);
49
+ const float indicator_h = static_cast<float>(pane_count) * height_px *
50
+ theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
51
+ return vroom::Layout{
52
+ width_px,
53
+ height_px,
54
+ axis_w,
55
+ theme.floats[VROOM_FLOAT_X_AXIS_HEIGHT_PX],
56
+ theme.floats[VROOM_FLOAT_RIGHT_PADDING_PX],
57
+ theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO],
58
+ 0.05f,
59
+ 0.05f,
60
+ indicator_h,
61
+ };
62
+ }
63
+
64
+ void VroomChart::ensure_rsi() {
65
+ if (!rsi_enabled || !rsi_dirty) return;
66
+ vroom::rsi::compute(candles.data(), candles.size(), rsi_period, rsi_cache);
67
+ if (rsi_ma_enabled) {
68
+ vroom::rsi::compute_ma(rsi_cache, rsi_ma_period, rsi_ma_cache);
69
+ } else {
70
+ rsi_ma_cache.clear();
71
+ }
72
+ rsi_dirty = false;
73
+ }
74
+
75
+ void VroomChart::ensure_macd() {
76
+ if (!macd_enabled || !macd_dirty) return;
77
+ vroom::macd::compute(candles.data(), candles.size(), macd_fast, macd_slow,
78
+ macd_signal, macd_cache, macd_signal_cache,
79
+ macd_hist_cache);
80
+ macd_dirty = false;
81
+ }
82
+
83
+ void VroomChart::ensure_overlays() {
84
+ if (!overlays_dirty) return;
85
+ overlay_caches.resize(overlays.size());
86
+ for (std::size_t i = 0; i < overlays.size(); ++i) {
87
+ const auto& ov = overlays[i];
88
+ vroom::ma::compute(candles.data(), candles.size(), ov.kind, ov.period,
89
+ ov.source, overlay_caches[i]);
90
+ }
91
+ overlays_dirty = false;
92
+ }
93
+
94
+ void VroomChart::ensure_vwap() {
95
+ if (!vwap_enabled || !vwap_dirty) return;
96
+ vroom::vwap::compute(candles.data(), candles.size(), vwap_reset_offset_min,
97
+ vwap_cache, vwap_breaks);
98
+ vwap_dirty = false;
99
+ }
100
+
101
+ void VroomChart::draw_chart(SkCanvas* canvas) {
102
+ const auto lay = layout();
103
+
104
+ // 1. Background
105
+ SkPaint bg;
106
+ bg.setColor(theme.colors[VROOM_COLOR_BACKGROUND]);
107
+ canvas->drawRect(SkRect::MakeWH(width_px, height_px), bg);
108
+
109
+ if (candles.empty()) return;
110
+
111
+ // 2. Visible slice + bounds + window_ms + geometry
112
+ const auto range = vroom::visible_indices(
113
+ candles.data(), candles.size(),
114
+ visible_start_ms, visible_end_ms);
115
+ const size_t n = range.end - range.start;
116
+ if (n == 0) return;
117
+ const ::VroomCandle* visible = candles.data() + range.start;
118
+
119
+ const auto bounds = price_bounds_initialized
120
+ ? price_bounds
121
+ : vroom::price_bounds(visible, n);
122
+ const int64_t window_ms = visible_end_ms - visible_start_ms;
123
+
124
+ const float candle_area_h = vroom::price_pane_bottom(lay);
125
+ const float candle_right =
126
+ width_px - lay.y_axis_width_px - lay.right_padding_px;
127
+
128
+ // 3. Update label fade state ONCE per frame — both gridlines and labels
129
+ // share these opacities so their animations stay in lockstep.
130
+ vroom::labels::update_y_fades(*this, lay, bounds);
131
+ vroom::labels::update_x_fades(*this, lay);
132
+
133
+ // 4. Gridlines — drawn before candles so candle bodies overlay them.
134
+ // Vertical (time) gridlines are intentionally disabled for now —
135
+ // re-enable by adding `vroom::labels::draw_x_gridlines(canvas, *this,
136
+ // candle_right, candle_area_h);` here.
137
+ vroom::labels::draw_y_gridlines(canvas, *this, lay, bounds,
138
+ candle_right, candle_area_h);
139
+
140
+ // 4.5. Volume bars — drawn under the candles so candles z-index above.
141
+ vroom::volume::draw(canvas, visible, n, lay, theme,
142
+ window_ms, visible_start_ms, candle_duration_ms);
143
+
144
+ // 5. Candles (wicks + bodies)
145
+ vroom::candles::draw(canvas, visible, n, lay, theme, bounds,
146
+ window_ms, visible_start_ms, candle_duration_ms);
147
+
148
+ // 5.5. Moving-average overlay lines (SMA/EMA) on the price pane, over the
149
+ // candles. They share the candle price scale and don't reserve a pane.
150
+ if (!overlays.empty()) {
151
+ ensure_overlays();
152
+ for (std::size_t k = 0; k < overlays.size(); ++k) {
153
+ if (overlay_caches[k].size() != candles.size()) continue;
154
+ const double* vis = overlay_caches[k].data() + range.start;
155
+ vroom::ma_overlay::draw(canvas, lay, bounds, visible, n, vis,
156
+ window_ms, visible_start_ms,
157
+ candle_duration_ms, candle_right,
158
+ candle_area_h, overlays[k].color,
159
+ overlays[k].width);
160
+ }
161
+ }
162
+
163
+ // 5.6. VWAP overlay (session, configurable reset) — a single price-pane line
164
+ // that breaks at each session reset (vwap_breaks).
165
+ if (vwap_enabled) {
166
+ ensure_vwap();
167
+ if (vwap_cache.size() == candles.size()) {
168
+ const double* vis = vwap_cache.data() + range.start;
169
+ const unsigned char* brk = vwap_breaks.size() == candles.size()
170
+ ? vwap_breaks.data() + range.start
171
+ : nullptr;
172
+ vroom::ma_overlay::draw(canvas, lay, bounds, visible, n, vis,
173
+ window_ms, visible_start_ms,
174
+ candle_duration_ms, candle_right,
175
+ candle_area_h, vwap_color, vwap_width, brk);
176
+ }
177
+ }
178
+
179
+ // 6. Axis backgrounds (mask any candle overflow). The x-axis separator
180
+ // line is intentionally omitted for now. The bottom strip anchors at
181
+ // x_axis_top (below any indicator pane) so it never paints over it.
182
+ SkPaint axis_bg;
183
+ axis_bg.setColor(theme.colors[VROOM_COLOR_BACKGROUND]);
184
+ canvas->drawRect(
185
+ SkRect::MakeXYWH(0, vroom::x_axis_top(lay), candle_right,
186
+ lay.x_axis_height_px),
187
+ axis_bg);
188
+ const float axis_block_w = width_px - candle_right;
189
+ canvas->drawRect(
190
+ SkRect::MakeXYWH(candle_right, 0, axis_block_w, height_px),
191
+ axis_bg);
192
+
193
+ // 7. Labels (read from y_fades / x_fades, no state mutation here)
194
+ vroom::labels::draw_y_labels(canvas, *this, lay, bounds);
195
+ vroom::labels::draw_x_labels(canvas, *this, lay);
196
+
197
+ // 7.5. Current-price line + box — above labels so the box covers any label
198
+ // it overlaps; tracks the latest close as the price scale moves.
199
+ vroom::price_indicator::draw(canvas, *this, lay, bounds,
200
+ candle_right, candle_area_h);
201
+
202
+ // 7.6. Indicator panes stacked below the candles, ordered by enable
203
+ // sequence (most recently enabled at the bottom). Each pane is
204
+ // INDICATOR_HEIGHT_FRAC of the height; the candle pane already shrank
205
+ // to fit them (see layout()).
206
+ if (lay.indicator_area_h > 0.f) {
207
+ struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
208
+ ActivePane panes[2];
209
+ int count = 0;
210
+ if (rsi_enabled) panes[count++] = {rsi_order, 0};
211
+ if (macd_enabled) panes[count++] = {macd_order, 1};
212
+ if (count == 2 && panes[0].order > panes[1].order) {
213
+ const ActivePane tmp = panes[0];
214
+ panes[0] = panes[1];
215
+ panes[1] = tmp;
216
+ }
217
+
218
+ const float pane_h =
219
+ height_px * theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
220
+ float pane_top = candle_area_h; // == price_pane_bottom(lay)
221
+ for (int i = 0; i < count; ++i) {
222
+ const float pane_bottom = pane_top + pane_h;
223
+ if (panes[i].type == 0) {
224
+ ensure_rsi();
225
+ const double* rsi_vis = rsi_cache.size() == candles.size()
226
+ ? rsi_cache.data() + range.start : nullptr;
227
+ const double* rsi_ma_vis =
228
+ (rsi_ma_enabled && rsi_ma_cache.size() == candles.size())
229
+ ? rsi_ma_cache.data() + range.start : nullptr;
230
+ vroom::rsi_pane::draw(canvas, *this, lay, visible, n, rsi_vis,
231
+ rsi_ma_vis, window_ms, visible_start_ms,
232
+ candle_duration_ms, candle_right, pane_top,
233
+ pane_bottom);
234
+ } else {
235
+ ensure_macd();
236
+ const double* macd_vis = macd_cache.size() == candles.size()
237
+ ? macd_cache.data() + range.start : nullptr;
238
+ const double* sig_vis =
239
+ macd_signal_cache.size() == candles.size()
240
+ ? macd_signal_cache.data() + range.start : nullptr;
241
+ const double* hist_vis =
242
+ macd_hist_cache.size() == candles.size()
243
+ ? macd_hist_cache.data() + range.start : nullptr;
244
+ vroom::macd_pane::draw(canvas, *this, lay, visible, n, macd_vis,
245
+ sig_vis, hist_vis, window_ms,
246
+ visible_start_ms, candle_duration_ms,
247
+ candle_right, pane_top, pane_bottom);
248
+ }
249
+ pane_top = pane_bottom;
250
+ }
251
+ }
252
+
253
+ // 7.7. Crosshair — drawn last so it sits on top of everything, including the
254
+ // indicator panes. The vertical line runs down to x_axis_top so it
255
+ // stays visible across the candle area and all below-chart panes; the
256
+ // horizontal line + ring stay in the price pane (clamped to
257
+ // candle_area_h).
258
+ if (crosshair_active) {
259
+ const float snap_x = vroom::snap_x_to_candle(
260
+ lay, visible, n, candle_duration_ms, visible_start_ms, window_ms,
261
+ crosshair_x_px);
262
+ vroom::crosshair::draw(canvas, *this, candle_right, candle_area_h,
263
+ vroom::x_axis_top(lay), snap_x);
264
+ }
265
+
266
+ // 8. GC fades that have fully faded out and aren't coming back.
267
+ vroom::labels::gc_y_fades(*this);
268
+ vroom::labels::gc_x_fades(*this);
269
+ }
270
+
271
+ void VroomChart::rebuild_chart_picture() {
272
+ // Compute dt for fade animations. We cap large gaps (resume from
273
+ // background, long idle) so we don't snap animations to completion.
274
+ auto now = std::chrono::steady_clock::now();
275
+ float dt = 0.f;
276
+ if (anim_started) {
277
+ dt = std::chrono::duration<float>(now - last_anim_tick).count();
278
+ if (dt > 0.1f) dt = 0.f;
279
+ }
280
+ last_anim_tick = now;
281
+ anim_started = true;
282
+ last_dt_seconds = dt;
283
+
284
+ SkPictureRecorder recorder;
285
+ SkCanvas* canvas = recorder.beginRecording(SkRect::MakeWH(width_px, height_px));
286
+ draw_chart(canvas);
287
+ chart_picture = recorder.finishRecordingAsPicture();
288
+ chart_dirty = false;
289
+ }
290
+
291
+ bool VroomChart::is_animating_now() const {
292
+ for (const auto& f : y_fades) {
293
+ if (f.opacity != f.target) return true;
294
+ }
295
+ for (const auto& f : x_fades) {
296
+ if (f.opacity != f.target) return true;
297
+ }
298
+ return false;
299
+ }
300
+
301
+ // chart_internal.h exports — bridge layer (JSI HostObject) consumers.
302
+
303
+ namespace vroom {
304
+
305
+ sk_sp<SkPicture> render_chart_picture(VroomChart* chart) {
306
+ if (!chart) return nullptr;
307
+ if (chart->chart_dirty || !chart->chart_picture || chart->is_animating_now()) {
308
+ chart->rebuild_chart_picture();
309
+ }
310
+ return chart->chart_picture;
311
+ }
312
+
313
+ bool is_animating(VroomChart* chart) {
314
+ return chart ? chart->is_animating_now() : false;
315
+ }
316
+
317
+ } // namespace vroom