react-native-vroom-chart 0.1.3 → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Darion Welch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -27,11 +27,13 @@ ChartHostObject::~ChartHostObject() {
27
27
  std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
28
28
  jsi::Runtime& rt) {
29
29
  std::vector<jsi::PropNameID> out;
30
- out.reserve(19);
30
+ out.reserve(22);
31
31
  out.push_back(jsi::PropNameID::forAscii(rt, "setCandles"));
32
32
  out.push_back(jsi::PropNameID::forAscii(rt, "setSize"));
33
33
  out.push_back(jsi::PropNameID::forAscii(rt, "setColor"));
34
+ out.push_back(jsi::PropNameID::forAscii(rt, "setFloat"));
34
35
  out.push_back(jsi::PropNameID::forAscii(rt, "setVisibleRange"));
36
+ out.push_back(jsi::PropNameID::forAscii(rt, "setDefaultCandleWidth"));
35
37
  out.push_back(jsi::PropNameID::forAscii(rt, "pan"));
36
38
  out.push_back(jsi::PropNameID::forAscii(rt, "translate"));
37
39
  out.push_back(jsi::PropNameID::forAscii(rt, "zoom"));
@@ -130,6 +132,25 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
130
132
  });
131
133
  }
132
134
 
135
+ if (name == "setFloat") {
136
+ // setFloat(keyIndex, value) — keyIndex is a VroomFloatKey (e.g. wick width).
137
+ // The core bounds-checks the key and marks dirty.
138
+ return jsi::Function::createFromHostFunction(
139
+ rt,
140
+ jsi::PropNameID::forAscii(rt, "setFloat"),
141
+ 2,
142
+ [this](jsi::Runtime& /*rt2*/,
143
+ const jsi::Value& /*thisVal*/,
144
+ const jsi::Value* args,
145
+ size_t count) -> jsi::Value {
146
+ if (count < 2) return jsi::Value::undefined();
147
+ const int key = static_cast<int>(args[0].asNumber());
148
+ vroom_chart_set_float(chart_, static_cast<VroomFloatKey>(key),
149
+ static_cast<float>(args[1].asNumber()));
150
+ return jsi::Value::undefined();
151
+ });
152
+ }
153
+
133
154
  if (name == "setVisibleRange") {
134
155
  // Pass 0,0 to clear (show all).
135
156
  // Timestamps are Number on the JS side — fine for ms-since-epoch since
@@ -150,6 +171,23 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
150
171
  });
151
172
  }
152
173
 
