react-native-vroom-chart 0.1.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/LICENSE +21 -0
- package/README.md +27 -0
- package/android/README.md +7 -0
- package/cpp/README.md +11 -0
- package/cpp/VroomChartHostObject.cpp +458 -0
- package/cpp/VroomChartHostObject.h +36 -0
- package/cpp/VroomJsiInstaller.cpp +69 -0
- package/cpp/VroomJsiInstaller.h +10 -0
- package/cpp/VroomSkiaContext.cpp +25 -0
- package/cpp/VroomSkiaContext.h +30 -0
- package/cpp/_core_include/vroom/vroom_chart.h +195 -0
- package/cpp/_core_src/candles.cpp +74 -0
- package/cpp/_core_src/candles.h +34 -0
- package/cpp/_core_src/chart.cpp +317 -0
- package/cpp/_core_src/chart.h +167 -0
- package/cpp/_core_src/chart_facade.cpp +570 -0
- package/cpp/_core_src/chart_internal.h +30 -0
- package/cpp/_core_src/crosshair.cpp +65 -0
- package/cpp/_core_src/crosshair.h +32 -0
- package/cpp/_core_src/fonts.cpp +22 -0
- package/cpp/_core_src/fonts.h +25 -0
- package/cpp/_core_src/labels.cpp +340 -0
- package/cpp/_core_src/labels.h +85 -0
- package/cpp/_core_src/ma.cpp +53 -0
- package/cpp/_core_src/ma.h +39 -0
- package/cpp/_core_src/ma_overlay.cpp +73 -0
- package/cpp/_core_src/ma_overlay.h +42 -0
- package/cpp/_core_src/macd.cpp +68 -0
- package/cpp/_core_src/macd.h +29 -0
- package/cpp/_core_src/macd_pane.cpp +199 -0
- package/cpp/_core_src/macd_pane.h +41 -0
- package/cpp/_core_src/price_indicator.cpp +106 -0
- package/cpp/_core_src/price_indicator.h +29 -0
- package/cpp/_core_src/rsi.cpp +70 -0
- package/cpp/_core_src/rsi.h +32 -0
- package/cpp/_core_src/rsi_pane.cpp +167 -0
- package/cpp/_core_src/rsi_pane.h +39 -0
- package/cpp/_core_src/theme.cpp +41 -0
- package/cpp/_core_src/theme.h +23 -0
- package/cpp/_core_src/ticks.cpp +49 -0
- package/cpp/_core_src/ticks.h +30 -0
- package/cpp/_core_src/viewport.cpp +141 -0
- package/cpp/_core_src/viewport.h +100 -0
- package/cpp/_core_src/volume.cpp +70 -0
- package/cpp/_core_src/volume.h +34 -0
- package/cpp/_core_src/vwap.cpp +51 -0
- package/cpp/_core_src/vwap.h +27 -0
- package/ios/README.md +7 -0
- package/ios/VroomChartModule.h +11 -0
- package/ios/VroomChartModule.mm +44 -0
- package/lib/index.d.mts +185 -0
- package/lib/index.d.ts +185 -0
- package/lib/index.js +429 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +396 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +75 -0
- package/react-native-vroom-chart.podspec +79 -0
- package/src/NativeVroomChart.ts +12 -0
- package/src/VroomChart.tsx +382 -0
- package/src/index.ts +14 -0
- package/src/jsi.d.ts +128 -0
- package/src/packCandles.ts +24 -0
- package/src/theme.ts +43 -0
- package/src/types.ts +27 -0
- package/src/useChartCore.ts +135 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#include "rsi_pane.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/SkPath.h"
|
|
11
|
+
#include "include/core/SkPathBuilder.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 <cmath>
|
|
18
|
+
#include <cstdio>
|
|
19
|
+
#include <cstring>
|
|
20
|
+
|
|
21
|
+
#include "chart.h"
|
|
22
|
+
#include "fonts.h"
|
|
23
|
+
#include "theme.h"
|
|
24
|
+
#include "viewport.h"
|
|
25
|
+
|
|
26
|
+
namespace vroom::rsi_pane {
|
|
27
|
+
|
|
28
|
+
namespace {
|
|
29
|
+
constexpr SkColor kRsiLine = 0xff8957e5; // violet RSI line (themable later)
|
|
30
|
+
constexpr SkColor kRsiMaLine = 0xffd29922; // amber RSI moving-average trendline
|
|
31
|
+
constexpr SkColor kRefLine = 0xff30363d; // band reference lines
|
|
32
|
+
constexpr SkColor kDivider = 0xff21262d; // pane separator
|
|
33
|
+
constexpr SkScalar kDash[2] = {3.f, 3.f};
|
|
34
|
+
} // namespace
|
|
35
|
+
|
|
36
|
+
void draw(SkCanvas* canvas,
|
|
37
|
+
const VroomChart& chart,
|
|
38
|
+
const Layout& lay,
|
|
39
|
+
const ::VroomCandle* visible,
|
|
40
|
+
std::size_t n,
|
|
41
|
+
const double* rsi_visible,
|
|
42
|
+
const double* rsi_ma_visible,
|
|
43
|
+
int64_t window_ms,
|
|
44
|
+
int64_t visible_start_ms,
|
|
45
|
+
int64_t candle_duration_ms,
|
|
46
|
+
float candle_right,
|
|
47
|
+
float pane_top,
|
|
48
|
+
float pane_bottom) {
|
|
49
|
+
if (!canvas || n == 0 || candle_right <= 0.f) return;
|
|
50
|
+
const float band_h = pane_bottom - pane_top;
|
|
51
|
+
if (band_h <= 0.f) return;
|
|
52
|
+
|
|
53
|
+
auto y_for = [&](double v) -> float {
|
|
54
|
+
return pane_bottom - static_cast<float>(v / 100.0) * band_h;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Mask the band with the background so candles/volume that overflow below
|
|
58
|
+
// the (shortened) price pane don't show through behind the RSI. The y-axis
|
|
59
|
+
// strip to the right is already masked full-height by draw_chart.
|
|
60
|
+
SkPaint bg;
|
|
61
|
+
bg.setColor(chart.theme.colors[VROOM_COLOR_BACKGROUND]);
|
|
62
|
+
canvas->drawRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom),
|
|
63
|
+
bg);
|
|
64
|
+
|
|
65
|
+
// Pane separator (top edge).
|
|
66
|
+
SkPaint divider;
|
|
67
|
+
divider.setColor(kDivider);
|
|
68
|
+
divider.setStrokeWidth(1.f);
|
|
69
|
+
canvas->drawLine(0.f, pane_top, candle_right, pane_top, divider);
|
|
70
|
+
|
|
71
|
+
// Keep the line + reference lines inside the pane / candle area.
|
|
72
|
+
canvas->save();
|
|
73
|
+
canvas->clipRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom));
|
|
74
|
+
|
|
75
|
+
// Configurable band reference lines (overbought / oversold).
|
|
76
|
+
SkPaint ref;
|
|
77
|
+
ref.setAntiAlias(true);
|
|
78
|
+
ref.setColor(kRefLine);
|
|
79
|
+
ref.setStrokeWidth(1.f);
|
|
80
|
+
ref.setPathEffect(SkDashPathEffect::Make(kDash, 0.f));
|
|
81
|
+
const float y_upper = y_for(chart.rsi_upper);
|
|
82
|
+
const float y_lower = y_for(chart.rsi_lower);
|
|
83
|
+
canvas->drawLine(0.f, y_upper, candle_right, y_upper, ref);
|
|
84
|
+
canvas->drawLine(0.f, y_lower, candle_right, y_lower, ref);
|
|
85
|
+
|
|
86
|
+
// Strokes a value series as a polyline; a NaN lifts the pen so undefined
|
|
87
|
+
// leading values (i < period) leave a gap.
|
|
88
|
+
auto stroke_series = [&](const double* series, SkColor color) {
|
|
89
|
+
if (!series) return;
|
|
90
|
+
SkPathBuilder path;
|
|
91
|
+
bool pen_down = false;
|
|
92
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
93
|
+
const double v = series[i];
|
|
94
|
+
if (!std::isfinite(v)) {
|
|
95
|
+
pen_down = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const float x = vroom::candle_center_x(
|
|
99
|
+
lay, visible[i].time_ms, candle_duration_ms, visible_start_ms,
|
|
100
|
+
window_ms);
|
|
101
|
+
const float y = y_for(v);
|
|
102
|
+
if (pen_down) {
|
|
103
|
+
path.lineTo(x, y);
|
|
104
|
+
} else {
|
|
105
|
+
path.moveTo(x, y);
|
|
106
|
+
pen_down = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
SkPaint line;
|
|
110
|
+
line.setAntiAlias(true);
|
|
111
|
+
line.setColor(color);
|
|
112
|
+
line.setStyle(SkPaint::kStroke_Style);
|
|
113
|
+
line.setStrokeWidth(1.5f);
|
|
114
|
+
canvas->drawPath(path.detach(), line);
|
|
115
|
+
};
|
|
116
|
+
stroke_series(rsi_visible, kRsiLine);
|
|
117
|
+
stroke_series(rsi_ma_visible, kRsiMaLine); // trendline on top
|
|
118
|
+
|
|
119
|
+
// Caption, top-left of the pane.
|
|
120
|
+
auto tf = vroom::axis_typeface();
|
|
121
|
+
if (tf) {
|
|
122
|
+
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
123
|
+
font.setSubpixel(true);
|
|
124
|
+
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
125
|
+
|
|
126
|
+
char caption[24];
|
|
127
|
+
std::snprintf(caption, sizeof(caption), "RSI %d", chart.rsi_period);
|
|
128
|
+
SkRect cb;
|
|
129
|
+
font.measureText(caption, std::strlen(caption), SkTextEncoding::kUTF8,
|
|
130
|
+
&cb);
|
|
131
|
+
SkPaint cap_paint;
|
|
132
|
+
cap_paint.setAntiAlias(true);
|
|
133
|
+
cap_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
|
|
134
|
+
canvas->drawString(caption, 6.f, pane_top + 4.f - cb.fTop, font,
|
|
135
|
+
cap_paint);
|
|
136
|
+
}
|
|
137
|
+
canvas->restore();
|
|
138
|
+
|
|
139
|
+
// 70 / 30 labels in the y-axis strip (drawn after the clip; the strip's
|
|
140
|
+
// background was already masked by draw_chart before this call).
|
|
141
|
+
if (tf) {
|
|
142
|
+
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
143
|
+
font.setSubpixel(true);
|
|
144
|
+
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
145
|
+
SkPaint text_paint;
|
|
146
|
+
text_paint.setAntiAlias(true);
|
|
147
|
+
text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
|
|
148
|
+
|
|
149
|
+
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
150
|
+
auto label = [&](const char* s, float y) {
|
|
151
|
+
SkRect tb;
|
|
152
|
+
const float tw = font.measureText(s, std::strlen(s),
|
|
153
|
+
SkTextEncoding::kUTF8, &tb);
|
|
154
|
+
const float text_x = axis_center_x - tw * 0.5f;
|
|
155
|
+
const float baseline_y = y - (tb.fTop + tb.fBottom) * 0.5f;
|
|
156
|
+
canvas->drawString(s, text_x, baseline_y, font, text_paint);
|
|
157
|
+
};
|
|
158
|
+
char ub[8];
|
|
159
|
+
char lb[8];
|
|
160
|
+
std::snprintf(ub, sizeof(ub), "%.0f", chart.rsi_upper);
|
|
161
|
+
std::snprintf(lb, sizeof(lb), "%.0f", chart.rsi_lower);
|
|
162
|
+
label(ub, y_upper);
|
|
163
|
+
label(lb, y_lower);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
} // namespace vroom::rsi_pane
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// RSI indicator pane — draws the RSI line, 70/30 reference lines, axis labels,
|
|
2
|
+
// and a caption in the band below the candles. Its own module alongside
|
|
3
|
+
// candles / volume / price_indicator so the chart orchestrator stays thin.
|
|
4
|
+
|
|
5
|
+
#pragma once
|
|
6
|
+
|
|
7
|
+
#include <cstddef>
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
|
|
10
|
+
#include "vroom/vroom_chart.h" // ::VroomCandle
|
|
11
|
+
|
|
12
|
+
class SkCanvas;
|
|
13
|
+
|
|
14
|
+
namespace vroom {
|
|
15
|
+
struct Layout;
|
|
16
|
+
} // namespace vroom
|
|
17
|
+
|
|
18
|
+
struct VroomChart;
|
|
19
|
+
|
|
20
|
+
namespace vroom::rsi_pane {
|
|
21
|
+
|
|
22
|
+
// Draws the RSI pane spanning vertically [pane_top, pane_bottom]. `rsi_visible`
|
|
23
|
+
// is the cached RSI series aligned with `visible` (NaN where undefined). Shares
|
|
24
|
+
// the candles' horizontal mapping (candle_center_x) so it scrolls in lock-step.
|
|
25
|
+
void draw(SkCanvas* canvas,
|
|
26
|
+
const VroomChart& chart,
|
|
27
|
+
const Layout& lay,
|
|
28
|
+
const ::VroomCandle* visible,
|
|
29
|
+
std::size_t n,
|
|
30
|
+
const double* rsi_visible,
|
|
31
|
+
const double* rsi_ma_visible,
|
|
32
|
+
int64_t window_ms,
|
|
33
|
+
int64_t visible_start_ms,
|
|
34
|
+
int64_t candle_duration_ms,
|
|
35
|
+
float candle_right,
|
|
36
|
+
float pane_top,
|
|
37
|
+
float pane_bottom);
|
|
38
|
+
|
|
39
|
+
} // namespace vroom::rsi_pane
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#include "theme.h"
|
|
2
|
+
|
|
3
|
+
namespace vroom {
|
|
4
|
+
|
|
5
|
+
namespace {
|
|
6
|
+
|
|
7
|
+
// GitHub-dark palette.
|
|
8
|
+
constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
|
|
9
|
+
0xff0d1117, // BACKGROUND
|
|
10
|
+
0xff26a69a, // BULL
|
|
11
|
+
0xffef5350, // BEAR
|
|
12
|
+
0xffaaaaaa, // WICK
|
|
13
|
+
0xff1a1e24, // GRID — ~20% darker than the crosshair so it recedes
|
|
14
|
+
0xffc9d1d9, // AXIS_TEXT
|
|
15
|
+
0xff303741, // CROSSHAIR — grid tone lightened ~44%; reads above GRID
|
|
16
|
+
0xff161b22, // TOOLTIP_BG
|
|
17
|
+
0xffc9d1d9, // TOOLTIP_TEXT
|
|
18
|
+
0xff3e4855, // CROSSHAIR_TARGET — CROSSHAIR lightened 30% (prior derived ring)
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
22
|
+
0.68f, // CANDLE_WIDTH_RATIO — body fills 68% of slot, 32% gap
|
|
23
|
+
1.0f, // WICK_WIDTH_PX
|
|
24
|
+
6.f, // RIGHT_PADDING_PX — small gutter between candles and y-axis
|
|
25
|
+
11.f, // AXIS_FONT_SIZE_PX
|
|
26
|
+
0.18f, // Y_AXIS_WIDTH_RATIO — fallback before font is loaded
|
|
27
|
+
22.f, // X_AXIS_HEIGHT_PX — bottom strip for time labels
|
|
28
|
+
0.5f, // VOLUME_OPACITY — candle color at 50% transparency
|
|
29
|
+
0.20f, // INDICATOR_HEIGHT_FRAC — below-chart indicator pane = 20% of height
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
} // namespace
|
|
33
|
+
|
|
34
|
+
Theme default_theme() {
|
|
35
|
+
Theme t;
|
|
36
|
+
for (int i = 0; i < VROOM_COLOR_COUNT_; ++i) t.colors[i] = kDefaultColors[i];
|
|
37
|
+
for (int i = 0; i < VROOM_FLOAT_COUNT_; ++i) t.floats[i] = kDefaultFloats[i];
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
} // namespace vroom
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Theme — colors and float-typed style parameters for a single chart.
|
|
2
|
+
//
|
|
3
|
+
// Layout: indexed arrays keyed by the public C-facade enums (VroomColorKey,
|
|
4
|
+
// VroomFloatKey) so we can stay ABI-stable while adding new keys. Wrapped in
|
|
5
|
+
// a struct so future expansion (booleans for visibility toggles, strings for
|
|
6
|
+
// label formats) lands as new sub-arrays without churning every field access.
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include "vroom/vroom_chart.h"
|
|
11
|
+
|
|
12
|
+
namespace vroom {
|
|
13
|
+
|
|
14
|
+
struct Theme {
|
|
15
|
+
uint32_t colors[VROOM_COLOR_COUNT_]{};
|
|
16
|
+
float floats[VROOM_FLOAT_COUNT_]{};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Returns a Theme pre-filled with the default dark palette + sensible
|
|
20
|
+
// layout defaults. Used by `VroomChart`'s constructor.
|
|
21
|
+
Theme default_theme();
|
|
22
|
+
|
|
23
|
+
} // namespace vroom
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#include "ticks.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cmath>
|
|
5
|
+
|
|
6
|
+
namespace vroom {
|
|
7
|
+
|
|
8
|
+
int64_t pick_time_interval(int64_t window_ms, float candle_area_w) {
|
|
9
|
+
static constexpr int64_t kIntervals[] = {
|
|
10
|
+
60'000LL, // 1m
|
|
11
|
+
5 * 60'000LL, // 5m
|
|
12
|
+
15 * 60'000LL, // 15m
|
|
13
|
+
30 * 60'000LL, // 30m
|
|
14
|
+
60 * 60'000LL, // 1h
|
|
15
|
+
2 * 60 * 60'000LL, // 2h
|
|
16
|
+
4 * 60 * 60'000LL, // 4h
|
|
17
|
+
6 * 60 * 60'000LL, // 6h
|
|
18
|
+
12 * 60 * 60'000LL, // 12h
|
|
19
|
+
24 * 60 * 60'000LL, // 1d
|
|
20
|
+
2 * 24 * 60 * 60'000LL, // 2d
|
|
21
|
+
7 * 24 * 60 * 60'000LL, // 1w
|
|
22
|
+
};
|
|
23
|
+
if (window_ms <= 0 || candle_area_w <= 0.f) return kIntervals[0];
|
|
24
|
+
const double ms_per_px =
|
|
25
|
+
static_cast<double>(window_ms) / candle_area_w;
|
|
26
|
+
for (int64_t i : kIntervals) {
|
|
27
|
+
if (static_cast<double>(i) / ms_per_px >= kXLabelMinSpacing) {
|
|
28
|
+
return i;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return kIntervals[sizeof(kIntervals) / sizeof(kIntervals[0]) - 1];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
double pick_price_interval(double range, float candle_area_h) {
|
|
35
|
+
if (range <= 0.0 || candle_area_h <= 0.f) return 1.0;
|
|
36
|
+
const int target_count =
|
|
37
|
+
std::max(2, static_cast<int>(candle_area_h / kYLabelTargetSpacing));
|
|
38
|
+
const double rough = range / target_count;
|
|
39
|
+
const double magnitude = std::pow(10.0, std::floor(std::log10(rough)));
|
|
40
|
+
const double normalized = rough / magnitude;
|
|
41
|
+
double nice;
|
|
42
|
+
if (normalized < 1.5) nice = 1.0;
|
|
43
|
+
else if (normalized < 3.5) nice = 2.0;
|
|
44
|
+
else if (normalized < 7.5) nice = 5.0;
|
|
45
|
+
else nice = 10.0;
|
|
46
|
+
return nice * magnitude;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
} // namespace vroom
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Tick algorithms — pick a label interval based on current scale so labels
|
|
2
|
+
// land on canonical boundaries with reasonable spacing.
|
|
3
|
+
//
|
|
4
|
+
// Pure functions, no state. Used by the label subsystem; safe for future
|
|
5
|
+
// indicators that need their own axis grids to reuse.
|
|
6
|
+
|
|
7
|
+
#pragma once
|
|
8
|
+
|
|
9
|
+
#include <cstdint>
|
|
10
|
+
|
|
11
|
+
namespace vroom {
|
|
12
|
+
|
|
13
|
+
// Minimum spacing between adjacent x-axis labels in pixels. Smaller → more
|
|
14
|
+
// labels visible at every zoom level; larger → sparser.
|
|
15
|
+
inline constexpr float kXLabelMinSpacing = 50.f;
|
|
16
|
+
|
|
17
|
+
// Target spacing for y-axis labels — used to derive a target count.
|
|
18
|
+
inline constexpr float kYLabelTargetSpacing = 55.f;
|
|
19
|
+
|
|
20
|
+
// Selects the smallest time interval whose pixel-spacing meets the minimum.
|
|
21
|
+
// Returns milliseconds. Hierarchy: 1m → 5m → 15m → 30m → 1h → 2h → 4h → 6h →
|
|
22
|
+
// 12h → 1d → 2d → 1w.
|
|
23
|
+
int64_t pick_time_interval(int64_t window_ms, float candle_area_w);
|
|
24
|
+
|
|
25
|
+
// "Nice number" tick selection for the y-axis: snaps to 1, 2, or 5 × 10ⁿ
|
|
26
|
+
// based on the price range and target spacing. Returns the price interval
|
|
27
|
+
// in the same units as the data (e.g. dollars).
|
|
28
|
+
double pick_price_interval(double range, float candle_area_h);
|
|
29
|
+
|
|
30
|
+
} // namespace vroom
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#include "viewport.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <limits>
|
|
5
|
+
|
|
6
|
+
namespace vroom {
|
|
7
|
+
|
|
8
|
+
float candle_body_width(const Layout& layout,
|
|
9
|
+
int64_t window_ms,
|
|
10
|
+
int64_t candle_duration_ms) {
|
|
11
|
+
if (window_ms <= 0 || candle_duration_ms <= 0) return 0.f;
|
|
12
|
+
const float usable =
|
|
13
|
+
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
14
|
+
const double slot = static_cast<double>(usable) *
|
|
15
|
+
(static_cast<double>(candle_duration_ms) /
|
|
16
|
+
static_cast<double>(window_ms));
|
|
17
|
+
return static_cast<float>(slot * layout.candle_width_ratio);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
float candle_center_x(const Layout& layout,
|
|
21
|
+
int64_t time_ms,
|
|
22
|
+
int64_t candle_duration_ms,
|
|
23
|
+
int64_t visible_start_ms,
|
|
24
|
+
int64_t window_ms) {
|
|
25
|
+
if (window_ms <= 0) return 0.f;
|
|
26
|
+
const float usable =
|
|
27
|
+
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
28
|
+
const double center_time =
|
|
29
|
+
static_cast<double>(time_ms) +
|
|
30
|
+
static_cast<double>(candle_duration_ms) * 0.5;
|
|
31
|
+
const double frac =
|
|
32
|
+
(center_time - static_cast<double>(visible_start_ms)) /
|
|
33
|
+
static_cast<double>(window_ms);
|
|
34
|
+
return static_cast<float>(static_cast<double>(usable) * frac);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
size_t snap_index_to_candle(const Layout& layout,
|
|
38
|
+
const ::VroomCandle* candles,
|
|
39
|
+
size_t count,
|
|
40
|
+
int64_t candle_duration_ms,
|
|
41
|
+
int64_t visible_start_ms,
|
|
42
|
+
int64_t window_ms,
|
|
43
|
+
float x_px) {
|
|
44
|
+
const float usable =
|
|
45
|
+
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
46
|
+
if (usable <= 0.f) return 0;
|
|
47
|
+
|
|
48
|
+
// Pixel x -> the period start time of the candle that would sit there.
|
|
49
|
+
const double target_center =
|
|
50
|
+
static_cast<double>(visible_start_ms) +
|
|
51
|
+
(static_cast<double>(x_px) / static_cast<double>(usable)) *
|
|
52
|
+
static_cast<double>(window_ms);
|
|
53
|
+
const int64_t key =
|
|
54
|
+
static_cast<int64_t>(target_center) - candle_duration_ms / 2;
|
|
55
|
+
|
|
56
|
+
// First candle with time_ms >= key, then pick whichever of it / its
|
|
57
|
+
// predecessor is closer in time.
|
|
58
|
+
auto* it = std::lower_bound(
|
|
59
|
+
candles, candles + count, key,
|
|
60
|
+
[](const ::VroomCandle& c, int64_t t) { return c.time_ms < t; });
|
|
61
|
+
if (it == candles) return 0;
|
|
62
|
+
if (it == candles + count) return count - 1;
|
|
63
|
+
const int64_t hi = it->time_ms;
|
|
64
|
+
const int64_t lo = (it - 1)->time_ms;
|
|
65
|
+
return (hi - key < key - lo) ? static_cast<size_t>(it - candles)
|
|
66
|
+
: static_cast<size_t>(it - 1 - candles);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
float snap_x_to_candle(const Layout& layout,
|
|
70
|
+
const ::VroomCandle* candles,
|
|
71
|
+
size_t count,
|
|
72
|
+
int64_t candle_duration_ms,
|
|
73
|
+
int64_t visible_start_ms,
|
|
74
|
+
int64_t window_ms,
|
|
75
|
+
float x_px) {
|
|
76
|
+
if (count == 0 || window_ms <= 0) return x_px;
|
|
77
|
+
|
|
78
|
+
const float usable =
|
|
79
|
+
layout.width_px - layout.y_axis_width_px - layout.right_padding_px;
|
|
80
|
+
if (usable <= 0.f) return x_px;
|
|
81
|
+
|
|
82
|
+
const size_t idx = snap_index_to_candle(
|
|
83
|
+
layout, candles, count, candle_duration_ms, visible_start_ms,
|
|
84
|
+
window_ms, x_px);
|
|
85
|
+
return candle_center_x(layout, candles[idx].time_ms, candle_duration_ms,
|
|
86
|
+
visible_start_ms, window_ms);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
IndexRange visible_indices(const ::VroomCandle* candles,
|
|
90
|
+
size_t count,
|
|
91
|
+
int64_t start_ms,
|
|
92
|
+
int64_t end_ms) {
|
|
93
|
+
if (count == 0) return {0, 0};
|
|
94
|
+
if (start_ms == 0 && end_ms == 0) return {0, count};
|
|
95
|
+
|
|
96
|
+
// first index with time_ms >= start_ms
|
|
97
|
+
auto* first = std::lower_bound(
|
|
98
|
+
candles, candles + count, start_ms,
|
|
99
|
+
[](const ::VroomCandle& c, int64_t t) { return c.time_ms < t; });
|
|
100
|
+
// first index with time_ms > end_ms
|
|
101
|
+
auto* last = std::upper_bound(
|
|
102
|
+
candles, candles + count, end_ms,
|
|
103
|
+
[](int64_t t, const ::VroomCandle& c) { return t < c.time_ms; });
|
|
104
|
+
return {
|
|
105
|
+
static_cast<size_t>(first - candles),
|
|
106
|
+
static_cast<size_t>(last - candles),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
PriceBounds price_bounds(const ::VroomCandle* candles, size_t count) {
|
|
111
|
+
PriceBounds b{
|
|
112
|
+
std::numeric_limits<double>::infinity(),
|
|
113
|
+
-std::numeric_limits<double>::infinity(),
|
|
114
|
+
};
|
|
115
|
+
for (size_t i = 0; i < count; ++i) {
|
|
116
|
+
b.min = std::min(b.min, candles[i].low);
|
|
117
|
+
b.max = std::max(b.max, candles[i].high);
|
|
118
|
+
}
|
|
119
|
+
if (count == 0) {
|
|
120
|
+
b.min = 0.0;
|
|
121
|
+
b.max = 1.0;
|
|
122
|
+
}
|
|
123
|
+
return b;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
float price_to_y(const Layout& layout,
|
|
127
|
+
const PriceBounds& bounds,
|
|
128
|
+
double price) {
|
|
129
|
+
// The candle drawing area is the full height minus the x-axis strip and
|
|
130
|
+
// any below-chart indicator pane.
|
|
131
|
+
const float candle_area_h = price_pane_bottom(layout);
|
|
132
|
+
const float top = candle_area_h * layout.top_padding_frac;
|
|
133
|
+
const float bot = candle_area_h * (1.f - layout.bottom_padding_frac);
|
|
134
|
+
const float draw_h = bot - top;
|
|
135
|
+
const double range = bounds.max - bounds.min;
|
|
136
|
+
if (range <= 0.0) return (top + bot) * 0.5f;
|
|
137
|
+
const double t = (price - bounds.min) / range; // 0..1, 0 at min
|
|
138
|
+
return bot - static_cast<float>(t) * draw_h; // invert: high price → low y
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
} // namespace vroom
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Viewport math — pure functions, no Skia, no state.
|
|
2
|
+
// Translates between candle indices, time, price, and screen pixels.
|
|
3
|
+
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include <cstddef>
|
|
7
|
+
#include <cstdint>
|
|
8
|
+
|
|
9
|
+
#include "vroom/vroom_chart.h"
|
|
10
|
+
|
|
11
|
+
namespace vroom {
|
|
12
|
+
|
|
13
|
+
struct Layout {
|
|
14
|
+
float width_px; // total chart width
|
|
15
|
+
float height_px; // total chart height
|
|
16
|
+
float y_axis_width_px; // reserved on the right for price labels
|
|
17
|
+
float x_axis_height_px; // reserved at the bottom for time labels
|
|
18
|
+
float right_padding_px; // small gutter between candles and y-axis
|
|
19
|
+
float candle_width_ratio; // 0..1 of slot stride taken by the body
|
|
20
|
+
float top_padding_frac; // fraction of candle area; keeps prices off edges
|
|
21
|
+
float bottom_padding_frac;
|
|
22
|
+
float indicator_area_h; // height reserved for below-chart indicator panes
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Bottom of the price (candle) pane = top of the indicator band. Shrinks when
|
|
26
|
+
// an indicator pane is present so candles, volume, and price labels reflow.
|
|
27
|
+
inline float price_pane_bottom(const Layout& l) {
|
|
28
|
+
return l.height_px - l.x_axis_height_px - l.indicator_area_h;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Top of the bottom time-axis strip. Stays anchored regardless of any indicator
|
|
32
|
+
// band (the strip is always the bottom x_axis_height_px of the chart).
|
|
33
|
+
inline float x_axis_top(const Layout& l) {
|
|
34
|
+
return l.height_px - l.x_axis_height_px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
struct PriceBounds {
|
|
38
|
+
double min;
|
|
39
|
+
double max;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Half-open [start, end) range of candle indices.
|
|
43
|
+
struct IndexRange {
|
|
44
|
+
size_t start;
|
|
45
|
+
size_t end;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Returns the indices of candles whose time_ms falls in [start_ms, end_ms].
|
|
49
|
+
// When both are 0, returns the full range (Phase 1 default-everything behavior).
|
|
50
|
+
// Candles must be sorted ascending by time_ms (invariant of the public API).
|
|
51
|
+
IndexRange visible_indices(const ::VroomCandle* candles,
|
|
52
|
+
size_t count,
|
|
53
|
+
int64_t start_ms,
|
|
54
|
+
int64_t end_ms);
|
|
55
|
+
|
|
56
|
+
// Width of one candle body in pixels, computed from the time slot a single
|
|
57
|
+
// candle occupies (candle_duration_ms / window_ms × candle area).
|
|
58
|
+
float candle_body_width(const Layout& layout,
|
|
59
|
+
int64_t window_ms,
|
|
60
|
+
int64_t candle_duration_ms);
|
|
61
|
+
|
|
62
|
+
// Center-x of a candle whose period starts at time_ms and lasts
|
|
63
|
+
// candle_duration_ms, given the current visible time window.
|
|
64
|
+
float candle_center_x(const Layout& layout,
|
|
65
|
+
int64_t time_ms,
|
|
66
|
+
int64_t candle_duration_ms,
|
|
67
|
+
int64_t visible_start_ms,
|
|
68
|
+
int64_t window_ms);
|
|
69
|
+
|
|
70
|
+
// Index of the candle whose center is nearest pixel x_px, searching
|
|
71
|
+
// [candles, candles+count). Clamps to the first/last candle. This is the
|
|
72
|
+
// integer counterpart of snap_x_to_candle — both share the same nearest-candle
|
|
73
|
+
// math. Precondition: count > 0 and window_ms > 0 (callers guard).
|
|
74
|
+
size_t snap_index_to_candle(const Layout& layout,
|
|
75
|
+
const ::VroomCandle* candles,
|
|
76
|
+
size_t count,
|
|
77
|
+
int64_t candle_duration_ms,
|
|
78
|
+
int64_t visible_start_ms,
|
|
79
|
+
int64_t window_ms,
|
|
80
|
+
float x_px);
|
|
81
|
+
|
|
82
|
+
// On-screen x of the candle whose center is nearest pixel x_px, searching
|
|
83
|
+
// [candles, candles+count). Clamps to the first/last candle. Returns x_px
|
|
84
|
+
// unchanged when there are no candles or the window is degenerate. Used to snap
|
|
85
|
+
// the crosshair to candles.
|
|
86
|
+
float snap_x_to_candle(const Layout& layout,
|
|
87
|
+
const ::VroomCandle* candles,
|
|
88
|
+
size_t count,
|
|
89
|
+
int64_t candle_duration_ms,
|
|
90
|
+
int64_t visible_start_ms,
|
|
91
|
+
int64_t window_ms,
|
|
92
|
+
float x_px);
|
|
93
|
+
|
|
94
|
+
// Min/max of (low..high) across the given range.
|
|
95
|
+
PriceBounds price_bounds(const ::VroomCandle* candles, size_t count);
|
|
96
|
+
|
|
97
|
+
// Map a price to y in pixels. y=0 is top of the chart.
|
|
98
|
+
float price_to_y(const Layout& layout, const PriceBounds& bounds, double price);
|
|
99
|
+
|
|
100
|
+
} // namespace vroom
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#include "volume.h"
|
|
2
|
+
|
|
3
|
+
#pragma clang diagnostic push
|
|
4
|
+
#pragma clang diagnostic ignored "-Wdocumentation"
|
|
5
|
+
#include "include/core/SkCanvas.h"
|
|
6
|
+
#include "include/core/SkPaint.h"
|
|
7
|
+
#include "include/core/SkRect.h"
|
|
8
|
+
#pragma clang diagnostic pop
|
|
9
|
+
|
|
10
|
+
#include <algorithm>
|
|
11
|
+
|
|
12
|
+
#include "theme.h"
|
|
13
|
+
#include "viewport.h"
|
|
14
|
+
|
|
15
|
+
namespace vroom::volume {
|
|
16
|
+
|
|
17
|
+
namespace {
|
|
18
|
+
constexpr float kVolumeFrac = 0.20f; // tallest bar = this fraction of the area
|
|
19
|
+
} // namespace
|
|
20
|
+
|
|
21
|
+
void draw(SkCanvas* canvas,
|
|
22
|
+
const ::VroomCandle* visible,
|
|
23
|
+
std::size_t n,
|
|
24
|
+
const Layout& lay,
|
|
25
|
+
const Theme& theme,
|
|
26
|
+
int64_t window_ms,
|
|
27
|
+
int64_t visible_start_ms,
|
|
28
|
+
int64_t candle_duration_ms) {
|
|
29
|
+
if (!canvas || n == 0) return;
|
|
30
|
+
|
|
31
|
+
// Auto-fit bar heights to the loudest volume currently in view.
|
|
32
|
+
double max_vol = 0.0;
|
|
33
|
+
for (std::size_t i = 0; i < n; ++i) max_vol = std::max(max_vol, visible[i].volume);
|
|
34
|
+
if (max_vol <= 0.0) return;
|
|
35
|
+
|
|
36
|
+
const float candle_area_h = vroom::price_pane_bottom(lay);
|
|
37
|
+
const float region_h = candle_area_h * kVolumeFrac;
|
|
38
|
+
|
|
39
|
+
const float body_w = vroom::candle_body_width(
|
|
40
|
+
lay, window_ms, candle_duration_ms);
|
|
41
|
+
const float half_body = body_w * 0.5f;
|
|
42
|
+
|
|
43
|
+
const float opacity = theme.floats[VROOM_FLOAT_VOLUME_OPACITY];
|
|
44
|
+
SkPaint bull_paint;
|
|
45
|
+
bull_paint.setAntiAlias(true);
|
|
46
|
+
bull_paint.setColor(theme.colors[VROOM_COLOR_BULL]);
|
|
47
|
+
bull_paint.setAlphaf(opacity);
|
|
48
|
+
|
|
49
|
+
SkPaint bear_paint;
|
|
50
|
+
bear_paint.setAntiAlias(true);
|
|
51
|
+
bear_paint.setColor(theme.colors[VROOM_COLOR_BEAR]);
|
|
52
|
+
bear_paint.setAlphaf(opacity);
|
|
53
|
+
|
|
54
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
55
|
+
const auto& c = visible[i];
|
|
56
|
+
if (c.volume <= 0.0) continue;
|
|
57
|
+
const bool bull = c.close >= c.open;
|
|
58
|
+
|
|
59
|
+
const float cx = vroom::candle_center_x(
|
|
60
|
+
lay, c.time_ms, candle_duration_ms,
|
|
61
|
+
visible_start_ms, window_ms);
|
|
62
|
+
const float h = std::max(
|
|
63
|
+
1.f, static_cast<float>(c.volume / max_vol) * region_h);
|
|
64
|
+
canvas->drawRect(
|
|
65
|
+
SkRect::MakeXYWH(cx - half_body, candle_area_h - h, body_w, h),
|
|
66
|
+
bull ? bull_paint : bear_paint);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
} // namespace vroom::volume
|