react-native-vroom-chart 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/CMakeLists.txt +8 -0
- package/android/README.md +11 -10
- package/cpp/VroomChartHostObject.cpp +140 -47
- package/cpp/VroomChartHostObject.h +1 -1
- package/cpp/VroomJsiInstaller.cpp +1 -1
- package/cpp/_core_src/price_format.cpp +55 -4
- package/cpp/_core_src/price_format.h +32 -5
- package/lib/index.js +62 -32
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +71 -34
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +92 -43
- package/src/jsi.d.ts +12 -9
- package/src/useChartCore.ts +4 -5
package/android/CMakeLists.txt
CHANGED
|
@@ -124,6 +124,14 @@ target_compile_options(vroomchart PRIVATE -Wno-mismatched-tags)
|
|
|
124
124
|
|
|
125
125
|
find_library(VROOM_LOG_LIB log)
|
|
126
126
|
|
|
127
|
+
# Android 15+ devices can use 16KB pages. NDK r27 and older default to 4KB
|
|
128
|
+
# ELF alignment (align=2**12); Play/SDK 35 then rejects libvroomchart.so.
|
|
129
|
+
# r28+ already aligns to 16KB, so these flags are redundant there but harmless.
|
|
130
|
+
target_link_options(vroomchart PRIVATE
|
|
131
|
+
"-Wl,-z,max-page-size=16384"
|
|
132
|
+
"-Wl,-z,common-page-size=16384"
|
|
133
|
+
)
|
|
134
|
+
|
|
127
135
|
target_link_libraries(vroomchart
|
|
128
136
|
${VROOM_LOG_LIB}
|
|
129
137
|
ReactAndroid::jsi
|
package/android/README.md
CHANGED
|
@@ -52,13 +52,14 @@ different, unmerged RTTI record for "the same" class and fails with
|
|
|
52
52
|
`Expected a Skia object of a different type`.
|
|
53
53
|
|
|
54
54
|
`VroomChartHostObject.cpp`'s `wrapPicture()` works around this only on
|
|
55
|
-
Android
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
Android. Two copies of Skia cannot share an `sk_sp<SkPicture>` pointer, and
|
|
56
|
+
serializing the chart picture (the original workaround) embedded the Android
|
|
57
|
+
system typeface on every pan/zoom frame — an OOM that killed the process after
|
|
58
|
+
a few seconds of gesturing.
|
|
59
|
+
|
|
60
|
+
The hot path now rasterizes the picture in vroom's Skia and hands RN-Skia raw
|
|
61
|
+
pixels via `Skia.Image.MakeImage`, so `<Image>` paints a framebuffer whose size
|
|
62
|
+
is bounded by the view. If rasterization fails, we still serialize, but with
|
|
63
|
+
`SkTypeface::SerializeBehavior::kDontIncludeData` so the font file is not
|
|
64
|
+
copied; `MakePicture` re-resolves `sans-serif` inside `librnskia.so`. iOS is
|
|
65
|
+
unchanged (one binary, direct `JsiSkPicture` wrap).
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include <string>
|
|
5
5
|
#include <vector>
|
|
6
6
|
|
|
7
|
+
#include "chart.h"
|
|
7
8
|
#include "chart_internal.h"
|
|
8
9
|
#include "vroom/vroom_chart.h"
|
|
9
10
|
|
|
@@ -11,6 +12,18 @@
|
|
|
11
12
|
#include "JsiSkNativeObjects.h"
|
|
12
13
|
#include "JsiSkPicture.h"
|
|
13
14
|
|
|
15
|
+
#if defined(__ANDROID__)
|
|
16
|
+
#include <algorithm>
|
|
17
|
+
#include <cmath>
|
|
18
|
+
|
|
19
|
+
#include "include/core/SkBitmap.h"
|
|
20
|
+
#include "include/core/SkCanvas.h"
|
|
21
|
+
#include "include/core/SkData.h"
|
|
22
|
+
#include "include/core/SkImageInfo.h"
|
|
23
|
+
#include "include/core/SkSerialProcs.h"
|
|
24
|
+
#include "include/core/SkTypeface.h"
|
|
25
|
+
#endif
|
|
26
|
+
|
|
14
27
|
namespace vroom {
|
|
15
28
|
|
|
16
29
|
namespace jsi = facebook::jsi;
|
|
@@ -74,27 +87,21 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
#if defined(__ANDROID__)
|
|
77
|
-
// Android only: RN-Skia
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// `getJsiObject<JsiSkPicture>(rt, value)` — a dynamic_pointer_cast
|
|
83
|
-
// under the hood), the cast fails cross-.so with "Expected a Skia object of
|
|
84
|
-
// a different type", because the object's *runtime* type was never actually
|
|
85
|
-
// compiled inside librnskia.so. iOS statically links everything into one
|
|
86
|
-
// binary, so no such mismatch exists there.
|
|
90
|
+
// Android only: RN-Skia lives in librnskia.so, vroom in libvroomchart.so, and
|
|
91
|
+
// each statically links its own libskia.a. A JsiSkPicture constructed here has
|
|
92
|
+
// our .so's RTTI, so RN-Skia's dynamic_pointer_cast fails with "Expected a
|
|
93
|
+
// Skia object of a different type". iOS statically links one binary, so the
|
|
94
|
+
// iOS wrap below can hand the sk_sp<SkPicture> through directly.
|
|
87
95
|
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
// Crossing that boundary used to serialize the chart picture and call
|
|
97
|
+
// Skia.Picture.MakePicture(bytes). Default SkPicture::serialize() embeds the
|
|
98
|
+
// Android system typeface (Noto-scale, megabytes) on every pan/zoom frame;
|
|
99
|
+
// Hermes never kept up and the process was OOM-killed (~3 GB RSS in ~10s).
|
|
100
|
+
//
|
|
101
|
+
// Preferred path: rasterize in *our* Skia and hand RN-Skia raw pixels via
|
|
102
|
+
// Skia.Image.MakeImage — memory is one framebuffer, labels stay correct.
|
|
103
|
+
// Fallback: still serialize, but with kDontIncludeData so the font file is
|
|
104
|
+
// not copied; MakePicture re-resolves sans-serif on RN-Skia's side.
|
|
98
105
|
namespace {
|
|
99
106
|
class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
|
|
100
107
|
public:
|
|
@@ -108,41 +115,119 @@ class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
|
|
|
108
115
|
private:
|
|
109
116
|
sk_sp<SkData> data_;
|
|
110
117
|
};
|
|
111
|
-
} // namespace
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (!
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
jsi::Value skiaApiObject(jsi::Runtime& rt, const char* name) {
|
|
120
|
+
auto skiaApi = rt.global().getProperty(rt, "SkiaApi");
|
|
121
|
+
if (!skiaApi.isObject()) return jsi::Value::undefined();
|
|
122
|
+
auto prop = skiaApi.asObject(rt).getProperty(rt, name);
|
|
123
|
+
return prop.isObject() ? std::move(prop) : jsi::Value::undefined();
|
|
124
|
+
}
|
|
118
125
|
|
|
119
|
-
|
|
126
|
+
// Uint8Array-shaped `{ buffer }` — MakePicture and Data.fromBytes both
|
|
127
|
+
// read the ArrayBuffer off that property.
|
|
128
|
+
jsi::Object bytesLike(jsi::Runtime& rt, sk_sp<SkData> data) {
|
|
129
|
+
auto buffer = std::make_shared<SkDataMutableBuffer>(std::move(data));
|
|
120
130
|
jsi::ArrayBuffer arrayBuffer(rt, buffer);
|
|
131
|
+
jsi::Object arg(rt);
|
|
132
|
+
arg.setProperty(rt, "buffer", arrayBuffer);
|
|
133
|
+
return arg;
|
|
134
|
+
}
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
if (!
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
sk_sp<const SkData> serializeTypefaceNoEmbed(SkTypeface* tf, void*) {
|
|
137
|
+
if (!tf) return nullptr;
|
|
138
|
+
return tf->serialize(SkTypeface::SerializeBehavior::kDontIncludeData);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
jsi::Value wrapSerializedPicture(jsi::Runtime& rt, const sk_sp<SkPicture>& pic) {
|
|
142
|
+
SkSerialProcs procs;
|
|
143
|
+
procs.fTypefaceProc = serializeTypefaceNoEmbed;
|
|
144
|
+
sk_sp<SkData> serialized = pic->serialize(&procs);
|
|
145
|
+
if (!serialized) return jsi::Value::null();
|
|
146
|
+
|
|
147
|
+
auto pictureFactory = skiaApiObject(rt, "Picture");
|
|
148
|
+
if (pictureFactory.isUndefined()) return jsi::Value::null();
|
|
126
149
|
auto pictureFactoryObj = pictureFactory.asObject(rt);
|
|
127
150
|
auto makePicture =
|
|
128
151
|
pictureFactoryObj.getPropertyAsFunction(rt, "MakePicture");
|
|
129
152
|
|
|
130
|
-
// Mirrors JsiSkPictureFactory::MakePicture's expected argument shape: an
|
|
131
|
-
// object with a `.buffer` property holding the ArrayBuffer (matching a
|
|
132
|
-
// Uint8Array-like value; the factory ignores everything else about it).
|
|
133
|
-
//
|
|
134
153
|
// Skia 2.11's NativeState host methods recover the factory via `this`
|
|
135
154
|
// (`fromThis` → "Expected PictureFactory but got non-object" if unbound).
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
return makePicture.callWithThis(rt, pictureFactoryObj,
|
|
156
|
+
bytesLike(rt, std::move(serialized)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
jsi::Value wrapRasterImage(jsi::Runtime& rt, const sk_sp<SkPicture>& pic,
|
|
160
|
+
float dpr) {
|
|
161
|
+
const SkRect cull = pic->cullRect();
|
|
162
|
+
const float scale = dpr > 0.f ? dpr : 1.f;
|
|
163
|
+
const int w =
|
|
164
|
+
std::max(1, static_cast<int>(std::ceil(cull.width() * scale)));
|
|
165
|
+
const int h =
|
|
166
|
+
std::max(1, static_cast<int>(std::ceil(cull.height() * scale)));
|
|
167
|
+
|
|
168
|
+
// Reused across frames on this JS thread so pan/zoom doesn't heap-allocate
|
|
169
|
+
// a new bitmap every time. Multiple charts share it; a size change reallocs.
|
|
170
|
+
thread_local SkBitmap raster;
|
|
171
|
+
const auto info = SkImageInfo::MakeN32Premul(w, h);
|
|
172
|
+
if ((raster.width() != w || raster.height() != h) &&
|
|
173
|
+
!raster.tryAllocPixels(info)) {
|
|
174
|
+
return jsi::Value::null();
|
|
175
|
+
}
|
|
176
|
+
raster.eraseColor(SK_ColorTRANSPARENT);
|
|
177
|
+
SkCanvas canvas(raster);
|
|
178
|
+
canvas.scale(scale, scale);
|
|
179
|
+
canvas.translate(-cull.left(), -cull.top());
|
|
180
|
+
canvas.drawPicture(pic);
|
|
181
|
+
|
|
182
|
+
SkPixmap pixmap;
|
|
183
|
+
if (!raster.peekPixels(&pixmap) || pixmap.addr() == nullptr) {
|
|
184
|
+
return jsi::Value::null();
|
|
185
|
+
}
|
|
186
|
+
sk_sp<SkData> pixels =
|
|
187
|
+
SkData::MakeWithCopy(pixmap.addr(), pixmap.computeByteSize());
|
|
188
|
+
if (!pixels) return jsi::Value::null();
|
|
189
|
+
|
|
190
|
+
auto dataFactory = skiaApiObject(rt, "Data");
|
|
191
|
+
auto imageFactory = skiaApiObject(rt, "Image");
|
|
192
|
+
if (dataFactory.isUndefined() || imageFactory.isUndefined()) {
|
|
193
|
+
return jsi::Value::null();
|
|
194
|
+
}
|
|
195
|
+
auto dataFactoryObj = dataFactory.asObject(rt);
|
|
196
|
+
auto imageFactoryObj = imageFactory.asObject(rt);
|
|
197
|
+
auto fromBytes = dataFactoryObj.getPropertyAsFunction(rt, "fromBytes");
|
|
198
|
+
auto makeImage = imageFactoryObj.getPropertyAsFunction(rt, "MakeImage");
|
|
199
|
+
|
|
200
|
+
auto skData =
|
|
201
|
+
fromBytes.callWithThis(rt, dataFactoryObj, bytesLike(rt, std::move(pixels)));
|
|
202
|
+
if (!skData.isObject()) return jsi::Value::null();
|
|
203
|
+
|
|
204
|
+
jsi::Object imageInfo(rt);
|
|
205
|
+
imageInfo.setProperty(rt, "width", static_cast<double>(w));
|
|
206
|
+
imageInfo.setProperty(rt, "height", static_cast<double>(h));
|
|
207
|
+
imageInfo.setProperty(rt, "colorType",
|
|
208
|
+
static_cast<double>(pixmap.colorType()));
|
|
209
|
+
imageInfo.setProperty(rt, "alphaType",
|
|
210
|
+
static_cast<double>(pixmap.alphaType()));
|
|
211
|
+
return makeImage.callWithThis(rt, imageFactoryObj, imageInfo, skData,
|
|
212
|
+
static_cast<double>(pixmap.rowBytes()));
|
|
213
|
+
}
|
|
214
|
+
} // namespace
|
|
215
|
+
|
|
216
|
+
static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
|
|
217
|
+
const sk_sp<SkPicture>& pic,
|
|
218
|
+
float dpr = 1.f) {
|
|
219
|
+
if (!pic) return facebook::jsi::Value::null();
|
|
220
|
+
auto image = wrapRasterImage(rt, pic, dpr);
|
|
221
|
+
if (!image.isNull()) return image;
|
|
222
|
+
return wrapSerializedPicture(rt, pic);
|
|
139
223
|
}
|
|
140
224
|
#else
|
|
141
225
|
// Shared helper: wraps a fresh picture for return to JS. Memory pressure is
|
|
142
226
|
// applied inside JsiSkPicture::create (NativeObject::create reads
|
|
143
227
|
// getMemoryPressure() → picture->approximateBytesUsed()).
|
|
144
228
|
static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
|
|
145
|
-
const sk_sp<SkPicture>& pic
|
|
229
|
+
const sk_sp<SkPicture>& pic,
|
|
230
|
+
float /*dpr*/ = 1.f) {
|
|
146
231
|
if (!pic) return facebook::jsi::Value::null();
|
|
147
232
|
auto host =
|
|
148
233
|
std::make_shared<RNSkia::JsiSkPicture>(/*context=*/nullptr, pic);
|
|
@@ -448,7 +533,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
448
533
|
const float dx = static_cast<float>(args[0].asNumber());
|
|
449
534
|
const float dy = static_cast<float>(args[1].asNumber());
|
|
450
535
|
vroom_chart_pan(chart_, dx, dy);
|
|
451
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
536
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
537
|
+
chart_->px_ratio);
|
|
452
538
|
});
|
|
453
539
|
}
|
|
454
540
|
|
|
@@ -467,7 +553,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
467
553
|
const float dx = static_cast<float>(args[0].asNumber());
|
|
468
554
|
const float dy = static_cast<float>(args[1].asNumber());
|
|
469
555
|
vroom_chart_translate(chart_, dx, dy);
|
|
470
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
556
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
557
|
+
chart_->px_ratio);
|
|
471
558
|
});
|
|
472
559
|
}
|
|
473
560
|
|
|
@@ -489,7 +576,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
489
576
|
const float fx = static_cast<float>(args[2].asNumber());
|
|
490
577
|
const float fy = static_cast<float>(args[3].asNumber());
|
|
491
578
|
vroom_chart_zoom(chart_, sx, sy, fx, fy);
|
|
492
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
579
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
580
|
+
chart_->px_ratio);
|
|
493
581
|
});
|
|
494
582
|
}
|
|
495
583
|
|
|
@@ -505,7 +593,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
505
593
|
if (count < 1) return jsi::Value::null();
|
|
506
594
|
const float dy = static_cast<float>(args[0].asNumber());
|
|
507
595
|
vroom_chart_scale_price_axis(chart_, dy);
|
|
508
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
596
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
597
|
+
chart_->px_ratio);
|
|
509
598
|
});
|
|
510
599
|
}
|
|
511
600
|
|
|
@@ -521,7 +610,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
521
610
|
if (count < 1) return jsi::Value::null();
|
|
522
611
|
const float dx = static_cast<float>(args[0].asNumber());
|
|
523
612
|
vroom_chart_scale_time_axis(chart_, dx);
|
|
524
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
613
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
614
|
+
chart_->px_ratio);
|
|
525
615
|
});
|
|
526
616
|
}
|
|
527
617
|
|
|
@@ -572,7 +662,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
572
662
|
const float x = static_cast<float>(args[0].asNumber());
|
|
573
663
|
const float y = static_cast<float>(args[1].asNumber());
|
|
574
664
|
vroom_chart_set_crosshair(chart_, x, y);
|
|
575
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
665
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
666
|
+
chart_->px_ratio);
|
|
576
667
|
});
|
|
577
668
|
}
|
|
578
669
|
|
|
@@ -587,7 +678,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
587
678
|
const jsi::Value* /*args*/,
|
|
588
679
|
size_t /*count*/) -> jsi::Value {
|
|
589
680
|
vroom_chart_clear_crosshair(chart_);
|
|
590
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
681
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
682
|
+
chart_->px_ratio);
|
|
591
683
|
});
|
|
592
684
|
}
|
|
593
685
|
|
|
@@ -1098,7 +1190,8 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
1098
1190
|
const jsi::Value& /*thisVal*/,
|
|
1099
1191
|
const jsi::Value* /*args*/,
|
|
1100
1192
|
size_t /*count*/) -> jsi::Value {
|
|
1101
|
-
return wrapPicture(rt2, render_chart_picture(chart_)
|
|
1193
|
+
return wrapPicture(rt2, render_chart_picture(chart_),
|
|
1194
|
+
chart_->px_ratio);
|
|
1102
1195
|
});
|
|
1103
1196
|
}
|
|
1104
1197
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Methods exposed to JS:
|
|
7
7
|
// setCandles(arrayBuffer) — copies packed VroomCandle[] into the core
|
|
8
8
|
// setSize(width, height, pxRatio)
|
|
9
|
-
// render() ->
|
|
9
|
+
// render() -> SkPicture (iOS) / SkImage (Android)
|
|
10
10
|
|
|
11
11
|
#pragma once
|
|
12
12
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// methods on the returned object:
|
|
6
6
|
// setCandles(arrayBuffer)
|
|
7
7
|
// setSize(w, h, pxRatio)
|
|
8
|
-
// render() ->
|
|
8
|
+
// render() -> SkPicture (iOS) / SkImage (Android)
|
|
9
9
|
//
|
|
10
10
|
// The host object's lifetime is managed by JS GC — when the React component
|
|
11
11
|
// drops its reference, the underlying VroomChart is destroyed.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
4
|
#include <cmath>
|
|
5
|
+
#include <cstddef>
|
|
5
6
|
#include <cstdio>
|
|
6
7
|
|
|
7
8
|
namespace vroom {
|
|
@@ -15,6 +16,38 @@ constexpr size_t kPlainCap = 340;
|
|
|
15
16
|
constexpr char kGroupSeparator = ',';
|
|
16
17
|
constexpr int kGroupSize = 3;
|
|
17
18
|
|
|
19
|
+
constexpr double kMillion = 1e6;
|
|
20
|
+
constexpr double kBillion = 1e9;
|
|
21
|
+
constexpr double kTrillion = 1e12;
|
|
22
|
+
|
|
23
|
+
double compact_divisor(CompactScale scale) {
|
|
24
|
+
switch (scale) {
|
|
25
|
+
case CompactScale::Million: return kMillion;
|
|
26
|
+
case CompactScale::Billion: return kBillion;
|
|
27
|
+
case CompactScale::Trillion: return kTrillion;
|
|
28
|
+
case CompactScale::None: return 1.0;
|
|
29
|
+
}
|
|
30
|
+
return 1.0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
char compact_suffix(CompactScale scale) {
|
|
34
|
+
switch (scale) {
|
|
35
|
+
case CompactScale::Million: return 'M';
|
|
36
|
+
case CompactScale::Billion: return 'B';
|
|
37
|
+
case CompactScale::Trillion: return 'T';
|
|
38
|
+
case CompactScale::None: return '\0';
|
|
39
|
+
}
|
|
40
|
+
return '\0';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
CompactScale compact_scale_for(double reference) {
|
|
44
|
+
const double r = std::fabs(reference);
|
|
45
|
+
if (!std::isfinite(r) || r < kMillion) return CompactScale::None;
|
|
46
|
+
if (r >= kTrillion) return CompactScale::Trillion;
|
|
47
|
+
if (r >= kBillion) return CompactScale::Billion;
|
|
48
|
+
return CompactScale::Million;
|
|
49
|
+
}
|
|
50
|
+
|
|
18
51
|
} // namespace
|
|
19
52
|
|
|
20
53
|
int significant_decimals(double reference) {
|
|
@@ -28,7 +61,8 @@ int significant_decimals(double reference) {
|
|
|
28
61
|
}
|
|
29
62
|
|
|
30
63
|
PriceFormat price_format_for(double reference) {
|
|
31
|
-
return PriceFormat{significant_decimals(reference), true
|
|
64
|
+
return PriceFormat{significant_decimals(reference), true,
|
|
65
|
+
compact_scale_for(reference)};
|
|
32
66
|
}
|
|
33
67
|
|
|
34
68
|
int price_decimals(double interval) {
|
|
@@ -40,8 +74,13 @@ int price_decimals(double interval) {
|
|
|
40
74
|
}
|
|
41
75
|
|
|
42
76
|
PriceFormat with_tick_guard(const PriceFormat& fmt, double interval) {
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
const double divisor = compact_divisor(fmt.compact);
|
|
78
|
+
const double display_interval =
|
|
79
|
+
(divisor != 1.0 && interval > 0.0 && std::isfinite(interval))
|
|
80
|
+
? interval / divisor
|
|
81
|
+
: interval;
|
|
82
|
+
return PriceFormat{std::max(fmt.decimals, price_decimals(display_interval)),
|
|
83
|
+
fmt.group, fmt.compact};
|
|
45
84
|
}
|
|
46
85
|
|
|
47
86
|
void format_price(char* buf, size_t buf_size, double price,
|
|
@@ -49,11 +88,23 @@ void format_price(char* buf, size_t buf_size, double price,
|
|
|
49
88
|
if (!buf || buf_size == 0) return;
|
|
50
89
|
const int decimals = std::clamp(fmt.decimals, 0, kPriceMaxDecimals);
|
|
51
90
|
|
|
91
|
+
const double divisor = compact_divisor(fmt.compact);
|
|
92
|
+
const double display = (divisor != 1.0) ? price / divisor : price;
|
|
93
|
+
|
|
52
94
|
char plain[kPlainCap];
|
|
53
|
-
if (std::snprintf(plain, sizeof(plain), "%.*f", decimals,
|
|
95
|
+
if (std::snprintf(plain, sizeof(plain), "%.*f", decimals, display) < 0) {
|
|
54
96
|
buf[0] = '\0';
|
|
55
97
|
return;
|
|
56
98
|
}
|
|
99
|
+
|
|
100
|
+
const char suffix = compact_suffix(fmt.compact);
|
|
101
|
+
if (suffix != '\0') {
|
|
102
|
+
// Compact form is already short; grouping would only fire if a tick
|
|
103
|
+
// wandered across a suffix boundary (1000.00M). Skip it.
|
|
104
|
+
std::snprintf(buf, buf_size, "%s%c", plain, suffix);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
57
108
|
if (!fmt.group) {
|
|
58
109
|
std::snprintf(buf, buf_size, "%s", plain);
|
|
59
110
|
return;
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
// 0.023397 -> 6 decimals 0.023397
|
|
14
14
|
// 80285.20 -> 2 decimals 80,285.20 (floor)
|
|
15
15
|
// 2513.92 -> 2 decimals 2,513.92 (floor)
|
|
16
|
+
// 1.59e6 -> compact 1.59M
|
|
17
|
+
// 3.219e7 -> compact 32.19M
|
|
18
|
+
// 3.454e10 -> compact 34.54B
|
|
19
|
+
// 1.2e12 -> compact 1.20T
|
|
20
|
+
//
|
|
21
|
+
// Past a million the integer digits eat the axis, so the format switches to a
|
|
22
|
+
// suffix (M / B / T) with two decimal places. The suffix is keyed off the
|
|
23
|
+
// asset's own price, not each tick, so every label on a market-cap chart
|
|
24
|
+
// shares a column and a zoomed-in 32.1M–32.2M range does not mix "32.15M"
|
|
25
|
+
// with "32,150,000.00".
|
|
16
26
|
//
|
|
17
27
|
// The reference is the asset's own price, not the visible range, so the
|
|
18
28
|
// precision holds steady while the user pans and zooms. A range-derived count
|
|
@@ -32,18 +42,31 @@ namespace vroom {
|
|
|
32
42
|
inline constexpr int kPriceSigDigits = 5;
|
|
33
43
|
|
|
34
44
|
// Prices at or above ~1 unit read as currency, where fewer than two decimals
|
|
35
|
-
// looks broken (a "$84,000" axis next to a "$80,285.20" last price).
|
|
45
|
+
// looks broken (a "$84,000" axis next to a "$80,285.20" last price). Compact
|
|
46
|
+
// suffixes use the same floor: "32.19M", not "32.2M".
|
|
36
47
|
inline constexpr int kPriceMinDecimals = 2;
|
|
37
48
|
|
|
38
49
|
// Past this the digits are noise, and %.*f starts printing the binary
|
|
39
50
|
// representation's tail rather than anything the feed meant.
|
|
40
51
|
inline constexpr int kPriceMaxDecimals = 12;
|
|
41
52
|
|
|
53
|
+
// Compact suffix for assets priced in the millions and up. Keyed off the
|
|
54
|
+
// reference, then applied to every label so the column stays uniform. Nothing
|
|
55
|
+
// below a million is abbreviated — no K.
|
|
56
|
+
enum class CompactScale {
|
|
57
|
+
None,
|
|
58
|
+
Million, // |ref| >= 1e6
|
|
59
|
+
Billion, // |ref| >= 1e9
|
|
60
|
+
Trillion, // |ref| >= 1e12
|
|
61
|
+
};
|
|
62
|
+
|
|
42
63
|
// How to render a price. `group` inserts thousands separators, which only ever
|
|
43
|
-
// affects the integer part.
|
|
64
|
+
// affects the integer part of a non-compact format. `compact` scales the value
|
|
65
|
+
// and appends M / B / T; grouping is skipped in that path.
|
|
44
66
|
struct PriceFormat {
|
|
45
|
-
int
|
|
46
|
-
bool
|
|
67
|
+
int decimals = kPriceMinDecimals;
|
|
68
|
+
bool group = true;
|
|
69
|
+
CompactScale compact = CompactScale::None;
|
|
47
70
|
};
|
|
48
71
|
|
|
49
72
|
// Decimals giving kPriceSigDigits significant digits for an asset priced around
|
|
@@ -51,7 +74,8 @@ struct PriceFormat {
|
|
|
51
74
|
// non-finite reference falls back to the floor.
|
|
52
75
|
int significant_decimals(double reference);
|
|
53
76
|
|
|
54
|
-
// The format for an asset priced around `reference
|
|
77
|
+
// The format for an asset priced around `reference`, including the compact
|
|
78
|
+
// suffix when |reference| is at least a million.
|
|
55
79
|
PriceFormat price_format_for(double reference);
|
|
56
80
|
|
|
57
81
|
// Decimal places needed so adjacent ticks `interval` apart don't collapse to
|
|
@@ -60,6 +84,9 @@ int price_decimals(double interval);
|
|
|
60
84
|
|
|
61
85
|
// `fmt` raised, if need be, so ticks `interval` apart stay distinct. The asset's
|
|
62
86
|
// precision is the floor; only a zoom deeper than that precision adds to it.
|
|
87
|
+
// `interval` is in the data's units; when `fmt` is compact the guard converts
|
|
88
|
+
// it into display units (interval / 1e6 for millions, etc.) so a 1,000-wide
|
|
89
|
+
// step on a 32M chart still gets enough decimals to print as 32.192M vs 32.193M.
|
|
63
90
|
PriceFormat with_tick_guard(const PriceFormat& fmt, double interval);
|
|
64
91
|
|
|
65
92
|
// Writes `price` into `buf` per `fmt`, truncating rather than overrunning.
|