174
+ if (name == "setDefaultCandleWidth") {
175
+ // setDefaultCandleWidth(px) — target candle body width for initial framing.
176
+ return jsi::Function::createFromHostFunction(
177
+ rt,
178
+ jsi::PropNameID::forAscii(rt, "setDefaultCandleWidth"),
179
+ 1,
180
+ [this](jsi::Runtime& /*rt2*/,
181
+ const jsi::Value& /*thisVal*/,
182
+ const jsi::Value* args,
183
+ size_t count) -> jsi::Value {
184
+ if (count < 1) return jsi::Value::undefined();
185
+ vroom_chart_set_default_candle_width(
186
+ chart_, static_cast<float>(args[0].asNumber()));
187
+ return jsi::Value::undefined();
188
+ });
189
+ }
190
+
153
191
  if (name == "pan") {
154
192
  // pan(dx, dy) -> JsiSkPicture
155
193
  // Mutates the visible range and renders in one JSI call so gesture
@@ -63,6 +63,29 @@ typedef struct VroomDrawing {
63
63
  float width; // stroke width in px
64
64
  } VroomDrawing;
65
65
 
66
+ // A resting-liquidity band: a price interval carrying a total order size on one
67
+ // side of the book. Anchored in price space, so it scales with the y-axis.
68
+ typedef struct VroomBand {
69
+ double min_price; // bottom of the interval
70
+ double max_price; // top of the interval
71
+ int32_t side; // 0 = buy, 1 = sell (selects the color)
72
+ double volume; // total resting size; drives the band's opacity
73
+ } VroomBand;
74
+
75
+ // Style shared by all liquidity bands. `buy_color`/`sell_color` are 0xAARRGGBB;
76
+ // the alpha is replaced per band by its volume mapped into
77
+ // [min_opacity, max_opacity]. `max_volume <= 0` auto-scales to the largest band.
78
+ // The leftward reach is min(width_px, width_frac * pane_width).
79
+ typedef struct VroomLiquidityStyle {
80
+ uint32_t buy_color;
81
+ uint32_t sell_color;
82
+ double max_volume;
83
+ float min_opacity;
84
+ float max_opacity;
85
+ float width_px;
86
+ float width_frac;
87
+ } VroomLiquidityStyle;
88
+
66
89
  // A continuous data coordinate at a pixel position (no candle snapping). Used to
67
90
  // translate a drawing-tool click into a data-space anchor.
68
91
  typedef struct VroomCoord {
@@ -101,6 +124,9 @@ typedef enum {
101
124
  VROOM_FLOAT_X_AXIS_HEIGHT_PX, // bottom strip reserved for time labels
102
125
  VROOM_FLOAT_VOLUME_OPACITY, // volume bar opacity (1=opaque)
103
126
  VROOM_FLOAT_INDICATOR_HEIGHT_FRAC, // below-chart indicator pane, fraction of height
127
+ VROOM_FLOAT_CANDLE_RADIUS_PX, // candle body corner radius px (0 = square)
128
+ VROOM_FLOAT_WICK_ROUND_CAP, // 0/1: round the wick end caps
129
+ VROOM_FLOAT_VOLUME_RADIUS_PX, // volume bar top-corner radius px (0 = square)
104
130
  VROOM_FLOAT_COUNT_
105
131
  } VroomFloatKey;
106
132
 
@@ -131,6 +157,12 @@ void vroom_chart_set_size(VroomChart* chart, float width_px, float height_px, fl
131
157
 
132
158
  void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_ms, int64_t end_ms);
133
159
 
160
+ // Target candle *body* width in px for the default/reset framing. Larger = more
161
+ // zoomed in (fewer candles), smaller = more zoomed out. 0 restores the legacy
162
+ // "most recent ~80 candles" default. Affects initial framing only; an explicit
163
+ // set_visible_range still overrides it.
164
+ void vroom_chart_set_default_candle_width(VroomChart* chart, float px);
165
+
134
166
  // Reads the current visible time window. Either out pointer may be null.
135
167
  // Both are 0 when the window is still uninitialized.
136
168
  void vroom_chart_get_visible_range(VroomChart* chart,
@@ -195,6 +227,15 @@ void vroom_chart_get_axis_metrics(VroomChart* chart,
195
227
  // ---- Crosshair ------------------------------------------------------------
196
228
 
197
229
  void vroom_chart_set_crosshair(VroomChart* chart, float x_px, float y_px);
230
+
231
+ // Show the crosshair at a data-space position (epoch ms + price) rather than
232
+ // pixels — used to drive one chart's crosshair from another's (cross-chart
233
+ // sync). Converts to pixels via the current visible window and price scale;
234
+ // the vertical line then snaps to the nearest candle center at draw time, so
235
+ // the crosshair lands on the candle for `time_ms`. The horizontal line sits at
236
+ // `price` (clamped to the pane when off-scale).
237
+ void vroom_chart_set_crosshair_data(VroomChart* chart, int64_t time_ms, double price);
238
+
198
239
  void vroom_chart_clear_crosshair(VroomChart* chart);
199
240
 
200
241
  // Fills *out with the OHLCV of the candle the crosshair currently snaps to and
@@ -211,6 +252,10 @@ bool vroom_chart_get_crosshair_candle(VroomChart* chart, VroomCandle* out);
211
252
  // `candle` is left untouched.
212
253
  typedef struct VroomCrosshairInfo {
213
254
  int64_t time_ms;
255
+ // Free price under the crosshair's horizontal line — the value drawn on the
256
+ // price badge (not snapped to any candle). Valid whenever the call returns
257
+ // true, including in the future region where `has_candle` is false.
258
+ double price;
214
259
  bool has_candle;
215
260
  VroomCandle candle;
216
261
  } VroomCrosshairInfo;
@@ -259,6 +304,14 @@ void vroom_chart_set_vwap(VroomChart* chart, bool enabled, int reset_offset_min,
259
304
  void vroom_chart_set_drawings(VroomChart* chart, const VroomDrawing* drawings,
260
305
  size_t count);
261
306
 
307
+ // ---- Liquidity bands (order-book depth overlay) ---------------------------
308
+
309
+ // Replaces the full set of resting-liquidity bands and their shared style.
310
+ // Bands render behind the candles, anchored at the inner edge of the price axis
311
+ // and fading left. Pass count 0 to clear; `style` may be null when count is 0.
312
+ void vroom_chart_set_liquidity(VroomChart* chart, const VroomBand* bands,
313
+ size_t count, const VroomLiquidityStyle* style);
314
+
262
315
  // Sets the transient in-progress "draft" the drawing tool shows while the user
263
316
  // places points. Node A is always shown; when `has_b`, node B is shown too.
264
317
  // `guide != 0` also draws the guideline A->B (the live line preview); `guide == 0`
@@ -39,6 +39,18 @@ void draw(SkCanvas* canvas,
39
39
  const uint32_t fill_bull = theme.colors[VROOM_COLOR_BULL];
40
40
  const uint32_t fill_bear = theme.colors[VROOM_COLOR_BEAR];
41
41
 
42
+ const float body_r = theme.floats[VROOM_FLOAT_CANDLE_RADIUS_PX];
43
+ const bool wick_round = theme.floats[VROOM_FLOAT_WICK_ROUND_CAP] > 0.5f;
44
+
45
+ constexpr float kBorderWidthPx = 1.f;
46
+ // Draw the border only when it differs from the fill; an inherited/transparent
47
+ // border color (the default, and the "hide" path) renders nothing — so a hidden
48
+ // border costs no pixels and never affects candle width.
49
+ const bool draw_border_bull =
50
+ resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull) != fill_bull;
51
+ const bool draw_border_bear =
52
+ resolve_color(theme.colors[VROOM_COLOR_BORDER_BEAR], fill_bear) != fill_bear;
53
+
42
54
  SkPaint bull_paint;
43
55
  bull_paint.setAntiAlias(true);
44
56
  bull_paint.setColor(fill_bull);
@@ -53,17 +65,19 @@ void draw(SkCanvas* canvas,
53
65
  resolve_color(theme.colors[VROOM_COLOR_WICK_BULL], fill_bull));
54
66
  wick_bull.setStrokeWidth(theme.floats[VROOM_FLOAT_WICK_WIDTH_PX]);
55
67
  wick_bull.setStyle(SkPaint::kStroke_Style);
68
+ wick_bull.setStrokeCap(wick_round ? SkPaint::kRound_Cap : SkPaint::kButt_Cap);
56
69
 
57
70
  SkPaint wick_bear = wick_bull;
58
71
  wick_bear.setColor(
59
72
  resolve_color(theme.colors[VROOM_COLOR_WICK_BEAR], fill_bear));
60
73
 
61
- // 1px body outlines. Default sentinel resolves to the fill color, so the
62
- // border is invisible until a consumer sets borderBull/borderBear.
74
+ // 1px body outlines, drawn *inside* the body (see the inset at draw time) so
75
+ // a contrasting border never widens the candle. Default sentinel resolves to
76
+ // the fill color; such borders are skipped entirely (draw_border_* above).
63
77
  SkPaint border_bull;
64
78
  border_bull.setAntiAlias(true);
65
79
  border_bull.setStyle(SkPaint::kStroke_Style);
66
- border_bull.setStrokeWidth(1.f);
80
+ border_bull.setStrokeWidth(kBorderWidthPx);
67
81
  border_bull.setColor(
68
82
  resolve_color(theme.colors[VROOM_COLOR_BORDER_BULL], fill_bull));
69
83
 
@@ -92,8 +106,32 @@ void draw(SkCanvas* canvas,
92
106
  const float y_bot = std::max(y_open, y_close);
93
107
  const float h = std::max(1.f, y_bot - y_top);
94
108
  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);
109
+ // Corner radius clamped so thin candles / short bodies don't over-round.
110
+ const float r = std::min({body_r, body_w * 0.5f, h * 0.5f});
111
+ const SkPaint& fill = bull ? bull_paint : bear_paint;
112
+ if (r > 0.f) {
113
+ canvas->drawRoundRect(body, r, r, fill);
114
+ } else {
115
+ canvas->drawRect(body, fill);
116
+ }
117
+
118
+ const bool draw_border = bull ? draw_border_bull : draw_border_bear;
119
+ // Inset by half the stroke so the centered stroke falls fully within the
120
+ // body: the border overlaps the fill's outer edge instead of straddling
121
+ // it, keeping the candle's footprint exactly body_w. Skip when the body is
122
+ // too small to hold it.
123
+ if (draw_border && body_w > kBorderWidthPx && h > kBorderWidthPx) {
124
+ const SkRect inner =
125
+ body.makeInset(kBorderWidthPx * 0.5f, kBorderWidthPx * 0.5f);
126
+ const SkPaint& bp = bull ? border_bull : border_bear;
127
+ // Concentric radius so the inside border follows the rounded body.
128
+ const float ir = std::max(0.f, r - kBorderWidthPx * 0.5f);
129
+ if (ir > 0.f) {
130
+ canvas->drawRoundRect(inner, ir, ir, bp);
131
+ } else {
132
+ canvas->drawRect(inner, bp);
133
+ }
134
+ }
97
135
  }
98
136
  }
99
137
 
@@ -23,6 +23,7 @@
23
23
  #include "crosshair.h"
24
24
  #include "drawings.h"
25
25
  #include "labels.h"
26
+ #include "liquidity.h"
26
27
  #include "ma.h"
27
28
  #include "ma_overlay.h"
28
29
  #include "macd.h"
@@ -142,6 +143,12 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
142
143
  vroom::volume::draw(canvas, visible, n, lay, theme,
143
144
  window_ms, visible_start_ms, candle_duration_ms);
144
145
 
146
+ // 4.6. Liquidity bands (resting-order depth) — behind the candles so the
147
+ // candle bodies paint over the volume-driven tint. Anchored in price
148
+ // space, so they scale with the y-axis.
149
+ vroom::liquidity::draw(canvas, *this, lay, bounds, candle_right,
150
+ candle_area_h);
151
+
145
152
  // 5. Candles (wicks + bodies)
146
153
  vroom::candles::draw(canvas, visible, n, lay, theme, bounds,
147
154
  window_ms, visible_start_ms, candle_duration_ms);
@@ -50,6 +50,10 @@ struct VroomChart {
50
50
  int64_t visible_start_ms = 0;
51
51
  int64_t visible_end_ms = 0;
52
52
 
53
+ // >0: default/reset framing targets this candle *body* width in px (drives
54
+ // initial zoom). 0 = legacy "last ~80 candles" behavior.
55
+ float default_candle_px = 0.f;
56
+
53
57
  // Cached y-axis width in pixels, sized to fit the widest formatted price
54
58
  // label. 0 = uncomputed; layout() falls back to a width ratio.
55
59
  float axis_width_px = 0.f;
@@ -140,6 +144,12 @@ struct VroomChart {
140
144
  uint32_t draft_color = 0xff2962ff;
141
145
  float draft_width = 2.f;
142
146
 
147
+ // --- liquidity bands (order-book depth overlay) ------------------------
148
+ // Resting-order bands anchored in price space, drawn behind the candles and
149
+ // fading left from the price axis. Empty when the overlay is off.
150
+ std::vector<VroomBand> bands;
151
+ VroomLiquidityStyle liquidity_style{};
152
+
143
153
  // --- theme --------------------------------------------------------------
144
154
  vroom::Theme theme;
145
155
 
@@ -54,10 +54,37 @@ 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.
57
+ // Frame the default view. When the consumer has set a target candle body width
58
+ // (default_candle_px), size the window so each candle renders ~that wide with the
59
+ // right edge pinned to the latest candle; otherwise fall back to the legacy
60
+ // "most recent ~80 candles". No-op when there are no candles.
59
61
  void apply_default_framing(VroomChart* chart) {
60
62
  if (chart->candles.empty()) return;
63
+
64
+ // Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
65
+ // body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
66
+ // Needs a valid layout (width_px); until size is known it falls through to
67
+ // the candle-count default below.
68
+ if (chart->default_candle_px > 0.f) {
69
+ const auto lay = chart->layout();
70
+ const double usable =
71
+ lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
72
+ const double ratio = chart->theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO];
73
+ const double dur = static_cast<double>(chart->candle_duration_ms);
74
+ const double body = std::clamp(static_cast<double>(chart->default_candle_px),
75
+ kMinCandleBodyPx, kMaxCandleBodyPx);
76
+ if (usable > 0.0 && ratio > 0.0 && dur > 0.0 && body > 0.0) {
77
+ int64_t window_ms = static_cast<int64_t>((usable * dur * ratio) / body);
78
+ const int64_t span =
79
+ chart->candles.back().time_ms - chart->candles.front().time_ms;
80
+ window_ms = std::clamp<int64_t>(window_ms, chart->candle_duration_ms,
81
+ span > 0 ? span : window_ms);
82
+ chart->visible_end_ms = chart->candles.back().time_ms;
83
+ chart->visible_start_ms = chart->visible_end_ms - window_ms;
84
+ return;
85
+ }
86
+ }
87
+
61
88
  constexpr size_t kDefaultVisible = 80;
62
89
  const size_t start_idx = chart->candles.size() > kDefaultVisible
63
90
  ? chart->candles.size() - kDefaultVisible
@@ -167,6 +194,16 @@ extern "C" void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_m
167
194
  chart->mark_dirty();
168
195
  }
169
196
 
197
+ extern "C" void vroom_chart_set_default_candle_width(VroomChart* chart, float px) {
198
+ if (!chart) return;
199
+ chart->default_candle_px = px;
200
+ // Re-frame so a value that arrives after set_candles still drives the initial
201
+ // view. Intended for initial framing only — calling it after the user has
202
+ // panned would snap the view back to the default frame.
203
+ if (!chart->candles.empty()) apply_default_framing(chart);
204
+ chart->mark_dirty();
205
+ }
206
+
170
207
  extern "C" void vroom_chart_get_visible_range(VroomChart* chart,
171
208
  int64_t* out_start_ms,
172
209
  int64_t* out_end_ms) {
@@ -623,6 +660,28 @@ extern "C" void vroom_chart_set_crosshair(VroomChart* chart, float x, float y) {
623
660
  chart->mark_dirty();
624
661
  }
625
662
 
663
+ extern "C" void vroom_chart_set_crosshair_data(VroomChart* chart,
664
+ int64_t time_ms, double price) {
665
+ if (!chart || chart->candles.empty()) return;
666
+ const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
667
+ if (window_ms <= 0) return;
668
+ const auto lay = chart->layout();
669
+ // Mirror coord_at's bounds so price -> y matches what's on screen.
670
+ const auto range = vroom::visible_indices(
671
+ chart->candles.data(), chart->candles.size(),
672
+ chart->visible_start_ms, chart->visible_end_ms);
673
+ const size_t n = range.end - range.start;
674
+ const auto bounds =
675
+ chart->price_bounds_manual
676
+ ? chart->price_bounds
677
+ : vroom::auto_price_bounds(chart->candles.data() + range.start, n);
678
+ chart->crosshair_active = true;
679
+ chart->crosshair_x_px =
680
+ vroom::x_at_time(lay, chart->visible_start_ms, window_ms, time_ms);
681
+ chart->crosshair_y_px = vroom::price_to_y(lay, bounds, price);
682
+ chart->mark_dirty();
683
+ }
684
+
626
685
  extern "C" void vroom_chart_clear_crosshair(VroomChart* chart) {
627
686
  if (!chart) return;
628
687
  chart->crosshair_active = false;
@@ -672,6 +731,13 @@ extern "C" bool vroom_chart_get_crosshair_info(VroomChart* chart,
672
731
  lay, visible, n, chart->candle_duration_ms,
673
732
  chart->visible_start_ms, window_ms, chart->crosshair_x_px);
674
733
  out->time_ms = snap.time_ms;
734
+ // Free price at the crosshair's horizontal line — same bounds as the price
735
+ // badge (crosshair.cpp) so the reported value equals what's drawn.
736
+ const auto bounds =
737
+ chart->price_bounds_manual
738
+ ? chart->price_bounds
739
+ : vroom::auto_price_bounds(visible, n);
740
+ out->price = vroom::y_to_price(lay, bounds, chart->crosshair_y_px);
675
741
  out->has_candle = snap.has_candle;
676
742
  if (snap.has_candle) out->candle = visible[snap.index];
677
743
  return true;
@@ -687,6 +753,17 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
687
753
  chart->mark_dirty();
688
754
  }
689
755
 
756
+ // ---- Liquidity bands (order-book depth overlay) ---------------------------
757
+
758
+ extern "C" void vroom_chart_set_liquidity(VroomChart* chart,
759
+ const VroomBand* bands, size_t count,
760
+ const VroomLiquidityStyle* style) {
761
+ if (!chart) return;
762
+ chart->bands.assign(bands, bands + count);
763
+ if (style) chart->liquidity_style = *style;
764
+ chart->mark_dirty();
765
+ }
766
+
690
767
  extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
691
768
  double a_price, bool has_b, int64_t b_time,
692
769
  double b_price, bool guide, uint32_t color,
@@ -0,0 +1,112 @@
1
+ #include "liquidity.h"
2
+
3
+ #include <algorithm>
4
+ #include <cstdint>
5
+
6
+ #pragma clang diagnostic push
7
+ #pragma clang diagnostic ignored "-Wdocumentation"
8
+ #include "include/core/SkCanvas.h"
9
+ #include "include/core/SkColor.h"
10
+ #include "include/core/SkPaint.h"
11
+ #include "include/core/SkPoint.h"
12
+ #include "include/core/SkRect.h"
13
+ #include "include/core/SkShader.h"
14
+ #include "include/core/SkTileMode.h"
15
+ // Skia's linear-gradient API differs across the Skia versions we build against:
16
+ // the WASM build uses a newer Skia (SkGradient.h + SkShaders::LinearGradient),
17
+ // while react-native-skia bundles an older one (SkGradientShader::MakeLinear).
18
+ // Neither ships both headers, so select whichever is present.
19
+ #if __has_include("include/effects/SkGradient.h")
20
+ # include "include/core/SkSpan.h"
21
+ # include "include/effects/SkGradient.h"
22
+ # define VROOM_SK_MODERN_GRADIENT 1
23
+ #else
24
+ # include "include/effects/SkGradientShader.h"
25
+ # define VROOM_SK_MODERN_GRADIENT 0
26
+ #endif
27
+ #pragma clang diagnostic pop
28
+
29
+ #include "chart.h"
30
+
31
+ namespace vroom::liquidity {
32
+ namespace {
33
+
34
+ // A horizontal shader fading from transparent at the left of `pts` to `base` at
35
+ // opacity `alpha` on the right, keeping RGB constant so the faded edge doesn't
36
+ // darken toward black. Abstracts over the two Skia gradient APIs above.
37
+ sk_sp<SkShader> left_fade_shader(const SkPoint pts[2], SkColor base, float alpha) {
38
+ #if VROOM_SK_MODERN_GRADIENT
39
+ const float r = SkColorGetR(base) / 255.f;
40
+ const float g = SkColorGetG(base) / 255.f;
41
+ const float b = SkColorGetB(base) / 255.f;
42
+ const SkColor4f colors[2] = {SkColor4f{r, g, b, 0.f}, SkColor4f{r, g, b, alpha}};
43
+ const SkGradient::Colors grad_colors(
44
+ SkSpan<const SkColor4f>(colors, 2), SkTileMode::kClamp);
45
+ const SkGradient grad(grad_colors, SkGradient::Interpolation{});
46
+ return SkShaders::LinearGradient(pts, grad);
47
+ #else
48
+ const auto a = static_cast<U8CPU>(alpha * 255.f + 0.5f);
49
+ const SkColor colors[2] = {SkColorSetA(base, 0), SkColorSetA(base, a)};
50
+ return SkGradientShader::MakeLinear(pts, colors, nullptr, 2, SkTileMode::kClamp);
51
+ #endif
52
+ }
53
+
54
+ } // namespace
55
+
56
+ void draw(SkCanvas* canvas,
57
+ const VroomChart& chart,
58
+ const Layout& lay,
59
+ const PriceBounds& bounds,
60
+ float candle_right,
61
+ float candle_area_h) {
62
+ if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
63
+ if (chart.bands.empty()) return;
64
+
65
+ const VroomLiquidityStyle& style = chart.liquidity_style;
66
+
67
+ // Leftward reach: the smaller of the px cap and the pane-fraction, clamped so
68
+ // the band never starts left of the pane.
69
+ const float frac_reach = style.width_frac > 0.f ? style.width_frac * candle_right
70
+ : candle_right;
71
+ const float px_reach = style.width_px > 0.f ? style.width_px : candle_right;
72
+ const float reach = std::min(px_reach, frac_reach);
73
+ const float left_x = std::max(0.f, candle_right - reach);
74
+
75
+ // Volume that maps to peak opacity: explicit, or the largest band's volume.
76
+ double max_vol = style.max_volume;
77
+ if (max_vol <= 0.0) {
78
+ for (const VroomBand& b : chart.bands) max_vol = std::max(max_vol, b.volume);
79
+ }
80
+
81
+ const float min_op = std::clamp(style.min_opacity, 0.f, 1.f);
82
+ const float max_op = std::clamp(style.max_opacity, 0.f, 1.f);
83
+
84
+ canvas->save();
85
+ canvas->clipRect(SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h));
86
+
87
+ const SkPoint pts[2] = {SkPoint{left_x, 0.f}, SkPoint{candle_right, 0.f}};
88
+
89
+ for (const VroomBand& b : chart.bands) {
90
+ // Higher price -> smaller y, so max_price is the top edge.
91
+ const float y_top = vroom::price_to_y(lay, bounds, b.max_price);
92
+ const float y_bot = vroom::price_to_y(lay, bounds, b.min_price);
93
+ if (y_bot < 0.f || y_top > candle_area_h) continue; // fully off-pane
94
+
95
+ const SkColor base =
96
+ static_cast<SkColor>(b.side == 0 ? style.buy_color : style.sell_color);
97
+
98
+ const double t = max_vol > 0.0
99
+ ? std::clamp(b.volume / max_vol, 0.0, 1.0)
100
+ : 0.0;
101
+ const float alpha = min_op + (max_op - min_op) * static_cast<float>(t);
102
+
103
+ SkPaint paint;
104
+ paint.setAntiAlias(true);
105
+ paint.setShader(left_fade_shader(pts, base, alpha));
106
+ canvas->drawRect(SkRect::MakeLTRB(left_x, y_top, candle_right, y_bot), paint);
107
+ }
108
+
109
+ canvas->restore();
110
+ }
111
+
112
+ } // namespace vroom::liquidity
@@ -0,0 +1,35 @@
1
+ // Liquidity bands — resting-order / order-book depth rendered on the price pane.
2
+ //
3
+ // Each band (chart.bands) is a price interval carrying a total size on one side
4
+ // of the book. It is drawn as a horizontal rectangle anchored at the inner edge
5
+ // of the price axis (`candle_right`), stretching left by the configured reach
6
+ // and fading to transparent with a left->right gradient. Color comes from the
7
+ // band's side (buy/sell); the band's alpha comes from its volume mapped into
8
+ // [min_opacity, max_opacity]. Because the vertical extent is derived from price
9
+ // via price_to_y, bands scale with the y-axis on zoom.
10
+ //
11
+ // Like ma_overlay / drawings this is its own module so the chart orchestrator
12
+ // stays thin. Drawn BEHIND the candles (before candles::draw) so candle bodies
13
+ // paint over the bands.
14
+
15
+ #pragma once
16
+
17
+ #include "viewport.h"
18
+ #include "vroom/vroom_chart.h"
19
+
20
+ class SkCanvas;
21
+ struct VroomChart;
22
+
23
+ namespace vroom::liquidity {
24
+
25
+ // Draws the liquidity bands. `candle_right` is the x of the price-axis strip
26
+ // (bands' right edge); `candle_area_h` is the price-pane bottom. Geometry is
27
+ // clipped to that rectangle so bands never bleed into the axis strips.
28
+ void draw(SkCanvas* canvas,
29
+ const VroomChart& chart,
30
+ const vroom::Layout& lay,
31
+ const vroom::PriceBounds& bounds,
32
+ float candle_right,
33
+ float candle_area_h);
34
+
35
+ } // namespace vroom::liquidity
@@ -33,6 +33,9 @@ constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
33
33
  22.f, // X_AXIS_HEIGHT_PX — bottom strip for time labels
34
34
  0.5f, // VOLUME_OPACITY — candle color at 50% transparency
35
35
  0.20f, // INDICATOR_HEIGHT_FRAC — below-chart indicator pane = 20% of height
36
+ 0.f, // CANDLE_RADIUS_PX — square by default
37
+ 0.f, // WICK_ROUND_CAP — butt caps by default
38
+ 0.f, // VOLUME_RADIUS_PX — square by default
36
39
  };
37
40
 
38
41
  } // namespace
@@ -4,6 +4,7 @@
4
4
  #pragma clang diagnostic ignored "-Wdocumentation"
5
5
  #include "include/core/SkCanvas.h"
6
6
  #include "include/core/SkPaint.h"
7
+ #include "include/core/SkRRect.h"
7
8
  #include "include/core/SkRect.h"
8
9
  #pragma clang diagnostic pop
9
10
 
@@ -41,6 +42,7 @@ void draw(SkCanvas* canvas,
41
42
  const float half_body = body_w * 0.5f;
42
43
 
43
44
  const float opacity = theme.floats[VROOM_FLOAT_VOLUME_OPACITY];
45
+ const float vol_r = theme.floats[VROOM_FLOAT_VOLUME_RADIUS_PX];
44
46
  SkPaint bull_paint;
45
47
  bull_paint.setAntiAlias(true);
46
48
  bull_paint.setColor(theme.colors[VROOM_COLOR_ACCENT_BULL]);
@@ -61,9 +63,19 @@ void draw(SkCanvas* canvas,
61
63
  visible_start_ms, window_ms);
62
64
  const float h = std::max(
63
65
  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);
66
+ const SkRect rect =
67
+ SkRect::MakeXYWH(cx - half_body, candle_area_h - h, body_w, h);
68
+ const SkPaint& paint = bull ? bull_paint : bear_paint;
69
+ // Round only the top corners; clamp so short/narrow bars don't over-round.
70
+ const float r = std::min({vol_r, body_w * 0.5f, h});
71
+ if (r > 0.f) {
72
+ SkRRect rr;
73
+ const SkVector radii[4] = {{r, r}, {r, r}, {0, 0}, {0, 0}}; // TL, TR, BR, BL
74
+ rr.setRectRadii(rect, radii);
75
+ canvas->drawRRect(rr, paint);
76
+ } else {
77
+ canvas->drawRect(rect, paint);
78
+ }
67
79
  }
68
80
  }
69
81