react-native-vroom-chart 0.15.0 → 0.16.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 +171 -1
- package/cpp/_core_include/vroom/vroom_chart.h +137 -4
- package/cpp/_core_src/atr.cpp +67 -0
- package/cpp/_core_src/atr.h +44 -0
- package/cpp/_core_src/atr_pane.cpp +162 -0
- package/cpp/_core_src/atr_pane.h +48 -0
- package/cpp/_core_src/chart.cpp +461 -172
- package/cpp/_core_src/chart.h +150 -1
- package/cpp/_core_src/chart_facade.cpp +209 -23
- package/cpp/_core_src/fair_value_gaps.cpp +76 -0
- package/cpp/_core_src/fair_value_gaps.h +54 -0
- package/cpp/_core_src/fvg_overlay.cpp +262 -0
- package/cpp/_core_src/fvg_overlay.h +43 -0
- package/cpp/_core_src/ichimoku.cpp +65 -0
- package/cpp/_core_src/ichimoku.h +45 -0
- package/cpp/_core_src/labels.cpp +3 -0
- package/cpp/_core_src/line_morph.h +159 -0
- package/cpp/_core_src/ma_overlay.cpp +259 -36
- package/cpp/_core_src/ma_overlay.h +57 -2
- package/cpp/_core_src/macd.cpp +24 -1
- package/cpp/_core_src/macd.h +16 -0
- package/cpp/_core_src/macd_pane.cpp +71 -62
- package/cpp/_core_src/macd_pane.h +13 -1
- package/cpp/_core_src/pane_series.h +169 -0
- package/cpp/_core_src/rsi.cpp +4 -0
- package/cpp/_core_src/rsi.h +7 -0
- package/cpp/_core_src/rsi_pane.cpp +85 -29
- package/cpp/_core_src/rsi_pane.h +11 -1
- package/cpp/_core_src/viewport.h +35 -0
- package/lib/index.d.mts +251 -3
- package/lib/index.d.ts +251 -3
- package/lib/index.js +224 -6
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +224 -6
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +21 -3
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +5 -0
- package/src/jsi.d.ts +97 -1
- package/src/types.ts +5 -0
- package/src/useChartCore.ts +284 -5
|
@@ -57,6 +57,7 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
57
57
|
out.push_back(jsi::PropNameID::forAscii(rt, "getVisiblePriceEnvelope"));
|
|
58
58
|
out.push_back(jsi::PropNameID::forAscii(rt, "preservePriceEnvelope"));
|
|
59
59
|
out.push_back(jsi::PropNameID::forAscii(rt, "beginIntervalMorph"));
|
|
60
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "beginStreamMorph"));
|
|
60
61
|
out.push_back(jsi::PropNameID::forAscii(rt, "setIntervalMorph"));
|
|
61
62
|
out.push_back(jsi::PropNameID::forAscii(rt, "pan"));
|
|
62
63
|
out.push_back(jsi::PropNameID::forAscii(rt, "translate"));
|
|
@@ -71,9 +72,12 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
71
72
|
out.push_back(jsi::PropNameID::forAscii(rt, "getCrosshairInfo"));
|
|
72
73
|
out.push_back(jsi::PropNameID::forAscii(rt, "setRSI"));
|
|
73
74
|
out.push_back(jsi::PropNameID::forAscii(rt, "setMACD"));
|
|
75
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setATR"));
|
|
74
76
|
out.push_back(jsi::PropNameID::forAscii(rt, "setOverlays"));
|
|
75
77
|
out.push_back(jsi::PropNameID::forAscii(rt, "setVWAP"));
|
|
76
78
|
out.push_back(jsi::PropNameID::forAscii(rt, "setBollinger"));
|
|
79
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setIchimoku"));
|
|
80
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setFairValueGaps"));
|
|
77
81
|
out.push_back(jsi::PropNameID::forAscii(rt, "setVolume"));
|
|
78
82
|
out.push_back(jsi::PropNameID::forAscii(rt, "setVolumeCollapse"));
|
|
79
83
|
out.push_back(jsi::PropNameID::forAscii(rt, "setAxisCollapse"));
|
|
@@ -510,6 +514,24 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
510
514
|
});
|
|
511
515
|
}
|
|
512
516
|
|
|
517
|
+
if (name == "beginStreamMorph") {
|
|
518
|
+
// beginStreamMorph() — capture the visible geometry so the next setCandles
|
|
519
|
+
// can ease a live tick into place. Leaves the axes alone, and continues
|
|
520
|
+
// from the shape on screen when one is still animating. Call before
|
|
521
|
+
// setCandles.
|
|
522
|
+
return jsi::Function::createFromHostFunction(
|
|
523
|
+
rt,
|
|
524
|
+
jsi::PropNameID::forAscii(rt, "beginStreamMorph"),
|
|
525
|
+
0,
|
|
526
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
527
|
+
const jsi::Value& /*thisVal*/,
|
|
528
|
+
const jsi::Value* /*args*/,
|
|
529
|
+
size_t /*count*/) -> jsi::Value {
|
|
530
|
+
vroom_chart_begin_stream_morph(chart_);
|
|
531
|
+
return jsi::Value::undefined();
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
513
535
|
if (name == "setIntervalMorph") {
|
|
514
536
|
// setIntervalMorph(t) — advance the capture toward the new candles. `t` is
|
|
515
537
|
// pre-eased progress: 0 = the captured frame, 1 = settled (capture freed).
|
|
@@ -761,7 +783,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
761
783
|
if (name == "setRSI") {
|
|
762
784
|
// setRSI({enabled, period, upperBand, lowerBand, maPeriod, maKind,
|
|
763
785
|
// maVisible, lineColor, lineWidth, lineVisible, maColor, maWidth,
|
|
764
|
-
// bandColor, bandsVisible}) — configures the RSI pane. No
|
|
786
|
+
// bandColor, bandsVisible, extremeFill}) — configures the RSI pane. No
|
|
787
|
+
// render; the next
|
|
765
788
|
// render() picks it up.
|
|
766
789
|
return jsi::Function::createFromHostFunction(
|
|
767
790
|
rt,
|
|
@@ -797,6 +820,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
797
820
|
s.getProperty(rt2, "bandColor").asNumber());
|
|
798
821
|
cfg.bands_visible =
|
|
799
822
|
s.getProperty(rt2, "bandsVisible").asBool() ? 1 : 0;
|
|
823
|
+
cfg.extreme_fill =
|
|
824
|
+
s.getProperty(rt2, "extremeFill").asBool() ? 1 : 0;
|
|
800
825
|
vroom_chart_set_rsi(chart_, &cfg);
|
|
801
826
|
return jsi::Value::undefined();
|
|
802
827
|
});
|
|
@@ -863,6 +888,34 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
863
888
|
});
|
|
864
889
|
}
|
|
865
890
|
|
|
891
|
+
if (name == "setATR") {
|
|
892
|
+
// setATR({enabled, period, smoothing, lineColor, lineWidth}) — configures
|
|
893
|
+
// the ATR pane. No render; the next render() picks it up.
|
|
894
|
+
return jsi::Function::createFromHostFunction(
|
|
895
|
+
rt,
|
|
896
|
+
jsi::PropNameID::forAscii(rt, "setATR"),
|
|
897
|
+
1,
|
|
898
|
+
[this](jsi::Runtime& rt2,
|
|
899
|
+
const jsi::Value& /*thisVal*/,
|
|
900
|
+
const jsi::Value* args,
|
|
901
|
+
size_t count) -> jsi::Value {
|
|
902
|
+
if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
|
|
903
|
+
auto s = args[0].asObject(rt2);
|
|
904
|
+
VroomATR cfg{};
|
|
905
|
+
cfg.enabled = s.getProperty(rt2, "enabled").asBool() ? 1 : 0;
|
|
906
|
+
cfg.period = static_cast<int32_t>(
|
|
907
|
+
s.getProperty(rt2, "period").asNumber());
|
|
908
|
+
cfg.smoothing = static_cast<int32_t>(
|
|
909
|
+
s.getProperty(rt2, "smoothing").asNumber());
|
|
910
|
+
cfg.line_color = static_cast<uint32_t>(
|
|
911
|
+
s.getProperty(rt2, "lineColor").asNumber());
|
|
912
|
+
cfg.line_width = static_cast<float>(
|
|
913
|
+
s.getProperty(rt2, "lineWidth").asNumber());
|
|
914
|
+
vroom_chart_set_atr(chart_, &cfg);
|
|
915
|
+
return jsi::Value::undefined();
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
866
919
|
if (name == "setOverlays") {
|
|
867
920
|
// setOverlays([{ kind, period, source, color, width }, ...]) — replaces the
|
|
868
921
|
// full set of MA/EMA overlay lines. No render; the next render() picks it up.
|
|
@@ -972,6 +1025,123 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
972
1025
|
});
|
|
973
1026
|
}
|
|
974
1027
|
|
|
1028
|
+
if (name == "setIchimoku") {
|
|
1029
|
+
// setIchimoku({enabled, tenkanPeriod, kijunPeriod, senkouBPeriod,
|
|
1030
|
+
// displacement, <line>Color/<line>Width/<line>Enabled for tenkan, kijun,
|
|
1031
|
+
// senkouA, senkouB and chikou, cloudEnabled, bullishCloudColor,
|
|
1032
|
+
// bearishCloudColor, cloudOpacity}) — the Ichimoku overlay. No render; the
|
|
1033
|
+
// next render() picks it up.
|
|
1034
|
+
return jsi::Function::createFromHostFunction(
|
|
1035
|
+
rt,
|
|
1036
|
+
jsi::PropNameID::forAscii(rt, "setIchimoku"),
|
|
1037
|
+
1,
|
|
1038
|
+
[this](jsi::Runtime& rt2,
|
|
1039
|
+
const jsi::Value& /*thisVal*/,
|
|
1040
|
+
const jsi::Value* args,
|
|
1041
|
+
size_t count) -> jsi::Value {
|
|
1042
|
+
if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
|
|
1043
|
+
auto s = args[0].asObject(rt2);
|
|
1044
|
+
const auto num = [&](const char* k) {
|
|
1045
|
+
return s.getProperty(rt2, k).asNumber();
|
|
1046
|
+
};
|
|
1047
|
+
const auto flag = [&](const char* k) {
|
|
1048
|
+
return s.getProperty(rt2, k).asBool() ? 1 : 0;
|
|
1049
|
+
};
|
|
1050
|
+
VroomIchimoku cfg{};
|
|
1051
|
+
cfg.enabled = flag("enabled");
|
|
1052
|
+
cfg.tenkan_period = static_cast<int32_t>(num("tenkanPeriod"));
|
|
1053
|
+
cfg.kijun_period = static_cast<int32_t>(num("kijunPeriod"));
|
|
1054
|
+
cfg.senkou_b_period = static_cast<int32_t>(num("senkouBPeriod"));
|
|
1055
|
+
cfg.displacement = static_cast<int32_t>(num("displacement"));
|
|
1056
|
+
cfg.tenkan_color = static_cast<uint32_t>(num("tenkanColor"));
|
|
1057
|
+
cfg.tenkan_width = static_cast<float>(num("tenkanWidth"));
|
|
1058
|
+
cfg.tenkan_enabled = flag("tenkanEnabled");
|
|
1059
|
+
cfg.kijun_color = static_cast<uint32_t>(num("kijunColor"));
|
|
1060
|
+
cfg.kijun_width = static_cast<float>(num("kijunWidth"));
|
|
1061
|
+
cfg.kijun_enabled = flag("kijunEnabled");
|
|
1062
|
+
cfg.senkou_a_color = static_cast<uint32_t>(num("senkouAColor"));
|
|
1063
|
+
cfg.senkou_a_width = static_cast<float>(num("senkouAWidth"));
|
|
1064
|
+
cfg.senkou_a_enabled = flag("senkouAEnabled");
|
|
1065
|
+
cfg.senkou_b_color = static_cast<uint32_t>(num("senkouBColor"));
|
|
1066
|
+
cfg.senkou_b_width = static_cast<float>(num("senkouBWidth"));
|
|
1067
|
+
cfg.senkou_b_enabled = flag("senkouBEnabled");
|
|
1068
|
+
cfg.chikou_color = static_cast<uint32_t>(num("chikouColor"));
|
|
1069
|
+
cfg.chikou_width = static_cast<float>(num("chikouWidth"));
|
|
1070
|
+
cfg.chikou_enabled = flag("chikouEnabled");
|
|
1071
|
+
cfg.cloud_enabled = flag("cloudEnabled");
|
|
1072
|
+
cfg.bullish_cloud_color =
|
|
1073
|
+
static_cast<uint32_t>(num("bullishCloudColor"));
|
|
1074
|
+
cfg.bearish_cloud_color =
|
|
1075
|
+
static_cast<uint32_t>(num("bearishCloudColor"));
|
|
1076
|
+
cfg.cloud_opacity = static_cast<float>(num("cloudOpacity"));
|
|
1077
|
+
vroom_chart_set_ichimoku(chart_, &cfg);
|
|
1078
|
+
return jsi::Value::undefined();
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (name == "setFairValueGaps") {
|
|
1083
|
+
// setFairValueGaps({enabled, maxBarsBack, waitForClose, fillType,
|
|
1084
|
+
// deleteAfterFill, extendBoxes, boxLength, bullishColor, bearishColor,
|
|
1085
|
+
// opacity, borderEnabled, borderStyle, borderWidth, bullishBorderColor,
|
|
1086
|
+
// bearishBorderColor, labelsEnabled, label, labelDistance, labelColor,
|
|
1087
|
+
// labelFontSize, showInverse, inverseBullishColor, inverseBearishColor,
|
|
1088
|
+
// inverseLabel}) — the Fair Value Gap overlay. No render; the next
|
|
1089
|
+
// render() picks it up.
|
|
1090
|
+
return jsi::Function::createFromHostFunction(
|
|
1091
|
+
rt,
|
|
1092
|
+
jsi::PropNameID::forAscii(rt, "setFairValueGaps"),
|
|
1093
|
+
1,
|
|
1094
|
+
[this](jsi::Runtime& rt2,
|
|
1095
|
+
const jsi::Value& /*thisVal*/,
|
|
1096
|
+
const jsi::Value* args,
|
|
1097
|
+
size_t count) -> jsi::Value {
|
|
1098
|
+
if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
|
|
1099
|
+
auto s = args[0].asObject(rt2);
|
|
1100
|
+
const auto num = [&](const char* k) {
|
|
1101
|
+
return s.getProperty(rt2, k).asNumber();
|
|
1102
|
+
};
|
|
1103
|
+
const auto flag = [&](const char* k) {
|
|
1104
|
+
return s.getProperty(rt2, k).asBool() ? 1 : 0;
|
|
1105
|
+
};
|
|
1106
|
+
VroomFairValueGaps cfg{};
|
|
1107
|
+
cfg.enabled = flag("enabled");
|
|
1108
|
+
cfg.max_bars_back = static_cast<int32_t>(num("maxBarsBack"));
|
|
1109
|
+
cfg.wait_for_close = flag("waitForClose");
|
|
1110
|
+
cfg.fill_type = static_cast<int32_t>(num("fillType"));
|
|
1111
|
+
cfg.delete_after_fill = flag("deleteAfterFill");
|
|
1112
|
+
cfg.extend_boxes = flag("extendBoxes");
|
|
1113
|
+
cfg.box_length = static_cast<int32_t>(num("boxLength"));
|
|
1114
|
+
cfg.bullish_color = static_cast<uint32_t>(num("bullishColor"));
|
|
1115
|
+
cfg.bearish_color = static_cast<uint32_t>(num("bearishColor"));
|
|
1116
|
+
cfg.opacity = static_cast<float>(num("opacity"));
|
|
1117
|
+
cfg.border_enabled = flag("borderEnabled");
|
|
1118
|
+
cfg.border_style = static_cast<int32_t>(num("borderStyle"));
|
|
1119
|
+
cfg.border_width = static_cast<float>(num("borderWidth"));
|
|
1120
|
+
cfg.bullish_border_color =
|
|
1121
|
+
static_cast<uint32_t>(num("bullishBorderColor"));
|
|
1122
|
+
cfg.bearish_border_color =
|
|
1123
|
+
static_cast<uint32_t>(num("bearishBorderColor"));
|
|
1124
|
+
cfg.labels_enabled = flag("labelsEnabled");
|
|
1125
|
+
// Held alive until the setter, which copies them.
|
|
1126
|
+
const std::string label =
|
|
1127
|
+
s.getProperty(rt2, "label").asString(rt2).utf8(rt2);
|
|
1128
|
+
cfg.label = label.c_str();
|
|
1129
|
+
cfg.label_distance = static_cast<int32_t>(num("labelDistance"));
|
|
1130
|
+
cfg.label_color = static_cast<uint32_t>(num("labelColor"));
|
|
1131
|
+
cfg.label_font_size = static_cast<float>(num("labelFontSize"));
|
|
1132
|
+
cfg.show_inverse = flag("showInverse");
|
|
1133
|
+
cfg.inverse_bullish_color =
|
|
1134
|
+
static_cast<uint32_t>(num("inverseBullishColor"));
|
|
1135
|
+
cfg.inverse_bearish_color =
|
|
1136
|
+
static_cast<uint32_t>(num("inverseBearishColor"));
|
|
1137
|
+
const std::string inverse_label =
|
|
1138
|
+
s.getProperty(rt2, "inverseLabel").asString(rt2).utf8(rt2);
|
|
1139
|
+
cfg.inverse_label = inverse_label.c_str();
|
|
1140
|
+
vroom_chart_set_fair_value_gaps(chart_, &cfg);
|
|
1141
|
+
return jsi::Value::undefined();
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
975
1145
|
if (name == "setVolume") {
|
|
976
1146
|
// setVolume({enabled, heightFrac, opacity, radiusPx, upColor, downColor})
|
|
977
1147
|
// — the volume bars. Negative floats / zero colors inherit the theme. No
|
|
@@ -69,6 +69,80 @@ typedef struct VroomBollinger {
|
|
|
69
69
|
float fill_opacity; // 0..1, multiplied into upper_color's alpha
|
|
70
70
|
} VroomBollinger;
|
|
71
71
|
|
|
72
|
+
// Ichimoku Kinko Hyo overlay drawn on the price pane: five lines plus the cloud
|
|
73
|
+
// (kumo) shaded between the two leading spans. No pane is reserved.
|
|
74
|
+
//
|
|
75
|
+
// Three of the lines are drawn away from the bar they were computed on:
|
|
76
|
+
// senkou A/B lead by `displacement` slots (so the cloud runs past the newest
|
|
77
|
+
// candle into empty time) and chikou lags by the same. Displacement is applied
|
|
78
|
+
// at draw time, so changing it re-renders without recomputing the series.
|
|
79
|
+
typedef struct VroomIchimoku {
|
|
80
|
+
int32_t enabled; // 0/1
|
|
81
|
+
int32_t tenkan_period; // conversion lookback (clamped >= 1; default 9)
|
|
82
|
+
int32_t kijun_period; // base lookback (clamped >= 1; default 26)
|
|
83
|
+
int32_t senkou_b_period; // span B lookback (clamped >= 1; default 52)
|
|
84
|
+
int32_t displacement; // slots the cloud leads / chikou lags (>= 0; default 26)
|
|
85
|
+
uint32_t tenkan_color; // 0xAARRGGBB
|
|
86
|
+
float tenkan_width; // stroke px
|
|
87
|
+
int32_t tenkan_enabled; // 0/1
|
|
88
|
+
uint32_t kijun_color;
|
|
89
|
+
float kijun_width;
|
|
90
|
+
int32_t kijun_enabled;
|
|
91
|
+
uint32_t senkou_a_color;
|
|
92
|
+
float senkou_a_width;
|
|
93
|
+
int32_t senkou_a_enabled;
|
|
94
|
+
uint32_t senkou_b_color;
|
|
95
|
+
float senkou_b_width;
|
|
96
|
+
int32_t senkou_b_enabled;
|
|
97
|
+
uint32_t chikou_color;
|
|
98
|
+
float chikou_width;
|
|
99
|
+
int32_t chikou_enabled;
|
|
100
|
+
int32_t cloud_enabled; // 0/1: shade between the leading spans
|
|
101
|
+
uint32_t bullish_cloud_color; // where senkou A is above senkou B
|
|
102
|
+
uint32_t bearish_cloud_color; // where senkou A is below senkou B
|
|
103
|
+
float cloud_opacity; // 0..1, multiplied into the cloud color's alpha
|
|
104
|
+
} VroomIchimoku;
|
|
105
|
+
|
|
106
|
+
// Fair Value Gap overlay drawn on the price pane: shaded boxes over three-candle
|
|
107
|
+
// imbalances, where candle i-1 and i+1's wicks fail to overlap and leave a band
|
|
108
|
+
// of price that was skipped. Bullish when candles[i-1].high < candles[i+1].low,
|
|
109
|
+
// bearish when candles[i-1].low > candles[i+1].high. No pane is reserved.
|
|
110
|
+
//
|
|
111
|
+
// Each box is anchored to the middle bar's open time and spans the untouched
|
|
112
|
+
// price range; it runs `box_length` slots right, or to the pane edge when
|
|
113
|
+
// `extend_boxes`. Only enabled, max_bars_back, wait_for_close and fill_type
|
|
114
|
+
// recompute — geometry and style are applied at draw time.
|
|
115
|
+
//
|
|
116
|
+
// With `show_inverse`, a filled gap draws a second box of the opposite polarity
|
|
117
|
+
// starting where the first one ends, lasting until price reclaims the band.
|
|
118
|
+
typedef struct VroomFairValueGaps {
|
|
119
|
+
int32_t enabled; // 0/1
|
|
120
|
+
int32_t max_bars_back; // bars to scan (clamped >= 0; default 300)
|
|
121
|
+
int32_t wait_for_close; // 0/1: withhold a gap until its 3rd bar closes
|
|
122
|
+
int32_t fill_type; // 0 = close through the far edge, 1 = wick
|
|
123
|
+
int32_t delete_after_fill; // 0/1: hide a gap once filled (else truncate it)
|
|
124
|
+
int32_t extend_boxes; // 0/1: run boxes to the pane edge
|
|
125
|
+
int32_t box_length; // box width in slots (clamped >= 1; default 20)
|
|
126
|
+
uint32_t bullish_color; // 0xAARRGGBB fill where the gap is bullish
|
|
127
|
+
uint32_t bearish_color; // fill where the gap is bearish
|
|
128
|
+
float opacity; // 0..1, multiplied into the fill color's alpha
|
|
129
|
+
int32_t border_enabled; // 0/1: stroke the box outline
|
|
130
|
+
int32_t border_style; // 0 = solid, 1 = dotted, 2 = dashed
|
|
131
|
+
float border_width; // stroke px
|
|
132
|
+
uint32_t bullish_border_color;
|
|
133
|
+
uint32_t bearish_border_color;
|
|
134
|
+
int32_t labels_enabled; // 0/1: draw `label` on each box
|
|
135
|
+
const char* label; // UTF-8, copied by the setter; NULL = "FVG"
|
|
136
|
+
int32_t label_distance; // slots of clearance, extend_boxes only (>= 0)
|
|
137
|
+
uint32_t label_color; // alpha 0 falls back to the box's border color
|
|
138
|
+
float label_font_size; // px; <= 0 falls back to the axis font size
|
|
139
|
+
int32_t show_inverse; // 0/1: keep drawing a filled gap, polarity flipped
|
|
140
|
+
uint32_t inverse_bullish_color; // fill where the inverted zone is bullish,
|
|
141
|
+
// i.e. where a bearish gap was violated
|
|
142
|
+
uint32_t inverse_bearish_color; // fill where a bullish gap was violated
|
|
143
|
+
const char* inverse_label; // UTF-8, copied by the setter; NULL = "iFVG"
|
|
144
|
+
} VroomFairValueGaps;
|
|
145
|
+
|
|
72
146
|
// MACD, drawn in its own pane below the candles: the difference between a fast
|
|
73
147
|
// and a slow moving average of `source`, a signal line smoothing that
|
|
74
148
|
// difference, and a histogram of the gap between the two.
|
|
@@ -128,8 +202,29 @@ typedef struct VroomRSI {
|
|
|
128
202
|
float ma_width;
|
|
129
203
|
uint32_t band_color; // both dashed rules; 0 inherits the default gray
|
|
130
204
|
int32_t bands_visible; // 0/1
|
|
205
|
+
// Shade where the line sits past a band, fading out at the rule and
|
|
206
|
+
// deepening toward the end of the scale. Takes its colors from
|
|
207
|
+
// VROOM_COLOR_ACCENT_BULL / _BEAR, and never reaches full opacity. 0/1.
|
|
208
|
+
int32_t extreme_fill;
|
|
131
209
|
} VroomRSI;
|
|
132
210
|
|
|
211
|
+
// Average True Range, drawn in its own pane below the candles: a single line
|
|
212
|
+
// measuring volatility in price units. True Range is the widest of the bar's
|
|
213
|
+
// own high-low span and the two gaps from its extremes to the previous close,
|
|
214
|
+
// smoothed over `period` bars.
|
|
215
|
+
//
|
|
216
|
+
// The style fields carry the same inherit sentinel as VroomMACD: a fully
|
|
217
|
+
// transparent color falls back to the built-in default and a non-positive width
|
|
218
|
+
// falls back to 1.5.
|
|
219
|
+
typedef struct VroomATR {
|
|
220
|
+
int32_t enabled; // 0/1
|
|
221
|
+
int32_t period; // lookback in candles (clamped >= 1; default 14)
|
|
222
|
+
int32_t smoothing; // 0 = Wilder's RMA, 1 = SMA, 2 = EMA
|
|
223
|
+
|
|
224
|
+
uint32_t line_color; // 0 inherits the default teal
|
|
225
|
+
float line_width; // stroke px; <= 0 inherits 1.5
|
|
226
|
+
} VroomATR;
|
|
227
|
+
|
|
133
228
|
// Session VWAP, drawn as a single line on the price pane. The session resets
|
|
134
229
|
// each UTC day shifted by `reset_offset_min` minutes, and the line lifts its pen
|
|
135
230
|
// at each reset. Color 0 inherits the default cyan; a non-positive width
|
|
@@ -475,10 +570,26 @@ void vroom_chart_preserve_price_envelope(VroomChart* chart,
|
|
|
475
570
|
// to 1. No-op when nothing is visible.
|
|
476
571
|
void vroom_chart_begin_interval_morph(VroomChart* chart, int32_t mode);
|
|
477
572
|
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
573
|
+
// Captures the same geometry for a live update to the series already shown —
|
|
574
|
+
// a tick to the in-progress bar. Always a transform, and unlike the interval
|
|
575
|
+
// morph it leaves the axes alone: the interval hasn't changed, so the ticks
|
|
576
|
+
// between the labels are still the right ones and must not be faded.
|
|
577
|
+
//
|
|
578
|
+
// Restarting one that is still running continues from the shape on screen
|
|
579
|
+
// rather than from the data under it, so ticks arriving faster than the
|
|
580
|
+
// animation lands stay smooth instead of snapping back each time.
|
|
581
|
+
//
|
|
582
|
+
// Call before set_candles, then drive vroom_chart_set_interval_morph from 0 to
|
|
583
|
+
// 1. No-op when nothing is visible. Not for an update that appends a bar: slots
|
|
584
|
+
// pair from the right edge, so a new bar would shift every candle onto its
|
|
585
|
+
// neighbour's geometry — advance the visible range instead and let the series
|
|
586
|
+
// translate.
|
|
587
|
+
void vroom_chart_begin_stream_morph(VroomChart* chart);
|
|
588
|
+
|
|
589
|
+
// Advances the morph started by either begin_*_morph above. `t` (clamped to
|
|
590
|
+
// 0..1) is the eased progress: 0 renders the captured geometry pixel-identically
|
|
591
|
+
// to the pre-swap frame, 1 renders the new candles and releases the capture.
|
|
592
|
+
// Driven per-frame by the host animation loop.
|
|
482
593
|
void vroom_chart_set_interval_morph(VroomChart* chart, float t);
|
|
483
594
|
|
|
484
595
|
void vroom_chart_pan(VroomChart* chart, float dx_px, float dy_px);
|
|
@@ -581,6 +692,11 @@ void vroom_chart_set_rsi(VroomChart* chart, const VroomRSI* cfg);
|
|
|
581
692
|
// re-render.
|
|
582
693
|
void vroom_chart_set_macd(VroomChart* chart, const VroomMACD* cfg);
|
|
583
694
|
|
|
695
|
+
// Configures the ATR indicator (its own pane below the candles, stacking in
|
|
696
|
+
// enable order like RSI and MACD). Period and smoothing changes recompute the
|
|
697
|
+
// series; color and width changes only re-render.
|
|
698
|
+
void vroom_chart_set_atr(VroomChart* chart, const VroomATR* cfg);
|
|
699
|
+
|
|
584
700
|
// Replaces the full set of moving-average overlay lines (SMA/EMA) drawn on the
|
|
585
701
|
// price pane. Pass count 0 to clear them. Overlays don't reserve a pane.
|
|
586
702
|
void vroom_chart_set_overlays(VroomChart* chart, const VroomOverlay* overlays,
|
|
@@ -596,6 +712,23 @@ void vroom_chart_set_vwap(VroomChart* chart, const VroomVWAP* cfg);
|
|
|
596
712
|
// changes only re-render; enabled/period/mult/source/basis changes recompute.
|
|
597
713
|
void vroom_chart_set_bollinger(VroomChart* chart, const VroomBollinger* cfg);
|
|
598
714
|
|
|
715
|
+
// Configures the Ichimoku overlay (five price-pane lines + the cloud between
|
|
716
|
+
// the leading spans; no pane is reserved). Only enabled and the three periods
|
|
717
|
+
// recompute; style, visibility and displacement changes just re-render.
|
|
718
|
+
//
|
|
719
|
+
// Enabling it also pulls the view forward so the leading spans, which sit past
|
|
720
|
+
// the newest candle, are on screen — the same future gap the default framing
|
|
721
|
+
// reserves when the indicator is already on at set_candles time.
|
|
722
|
+
void vroom_chart_set_ichimoku(VroomChart* chart, const VroomIchimoku* cfg);
|
|
723
|
+
|
|
724
|
+
// Configures the Fair Value Gap overlay (shaded imbalance boxes on the price
|
|
725
|
+
// pane; no pane is reserved). Only enabled, max_bars_back, wait_for_close and
|
|
726
|
+
// fill_type recompute; box geometry, style and labels just re-render.
|
|
727
|
+
//
|
|
728
|
+
// `cfg->label` is copied, so the caller may free it as soon as this returns.
|
|
729
|
+
void vroom_chart_set_fair_value_gaps(VroomChart* chart,
|
|
730
|
+
const VroomFairValueGaps* cfg);
|
|
731
|
+
|
|
599
732
|
// Configures the volume bars. Render-only — the bars come straight off each
|
|
600
733
|
// candle's volume, so nothing is recomputed. Bars are enabled by default; pass
|
|
601
734
|
// a config with `enabled` 0 to hide them.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#include "atr.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm> // std::max
|
|
4
|
+
#include <cmath> // std::fabs, std::nan
|
|
5
|
+
|
|
6
|
+
#include "ma.h"
|
|
7
|
+
#include "series_ma.h"
|
|
8
|
+
|
|
9
|
+
namespace vroom::atr {
|
|
10
|
+
|
|
11
|
+
void compute(const ::VroomCandle* candles, std::size_t n, int period,
|
|
12
|
+
int smoothing, std::vector<double>& out) {
|
|
13
|
+
out.assign(n, std::nan(""));
|
|
14
|
+
if (!candles || period < 1) return;
|
|
15
|
+
const std::size_t P = static_cast<std::size_t>(period);
|
|
16
|
+
if (n < P) return;
|
|
17
|
+
|
|
18
|
+
std::vector<double> tr(n);
|
|
19
|
+
tr[0] = candles[0].high - candles[0].low; // no previous close to gap from
|
|
20
|
+
for (std::size_t i = 1; i < n; ++i) {
|
|
21
|
+
const double prev_close = candles[i - 1].close;
|
|
22
|
+
tr[i] = std::max({candles[i].high - candles[i].low,
|
|
23
|
+
std::fabs(candles[i].high - prev_close),
|
|
24
|
+
std::fabs(candles[i].low - prev_close)});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (smoothing == kSma) {
|
|
28
|
+
vroom::series_ma::smooth(tr, vroom::ma::KIND_SMA, period, out);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (smoothing == kEma) {
|
|
32
|
+
vroom::series_ma::smooth(tr, vroom::ma::KIND_EMA, period, out);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Wilder's RMA: seeded with the simple average of the first P true ranges,
|
|
37
|
+
// then avg = (prevAvg * (P - 1) + current) / P.
|
|
38
|
+
double avg = 0.0;
|
|
39
|
+
for (std::size_t i = 0; i < P; ++i) avg += tr[i];
|
|
40
|
+
avg /= static_cast<double>(P);
|
|
41
|
+
out[P - 1] = avg;
|
|
42
|
+
|
|
43
|
+
const double pm1 = static_cast<double>(P - 1);
|
|
44
|
+
const double pd = static_cast<double>(P);
|
|
45
|
+
for (std::size_t i = P; i < n; ++i) {
|
|
46
|
+
avg = (avg * pm1 + tr[i]) / pd;
|
|
47
|
+
out[i] = avg;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
double autoscale(const double* visible, std::size_t n) {
|
|
52
|
+
double scale = 0.0;
|
|
53
|
+
if (!visible) return scale;
|
|
54
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
55
|
+
if (std::isfinite(visible[i])) scale = std::max(scale, visible[i]);
|
|
56
|
+
}
|
|
57
|
+
return scale;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
double band_fraction(double v, double scale, double y_scale) {
|
|
61
|
+
// Nothing on show yet — everything sits on the baseline rather than
|
|
62
|
+
// dividing by zero.
|
|
63
|
+
if (!(scale > 0.0)) return 0.0;
|
|
64
|
+
return (v / scale) * kBandPadFraction * y_scale;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} // namespace vroom::atr
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Average True Range — pure computation, no Skia. Kept Skia-free so it builds
|
|
2
|
+
// into the unit-test target.
|
|
3
|
+
//
|
|
4
|
+
// True Range is the widest of the bar's own high-low span and the two gaps from
|
|
5
|
+
// its extremes to the previous close, so an overnight jump the bar's range
|
|
6
|
+
// misses still counts. ATR is that series smoothed over `period` bars.
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <cstddef>
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
#include "vroom/vroom_chart.h" // ::VroomCandle
|
|
14
|
+
|
|
15
|
+
namespace vroom::atr {
|
|
16
|
+
|
|
17
|
+
// How the true-range series is smoothed. kRma is Wilder's original
|
|
18
|
+
// (alpha = 1/period) and the conventional default.
|
|
19
|
+
enum Smoothing { kRma = 0, kSma = 1, kEma = 2 };
|
|
20
|
+
|
|
21
|
+
// How much of the pane band the curve is allowed to fill, so a peak stops short
|
|
22
|
+
// of the separator above it instead of touching.
|
|
23
|
+
constexpr double kBandPadFraction = 0.85;
|
|
24
|
+
|
|
25
|
+
// Computes ATR over [candles, candles+n). `period` is clamped to >= 1.
|
|
26
|
+
// Fills `out` (resized to n): out[i] is the ATR at candle i in price units, or
|
|
27
|
+
// NaN before the first defined value. True range exists from bar 0 (which has
|
|
28
|
+
// no previous close, so it falls back to high - low), so the first ATR lands at
|
|
29
|
+
// index period - 1 — one earlier than RSI.
|
|
30
|
+
void compute(const ::VroomCandle* candles, std::size_t n, int period,
|
|
31
|
+
int smoothing, std::vector<double>& out);
|
|
32
|
+
|
|
33
|
+
// The value the pane band is fitted to: the largest finite ATR on show, or 0
|
|
34
|
+
// when there is nothing to plot. ATR is strictly positive, so the domain runs
|
|
35
|
+
// from 0 at the band's bottom edge up to this.
|
|
36
|
+
double autoscale(const double* visible, std::size_t n);
|
|
37
|
+
|
|
38
|
+
// Where a value sits in the pane band, as a fraction of its height — 0 at the
|
|
39
|
+
// bottom edge, 1 at the top. `y_scale` is the user's y-axis zoom (1 = the
|
|
40
|
+
// default fit). Split out of the renderer so the interval-morph capture maps
|
|
41
|
+
// its geometry through the same math the pane draws with.
|
|
42
|
+
double band_fraction(double v, double scale, double y_scale);
|
|
43
|
+
|
|
44
|
+
} // namespace vroom::atr
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#include "atr_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/SkPathBuilder.h"
|
|
11
|
+
#include "include/core/SkRect.h"
|
|
12
|
+
#include "include/core/SkTypeface.h"
|
|
13
|
+
#pragma clang diagnostic pop
|
|
14
|
+
|
|
15
|
+
#include <algorithm>
|
|
16
|
+
#include <cmath>
|
|
17
|
+
#include <cstdio>
|
|
18
|
+
#include <cstring>
|
|
19
|
+
|
|
20
|
+
#include "atr.h"
|
|
21
|
+
#include "chart.h"
|
|
22
|
+
#include "fonts.h"
|
|
23
|
+
#include "line_morph.h"
|
|
24
|
+
#include "pane_series.h"
|
|
25
|
+
#include "price_format.h"
|
|
26
|
+
#include "style_inherit.h"
|
|
27
|
+
#include "theme.h"
|
|
28
|
+
#include "viewport.h"
|
|
29
|
+
|
|
30
|
+
namespace vroom::atr_pane {
|
|
31
|
+
|
|
32
|
+
namespace {
|
|
33
|
+
using vroom::style::color_or;
|
|
34
|
+
using vroom::style::width_or;
|
|
35
|
+
|
|
36
|
+
constexpr SkColor kAtrLine = 0xff26a69a; // teal
|
|
37
|
+
constexpr SkColor kDivider = 0xff21262d; // pane separator
|
|
38
|
+
constexpr float kLineWidth = 1.5f;
|
|
39
|
+
} // namespace
|
|
40
|
+
|
|
41
|
+
void draw(SkCanvas* canvas,
|
|
42
|
+
const VroomChart& chart,
|
|
43
|
+
const Layout& lay,
|
|
44
|
+
const ::VroomCandle* visible,
|
|
45
|
+
std::size_t n,
|
|
46
|
+
const double* atr_visible,
|
|
47
|
+
int64_t window_ms,
|
|
48
|
+
int64_t visible_start_ms,
|
|
49
|
+
int64_t candle_duration_ms,
|
|
50
|
+
float candle_right,
|
|
51
|
+
float pane_top,
|
|
52
|
+
float pane_bottom,
|
|
53
|
+
const vroom::LineMorph* from,
|
|
54
|
+
float morph_t) {
|
|
55
|
+
if (!canvas || candle_right <= 0.f) return;
|
|
56
|
+
const float band_h = pane_bottom - pane_top;
|
|
57
|
+
if (band_h <= 0.f) return;
|
|
58
|
+
morph_t = std::clamp(morph_t, 0.f, 1.f);
|
|
59
|
+
// A fade's outgoing half has no new data at all, so the capture is the whole
|
|
60
|
+
// frame. Nothing on either side means nothing to paint, shell included.
|
|
61
|
+
if (n == 0 && vroom::morph_line_count(from, morph_t) == 0) return;
|
|
62
|
+
|
|
63
|
+
const VroomATR& cfg = chart.atr;
|
|
64
|
+
|
|
65
|
+
// ATR is strictly positive, so the domain runs 0..peak off the bottom edge.
|
|
66
|
+
// User y-zoom stretches it from there; 1.0 is the default fit.
|
|
67
|
+
const double scale = vroom::atr::autoscale(atr_visible, n);
|
|
68
|
+
auto y_for = [&](double v) -> float {
|
|
69
|
+
return pane_bottom -
|
|
70
|
+
static_cast<float>(
|
|
71
|
+
vroom::atr::band_fraction(v, scale, chart.atr_y_scale)) *
|
|
72
|
+
band_h;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// The peak the label reports. Mid-morph it eases out of the fit the capture
|
|
76
|
+
// was taken against, so the number tracks the curve instead of jumping to
|
|
77
|
+
// the new resolution's peak on the first frame.
|
|
78
|
+
const double label_scale =
|
|
79
|
+
(from && morph_t < 1.f)
|
|
80
|
+
? from->scale + (scale - from->scale) * static_cast<double>(morph_t)
|
|
81
|
+
: scale;
|
|
82
|
+
|
|
83
|
+
// Mask the band (candles can overflow below the shortened price pane).
|
|
84
|
+
SkPaint bg;
|
|
85
|
+
bg.setColor(chart.theme.colors[VROOM_COLOR_BACKGROUND]);
|
|
86
|
+
canvas->drawRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom),
|
|
87
|
+
bg);
|
|
88
|
+
|
|
89
|
+
// Pane separator (top edge).
|
|
90
|
+
SkPaint divider;
|
|
91
|
+
divider.setColor(kDivider);
|
|
92
|
+
divider.setStrokeWidth(1.f);
|
|
93
|
+
canvas->drawLine(0.f, pane_top, candle_right, pane_top, divider);
|
|
94
|
+
|
|
95
|
+
canvas->save();
|
|
96
|
+
canvas->clipRect(SkRect::MakeLTRB(0.f, pane_top, candle_right, pane_bottom));
|
|
97
|
+
|
|
98
|
+
if (atr_visible || from) {
|
|
99
|
+
SkPaint line;
|
|
100
|
+
line.setAntiAlias(true);
|
|
101
|
+
line.setColor(color_or(cfg.line_color, kAtrLine));
|
|
102
|
+
line.setStyle(SkPaint::kStroke_Style);
|
|
103
|
+
line.setStrokeWidth(width_or(cfg.line_width, kLineWidth));
|
|
104
|
+
canvas->drawPath(
|
|
105
|
+
vroom::pane_series::build_path(
|
|
106
|
+
lay, visible, n, atr_visible, window_ms, visible_start_ms,
|
|
107
|
+
candle_duration_ms, pane_top, pane_bottom, from, morph_t, y_for),
|
|
108
|
+
line);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Caption, top-left of the pane.
|
|
112
|
+
auto tf = vroom::axis_typeface();
|
|
113
|
+
if (tf) {
|
|
114
|
+
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
115
|
+
font.setSubpixel(true);
|
|
116
|
+
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
117
|
+
char caption[24];
|
|
118
|
+
std::snprintf(caption, sizeof(caption), "ATR %d", cfg.period);
|
|
119
|
+
SkRect cb;
|
|
120
|
+
font.measureText(caption, std::strlen(caption), SkTextEncoding::kUTF8,
|
|
121
|
+
&cb);
|
|
122
|
+
SkPaint cap_paint;
|
|
123
|
+
cap_paint.setAntiAlias(true);
|
|
124
|
+
cap_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
|
|
125
|
+
canvas->drawString(caption, 6.f, pane_top + 4.f - cb.fTop, font,
|
|
126
|
+
cap_paint);
|
|
127
|
+
}
|
|
128
|
+
canvas->restore();
|
|
129
|
+
|
|
130
|
+
// Peak label in the y-axis strip — ATR is in price units, so it shares the
|
|
131
|
+
// price axis's formatting. Hidden with that strip.
|
|
132
|
+
if (tf && label_scale > 0.0 && lay.y_axis_opacity > 0.f) {
|
|
133
|
+
SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
|
|
134
|
+
font.setSubpixel(true);
|
|
135
|
+
font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
|
|
136
|
+
SkPaint text_paint;
|
|
137
|
+
text_paint.setAntiAlias(true);
|
|
138
|
+
text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
|
|
139
|
+
text_paint.setAlphaf(text_paint.getAlphaf() * lay.y_axis_opacity);
|
|
140
|
+
char label[48];
|
|
141
|
+
vroom::format_price(label, sizeof(label), label_scale, chart.price_fmt);
|
|
142
|
+
SkRect tb;
|
|
143
|
+
const float tw = font.measureText(label, std::strlen(label),
|
|
144
|
+
SkTextEncoding::kUTF8, &tb);
|
|
145
|
+
const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
|
|
146
|
+
const float text_x = axis_center_x - tw * 0.5f;
|
|
147
|
+
// A series' own peak lands at the top of the padded band whatever the
|
|
148
|
+
// fit is, so the label's height doesn't move as the morph re-scales —
|
|
149
|
+
// only the number it reports does. Clamped into the band so a zoomed-in
|
|
150
|
+
// peak doesn't label another pane.
|
|
151
|
+
const float half_text = (tb.fBottom - tb.fTop) * 0.5f;
|
|
152
|
+
const float peak_y = std::clamp(
|
|
153
|
+
pane_bottom - static_cast<float>(vroom::atr::band_fraction(
|
|
154
|
+
label_scale, label_scale, chart.atr_y_scale)) *
|
|
155
|
+
band_h,
|
|
156
|
+
pane_top + half_text, pane_bottom - half_text);
|
|
157
|
+
const float baseline_y = peak_y - (tb.fTop + tb.fBottom) * 0.5f;
|
|
158
|
+
canvas->drawString(label, text_x, baseline_y, font, text_paint);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
} // namespace vroom::atr_pane
|