react-native-vroom-chart 0.10.0 → 0.11.1
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 +27 -0
- package/cpp/_core_src/chart.cpp +11 -3
- package/cpp/_core_src/chart.h +9 -0
- package/cpp/_core_src/chart_facade.cpp +27 -0
- package/cpp/_core_src/crosshair.cpp +5 -3
- package/cpp/_core_src/drawing_bounds.h +70 -0
- package/cpp/_core_src/drawings.cpp +60 -11
- package/cpp/_core_src/drawings.h +15 -0
- package/cpp/_core_src/labels.cpp +31 -7
- package/cpp/_core_src/ma_overlay.h +5 -0
- package/cpp/_core_src/price_format.cpp +87 -0
- package/cpp/_core_src/price_format.h +69 -0
- package/cpp/_core_src/price_indicator.cpp +5 -3
- package/cpp/_core_src/price_lines.cpp +7 -5
- package/cpp/_core_src/ticks.cpp +0 -14
- package/cpp/_core_src/ticks.h +2 -8
- package/cpp/_core_src/tip_anchor.h +49 -0
- package/lib/index.d.mts +72 -7
- package/lib/index.d.ts +72 -7
- package/package.json +2 -1
- package/react-native-vroom-chart.podspec +21 -7
|
@@ -187,6 +187,13 @@ typedef struct VroomDrawing {
|
|
|
187
187
|
int32_t kind; // 0 = line, 1 = box, 2 = pencil, 3 = path
|
|
188
188
|
const VroomDrawPoint* points; // pencil/path points (kind 2/3), else null
|
|
189
189
|
int32_t point_count; // number of `points`, else 0
|
|
190
|
+
// Box (kind 1) interior fill, 0xAARRGGBB. Alpha 0 means unset, in which case
|
|
191
|
+
// the interior falls back to `color` at 10% alpha. Ignored by other kinds.
|
|
192
|
+
uint32_t fill;
|
|
193
|
+
// Protects the drawing from editing: hit_test reports its body (so a host
|
|
194
|
+
// can still select it and offer to unlock) but never its handles, and no
|
|
195
|
+
// handles are drawn. Rendering is otherwise unaffected.
|
|
196
|
+
bool locked;
|
|
190
197
|
} VroomDrawing;
|
|
191
198
|
|
|
192
199
|
// A resting-liquidity band: a price interval carrying a total order size on one
|
|
@@ -257,6 +264,14 @@ typedef struct VroomCoord {
|
|
|
257
264
|
double price;
|
|
258
265
|
} VroomCoord;
|
|
259
266
|
|
|
267
|
+
// An axis-aligned rectangle in CSS pixels, relative to the chart's top-left.
|
|
268
|
+
typedef struct VroomRect {
|
|
269
|
+
float x;
|
|
270
|
+
float y;
|
|
271
|
+
float width;
|
|
272
|
+
float height;
|
|
273
|
+
} VroomRect;
|
|
274
|
+
|
|
260
275
|
// ---- Styling keys ---------------------------------------------------------
|
|
261
276
|
|
|
262
277
|
typedef enum {
|
|
@@ -578,6 +593,18 @@ bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px, float y_px,
|
|
|
578
593
|
int32_t* out_index, int32_t* out_part,
|
|
579
594
|
float* out_t);
|
|
580
595
|
|
|
596
|
+
// Fills *out with the pixel bounding box of committed drawing `index` and
|
|
597
|
+
// returns true; false (out untouched) for an out-of-range index, an empty
|
|
598
|
+
// series, or a degenerate viewport.
|
|
599
|
+
//
|
|
600
|
+
// The rectangle spans the shape's painted extent — every anchor of a pencil or
|
|
601
|
+
// path, both corners of a line or box — grown by half the stroke width, so it
|
|
602
|
+
// covers the pixels the stroke actually paints. It is the anchor a host uses to
|
|
603
|
+
// position UI against the selection, and because it is computed from live core
|
|
604
|
+
// state it stays correct mid-reshape, before the edit reaches the host's array.
|
|
605
|
+
bool vroom_chart_drawing_bounds(VroomChart* chart, int32_t index,
|
|
606
|
+
VroomRect* out);
|
|
607
|
+
|
|
581
608
|
// Selects a committed drawing (renders its handles). `index` -1 clears the
|
|
582
609
|
// selection. `grabbed_endpoint` renders that handle 50% larger while it's being
|
|
583
610
|
// dragged (0/1 for a line, 0 for the box corner the host normalized to, the
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
#include "rsi.h"
|
|
37
37
|
#include "rsi_pane.h"
|
|
38
38
|
#include "style_inherit.h"
|
|
39
|
+
#include "tip_anchor.h"
|
|
39
40
|
#include "tip_pulse.h"
|
|
40
41
|
#include "volume.h"
|
|
41
42
|
#include "vwap.h"
|
|
@@ -311,16 +312,23 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
|
|
|
311
312
|
// 5.8. Line-mode tip marker — the dot (and optional pulse) at the newest
|
|
312
313
|
// close. Above the overlays so it stays the eye's anchor, but before the
|
|
313
314
|
// axis masks, which trim the ring at the price scale.
|
|
315
|
+
//
|
|
316
|
+
// Unlike every layer above, this one anchors to the newest candle in
|
|
317
|
+
// the series rather than the newest on screen, so panning into history
|
|
318
|
+
// carries the dot off the right edge with its candle (tip_anchor.h).
|
|
314
319
|
if (fade > 0.f && theme.floats[VROOM_FLOAT_LINE_TIP_DOT] > 0.5f) {
|
|
320
|
+
const auto anchor = vroom::tip_anchor::at(range.start, range.end,
|
|
321
|
+
candles.size(), morphing);
|
|
315
322
|
vroom::ma_overlay::draw_close_tip(
|
|
316
|
-
canvas, lay, bounds, visible,
|
|
317
|
-
candle_duration_ms, candle_right, candle_area_h,
|
|
323
|
+
canvas, lay, bounds, visible, anchor.slot_count, window_ms,
|
|
324
|
+
visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
|
|
318
325
|
theme.colors[VROOM_COLOR_LINE],
|
|
319
326
|
theme.colors[VROOM_COLOR_BACKGROUND],
|
|
320
327
|
theme.floats[VROOM_FLOAT_LINE_WIDTH_PX], fade,
|
|
321
328
|
theme.floats[VROOM_FLOAT_LINE_TIP_PULSE] > 0.5f,
|
|
322
329
|
tip_pulse_elapsed_s / vroom::tip_pulse::kPeriodSeconds,
|
|
323
|
-
morph_src
|
|
330
|
+
anchor.use_morph ? morph_src : nullptr,
|
|
331
|
+
anchor.use_morph ? morph_n : 0, morph_t);
|
|
324
332
|
}
|
|
325
333
|
|
|
326
334
|
// 6. Axis backgrounds (mask any candle overflow). The x-axis separator
|
package/cpp/_core_src/chart.h
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
#pragma clang diagnostic pop
|
|
24
24
|
|
|
25
25
|
#include "labels.h"
|
|
26
|
+
#include "price_format.h"
|
|
26
27
|
#include "theme.h"
|
|
27
28
|
#include "viewport.h"
|
|
28
29
|
|
|
@@ -89,6 +90,12 @@ struct VroomChart {
|
|
|
89
90
|
// label. 0 = uncomputed; layout() falls back to a width ratio.
|
|
90
91
|
float axis_width_px = 0.f;
|
|
91
92
|
|
|
93
|
+
// How prices render, derived from the asset's own scale. Cached beside the
|
|
94
|
+
// width because the two are measured together — every label site reads this
|
|
95
|
+
// so none of them can disagree with the strip they're drawn in. Both are
|
|
96
|
+
// refreshed by labels::recompute_axis_width.
|
|
97
|
+
vroom::PriceFormat price_fmt{};
|
|
98
|
+
|
|
92
99
|
// --- price bounds (y-axis state) ---------------------------------------
|
|
93
100
|
// Two modes. Auto (price_bounds_manual == false, the default): the y-range
|
|
94
101
|
// continuously follows the visible candles each frame and `price_bounds`
|
|
@@ -199,6 +206,8 @@ struct VroomChart {
|
|
|
199
206
|
float width = 2.f;
|
|
200
207
|
int32_t kind = 0;
|
|
201
208
|
std::vector<VroomDrawPoint> points; // pencil path (kind 2)
|
|
209
|
+
uint32_t fill = 0; // 0 alpha = derive from color
|
|
210
|
+
bool locked = false;
|
|
202
211
|
};
|
|
203
212
|
std::vector<StoredDrawing> drawings;
|
|
204
213
|
|
|
@@ -926,6 +926,8 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
|
|
|
926
926
|
d.color = src.color;
|
|
927
927
|
d.width = src.width;
|
|
928
928
|
d.kind = src.kind;
|
|
929
|
+
d.fill = src.fill;
|
|
930
|
+
d.locked = src.locked;
|
|
929
931
|
if ((src.kind == 2 || src.kind == 3) && src.points && src.point_count > 0) {
|
|
930
932
|
d.points.assign(src.points, src.points + src.point_count);
|
|
931
933
|
// Keep a/b mirroring the path ends so bounds/handle code is uniform.
|
|
@@ -1152,6 +1154,31 @@ extern "C" bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px,
|
|
|
1152
1154
|
return true;
|
|
1153
1155
|
}
|
|
1154
1156
|
|
|
1157
|
+
extern "C" bool vroom_chart_drawing_bounds(VroomChart* chart, int32_t index,
|
|
1158
|
+
VroomRect* out) {
|
|
1159
|
+
if (!chart || !out || chart->candles.empty()) return false;
|
|
1160
|
+
const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
|
|
1161
|
+
if (window_ms <= 0) return false;
|
|
1162
|
+
const auto lay = chart->layout();
|
|
1163
|
+
// Same bounds as coord_at / draw_chart so the rectangle matches the render.
|
|
1164
|
+
const auto range = vroom::visible_indices(
|
|
1165
|
+
chart->candles.data(), chart->candles.size(),
|
|
1166
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
1167
|
+
const size_t n = range.end - range.start;
|
|
1168
|
+
const auto bounds =
|
|
1169
|
+
chart->price_bounds_manual
|
|
1170
|
+
? chart->price_bounds
|
|
1171
|
+
: vroom::auto_price_bounds(chart->candles.data() + range.start, n);
|
|
1172
|
+
vroom::drawing_bounds::RectPx r{};
|
|
1173
|
+
if (!vroom::drawings::bounds_of(*chart, lay, bounds, window_ms, index, &r))
|
|
1174
|
+
return false;
|
|
1175
|
+
out->x = r.x;
|
|
1176
|
+
out->y = r.y;
|
|
1177
|
+
out->width = r.width;
|
|
1178
|
+
out->height = r.height;
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1155
1182
|
extern "C" bool vroom_chart_hit_test_price_line(VroomChart* chart, float x_px,
|
|
1156
1183
|
float y_px, int32_t* out_index,
|
|
1157
1184
|
int32_t* out_part) {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
#include "chart.h"
|
|
22
22
|
#include "fonts.h"
|
|
23
|
+
#include "price_format.h"
|
|
23
24
|
#include "theme.h"
|
|
24
25
|
#include "ticks.h"
|
|
25
26
|
|
|
@@ -146,11 +147,12 @@ void draw(SkCanvas* canvas,
|
|
|
146
147
|
// sharing the y-axis labels' column.
|
|
147
148
|
if (lay.y_axis_width_px > 0.f) {
|
|
148
149
|
const double price = vroom::y_to_price(lay, bounds, cy);
|
|
149
|
-
char buf[
|
|
150
|
-
const
|
|
150
|
+
char buf[48];
|
|
151
|
+
const vroom::PriceFormat fmt = vroom::with_tick_guard(
|
|
152
|
+
chart.price_fmt,
|
|
151
153
|
vroom::pick_price_interval(bounds.max - bounds.min,
|
|
152
154
|
vroom::price_pane_bottom(lay)));
|
|
153
|
-
vroom::format_price(buf, sizeof(buf), price,
|
|
155
|
+
vroom::format_price(buf, sizeof(buf), price, fmt);
|
|
154
156
|
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
155
157
|
draw_badge(canvas, font, buf, axis_center_x, cy, badge_fill,
|
|
156
158
|
SK_ColorWHITE);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The pixel bounding box of a drawing (see bounds_of in drawings.cpp).
|
|
2
|
+
//
|
|
3
|
+
// A host anchoring UI to a selected drawing needs one rectangle, whatever the
|
|
4
|
+
// shape: a box and a freehand stroke both have to yield something positionable.
|
|
5
|
+
// Reducing the projected anchors to their extent gives that uniformly, where the
|
|
6
|
+
// anchors themselves would not — a pencil stroke's `a`/`b` are just its first
|
|
7
|
+
// and last samples, which say nothing about how far the stroke wandered between
|
|
8
|
+
// them.
|
|
9
|
+
//
|
|
10
|
+
// The extent is grown by half the stroke width on every side, because a stroke
|
|
11
|
+
// straddles its path: a 2px line through y=100 paints 99..101. Without that the
|
|
12
|
+
// rectangle would clip the very pixels it is meant to describe.
|
|
13
|
+
//
|
|
14
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
15
|
+
// tests/test_drawing_bounds.cpp.
|
|
16
|
+
|
|
17
|
+
#pragma once
|
|
18
|
+
|
|
19
|
+
#include <algorithm>
|
|
20
|
+
#include <cmath>
|
|
21
|
+
|
|
22
|
+
namespace vroom::drawing_bounds {
|
|
23
|
+
|
|
24
|
+
// An axis-aligned rectangle in CSS pixels, relative to the chart's top-left.
|
|
25
|
+
struct RectPx {
|
|
26
|
+
float x = 0.f;
|
|
27
|
+
float y = 0.f;
|
|
28
|
+
float width = 0.f;
|
|
29
|
+
float height = 0.f;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Accumulates projected points into their extent. Points are fed one at a time
|
|
33
|
+
// so callers can project straight from their own storage without materializing
|
|
34
|
+
// an intermediate array.
|
|
35
|
+
struct Accumulator {
|
|
36
|
+
float min_x = 0.f;
|
|
37
|
+
float min_y = 0.f;
|
|
38
|
+
float max_x = 0.f;
|
|
39
|
+
float max_y = 0.f;
|
|
40
|
+
bool has = false;
|
|
41
|
+
|
|
42
|
+
// Non-finite coordinates are dropped rather than propagated: a single NaN
|
|
43
|
+
// would otherwise poison every comparison and hand the host an unusable
|
|
44
|
+
// rectangle.
|
|
45
|
+
void add(float x, float y) {
|
|
46
|
+
if (!std::isfinite(x) || !std::isfinite(y)) return;
|
|
47
|
+
if (!has) {
|
|
48
|
+
min_x = max_x = x;
|
|
49
|
+
min_y = max_y = y;
|
|
50
|
+
has = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
min_x = std::min(min_x, x);
|
|
54
|
+
max_x = std::max(max_x, x);
|
|
55
|
+
min_y = std::min(min_y, y);
|
|
56
|
+
max_y = std::max(max_y, y);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The extent grown by half of `stroke_width` on each side. A single point
|
|
60
|
+
// yields a zero-extent rectangle inflated to the stroke's own footprint,
|
|
61
|
+
// which is exactly what a one-sample pencil stroke paints.
|
|
62
|
+
RectPx to_rect(float stroke_width) const {
|
|
63
|
+
if (!has) return RectPx{};
|
|
64
|
+
const float pad = std::max(stroke_width, 0.f) * 0.5f;
|
|
65
|
+
return RectPx{min_x - pad, min_y - pad, (max_x - min_x) + pad * 2.f,
|
|
66
|
+
(max_y - min_y) + pad * 2.f};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
} // namespace vroom::drawing_bounds
|
|
@@ -51,17 +51,23 @@ SkRect box_rect(SkPoint a, SkPoint b) {
|
|
|
51
51
|
std::max(a.fX, b.fX), std::max(a.fY, b.fY));
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Strokes the box outline and paints
|
|
55
|
-
//
|
|
56
|
-
|
|
54
|
+
// Strokes the box outline and paints its interior, spanning opposite corners
|
|
55
|
+
// `a` and `b`. `fill_color` is used as given; alpha 0 means the caller didn't
|
|
56
|
+
// set one, and the interior falls back to a faint tint of the border (~10% of
|
|
57
|
+
// its alpha) — a solid default would hide the candles the box was drawn around.
|
|
58
|
+
void draw_box(SkCanvas* canvas, SkPoint a, SkPoint b, SkColor color, float width,
|
|
59
|
+
SkColor fill_color) {
|
|
57
60
|
const SkRect r = box_rect(a, b);
|
|
58
61
|
|
|
59
62
|
SkPaint fill;
|
|
60
63
|
fill.setAntiAlias(true);
|
|
61
64
|
fill.setStyle(SkPaint::kFill_Style);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
if (SkColorGetA(fill_color) != 0) {
|
|
66
|
+
fill.setColor(fill_color);
|
|
67
|
+
} else {
|
|
68
|
+
const U8CPU fill_alpha = static_cast<U8CPU>(SkColorGetA(color) * 0.1f);
|
|
69
|
+
fill.setColor(SkColorSetA(color, fill_alpha));
|
|
70
|
+
}
|
|
65
71
|
canvas->drawRect(r, fill);
|
|
66
72
|
|
|
67
73
|
SkPaint border;
|
|
@@ -334,7 +340,8 @@ void draw(SkCanvas* canvas,
|
|
|
334
340
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
335
341
|
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
336
342
|
if (d.kind == 1) {
|
|
337
|
-
draw_box(canvas, a, b, static_cast<SkColor>(d.color), d.width
|
|
343
|
+
draw_box(canvas, a, b, static_cast<SkColor>(d.color), d.width,
|
|
344
|
+
static_cast<SkColor>(d.fill));
|
|
338
345
|
continue;
|
|
339
346
|
}
|
|
340
347
|
SkPaint line;
|
|
@@ -348,9 +355,12 @@ void draw(SkCanvas* canvas,
|
|
|
348
355
|
}
|
|
349
356
|
|
|
350
357
|
// 1b. Handles on the selected committed drawing (unclipped, like the draft's
|
|
351
|
-
// dots). The grabbed endpoint renders 50% larger.
|
|
358
|
+
// dots). The grabbed endpoint renders 50% larger. A locked drawing shows
|
|
359
|
+
// none: nothing about it can be dragged, so an affordance saying
|
|
360
|
+
// otherwise would be a lie.
|
|
352
361
|
if (chart.selected_drawing >= 0 &&
|
|
353
|
-
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size()
|
|
362
|
+
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size() &&
|
|
363
|
+
!chart.drawings[chart.selected_drawing].locked) {
|
|
354
364
|
const auto& d = chart.drawings[chart.selected_drawing];
|
|
355
365
|
const SkPoint sa = to_px(chart, lay, bounds, window_ms, d.a);
|
|
356
366
|
const SkPoint sb = to_px(chart, lay, bounds, window_ms, d.b);
|
|
@@ -432,8 +442,10 @@ void draw(SkCanvas* canvas,
|
|
|
432
442
|
canvas->save();
|
|
433
443
|
canvas->clipRect(clip);
|
|
434
444
|
if (chart.draft_kind == 1) {
|
|
445
|
+
// The draft has no fill of its own — the preview shows the default
|
|
446
|
+
// tint until the box is committed with one.
|
|
435
447
|
draw_box(canvas, a, b, static_cast<SkColor>(chart.draft_color),
|
|
436
|
-
chart.draft_width);
|
|
448
|
+
chart.draft_width, SK_ColorTRANSPARENT);
|
|
437
449
|
} else {
|
|
438
450
|
SkPaint guide;
|
|
439
451
|
guide.setAntiAlias(true);
|
|
@@ -463,9 +475,13 @@ HitResult hit_test(const VroomChart& chart,
|
|
|
463
475
|
if (window_ms <= 0 || chart.drawings.empty()) return miss;
|
|
464
476
|
|
|
465
477
|
// Grab-priority: if a drawing is selected, its (visible) handles win first.
|
|
478
|
+
// A locked drawing draws no handles, so there is nothing here to grab — it
|
|
479
|
+
// falls through to the body pass and stays selectable, which is how a host
|
|
480
|
+
// toolbar can still reach it to unlock it.
|
|
466
481
|
constexpr float kHandleHit = kNodeRingRadius + 6.f;
|
|
467
482
|
if (chart.selected_drawing >= 0 &&
|
|
468
|
-
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size()
|
|
483
|
+
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size() &&
|
|
484
|
+
!chart.drawings[chart.selected_drawing].locked) {
|
|
469
485
|
const auto& d = chart.drawings[chart.selected_drawing];
|
|
470
486
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
471
487
|
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
@@ -546,4 +562,37 @@ HitResult hit_test(const VroomChart& chart,
|
|
|
546
562
|
return best_i >= 0 ? HitResult{best_i, best_part, best_t} : miss;
|
|
547
563
|
}
|
|
548
564
|
|
|
565
|
+
bool bounds_of(const VroomChart& chart,
|
|
566
|
+
const Layout& lay,
|
|
567
|
+
const PriceBounds& bounds,
|
|
568
|
+
int64_t window_ms,
|
|
569
|
+
int32_t index,
|
|
570
|
+
vroom::drawing_bounds::RectPx* out) {
|
|
571
|
+
if (!out || window_ms <= 0) return false;
|
|
572
|
+
if (index < 0 || static_cast<size_t>(index) >= chart.drawings.size())
|
|
573
|
+
return false;
|
|
574
|
+
const auto& d = chart.drawings[static_cast<size_t>(index)];
|
|
575
|
+
|
|
576
|
+
vroom::drawing_bounds::Accumulator acc;
|
|
577
|
+
if ((d.kind == 2 || d.kind == 3) && !d.points.empty()) {
|
|
578
|
+
// Pencil/path: every sample counts. `a`/`b` only mirror the first and
|
|
579
|
+
// last, so they would miss however far the stroke wandered between them.
|
|
580
|
+
for (const auto& p : d.points) {
|
|
581
|
+
const SkPoint q = to_px(chart, lay, bounds, window_ms, p);
|
|
582
|
+
acc.add(q.fX, q.fY);
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
// Line/box: the two stored corners already span the shape — a box's
|
|
586
|
+
// derived corners are exactly their extent.
|
|
587
|
+
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
588
|
+
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
589
|
+
acc.add(a.fX, a.fY);
|
|
590
|
+
acc.add(b.fX, b.fY);
|
|
591
|
+
}
|
|
592
|
+
if (!acc.has) return false;
|
|
593
|
+
|
|
594
|
+
*out = acc.to_rect(d.width > 0.f ? d.width : 2.f);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
|
|
549
598
|
} // namespace vroom::drawings
|
package/cpp/_core_src/drawings.h
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
#pragma once
|
|
14
14
|
|
|
15
|
+
#include "drawing_bounds.h"
|
|
15
16
|
#include "viewport.h"
|
|
16
17
|
#include "vroom/vroom_chart.h"
|
|
17
18
|
|
|
@@ -59,4 +60,18 @@ HitResult hit_test(const VroomChart& chart,
|
|
|
59
60
|
float x,
|
|
60
61
|
float y);
|
|
61
62
|
|
|
63
|
+
// The pixel bounding box of committed drawing `index`, written to `out`. False
|
|
64
|
+
// (leaving `out` untouched) for an out-of-range index or a degenerate viewport.
|
|
65
|
+
//
|
|
66
|
+
// The rectangle covers the shape's painted extent — every anchor of a pencil or
|
|
67
|
+
// path, both corners of a line or box — grown by half the stroke width. It is
|
|
68
|
+
// what the host positions a selection toolbar against, so it tracks the drawing
|
|
69
|
+
// live through a pan, a zoom, and a reshape in progress.
|
|
70
|
+
bool bounds_of(const VroomChart& chart,
|
|
71
|
+
const vroom::Layout& lay,
|
|
72
|
+
const vroom::PriceBounds& bounds,
|
|
73
|
+
int64_t window_ms,
|
|
74
|
+
int32_t index,
|
|
75
|
+
vroom::drawing_bounds::RectPx* out);
|
|
76
|
+
|
|
62
77
|
} // namespace vroom::drawings
|
package/cpp/_core_src/labels.cpp
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
#include "chart.h"
|
|
21
21
|
#include "fonts.h"
|
|
22
|
+
#include "price_format.h"
|
|
22
23
|
#include "ticks.h"
|
|
23
24
|
#include "viewport.h"
|
|
24
25
|
|
|
@@ -47,6 +48,19 @@ void advance(Fade& f, float step, float dt) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// What the asset is worth, which is what its price precision keys off. The
|
|
52
|
+
// newest close rather than the visible range, so panning and zooming don't
|
|
53
|
+
// shift the decimals around under the user.
|
|
54
|
+
//
|
|
55
|
+
// An asset that has crossed orders of magnitude within one series (a token at
|
|
56
|
+
// 0.00001 that later trades at 100) reads its recent scale here, so deep
|
|
57
|
+
// history would round flat — the tick guard at each label site is what rescues
|
|
58
|
+
// it, since the bounds down there are tiny enough to demand more decimals.
|
|
59
|
+
double reference_price(const VroomChart& chart) {
|
|
60
|
+
if (!chart.candles.empty()) return chart.candles.back().close;
|
|
61
|
+
return (chart.price_bounds.max + chart.price_bounds.min) * 0.5;
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
// Drives a whole axis off the morph envelope instead of the per-label fades:
|
|
51
65
|
// everything in the phase's tick set shares one opacity, and anything outside it
|
|
52
66
|
// is dropped outright. At the midpoint the axis is transparent, so swapping the
|
|
@@ -153,15 +167,16 @@ void draw_y_labels(SkCanvas* canvas,
|
|
|
153
167
|
// Horizontal center of the y-axis container ([width - y_axis_width, width]).
|
|
154
168
|
// Labels (and the price box) center on this so their text shares a column.
|
|
155
169
|
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
156
|
-
const
|
|
170
|
+
const vroom::PriceFormat fmt = vroom::with_tick_guard(
|
|
171
|
+
chart.price_fmt,
|
|
157
172
|
vroom::pick_price_interval(bounds.max - bounds.min, candle_area_h));
|
|
158
173
|
|
|
159
174
|
for (const auto& f : chart.y_fades) {
|
|
160
175
|
if (f.opacity <= 1e-3f) continue;
|
|
161
176
|
const float y = vroom::price_to_y(lay, bounds, f.price);
|
|
162
177
|
|
|
163
|
-
char buf[
|
|
164
|
-
vroom::format_price(buf, sizeof(buf), f.price,
|
|
178
|
+
char buf[48];
|
|
179
|
+
vroom::format_price(buf, sizeof(buf), f.price, fmt);
|
|
165
180
|
const size_t len = std::strlen(buf);
|
|
166
181
|
const float text_w = font.measureText(
|
|
167
182
|
buf, len, SkTextEncoding::kUTF8);
|
|
@@ -347,6 +362,10 @@ void gc_x_fades(VroomChart& chart) {
|
|
|
347
362
|
// ----- Axis-width sizing ----------------------------------------------------
|
|
348
363
|
|
|
349
364
|
void recompute_axis_width(VroomChart& chart) {
|
|
365
|
+
// Precision first, and unconditionally: the label sites read it whether or
|
|
366
|
+
// not a typeface has loaded, and it's what the measurement below sizes for.
|
|
367
|
+
chart.price_fmt = vroom::price_format_for(reference_price(chart));
|
|
368
|
+
|
|
350
369
|
auto tf = vroom::axis_typeface();
|
|
351
370
|
if (!tf) {
|
|
352
371
|
chart.axis_width_px = 0.f;
|
|
@@ -370,11 +389,16 @@ void recompute_axis_width(VroomChart& chart) {
|
|
|
370
389
|
|
|
371
390
|
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
372
391
|
const auto lay = chart.layout();
|
|
373
|
-
|
|
392
|
+
// Same guard the label sites apply, against the bounds this is sizing for,
|
|
393
|
+
// so a zoom deep enough to add decimals widens the strip to hold them.
|
|
394
|
+
const vroom::PriceFormat fmt = vroom::with_tick_guard(
|
|
395
|
+
chart.price_fmt,
|
|
374
396
|
vroom::pick_price_interval(hi - lo, vroom::price_pane_bottom(lay)));
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
397
|
+
// At the same decimals the longest label is whichever bound has the most
|
|
398
|
+
// integer digits, separators included.
|
|
399
|
+
char buf_hi[48], buf_lo[48];
|
|
400
|
+
vroom::format_price(buf_hi, sizeof(buf_hi), hi, fmt);
|
|
401
|
+
vroom::format_price(buf_lo, sizeof(buf_lo), lo, fmt);
|
|
378
402
|
const float w_hi = font.measureText(
|
|
379
403
|
buf_hi, std::strlen(buf_hi), SkTextEncoding::kUTF8);
|
|
380
404
|
const float w_lo = font.measureText(
|
|
@@ -108,6 +108,11 @@ void draw_close_gradient(SkCanvas* canvas,
|
|
|
108
108
|
// switch. `opacity` scales everything, fading the marker in with the line during
|
|
109
109
|
// the candle→line morph.
|
|
110
110
|
//
|
|
111
|
+
// `n` counts the slots to the candle being marked, which is the series' newest
|
|
112
|
+
// rather than the visible slice's — see tip_anchor.h, which derives it. That
|
|
113
|
+
// candle can sit off the pane when panned into history, and the marker then
|
|
114
|
+
// draws nothing.
|
|
115
|
+
//
|
|
111
116
|
// `pulse_phase` is in cycles and wraps, so the caller can hand over elapsed time
|
|
112
117
|
// divided by tip_pulse::kPeriodSeconds. Ignored unless `pulse`.
|
|
113
118
|
void draw_close_tip(SkCanvas* canvas,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#include "price_format.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cmath>
|
|
5
|
+
#include <cstdio>
|
|
6
|
+
|
|
7
|
+
namespace vroom {
|
|
8
|
+
|
|
9
|
+
namespace {
|
|
10
|
+
|
|
11
|
+
// Room for %.*f of any finite double at kPriceMaxDecimals: sign, the 309
|
|
12
|
+
// integer digits DBL_MAX can reach, the point, and the decimals.
|
|
13
|
+
constexpr size_t kPlainCap = 340;
|
|
14
|
+
|
|
15
|
+
constexpr char kGroupSeparator = ',';
|
|
16
|
+
constexpr int kGroupSize = 3;
|
|
17
|
+
|
|
18
|
+
} // namespace
|
|
19
|
+
|
|
20
|
+
int significant_decimals(double reference) {
|
|
21
|
+
const double r = std::fabs(reference);
|
|
22
|
+
if (!(r > 0.0) || !std::isfinite(r)) return kPriceMinDecimals;
|
|
23
|
+
// floor(log10) is the exponent of the leading digit, so subtracting it from
|
|
24
|
+
// the digit budget lands the last significant digit on the final decimal.
|
|
25
|
+
const int exponent = static_cast<int>(std::floor(std::log10(r)));
|
|
26
|
+
return std::clamp(kPriceSigDigits - 1 - exponent, kPriceMinDecimals,
|
|
27
|
+
kPriceMaxDecimals);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
PriceFormat price_format_for(double reference) {
|
|
31
|
+
return PriceFormat{significant_decimals(reference), true};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
int price_decimals(double interval) {
|
|
35
|
+
if (!(interval > 0.0) || !std::isfinite(interval)) return kPriceMinDecimals;
|
|
36
|
+
const double d = std::ceil(-std::log10(interval) - 1e-12);
|
|
37
|
+
if (d < 0.0) return 0;
|
|
38
|
+
if (d > static_cast<double>(kPriceMaxDecimals)) return kPriceMaxDecimals;
|
|
39
|
+
return static_cast<int>(d);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
PriceFormat with_tick_guard(const PriceFormat& fmt, double interval) {
|
|
43
|
+
return PriceFormat{std::max(fmt.decimals, price_decimals(interval)),
|
|
44
|
+
fmt.group};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void format_price(char* buf, size_t buf_size, double price,
|
|
48
|
+
const PriceFormat& fmt) {
|
|
49
|
+
if (!buf || buf_size == 0) return;
|
|
50
|
+
const int decimals = std::clamp(fmt.decimals, 0, kPriceMaxDecimals);
|
|
51
|
+
|
|
52
|
+
char plain[kPlainCap];
|
|
53
|
+
if (std::snprintf(plain, sizeof(plain), "%.*f", decimals, price) < 0) {
|
|
54
|
+
buf[0] = '\0';
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!fmt.group) {
|
|
58
|
+
std::snprintf(buf, buf_size, "%s", plain);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const char* src = plain;
|
|
63
|
+
size_t out = 0;
|
|
64
|
+
// Leaves room for the terminator on every write, so the result truncates
|
|
65
|
+
// the way snprintf would rather than running off the end.
|
|
66
|
+
const auto put = [&](char c) {
|
|
67
|
+
if (out + 1 < buf_size) buf[out++] = c;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (*src == '-' || *src == '+') put(*src++);
|
|
71
|
+
|
|
72
|
+
// Digits up to the point, or the whole run when decimals == 0. A non-finite
|
|
73
|
+
// price has none, and falls through to the tail copy as "inf" / "nan".
|
|
74
|
+
const char* int_end = src;
|
|
75
|
+
while (*int_end >= '0' && *int_end <= '9') ++int_end;
|
|
76
|
+
|
|
77
|
+
const ptrdiff_t digits = int_end - src;
|
|
78
|
+
for (ptrdiff_t i = 0; i < digits; ++i) {
|
|
79
|
+
if (i > 0 && (digits - i) % kGroupSize == 0) put(kGroupSeparator);
|
|
80
|
+
put(src[i]);
|
|
81
|
+
}
|
|
82
|
+
for (const char* p = int_end; *p != '\0'; ++p) put(*p);
|
|
83
|
+
|
|
84
|
+
buf[out] = '\0';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
} // namespace vroom
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// How a price is turned into the string on the y-axis.
|
|
2
|
+
//
|
|
3
|
+
// Precision follows the asset's own scale rather than the tick interval: a
|
|
4
|
+
// sub-cent token needs nine decimals to say anything at all, while a
|
|
5
|
+
// five-figure one is unreadable past two. Deriving it from the interval — as
|
|
6
|
+
// the axis used to — gets both wrong, printing "84000" for the one and
|
|
7
|
+
// "0.000060" for the other.
|
|
8
|
+
//
|
|
9
|
+
// The rule is a fixed count of significant digits with a floor, which is what
|
|
10
|
+
// reproduces a conventional price scale across the whole range:
|
|
11
|
+
//
|
|
12
|
+
// 4.4094e-5 -> 9 decimals 0.000044094
|
|
13
|
+
// 0.023397 -> 6 decimals 0.023397
|
|
14
|
+
// 80285.20 -> 2 decimals 80,285.20 (floor)
|
|
15
|
+
// 2513.92 -> 2 decimals 2,513.92 (floor)
|
|
16
|
+
//
|
|
17
|
+
// The reference is the asset's own price, not the visible range, so the
|
|
18
|
+
// precision holds steady while the user pans and zooms. A range-derived count
|
|
19
|
+
// would change under the user's finger and drag the axis width along with it.
|
|
20
|
+
//
|
|
21
|
+
// Skia-free and separately compiled so the unit tests can cover it; see
|
|
22
|
+
// tests/test_price_format.cpp.
|
|
23
|
+
|
|
24
|
+
#pragma once
|
|
25
|
+
|
|
26
|
+
#include <cstddef>
|
|
27
|
+
|
|
28
|
+
namespace vroom {
|
|
29
|
+
|
|
30
|
+
// Significant digits a price is quoted to. Five is what conventional price
|
|
31
|
+
// scales show: it keeps 0.023397 intact and still fits 0.000044094.
|
|
32
|
+
inline constexpr int kPriceSigDigits = 5;
|
|
33
|
+
|
|
34
|
+
// Prices at or above ~1 unit read as currency, where fewer than two decimals
|
|
35
|
+
// looks broken (a "$84,000" axis next to a "$80,285.20" last price).
|
|
36
|
+
inline constexpr int kPriceMinDecimals = 2;
|
|
37
|
+
|
|
38
|
+
// Past this the digits are noise, and %.*f starts printing the binary
|
|
39
|
+
// representation's tail rather than anything the feed meant.
|
|
40
|
+
inline constexpr int kPriceMaxDecimals = 12;
|
|
41
|
+
|
|
42
|
+
// How to render a price. `group` inserts thousands separators, which only ever
|
|
43
|
+
// affects the integer part.
|
|
44
|
+
struct PriceFormat {
|
|
45
|
+
int decimals = kPriceMinDecimals;
|
|
46
|
+
bool group = true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Decimals giving kPriceSigDigits significant digits for an asset priced around
|
|
50
|
+
// |reference|, clamped to [kPriceMinDecimals, kPriceMaxDecimals]. A zero or
|
|
51
|
+
// non-finite reference falls back to the floor.
|
|
52
|
+
int significant_decimals(double reference);
|
|
53
|
+
|
|
54
|
+
// The format for an asset priced around `reference`.
|
|
55
|
+
PriceFormat price_format_for(double reference);
|
|
56
|
+
|
|
57
|
+
// Decimal places needed so adjacent ticks `interval` apart don't collapse to
|
|
58
|
+
// the same string. 0.01 → 2, 0.005 → 3, 1e-8 → 8. Clamped to [0, 12].
|
|
59
|
+
int price_decimals(double interval);
|
|
60
|
+
|
|
61
|
+
// `fmt` raised, if need be, so ticks `interval` apart stay distinct. The asset's
|
|
62
|
+
// precision is the floor; only a zoom deeper than that precision adds to it.
|
|
63
|
+
PriceFormat with_tick_guard(const PriceFormat& fmt, double interval);
|
|
64
|
+
|
|
65
|
+
// Writes `price` into `buf` per `fmt`, truncating rather than overrunning.
|
|
66
|
+
void format_price(char* buf, size_t buf_size, double price,
|
|
67
|
+
const PriceFormat& fmt);
|
|
68
|
+
|
|
69
|
+
} // namespace vroom
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
#include "chart.h"
|
|
20
20
|
#include "fonts.h"
|
|
21
|
+
#include "price_format.h"
|
|
21
22
|
#include "theme.h"
|
|
22
23
|
#include "ticks.h"
|
|
23
24
|
#include "viewport.h"
|
|
@@ -66,10 +67,11 @@ void draw(SkCanvas* canvas,
|
|
|
66
67
|
font.setSubpixel(true);
|
|
67
68
|
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
68
69
|
|
|
69
|
-
char buf[
|
|
70
|
-
const
|
|
70
|
+
char buf[48];
|
|
71
|
+
const vroom::PriceFormat fmt = vroom::with_tick_guard(
|
|
72
|
+
chart.price_fmt,
|
|
71
73
|
vroom::pick_price_interval(bounds.max - bounds.min, candle_area_h));
|
|
72
|
-
vroom::format_price(buf, sizeof(buf), last.close,
|
|
74
|
+
vroom::format_price(buf, sizeof(buf), last.close, fmt);
|
|
73
75
|
const size_t len = std::strlen(buf);
|
|
74
76
|
|
|
75
77
|
// Measure the tight glyph bounds (origin at the baseline) so we can center
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
#include "chart.h"
|
|
23
23
|
#include "fonts.h"
|
|
24
|
+
#include "price_format.h"
|
|
24
25
|
#include "price_line_layout.h"
|
|
25
26
|
#include "theme.h"
|
|
26
27
|
#include "ticks.h"
|
|
@@ -214,11 +215,11 @@ void draw_axis_badge(SkCanvas* canvas,
|
|
|
214
215
|
double price,
|
|
215
216
|
float y,
|
|
216
217
|
SkColor fill,
|
|
217
|
-
|
|
218
|
+
const PriceFormat& fmt) {
|
|
218
219
|
if (lay.y_axis_width_px <= 0.f) return;
|
|
219
220
|
|
|
220
|
-
char buf[
|
|
221
|
-
vroom::format_price(buf, sizeof(buf), price,
|
|
221
|
+
char buf[48];
|
|
222
|
+
vroom::format_price(buf, sizeof(buf), price, fmt);
|
|
222
223
|
const size_t len = std::strlen(buf);
|
|
223
224
|
|
|
224
225
|
SkRect tb;
|
|
@@ -255,7 +256,8 @@ void draw(SkCanvas* canvas,
|
|
|
255
256
|
const VroomPriceLineStyle& style = chart.price_line_style;
|
|
256
257
|
SkFont font;
|
|
257
258
|
const bool has_font = label_font(chart, &font);
|
|
258
|
-
const
|
|
259
|
+
const vroom::PriceFormat fmt = vroom::with_tick_guard(
|
|
260
|
+
chart.price_fmt,
|
|
259
261
|
vroom::pick_price_interval(bounds.max - bounds.min, candle_area_h));
|
|
260
262
|
|
|
261
263
|
for (size_t i = 0; i < chart.price_lines.size(); ++i) {
|
|
@@ -355,7 +357,7 @@ void draw(SkCanvas* canvas,
|
|
|
355
357
|
|
|
356
358
|
if (has_font && (pl.flags & VROOM_PRICE_LINE_AXIS_LABEL) != 0) {
|
|
357
359
|
draw_axis_badge(canvas, font, lay, render_price(chart, i), y,
|
|
358
|
-
line_color,
|
|
360
|
+
line_color, fmt);
|
|
359
361
|
}
|
|
360
362
|
}
|
|
361
363
|
}
|
package/cpp/_core_src/ticks.cpp
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
4
|
#include <cmath>
|
|
5
|
-
#include <cstdio>
|
|
6
5
|
#include <ctime>
|
|
7
6
|
|
|
8
7
|
namespace vroom {
|
|
@@ -146,17 +145,4 @@ double pick_price_interval(double range, float candle_area_h) {
|
|
|
146
145
|
return nice * magnitude;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
int price_decimals(double interval) {
|
|
150
|
-
if (!(interval > 0.0) || !std::isfinite(interval)) return 2;
|
|
151
|
-
const double d = std::ceil(-std::log10(interval) - 1e-12);
|
|
152
|
-
if (d < 0.0) return 0;
|
|
153
|
-
if (d > 12.0) return 12;
|
|
154
|
-
return static_cast<int>(d);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
void format_price(char* buf, size_t buf_size, double price, int decimals) {
|
|
158
|
-
if (!buf || buf_size == 0) return;
|
|
159
|
-
std::snprintf(buf, buf_size, "%.*f", decimals, price);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
148
|
} // namespace vroom
|
package/cpp/_core_src/ticks.h
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
#pragma once
|
|
8
8
|
|
|
9
|
-
#include <cstddef>
|
|
10
9
|
#include <cstdint>
|
|
11
10
|
|
|
12
11
|
namespace vroom {
|
|
@@ -49,13 +48,8 @@ int64_t next_tick(int64_t t, const TimeTick& tick);
|
|
|
49
48
|
// "Nice number" tick selection for the y-axis: snaps to 1, 2, or 5 × 10ⁿ
|
|
50
49
|
// based on the price range and target spacing. Returns the price interval
|
|
51
50
|
// in the same units as the data (e.g. dollars).
|
|
51
|
+
//
|
|
52
|
+
// Turning that interval into a label string is price_format.h's job.
|
|
52
53
|
double pick_price_interval(double range, float candle_area_h);
|
|
53
54
|
|
|
54
|
-
// Decimal places needed so adjacent ticks `interval` apart don't collapse to
|
|
55
|
-
// the same string. 0.01 → 2, 0.005 → 3, 1e-8 → 8. Clamped to [0, 12].
|
|
56
|
-
int price_decimals(double interval);
|
|
57
|
-
|
|
58
|
-
// Writes `price` into `buf` using `decimals` (from price_decimals).
|
|
59
|
-
void format_price(char* buf, size_t buf_size, double price, int decimals);
|
|
60
|
-
|
|
61
55
|
} // namespace vroom
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Which slot the line chart's tip marker anchors to (see draw_close_tip in
|
|
2
|
+
// ma_overlay.cpp).
|
|
3
|
+
//
|
|
4
|
+
// The tip marks the newest close in the *series*, not the newest close on
|
|
5
|
+
// screen — the same rule the price indicator follows (price_indicator.cpp).
|
|
6
|
+
// Panning into history therefore carries the dot off the right edge with the
|
|
7
|
+
// candle it belongs to, where draw_close_tip's on-pane guard drops it.
|
|
8
|
+
//
|
|
9
|
+
// Every other line-chart layer draws the visible slice, so the tip is the one
|
|
10
|
+
// caller that has to reach past range.end. The slice is a view into the same
|
|
11
|
+
// contiguous candle buffer, so widening the slot count is enough to reach the
|
|
12
|
+
// newest candle — no second pointer.
|
|
13
|
+
//
|
|
14
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
15
|
+
// tests/test_tip_anchor.cpp.
|
|
16
|
+
|
|
17
|
+
#pragma once
|
|
18
|
+
|
|
19
|
+
#include <cstddef>
|
|
20
|
+
|
|
21
|
+
namespace vroom::tip_anchor {
|
|
22
|
+
|
|
23
|
+
// How draw_close_tip should be called for one frame.
|
|
24
|
+
struct Anchor {
|
|
25
|
+
// Slot count to pass in place of the visible count, so slot 0 resolves to
|
|
26
|
+
// the newest candle in the series. Never 0 for a non-empty visible slice.
|
|
27
|
+
std::size_t slot_count;
|
|
28
|
+
// Whether the interval-morph capture still lines up with slot 0.
|
|
29
|
+
bool use_morph;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// `range_start`/`range_end` are the visible slice's half-open bounds into a
|
|
33
|
+
// series of `total` candles; `morphing` is whether an interval morph is running.
|
|
34
|
+
//
|
|
35
|
+
// The capture taken at a timeframe switch is indexed from the newest *visible*
|
|
36
|
+
// candle, so it only pairs with slot 0 while the newest candle is the one at the
|
|
37
|
+
// right edge. Scrolled back, pairing them would interpolate the tip between two
|
|
38
|
+
// unrelated candles and strand the dot mid-pane for the length of the switch.
|
|
39
|
+
inline Anchor at(std::size_t range_start,
|
|
40
|
+
std::size_t range_end,
|
|
41
|
+
std::size_t total,
|
|
42
|
+
bool morphing) {
|
|
43
|
+
// A caller with an empty or out-of-bounds slice has nothing to anchor to;
|
|
44
|
+
// reporting 0 slots lets draw_close_tip take its existing early-out.
|
|
45
|
+
if (range_start >= total) return Anchor{0, false};
|
|
46
|
+
return Anchor{total - range_start, morphing && range_end >= total};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
} // namespace vroom::tip_anchor
|
package/lib/index.d.mts
CHANGED
|
@@ -177,6 +177,12 @@ type DrawingBase = {
|
|
|
177
177
|
color?: VroomColor;
|
|
178
178
|
/** Stroke width in px. Default 2. */
|
|
179
179
|
width?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Protect the drawing from editing. A locked drawing still renders and can
|
|
182
|
+
* still be selected — so a toolbar can offer to unlock it — but it can't be
|
|
183
|
+
* dragged, reshaped or deleted, and its grab handles aren't drawn. Web only.
|
|
184
|
+
*/
|
|
185
|
+
locked?: boolean;
|
|
180
186
|
};
|
|
181
187
|
/** A two-point trendline from `points[0]` to `points[1]`. */
|
|
182
188
|
type LineDrawing = DrawingBase & {
|
|
@@ -192,6 +198,15 @@ type BoxDrawing = DrawingBase & {
|
|
|
192
198
|
type: 'box';
|
|
193
199
|
/** Two opposite corners, in data space. */
|
|
194
200
|
points: [DrawPoint, DrawPoint];
|
|
201
|
+
/**
|
|
202
|
+
* Interior fill, painted beneath the stroke. Omitted, the interior keeps its
|
|
203
|
+
* default tint of the stroke color at 10% alpha.
|
|
204
|
+
*
|
|
205
|
+
* A solid fill hides the candles the box was drawn around, so this usually
|
|
206
|
+
* wants an alpha — as the high byte of an 8-digit hex (`'#5400ce2c'` is a
|
|
207
|
+
* green at 33%), since vroom reads hex as `#aarrggbb`, not CSS `#rrggbbaa`.
|
|
208
|
+
*/
|
|
209
|
+
fill?: VroomColor;
|
|
195
210
|
};
|
|
196
211
|
/**
|
|
197
212
|
* A freehand pencil stroke: an open path through `points`, in order. Unlike the
|
|
@@ -224,6 +239,35 @@ type PathDrawing = DrawingBase & {
|
|
|
224
239
|
* `'line'` and `'box'` are always exactly two points.
|
|
225
240
|
*/
|
|
226
241
|
type Drawing = LineDrawing | BoxDrawing | PencilDrawing | PathDrawing;
|
|
242
|
+
/** An axis-aligned rectangle in CSS px, relative to the chart container. */
|
|
243
|
+
type DrawingRect = {
|
|
244
|
+
x: number;
|
|
245
|
+
y: number;
|
|
246
|
+
width: number;
|
|
247
|
+
height: number;
|
|
248
|
+
};
|
|
249
|
+
/** The currently selected drawing and where it sits on screen. */
|
|
250
|
+
type DrawingSelection = {
|
|
251
|
+
/**
|
|
252
|
+
* The selected drawing. While a drag or reshape is in flight this holds the
|
|
253
|
+
* last committed geometry — the edit reaches you through `onDrawingChange`
|
|
254
|
+
* on release — whereas `rect` tracks the shape live.
|
|
255
|
+
*/
|
|
256
|
+
drawing: Drawing;
|
|
257
|
+
/** The drawing's bounds in CSS px, relative to the chart container. */
|
|
258
|
+
rect: DrawingRect;
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* The subset of a drawing's appearance a host can change after the fact,
|
|
262
|
+
* through `restyle`. Fields left out are kept as they were.
|
|
263
|
+
*/
|
|
264
|
+
type DrawingStyle = {
|
|
265
|
+
color?: VroomColor;
|
|
266
|
+
width?: number;
|
|
267
|
+
/** Box interior fill; ignored by the other drawing types. */
|
|
268
|
+
fill?: VroomColor;
|
|
269
|
+
locked?: boolean;
|
|
270
|
+
};
|
|
227
271
|
/**
|
|
228
272
|
* Storage adapter for **managed** drawing persistence. Provide it via the
|
|
229
273
|
* `drawingStore` prop and the chart owns the drawings array itself — loading and
|
|
@@ -264,11 +308,11 @@ type UndoRedoState = {
|
|
|
264
308
|
canRedo: boolean;
|
|
265
309
|
};
|
|
266
310
|
/**
|
|
267
|
-
* Programmatic
|
|
268
|
-
* managed mode — for toolbar buttons and other UI outside
|
|
269
|
-
* keyboard shortcuts (⌘Z / ⇧⌘Z / Ctrl+Y) work without this.
|
|
311
|
+
* Programmatic controls over the chart's drawings, published through the
|
|
312
|
+
* `historyRef` prop in managed mode — for toolbar buttons and other UI outside
|
|
313
|
+
* the chart. The keyboard shortcuts (⌘Z / ⇧⌘Z / Ctrl+Y) work without this.
|
|
270
314
|
*/
|
|
271
|
-
type
|
|
315
|
+
type DrawingControls = {
|
|
272
316
|
/** Roll back the most recent committed drawing action. No-op when empty. */
|
|
273
317
|
undo: () => void;
|
|
274
318
|
/** Re-apply the most recently undone action. No-op when empty. */
|
|
@@ -279,6 +323,16 @@ type UndoRedoControls = {
|
|
|
279
323
|
* chart for what the user perceives as a brand-new context.
|
|
280
324
|
*/
|
|
281
325
|
clearHistory: () => void;
|
|
326
|
+
/**
|
|
327
|
+
* Restyle the drawing with this `id`, merging `patch` over its current
|
|
328
|
+
* appearance. No-op for an unknown id.
|
|
329
|
+
*
|
|
330
|
+
* In managed mode the chart owns the drawings array, so this is the only way
|
|
331
|
+
* to say "this drawing's color is now X" — the array is otherwise mutated by
|
|
332
|
+
* user gestures alone. The change records an undo step and persists like any
|
|
333
|
+
* other edit.
|
|
334
|
+
*/
|
|
335
|
+
restyle: (id: string, patch: DrawingStyle) => void;
|
|
282
336
|
};
|
|
283
337
|
/** Price source for a moving average. */
|
|
284
338
|
type MASource = 'close' | 'open' | 'high' | 'low' | 'hl2' | 'hlc3' | 'ohlc4';
|
|
@@ -749,6 +803,17 @@ type VroomChartCoreProps = {
|
|
|
749
803
|
* drawing with this `id` from your controlled `drawings` state. Web only.
|
|
750
804
|
*/
|
|
751
805
|
onDrawingDelete?: (id: string) => void;
|
|
806
|
+
/**
|
|
807
|
+
* Fired when the selected drawing changes, and again whenever its position on
|
|
808
|
+
* screen moves — panning, zooming, resizing, or dragging the shape itself.
|
|
809
|
+
* `null` on deselect.
|
|
810
|
+
*
|
|
811
|
+
* Anchor a floating toolbar to `selection.rect` and it will stay attached to
|
|
812
|
+
* the drawing: the rect is recomputed from live chart state once per painted
|
|
813
|
+
* frame, so it tracks 1:1 rather than settling after the gesture. Works in
|
|
814
|
+
* both controlled and managed mode. Web only.
|
|
815
|
+
*/
|
|
816
|
+
onSelectionChange?: (selection: DrawingSelection | null) => void;
|
|
752
817
|
/**
|
|
753
818
|
* Fired when the chart wants the mode changed — e.g. it requests `'pan'` after
|
|
754
819
|
* the user clicks away from a just-drawn line. Since `mode` is controlled, the
|
|
@@ -769,13 +834,13 @@ type VroomChartCoreProps = {
|
|
|
769
834
|
*/
|
|
770
835
|
onHistoryChange?: (state: UndoRedoState) => void;
|
|
771
836
|
/**
|
|
772
|
-
* Receives programmatic `undo`/`redo`/`clearHistory` controls in
|
|
773
|
-
* (e.g. `useRef<
|
|
837
|
+
* Receives programmatic `undo`/`redo`/`clearHistory`/`restyle` controls in
|
|
838
|
+
* managed mode (e.g. `useRef<DrawingControls | null>(null)` passed here, then
|
|
774
839
|
* `historyRef.current?.undo()` from a toolbar button). Set to `null` while
|
|
775
840
|
* unmounted or when no `drawingStore` is present. Web only.
|
|
776
841
|
*/
|
|
777
842
|
historyRef?: {
|
|
778
|
-
current:
|
|
843
|
+
current: DrawingControls | null;
|
|
779
844
|
};
|
|
780
845
|
/**
|
|
781
846
|
* Fired when the user presses the undo shortcut (⌘Z / Ctrl+Z) in controlled
|
package/lib/index.d.ts
CHANGED
|
@@ -177,6 +177,12 @@ type DrawingBase = {
|
|
|
177
177
|
color?: VroomColor;
|
|
178
178
|
/** Stroke width in px. Default 2. */
|
|
179
179
|
width?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Protect the drawing from editing. A locked drawing still renders and can
|
|
182
|
+
* still be selected — so a toolbar can offer to unlock it — but it can't be
|
|
183
|
+
* dragged, reshaped or deleted, and its grab handles aren't drawn. Web only.
|
|
184
|
+
*/
|
|
185
|
+
locked?: boolean;
|
|
180
186
|
};
|
|
181
187
|
/** A two-point trendline from `points[0]` to `points[1]`. */
|
|
182
188
|
type LineDrawing = DrawingBase & {
|
|
@@ -192,6 +198,15 @@ type BoxDrawing = DrawingBase & {
|
|
|
192
198
|
type: 'box';
|
|
193
199
|
/** Two opposite corners, in data space. */
|
|
194
200
|
points: [DrawPoint, DrawPoint];
|
|
201
|
+
/**
|
|
202
|
+
* Interior fill, painted beneath the stroke. Omitted, the interior keeps its
|
|
203
|
+
* default tint of the stroke color at 10% alpha.
|
|
204
|
+
*
|
|
205
|
+
* A solid fill hides the candles the box was drawn around, so this usually
|
|
206
|
+
* wants an alpha — as the high byte of an 8-digit hex (`'#5400ce2c'` is a
|
|
207
|
+
* green at 33%), since vroom reads hex as `#aarrggbb`, not CSS `#rrggbbaa`.
|
|
208
|
+
*/
|
|
209
|
+
fill?: VroomColor;
|
|
195
210
|
};
|
|
196
211
|
/**
|
|
197
212
|
* A freehand pencil stroke: an open path through `points`, in order. Unlike the
|
|
@@ -224,6 +239,35 @@ type PathDrawing = DrawingBase & {
|
|
|
224
239
|
* `'line'` and `'box'` are always exactly two points.
|
|
225
240
|
*/
|
|
226
241
|
type Drawing = LineDrawing | BoxDrawing | PencilDrawing | PathDrawing;
|
|
242
|
+
/** An axis-aligned rectangle in CSS px, relative to the chart container. */
|
|
243
|
+
type DrawingRect = {
|
|
244
|
+
x: number;
|
|
245
|
+
y: number;
|
|
246
|
+
width: number;
|
|
247
|
+
height: number;
|
|
248
|
+
};
|
|
249
|
+
/** The currently selected drawing and where it sits on screen. */
|
|
250
|
+
type DrawingSelection = {
|
|
251
|
+
/**
|
|
252
|
+
* The selected drawing. While a drag or reshape is in flight this holds the
|
|
253
|
+
* last committed geometry — the edit reaches you through `onDrawingChange`
|
|
254
|
+
* on release — whereas `rect` tracks the shape live.
|
|
255
|
+
*/
|
|
256
|
+
drawing: Drawing;
|
|
257
|
+
/** The drawing's bounds in CSS px, relative to the chart container. */
|
|
258
|
+
rect: DrawingRect;
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* The subset of a drawing's appearance a host can change after the fact,
|
|
262
|
+
* through `restyle`. Fields left out are kept as they were.
|
|
263
|
+
*/
|
|
264
|
+
type DrawingStyle = {
|
|
265
|
+
color?: VroomColor;
|
|
266
|
+
width?: number;
|
|
267
|
+
/** Box interior fill; ignored by the other drawing types. */
|
|
268
|
+
fill?: VroomColor;
|
|
269
|
+
locked?: boolean;
|
|
270
|
+
};
|
|
227
271
|
/**
|
|
228
272
|
* Storage adapter for **managed** drawing persistence. Provide it via the
|
|
229
273
|
* `drawingStore` prop and the chart owns the drawings array itself — loading and
|
|
@@ -264,11 +308,11 @@ type UndoRedoState = {
|
|
|
264
308
|
canRedo: boolean;
|
|
265
309
|
};
|
|
266
310
|
/**
|
|
267
|
-
* Programmatic
|
|
268
|
-
* managed mode — for toolbar buttons and other UI outside
|
|
269
|
-
* keyboard shortcuts (⌘Z / ⇧⌘Z / Ctrl+Y) work without this.
|
|
311
|
+
* Programmatic controls over the chart's drawings, published through the
|
|
312
|
+
* `historyRef` prop in managed mode — for toolbar buttons and other UI outside
|
|
313
|
+
* the chart. The keyboard shortcuts (⌘Z / ⇧⌘Z / Ctrl+Y) work without this.
|
|
270
314
|
*/
|
|
271
|
-
type
|
|
315
|
+
type DrawingControls = {
|
|
272
316
|
/** Roll back the most recent committed drawing action. No-op when empty. */
|
|
273
317
|
undo: () => void;
|
|
274
318
|
/** Re-apply the most recently undone action. No-op when empty. */
|
|
@@ -279,6 +323,16 @@ type UndoRedoControls = {
|
|
|
279
323
|
* chart for what the user perceives as a brand-new context.
|
|
280
324
|
*/
|
|
281
325
|
clearHistory: () => void;
|
|
326
|
+
/**
|
|
327
|
+
* Restyle the drawing with this `id`, merging `patch` over its current
|
|
328
|
+
* appearance. No-op for an unknown id.
|
|
329
|
+
*
|
|
330
|
+
* In managed mode the chart owns the drawings array, so this is the only way
|
|
331
|
+
* to say "this drawing's color is now X" — the array is otherwise mutated by
|
|
332
|
+
* user gestures alone. The change records an undo step and persists like any
|
|
333
|
+
* other edit.
|
|
334
|
+
*/
|
|
335
|
+
restyle: (id: string, patch: DrawingStyle) => void;
|
|
282
336
|
};
|
|
283
337
|
/** Price source for a moving average. */
|
|
284
338
|
type MASource = 'close' | 'open' | 'high' | 'low' | 'hl2' | 'hlc3' | 'ohlc4';
|
|
@@ -749,6 +803,17 @@ type VroomChartCoreProps = {
|
|
|
749
803
|
* drawing with this `id` from your controlled `drawings` state. Web only.
|
|
750
804
|
*/
|
|
751
805
|
onDrawingDelete?: (id: string) => void;
|
|
806
|
+
/**
|
|
807
|
+
* Fired when the selected drawing changes, and again whenever its position on
|
|
808
|
+
* screen moves — panning, zooming, resizing, or dragging the shape itself.
|
|
809
|
+
* `null` on deselect.
|
|
810
|
+
*
|
|
811
|
+
* Anchor a floating toolbar to `selection.rect` and it will stay attached to
|
|
812
|
+
* the drawing: the rect is recomputed from live chart state once per painted
|
|
813
|
+
* frame, so it tracks 1:1 rather than settling after the gesture. Works in
|
|
814
|
+
* both controlled and managed mode. Web only.
|
|
815
|
+
*/
|
|
816
|
+
onSelectionChange?: (selection: DrawingSelection | null) => void;
|
|
752
817
|
/**
|
|
753
818
|
* Fired when the chart wants the mode changed — e.g. it requests `'pan'` after
|
|
754
819
|
* the user clicks away from a just-drawn line. Since `mode` is controlled, the
|
|
@@ -769,13 +834,13 @@ type VroomChartCoreProps = {
|
|
|
769
834
|
*/
|
|
770
835
|
onHistoryChange?: (state: UndoRedoState) => void;
|
|
771
836
|
/**
|
|
772
|
-
* Receives programmatic `undo`/`redo`/`clearHistory` controls in
|
|
773
|
-
* (e.g. `useRef<
|
|
837
|
+
* Receives programmatic `undo`/`redo`/`clearHistory`/`restyle` controls in
|
|
838
|
+
* managed mode (e.g. `useRef<DrawingControls | null>(null)` passed here, then
|
|
774
839
|
* `historyRef.current?.undo()` from a toolbar button). Set to `null` while
|
|
775
840
|
* unmounted or when no `drawingStore` is present. Web only.
|
|
776
841
|
*/
|
|
777
842
|
historyRef?: {
|
|
778
|
-
current:
|
|
843
|
+
current: DrawingControls | null;
|
|
779
844
|
};
|
|
780
845
|
/**
|
|
781
846
|
* Fired when the user presses the undo shortcut (⌘Z / Ctrl+Z) in controlled
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-vroom-chart",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Mobile-first Skia candlestick chart for React Native",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Darion Welch",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"typecheck": "tsc --noEmit",
|
|
84
84
|
"test": "vitest run",
|
|
85
85
|
"build": "tsup",
|
|
86
|
+
"check:podspec": "ruby scripts/check-podspec-portable.rb",
|
|
86
87
|
"vendor:core": "node scripts/vendor-core.mjs"
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require "pathname"
|
|
2
3
|
|
|
3
4
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Locate the @shopify/react-native-skia source. We need it because RN-Skia ships
|
|
7
|
+
# its Skia headers via `#include "include/core/SkPicture.h"`, which only resolves
|
|
8
|
+
# when the include path points at the source `cpp/` and `cpp/skia/` directories —
|
|
9
|
+
# CocoaPods' flat Headers/Public/ layout loses the `include/...` prefix.
|
|
10
|
+
#
|
|
11
|
+
# The paths must be emitted relative to $(PODS_TARGET_SRCROOT), never absolute.
|
|
12
|
+
# CocoaPods serializes this spec to `Pods/Local Podspecs/*.podspec.json` and hashes
|
|
13
|
+
# that file into Podfile.lock's SPEC CHECKSUMS, so an absolute path bakes the
|
|
14
|
+
# checkout root of whoever ran `pod install` into the lockfile and every other
|
|
15
|
+
# machine fails `pod install --deployment`. Resolving at spec-eval time (rather
|
|
16
|
+
# than hardcoding `../@shopify/react-native-skia`) keeps this correct under
|
|
17
|
+
# non-hoisted layouts such as pnpm's.
|
|
9
18
|
skia_pkg_json = `node --print "require.resolve('@shopify/react-native-skia/package.json')"`.strip
|
|
19
|
+
raise "react-native-vroom-chart: could not resolve @shopify/react-native-skia" if skia_pkg_json.empty?
|
|
10
20
|
skia_src_dir = File.dirname(skia_pkg_json)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
skia_rel_dir = File.join("$(PODS_TARGET_SRCROOT)",
|
|
22
|
+
Pathname.new(skia_src_dir).relative_path_from(Pathname.new(__dir__)).to_s)
|
|
23
|
+
skia_cpp_dir = File.join(skia_rel_dir, "cpp")
|
|
24
|
+
skia_skia_dir = File.join(skia_rel_dir, "cpp", "skia")
|
|
25
|
+
skia_api_dir = File.join(skia_rel_dir, "cpp", "api")
|
|
26
|
+
# Absolute is fine here: the result only selects a preprocessor-define string,
|
|
27
|
+
# so it never reaches the emitted spec.
|
|
14
28
|
use_graphite = File.exist?(File.join(skia_src_dir, "libs", ".graphite"))
|
|
15
29
|
skia_preprocessor_defs = use_graphite ?
|
|
16
30
|
"$(inherited) SK_GRAPHITE=1 SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1 SK_DISABLE_LEGACY_SHAPER_FACTORY=1" :
|