react-native-vroom-chart 0.1.2 → 0.1.4
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 +79 -0
- package/cpp/_core_src/chart.cpp +8 -2
- package/cpp/_core_src/chart.h +25 -4
- package/cpp/_core_src/chart_facade.cpp +151 -38
- 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/viewport.cpp +33 -0
- package/cpp/_core_src/viewport.h +24 -0
- package/lib/index.d.mts +84 -2
- package/lib/index.d.ts +84 -2
- package/lib/index.js +4 -2
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +4 -2
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +4 -2
|
@@ -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 {
|
|
@@ -109,6 +130,24 @@ void vroom_chart_set_size(VroomChart* chart, float width_px, float height_px, fl
|
|
|
109
130
|
// ---- Viewport -------------------------------------------------------------
|
|
110
131
|
|
|
111
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
|
+
|
|
112
151
|
void vroom_chart_pan(VroomChart* chart, float dx_px, float dy_px);
|
|
113
152
|
// Directional zoom. scale_x scales the time window around focus_x_px (>1 =
|
|
114
153
|
// narrower window, wider candles); scale_y scales the price range around
|
|
@@ -156,6 +195,15 @@ void vroom_chart_get_axis_metrics(VroomChart* chart,
|
|
|
156
195
|
// ---- Crosshair ------------------------------------------------------------
|
|
157
196
|
|
|
158
197
|
void vroom_chart_set_crosshair(VroomChart* chart, float x_px, float y_px);
|
|
198
|
+
|
|
199
|
+
// Show the crosshair at a data-space position (epoch ms + price) rather than
|
|
200
|
+
// pixels — used to drive one chart's crosshair from another's (cross-chart
|
|
201
|
+
// sync). Converts to pixels via the current visible window and price scale;
|
|
202
|
+
// the vertical line then snaps to the nearest candle center at draw time, so
|
|
203
|
+
// the crosshair lands on the candle for `time_ms`. The horizontal line sits at
|
|
204
|
+
// `price` (clamped to the pane when off-scale).
|
|
205
|
+
void vroom_chart_set_crosshair_data(VroomChart* chart, int64_t time_ms, double price);
|
|
206
|
+
|
|
159
207
|
void vroom_chart_clear_crosshair(VroomChart* chart);
|
|
160
208
|
|
|
161
209
|
// Fills *out with the OHLCV of the candle the crosshair currently snaps to and
|
|
@@ -172,6 +220,10 @@ bool vroom_chart_get_crosshair_candle(VroomChart* chart, VroomCandle* out);
|
|
|
172
220
|
// `candle` is left untouched.
|
|
173
221
|
typedef struct VroomCrosshairInfo {
|
|
174
222
|
int64_t time_ms;
|
|
223
|
+
// Free price under the crosshair's horizontal line — the value drawn on the
|
|
224
|
+
// price badge (not snapped to any candle). Valid whenever the call returns
|
|
225
|
+
// true, including in the future region where `has_candle` is false.
|
|
226
|
+
double price;
|
|
175
227
|
bool has_candle;
|
|
176
228
|
VroomCandle candle;
|
|
177
229
|
} VroomCrosshairInfo;
|
|
@@ -212,6 +264,33 @@ void vroom_chart_set_overlays(VroomChart* chart, const VroomOverlay* overlays,
|
|
|
212
264
|
void vroom_chart_set_vwap(VroomChart* chart, bool enabled, int reset_offset_min,
|
|
213
265
|
uint32_t color, float width);
|
|
214
266
|
|
|
267
|
+
// ---- Drawings (line annotations) ------------------------------------------
|
|
268
|
+
|
|
269
|
+
// Replaces the full set of committed line drawings (data-anchored, so they track
|
|
270
|
+
// the candles on pan/zoom). Pass count 0 to clear. Drawings render on the price
|
|
271
|
+
// pane above the candles/overlays and below the axis labels & crosshair.
|
|
272
|
+
void vroom_chart_set_drawings(VroomChart* chart, const VroomDrawing* drawings,
|
|
273
|
+
size_t count);
|
|
274
|
+
|
|
275
|
+
// Sets the transient in-progress "draft" the drawing tool shows while the user
|
|
276
|
+
// places points. Node A is always shown; when `has_b`, node B is shown too.
|
|
277
|
+
// `guide != 0` also draws the guideline A->B (the live line preview); `guide == 0`
|
|
278
|
+
// draws node dots only (the committed segment already renders via set_drawings).
|
|
279
|
+
// `color`/`width` style the guideline to match the eventual line.
|
|
280
|
+
void vroom_chart_set_draft(VroomChart* chart, int64_t a_time, double a_price,
|
|
281
|
+
bool has_b, int64_t b_time, double b_price,
|
|
282
|
+
bool guide, uint32_t color, float width);
|
|
283
|
+
|
|
284
|
+
// Clears the draft (hides the in-progress node dots / guideline).
|
|
285
|
+
void vroom_chart_clear_draft(VroomChart* chart);
|
|
286
|
+
|
|
287
|
+
// Fills *out with the continuous data coordinate (time_ms, price) at pixel
|
|
288
|
+
// (x_px, y_px) using the free (non-snapped) mapping, and returns true. Returns
|
|
289
|
+
// false (leaving *out untouched) when there are no candles or the viewport is
|
|
290
|
+
// degenerate.
|
|
291
|
+
bool vroom_chart_coord_at(VroomChart* chart, float x_px, float y_px,
|
|
292
|
+
VroomCoord* out);
|
|
293
|
+
|
|
215
294
|
// ---- Rendering ------------------------------------------------------------
|
|
216
295
|
|
|
217
296
|
void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas);
|
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;
|
|
@@ -119,6 +122,24 @@ struct VroomChart {
|
|
|
119
122
|
std::vector<unsigned char> vwap_breaks;
|
|
120
123
|
bool vwap_dirty = true;
|
|
121
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
|
+
|
|
122
143
|
// --- theme --------------------------------------------------------------
|
|
123
144
|
vroom::Theme theme;
|
|
124
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;
|
|
@@ -417,7 +446,7 @@ extern "C" void vroom_chart_resize_indicator_pane(VroomChart* chart, float dy_px
|
|
|
417
446
|
// Preserve candle pixel scale: scale the price range by the price-pane
|
|
418
447
|
// height ratio, anchoring the top (max) price so existing candles stay put
|
|
419
448
|
// and the newly revealed space opens at the bottom.
|
|
420
|
-
if (chart->
|
|
449
|
+
if (chart->price_bounds_manual && old_pane_bottom > 0.f) {
|
|
421
450
|
const float new_pane_bottom = content_h - new_band;
|
|
422
451
|
const double scale = static_cast<double>(new_pane_bottom) /
|
|
423
452
|
static_cast<double>(old_pane_bottom);
|
|
@@ -502,7 +531,8 @@ extern "C" void vroom_chart_zoom(VroomChart* chart, float scale_x, float scale_y
|
|
|
502
531
|
|
|
503
532
|
// --- Y (price) zoom around fy ------------------------------------------
|
|
504
533
|
// scale_y > 1 (vertical pinch out) narrows the price range → taller candles.
|
|
505
|
-
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);
|
|
506
536
|
const float candle_area_h = vroom::price_pane_bottom(chart->layout());
|
|
507
537
|
const double range = chart->price_bounds.max - chart->price_bounds.min;
|
|
508
538
|
if (candle_area_h > 0.f && range > 0.0) {
|
|
@@ -593,6 +623,28 @@ extern "C" void vroom_chart_set_crosshair(VroomChart* chart, float x, float y) {
|
|
|
593
623
|
chart->mark_dirty();
|
|
594
624
|
}
|
|
595
625
|
|
|
626
|
+
extern "C" void vroom_chart_set_crosshair_data(VroomChart* chart,
|
|
627
|
+
int64_t time_ms, double price) {
|
|
628
|
+
if (!chart || chart->candles.empty()) return;
|
|
629
|
+
const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
|
|
630
|
+
if (window_ms <= 0) return;
|
|
631
|
+
const auto lay = chart->layout();
|
|
632
|
+
// Mirror coord_at's bounds so price -> y matches what's on screen.
|
|
633
|
+
const auto range = vroom::visible_indices(
|
|
634
|
+
chart->candles.data(), chart->candles.size(),
|
|
635
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
636
|
+
const size_t n = range.end - range.start;
|
|
637
|
+
const auto bounds =
|
|
638
|
+
chart->price_bounds_manual
|
|
639
|
+
? chart->price_bounds
|
|
640
|
+
: vroom::auto_price_bounds(chart->candles.data() + range.start, n);
|
|
641
|
+
chart->crosshair_active = true;
|
|
642
|
+
chart->crosshair_x_px =
|
|
643
|
+
vroom::x_at_time(lay, chart->visible_start_ms, window_ms, time_ms);
|
|
644
|
+
chart->crosshair_y_px = vroom::price_to_y(lay, bounds, price);
|
|
645
|
+
chart->mark_dirty();
|
|
646
|
+
}
|
|
647
|
+
|
|
596
648
|
extern "C" void vroom_chart_clear_crosshair(VroomChart* chart) {
|
|
597
649
|
if (!chart) return;
|
|
598
650
|
chart->crosshair_active = false;
|
|
@@ -642,11 +694,72 @@ extern "C" bool vroom_chart_get_crosshair_info(VroomChart* chart,
|
|
|
642
694
|
lay, visible, n, chart->candle_duration_ms,
|
|
643
695
|
chart->visible_start_ms, window_ms, chart->crosshair_x_px);
|
|
644
696
|
out->time_ms = snap.time_ms;
|
|
697
|
+
// Free price at the crosshair's horizontal line — same bounds as the price
|
|
698
|
+
// badge (crosshair.cpp) so the reported value equals what's drawn.
|
|
699
|
+
const auto bounds =
|
|
700
|
+
chart->price_bounds_manual
|
|
701
|
+
? chart->price_bounds
|
|
702
|
+
: vroom::auto_price_bounds(visible, n);
|
|
703
|
+
out->price = vroom::y_to_price(lay, bounds, chart->crosshair_y_px);
|
|
645
704
|
out->has_candle = snap.has_candle;
|
|
646
705
|
if (snap.has_candle) out->candle = visible[snap.index];
|
|
647
706
|
return true;
|
|
648
707
|
}
|
|
649
708
|
|
|
709
|
+
// ---- Drawings (line annotations) ------------------------------------------
|
|
710
|
+
|
|
711
|
+
extern "C" void vroom_chart_set_drawings(VroomChart* chart,
|
|
712
|
+
const VroomDrawing* drawings,
|
|
713
|
+
size_t count) {
|
|
714
|
+
if (!chart) return;
|
|
715
|
+
chart->drawings.assign(drawings, drawings + count);
|
|
716
|
+
chart->mark_dirty();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
|
|
720
|
+
double a_price, bool has_b, int64_t b_time,
|
|
721
|
+
double b_price, bool guide, uint32_t color,
|
|
722
|
+
float width) {
|
|
723
|
+
if (!chart) return;
|
|
724
|
+
chart->draft_active = true;
|
|
725
|
+
chart->draft_a = VroomDrawPoint{a_time, a_price};
|
|
726
|
+
chart->draft_has_b = has_b;
|
|
727
|
+
chart->draft_b = VroomDrawPoint{b_time, b_price};
|
|
728
|
+
chart->draft_guide = guide;
|
|
729
|
+
chart->draft_color = color;
|
|
730
|
+
chart->draft_width = width;
|
|
731
|
+
chart->mark_dirty();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
extern "C" void vroom_chart_clear_draft(VroomChart* chart) {
|
|
735
|
+
if (!chart) return;
|
|
736
|
+
if (!chart->draft_active) return;
|
|
737
|
+
chart->draft_active = false;
|
|
738
|
+
chart->mark_dirty();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
extern "C" bool vroom_chart_coord_at(VroomChart* chart, float x_px, float y_px,
|
|
742
|
+
VroomCoord* out) {
|
|
743
|
+
if (!chart || !out || chart->candles.empty()) return false;
|
|
744
|
+
const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
|
|
745
|
+
if (window_ms <= 0) return false;
|
|
746
|
+
const auto lay = chart->layout();
|
|
747
|
+
// Mirror draw_chart's bounds: the frozen manual scale, else the auto-fit
|
|
748
|
+
// over the visible slice — so y_to_price matches what's on screen.
|
|
749
|
+
const auto range = vroom::visible_indices(
|
|
750
|
+
chart->candles.data(), chart->candles.size(),
|
|
751
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
752
|
+
const size_t n = range.end - range.start;
|
|
753
|
+
const auto bounds =
|
|
754
|
+
chart->price_bounds_manual
|
|
755
|
+
? chart->price_bounds
|
|
756
|
+
: vroom::auto_price_bounds(chart->candles.data() + range.start, n);
|
|
757
|
+
out->time_ms =
|
|
758
|
+
vroom::time_at_x(lay, chart->visible_start_ms, window_ms, x_px);
|
|
759
|
+
out->price = vroom::y_to_price(lay, bounds, y_px);
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
|
|
650
763
|
// ---- Indicators -----------------------------------------------------------
|
|
651
764
|
|
|
652
765
|
extern "C" void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#include "drawings.h"
|
|
2
|
+
|
|
3
|
+
#pragma clang diagnostic push
|
|
4
|
+
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
5
|
+
#include "include/core/SkCanvas.h"
|
|
6
|
+
#include "include/core/SkColor.h"
|
|
7
|
+
#include "include/core/SkPaint.h"
|
|
8
|
+
#include "include/core/SkPoint.h"
|
|
9
|
+
#include "include/core/SkRect.h"
|
|
10
|
+
#pragma clang diagnostic pop
|
|
11
|
+
|
|
12
|
+
#include "chart.h"
|
|
13
|
+
|
|
14
|
+
namespace vroom::drawings {
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
// Node-dot styling (fixed for v1, not themed): a 4px black dot with a 2px blue
|
|
18
|
+
// ring around it. kNodeFillRadius is the black core; the blue stroke is centered
|
|
19
|
+
// at kNodeRingRadius so it sits just outside the core.
|
|
20
|
+
constexpr SkColor kNodeFill = 0xff000000; // black core
|
|
21
|
+
constexpr SkColor kNodeBorder = 0xff2962ff; // blue ring
|
|
22
|
+
constexpr float kNodeFillRadius = 2.f; // 4px diameter
|
|
23
|
+
constexpr float kNodeBorderWidth = 2.f;
|
|
24
|
+
constexpr float kNodeRingRadius = 3.f; // ring centered outside the core
|
|
25
|
+
|
|
26
|
+
// Maps a data-space point to pixels in the price pane.
|
|
27
|
+
SkPoint to_px(const VroomChart& chart, const Layout& lay, const PriceBounds& bounds,
|
|
28
|
+
int64_t window_ms, const VroomDrawPoint& p) {
|
|
29
|
+
const float x = vroom::x_at_time(lay, chart.visible_start_ms, window_ms, p.time_ms);
|
|
30
|
+
const float y = vroom::price_to_y(lay, bounds, p.price);
|
|
31
|
+
return SkPoint{x, y};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void draw_node(SkCanvas* canvas, SkPoint pt) {
|
|
35
|
+
SkPaint fill;
|
|
36
|
+
fill.setAntiAlias(true);
|
|
37
|
+
fill.setColor(kNodeFill);
|
|
38
|
+
canvas->drawCircle(pt.fX, pt.fY, kNodeFillRadius, fill);
|
|
39
|
+
|
|
40
|
+
SkPaint border;
|
|
41
|
+
border.setAntiAlias(true);
|
|
42
|
+
border.setColor(kNodeBorder);
|
|
43
|
+
border.setStyle(SkPaint::kStroke_Style);
|
|
44
|
+
border.setStrokeWidth(kNodeBorderWidth);
|
|
45
|
+
canvas->drawCircle(pt.fX, pt.fY, kNodeRingRadius, border);
|
|
46
|
+
}
|
|
47
|
+
} // namespace
|
|
48
|
+
|
|
49
|
+
void draw(SkCanvas* canvas,
|
|
50
|
+
const VroomChart& chart,
|
|
51
|
+
const Layout& lay,
|
|
52
|
+
const PriceBounds& bounds,
|
|
53
|
+
float candle_right,
|
|
54
|
+
float candle_area_h) {
|
|
55
|
+
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
56
|
+
const int64_t window_ms = chart.visible_end_ms - chart.visible_start_ms;
|
|
57
|
+
if (window_ms <= 0) return;
|
|
58
|
+
|
|
59
|
+
const SkRect clip = SkRect::MakeLTRB(0.f, 0.f, candle_right, candle_area_h);
|
|
60
|
+
|
|
61
|
+
// 1. Committed line segments, clipped to the candle area.
|
|
62
|
+
if (!chart.drawings.empty()) {
|
|
63
|
+
canvas->save();
|
|
64
|
+
canvas->clipRect(clip);
|
|
65
|
+
for (const VroomDrawing& d : chart.drawings) {
|
|
66
|
+
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
67
|
+
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
68
|
+
SkPaint line;
|
|
69
|
+
line.setAntiAlias(true);
|
|
70
|
+
line.setColor(static_cast<SkColor>(d.color));
|
|
71
|
+
line.setStyle(SkPaint::kStroke_Style);
|
|
72
|
+
line.setStrokeWidth(d.width > 0.f ? d.width : 2.f);
|
|
73
|
+
canvas->drawLine(a, b, line);
|
|
74
|
+
}
|
|
75
|
+
canvas->restore();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!chart.draft_active) return;
|
|
79
|
+
|
|
80
|
+
const SkPoint a = to_px(chart, lay, bounds, window_ms, chart.draft_a);
|
|
81
|
+
const bool has_b = chart.draft_has_b;
|
|
82
|
+
const SkPoint b =
|
|
83
|
+
has_b ? to_px(chart, lay, bounds, window_ms, chart.draft_b) : a;
|
|
84
|
+
|
|
85
|
+
// 2. Guideline preview (A->B), clipped to the candle area. Only while the
|
|
86
|
+
// second point is still being placed (draft_guide); once committed, the
|
|
87
|
+
// solid segment comes from chart.drawings instead.
|
|
88
|
+
if (chart.draft_guide && has_b) {
|
|
89
|
+
canvas->save();
|
|
90
|
+
canvas->clipRect(clip);
|
|
91
|
+
SkPaint guide;
|
|
92
|
+
guide.setAntiAlias(true);
|
|
93
|
+
guide.setColor(static_cast<SkColor>(chart.draft_color));
|
|
94
|
+
guide.setStyle(SkPaint::kStroke_Style);
|
|
95
|
+
guide.setStrokeWidth(chart.draft_width > 0.f ? chart.draft_width : 2.f);
|
|
96
|
+
canvas->drawLine(a, b, guide);
|
|
97
|
+
canvas->restore();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Node dots on top (not clipped, so an edge dot still renders fully).
|
|
101
|
+
// While guiding (placing the second point) only the anchor dot shows; the
|
|
102
|
+
// moving end is conveyed by the guideline. Once selected (committed) both
|
|
103
|
+
// endpoints show dots.
|
|
104
|
+
draw_node(canvas, a);
|
|
105
|
+
if (has_b && !chart.draft_guide) draw_node(canvas, b);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} // namespace vroom::drawings
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Drawing annotations — user-placed line tools rendered on the price pane.
|
|
2
|
+
//
|
|
3
|
+
// Two kinds of geometry are drawn here:
|
|
4
|
+
// * Committed lines (chart.drawings) — two-point trendlines anchored in data
|
|
5
|
+
// space, so they stay glued to the candles as the user pans/zooms.
|
|
6
|
+
// * The transient "draft" (chart.draft_*) — the in-progress node dots and the
|
|
7
|
+
// live guideline shown while the user is placing a line.
|
|
8
|
+
//
|
|
9
|
+
// Like ma_overlay / crosshair this is its own module so the chart orchestrator
|
|
10
|
+
// stays thin. Drawn after the overlays and before the axis backgrounds.
|
|
11
|
+
|
|
12
|
+
#pragma once
|
|
13
|
+
|
|
14
|
+
#include "viewport.h"
|
|
15
|
+
#include "vroom/vroom_chart.h"
|
|
16
|
+
|
|
17
|
+
class SkCanvas;
|
|
18
|
+
struct VroomChart;
|
|
19
|
+
|
|
20
|
+
namespace vroom::drawings {
|
|
21
|
+
|
|
22
|
+
// Draws the committed line drawings and the in-progress draft. `candle_right` is
|
|
23
|
+
// the x of the y-axis strip and `candle_area_h` the price-pane bottom; geometry
|
|
24
|
+
// is clipped to that rectangle so lines never bleed into the axis strips or the
|
|
25
|
+
// indicator panes below. Node dots are exempt from the clip so a dot placed at
|
|
26
|
+
// the very edge still renders fully.
|
|
27
|
+
void draw(SkCanvas* canvas,
|
|
28
|
+
const VroomChart& chart,
|
|
29
|
+
const vroom::Layout& lay,
|
|
30
|
+
const vroom::PriceBounds& bounds,
|
|
31
|
+
float candle_right,
|
|
32
|
+
float candle_area_h);
|
|
33
|
+
|
|
34
|
+
} // namespace vroom::drawings
|
package/cpp/_core_src/labels.cpp
CHANGED
|
@@ -318,7 +318,7 @@ void recompute_axis_width(VroomChart& chart) {
|
|
|
318
318
|
return;
|
|
319
319
|
}
|
|
320
320
|
double hi, lo;
|
|
321
|
-
if (chart.
|
|
321
|
+
if (chart.price_bounds_manual) {
|
|
322
322
|
hi = chart.price_bounds.max;
|
|
323
323
|
lo = chart.price_bounds.min;
|
|
324
324
|
} else if (!chart.candles.empty()) {
|
|
@@ -35,6 +35,31 @@ float candle_center_x(const Layout& layout,
|
|
|
35
35
|
return static_cast<float>(static_cast<double>(usable) * frac);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
int64_t time_at_x(const Layout& layout,
|
|
39
|
+
int64_t visible_start_ms,
|
|
40
|
+
int64_t window_ms,
|
|
41
|
+
float x_px) {
|
|
42
|
+
const float usable =
|
|
43
|
+
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
44
|
+
if (usable <= 0.f || window_ms <= 0) return visible_start_ms;
|
|
45
|
+
const double frac = static_cast<double>(x_px) / static_cast<double>(usable);
|
|
46
|
+
return visible_start_ms +
|
|
47
|
+
static_cast<int64_t>(std::llround(frac * static_cast<double>(window_ms)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
float x_at_time(const Layout& layout,
|
|
51
|
+
int64_t visible_start_ms,
|
|
52
|
+
int64_t window_ms,
|
|
53
|
+
int64_t time_ms) {
|
|
54
|
+
if (window_ms <= 0) return 0.f;
|
|
55
|
+
const float usable =
|
|
56
|
+
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
57
|
+
const double frac =
|
|
58
|
+
static_cast<double>(time_ms - visible_start_ms) /
|
|
59
|
+
static_cast<double>(window_ms);
|
|
60
|
+
return static_cast<float>(static_cast<double>(usable) * frac);
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
size_t snap_index_to_candle(const Layout& layout,
|
|
39
64
|
const ::VroomCandle* candles,
|
|
40
65
|
size_t count,
|
|
@@ -180,6 +205,14 @@ PriceBounds price_bounds(const ::VroomCandle* candles, size_t count) {
|
|
|
180
205
|
return b;
|
|
181
206
|
}
|
|
182
207
|
|
|
208
|
+
PriceBounds auto_price_bounds(const ::VroomCandle* candles, size_t count) {
|
|
209
|
+
PriceBounds b = price_bounds(candles, count);
|
|
210
|
+
if (count == 0) return b;
|
|
211
|
+
const double mid = (b.min + b.max) * 0.5;
|
|
212
|
+
const double half = (b.max - b.min) * 0.5 * kAutoYZoom;
|
|
213
|
+
return {mid - half, mid + half};
|
|
214
|
+
}
|
|
215
|
+
|
|
183
216
|
float price_to_y(const Layout& layout,
|
|
184
217
|
const PriceBounds& bounds,
|
|
185
218
|
double price) {
|