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 +21 -0
- package/cpp/VroomChartHostObject.cpp +39 -1
- package/cpp/_core_include/vroom/vroom_chart.h +53 -0
- package/cpp/_core_src/candles.cpp +43 -5
- package/cpp/_core_src/chart.cpp +7 -0
- package/cpp/_core_src/chart.h +10 -0
- package/cpp/_core_src/chart_facade.cpp +79 -2
- package/cpp/_core_src/liquidity.cpp +112 -0
- package/cpp/_core_src/liquidity.h +35 -0
- package/cpp/_core_src/theme.cpp +3 -0
- package/cpp/_core_src/volume.cpp +15 -3
- package/lib/index.d.mts +88 -4
- package/lib/index.d.ts +88 -4
- package/lib/index.js +36 -5
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +36 -5
- package/lib/index.mjs.map +1 -1
- package/package.json +10 -11
- package/src/VroomChart.tsx +6 -2
- package/src/jsi.d.ts +11 -0
- package/src/theme.ts +29 -6
- package/src/useChartCore.ts +20 -1
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(
|
|
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
|
|
62
|
-
// border
|
|
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(
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -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);
|
package/cpp/_core_src/chart.h
CHANGED
|
@@ -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
|
|
58
|
-
//
|
|
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
|
package/cpp/_core_src/theme.cpp
CHANGED
|
@@ -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
|
package/cpp/_core_src/volume.cpp
CHANGED
|
@@ -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
|
-
|
|
65
|
-
SkRect::MakeXYWH(cx - half_body, candle_area_h - h, body_w, h)
|
|
66
|
-
|
|
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
|
|