react-native-vroom-chart 0.1.1 → 0.1.3

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.
@@ -49,6 +49,27 @@ typedef struct VroomOverlay {
49
49
  float width; // stroke width in px
50
50
  } VroomOverlay;
51
51
 
52
+ // A drawing anchor in data space (so a drawing tracks the candles on pan/zoom).
53
+ typedef struct VroomDrawPoint {
54
+ int64_t time_ms; // epoch milliseconds (not snapped to a candle slot)
55
+ double price;
56
+ } VroomDrawPoint;
57
+
58
+ // A committed line drawing: a two-point trendline on the price pane.
59
+ typedef struct VroomDrawing {
60
+ VroomDrawPoint a;
61
+ VroomDrawPoint b;
62
+ uint32_t color; // 0xAARRGGBB
63
+ float width; // stroke width in px
64
+ } VroomDrawing;
65
+
66
+ // A continuous data coordinate at a pixel position (no candle snapping). Used to
67
+ // translate a drawing-tool click into a data-space anchor.
68
+ typedef struct VroomCoord {
69
+ int64_t time_ms;
70
+ double price;
71
+ } VroomCoord;
72
+
52
73
  // ---- Styling keys ---------------------------------------------------------
53
74
 
54
75
  typedef enum {
@@ -62,6 +83,12 @@ typedef enum {
62
83
  VROOM_COLOR_TOOLTIP_BG,
63
84
  VROOM_COLOR_TOOLTIP_TEXT,
64
85
  VROOM_COLOR_CROSSHAIR_TARGET, // the hollow ring/dot at the intersection
86
+ VROOM_COLOR_BORDER_BULL, // bull body 1px outline; 0 alpha => inherit BULL fill
87
+ VROOM_COLOR_BORDER_BEAR, // bear body 1px outline; 0 alpha => inherit BEAR fill
88
+ VROOM_COLOR_WICK_BULL, // bull wick; 0 alpha => inherit BULL fill
89
+ VROOM_COLOR_WICK_BEAR, // bear wick; 0 alpha => inherit BEAR fill
90
+ VROOM_COLOR_ACCENT_BULL, // generic up color: price indicator, volume, MACD
91
+ VROOM_COLOR_ACCENT_BEAR, // generic down color
65
92
  VROOM_COLOR_COUNT_
66
93
  } VroomColorKey;
67
94
 
@@ -103,6 +130,24 @@ void vroom_chart_set_size(VroomChart* chart, float width_px, float height_px, fl
103
130
  // ---- Viewport -------------------------------------------------------------
104
131
 
105
132
  void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_ms, int64_t end_ms);
133
+
134
+ // Reads the current visible time window. Either out pointer may be null.
135
+ // Both are 0 when the window is still uninitialized.
136
+ void vroom_chart_get_visible_range(VroomChart* chart,
137
+ int64_t* out_start_ms, int64_t* out_end_ms);
138
+
139
+ // Reset to the fresh-mount view: frame the most recent ~80 candles and
140
+ // re-enable continuous y auto-fit (the price range follows the visible candles
141
+ // until the next manual y gesture). Use when the data series is wholesale
142
+ // replaced — e.g. switching assets. With no candles loaded, clears the window
143
+ // to 0/0 so the next set_candles applies the default framing.
144
+ void vroom_chart_reset_view(VroomChart* chart);
145
+
146
+ // Re-enable continuous y auto-fit only; the time window is untouched. Use
147
+ // after repositioning the window for a same-asset data swap (e.g. a timeframe
148
+ // switch) so the price scale re-fits the newly visible candles.
149
+ void vroom_chart_reset_price_scale(VroomChart* chart);
150
+
106
151
  void vroom_chart_pan(VroomChart* chart, float dx_px, float dy_px);
107
152
  // Directional zoom. scale_x scales the time window around focus_x_px (>1 =
108
153
  // narrower window, wider candles); scale_y scales the price range around
@@ -126,6 +171,19 @@ void vroom_chart_translate(VroomChart* chart, float dx_px, float dy_px);
126
171
  void vroom_chart_scale_price_axis(VroomChart* chart, float dy_px);
127
172
  void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px);
