react-native-vroom-chart 0.11.0 → 0.12.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 +22 -0
- package/cpp/_core_include/vroom/vroom_chart.h +46 -2
- package/cpp/_core_src/chart.cpp +5 -2
- package/cpp/_core_src/chart.h +9 -0
- package/cpp/_core_src/chart_facade.cpp +36 -0
- package/cpp/_core_src/crosshair.cpp +10 -5
- 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 +7 -2
- package/cpp/_core_src/macd_pane.cpp +3 -2
- package/cpp/_core_src/price_indicator.cpp +9 -5
- package/cpp/_core_src/price_lines.cpp +9 -4
- package/cpp/_core_src/rsi_pane.cpp +3 -1
- package/cpp/_core_src/theme.cpp +2 -2
- package/cpp/_core_src/viewport.h +32 -0
- package/lib/index.d.mts +99 -7
- package/lib/index.d.ts +99 -7
- package/lib/index.js +102 -5
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +102 -5
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +129 -5
- package/src/jsi.d.ts +7 -0
- package/src/theme.ts +5 -0
|
@@ -63,6 +63,7 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
63
63
|
out.push_back(jsi::PropNameID::forAscii(rt, "setBollinger"));
|
|
64
64
|
out.push_back(jsi::PropNameID::forAscii(rt, "setVolume"));
|
|
65
65
|
out.push_back(jsi::PropNameID::forAscii(rt, "setVolumeCollapse"));
|
|
66
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setAxisCollapse"));
|
|
66
67
|
out.push_back(jsi::PropNameID::forAscii(rt, "coordAt"));
|
|
67
68
|
out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLines"));
|
|
68
69
|
out.push_back(jsi::PropNameID::forAscii(rt, "hitTestPriceLine"));
|
|
@@ -919,6 +920,27 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
919
920
|
});
|
|
920
921
|
}
|
|
921
922
|
|
|
923
|
+
if (name == "setAxisCollapse") {
|
|
924
|
+
// setAxisCollapse(yT, xT) — hides the price / time axis strips. Both take
|
|
925
|
+
// EASED progress (0 shown, 1 hidden); there is no per-element stagger for
|
|
926
|
+
// the core to distribute, so the host owns the curve.
|
|
927
|
+
return jsi::Function::createFromHostFunction(
|
|
928
|
+
rt,
|
|
929
|
+
jsi::PropNameID::forAscii(rt, "setAxisCollapse"),
|
|
930
|
+
2,
|
|
931
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
932
|
+
const jsi::Value& /*thisVal*/,
|
|
933
|
+
const jsi::Value* args,
|
|
934
|
+
size_t count) -> jsi::Value {
|
|
935
|
+
if (count < 2) return jsi::Value::undefined();
|
|
936
|
+
vroom_chart_set_axis_collapse(
|
|
937
|
+
chart_,
|
|
938
|
+
static_cast<float>(args[0].asNumber()),
|
|
939
|
+
static_cast<float>(args[1].asNumber()));
|
|
940
|
+
return jsi::Value::undefined();
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
922
944
|
if (name == "coordAt") {
|
|
923
945
|
// coordAt(x, y) -> { timeMs, price } | null. The continuous data coordinate
|
|
924
946
|
// at a pixel — not snapped to a candle slot. Null when there are no candles
|
|
@@ -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 {
|
|
@@ -267,8 +282,12 @@ typedef enum {
|
|
|
267
282
|
VROOM_COLOR_GRID,
|
|
268
283
|
VROOM_COLOR_AXIS_TEXT,
|
|
269
284
|
VROOM_COLOR_CROSSHAIR,
|
|
270
|
-
VROOM_COLOR_TOOLTIP_BG,
|
|
271
|
-
|
|
285
|
+
// Retired (was VROOM_COLOR_TOOLTIP_BG, never rendered). Held open because
|
|
286
|
+
// the TypeScript theme maps hardcode the indices below it.
|
|
287
|
+
VROOM_COLOR_RESERVED_7_,
|
|
288
|
+
// Text drawn on a filled badge: the current-price indicator, the crosshair's
|
|
289
|
+
// axis badges, and price-line pills. Defaults to white.
|
|
290
|
+
VROOM_COLOR_BADGE_TEXT,
|
|
272
291
|
VROOM_COLOR_CROSSHAIR_TARGET, // the hollow ring/dot at the intersection
|
|
273
292
|
VROOM_COLOR_BORDER_BULL, // bull body 1px outline; 0 alpha => inherit BULL fill
|
|
274
293
|
VROOM_COLOR_BORDER_BEAR, // bear body 1px outline; 0 alpha => inherit BEAR fill
|
|
@@ -549,6 +568,19 @@ void vroom_chart_set_volume(VroomChart* chart, const VroomVolume* cfg);
|
|
|
549
568
|
// `enabled`, so the host only needs it while animating.
|
|
550
569
|
void vroom_chart_set_volume_collapse(VroomChart* chart, float t, int32_t easing);
|
|
551
570
|
|
|
571
|
+
// Collapses the axis strips: 0 leaves a strip at full size, 1 hides it. Driven
|
|
572
|
+
// per-frame by the host animation loop, which owns the eased clock — unlike the
|
|
573
|
+
// volume collapse there is no per-element stagger, so these take *eased*
|
|
574
|
+
// progress directly.
|
|
575
|
+
//
|
|
576
|
+
// Hiding is a collapse rather than a zeroed dimension because the y-axis width
|
|
577
|
+
// is measured from the widest price label and recomputed on most data changes,
|
|
578
|
+
// which would overwrite a zero. The price pane grows into whatever the strips
|
|
579
|
+
// give up, and each strip's contents (labels, price badge, crosshair and
|
|
580
|
+
// price-line badges) fade out over the first half of the collapse so text is
|
|
581
|
+
// never squeezed into a strip too narrow to hold it.
|
|
582
|
+
void vroom_chart_set_axis_collapse(VroomChart* chart, float y_t, float x_t);
|
|
583
|
+
|
|
552
584
|
// ---- Drawings (line annotations) ------------------------------------------
|
|
553
585
|
|
|
554
586
|
// Replaces the full set of committed line drawings (data-anchored, so they track
|
|
@@ -578,6 +610,18 @@ bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px, float y_px,
|
|
|
578
610
|
int32_t* out_index, int32_t* out_part,
|
|
579
611
|
float* out_t);
|
|
580
612
|
|
|
613
|
+
// Fills *out with the pixel bounding box of committed drawing `index` and
|
|
614
|
+
// returns true; false (out untouched) for an out-of-range index, an empty
|
|
615
|
+
// series, or a degenerate viewport.
|
|
616
|
+
//
|
|
617
|
+
// The rectangle spans the shape's painted extent — every anchor of a pencil or
|
|
618
|
+
// path, both corners of a line or box — grown by half the stroke width, so it
|
|
619
|
+
// covers the pixels the stroke actually paints. It is the anchor a host uses to
|
|
620
|
+
// position UI against the selection, and because it is computed from live core
|
|
621
|
+
// state it stays correct mid-reshape, before the edit reaches the host's array.
|
|
622
|
+
bool vroom_chart_drawing_bounds(VroomChart* chart, int32_t index,
|
|
623
|
+
VroomRect* out);
|
|
624
|
+
|
|
581
625
|
// Selects a committed drawing (renders its handles). `index` -1 clears the
|
|
582
626
|
// selection. `grabbed_endpoint` renders that handle 50% larger while it's being
|
|
583
627
|
// dragged (0/1 for a line, 0 for the box corner the host normalized to, the
|
package/cpp/_core_src/chart.cpp
CHANGED
|
@@ -64,13 +64,16 @@ vroom::Layout VroomChart::layout() const {
|
|
|
64
64
|
return vroom::Layout{
|
|
65
65
|
width_px,
|
|
66
66
|
height_px,
|
|
67
|
-
axis_w,
|
|
68
|
-
theme.floats[VROOM_FLOAT_X_AXIS_HEIGHT_PX],
|
|
67
|
+
vroom::axis_extent(axis_w, y_axis_collapse_t),
|
|
68
|
+
vroom::axis_extent(theme.floats[VROOM_FLOAT_X_AXIS_HEIGHT_PX],
|
|
69
|
+
x_axis_collapse_t),
|
|
69
70
|
theme.floats[VROOM_FLOAT_RIGHT_PADDING_PX],
|
|
70
71
|
theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO],
|
|
71
72
|
0.05f,
|
|
72
73
|
0.05f,
|
|
73
74
|
indicator_h,
|
|
75
|
+
vroom::axis_opacity(y_axis_collapse_t),
|
|
76
|
+
vroom::axis_opacity(x_axis_collapse_t),
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
79
|
|
package/cpp/_core_src/chart.h
CHANGED
|
@@ -191,6 +191,13 @@ struct VroomChart {
|
|
|
191
191
|
float volume_collapse_t = 0.f;
|
|
192
192
|
int32_t volume_collapse_easing = VROOM_EASING_IN_OUT;
|
|
193
193
|
|
|
194
|
+
// Collapse of the axis strips, driven per-frame by the host animation loop:
|
|
195
|
+
// 0 = full strip, 1 = hidden. Unlike the volume collapse these change the
|
|
196
|
+
// layout, so the price pane grows into whatever the strips give up. The host
|
|
197
|
+
// pre-eases them (there is no per-element stagger to distribute here).
|
|
198
|
+
float y_axis_collapse_t = 0.f;
|
|
199
|
+
float x_axis_collapse_t = 0.f;
|
|
200
|
+
|
|
194
201
|
// --- drawings (annotations) --------------------------------------------
|
|
195
202
|
// Committed drawings, anchored in data space so they track the candles on
|
|
196
203
|
// pan/zoom. Drawn on the price pane above candles/overlays.
|
|
@@ -206,6 +213,8 @@ struct VroomChart {
|
|
|
206
213
|
float width = 2.f;
|
|
207
214
|
int32_t kind = 0;
|
|
208
215
|
std::vector<VroomDrawPoint> points; // pencil path (kind 2)
|
|
216
|
+
uint32_t fill = 0; // 0 alpha = derive from color
|
|
217
|
+
bool locked = false;
|
|
209
218
|
};
|
|
210
219
|
std::vector<StoredDrawing> drawings;
|
|
211
220
|
|
|
@@ -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) {
|
|
@@ -1374,6 +1401,15 @@ extern "C" void vroom_chart_set_volume_collapse(VroomChart* chart, float t,
|
|
|
1374
1401
|
chart->mark_dirty();
|
|
1375
1402
|
}
|
|
1376
1403
|
|
|
1404
|
+
extern "C" void vroom_chart_set_axis_collapse(VroomChart* chart, float y_t,
|
|
1405
|
+
float x_t) {
|
|
1406
|
+
if (!chart) return;
|
|
1407
|
+
chart->y_axis_collapse_t = std::clamp(y_t, 0.f, 1.f);
|
|
1408
|
+
chart->x_axis_collapse_t = std::clamp(x_t, 0.f, 1.f);
|
|
1409
|
+
// Changes the layout, so every cached pane geometry has to be re-derived.
|
|
1410
|
+
chart->mark_dirty();
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1377
1413
|
// ---- Direct draw (used by hosts that don't need the SkPicture cache) ------
|
|
1378
1414
|
|
|
1379
1415
|
extern "C" void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas) {
|
|
@@ -42,7 +42,9 @@ void draw_badge(SkCanvas* canvas,
|
|
|
42
42
|
float cx,
|
|
43
43
|
float cy,
|
|
44
44
|
SkColor fill,
|
|
45
|
-
SkColor text_color
|
|
45
|
+
SkColor text_color,
|
|
46
|
+
float opacity) {
|
|
47
|
+
if (opacity <= 0.f) return;
|
|
46
48
|
const size_t len = std::strlen(text);
|
|
47
49
|
SkRect tb;
|
|
48
50
|
const float text_w = font.measureText(text, len, SkTextEncoding::kUTF8, &tb);
|
|
@@ -54,11 +56,13 @@ void draw_badge(SkCanvas* canvas,
|
|
|
54
56
|
SkPaint box;
|
|
55
57
|
box.setAntiAlias(true);
|
|
56
58
|
box.setColor(fill);
|
|
59
|
+
box.setAlphaf(box.getAlphaf() * opacity);
|
|
57
60
|
canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
|
|
58
61
|
|
|
59
62
|
SkPaint text_paint;
|
|
60
63
|
text_paint.setAntiAlias(true);
|
|
61
64
|
text_paint.setColor(text_color);
|
|
65
|
+
text_paint.setAlphaf(text_paint.getAlphaf() * opacity);
|
|
62
66
|
canvas->drawString(text, cx - text_w * 0.5f,
|
|
63
67
|
cy - (tb.fTop + tb.fBottom) * 0.5f, font, text_paint);
|
|
64
68
|
}
|
|
@@ -108,8 +112,8 @@ void draw(SkCanvas* canvas,
|
|
|
108
112
|
ring.setStrokeWidth(2.f); // thicker border so the dot reads clearly
|
|
109
113
|
canvas->drawCircle(cx, cy, kRingRadius, ring);
|
|
110
114
|
|
|
111
|
-
// Axis badges
|
|
112
|
-
//
|
|
115
|
+
// Axis badges sit on top of the axis labels and the current-price indicator
|
|
116
|
+
// since the crosshair is the last draw step.
|
|
113
117
|
// They need the axis typeface; if it isn't loaded the lines alone suffice.
|
|
114
118
|
auto tf = vroom::axis_typeface();
|
|
115
119
|
if (!tf) return;
|
|
@@ -119,6 +123,7 @@ void draw(SkCanvas* canvas,
|
|
|
119
123
|
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
120
124
|
|
|
121
125
|
const SkColor badge_fill = chart.theme.colors[VROOM_COLOR_CROSSHAIR_TARGET];
|
|
126
|
+
const SkColor badge_text = chart.theme.colors[VROOM_COLOR_BADGE_TEXT];
|
|
122
127
|
|
|
123
128
|
// Date/time badge over the x-axis strip, centered on the vertical line and
|
|
124
129
|
// vertically aligned with the time labels (centered in the strip).
|
|
@@ -140,7 +145,7 @@ void draw(SkCanvas* canvas,
|
|
|
140
145
|
const float badge_cx =
|
|
141
146
|
std::clamp(cx, half_w, std::max(half_w, candle_right - half_w));
|
|
142
147
|
draw_badge(canvas, font, buf, badge_cx, strip_center_y, badge_fill,
|
|
143
|
-
|
|
148
|
+
badge_text, lay.x_axis_opacity);
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
// Price badge over the y-axis strip, centered on the horizontal line and
|
|
@@ -155,7 +160,7 @@ void draw(SkCanvas* canvas,
|
|
|
155
160
|
vroom::format_price(buf, sizeof(buf), price, fmt);
|
|
156
161
|
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
157
162
|
draw_badge(canvas, font, buf, axis_center_x, cy, badge_fill,
|
|
158
|
-
|
|
163
|
+
badge_text, lay.y_axis_opacity);
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -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
|
@@ -147,6 +147,9 @@ void draw_y_labels(SkCanvas* canvas,
|
|
|
147
147
|
const PriceBounds& bounds) {
|
|
148
148
|
auto tf = vroom::axis_typeface();
|
|
149
149
|
if (!tf) return;
|
|
150
|
+
// The fade state still advances in update_y_fades, so a hidden axis picks up
|
|
151
|
+
// wherever the ticks moved to once it is revealed again.
|
|
152
|
+
if (lay.y_axis_opacity <= 0.f) return;
|
|
150
153
|
|
|
151
154
|
const float candle_area_h = vroom::price_pane_bottom(lay);
|
|
152
155
|
|
|
@@ -186,7 +189,7 @@ void draw_y_labels(SkCanvas* canvas,
|
|
|
186
189
|
if (baseline_y - cap_h < 0.f) continue;
|
|
187
190
|
if (baseline_y > candle_area_h) continue;
|
|
188
191
|
|
|
189
|
-
text_paint.setAlphaf(f.opacity);
|
|
192
|
+
text_paint.setAlphaf(f.opacity * lay.y_axis_opacity);
|
|
190
193
|
canvas->drawString(buf, text_x, baseline_y, font, text_paint);
|
|
191
194
|
}
|
|
192
195
|
}
|
|
@@ -279,6 +282,8 @@ void draw_x_labels(SkCanvas* canvas,
|
|
|
279
282
|
auto tf = vroom::axis_typeface();
|
|
280
283
|
if (!tf || chart.candles.empty()) return;
|
|
281
284
|
if (end_ms <= start_ms) return;
|
|
285
|
+
// See draw_y_labels: update_x_fades keeps running while the strip is hidden.
|
|
286
|
+
if (lay.x_axis_opacity <= 0.f) return;
|
|
282
287
|
|
|
283
288
|
// X-axis labels live in the bottom strip, which stays anchored regardless
|
|
284
289
|
// of any indicator pane above it.
|
|
@@ -345,7 +350,7 @@ void draw_x_labels(SkCanvas* canvas,
|
|
|
345
350
|
if (text_x < 0.f) continue;
|
|
346
351
|
if (text_x + text_w > candle_area_w) continue;
|
|
347
352
|
|
|
348
|
-
text_paint.setAlphaf(f.opacity);
|
|
353
|
+
text_paint.setAlphaf(f.opacity * lay.x_axis_opacity);
|
|
349
354
|
canvas->drawString(buf, text_x, baseline_y, font, text_paint);
|
|
350
355
|
}
|
|
351
356
|
}
|
|
@@ -211,14 +211,15 @@ void draw(SkCanvas* canvas,
|
|
|
211
211
|
}
|
|
212
212
|
canvas->restore();
|
|
213
213
|
|
|
214
|
-
// "0" label in the y-axis strip at the zero line.
|
|
215
|
-
if (tf) {
|
|
214
|
+
// "0" label in the y-axis strip at the zero line — hidden with that strip.
|
|
215
|
+
if (tf && lay.y_axis_opacity > 0.f) {
|
|
216
216
|
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
217
217
|
font.setSubpixel(true);
|
|
218
218
|
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
219
219
|
SkPaint text_paint;
|
|
220
220
|
text_paint.setAntiAlias(true);
|
|
221
221
|
text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
|
|
222
|
+
text_paint.setAlphaf(text_paint.getAlphaf() * lay.y_axis_opacity);
|
|
222
223
|
SkRect tb;
|
|
223
224
|
const float tw =
|
|
224
225
|
font.measureText("0", 1, SkTextEncoding::kUTF8, &tb);
|
|
@@ -59,9 +59,11 @@ void draw(SkCanvas* canvas,
|
|
|
59
59
|
canvas->drawLine(0.f, y, candle_right, y, line);
|
|
60
60
|
|
|
61
61
|
// Price box in the y-axis strip. Needs the axis typeface; if it isn't
|
|
62
|
-
// loaded yet, the line alone still conveys the level.
|
|
62
|
+
// loaded yet, the line alone still conveys the level. Same when the y-axis
|
|
63
|
+
// is hidden — the level stays readable, it just loses its badge.
|
|
63
64
|
auto tf = vroom::axis_typeface();
|
|
64
65
|
if (!tf) return;
|
|
66
|
+
if (lay.y_axis_opacity <= 0.f) return;
|
|
65
67
|
|
|
66
68
|
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
67
69
|
font.setSubpixel(true);
|
|
@@ -95,14 +97,16 @@ void draw(SkCanvas* canvas,
|
|
|
95
97
|
SkPaint box;
|
|
96
98
|
box.setAntiAlias(true);
|
|
97
99
|
box.setColor(color);
|
|
100
|
+
box.setAlphaf(box.getAlphaf() * lay.y_axis_opacity);
|
|
98
101
|
canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
|
|
99
102
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
+
// Price text horizontally centered on the same axis center, and vertically
|
|
104
|
+
// centered: shift the baseline so the glyph bounds' midpoint lands on `y`
|
|
105
|
+
// (= the box center and the dotted line).
|
|
103
106
|
SkPaint text_paint;
|
|
104
107
|
text_paint.setAntiAlias(true);
|
|
105
|
-
text_paint.setColor(
|
|
108
|
+
text_paint.setColor(chart.theme.colors[VROOM_COLOR_BADGE_TEXT]);
|
|
109
|
+
text_paint.setAlphaf(text_paint.getAlphaf() * lay.y_axis_opacity);
|
|
106
110
|
const float text_x = axis_center_x - text_w * 0.5f;
|
|
107
111
|
const float baseline_y = y - (tb.fTop + tb.fBottom) * 0.5f;
|
|
108
112
|
canvas->drawString(buf, text_x, baseline_y, font, text_paint);
|
|
@@ -215,8 +215,10 @@ void draw_axis_badge(SkCanvas* canvas,
|
|
|
215
215
|
double price,
|
|
216
216
|
float y,
|
|
217
217
|
SkColor fill,
|
|
218
|
+
SkColor text_color,
|
|
218
219
|
const PriceFormat& fmt) {
|
|
219
220
|
if (lay.y_axis_width_px <= 0.f) return;
|
|
221
|
+
if (lay.y_axis_opacity <= 0.f) return;
|
|
220
222
|
|
|
221
223
|
char buf[48];
|
|
222
224
|
vroom::format_price(buf, sizeof(buf), price, fmt);
|
|
@@ -233,11 +235,13 @@ void draw_axis_badge(SkCanvas* canvas,
|
|
|
233
235
|
SkPaint box;
|
|
234
236
|
box.setAntiAlias(true);
|
|
235
237
|
box.setColor(fill);
|
|
238
|
+
box.setAlphaf(box.getAlphaf() * lay.y_axis_opacity);
|
|
236
239
|
canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
|
|
237
240
|
|
|
238
241
|
SkPaint text;
|
|
239
242
|
text.setAntiAlias(true);
|
|
240
|
-
text.setColor(
|
|
243
|
+
text.setColor(text_color);
|
|
244
|
+
text.setAlphaf(text.getAlphaf() * lay.y_axis_opacity);
|
|
241
245
|
canvas->drawString(buf, cx - text_w * 0.5f,
|
|
242
246
|
y - (tb.fTop + tb.fBottom) * 0.5f, font, text);
|
|
243
247
|
}
|
|
@@ -254,6 +258,7 @@ void draw(SkCanvas* canvas,
|
|
|
254
258
|
if (chart.price_lines.empty()) return;
|
|
255
259
|
|
|
256
260
|
const VroomPriceLineStyle& style = chart.price_line_style;
|
|
261
|
+
const SkColor badge_text = chart.theme.colors[VROOM_COLOR_BADGE_TEXT];
|
|
257
262
|
SkFont font;
|
|
258
263
|
const bool has_font = label_font(chart, &font);
|
|
259
264
|
const vroom::PriceFormat fmt = vroom::with_tick_guard(
|
|
@@ -336,12 +341,12 @@ void draw(SkCanvas* canvas,
|
|
|
336
341
|
line_color);
|
|
337
342
|
}
|
|
338
343
|
if (!group.quantity.empty()) {
|
|
339
|
-
// Solid fill +
|
|
344
|
+
// Solid fill + badge text, so size reads at a glance against the
|
|
340
345
|
// translucent body.
|
|
341
346
|
draw_pill(canvas, group.quantity, first == &group.quantity,
|
|
342
347
|
last == &group.quantity, line_color, SK_ColorTRANSPARENT);
|
|
343
348
|
draw_centered_text(canvas, font, pl.quantity, group.quantity, y,
|
|
344
|
-
|
|
349
|
+
badge_text);
|
|
345
350
|
}
|
|
346
351
|
if (!group.close.empty()) {
|
|
347
352
|
const SkColor close_color =
|
|
@@ -357,7 +362,7 @@ void draw(SkCanvas* canvas,
|
|
|
357
362
|
|
|
358
363
|
if (has_font && (pl.flags & VROOM_PRICE_LINE_AXIS_LABEL) != 0) {
|
|
359
364
|
draw_axis_badge(canvas, font, lay, render_price(chart, i), y,
|
|
360
|
-
line_color, fmt);
|
|
365
|
+
line_color, badge_text, fmt);
|
|
361
366
|
}
|
|
362
367
|
}
|
|
363
368
|
}
|
|
@@ -158,13 +158,15 @@ void draw(SkCanvas* canvas,
|
|
|
158
158
|
// 70 / 30 labels in the y-axis strip (drawn after the clip; the strip's
|
|
159
159
|
// background was already masked by draw_chart before this call). They label
|
|
160
160
|
// the band rules, so they go with them.
|
|
161
|
-
|
|
161
|
+
// The band labels live in the y-axis strip, so they go with it when it hides.
|
|
162
|
+
if (tf && cfg.bands_visible && lay.y_axis_opacity > 0.f) {
|
|
162
163
|
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
163
164
|
font.setSubpixel(true);
|
|
164
165
|
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
165
166
|
SkPaint text_paint;
|
|
166
167
|
text_paint.setAntiAlias(true);
|
|
167
168
|
text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
|
|
169
|
+
text_paint.setAlphaf(text_paint.getAlphaf() * lay.y_axis_opacity);
|
|
168
170
|
|
|
169
171
|
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
170
172
|
auto label = [&](const char* s, float y) {
|