react-native-vroom-chart 0.1.4 → 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.
@@ -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,
@@ -272,6 +304,14 @@ void vroom_chart_set_vwap(VroomChart* chart, bool enabled, int reset_offset_min,
272
304
  void vroom_chart_set_drawings(VroomChart* chart, const VroomDrawing* drawings,
273
305
  size_t count);
274
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
+
275
315
  // Sets the transient in-progress "draft" the drawing tool shows while the user
276
316
  // places points. Node A is always shown; when `has_b`, node B is shown too.
277
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) {
@@ -716,6 +753,17 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
716
753
  chart->mark_dirty();
717
754
  }
718
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
+
719
767
  extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
720
768
  double a_price, bool has_b, int64_t b_time,
721
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
 
package/lib/index.d.mts CHANGED
@@ -68,14 +68,22 @@ type VroomTheme = {
68
68
  accentBull?: VroomColor;
69
69
  /** Generic down color for the price indicator, volume bars, and MACD histogram. Defaults to red; independent of `bear`. */
70
70
  accentBear?: VroomColor;
71
- /** Up candle body 1px border. Defaults to the bull fill color. */
71
+ /** Up candle body border (1px, drawn *inside* the body so it never changes candle width). Omit or set to the bull fill color to hide it. */
72
72
  borderBull?: VroomColor;
73
- /** Down candle body 1px border. Defaults to the bear fill color. */
73
+ /** Down candle body border (1px, drawn *inside* the body so it never changes candle width). Omit or set to the bear fill color to hide it. */
74
74
  borderBear?: VroomColor;
75
75
  /** Up candle wick color. Defaults to the bull fill color. */
76
76
  wickBull?: VroomColor;
77
77
  /** Down candle wick color. Defaults to the bear fill color. */
78
78
  wickBear?: VroomColor;
79
+ /** Wick stroke width in px (applies to both up and down wicks). Defaults to 1. */
80
+ wickWidth?: number;
81
+ /** Corner radius (px) of candle bodies. Defaults to 0 (square). */
82
+ candleRadius?: number;
83
+ /** Round the wick end caps. Defaults to false. */
84
+ wickRoundCap?: boolean;
85
+ /** Corner radius (px) of the *top* of volume bars. Defaults to 0 (square). */
86
+ volumeRadius?: number;
79
87
  /** Gridlines. */
80
88
  grid?: VroomColor;
81
89
  /** Axis label text (price + time). */
@@ -169,6 +177,51 @@ type VWAPConfig = {
169
177
  /** Stroke width in px. Default 1.5. */
170
178
  width?: number;
171
179
  };
180
+ /**
181
+ * A single resting-liquidity band: a price interval carrying a total order size
182
+ * on one side of the book. Consolidate raw L2 levels into these buckets before
183
+ * passing them. The vertical extent is defined in price space, so bands scale
184
+ * with the y-axis on zoom.
185
+ */
186
+ type LiquidityBand = {
187
+ /** Bottom of the price interval. */
188
+ minPrice: number;
189
+ /** Top of the price interval. A per-level order uses a thin interval. */
190
+ maxPrice: number;
191
+ /** Which side of the book — selects the buy vs sell color. */
192
+ side: 'buy' | 'sell';
193
+ /** Total resting size in this band; drives the band's opacity. */
194
+ volume: number;
195
+ };
196
+ /**
197
+ * Resting-order / order-book liquidity overlay. Each band is drawn as a
198
+ * horizontal rectangle anchored at the inner edge of the price axis, stretching
199
+ * left (~`widthPx`/`widthFrac` of the pane) and fading out with a gradient,
200
+ * colored by side and made more opaque with volume. Rendered behind the candles.
201
+ * Omit the prop (or pass no bands) to hide the overlay.
202
+ */
203
+ type LiquidityConfig = {
204
+ /** The bands to render. */
205
+ bands: LiquidityBand[];
206
+ /** Buy-side color (hex string or packed ARGB number). Default teal-green. */
207
+ buyColor?: VroomColor;
208
+ /** Sell-side color (hex string or packed ARGB number). Default red. */
209
+ sellColor?: VroomColor;
210
+ /**
211
+ * Volume mapped to the peak (`maxOpacity`) band. Omit to auto-scale to the
212
+ * largest band's volume each render.
213
+ */
214
+ maxVolume?: number;
215
+ /** Opacity floor for the smallest band. Default 0.05. */
216
+ minOpacity?: number;
217
+ /** Opacity at `maxVolume`. Default 0.8. */
218
+ maxOpacity?: number;
219
+ /** Leftward reach from the axis in px. Default 300. */
220
+ widthPx?: number;
221
+ /** Leftward reach as a fraction of pane width. Default 0.25. The smaller of
222
+ * `widthPx` and `widthFrac * paneWidth` is used. */
223
+ widthFrac?: number;
224
+ };
172
225
  /** MACD indicator config. Rendered in its own pane below the candles. */
173
226
  type MACDConfig = {
174
227
  enabled?: boolean;
@@ -204,6 +257,14 @@ type VroomChartCoreProps = {
204
257
  height?: number;
205
258
  /** Time window to render. Omit (or both 0) to show every candle. */
206
259
  visibleRange?: VisibleRange;
260
+ /**
261
+ * Target candle *body* width in logical px for the INITIAL framing. Larger →
262
+ * more zoomed in (fewer candles); smaller → more zoomed out. Only affects
263
+ * first load / reset; an explicit `visibleRange` takes precedence. Omit for
264
+ * the default (~80 candles). Useful to keep candles a consistent size across
265
+ * devices of different widths.
266
+ */
267
+ defaultCandleWidth?: number;
207
268
  theme?: VroomTheme;
208
269
  /** RSI indicator (pane below the candles). Omit/disable to hide it. */
209
270
  rsi?: RSIConfig;
@@ -213,6 +274,8 @@ type VroomChartCoreProps = {
213
274
  movingAverages?: MovingAverageOverlay[];
214
275
  /** VWAP overlay (session anchor, configurable reset). */
215
276
  vwap?: VWAPConfig;
277
+ /** Resting-order / order-book liquidity bands drawn behind the candles. */
278
+ liquidity?: LiquidityConfig;
216
279
  /**
217
280
  * Pixels the crosshair dot / horizontal line sit *above* the touch point so
218
281
  * they aren't hidden under the thumb. The vertical line stays centered on the