128
173
 
174
+ // Scale the y-axis of the below-chart indicator pane that contains y_px.
175
+ // dy_px > 0 (drag down) zooms out (widens the visible value range); dy_px < 0
176
+ // zooms in. RSI scales about 50, MACD about its zero line. No-op when y_px is
177
+ // not over an indicator pane.
178
+ void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px, float dy_px);
179
+
180
+ // Drag the separator between the price pane and the below-chart indicator band.
181
+ // dy_px > 0 (drag down) grows the price pane and shrinks the indicator band.
182
+ // Candle pixel scale is preserved: the price range is widened/narrowed in step
183
+ // (anchored at the top price), so the viewport reveals/hides price rather than
184
+ // rescaling candles. No-op when no indicator pane is shown.
185
+ void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px);
186
+
129
187
  // Reads the current y-axis width, x-axis height, and below-chart indicator pane
130
188
  // height in pixels so callers can hit-test gestures against each region on the
131
189
  // JS side. out_indicator_height_px is 0 when no indicator pane is shown.
@@ -193,6 +251,33 @@ void vroom_chart_set_overlays(VroomChart* chart, const VroomOverlay* overlays,
193
251
  void vroom_chart_set_vwap(VroomChart* chart, bool enabled, int reset_offset_min,
194
252
  uint32_t color, float width);
195
253
 
254
+ // ---- Drawings (line annotations) ------------------------------------------
255
+
256
+ // Replaces the full set of committed line drawings (data-anchored, so they track
257
+ // the candles on pan/zoom). Pass count 0 to clear. Drawings render on the price
258
+ // pane above the candles/overlays and below the axis labels & crosshair.
259
+ void vroom_chart_set_drawings(VroomChart* chart, const VroomDrawing* drawings,
260
+ size_t count);
261
+
262
+ // Sets the transient in-progress "draft" the drawing tool shows while the user
263
+ // places points. Node A is always shown; when `has_b`, node B is shown too.
264
+ // `guide != 0` also draws the guideline A->B (the live line preview); `guide == 0`
265
+ // draws node dots only (the committed segment already renders via set_drawings).
266
+ // `color`/`width` style the guideline to match the eventual line.
267
+ void vroom_chart_set_draft(VroomChart* chart, int64_t a_time, double a_price,
268
+ bool has_b, int64_t b_time, double b_price,
269
+ bool guide, uint32_t color, float width);
270
+
271
+ // Clears the draft (hides the in-progress node dots / guideline).
272
+ void vroom_chart_clear_draft(VroomChart* chart);
273
+
274
+ // Fills *out with the continuous data coordinate (time_ms, price) at pixel
275
+ // (x_px, y_px) using the free (non-snapped) mapping, and returns true. Returns
276
+ // false (leaving *out untouched) when there are no candles or the viewport is
277
+ // degenerate.
278
+ bool vroom_chart_coord_at(VroomChart* chart, float x_px, float y_px,
279
+ VroomCoord* out);
280
+
196
281
  // ---- Rendering ------------------------------------------------------------
197
282
 
198
283
  void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas);
@@ -14,6 +14,14 @@
14
14
 
