react-native-vroom-chart 0.2.0 → 0.4.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/cpp/VroomChartHostObject.cpp +38 -1
- package/cpp/_core_include/vroom/vroom_chart.h +86 -11
- package/cpp/_core_src/candles.cpp +32 -5
- package/cpp/_core_src/candles.h +8 -1
- package/cpp/_core_src/chart.cpp +21 -3
- package/cpp/_core_src/chart.h +35 -4
- package/cpp/_core_src/chart_facade.cpp +157 -2
- package/cpp/_core_src/drawings.cpp +313 -15
- package/cpp/_core_src/drawings.h +23 -0
- package/cpp/_core_src/ma_overlay.cpp +9 -1
- package/cpp/_core_src/ma_overlay.h +2 -1
- package/cpp/_core_src/theme.cpp +2 -0
- package/lib/index.d.mts +114 -13
- package/lib/index.d.ts +114 -13
- package/lib/index.js +66 -3
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +66 -3
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +69 -0
- package/src/index.ts +1 -0
- package/src/jsi.d.ts +4 -0
- package/src/theme.ts +2 -0
- package/src/types.ts +1 -0
- package/src/useChartCore.ts +4 -0
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
#include "include/core/SkCanvas.h"
|
|
6
6
|
#include "include/core/SkColor.h"
|
|
7
7
|
#include "include/core/SkPaint.h"
|
|
8
|
+
#include "include/core/SkPath.h"
|
|
9
|
+
#include "include/core/SkPathBuilder.h"
|
|
8
10
|
#include "include/core/SkPoint.h"
|
|
9
11
|
#include "include/core/SkRect.h"
|
|
10
12
|
#pragma clang diagnostic pop
|
|
11
13
|
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <array>
|
|
16
|
+
#include <cmath>
|
|
17
|
+
|
|
12
18
|
#include "chart.h"
|
|
13
19
|
|
|
14
20
|
namespace vroom::drawings {
|
|
@@ -31,18 +37,176 @@ SkPoint to_px(const VroomChart& chart, const Layout& lay, const PriceBounds& bou
|
|
|
31
37
|
return SkPoint{x, y};
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
// The four corners of the box with opposite corners `a` and `b`, in the fixed
|
|
41
|
+
// order used by hit-test `part` indices: a, (b.x,a.y), b, (a.x,b.y). Corner k's
|
|
42
|
+
// diagonal is corner (k+2)%4.
|
|
43
|
+
std::array<SkPoint, 4> box_corners(SkPoint a, SkPoint b) {
|
|
44
|
+
return {SkPoint{a.fX, a.fY}, SkPoint{b.fX, a.fY}, SkPoint{b.fX, b.fY},
|
|
45
|
+
SkPoint{a.fX, b.fY}};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The normalized (L,T,R,B) rectangle spanning opposite corners `a` and `b`.
|
|
49
|
+
SkRect box_rect(SkPoint a, SkPoint b) {
|
|
50
|
+
return SkRect::MakeLTRB(std::min(a.fX, b.fX), std::min(a.fY, b.fY),
|
|
51
|
+
std::max(a.fX, b.fX), std::max(a.fY, b.fY));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Strokes the box outline and paints a faint fill (~10% of the border alpha) of
|
|
55
|
+
// the same color, spanning opposite corners `a` and `b`.
|
|
56
|
+
void draw_box(SkCanvas* canvas, SkPoint a, SkPoint b, SkColor color, float width) {
|
|
57
|
+
const SkRect r = box_rect(a, b);
|
|
58
|
+
|
|
59
|
+
SkPaint fill;
|
|
60
|
+
fill.setAntiAlias(true);
|
|
61
|
+
fill.setStyle(SkPaint::kFill_Style);
|
|
62
|
+
// Faint fill: 10% of the border's alpha, same RGB.
|
|
63
|
+
const U8CPU fill_alpha = static_cast<U8CPU>(SkColorGetA(color) * 0.1f);
|
|
64
|
+
fill.setColor(SkColorSetA(color, fill_alpha));
|
|
65
|
+
canvas->drawRect(r, fill);
|
|
66
|
+
|
|
67
|
+
SkPaint border;
|
|
68
|
+
border.setAntiAlias(true);
|
|
69
|
+
border.setColor(color);
|
|
70
|
+
border.setStyle(SkPaint::kStroke_Style);
|
|
71
|
+
border.setStrokeWidth(width > 0.f ? width : 2.f);
|
|
72
|
+
canvas->drawRect(r, border);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Strokes a freehand path through `pts` (data space). The polyline is smoothed
|
|
76
|
+
// with quadratic curves through segment midpoints — each captured point becomes
|
|
77
|
+
// a control point, so the stroke reads as one organic curve instead of a chain
|
|
78
|
+
// of visible corners. Round caps/joins give it a pen-like feel.
|
|
79
|
+
void draw_path(SkCanvas* canvas, const VroomChart& chart, const Layout& lay,
|
|
80
|
+
const PriceBounds& bounds, int64_t window_ms,
|
|
81
|
+
const std::vector<VroomDrawPoint>& pts, SkColor color,
|
|
82
|
+
float width) {
|
|
83
|
+
if (pts.empty()) return;
|
|
84
|
+
const float w = width > 0.f ? width : 2.f;
|
|
85
|
+
|
|
86
|
+
SkPaint paint;
|
|
87
|
+
paint.setAntiAlias(true);
|
|
88
|
+
paint.setColor(color);
|
|
89
|
+
|
|
90
|
+
// A stroke that's still a single sample renders as a dot, so pressing down
|
|
91
|
+
// gives immediate feedback before the pointer moves.
|
|
92
|
+
if (pts.size() == 1) {
|
|
93
|
+
const SkPoint p = to_px(chart, lay, bounds, window_ms, pts[0]);
|
|
94
|
+
paint.setStyle(SkPaint::kFill_Style);
|
|
95
|
+
canvas->drawCircle(p.fX, p.fY, w * 0.5f, paint);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
SkPathBuilder builder;
|
|
100
|
+
builder.moveTo(to_px(chart, lay, bounds, window_ms, pts[0]));
|
|
101
|
+
// Curve through every interior point, ending each quad at the midpoint of
|
|
102
|
+
// the next segment; finish with a straight run to the final sample.
|
|
103
|
+
for (size_t i = 1; i + 1 < pts.size(); ++i) {
|
|
104
|
+
const SkPoint cur = to_px(chart, lay, bounds, window_ms, pts[i]);
|
|
105
|
+
const SkPoint next = to_px(chart, lay, bounds, window_ms, pts[i + 1]);
|
|
106
|
+
builder.quadTo(cur, SkPoint{(cur.fX + next.fX) * 0.5f,
|
|
107
|
+
(cur.fY + next.fY) * 0.5f});
|
|
108
|
+
}
|
|
109
|
+
builder.lineTo(to_px(chart, lay, bounds, window_ms, pts.back()));
|
|
110
|
+
|
|
111
|
+
paint.setStyle(SkPaint::kStroke_Style);
|
|
112
|
+
paint.setStrokeWidth(w);
|
|
113
|
+
paint.setStrokeCap(SkPaint::kRound_Cap);
|
|
114
|
+
paint.setStrokeJoin(SkPaint::kRound_Join);
|
|
115
|
+
canvas->drawPath(builder.detach(), paint);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
void draw_node(SkCanvas* canvas, SkPoint pt, float scale = 1.f) {
|
|
35
119
|
SkPaint fill;
|
|
36
120
|
fill.setAntiAlias(true);
|
|
37
121
|
fill.setColor(kNodeFill);
|
|
38
|
-
canvas->drawCircle(pt.fX, pt.fY, kNodeFillRadius, fill);
|
|
122
|
+
canvas->drawCircle(pt.fX, pt.fY, kNodeFillRadius * scale, fill);
|
|
39
123
|
|
|
40
124
|
SkPaint border;
|
|
41
125
|
border.setAntiAlias(true);
|
|
42
126
|
border.setColor(kNodeBorder);
|
|
43
127
|
border.setStyle(SkPaint::kStroke_Style);
|
|
44
128
|
border.setStrokeWidth(kNodeBorderWidth);
|
|
45
|
-
canvas->drawCircle(pt.fX, pt.fY, kNodeRingRadius, border);
|
|
129
|
+
canvas->drawCircle(pt.fX, pt.fY, kNodeRingRadius * scale, border);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Clamped projection parameter (0..1) of point (px,py) onto segment
|
|
133
|
+
// (ax,ay)-(bx,by): 0 at A, 1 at B.
|
|
134
|
+
float segment_t(float px, float py, float ax, float ay, float bx, float by) {
|
|
135
|
+
const float dx = bx - ax;
|
|
136
|
+
const float dy = by - ay;
|
|
137
|
+
const float len2 = dx * dx + dy * dy;
|
|
138
|
+
const float t = len2 > 0.f ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0.f;
|
|
139
|
+
return std::clamp(t, 0.f, 1.f);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Shortest distance from point (px,py) to segment (ax,ay)-(bx,by), in px.
|
|
143
|
+
float dist_to_segment(float px, float py, float ax, float ay, float bx, float by) {
|
|
144
|
+
const float t = segment_t(px, py, ax, ay, bx, by);
|
|
145
|
+
const float cx = ax + t * (bx - ax);
|
|
146
|
+
const float cy = ay + t * (by - ay);
|
|
147
|
+
const float ex = px - cx;
|
|
148
|
+
const float ey = py - cy;
|
|
149
|
+
return std::sqrt(ex * ex + ey * ey);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// True when (px,py) is inside the box (the grabbable fill area).
|
|
153
|
+
bool point_in_rect(float px, float py, SkPoint a, SkPoint b) {
|
|
154
|
+
const SkRect r = box_rect(a, b);
|
|
155
|
+
return px >= r.fLeft && px <= r.fRight && py >= r.fTop && py <= r.fBottom;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Shortest distance from (px,py) to the freehand polyline through `pts`, in px.
|
|
159
|
+
// Cheap bounding-box reject first (a stroke can hold many points and most
|
|
160
|
+
// hit-tests miss), then distance-to-segment across consecutive samples — the
|
|
161
|
+
// same two-stage approach Excalidraw uses for its freedraw elements. Returns a
|
|
162
|
+
// value greater than `tol` on a clear miss.
|
|
163
|
+
float dist_to_path(float px, float py, const VroomChart& chart, const Layout& lay,
|
|
164
|
+
const PriceBounds& bounds, int64_t window_ms,
|
|
165
|
+
const std::vector<VroomDrawPoint>& pts, float tol) {
|
|
166
|
+
const float kMiss = tol + 1.f;
|
|
167
|
+
if (pts.size() < 2) return kMiss;
|
|
168
|
+
|
|
169
|
+
float min_x = 0.f, max_x = 0.f, min_y = 0.f, max_y = 0.f;
|
|
170
|
+
for (size_t i = 0; i < pts.size(); ++i) {
|
|
171
|
+
const SkPoint p = to_px(chart, lay, bounds, window_ms, pts[i]);
|
|
172
|
+
if (i == 0) {
|
|
173
|
+
min_x = max_x = p.fX;
|
|
174
|
+
min_y = max_y = p.fY;
|
|
175
|
+
} else {
|
|
176
|
+
min_x = std::min(min_x, p.fX);
|
|
177
|
+
max_x = std::max(max_x, p.fX);
|
|
178
|
+
min_y = std::min(min_y, p.fY);
|
|
179
|
+
max_y = std::max(max_y, p.fY);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (px < min_x - tol || px > max_x + tol || py < min_y - tol ||
|
|
183
|
+
py > max_y + tol) {
|
|
184
|
+
return kMiss;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
float best = kMiss;
|
|
188
|
+
SkPoint prev = to_px(chart, lay, bounds, window_ms, pts[0]);
|
|
189
|
+
for (size_t i = 1; i < pts.size(); ++i) {
|
|
190
|
+
const SkPoint cur = to_px(chart, lay, bounds, window_ms, pts[i]);
|
|
191
|
+
best = std::min(best,
|
|
192
|
+
dist_to_segment(px, py, prev.fX, prev.fY, cur.fX, cur.fY));
|
|
193
|
+
prev = cur;
|
|
194
|
+
}
|
|
195
|
+
return best;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Shortest distance from (px,py) to the box's four edges, in px (0 when on an
|
|
199
|
+
// edge). Used so the border is grabbable even outside the fill's tolerance.
|
|
200
|
+
float dist_to_rect_edges(float px, float py, SkPoint a, SkPoint b) {
|
|
201
|
+
const auto c = box_corners(a, b);
|
|
202
|
+
float best = dist_to_segment(px, py, c[0].fX, c[0].fY, c[1].fX, c[1].fY);
|
|
203
|
+
for (int i = 1; i < 4; ++i) {
|
|
204
|
+
const SkPoint& p0 = c[i];
|
|
205
|
+
const SkPoint& p1 = c[(i + 1) % 4];
|
|
206
|
+
best = std::min(best,
|
|
207
|
+
dist_to_segment(px, py, p0.fX, p0.fY, p1.fX, p1.fY));
|
|
208
|
+
}
|
|
209
|
+
return best;
|
|
46
210
|
}
|
|
47
211
|
} // namespace
|
|
48
212
|
|
|
@@ -62,9 +226,18 @@ void draw(SkCanvas* canvas,
|
|
|
62
226
|
if (!chart.drawings.empty()) {
|
|
63
227
|
canvas->save();
|
|
64
228
|
canvas->clipRect(clip);
|
|
65
|
-
for (const
|
|
229
|
+
for (const auto& d : chart.drawings) {
|
|
230
|
+
if (d.kind == 2) {
|
|
231
|
+
draw_path(canvas, chart, lay, bounds, window_ms, d.points,
|
|
232
|
+
static_cast<SkColor>(d.color), d.width);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
66
235
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
67
236
|
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
237
|
+
if (d.kind == 1) {
|
|
238
|
+
draw_box(canvas, a, b, static_cast<SkColor>(d.color), d.width);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
68
241
|
SkPaint line;
|
|
69
242
|
line.setAntiAlias(true);
|
|
70
243
|
line.setColor(static_cast<SkColor>(d.color));
|
|
@@ -75,34 +248,159 @@ void draw(SkCanvas* canvas,
|
|
|
75
248
|
canvas->restore();
|
|
76
249
|
}
|
|
77
250
|
|
|
251
|
+
// 1b. Handles on the selected committed drawing (unclipped, like the draft's
|
|
252
|
+
// dots). The grabbed endpoint renders 50% larger.
|
|
253
|
+
if (chart.selected_drawing >= 0 &&
|
|
254
|
+
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size()) {
|
|
255
|
+
const auto& d = chart.drawings[chart.selected_drawing];
|
|
256
|
+
const SkPoint sa = to_px(chart, lay, bounds, window_ms, d.a);
|
|
257
|
+
const SkPoint sb = to_px(chart, lay, bounds, window_ms, d.b);
|
|
258
|
+
if (d.kind == 2) {
|
|
259
|
+
// Pencil: anchors on the first/last point are a visual cue that the
|
|
260
|
+
// stroke is movable — they aren't grab handles, so never enlarged.
|
|
261
|
+
draw_node(canvas, sa);
|
|
262
|
+
draw_node(canvas, sb);
|
|
263
|
+
} else if (d.kind == 1) {
|
|
264
|
+
// Four corner handles. The gesture layer stores the grabbed corner
|
|
265
|
+
// as endpoint `a` (its diagonal as `b`), so corner 0 is the active
|
|
266
|
+
// one while dragging.
|
|
267
|
+
const auto corners = box_corners(sa, sb);
|
|
268
|
+
for (int i = 0; i < 4; ++i) {
|
|
269
|
+
draw_node(canvas, corners[i],
|
|
270
|
+
(chart.grabbed_endpoint == 0 && i == 0) ? 1.5f : 1.f);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
draw_node(canvas, sa, chart.grabbed_endpoint == 0 ? 1.5f : 1.f);
|
|
274
|
+
draw_node(canvas, sb, chart.grabbed_endpoint == 1 ? 1.5f : 1.f);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
78
278
|
if (!chart.draft_active) return;
|
|
79
279
|
|
|
280
|
+
// 1c. Freehand stroke in progress: draw the live path, clipped like the
|
|
281
|
+
// committed shapes. It grows a point at a time via append_draft_point.
|
|
282
|
+
if (chart.draft_kind == 2) {
|
|
283
|
+
if (chart.draft_points.empty()) return;
|
|
284
|
+
canvas->save();
|
|
285
|
+
canvas->clipRect(clip);
|
|
286
|
+
draw_path(canvas, chart, lay, bounds, window_ms, chart.draft_points,
|
|
287
|
+
static_cast<SkColor>(chart.draft_color), chart.draft_width);
|
|
288
|
+
canvas->restore();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
80
292
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, chart.draft_a);
|
|
81
293
|
const bool has_b = chart.draft_has_b;
|
|
82
294
|
const SkPoint b =
|
|
83
295
|
has_b ? to_px(chart, lay, bounds, window_ms, chart.draft_b) : a;
|
|
84
296
|
|
|
85
|
-
// 2.
|
|
86
|
-
//
|
|
87
|
-
//
|
|
297
|
+
// 2. Live preview (A->B), clipped to the candle area. Only while the second
|
|
298
|
+
// point is still being placed (draft_guide); once committed, the solid
|
|
299
|
+
// shape comes from chart.drawings instead. A box previews as a rectangle;
|
|
300
|
+
// a line as a guideline.
|
|
88
301
|
if (chart.draft_guide && has_b) {
|
|
89
302
|
canvas->save();
|
|
90
303
|
canvas->clipRect(clip);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
304
|
+
if (chart.draft_kind == 1) {
|
|
305
|
+
draw_box(canvas, a, b, static_cast<SkColor>(chart.draft_color),
|
|
306
|
+
chart.draft_width);
|
|
307
|
+
} else {
|
|
308
|
+
SkPaint guide;
|
|
309
|
+
guide.setAntiAlias(true);
|
|
310
|
+
guide.setColor(static_cast<SkColor>(chart.draft_color));
|
|
311
|
+
guide.setStyle(SkPaint::kStroke_Style);
|
|
312
|
+
guide.setStrokeWidth(chart.draft_width > 0.f ? chart.draft_width : 2.f);
|
|
313
|
+
canvas->drawLine(a, b, guide);
|
|
314
|
+
}
|
|
97
315
|
canvas->restore();
|
|
98
316
|
}
|
|
99
317
|
|
|
100
318
|
// 3. Node dots on top (not clipped, so an edge dot still renders fully).
|
|
101
319
|
// While guiding (placing the second point) only the anchor dot shows; the
|
|
102
|
-
// moving end is conveyed by the
|
|
103
|
-
// endpoints show dots.
|
|
320
|
+
// moving end is conveyed by the preview shape. Once selected (committed)
|
|
321
|
+
// both endpoints show dots.
|
|
104
322
|
draw_node(canvas, a);
|
|
105
323
|
if (has_b && !chart.draft_guide) draw_node(canvas, b);
|
|
106
324
|
}
|
|
107
325
|
|
|
326
|
+
HitResult hit_test(const VroomChart& chart,
|
|
327
|
+
const Layout& lay,
|
|
328
|
+
const PriceBounds& bounds,
|
|
329
|
+
int64_t window_ms,
|
|
330
|
+
float x,
|
|
331
|
+
float y) {
|
|
332
|
+
HitResult miss{-1, -1, 0.f};
|
|
333
|
+
if (window_ms <= 0 || chart.drawings.empty()) return miss;
|
|
334
|
+
|
|
335
|
+
// Grab-priority: if a drawing is selected, its (visible) handles win first.
|
|
336
|
+
constexpr float kHandleHit = kNodeRingRadius + 6.f;
|
|
337
|
+
if (chart.selected_drawing >= 0 &&
|
|
338
|
+
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size()) {
|
|
339
|
+
const auto& d = chart.drawings[chart.selected_drawing];
|
|
340
|
+
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
341
|
+
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
342
|
+
if (d.kind == 2) {
|
|
343
|
+
// Pencil has no grab handles — its anchors translate like any other
|
|
344
|
+
// part of the stroke, so fall through to the body pass.
|
|
345
|
+
} else if (d.kind == 1) {
|
|
346
|
+
// Box: four corner handles (part 0..3).
|
|
347
|
+
const auto corners = box_corners(a, b);
|
|
348
|
+
for (int c = 0; c < 4; ++c) {
|
|
349
|
+
if (std::hypot(x - corners[c].fX, y - corners[c].fY) <= kHandleHit)
|
|
350
|
+
return HitResult{chart.selected_drawing, c, 0.f};
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
if (std::hypot(x - a.fX, y - a.fY) <= kHandleHit)
|
|
354
|
+
return HitResult{chart.selected_drawing, 0, 0.f};
|
|
355
|
+
if (std::hypot(x - b.fX, y - b.fY) <= kHandleHit)
|
|
356
|
+
return HitResult{chart.selected_drawing, 1, 1.f};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Otherwise the nearest drawing body within tolerance (topmost = last drawn).
|
|
361
|
+
// Line body is distance-to-segment (part 2); box body is the interior (dist
|
|
362
|
+
// 0) or a nearby edge (part 4).
|
|
363
|
+
constexpr float kBodyHit = 6.f;
|
|
364
|
+
float best = kBodyHit;
|
|
365
|
+
int32_t best_i = -1;
|
|
366
|
+
int32_t best_part = 2;
|
|
367
|
+
float best_t = 0.f;
|
|
368
|
+
for (size_t i = 0; i < chart.drawings.size(); ++i) {
|
|
369
|
+
const auto& d = chart.drawings[i];
|
|
370
|
+
if (d.kind == 2) {
|
|
371
|
+
const float dist = dist_to_path(x, y, chart, lay, bounds, window_ms,
|
|
372
|
+
d.points, kBodyHit);
|
|
373
|
+
if (dist <= best) { // <= so later (topmost) drawings win ties
|
|
374
|
+
best = dist;
|
|
375
|
+
best_i = static_cast<int32_t>(i);
|
|
376
|
+
best_part = 5;
|
|
377
|
+
best_t = 0.f;
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
382
|
+
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
383
|
+
if (d.kind == 1) {
|
|
384
|
+
const float dist = point_in_rect(x, y, a, b)
|
|
385
|
+
? 0.f
|
|
386
|
+
: dist_to_rect_edges(x, y, a, b);
|
|
387
|
+
if (dist <= best) { // <= so later (topmost) drawings win ties
|
|
388
|
+
best = dist;
|
|
389
|
+
best_i = static_cast<int32_t>(i);
|
|
390
|
+
best_part = 4;
|
|
391
|
+
best_t = 0.f;
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
const float dist = dist_to_segment(x, y, a.fX, a.fY, b.fX, b.fY);
|
|
395
|
+
if (dist <= best) { // <= so later (topmost) drawings win ties
|
|
396
|
+
best = dist;
|
|
397
|
+
best_i = static_cast<int32_t>(i);
|
|
398
|
+
best_part = 2;
|
|
399
|
+
best_t = segment_t(x, y, a.fX, a.fY, b.fX, b.fY);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return best_i >= 0 ? HitResult{best_i, best_part, best_t} : miss;
|
|
404
|
+
}
|
|
405
|
+
|
|
108
406
|
} // namespace vroom::drawings
|
package/cpp/_core_src/drawings.h
CHANGED
|
@@ -31,4 +31,27 @@ void draw(SkCanvas* canvas,
|
|
|
31
31
|
float candle_right,
|
|
32
32
|
float candle_area_h);
|
|
33
33
|
|
|
34
|
+
// Result of a hit-test against the committed drawings. `index` is the drawing
|
|
35
|
+
// index (or -1 for a miss). `part` depends on the drawing kind:
|
|
36
|
+
// * line — 0 (endpoint A), 1 (endpoint B), or 2 (line body).
|
|
37
|
+
// * box — 0..3 (the four corners, in the order returned by box_corners:
|
|
38
|
+
// a, (b.x,a.y), b, (a.x,b.y)) or 4 (box body: interior or edges).
|
|
39
|
+
// * pencil — always 5 (stroke body). A freehand stroke has no grab handles:
|
|
40
|
+
// its end anchors are a visual cue only and translate the whole
|
|
41
|
+
// path like any other part of it.
|
|
42
|
+
// Corner/endpoint hits are only reported for the currently selected drawing
|
|
43
|
+
// (whose handles are visible).
|
|
44
|
+
struct HitResult {
|
|
45
|
+
int32_t index;
|
|
46
|
+
int32_t part;
|
|
47
|
+
float t; // 0..1 grab position along a line segment (A→B); 0 for box/handle hits
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
HitResult hit_test(const VroomChart& chart,
|
|
51
|
+
const vroom::Layout& lay,
|
|
52
|
+
const vroom::PriceBounds& bounds,
|
|
53
|
+
int64_t window_ms,
|
|
54
|
+
float x,
|
|
55
|
+
float y);
|
|
56
|
+
|
|
34
57
|
} // namespace vroom::drawings
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#include "include/core/SkRect.h"
|
|
11
11
|
#pragma clang diagnostic pop
|
|
12
12
|
|
|
13
|
+
#include <algorithm>
|
|
13
14
|
#include <cmath>
|
|
14
15
|
|
|
15
16
|
#include "viewport.h"
|
|
@@ -29,11 +30,14 @@ void draw(SkCanvas* canvas,
|
|
|
29
30
|
float candle_area_h,
|
|
30
31
|
uint32_t color,
|
|
31
32
|
float width,
|
|
32
|
-
const unsigned char* break_before
|
|
33
|
+
const unsigned char* break_before,
|
|
34
|
+
float opacity) {
|
|
33
35
|
if (!canvas || !values_visible || n == 0 || candle_right <= 0.f ||
|
|
34
36
|
candle_area_h <= 0.f) {
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
39
|
+
opacity = std::clamp(opacity, 0.f, 1.f);
|
|
40
|
+
if (opacity <= 0.f) return;
|
|
37
41
|
|
|
38
42
|
// SkPathBuilder (not SkPath's edit methods, removed in newer Skia tips).
|
|
39
43
|
SkPathBuilder path;
|
|
@@ -59,6 +63,10 @@ void draw(SkCanvas* canvas,
|
|
|
59
63
|
SkPaint line;
|
|
60
64
|
line.setAntiAlias(true);
|
|
61
65
|
line.setColor(static_cast<SkColor>(color));
|
|
66
|
+
// Fade the line in during the candle→line morph (multiplies the color alpha).
|
|
67
|
+
if (opacity < 1.f) {
|
|
68
|
+
line.setAlphaf(line.getAlphaf() * opacity);
|
|
69
|
+
}
|
|
62
70
|
line.setStyle(SkPaint::kStroke_Style);
|
|
63
71
|
line.setStrokeWidth(width > 0.f ? width : 1.5f);
|
|
64
72
|
|
package/cpp/_core_src/theme.cpp
CHANGED
|
@@ -22,6 +22,7 @@ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
|
|
|
22
22
|
0x00000000, // WICK_BEAR — transparent sentinel: inherit BEAR fill
|
|
23
23
|
0xff26a69a, // ACCENT_BULL — classic teal-green (price indicator, volume, MACD)
|
|
24
24
|
0xffef5350, // ACCENT_BEAR — classic red
|
|
25
|
+
0xffc9d1d9, // LINE — line-chart close polyline; neutral foreground (AXIS_TEXT tone)
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
@@ -36,6 +37,7 @@ constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
|
36
37
|
0.f, // CANDLE_RADIUS_PX — square by default
|
|
37
38
|
0.f, // WICK_ROUND_CAP — butt caps by default
|
|
38
39
|
0.f, // VOLUME_RADIUS_PX — square by default
|
|
40
|
+
1.5f, // LINE_WIDTH_PX — line-chart polyline stroke width
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
} // namespace
|
package/lib/index.d.mts
CHANGED
|
@@ -92,6 +92,10 @@ type VroomTheme = {
|
|
|
92
92
|
crosshair?: VroomColor;
|
|
93
93
|
/** Crosshair target — the hollow ring/dot at the intersection. */
|
|
94
94
|
crosshairTarget?: VroomColor;
|
|
95
|
+
/** Line-chart-mode close polyline color. Defaults to a neutral foreground. */
|
|
96
|
+
lineColor?: VroomColor;
|
|
97
|
+
/** Line-chart-mode polyline stroke width in px. Defaults to 1.5. */
|
|
98
|
+
lineWidth?: number;
|
|
95
99
|
};
|
|
96
100
|
/** A time window over the candle data, as Unix epoch milliseconds. */
|
|
97
101
|
type VisibleRange = {
|
|
@@ -106,8 +110,15 @@ type VisibleRange = {
|
|
|
106
110
|
* 'draw' — left-clicks place drawing points; panning/zooming are suppressed.
|
|
107
111
|
*/
|
|
108
112
|
type ChartMode = 'pan' | 'draw';
|
|
113
|
+
/**
|
|
114
|
+
* How the price series is drawn.
|
|
115
|
+
* 'candles' — default: candlestick bodies + wicks.
|
|
116
|
+
* 'line' — a single polyline through each candle's close. Volume, indicators,
|
|
117
|
+
* overlays, crosshair, and drawings still render.
|
|
118
|
+
*/
|
|
119
|
+
type ChartType = 'candles' | 'line';
|
|
109
120
|
/** Active drawing tool while in `draw` mode. `null` draws nothing. */
|
|
110
|
-
type DrawTool = null | 'line';
|
|
121
|
+
type DrawTool = null | 'line' | 'box' | 'pencil';
|
|
111
122
|
/** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
|
|
112
123
|
type DrawPoint = {
|
|
113
124
|
/** Anchor time as Unix epoch milliseconds (not snapped to a candle slot). */
|
|
@@ -115,23 +126,81 @@ type DrawPoint = {
|
|
|
115
126
|
/** Anchor price. */
|
|
116
127
|
price: number;
|
|
117
128
|
};
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
* persisted annotations; the chart appends a new one (via `onDrawingComplete`)
|
|
121
|
-
* each time the user finishes drawing. For now only the `'line'` (two-point
|
|
122
|
-
* trendline) type exists.
|
|
123
|
-
*/
|
|
124
|
-
type Drawing = {
|
|
129
|
+
/** Fields shared by every drawing type. */
|
|
130
|
+
type DrawingBase = {
|
|
125
131
|
/** Stable unique id (the chart generates one for drawings it creates). */
|
|
126
132
|
id: string;
|
|
127
|
-
|
|
128
|
-
/** The two endpoints, in data space. */
|
|
129
|
-
points: [DrawPoint, DrawPoint];
|
|
130
|
-
/** Line color (hex string or packed ARGB number). Default solid blue. */
|
|
133
|
+
/** Stroke color (hex string or packed ARGB number). Default solid blue. */
|
|
131
134
|
color?: VroomColor;
|
|
132
135
|
/** Stroke width in px. Default 2. */
|
|
133
136
|
width?: number;
|
|
134
137
|
};
|
|
138
|
+
/** A two-point trendline from `points[0]` to `points[1]`. */
|
|
139
|
+
type LineDrawing = DrawingBase & {
|
|
140
|
+
type: 'line';
|
|
141
|
+
/** The two endpoints, in data space. */
|
|
142
|
+
points: [DrawPoint, DrawPoint];
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* An axis-aligned rectangle whose two opposite corners are `points[0]` and
|
|
146
|
+
* `points[1]` (the other two corners are derived).
|
|
147
|
+
*/
|
|
148
|
+
type BoxDrawing = DrawingBase & {
|
|
149
|
+
type: 'box';
|
|
150
|
+
/** Two opposite corners, in data space. */
|
|
151
|
+
points: [DrawPoint, DrawPoint];
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* A freehand pencil stroke: an open path through `points`, in order. Unlike the
|
|
155
|
+
* other tools a stroke has a variable number of points, and once committed it
|
|
156
|
+
* can only be translated — never reshaped.
|
|
157
|
+
*/
|
|
158
|
+
type PencilDrawing = DrawingBase & {
|
|
159
|
+
type: 'pencil';
|
|
160
|
+
/** The path's points in draw order (at least 2), in data space. */
|
|
161
|
+
points: DrawPoint[];
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* A committed drawing. Pass an array of these via the `drawings` prop to render
|
|
165
|
+
* persisted annotations; the chart appends a new one (via `onDrawingComplete`)
|
|
166
|
+
* each time the user finishes drawing.
|
|
167
|
+
*
|
|
168
|
+
* This is a discriminated union on `type` — narrow on it before reading
|
|
169
|
+
* `points[1]`, since a `'pencil'` stroke has a variable-length array while
|
|
170
|
+
* `'line'` and `'box'` are always exactly two points.
|
|
171
|
+
*/
|
|
172
|
+
type Drawing = LineDrawing | BoxDrawing | PencilDrawing;
|
|
173
|
+
/**
|
|
174
|
+
* Storage adapter for **managed** drawing persistence. Provide it via the
|
|
175
|
+
* `drawingStore` prop and the chart owns the drawings array itself — loading and
|
|
176
|
+
* saving through this adapter instead of you wiring the controlled `drawings`
|
|
177
|
+
* prop + `onDrawing*` callbacks.
|
|
178
|
+
*
|
|
179
|
+
* The adapter is an **opaque string key-value store** — the chart serializes
|
|
180
|
+
* drawings into a **versioned envelope** (`{ v, drawings }`) and hands you the
|
|
181
|
+
* string; you just persist bytes. Because the library owns the schema and
|
|
182
|
+
* migrates old payloads on load, adding drawing tools or persisted fields later
|
|
183
|
+
* never changes this interface — your adapter is written once.
|
|
184
|
+
*
|
|
185
|
+
* `marketId` is the chart's `seriesKey`, so drawings are bucketed per market:
|
|
186
|
+
* they persist across timeframe changes (same key) but not across markets. Both
|
|
187
|
+
* methods may be async (localStorage is sync; AsyncStorage / MMKV / a REST
|
|
188
|
+
* backend are async). The chart debounces `save`. Consumers that only handle a
|
|
189
|
+
* single market can ignore `marketId`.
|
|
190
|
+
*/
|
|
191
|
+
type DrawingStore = {
|
|
192
|
+
/**
|
|
193
|
+
* Return the raw string previously handed to `save` for `marketId`, or
|
|
194
|
+
* `null`/`undefined`/`''` if nothing is stored. Sync or async.
|
|
195
|
+
*/
|
|
196
|
+
load: (marketId: string) => string | null | undefined | Promise<string | null | undefined>;
|
|
197
|
+
/**
|
|
198
|
+
* Persist the opaque `data` string for `marketId`. Sync or async; the chart
|
|
199
|
+
* debounces calls. The string is a versioned envelope owned by the library —
|
|
200
|
+
* store it verbatim, don't parse or reshape it.
|
|
201
|
+
*/
|
|
202
|
+
save: (marketId: string, data: string) => void | Promise<void>;
|
|
203
|
+
};
|
|
135
204
|
/** RSI indicator config. Rendered in a pane below the candles when enabled. */
|
|
136
205
|
type RSIConfig = {
|
|
137
206
|
enabled?: boolean;
|
|
@@ -265,6 +334,18 @@ type VroomChartCoreProps = {
|
|
|
265
334
|
* devices of different widths.
|
|
266
335
|
*/
|
|
267
336
|
defaultCandleWidth?: number;
|
|
337
|
+
/**
|
|
338
|
+
* Price-series render style. `'candles'` (default) draws candlesticks;
|
|
339
|
+
* `'line'` draws a polyline through each candle's close (style it with
|
|
340
|
+
* `theme.lineColor` / `theme.lineWidth`). All other layers are unaffected.
|
|
341
|
+
*/
|
|
342
|
+
chartType?: ChartType;
|
|
343
|
+
/**
|
|
344
|
+
* Duration (ms) of the animated candle↔line transition when `chartType`
|
|
345
|
+
* changes. Default ~300. `0` snaps instantly. Ignored (snaps) when the OS
|
|
346
|
+
* requests reduced motion, which instead uses a plain cross-fade.
|
|
347
|
+
*/
|
|
348
|
+
transitionMs?: number;
|
|
268
349
|
theme?: VroomTheme;
|
|
269
350
|
/** RSI indicator (pane below the candles). Omit/disable to hide it. */
|
|
270
351
|
rsi?: RSIConfig;
|
|
@@ -306,10 +387,30 @@ type VroomChartCoreProps = {
|
|
|
306
387
|
* Committed drawings to render, anchored to data so they track the candles on
|
|
307
388
|
* pan/zoom. This is a controlled prop: append the value the chart hands you in
|
|
308
389
|
* `onDrawingComplete` to persist it.
|
|
390
|
+
*
|
|
391
|
+
* Ignored when `drawingStore` is set (the chart then owns the array itself).
|
|
309
392
|
*/
|
|
310
393
|
drawings?: Drawing[];
|
|
394
|
+
/**
|
|
395
|
+
* Opt into **managed** drawing persistence: the chart owns the drawings array
|
|
396
|
+
* internally and loads/saves it through this adapter, keyed by `seriesKey`.
|
|
397
|
+
* When set, `drawings` and the `onDrawing*` callbacks are ignored. Web only.
|
|
398
|
+
* Requires `seriesKey` — without one, drawings work in-session but aren't saved.
|
|
399
|
+
*/
|
|
400
|
+
drawingStore?: DrawingStore;
|
|
311
401
|
/** Fired with the finished drawing when the user completes one. */
|
|
312
402
|
onDrawingComplete?: (drawing: Drawing) => void;
|
|
403
|
+
/**
|
|
404
|
+
* Fired after the user drags a selected line's endpoint handle. The payload is
|
|
405
|
+
* the same drawing (same `id`) with updated `points`; apply it to your
|
|
406
|
+
* controlled `drawings` state (replace by id). Web only.
|
|
407
|
+
*/
|
|
408
|
+
onDrawingChange?: (drawing: Drawing) => void;
|
|
409
|
+
/**
|
|
410
|
+
* Fired when the user deletes the selected line (Backspace/Delete). Remove the
|
|
411
|
+
* drawing with this `id` from your controlled `drawings` state. Web only.
|
|
412
|
+
*/
|
|
413
|
+
onDrawingDelete?: (id: string) => void;
|
|
313
414
|
/**
|
|
314
415
|
* Fired when the chart wants the mode changed — e.g. it requests `'pan'` after
|
|
315
416
|
* the user clicks away from a just-drawn line. Since `mode` is controlled, the
|
|
@@ -349,4 +450,4 @@ declare global {
|
|
|
349
450
|
*/
|
|
350
451
|
declare function VroomChart(props: VroomChartProps): React.JSX.Element;
|
|
351
452
|
|
|
352
|
-
export { type Candle, type CrosshairEvent, type MACDConfig, type MASource, type MovingAverageOverlay, type RSIConfig, type VWAPConfig, type VisibleRange, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme };
|
|
453
|
+
export { type Candle, type ChartType, type CrosshairEvent, type MACDConfig, type MASource, type MovingAverageOverlay, type RSIConfig, type VWAPConfig, type VisibleRange, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme };
|