react-native-vroom-chart 0.16.0 → 0.17.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 +81 -0
- package/cpp/_core_include/vroom/vroom_chart.h +41 -0
- package/cpp/_core_src/candles.cpp +15 -14
- package/cpp/_core_src/chart.cpp +297 -28
- package/cpp/_core_src/chart.h +71 -2
- package/cpp/_core_src/chart_facade.cpp +27 -0
- package/cpp/_core_src/color_lerp.h +28 -0
- package/cpp/_core_src/loading_line.cpp +183 -0
- package/cpp/_core_src/loading_line.h +38 -0
- package/cpp/_core_src/loading_wave.h +140 -0
- package/cpp/_core_src/ma_overlay.cpp +15 -10
- package/cpp/_core_src/ma_overlay.h +7 -0
- package/cpp/_core_src/price_indicator.cpp +21 -7
- package/cpp/_core_src/price_indicator.h +11 -1
- package/cpp/_core_src/price_indicator_anim.h +81 -0
- package/cpp/_core_src/theme.cpp +5 -0
- package/cpp/_core_src/tip_geometry.h +55 -0
- package/cpp/_core_src/viewport.h +33 -0
- package/lib/index.d.mts +30 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +71 -11
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +71 -11
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +12 -0
- package/src/jsi.d.ts +42 -0
- package/src/theme.ts +1 -0
- package/src/useChartCore.ts +119 -4
|
@@ -10,6 +10,7 @@ class SkCanvas;
|
|
|
10
10
|
struct VroomChart;
|
|
11
11
|
|
|
12
12
|
namespace vroom {
|
|
13
|
+
struct CandleSnapshot;
|
|
13
14
|
struct Layout;
|
|
14
15
|
struct PriceBounds;
|
|
15
16
|
} // namespace vroom
|
|
@@ -19,11 +20,20 @@ namespace vroom::price_indicator {
|
|
|
19
20
|
// Draws the line across [0, candle_right] at the latest close's y, plus the
|
|
20
21
|
// price box in the y-axis strip. `candle_area_h` is the y of the x-axis
|
|
21
22
|
// separator; the indicator is skipped if the close maps outside [0, candle_area_h].
|
|
23
|
+
//
|
|
24
|
+
// `morph_from` is the newest candle's outgoing capture (slot 0 of
|
|
25
|
+
// VroomChart::morph_from), or null to draw the settled close. When present the
|
|
26
|
+
// level eases across `morph_t` on the same clock as the candle it marks, so the
|
|
27
|
+
// badge tracks the bar instead of jumping ahead of it — see
|
|
28
|
+
// price_indicator_anim.h. Callers must only pass a capture that pairs with the
|
|
29
|
+
// newest candle (tip_anchor.h).
|
|
22
30
|
void draw(SkCanvas* canvas,
|
|
23
31
|
const VroomChart& chart,
|
|
24
32
|
const Layout& lay,
|
|
25
33
|
const PriceBounds& bounds,
|
|
26
34
|
float candle_right,
|
|
27
|
-
float candle_area_h
|
|
35
|
+
float candle_area_h,
|
|
36
|
+
const CandleSnapshot* morph_from = nullptr,
|
|
37
|
+
float morph_t = 1.f);
|
|
28
38
|
|
|
29
39
|
} // namespace vroom::price_indicator
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Where the current-price indicator sits mid-morph (see price_indicator.cpp).
|
|
2
|
+
//
|
|
3
|
+
// The indicator marks the latest close, so every tick moves it. Drawing it
|
|
4
|
+
// straight from that close makes it jump while the candle it belongs to eases
|
|
5
|
+
// into place, which reads as the badge coming loose from the chart. Instead it
|
|
6
|
+
// rides the same capture the candles do: morph_from[0] is the newest candle's
|
|
7
|
+
// outgoing geometry and interval_morph_t is the host's eased clock, so the
|
|
8
|
+
// indicator lands on the candle's close edge on every frame rather than only at
|
|
9
|
+
// the two ends.
|
|
10
|
+
//
|
|
11
|
+
// The y is deliberately the same expression close_vertex uses for slot 0
|
|
12
|
+
// (ma_overlay.cpp). Matching it by construction, rather than by giving the two
|
|
13
|
+
// the same duration, is what keeps the badge glued to the line chart's tip.
|
|
14
|
+
//
|
|
15
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
16
|
+
// tests/test_price_indicator_anim.cpp.
|
|
17
|
+
|
|
18
|
+
#pragma once
|
|
19
|
+
|
|
20
|
+
#include <algorithm>
|
|
21
|
+
|
|
22
|
+
#include "viewport.h"
|
|
23
|
+
|
|
24
|
+
namespace vroom::price_indicator_anim {
|
|
25
|
+
|
|
26
|
+
// One frame of the indicator.
|
|
27
|
+
struct Level {
|
|
28
|
+
float y; // pixels, on the candle's close edge
|
|
29
|
+
double price; // what the badge should read
|
|
30
|
+
float bull_t; // 0 = the captured direction's color, 1 = the new one
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
namespace detail {
|
|
34
|
+
inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
35
|
+
|
|
36
|
+
// The price a captured band fraction stood for. The capture stores fractions so
|
|
37
|
+
// it survives a resize or a rescale, so recovering the price it came from needs
|
|
38
|
+
// the band it was measured against — which is why morph_from_bounds is kept.
|
|
39
|
+
inline double price_at_fraction(const PriceBounds& b, double frac) {
|
|
40
|
+
return b.min + frac * (b.max - b.min);
|
|
41
|
+
}
|
|
42
|
+
} // namespace detail
|
|
43
|
+
|
|
44
|
+
// `from` is the newest candle's capture, or null when nothing is morphing or
|
|
45
|
+
// the capture doesn't pair with the newest candle (panned into history — see
|
|
46
|
+
// tip_anchor.h, which the caller shares the pairing rule with). A null capture
|
|
47
|
+
// gives the settled values, which is what the indicator drew before it
|
|
48
|
+
// animated at all.
|
|
49
|
+
//
|
|
50
|
+
// `from_bounds` is the price band `from` was captured against
|
|
51
|
+
// (VroomChart::morph_from_bounds).
|
|
52
|
+
inline Level level_at(const Layout& lay,
|
|
53
|
+
const PriceBounds& bounds,
|
|
54
|
+
double close_new,
|
|
55
|
+
bool bull_new,
|
|
56
|
+
const CandleSnapshot* from,
|
|
57
|
+
const PriceBounds& from_bounds,
|
|
58
|
+
float morph_t) {
|
|
59
|
+
const float to_y = vroom::price_to_y(lay, bounds, close_new);
|
|
60
|
+
if (!from) return Level{to_y, close_new, 1.f};
|
|
61
|
+
|
|
62
|
+
const float t = std::clamp(morph_t, 0.f, 1.f);
|
|
63
|
+
|
|
64
|
+
// Two different spaces on purpose. The y interpolates in band fractions,
|
|
65
|
+
// because that is the space the capture is in and the space the candles
|
|
66
|
+
// move through — anything else would start the indicator off the pixel the
|
|
67
|
+
// close occupied last frame. The price interpolates in price space, so both
|
|
68
|
+
// ends read exactly the close they belong to even when this tick set a new
|
|
69
|
+
// extreme and rescaled the band underneath. The two agree whenever the band
|
|
70
|
+
// holds still, which is the overwhelmingly common case: y_at_fraction is
|
|
71
|
+
// affine, so a fraction lerp and a price lerp are then the same function.
|
|
72
|
+
const float y =
|
|
73
|
+
detail::lerp(vroom::y_at_fraction(lay, from->close), to_y, t);
|
|
74
|
+
const double close_old =
|
|
75
|
+
detail::price_at_fraction(from_bounds, from->close);
|
|
76
|
+
|
|
77
|
+
return Level{y, close_old + (close_new - close_old) * static_cast<double>(t),
|
|
78
|
+
from->bull == bull_new ? 1.f : t};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} // namespace vroom::price_indicator_anim
|
package/cpp/_core_src/theme.cpp
CHANGED
|
@@ -23,6 +23,11 @@ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
|
|
|
23
23
|
0xff26a69a, // ACCENT_BULL — classic teal-green (price indicator, volume, MACD)
|
|
24
24
|
0xffef5350, // ACCENT_BEAR — classic red
|
|
25
25
|
0xff8957e5, // LINE — line-chart close polyline; violet, matching the RSI line
|
|
26
|
+
// SKELETON — transparent sentinel: inherit GRID, the way BORDER_BULL and
|
|
27
|
+
// the wick colors inherit their fills. The gridlines are already the
|
|
28
|
+
// chart's tone for structure rather than data, which is what the loading
|
|
29
|
+
// line is, so matching them keeps it from being read as a series.
|
|
30
|
+
0x00000000,
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
constexpr float kDefaultFloats[VROOM_FLOAT_COUNT_] = {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// How big the line chart's tip marker is, and how much gutter it needs.
|
|
2
|
+
//
|
|
3
|
+
// The dot marks the newest close, which on a view pinned to the latest bar sits
|
|
4
|
+
// half a slot from the right edge of the plot — a few pixels. The dot's own
|
|
5
|
+
// radius is larger than that, so it can only be drawn in full if it is allowed
|
|
6
|
+
// to spill into the gutter between the plot and the y-axis strip. That makes the
|
|
7
|
+
// marker's size a layout input, not just a paint detail, so the renderer
|
|
8
|
+
// (draw_close_tip in ma_overlay.cpp) and the layout (VroomChart::layout) read it
|
|
9
|
+
// from here rather than each carrying its own copy.
|
|
10
|
+
//
|
|
11
|
+
// The pulse ring is deliberately not part of this. At its widest it is 3.5x the
|
|
12
|
+
// border radius, and reserving that much gutter would eat the plot; it is
|
|
13
|
+
// allowed to clip against the axis.
|
|
14
|
+
//
|
|
15
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
16
|
+
// tests/test_tip_geometry.cpp.
|
|
17
|
+
|
|
18
|
+
#pragma once
|
|
19
|
+
|
|
20
|
+
#include <algorithm>
|
|
21
|
+
|
|
22
|
+
namespace vroom::tip_geometry {
|
|
23
|
+
|
|
24
|
+
// Width of the background-colored ring that separates the tip dot from the line
|
|
25
|
+
// and from the pulse expanding out behind it.
|
|
26
|
+
constexpr float kBorderPx = 2.f;
|
|
27
|
+
|
|
28
|
+
// Gap left between the dot's outer edge and the y-axis strip. The dot touching
|
|
29
|
+
// the price labels reads as a rendering fault even when nothing is clipped.
|
|
30
|
+
constexpr float kClearPx = 4.f;
|
|
31
|
+
|
|
32
|
+
struct Geometry {
|
|
33
|
+
float dot_r; // the filled dot in the line's color
|
|
34
|
+
float border_r; // the dot plus its background-colored halo
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Scaling off the stroke keeps the marker proportionate at any line width; the
|
|
38
|
+
// floor stops a hairline chart from getting an invisible dot.
|
|
39
|
+
inline Geometry of(float line_width) {
|
|
40
|
+
const float w = line_width > 0.f ? line_width : 1.5f;
|
|
41
|
+
const float dot_r = std::max(2.f, w * 1.5f);
|
|
42
|
+
return Geometry{dot_r, dot_r + kBorderPx};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Gutter width that lets the dot draw in full with `kClearPx` to spare.
|
|
46
|
+
//
|
|
47
|
+
// draw_close_tip drops the marker once its center passes the plot's right edge,
|
|
48
|
+
// so the center is at worst flush with that edge and the dot overhangs it by
|
|
49
|
+
// exactly `border_r`. A gutter of that plus the clearance therefore makes "the
|
|
50
|
+
// dot never touches the axis strip" true by construction, whatever the window.
|
|
51
|
+
inline float gutter_px(float line_width) {
|
|
52
|
+
return of(line_width).border_r + kClearPx;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} // namespace vroom::tip_geometry
|
package/cpp/_core_src/viewport.h
CHANGED
|
@@ -92,6 +92,30 @@ struct CandleSnapshot {
|
|
|
92
92
|
float x;
|
|
93
93
|
float open, high, low, close;
|
|
94
94
|
bool bull; // close >= open; selects the fill / wick / border color
|
|
95
|
+
// Set when the capture came from the loading skeleton. Such a slot starts
|
|
96
|
+
// out grey and semi-transparent rather than in its own bull/bear color, so
|
|
97
|
+
// the draw path has to blend from `skeleton_alpha`-scaled grey instead —
|
|
98
|
+
// that color blend is what makes the hand-off read as the placeholder
|
|
99
|
+
// *becoming* the data (see candles::draw).
|
|
100
|
+
bool skeleton = false;
|
|
101
|
+
// The wave's alpha for this bar at capture time. Carried so the morph's
|
|
102
|
+
// first frame matches the skeleton frame it replaced; without it every bar
|
|
103
|
+
// would jump to full opacity the instant data landed.
|
|
104
|
+
float skeleton_alpha = 1.f;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// One vertex of the loading line's morph, holding both ends of the animation
|
|
108
|
+
// so a frame is a plain lerp between them.
|
|
109
|
+
//
|
|
110
|
+
// The two y's are in different spaces on purpose. The sine knows nothing about
|
|
111
|
+
// prices, so it's a fraction of the pane; the candle centre is a fraction of
|
|
112
|
+
// the price band, the same space CandleSnapshot uses — which is what keeps the
|
|
113
|
+
// line sitting exactly where the bars emerge from, since both resolve through
|
|
114
|
+
// y_at_fraction.
|
|
115
|
+
struct LinePoint {
|
|
116
|
+
float x; // fraction of the candle-area width
|
|
117
|
+
float from_y; // fraction of the pane height — the sine, frozen at capture
|
|
118
|
+
float to_y; // fraction of the price band — the candle's vertical centre
|
|
95
119
|
};
|
|
96
120
|
|
|
97
121
|
// How many captured slots still contribute to a frame — 0 once the morph is
|
|
@@ -140,6 +164,15 @@ inline void blend_candle_snapshots(CandleSnapshot* dst, std::size_t dst_n,
|
|
|
140
164
|
// `bull` stays the fresh one: the draw path colors a paired slot from
|
|
141
165
|
// the live candle and only reads the capture's flag for a slot the next
|
|
142
166
|
// update drops, where the newer direction is the better answer.
|
|
167
|
+
//
|
|
168
|
+
// The skeleton state, by contrast, carries over from the interrupted
|
|
169
|
+
// capture: a tick landing mid-hand-off must not abandon the grey blend
|
|
170
|
+
// partway, or the bar would snap to full color while its geometry is
|
|
171
|
+
// still moving. Its alpha follows the same lerp as the geometry.
|
|
172
|
+
if (from.skeleton) {
|
|
173
|
+
to.skeleton = true;
|
|
174
|
+
to.skeleton_alpha = mix(from.skeleton_alpha, 1.f);
|
|
175
|
+
}
|
|
143
176
|
}
|
|
144
177
|
}
|
|
145
178
|
|
package/lib/index.d.mts
CHANGED
|
@@ -106,6 +106,19 @@ type VroomTheme = {
|
|
|
106
106
|
crosshairTarget?: VroomColor;
|
|
107
107
|
/** Line-chart-mode close polyline color. Defaults to violet, matching the RSI line. */
|
|
108
108
|
lineColor?: VroomColor;
|
|
109
|
+
/**
|
|
110
|
+
* The line drawn across the plot while loading (see the `loading` prop).
|
|
111
|
+
* Defaults to inheriting `grid`: the gridlines are already the chart's tone
|
|
112
|
+
* for structure rather than data, which is what the line is, so matching
|
|
113
|
+
* them keeps it from being read as a series.
|
|
114
|
+
*
|
|
115
|
+
* The line breathes between roughly 60% and 100% of whatever color it ends
|
|
116
|
+
* up with — a pulse, not a dimmer, so a recessive color stays legible. Any
|
|
117
|
+
* alpha given here multiplies into that, so an opaque color is the usual
|
|
118
|
+
* choice. Pass `lineColor` to make the line read as the chart's own series
|
|
119
|
+
* warming up instead.
|
|
120
|
+
*/
|
|
121
|
+
skeleton?: VroomColor;
|
|
109
122
|
/** Line-chart-mode polyline stroke width in px. Defaults to 1.5. */
|
|
110
123
|
lineWidth?: number;
|
|
111
124
|
/**
|
|
@@ -1059,6 +1072,23 @@ type ATRConfig = {
|
|
|
1059
1072
|
type VroomChartCoreProps = {
|
|
1060
1073
|
/** OHLCV bars to render. The only required prop. */
|
|
1061
1074
|
candles: Candle[];
|
|
1075
|
+
/**
|
|
1076
|
+
* Whether the series is still being fetched. While this is true *and*
|
|
1077
|
+
* `candles` is empty, the chart draws a single grey line across the plot,
|
|
1078
|
+
* drifting in a slow sine wave, in place of the scene. Gestures, the
|
|
1079
|
+
* crosshair, the price badge, the axis labels and any indicator panes are all
|
|
1080
|
+
* suppressed for the duration.
|
|
1081
|
+
*
|
|
1082
|
+
* When the data arrives the line doesn't cut away — it becomes the chart, in
|
|
1083
|
+
* two steps that split `transitionMs`: it reshapes to pass through the
|
|
1084
|
+
* vertical centre of every candle about to be drawn, then fades out while
|
|
1085
|
+
* those candles grow outward from it and their color fades up.
|
|
1086
|
+
*
|
|
1087
|
+
* Passing `candles` alongside `loading` leaves the real chart up, so a
|
|
1088
|
+
* background refresh of an already-loaded series won't blank out. Default
|
|
1089
|
+
* false.
|
|
1090
|
+
*/
|
|
1091
|
+
loading?: boolean;
|
|
1062
1092
|
/**
|
|
1063
1093
|
* Identity of the data series (e.g. "BTC-USD"). When it changes between
|
|
1064
1094
|
* renders the chart resets to the default view (most recent candles + price
|
package/lib/index.d.ts
CHANGED
|
@@ -106,6 +106,19 @@ type VroomTheme = {
|
|
|
106
106
|
crosshairTarget?: VroomColor;
|
|
107
107
|
/** Line-chart-mode close polyline color. Defaults to violet, matching the RSI line. */
|
|
108
108
|
lineColor?: VroomColor;
|
|
109
|
+
/**
|
|
110
|
+
* The line drawn across the plot while loading (see the `loading` prop).
|
|
111
|
+
* Defaults to inheriting `grid`: the gridlines are already the chart's tone
|
|
112
|
+
* for structure rather than data, which is what the line is, so matching
|
|
113
|
+
* them keeps it from being read as a series.
|
|
114
|
+
*
|
|
115
|
+
* The line breathes between roughly 60% and 100% of whatever color it ends
|
|
116
|
+
* up with — a pulse, not a dimmer, so a recessive color stays legible. Any
|
|
117
|
+
* alpha given here multiplies into that, so an opaque color is the usual
|
|
118
|
+
* choice. Pass `lineColor` to make the line read as the chart's own series
|
|
119
|
+
* warming up instead.
|
|
120
|
+
*/
|
|
121
|
+
skeleton?: VroomColor;
|
|
109
122
|
/** Line-chart-mode polyline stroke width in px. Defaults to 1.5. */
|
|
110
123
|
lineWidth?: number;
|
|
111
124
|
/**
|
|
@@ -1059,6 +1072,23 @@ type ATRConfig = {
|
|
|
1059
1072
|
type VroomChartCoreProps = {
|
|
1060
1073
|
/** OHLCV bars to render. The only required prop. */
|
|
1061
1074
|
candles: Candle[];
|
|
1075
|
+
/**
|
|
1076
|
+
* Whether the series is still being fetched. While this is true *and*
|
|
1077
|
+
* `candles` is empty, the chart draws a single grey line across the plot,
|
|
1078
|
+
* drifting in a slow sine wave, in place of the scene. Gestures, the
|
|
1079
|
+
* crosshair, the price badge, the axis labels and any indicator panes are all
|
|
1080
|
+
* suppressed for the duration.
|
|
1081
|
+
*
|
|
1082
|
+
* When the data arrives the line doesn't cut away — it becomes the chart, in
|
|
1083
|
+
* two steps that split `transitionMs`: it reshapes to pass through the
|
|
1084
|
+
* vertical centre of every candle about to be drawn, then fades out while
|
|
1085
|
+
* those candles grow outward from it and their color fades up.
|
|
1086
|
+
*
|
|
1087
|
+
* Passing `candles` alongside `loading` leaves the real chart up, so a
|
|
1088
|
+
* background refresh of an already-loaded series won't blank out. Default
|
|
1089
|
+
* false.
|
|
1090
|
+
*/
|
|
1091
|
+
loading?: boolean;
|
|
1062
1092
|
/**
|
|
1063
1093
|
* Identity of the data series (e.g. "BTC-USD"). When it changes between
|
|
1064
1094
|
* renders the chart resets to the default view (most recent candles + price
|
package/lib/index.js
CHANGED
|
@@ -186,8 +186,10 @@ var COLOR_KEYS = {
|
|
|
186
186
|
// VROOM_COLOR_ACCENT_BULL
|
|
187
187
|
accentBear: 15,
|
|
188
188
|
// VROOM_COLOR_ACCENT_BEAR
|
|
189
|
-
lineColor: 16
|
|
189
|
+
lineColor: 16,
|
|
190
190
|
// VROOM_COLOR_LINE
|
|
191
|
+
skeleton: 17
|
|
192
|
+
// VROOM_COLOR_SKELETON
|
|
191
193
|
};
|
|
192
194
|
var FLOAT_KEYS = {
|
|
193
195
|
wickWidth: 1,
|
|
@@ -487,7 +489,7 @@ function ensureInstalled() {
|
|
|
487
489
|
}
|
|
488
490
|
installed = true;
|
|
489
491
|
}
|
|
490
|
-
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition) {
|
|
492
|
+
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition, loading) {
|
|
491
493
|
const handleRef = (0, import_react.useRef)(null);
|
|
492
494
|
const defaultWidthAppliedRef = (0, import_react.useRef)(false);
|
|
493
495
|
const volumeCollapseRef = (0, import_react.useRef)(null);
|
|
@@ -522,18 +524,26 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
522
524
|
const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
|
|
523
525
|
onFrameRef.current = transition?.onFrame;
|
|
524
526
|
const seriesKey = transition?.seriesKey;
|
|
527
|
+
const loadingHandoffRef = (0, import_react.useRef)(false);
|
|
525
528
|
const endIntervalMorph = (0, import_react.useCallback)(() => {
|
|
526
529
|
if (intervalMorphRaf.current != null) {
|
|
527
530
|
cancelAnimationFrame(intervalMorphRaf.current);
|
|
528
531
|
intervalMorphRaf.current = null;
|
|
529
532
|
}
|
|
530
|
-
handleRef.current
|
|
533
|
+
const h = handleRef.current;
|
|
534
|
+
if (loadingHandoffRef.current) {
|
|
535
|
+
loadingHandoffRef.current = false;
|
|
536
|
+
h?.setLoadingMorph(1);
|
|
537
|
+
h?.beginLoadingReveal();
|
|
538
|
+
}
|
|
539
|
+
h?.setIntervalMorph(1);
|
|
531
540
|
}, []);
|
|
532
|
-
const startIntervalMorph = (0, import_react.useCallback)((h) => {
|
|
541
|
+
const startIntervalMorph = (0, import_react.useCallback)((h, durationMs) => {
|
|
533
542
|
const { ms, easing } = animRef.current;
|
|
543
|
+
const dur = durationMs ?? ms;
|
|
534
544
|
const start = performance.now();
|
|
535
545
|
const step = (now) => {
|
|
536
|
-
const p = Math.min(1, (now - start) /
|
|
546
|
+
const p = Math.min(1, (now - start) / dur);
|
|
537
547
|
h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
|
|
538
548
|
const pic = h.render();
|
|
539
549
|
if (pic) onFrameRef.current?.(pic);
|
|
@@ -541,6 +551,31 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
541
551
|
};
|
|
542
552
|
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
543
553
|
}, []);
|
|
554
|
+
const startLoadingHandoff = (0, import_react.useCallback)(
|
|
555
|
+
(h) => {
|
|
556
|
+
const { ms, easing } = animRef.current;
|
|
557
|
+
const half = ms / 2;
|
|
558
|
+
loadingHandoffRef.current = true;
|
|
559
|
+
h.beginLoadingMorph();
|
|
560
|
+
const start = performance.now();
|
|
561
|
+
const step = (now) => {
|
|
562
|
+
const p = Math.min(1, (now - start) / half);
|
|
563
|
+
h.setLoadingMorph(p < 1 ? ease(easing, p) : 1);
|
|
564
|
+
const pic = h.render();
|
|
565
|
+
if (pic) onFrameRef.current?.(pic);
|
|
566
|
+
if (p < 1) {
|
|
567
|
+
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
intervalMorphRaf.current = null;
|
|
571
|
+
loadingHandoffRef.current = false;
|
|
572
|
+
h.beginLoadingReveal();
|
|
573
|
+
startIntervalMorph(h, half);
|
|
574
|
+
};
|
|
575
|
+
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
576
|
+
},
|
|
577
|
+
[startIntervalMorph]
|
|
578
|
+
);
|
|
544
579
|
const settleStream = (0, import_react.useCallback)((keepMorph = false) => {
|
|
545
580
|
if (streamRaf.current != null) {
|
|
546
581
|
cancelAnimationFrame(streamRaf.current);
|
|
@@ -610,6 +645,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
610
645
|
const explicit = visibleRange != null;
|
|
611
646
|
const startMs = visibleRange?.startMs ?? 0;
|
|
612
647
|
const endMs = visibleRange?.endMs ?? 0;
|
|
648
|
+
const showLoadingLine = loading === true && candles.length === 0;
|
|
649
|
+
const lineUpRef = (0, import_react.useRef)(false);
|
|
613
650
|
const themeKey = theme ? JSON.stringify(theme) : "";
|
|
614
651
|
const rsiKey = rsi ? JSON.stringify(rsi) : "";
|
|
615
652
|
const macdKey = macd ? JSON.stringify(macd) : "";
|
|
@@ -632,6 +669,19 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
632
669
|
defaultWidthAppliedRef.current = true;
|
|
633
670
|
}
|
|
634
671
|
let morphing = false;
|
|
672
|
+
if (showLoadingLine) {
|
|
673
|
+
if (!lineUpRef.current) {
|
|
674
|
+
endIntervalMorph();
|
|
675
|
+
settleStream();
|
|
676
|
+
h.setCandles(packCandles([]));
|
|
677
|
+
prevDataRef.current = null;
|
|
678
|
+
}
|
|
679
|
+
h.setLoading(true, !animRef.current.reduceMotion);
|
|
680
|
+
lineUpRef.current = true;
|
|
681
|
+
} else if (lineUpRef.current && candles.length === 0) {
|
|
682
|
+
h.setLoading(false, true);
|
|
683
|
+
lineUpRef.current = false;
|
|
684
|
+
}
|
|
635
685
|
if (candles.length > 0) {
|
|
636
686
|
const prev = prevDataRef.current;
|
|
637
687
|
const freshHandle = prev == null || prev.handle !== h;
|
|
@@ -639,6 +689,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
639
689
|
const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
|
|
640
690
|
let tfArgs = null;
|
|
641
691
|
let prevEnvelope = null;
|
|
692
|
+
let handOff = false;
|
|
642
693
|
let stream = null;
|
|
643
694
|
if (transitionKind === "stream" && prev != null && !explicit) {
|
|
644
695
|
const { stream: mode, streamMs, reduceMotion } = animRef.current;
|
|
@@ -688,6 +739,11 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
688
739
|
} else if (transitionKind === "initial" || transitionKind === "reset") {
|
|
689
740
|
endIntervalMorph();
|
|
690
741
|
}
|
|
742
|
+
if (lineUpRef.current) {
|
|
743
|
+
lineUpRef.current = false;
|
|
744
|
+
handOff = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
|
|
745
|
+
if (!handOff) h.setLoading(false, true);
|
|
746
|
+
}
|
|
691
747
|
h.setCandles(packCandles(candles));
|
|
692
748
|
if (transitionKind === "timeframe") {
|
|
693
749
|
const newStepMs = inferStepMs(candles);
|
|
@@ -709,6 +765,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
709
765
|
} else if (transitionKind === "reset") {
|
|
710
766
|
h.resetView();
|
|
711
767
|
}
|
|
768
|
+
if (handOff) startLoadingHandoff(h);
|
|
712
769
|
prevDataRef.current = { handle: h, candles, seriesKey };
|
|
713
770
|
}
|
|
714
771
|
}
|
|
@@ -738,7 +795,7 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
738
795
|
footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
|
|
739
796
|
);
|
|
740
797
|
if (!morphing) setPicture(h.render());
|
|
741
|
-
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph, startStreamAnim, settleStream]);
|
|
798
|
+
}, [candles, showLoadingLine, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, startLoadingHandoff, endIntervalMorph, startStreamAnim, settleStream]);
|
|
742
799
|
return { handle: handleRef.current, picture, volumeCollapseRef };
|
|
743
800
|
}
|
|
744
801
|
|
|
@@ -750,6 +807,7 @@ function isSkImage(frame) {
|
|
|
750
807
|
function VroomChart(props) {
|
|
751
808
|
const {
|
|
752
809
|
candles,
|
|
810
|
+
loading,
|
|
753
811
|
seriesKey,
|
|
754
812
|
width: widthProp,
|
|
755
813
|
height: heightProp,
|
|
@@ -867,8 +925,10 @@ function VroomChart(props) {
|
|
|
867
925
|
streamTransitionMs,
|
|
868
926
|
reduceMotion,
|
|
869
927
|
onFrame
|
|
870
|
-
}
|
|
928
|
+
},
|
|
929
|
+
loading
|
|
871
930
|
);
|
|
931
|
+
const showLoadingLine = loading === true && candles.length === 0;
|
|
872
932
|
const crosshairActive = (0, import_react2.useRef)(false);
|
|
873
933
|
const footprintActive = (0, import_react2.useRef)(false);
|
|
874
934
|
const lastCrosshairTime = (0, import_react2.useRef)(null);
|
|
@@ -1148,7 +1208,7 @@ function VroomChart(props) {
|
|
|
1148
1208
|
pane: null
|
|
1149
1209
|
});
|
|
1150
1210
|
};
|
|
1151
|
-
const pan = import_react_native_gesture_handler.Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
|
|
1211
|
+
const pan = import_react_native_gesture_handler.Gesture.Pan().enabled(!showLoadingLine).runOnJS(true).maxPointers(1).onStart((e) => {
|
|
1152
1212
|
cancelDecay();
|
|
1153
1213
|
panMode.current = hitAxis(e.x, e.y);
|
|
1154
1214
|
priceDrag.current = null;
|
|
@@ -1243,7 +1303,7 @@ function VroomChart(props) {
|
|
|
1243
1303
|
enableX: false,
|
|
1244
1304
|
enableY: false
|
|
1245
1305
|
});
|
|
1246
|
-
const pinch = import_react_native_gesture_handler.Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
|
|
1306
|
+
const pinch = import_react_native_gesture_handler.Gesture.Pinch().enabled(!showLoadingLine).runOnJS(true).onTouchesDown((e) => {
|
|
1247
1307
|
if (e.numberOfTouches < 2) return;
|
|
1248
1308
|
dismissFootprint();
|
|
1249
1309
|
const [a, b] = e.allTouches;
|
|
@@ -1281,7 +1341,7 @@ function VroomChart(props) {
|
|
|
1281
1341
|
if (next) applyFrame(next);
|
|
1282
1342
|
maybeStartAnim();
|
|
1283
1343
|
});
|
|
1284
|
-
const longPress = import_react_native_gesture_handler.Gesture.LongPress().runOnJS(true).onStart((e) => {
|
|
1344
|
+
const longPress = import_react_native_gesture_handler.Gesture.LongPress().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
|
|
1285
1345
|
if (!handle) return;
|
|
1286
1346
|
if (hitAxis(e.x, e.y) !== "chart") return;
|
|
1287
1347
|
if (hitPriceLine(e.x, e.y)) return;
|
|
@@ -1301,7 +1361,7 @@ function VroomChart(props) {
|
|
|
1301
1361
|
reason: "show"
|
|
1302
1362
|
});
|
|
1303
1363
|
});
|
|
1304
|
-
const tap = import_react_native_gesture_handler.Gesture.Tap().runOnJS(true).onStart((e) => {
|
|
1364
|
+
const tap = import_react_native_gesture_handler.Gesture.Tap().enabled(!showLoadingLine).runOnJS(true).onStart((e) => {
|
|
1305
1365
|
if (!handle) return;
|
|
1306
1366
|
const prints = footprints ?? [];
|
|
1307
1367
|
const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
|