15
15
  namespace vroom::candles {
16
16
 
17
+ namespace {
18
+ // New border/wick colors use a transparent sentinel (alpha == 0) to mean
19
+ // "inherit the body fill color". Resolve to `fill` in that case.
20
+ inline uint32_t resolve_color(uint32_t color, uint32_t fill) {
21
+ return (color >> 24) == 0 ? fill : color;
22
+ }
23
+ } // namespace
24
+
17
25
  void draw(SkCanvas* canvas,
18
26
  const ::VroomCandle* visible,
19
27
  std::size_t n,
@@ -28,22 +36,40 @@ void draw(SkCanvas* canvas,
28
36
  const float body_w = vroom::candle_body_width(
29
37
  lay, window_ms, candle_duration_ms);
30
38
 
39
+ const uint32_t fill_bull = theme.colors[VROOM_COLOR_BULL];
40
+ const uint32_t fill_bear = theme.colors[VROOM_COLOR_BEAR];
41
+
31
42
  SkPaint bull_paint;
32
43
  bull_paint.setAntiAlias(true);
33
- bull_paint.setColor(theme.colors[VROOM_COLOR_BULL]);
44
+ bull_paint.setColor(fill_bull);
34
45
 
35
46
  SkPaint bear_paint;
36
47
  bear_paint.setAntiAlias(true);
37
- bear_paint.setColor(theme.colors[VROOM_COLOR_BEAR]);
48
+ bear_paint.setColor(fill_bear);
38
49
 
39
50
  SkPaint wick_bull;
40
51
  wick_bull.setAntiAlias(true);
41
- wick_bull.setColor(theme.colors[VROOM_COLOR_BULL]);
52
+ wick_bull.setColor(
53
+ resolve_color(theme.colors[VROOM_COLOR_WICK_BULL], fill_bull));
42
54
  wick_bull.setStrokeWidth(theme.floats[VROOM_FLOAT_WICK_WIDTH_PX]);
43
55
  wick_bull.setStyle(SkPaint::kStroke_Style);
44
56
 
45
57
  SkPaint wick_bear = wick_bull;
46
- wick_bear.setColor(theme.colors[VROOM_COLOR_BEAR]);
58
+ wick_bear.setColor(
59
+ resolve_color(theme.colors[VROOM_COLOR_WICK_BEAR], fill_bear));
60
+
61
+ // 1px body outlines. Default sentinel resolves to the fill color, so the
62
+ // border is invisible until a consumer sets borderBull/borderBear.
63
+ SkPaint border_bull;
64
+ border_bull.setAntiAlias(true);
65
+ border_bull.setStyle(SkPaint::kStroke_Style);
66
+ border_bull.setStrokeWidth(1.f);
67
+ border_bull.setColor(
68
+ resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull));
69
+
70
+ SkPaint border_bear = border_bull;
71
+ border_bear.setColor(
72
+ resolve_color(theme.colors[VROOM_COLOR_BORDER_BEAR], fill_bear));
47
73
 
48
74
  const float half_body = body_w * 0.5f;
49
75
 
@@ -65,9 +91,9 @@ void draw(SkCanvas* canvas,
65
91
  const float y_top = std::min(y_open, y_close);
66
92
  const float y_bot = std::max(y_open, y_close);
67
93
  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);
94
+ const SkRect body = SkRect::MakeXYWH(cx - half_body, y_top, body_w, h);
95
+ canvas->drawRect(body, bull ? bull_paint : bear_paint);
96
+ canvas->drawRect(body, bull ? border_bull : border_bear);
71
97
  }
72
98
  }
73
99
 
@@ -21,6 +21,7 @@
21
21
  #include "candles.h"
22
22
  #include "chart_internal.h"
23
23
  #include "crosshair.h"
24
+ #include "drawings.h"
24
25
  #include "labels.h"
25
26
  #include "ma.h"
26
27
  #include "ma_overlay.h"
@@ -116,9 +117,9 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
116
117
  if (n == 0) return;
117
118
  const ::VroomCandle* visible = candles.data() + range.start;
118
119
 
119
- const auto bounds = price_bounds_initialized
120
+ const auto bounds = price_bounds_manual
120
121
  ? price_bounds
121
- : vroom::price_bounds(visible, n);
122
+ : vroom::auto_price_bounds(visible, n);
122
123
  const int64_t window_ms = visible_end_ms - visible_start_ms;
123
124
 
124
125
  const float candle_area_h = vroom::price_pane_bottom(lay);
@@ -176,6 +177,11 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
176
177
  }
177
178
  }
178
179
 
180
+ // 5.7. Drawing annotations (committed line tools + the in-progress draft).
181
+ // On the price pane above the candles/overlays, below the axis labels.
182
+ vroom::drawings::draw(canvas, *this, lay, bounds, candle_right,
183
+ candle_area_h);
184
+
179
185
  // 6. Axis backgrounds (mask any candle overflow). The x-axis separator
180
186
  // line is intentionally omitted for now. The bottom strip anchors at
