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.
- package/cpp/_core_include/vroom/vroom_chart.h +85 -0
- package/cpp/_core_src/candles.cpp +33 -7
- package/cpp/_core_src/chart.cpp +8 -2
- package/cpp/_core_src/chart.h +31 -4
- package/cpp/_core_src/chart_facade.cpp +217 -37
- package/cpp/_core_src/drawings.cpp +108 -0
- package/cpp/_core_src/drawings.h +34 -0
- package/cpp/_core_src/labels.cpp +1 -1
- package/cpp/_core_src/macd_pane.cpp +6 -3
- package/cpp/_core_src/price_indicator.cpp +1 -1
- package/cpp/_core_src/rsi_pane.cpp +6 -1
- package/cpp/_core_src/theme.cpp +6 -0
- package/cpp/_core_src/viewport.cpp +33 -0
- package/cpp/_core_src/viewport.h +24 -0
- package/cpp/_core_src/volume.cpp +2 -2
- package/lib/index.d.mts +75 -2
- package/lib/index.d.ts +75 -2
- package/lib/index.js +13 -1
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +13 -1
- package/lib/index.mjs.map +1 -1
- package/package.json +11 -10
- package/src/theme.ts +6 -0
- package/LICENSE +0 -21
|
@@ -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(
|
|
44
|
+
bull_paint.setColor(fill_bull);
|
|
34
45
|
|
|
35
46
|
SkPaint bear_paint;
|
|
36
47
|
bear_paint.setAntiAlias(true);
|
|
37
|
-
bear_paint.setColor(
|
|
48
|
+
bear_paint.setColor(fill_bear);
|
|
38
49
|
|
|
39
50
|
SkPaint wick_bull;
|
|
40
51
|
wick_bull.setAntiAlias(true);
|
|
41
|
-
wick_bull.setColor(
|
|
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(
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -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 =
|
|
120
|
+
const auto bounds = price_bounds_manual
|
|
120
121
|
? price_bounds
|
|
121
|
-
: vroom::
|
|
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.
|
package/cpp/_core_src/chart.h
CHANGED
|
@@ -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 ~
|
|
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
|
-
//
|
|
59
|
-
//
|
|
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
|
|
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
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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->
|
|
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
|
-
|
|
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->
|
|
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,
|