react-native-vroom-chart 0.5.0 → 0.6.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/android/CMakeLists.txt +143 -0
- package/android/README.md +60 -5
- package/android/build.gradle +115 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/VroomChartJni.cpp +24 -0
- package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
- package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
- package/cpp/VroomChartHostObject.cpp +218 -1
- package/cpp/VroomJsiInstaller.cpp +11 -1
- package/cpp/VroomSkiaContext.cpp +15 -7
- package/cpp/_core_include/vroom/vroom_chart.h +70 -0
- package/cpp/_core_src/chart.cpp +7 -0
- package/cpp/_core_src/chart.h +27 -0
- package/cpp/_core_src/chart_facade.cpp +91 -0
- package/cpp/_core_src/price_line_layout.cpp +87 -0
- package/cpp/_core_src/price_line_layout.h +80 -0
- package/cpp/_core_src/price_lines.cpp +428 -0
- package/cpp/_core_src/price_lines.h +56 -0
- package/lib/index.d.mts +104 -1
- package/lib/index.d.ts +104 -1
- package/lib/index.js +98 -7
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +98 -7
- package/lib/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/VroomChart.tsx +93 -8
- package/src/index.ts +2 -0
- package/src/jsi.d.ts +48 -0
- package/src/types.ts +2 -0
- package/src/useChartCore.ts +65 -3
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#include "price_lines.h"
|
|
2
|
+
|
|
3
|
+
#pragma clang diagnostic push
|
|
4
|
+
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
5
|
+
#include "include/core/SkCanvas.h"
|
|
6
|
+
#include "include/core/SkColor.h"
|
|
7
|
+
#include "include/core/SkFont.h"
|
|
8
|
+
#include "include/core/SkFontTypes.h"
|
|
9
|
+
#include "include/core/SkPaint.h"
|
|
10
|
+
#include "include/core/SkPathEffect.h"
|
|
11
|
+
#include "include/core/SkRRect.h"
|
|
12
|
+
#include "include/core/SkRect.h"
|
|
13
|
+
#include "include/core/SkTypeface.h"
|
|
14
|
+
#include "include/effects/SkDashPathEffect.h"
|
|
15
|
+
#pragma clang diagnostic pop
|
|
16
|
+
|
|
17
|
+
#include <algorithm>
|
|
18
|
+
#include <cmath>
|
|
19
|
+
#include <cstdio>
|
|
20
|
+
#include <cstring>
|
|
21
|
+
|
|
22
|
+
#include "chart.h"
|
|
23
|
+
#include "fonts.h"
|
|
24
|
+
#include "price_line_layout.h"
|
|
25
|
+
#include "theme.h"
|
|
26
|
+
#include "viewport.h"
|
|
27
|
+
|
|
28
|
+
namespace vroom::price_lines {
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
constexpr SkScalar kDotted[2] = {2.f, 2.f};
|
|
33
|
+
constexpr SkScalar kDashed[2] = {6.f, 4.f};
|
|
34
|
+
constexpr float kBorderWidth = 1.f; // pill outline
|
|
35
|
+
constexpr float kGhostAlpha = 0.35f; // pre-drag position marker
|
|
36
|
+
constexpr float kCloseInset = 6.f; // X glyph inset within its square cell
|
|
37
|
+
constexpr float kCloseStroke = 1.5f;
|
|
38
|
+
constexpr float kDividerAlpha = 0.5f;
|
|
39
|
+
|
|
40
|
+
// The reference glyph whose tight bounds set every pill's height, so a group's
|
|
41
|
+
// segments stay the same height whether or not their text has descenders — and
|
|
42
|
+
// match the price badge, which measures digits.
|
|
43
|
+
constexpr char kHeightRef[] = "0";
|
|
44
|
+
|
|
45
|
+
// Dash pattern for a VroomPriceLine::line_style. Null = solid.
|
|
46
|
+
sk_sp<SkPathEffect> dash_for(int32_t line_style) {
|
|
47
|
+
if (line_style == 1) return SkDashPathEffect::Make(kDotted, 0.f);
|
|
48
|
+
if (line_style == 2) return SkDashPathEffect::Make(kDashed, 0.f);
|
|
49
|
+
return nullptr;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Brightens `c`'s channels by `mul` (clamped at white), leaving alpha alone.
|
|
53
|
+
// Used for the hovered segment so a line lifts under the cursor without
|
|
54
|
+
// changing size or position.
|
|
55
|
+
SkColor boost(SkColor c, float mul) {
|
|
56
|
+
if (mul <= 1.f) return c;
|
|
57
|
+
const auto up = [mul](U8CPU v) {
|
|
58
|
+
return static_cast<U8CPU>(
|
|
59
|
+
std::min(255.f, static_cast<float>(v) * mul + 0.5f));
|
|
60
|
+
};
|
|
61
|
+
return SkColorSetARGB(SkColorGetA(c), up(SkColorGetR(c)), up(SkColorGetG(c)),
|
|
62
|
+
up(SkColorGetB(c)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The price a line renders at: the live drag position while it's being dragged,
|
|
66
|
+
// otherwise its committed price. The committed value is never mutated — the host
|
|
67
|
+
// applies or rejects the drag by restating its lines.
|
|
68
|
+
double render_price(const VroomChart& chart, size_t i) {
|
|
69
|
+
if (chart.dragged_price_line == static_cast<int32_t>(i)) {
|
|
70
|
+
return chart.dragged_price_line_price;
|
|
71
|
+
}
|
|
72
|
+
return chart.price_lines[i].price;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The label font: the axis typeface, emboldened (these labels are chrome that
|
|
76
|
+
// must stay legible over candles). Returns false when no typeface is loaded yet,
|
|
77
|
+
// in which case callers fall back to lines without labels.
|
|
78
|
+
bool label_font(const VroomChart& chart, SkFont* out) {
|
|
79
|
+
auto tf = vroom::axis_typeface();
|
|
80
|
+
if (!tf) return false;
|
|
81
|
+
const float size = chart.price_line_style.font_size_px > 0.f
|
|
82
|
+
? chart.price_line_style.font_size_px
|
|
83
|
+
: chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX];
|
|
84
|
+
*out = SkFont(tf, size);
|
|
85
|
+
out->setSubpixel(true);
|
|
86
|
+
out->setEmbolden(true);
|
|
87
|
+
out->setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
float text_width(const SkFont& font, const std::string& s) {
|
|
92
|
+
if (s.empty()) return 0.f;
|
|
93
|
+
return font.measureText(s.data(), s.size(), SkTextEncoding::kUTF8);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Measures one line's label so both passes lay it out identically.
|
|
97
|
+
LabelMetrics metrics_for(const SkFont& font,
|
|
98
|
+
const VroomChart::StoredPriceLine& pl) {
|
|
99
|
+
SkRect ref;
|
|
100
|
+
font.measureText(kHeightRef, std::strlen(kHeightRef), SkTextEncoding::kUTF8,
|
|
101
|
+
&ref);
|
|
102
|
+
|
|
103
|
+
LabelMetrics m;
|
|
104
|
+
m.text_w = text_width(font, pl.text);
|
|
105
|
+
m.quantity_w = text_width(font, pl.quantity);
|
|
106
|
+
m.label_h = ref.height() + 2.f * kPadV;
|
|
107
|
+
m.closable = (pl.flags & VROOM_PRICE_LINE_CLOSABLE) != 0;
|
|
108
|
+
return m;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Rounds only the corners on the group's outer edges, so butted segments read as
|
|
112
|
+
// one continuous pill with dividers rather than a row of separate lozenges.
|
|
113
|
+
SkRRect segment_rrect(const Rect& r, bool round_left, bool round_right) {
|
|
114
|
+
const SkRect rect = SkRect::MakeLTRB(r.left, r.top, r.right, r.bottom);
|
|
115
|
+
const float l = round_left ? kCorner : 0.f;
|
|
116
|
+
const float rr = round_right ? kCorner : 0.f;
|
|
117
|
+
// setRectRadii takes corners in UL, UR, LR, LL order.
|
|
118
|
+
const SkVector radii[4] = {{l, l}, {rr, rr}, {rr, rr}, {l, l}};
|
|
119
|
+
SkRRect out;
|
|
120
|
+
out.setRectRadii(rect, radii);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Draws `s` horizontally centered in `cell` and vertically centered on `cy`,
|
|
125
|
+
// shifting the baseline so the glyphs' own midpoint lands on the line.
|
|
126
|
+
void draw_centered_text(SkCanvas* canvas,
|
|
127
|
+
const SkFont& font,
|
|
128
|
+
const std::string& s,
|
|
129
|
+
const Rect& cell,
|
|
130
|
+
float cy,
|
|
131
|
+
SkColor color) {
|
|
132
|
+
if (s.empty()) return;
|
|
133
|
+
SkRect tb;
|
|
134
|
+
const float w =
|
|
135
|
+
font.measureText(s.data(), s.size(), SkTextEncoding::kUTF8, &tb);
|
|
136
|
+
|
|
137
|
+
SkPaint paint;
|
|
138
|
+
paint.setAntiAlias(true);
|
|
139
|
+
paint.setColor(color);
|
|
140
|
+
const float cx = (cell.left + cell.right) * 0.5f;
|
|
141
|
+
canvas->drawString(s.c_str(), cx - w * 0.5f,
|
|
142
|
+
cy - (tb.fTop + tb.fBottom) * 0.5f, font, paint);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Fills a segment and strokes its 1px outline.
|
|
146
|
+
void draw_pill(SkCanvas* canvas,
|
|
147
|
+
const Rect& r,
|
|
148
|
+
bool round_left,
|
|
149
|
+
bool round_right,
|
|
150
|
+
SkColor fill,
|
|
151
|
+
SkColor border) {
|
|
152
|
+
const SkRRect rr = segment_rrect(r, round_left, round_right);
|
|
153
|
+
|
|
154
|
+
SkPaint bg;
|
|
155
|
+
bg.setAntiAlias(true);
|
|
156
|
+
bg.setColor(fill);
|
|
157
|
+
canvas->drawRRect(rr, bg);
|
|
158
|
+
|
|
159
|
+
if (SkColorGetA(border) == 0) return;
|
|
160
|
+
SkPaint outline;
|
|
161
|
+
outline.setAntiAlias(true);
|
|
162
|
+
outline.setColor(border);
|
|
163
|
+
outline.setStyle(SkPaint::kStroke_Style);
|
|
164
|
+
outline.setStrokeWidth(kBorderWidth);
|
|
165
|
+
// Inset by half the stroke so the outline sits inside the segment and butted
|
|
166
|
+
// neighbours don't double up along their shared edge.
|
|
167
|
+
canvas->drawRRect(segment_rrect(
|
|
168
|
+
Rect{r.left + kBorderWidth * 0.5f,
|
|
169
|
+
r.top + kBorderWidth * 0.5f,
|
|
170
|
+
r.right - kBorderWidth * 0.5f,
|
|
171
|
+
r.bottom - kBorderWidth * 0.5f},
|
|
172
|
+
round_left, round_right),
|
|
173
|
+
outline);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The close affordance: an X stroked inside its square cell.
|
|
177
|
+
void draw_close_icon(SkCanvas* canvas, const Rect& cell, SkColor color) {
|
|
178
|
+
SkPaint paint;
|
|
179
|
+
paint.setAntiAlias(true);
|
|
180
|
+
paint.setColor(color);
|
|
181
|
+
paint.setStyle(SkPaint::kStroke_Style);
|
|
182
|
+
paint.setStrokeWidth(kCloseStroke);
|
|
183
|
+
paint.setStrokeCap(SkPaint::kRound_Cap);
|
|
184
|
+
|
|
185
|
+
// Keep the glyph square even if the cell isn't, and never let a tiny cell
|
|
186
|
+
// invert the inset.
|
|
187
|
+
const float inset =
|
|
188
|
+
std::min(kCloseInset, std::min(cell.width(), cell.bottom - cell.top) * 0.5f);
|
|
189
|
+
const float l = cell.left + inset;
|
|
190
|
+
const float r = cell.right - inset;
|
|
191
|
+
const float t = cell.top + inset;
|
|
192
|
+
const float b = cell.bottom - inset;
|
|
193
|
+
canvas->drawLine(l, t, r, b, paint);
|
|
194
|
+
canvas->drawLine(r, t, l, b, paint);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A 1px vertical rule on a segment's left edge, separating butted segments.
|
|
198
|
+
void draw_divider(SkCanvas* canvas, const Rect& seg, SkColor color) {
|
|
199
|
+
SkPaint paint;
|
|
200
|
+
paint.setAntiAlias(false);
|
|
201
|
+
paint.setColor(SkColorSetA(
|
|
202
|
+
color, static_cast<U8CPU>(SkColorGetA(color) * kDividerAlpha)));
|
|
203
|
+
paint.setStrokeWidth(1.f);
|
|
204
|
+
canvas->drawLine(seg.left, seg.top + kBorderWidth, seg.left,
|
|
205
|
+
seg.bottom - kBorderWidth, paint);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The price badge in the y-axis strip, matching the current-price indicator's
|
|
209
|
+
// geometry so the two read as the same chrome.
|
|
210
|
+
void draw_axis_badge(SkCanvas* canvas,
|
|
211
|
+
const SkFont& font,
|
|
212
|
+
const Layout& lay,
|
|
213
|
+
double price,
|
|
214
|
+
float y,
|
|
215
|
+
SkColor fill) {
|
|
216
|
+
if (lay.y_axis_width_px <= 0.f) return;
|
|
217
|
+
|
|
218
|
+
char buf[32];
|
|
219
|
+
std::snprintf(buf, sizeof(buf), "%.2f", price);
|
|
220
|
+
const size_t len = std::strlen(buf);
|
|
221
|
+
|
|
222
|
+
SkRect tb;
|
|
223
|
+
const float text_w = font.measureText(buf, len, SkTextEncoding::kUTF8, &tb);
|
|
224
|
+
const float box_w = text_w + 2.f * kPadH;
|
|
225
|
+
const float box_h = tb.height() + 2.f * kPadV;
|
|
226
|
+
const float cx = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
227
|
+
const SkRect rect =
|
|
228
|
+
SkRect::MakeXYWH(cx - box_w * 0.5f, y - box_h * 0.5f, box_w, box_h);
|
|
229
|
+
|
|
230
|
+
SkPaint box;
|
|
231
|
+
box.setAntiAlias(true);
|
|
232
|
+
box.setColor(fill);
|
|
233
|
+
canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
|
|
234
|
+
|
|
235
|
+
SkPaint text;
|
|
236
|
+
text.setAntiAlias(true);
|
|
237
|
+
text.setColor(SK_ColorWHITE);
|
|
238
|
+
canvas->drawString(buf, cx - text_w * 0.5f,
|
|
239
|
+
y - (tb.fTop + tb.fBottom) * 0.5f, font, text);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
} // namespace
|
|
243
|
+
|
|
244
|
+
void draw(SkCanvas* canvas,
|
|
245
|
+
const VroomChart& chart,
|
|
246
|
+
const Layout& lay,
|
|
247
|
+
const PriceBounds& bounds,
|
|
248
|
+
float candle_right,
|
|
249
|
+
float candle_area_h) {
|
|
250
|
+
if (!canvas || candle_right <= 0.f || candle_area_h <= 0.f) return;
|
|
251
|
+
if (chart.price_lines.empty()) return;
|
|
252
|
+
|
|
253
|
+
const VroomPriceLineStyle& style = chart.price_line_style;
|
|
254
|
+
SkFont font;
|
|
255
|
+
const bool has_font = label_font(chart, &font);
|
|
256
|
+
|
|
257
|
+
for (size_t i = 0; i < chart.price_lines.size(); ++i) {
|
|
258
|
+
const VroomChart::StoredPriceLine& pl = chart.price_lines[i];
|
|
259
|
+
const bool dragging = chart.dragged_price_line == static_cast<int32_t>(i);
|
|
260
|
+
const bool hovered = chart.hovered_price_line == static_cast<int32_t>(i);
|
|
261
|
+
|
|
262
|
+
const float y = vroom::price_to_y(lay, bounds, render_price(chart, i));
|
|
263
|
+
if (y < 0.f || y > candle_area_h) continue; // off-pane: cull, don't clamp
|
|
264
|
+
|
|
265
|
+
const SkColor color = static_cast<SkColor>(pl.color);
|
|
266
|
+
const float stroke = pl.width > 0.f ? pl.width : 1.f;
|
|
267
|
+
// Hovering the line body lifts the whole line; hovering just the close
|
|
268
|
+
// button lifts only that button (handled below).
|
|
269
|
+
const bool line_hot = hovered && chart.hovered_price_line_part == 0;
|
|
270
|
+
const SkColor line_color =
|
|
271
|
+
line_hot || dragging ? boost(color, style.hover_boost) : color;
|
|
272
|
+
|
|
273
|
+
SkPaint line;
|
|
274
|
+
line.setAntiAlias(true);
|
|
275
|
+
line.setColor(line_color);
|
|
276
|
+
line.setStrokeWidth(stroke);
|
|
277
|
+
line.setPathEffect(dash_for(pl.line_style));
|
|
278
|
+
|
|
279
|
+
// A faint marker at the pre-drag price, so the user can see how far
|
|
280
|
+
// they've moved and where releasing outside will snap back to.
|
|
281
|
+
if (dragging) {
|
|
282
|
+
const float ghost_y = vroom::price_to_y(lay, bounds, pl.price);
|
|
283
|
+
if (ghost_y >= 0.f && ghost_y <= candle_area_h) {
|
|
284
|
+
SkPaint ghost = line;
|
|
285
|
+
ghost.setColor(SkColorSetA(
|
|
286
|
+
color, static_cast<U8CPU>(SkColorGetA(color) * kGhostAlpha)));
|
|
287
|
+
canvas->drawLine(0.f, ghost_y, candle_right, ghost_y, ghost);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const bool extend_left = (pl.flags & VROOM_PRICE_LINE_EXTEND_LEFT) != 0;
|
|
292
|
+
|
|
293
|
+
LabelMetrics metrics;
|
|
294
|
+
if (has_font) metrics = metrics_for(font, pl);
|
|
295
|
+
const GroupLayout group = layout_group(metrics, y, candle_right, style);
|
|
296
|
+
|
|
297
|
+
if (group.empty()) {
|
|
298
|
+
// Nothing to anchor a partial span to, so a bare line always spans
|
|
299
|
+
// the pane.
|
|
300
|
+
canvas->drawLine(0.f, y, candle_right, y, line);
|
|
301
|
+
} else {
|
|
302
|
+
// Two segments with the label group punched out between them, so the
|
|
303
|
+
// dashes don't show through the translucent pills.
|
|
304
|
+
if (extend_left && group.left > 0.f) {
|
|
305
|
+
canvas->drawLine(0.f, y, group.left, y, line);
|
|
306
|
+
}
|
|
307
|
+
if (group.right < candle_right) {
|
|
308
|
+
canvas->drawLine(group.right, y, candle_right, y, line);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!group.empty()) {
|
|
313
|
+
const SkColor body_bg = static_cast<SkColor>(style.body_bg);
|
|
314
|
+
const bool close_hot = hovered && chart.hovered_price_line_part == 1;
|
|
315
|
+
|
|
316
|
+
// Only the group's outermost corners are rounded.
|
|
317
|
+
const Rect* first = !group.body.empty()
|
|
318
|
+
? &group.body
|
|
319
|
+
: (!group.quantity.empty() ? &group.quantity
|
|
320
|
+
: &group.close);
|
|
321
|
+
const Rect* last = !group.close.empty()
|
|
322
|
+
? &group.close
|
|
323
|
+
: (!group.quantity.empty() ? &group.quantity
|
|
324
|
+
: &group.body);
|
|
325
|
+
|
|
326
|
+
if (!group.body.empty()) {
|
|
327
|
+
draw_pill(canvas, group.body, first == &group.body,
|
|
328
|
+
last == &group.body, body_bg, line_color);
|
|
329
|
+
draw_centered_text(canvas, font, pl.text, group.body, y,
|
|
330
|
+
line_color);
|
|
331
|
+
}
|
|
332
|
+
if (!group.quantity.empty()) {
|
|
333
|
+
// Solid fill + white text, so size reads at a glance against the
|
|
334
|
+
// translucent body.
|
|
335
|
+
draw_pill(canvas, group.quantity, first == &group.quantity,
|
|
336
|
+
last == &group.quantity, line_color, SK_ColorTRANSPARENT);
|
|
337
|
+
draw_centered_text(canvas, font, pl.quantity, group.quantity, y,
|
|
338
|
+
SK_ColorWHITE);
|
|
339
|
+
}
|
|
340
|
+
if (!group.close.empty()) {
|
|
341
|
+
const SkColor close_color =
|
|
342
|
+
close_hot ? boost(color, style.hover_boost) : line_color;
|
|
343
|
+
draw_pill(canvas, group.close, first == &group.close,
|
|
344
|
+
last == &group.close, body_bg, close_color);
|
|
345
|
+
if (!group.quantity.empty()) {
|
|
346
|
+
draw_divider(canvas, group.close, close_color);
|
|
347
|
+
}
|
|
348
|
+
draw_close_icon(canvas, group.close, close_color);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (has_font && (pl.flags & VROOM_PRICE_LINE_AXIS_LABEL) != 0) {
|
|
353
|
+
draw_axis_badge(canvas, font, lay, render_price(chart, i), y,
|
|
354
|
+
line_color);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
HitResult hit_test(const VroomChart& chart,
|
|
360
|
+
const Layout& lay,
|
|
361
|
+
const PriceBounds& bounds,
|
|
362
|
+
float candle_right,
|
|
363
|
+
float candle_area_h,
|
|
364
|
+
float x,
|
|
365
|
+
float y) {
|
|
366
|
+
HitResult miss{-1, -1};
|
|
367
|
+
if (chart.price_lines.empty()) return miss;
|
|
368
|
+
if (candle_right <= 0.f || candle_area_h <= 0.f) return miss;
|
|
369
|
+
|
|
370
|
+
const VroomPriceLineStyle& style = chart.price_line_style;
|
|
371
|
+
SkFont font;
|
|
372
|
+
const bool has_font = label_font(chart, &font);
|
|
373
|
+
|
|
374
|
+
// Close buttons first — they sit on top of the line, so a press there must
|
|
375
|
+
// never be read as the start of a drag.
|
|
376
|
+
int32_t best = -1;
|
|
377
|
+
float best_dist = 0.f;
|
|
378
|
+
for (size_t i = 0; i < chart.price_lines.size(); ++i) {
|
|
379
|
+
const VroomChart::StoredPriceLine& pl = chart.price_lines[i];
|
|
380
|
+
if ((pl.flags & VROOM_PRICE_LINE_CLOSABLE) == 0) continue;
|
|
381
|
+
if (!has_font) continue; // no font, no rendered button to press
|
|
382
|
+
|
|
383
|
+
const float ly = vroom::price_to_y(lay, bounds, render_price(chart, i));
|
|
384
|
+
if (ly < 0.f || ly > candle_area_h) continue;
|
|
385
|
+
|
|
386
|
+
const GroupLayout group =
|
|
387
|
+
layout_group(metrics_for(font, pl), ly, candle_right, style);
|
|
388
|
+
if (!contains(group.close, x, y)) continue;
|
|
389
|
+
|
|
390
|
+
const float dist = std::fabs(y - ly);
|
|
391
|
+
if (best < 0 || dist < best_dist) {
|
|
392
|
+
best = static_cast<int32_t>(i);
|
|
393
|
+
best_dist = dist;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (best >= 0) return HitResult{best, 1};
|
|
397
|
+
|
|
398
|
+
// Then draggable lines, nearest in y.
|
|
399
|
+
for (size_t i = 0; i < chart.price_lines.size(); ++i) {
|
|
400
|
+
const VroomChart::StoredPriceLine& pl = chart.price_lines[i];
|
|
401
|
+
if ((pl.flags & VROOM_PRICE_LINE_DRAGGABLE) == 0) continue;
|
|
402
|
+
|
|
403
|
+
const float ly = vroom::price_to_y(lay, bounds, render_price(chart, i));
|
|
404
|
+
if (ly < 0.f || ly > candle_area_h) continue;
|
|
405
|
+
if (!hits_line(ly, pl.width, y)) continue;
|
|
406
|
+
|
|
407
|
+
// Don't grab where the line isn't drawn: without extend_left only the
|
|
408
|
+
// label group and the run to the axis are live.
|
|
409
|
+
if ((pl.flags & VROOM_PRICE_LINE_EXTEND_LEFT) == 0) {
|
|
410
|
+
const GroupLayout group = layout_group(
|
|
411
|
+
has_font ? metrics_for(font, pl) : LabelMetrics{}, ly,
|
|
412
|
+
candle_right, style);
|
|
413
|
+
if (!group.empty() && x < group.left) continue;
|
|
414
|
+
}
|
|
415
|
+
if (x < 0.f || x > candle_right) continue;
|
|
416
|
+
|
|
417
|
+
const float dist = std::fabs(y - ly);
|
|
418
|
+
if (best < 0 || dist < best_dist) {
|
|
419
|
+
best = static_cast<int32_t>(i);
|
|
420
|
+
best_dist = dist;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (best >= 0) return HitResult{best, 0};
|
|
424
|
+
|
|
425
|
+
return miss;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
} // namespace vroom::price_lines
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Consumer-supplied horizontal price status lines — the primitive behind resting
|
|
2
|
+
// limit orders, take-profits and liquidation levels.
|
|
3
|
+
//
|
|
4
|
+
// Each line is a styled horizontal rule across the price pane ending in a label
|
|
5
|
+
// group (body pill + optional quantity pill + optional close button) and an
|
|
6
|
+
// optional price badge in the y-axis strip, closely modeled on the current-price
|
|
7
|
+
// indicator it sits beside. Lines can be dragged vertically and closed; the
|
|
8
|
+
// interaction state (hover, live drag price) lives on the chart and is driven by
|
|
9
|
+
// the host's gesture layer through the public facade.
|
|
10
|
+
//
|
|
11
|
+
// Geometry is computed in price_line_layout.h so the draw and hit-test passes
|
|
12
|
+
// agree by construction.
|
|
13
|
+
|
|
14
|
+
#pragma once
|
|
15
|
+
|
|
16
|
+
#include <cstdint>
|
|
17
|
+
|
|
18
|
+
class SkCanvas;
|
|
19
|
+
struct VroomChart;
|
|
20
|
+
|
|
21
|
+
namespace vroom {
|
|
22
|
+
struct Layout;
|
|
23
|
+
struct PriceBounds;
|
|
24
|
+
} // namespace vroom
|
|
25
|
+
|
|
26
|
+
namespace vroom::price_lines {
|
|
27
|
+
|
|
28
|
+
// Draws every price line. `candle_right` is the inner edge of the y-axis strip
|
|
29
|
+
// and `candle_area_h` the y of the x-axis separator; a line whose price maps
|
|
30
|
+
// outside [0, candle_area_h] is skipped rather than clamped.
|
|
31
|
+
void draw(SkCanvas* canvas,
|
|
32
|
+
const VroomChart& chart,
|
|
33
|
+
const Layout& lay,
|
|
34
|
+
const PriceBounds& bounds,
|
|
35
|
+
float candle_right,
|
|
36
|
+
float candle_area_h);
|
|
37
|
+
|
|
38
|
+
// A hit against a price line. `index` is -1 for a miss; `part` is 0 for the line
|
|
39
|
+
// or its label body (the drag target) and 1 for the close button.
|
|
40
|
+
struct HitResult {
|
|
41
|
+
int32_t index;
|
|
42
|
+
int32_t part;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Hit-tests pixel (x, y). Close buttons win over lines, and among several
|
|
46
|
+
// candidates the nearest in y wins. Only draggable lines report part 0 and only
|
|
47
|
+
// closable lines report part 1.
|
|
48
|
+
HitResult hit_test(const VroomChart& chart,
|
|
49
|
+
const Layout& lay,
|
|
50
|
+
const PriceBounds& bounds,
|
|
51
|
+
float candle_right,
|
|
52
|
+
float candle_area_h,
|
|
53
|
+
float x,
|
|
54
|
+
float y);
|
|
55
|
+
|
|
56
|
+
} // namespace vroom::price_lines
|
package/lib/index.d.mts
CHANGED
|
@@ -352,6 +352,78 @@ type LiquidityConfig = {
|
|
|
352
352
|
* `widthPx` and `widthFrac * paneWidth` is used. */
|
|
353
353
|
widthFrac?: number;
|
|
354
354
|
};
|
|
355
|
+
/**
|
|
356
|
+
* A consumer-supplied horizontal status line at a fixed price — the primitive
|
|
357
|
+
* behind resting limit orders, take-profits, stop-losses and liquidation levels.
|
|
358
|
+
*
|
|
359
|
+
* Renders as a line across the price pane ending in a label group (a `text` pill
|
|
360
|
+
* plus an optional solid-filled `quantity` pill and an optional close button),
|
|
361
|
+
* with a price badge in the y-axis strip.
|
|
362
|
+
*
|
|
363
|
+
* Interaction is opt-in and callback-gated, mirroring TradingView: the line is
|
|
364
|
+
* only draggable when `draggable` is set, and the close button only renders when
|
|
365
|
+
* you pass `onPriceLineClose`. Dragging is a *preview* — the chart never mutates
|
|
366
|
+
* the price you gave it, so a move your backend rejects reverts on its own
|
|
367
|
+
* simply by leaving your `priceLines` state unchanged.
|
|
368
|
+
*/
|
|
369
|
+
type PriceLine = {
|
|
370
|
+
/** Stable unique id, echoed back by every callback. */
|
|
371
|
+
id: string;
|
|
372
|
+
/** Where the line sits on the price scale. */
|
|
373
|
+
price: number;
|
|
374
|
+
/** Body label — render whatever you like (e.g. `'Limit Buy @ 13.79'`). */
|
|
375
|
+
text?: string;
|
|
376
|
+
/**
|
|
377
|
+
* Trailing segment, drawn as a solid-filled pill with white text so size reads
|
|
378
|
+
* at a glance (e.g. `'x 5.206'`). Omit to hide it.
|
|
379
|
+
*/
|
|
380
|
+
quantity?: string;
|
|
381
|
+
/** Line, border, body text and close-icon color. Defaults to a soft red. */
|
|
382
|
+
color?: VroomColor;
|
|
383
|
+
/** Stroke width in px. Default 1. */
|
|
384
|
+
width?: number;
|
|
385
|
+
/** Line style. Default `'dotted'`, matching the current-price indicator. */
|
|
386
|
+
lineStyle?: 'solid' | 'dotted' | 'dashed';
|
|
387
|
+
/**
|
|
388
|
+
* Let the user drag this line vertically to a new price. Default false.
|
|
389
|
+
* Pair with `onPriceLineDragEnd` to commit the move.
|
|
390
|
+
*/
|
|
391
|
+
draggable?: boolean;
|
|
392
|
+
/**
|
|
393
|
+
* Show the close button on this line. Defaults to true, but the button only
|
|
394
|
+
* ever renders if you also pass `onPriceLineClose` — set this to false to opt a
|
|
395
|
+
* single line out (e.g. a liquidation level the user can't dismiss).
|
|
396
|
+
*/
|
|
397
|
+
closable?: boolean;
|
|
398
|
+
/** Extend the line to the pane's left edge. Default true. */
|
|
399
|
+
extendLeft?: boolean;
|
|
400
|
+
/** Show the price badge in the y-axis strip. Default true. */
|
|
401
|
+
axisLabel?: boolean;
|
|
402
|
+
};
|
|
403
|
+
/** Shared layout/style for every price line, passed via `priceLinesStyle`. */
|
|
404
|
+
type PriceLinesStyle = {
|
|
405
|
+
/**
|
|
406
|
+
* Translucent fill behind the body and close-button pills, so the label reads
|
|
407
|
+
* over candles without hiding them. Defaults to a dark translucent grey.
|
|
408
|
+
*/
|
|
409
|
+
bodyBackground?: VroomColor;
|
|
410
|
+
/** Label font size in px. Defaults to the axis font size. */
|
|
411
|
+
fontSize?: number;
|
|
412
|
+
/**
|
|
413
|
+
* How far in from the price axis the label group sits, as a fraction of pane
|
|
414
|
+
* width (0 = flush against the axis, 0.5 = at the pane's midpoint). Only
|
|
415
|
+
* applies when `align` is `'right'`. Default 0.
|
|
416
|
+
*/
|
|
417
|
+
inset?: number;
|
|
418
|
+
/** Where the label group sits horizontally. Default `'right'`. */
|
|
419
|
+
align?: 'left' | 'center' | 'right';
|
|
420
|
+
/**
|
|
421
|
+
* How much the hovered line or close button brightens, as a channel
|
|
422
|
+
* multiplier. 1 disables the highlight. Default 1.25. Web only — touch
|
|
423
|
+
* platforms have no hover state.
|
|
424
|
+
*/
|
|
425
|
+
hoverBoost?: number;
|
|
426
|
+
};
|
|
355
427
|
/** MACD indicator config. Rendered in its own pane below the candles. */
|
|
356
428
|
type MACDConfig = {
|
|
357
429
|
enabled?: boolean;
|
|
@@ -420,6 +492,37 @@ type VroomChartCoreProps = {
|
|
|
420
492
|
bollingerBands?: BollingerBandsConfig;
|
|
421
493
|
/** Resting-order / order-book liquidity bands drawn behind the candles. */
|
|
422
494
|
liquidity?: LiquidityConfig;
|
|
495
|
+
/**
|
|
496
|
+
* Horizontal status lines at consumer-supplied prices (resting orders, TP/SL,
|
|
497
|
+
* liquidation levels), each with a text label and an optional close button.
|
|
498
|
+
*
|
|
499
|
+
* This is a controlled prop: the chart renders exactly what you pass and never
|
|
500
|
+
* mutates it. A drag paints a live preview and reports the new price through
|
|
501
|
+
* `onPriceLineDragEnd`; the line only actually moves once you update this
|
|
502
|
+
* array, so a rejected move needs no explicit rollback.
|
|
503
|
+
*/
|
|
504
|
+
priceLines?: PriceLine[];
|
|
505
|
+
/** Shared layout/style for every entry in `priceLines`. */
|
|
506
|
+
priceLinesStyle?: PriceLinesStyle;
|
|
507
|
+
/**
|
|
508
|
+
* Fired continuously while a draggable price line is being dragged, with the
|
|
509
|
+
* price under the pointer. Use it for a live readout (e.g. an order ticket);
|
|
510
|
+
* wait for `onPriceLineDragEnd` to commit.
|
|
511
|
+
*/
|
|
512
|
+
onPriceLineDrag?: (id: string, price: number) => void;
|
|
513
|
+
/**
|
|
514
|
+
* Fired once when the user drops a dragged price line. Submit the new price to
|
|
515
|
+
* your backend here, then update `priceLines` on success — the line snaps back
|
|
516
|
+
* on its own if you don't. Dragging is cancelled (and this never fires) if the
|
|
517
|
+
* user presses Escape.
|
|
518
|
+
*/
|
|
519
|
+
onPriceLineDragEnd?: (id: string, price: number) => void;
|
|
520
|
+
/**
|
|
521
|
+
* Fired when the user activates a price line's close button. Passing this
|
|
522
|
+
* handler is what makes the button render at all; individual lines opt out with
|
|
523
|
+
* `closable: false`.
|
|
524
|
+
*/
|
|
525
|
+
onPriceLineClose?: (id: string) => void;
|
|
423
526
|
/**
|
|
424
527
|
* Pixels the crosshair dot / horizontal line sit *above* the touch point so
|
|
425
528
|
* they aren't hidden under the thumb. The vertical line stays centered on the
|
|
@@ -547,4 +650,4 @@ declare global {
|
|
|
547
650
|
*/
|
|
548
651
|
declare function VroomChart(props: VroomChartProps): React.JSX.Element;
|
|
549
652
|
|
|
550
|
-
export { type BollingerBandsConfig, 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 };
|
|
653
|
+
export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type MACDConfig, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type VWAPConfig, type VisibleRange, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme };
|