181
187
  // x_axis_top (below any indicator pane) so it never paints over it.
@@ -46,7 +46,7 @@ struct VroomChart {
46
46
  float height_px = 0.f;
47
47
  float px_ratio = 1.f;
48
48
 
49
- // 0/0 = uninitialized; set_candles defaults to last ~60 candles.
49
+ // 0/0 = uninitialized; set_candles defaults to last ~80 candles.
50
50
  int64_t visible_start_ms = 0;
51
51
  int64_t visible_end_ms = 0;
52
52
 
@@ -55,10 +55,13 @@ struct VroomChart {
55
55
  float axis_width_px = 0.f;
56
56
 
57
57
  // --- price bounds (y-axis state) ---------------------------------------
58
- // Persistent across pans (panning preserves the price scale). Mutated
59
- // only by pinch zoom, axis drags, vertical drag-translate.
58
+ // Two modes. Auto (price_bounds_manual == false, the default): the y-range
59
+ // continuously follows the visible candles each frame and `price_bounds`
60
+ // is ignored. Manual: the user has touched the y-axis (pinch zoom, axis
61
+ // drag, vertical drag-translate) and `price_bounds` is frozen until
62
+ // vroom_chart_reset_view / reset_price_scale re-enables auto mode.
60
63
  vroom::PriceBounds price_bounds{0.0, 1.0};
61
- bool price_bounds_initialized = false;
64
+ bool price_bounds_manual = false;
62
65
 
63
66
  // --- crosshair (not yet drawn) -----------------------------------------
64
67
  bool crosshair_active = false;
@@ -78,6 +81,9 @@ struct VroomChart {
78
81
  std::vector<double> rsi_cache; // RSI per candle (NaN where undefined)
79
82
  std::vector<double> rsi_ma_cache; // SMA of rsi_cache (empty if MA off)
80
83
  bool rsi_dirty = true;
84
+ // User y-axis zoom for the RSI pane (drag on its y-axis strip). 1.0 = the
85
+ // default 0..100 fit; >1 zooms in (taller), <1 zooms out. Anchored at 50.
86
+ double rsi_y_scale = 1.0;
81
87
 
82
88
  // MACD, drawn in its own pane. Caches aligned to `candles` (NaN warmup).
83
89
  bool macd_enabled = false;
@@ -88,6 +94,9 @@ struct VroomChart {
88
94
  std::vector<double> macd_signal_cache;
89
95
  std::vector<double> macd_hist_cache;
90
96
  bool macd_dirty = true;
97
+ // User y-axis zoom for the MACD pane (drag on its y-axis strip). 1.0 = the
98
+ // default auto-fit amplitude; >1 zooms in, <1 zooms out. Anchored at zero.
99
+ double macd_y_scale = 1.0;
91
100
 
92
101
  // Stacking order for the indicator panes: each indicator gets the next
93
102
  // sequence number on its off->on transition, so the most recently enabled
@@ -113,6 +122,24 @@ struct VroomChart {
113
122
  std::vector<unsigned char> vwap_breaks;
114
123
  bool vwap_dirty = true;
115
124
 
125
+ // --- drawings (line annotations) ---------------------------------------
126
+ // Committed two-point lines, anchored in data space so they track the
127
+ // candles on pan/zoom. Drawn on the price pane above candles/overlays.
128
+ std::vector<VroomDrawing> drawings;
129
+
130
+ // Transient in-progress "draft" the drawing tool shows while placing points.
131
+ // draft_a is always drawn (node dot); draft_b is drawn when draft_has_b.
132
+ // draft_guide draws the live guideline A->B; when false only node dots show
133
+ // (the committed segment renders via `drawings`). draft_color/draft_width
134
+ // style the guideline to match the eventual line.
135
+ bool draft_active = false;
136
+ VroomDrawPoint draft_a{};
137
+ bool draft_has_b = false;
138
+ VroomDrawPoint draft_b{};
139
+ bool draft_guide = false;
140
+ uint32_t draft_color = 0xff2962ff;
141
+ float draft_width = 2.f;
142
+
116
143
  // --- theme --------------------------------------------------------------
117
144
  vroom::Theme theme;
118
145
 
@@ -54,6 +54,33 @@ int64_t damp_future_delta(int64_t delta_ms, int64_t cur_future,
54
54
  return static_cast<int64_t>(static_cast<double>(delta_ms) * resist);
55
55
  }
56
56
 
57
+ // Frame the default view: the most recent ~80 candles — a narrower x-window
58
+ // so candles read wider. No-op when there are no candles.
59
+ void apply_default_framing(VroomChart* chart) {
60
+ if (chart->candles.empty()) return;
61
+ constexpr size_t kDefaultVisible = 80;
62
+ const size_t start_idx = chart->candles.size() > kDefaultVisible
63
+ ? chart->candles.size() - kDefaultVisible
64
+ : 0;
65
+ chart->visible_start_ms = chart->candles[start_idx].time_ms;
66
+ chart->visible_end_ms = chart->candles.back().time_ms;
67
+ }
68
+
69
+ // On the first manual-y gesture, adopt the on-screen auto-fit bounds so the
70
+ // gesture continues from exactly what the user sees. No-op once manual.
71
+ void ensure_manual_price_bounds(VroomChart* chart) {
72
+ if (chart->price_bounds_manual) return;
73
+ const auto idx = vroom::visible_indices(
74
+ chart->candles.data(), chart->candles.size(),
75
+ chart->visible_start_ms, chart->visible_end_ms);
76
+ if (idx.end > idx.start) {
77
+ chart->price_bounds = vroom::auto_price_bounds(
78
+ chart->candles.data() + idx.start, idx.end - idx.start);
79
+ } // else: no visible candles — keep whatever bounds we had
80
+ chart->price_bounds_manual = true;
81
+ vroom::labels::recompute_axis_width(*chart);
82
+ }
83
+
57
84
  } // namespace
58
85
 
59
86
  // ---- Lifecycle -------------------------------------------------------------
@@ -85,40 +112,12 @@ extern "C" void vroom_chart_set_candles(VroomChart* chart, const VroomCandle* da
85
112
  }
86
113
  vroom::labels::recompute_axis_width(*chart);
87
114
 
88
- // Default the visible window to the most recent ~80 candles when the
89
- // consumer hasn't set one a narrower x-window so candles read wider.
90
- if (chart->visible_start_ms == 0 && chart->visible_end_ms == 0 &&
91
- !chart->candles.empty()) {
92
- constexpr size_t kDefaultVisible = 80;
93
- const size_t start_idx = chart->candles.size() > kDefaultVisible
94
- ? chart->candles.size() - kDefaultVisible
95
- : 0;
96
- chart->visible_start_ms = chart->candles[start_idx].time_ms;
97
- chart->visible_end_ms = chart->candles.back().time_ms;
98
- }
99
-
100
- // Snap the price (y-axis) bounds to whatever's visible — but only on the
101
- // first load. After they're initialized, leave them alone so live data
102
- // updates (new / updated candles) preserve the user's pan/scale; from then
103
- // on only zoom / axis-drags / vertical translate change them.
104
- if (!chart->candles.empty() && !chart->price_bounds_initialized) {
105
- const auto idx = vroom::visible_indices(
106
- chart->candles.data(), chart->candles.size(),
107
- chart->visible_start_ms, chart->visible_end_ms);
108
- if (idx.end > idx.start) {
109
- auto b = vroom::price_bounds(
110
- chart->candles.data() + idx.start, idx.end - idx.start);
111
- // Widen the default y-window beyond the data's min/max so candles
112
- // fill less vertical space (shorter candles, more headroom). 1.0 =
113
- // snug; larger = wider. Only the default — zoom/axis-drag override.
114
- constexpr double kDefaultYZoom = 1.5;
115
- const double mid = (b.min + b.max) * 0.5;
116
- const double half = (b.max - b.min) * 0.5 * kDefaultYZoom;
117
- b.min = mid - half;
118
- b.max = mid + half;
119
- chart->price_bounds = b;
120
- chart->price_bounds_initialized = true;
121
- }
115
+ // Default the visible window when the consumer hasn't set one. The y-axis
116
+ // needs no equivalent: while the price scale is in auto mode the draw path
117
+ // fits it to the visible candles every frame, and once the user has taken
118
+ // it manual, live data updates must not disturb their pan/scale.
119
+ if (chart->visible_start_ms == 0 && chart->visible_end_ms == 0) {
120
+ apply_default_framing(chart);
122
121
  }
123
122
 
124
123
  chart->mark_dirty();
@@ -168,6 +167,35 @@ extern "C" void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_m
168
167
  chart->mark_dirty();
169
168
  }
170
169
 
170
+ extern "C" void vroom_chart_get_visible_range(VroomChart* chart,
171
+ int64_t* out_start_ms,
172
+ int64_t* out_end_ms) {
173
+ if (!chart) return;
174
+ if (out_start_ms) *out_start_ms = chart->visible_start_ms;
175
+ if (out_end_ms) *out_end_ms = chart->visible_end_ms;
176
+ }
177
+
178
+ extern "C" void vroom_chart_reset_view(VroomChart* chart) {
179
+ if (!chart) return;
180
+ chart->visible_start_ms = 0;
181
+ chart->visible_end_ms = 0;
182
+ chart->price_bounds_manual = false;
183
+ apply_default_framing(chart);
184
+ vroom::labels::recompute_axis_width(*chart);
185
+ if (chart->cb.on_viewport_changed) {
186
+ chart->cb.on_viewport_changed(chart->user_ctx, chart->visible_start_ms,
187
+ chart->visible_end_ms);
188
+ }
189
+ chart->mark_dirty();
190
+ }
191
+
192
+ extern "C" void vroom_chart_reset_price_scale(VroomChart* chart) {
193
+ if (!chart || !chart->price_bounds_manual) return;
194
+ chart->price_bounds_manual = false;
195
+ vroom::labels::recompute_axis_width(*chart);
196
+ chart->mark_dirty();
197
+ }
198
+
171
199
  extern "C" void vroom_chart_pan(VroomChart* chart, float dx_px, float /*dy_px*/) {
172
200
  if (!chart || dx_px == 0.f || chart->candles.empty()) return;
173
201
 
@@ -269,7 +297,8 @@ extern "C" void vroom_chart_translate(VroomChart* chart, float dx_px, float dy_p
269
297
  // Vertical: shift price bounds by the price-equivalent of dy_px. Range
270
298
  // stays constant. We divide by draw_h (the 90% slice of candle area
271
299
  // that's actually used for price → y) so translation feels 1:1.
272
- if (dy_px != 0.f && chart->price_bounds_initialized) {
300
+ if (dy_px != 0.f && !chart->candles.empty()) {
301
+ ensure_manual_price_bounds(chart);
273
302
  const float candle_area_h = vroom::price_pane_bottom(chart->layout());
274
303
  const double draw_h = static_cast<double>(candle_area_h) * 0.9;
275
304
  if (draw_h > 0.0) {
@@ -296,7 +325,7 @@ extern "C" void vroom_chart_translate(VroomChart* chart, float dx_px, float dy_p
296
325
 
297
326
  extern "C" void vroom_chart_scale_price_axis(VroomChart* chart, float dy_px) {
298
327
  if (!chart || dy_px == 0.f) return;
299
- if (!chart->price_bounds_initialized) return;
328
+ ensure_manual_price_bounds(chart);
300
329
 
301
330
  const double range = chart->price_bounds.max - chart->price_bounds.min;
302
331
  if (range <= 0.0) return;
@@ -387,6 +416,102 @@ extern "C" void vroom_chart_scale_time_axis(VroomChart* chart, float dx_px) {
387
416
  chart->mark_dirty();
388
417
  }
389
418
 
419
+ extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px) {
420
+ if (!chart || dy_px == 0.f) return;
421
+
422
+ const int pane_count = (chart->rsi_enabled ? 1 : 0) + (chart->macd_enabled ? 1 : 0);
423
+ if (pane_count == 0) return; // nothing below the chart to resize
424
+
425
+ const auto lay = chart->layout();
426
+ const float old_pane_bottom = vroom::price_pane_bottom(lay);
427
+ const float old_band = lay.indicator_area_h;
428
+
429
+ // Drag down (dy > 0) shrinks the indicator band and grows the price pane.
430
+ // Keep both regions usable: each pane >= kMinPanePx, price pane >= a share
431
+ // of the candle+indicator area.
432
+ constexpr float kMinPanePx = 48.f;
433
+ const float content_h = lay.height_px - lay.x_axis_height_px; // candle + indicators
434
+ const float min_band = static_cast<float>(pane_count) * kMinPanePx;
435
+ const float max_band = content_h * 0.70f; // leave the price pane >= 30%
436
+ if (max_band <= min_band) return;
437
+
438
+ float new_band = old_band - dy_px;
439
+ new_band = std::clamp(new_band, min_band, max_band);
440
+ if (new_band == old_band) return;
441
+
442
+ const float per_pane = new_band / static_cast<float>(pane_count);
443
+ chart->theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC] =
444
+ chart->height_px > 0.f ? per_pane / chart->height_px : 0.f;
445
+
446
+ // Preserve candle pixel scale: scale the price range by the price-pane
447
+ // height ratio, anchoring the top (max) price so existing candles stay put
448
+ // and the newly revealed space opens at the bottom.
449
+ if (chart->price_bounds_manual && old_pane_bottom > 0.f) {
450
+ const float new_pane_bottom = content_h - new_band;
451
+ const double scale = static_cast<double>(new_pane_bottom) /
452
+ static_cast<double>(old_pane_bottom);
453
+ const double range = chart->price_bounds.max - chart->price_bounds.min;
454
+ if (range > 0.0 && scale > 0.0) {
455
+ chart->price_bounds.min =
456
+ chart->price_bounds.max - range * scale;
457
+ vroom::labels::recompute_axis_width(*chart);
458
+ }
459
+ }
460
+
461
+ chart->mark_dirty();
462
+ }
463
+
464
+ extern "C" void vroom_chart_scale_indicator_axis(VroomChart* chart, float y_px,
465
+ float dy_px) {
466
+ if (!chart || dy_px == 0.f) return;
467
+
468
+ const auto lay = chart->layout();
469
+ if (lay.indicator_area_h <= 0.f) return;
470
+
471
+ // Rebuild the same ordered pane stack draw_chart uses (most recently
472
+ // enabled pane sorts to the bottom) to find which pane y_px falls in.
473
+ struct ActivePane { int order; int type; }; // type: 0 = RSI, 1 = MACD
474
+ ActivePane panes[2];
475
+ int count = 0;
476
+ if (chart->rsi_enabled) panes[count++] = {chart->rsi_order, 0};
477
+ if (chart->macd_enabled) panes[count++] = {chart->macd_order, 1};
478
+ if (count == 0) return;
479
+ if (count == 2 && panes[0].order > panes[1].order) {
480
+ const ActivePane tmp = panes[0];
481
+ panes[0] = panes[1];
482
+ panes[1] = tmp;
483
+ }
484
+
485
+ const float pane_h =
486
+ chart->height_px * chart->theme.floats[VROOM_FLOAT_INDICATOR_HEIGHT_FRAC];
487
+ if (pane_h <= 0.f) return;
488
+
489
+ int target = -1; // 0 = RSI, 1 = MACD
490
+ float pane_top = vroom::price_pane_bottom(lay);
491
+ for (int i = 0; i < count; ++i) {
492
+ const float pane_bottom = pane_top + pane_h;
493
+ if (y_px >= pane_top && y_px < pane_bottom) {
494
+ target = panes[i].type;
495
+ break;
496
+ }
497
+ pane_top = pane_bottom;
498
+ }
499
+ if (target < 0) return; // not over a pane
500
+
501
+ // Drag down (dy > 0) widens the visible value range (zoom out), matching
502
+ // scale_price_axis's sign. The zoom is the inverse of the range scale.
503
+ double range_scale = 1.0 + static_cast<double>(dy_px) / kAxisDragSensitivity;
504
+ if (range_scale < 0.05) range_scale = 0.05; // never collapse or flip
505
+
506
+ double& zoom = target == 0 ? chart->rsi_y_scale : chart->macd_y_scale;
507
+ double next = zoom / range_scale;
508
+ next = std::clamp(next, 0.1, 10.0);
509
+ if (next == zoom) return;
510
+ zoom = next;
511
+
512
+ chart->mark_dirty();
513
+ }
514
+
390
515
  extern "C" void vroom_chart_get_axis_metrics(VroomChart* chart,
391
516
  float* out_y_axis_width_px,
392
517
  float* out_x_axis_height_px,
@@ -406,7 +531,8 @@ extern "C" void vroom_chart_zoom(VroomChart* chart, float scale_x, float scale_y
406
531
 
407
532
  // --- Y (price) zoom around fy ------------------------------------------
408
533
  // scale_y > 1 (vertical pinch out) narrows the price range → taller candles.
409
- if (scale_y > 0.f && scale_y != 1.f && chart->price_bounds_initialized) {
534
+ if (scale_y > 0.f && scale_y != 1.f && !chart->candles.empty()) {
535
+ ensure_manual_price_bounds(chart);
410
536
  const float candle_area_h = vroom::price_pane_bottom(chart->layout());
411
537
  const double range = chart->price_bounds.max - chart->price_bounds.min;
412
538
  if (candle_area_h > 0.f && range > 0.0) {
@@ -551,6 +677,60 @@ extern "C" bool vroom_chart_get_crosshair_info(VroomChart* chart,
551
677
  return true;
552
678
  }
553
679
 
680
+ // ---- Drawings (line annotations) ------------------------------------------
681
+
682
+ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
683
+ const VroomDrawing* drawings,
684
+ size_t count) {
685
+ if (!chart) return;
686
+ chart->drawings.assign(drawings, drawings + count);
687
+ chart->mark_dirty();
688
+ }
689
+
690
+ extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
691
+ double a_price, bool has_b, int64_t b_time,
692
+ double b_price, bool guide, uint32_t color,
693
+ float width) {
694
+ if (!chart) return;
695
+ chart->draft_active = true;
696
+ chart->draft_a = VroomDrawPoint{a_time, a_price};
697
+ chart->draft_has_b = has_b;
698
+ chart->draft_b = VroomDrawPoint{b_time, b_price};
699
+ chart->draft_guide = guide;
700
+ chart->draft_color = color;
701
+ chart->draft_width = width;
702
+ chart->mark_dirty();
703
+ }
704
+
705
+ extern "C" void vroom_chart_clear_draft(VroomChart* chart) {
706
+ if (!chart) return;
707
+ if (!chart->draft_active) return;
708
+ chart->draft_active = false;
709
+ chart->mark_dirty();
710
+ }
711
+
712
+ extern "C" bool vroom_chart_coord_at(VroomChart* chart, float x_px, float y_px,
713
+ VroomCoord* out) {
714
+ if (!chart || !out || chart->candles.empty()) return false;
715
+ const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
716
+ if (window_ms <= 0) return false;
717
+ const auto lay = chart->layout();
718
+ // Mirror draw_chart's bounds: the frozen manual scale, else the auto-fit
719
+ // over the visible slice — so y_to_price matches what's on screen.
720
+ const auto range = vroom::visible_indices(
721
+ chart->candles.data(), chart->candles.size(),
722
+ chart->visible_start_ms, chart->visible_end_ms);
723
+ const size_t n = range.end - range.start;
724
+ const auto bounds =
725
+ chart->price_bounds_manual
726
+ ? chart->price_bounds
727
+ : vroom::auto_price_bounds(chart->candles.data() + range.start, n);
728
+ out->time_ms =
729
+ vroom::time_at_x(lay, chart->visible_start_ms, window_ms, x_px);
730
+ out->price = vroom::y_to_price(lay, bounds, y_px);
731
+ return true;
732
+ }
733
+
554
734
  // ---- Indicators -----------------------------------------------------------
555
735
 
556
736
  extern "C" void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,