react-native-vroom-chart 0.7.0 → 0.9.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 +131 -1
- package/cpp/_core_include/vroom/vroom_chart.h +54 -14
- package/cpp/_core_src/chart.cpp +36 -2
- package/cpp/_core_src/chart.h +29 -12
- package/cpp/_core_src/chart_facade.cpp +72 -10
- package/cpp/_core_src/curve.h +67 -0
- package/cpp/_core_src/drawings.cpp +147 -4
- package/cpp/_core_src/drawings.h +10 -5
- package/cpp/_core_src/ma_overlay.cpp +200 -34
- package/cpp/_core_src/ma_overlay.h +41 -4
- package/cpp/_core_src/theme.cpp +3 -0
- package/cpp/_core_src/tip_pulse.h +74 -0
- package/lib/index.d.mts +73 -4
- package/lib/index.d.ts +73 -4
- package/lib/index.js +211 -46
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +216 -54
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/VroomChart.tsx +46 -23
- package/src/dataTransitions.ts +148 -0
- package/src/index.ts +6 -0
- package/src/jsi.d.ts +51 -0
- package/src/theme.ts +7 -0
- package/src/useChartCore.ts +179 -5
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The pulsing ring at the line chart's tip — a phase in, a radius and two
|
|
2
|
+
// alphas out (VROOM_FLOAT_LINE_TIP_PULSE).
|
|
3
|
+
//
|
|
4
|
+
// Shape and timing follow TradingView's last-price animation, whose numbers are
|
|
5
|
+
// well-tuned: expand while the fill washes out and the edge sharpens, keep
|
|
6
|
+
// expanding while the edge fades, then rest. That rest is nearly half the period
|
|
7
|
+
// and it is what makes the ring read as a heartbeat instead of a strobe.
|
|
8
|
+
//
|
|
9
|
+
// Radii come out as multiples of the ring's start radius rather than pixels,
|
|
10
|
+
// because the tip dot scales with the line width and the ring has to scale with
|
|
11
|
+
// it (see draw_close_tip in ma_overlay.cpp).
|
|
12
|
+
//
|
|
13
|
+
// Skia-free and header-only so the unit tests can cover it; see
|
|
14
|
+
// tests/test_tip_pulse.cpp.
|
|
15
|
+
|
|
16
|
+
#pragma once
|
|
17
|
+
|
|
18
|
+
#include <cmath>
|
|
19
|
+
|
|
20
|
+
namespace vroom::tip_pulse {
|
|
21
|
+
|
|
22
|
+
// One full expand-and-rest cycle.
|
|
23
|
+
constexpr float kPeriodSeconds = 2.6f;
|
|
24
|
+
|
|
25
|
+
// The ring at one instant. Alphas already account for the rest stage, so a
|
|
26
|
+
// caller can paint unconditionally and simply draw nothing visible.
|
|
27
|
+
struct Frame {
|
|
28
|
+
float radius_mul; // multiple of the ring's start radius
|
|
29
|
+
float fill_alpha;
|
|
30
|
+
float stroke_alpha;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
namespace detail {
|
|
34
|
+
|
|
35
|
+
struct Stage {
|
|
36
|
+
float end; // phase this stage runs until
|
|
37
|
+
float start_radius, end_radius;
|
|
38
|
+
float start_fill, end_fill;
|
|
39
|
+
float start_stroke, end_stroke;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// The fill fades as the ring grows, while the edge first *gains* alpha — that
|
|
43
|
+
// crossover is what gives the bloom its snap — before fading out in stage two.
|
|
44
|
+
constexpr Stage kStages[] = {
|
|
45
|
+
{0.25f, 1.0f, 2.5f, 0.25f, 0.f, 0.40f, 0.80f},
|
|
46
|
+
{0.525f, 2.5f, 3.5f, 0.f, 0.f, 0.80f, 0.f},
|
|
47
|
+
{1.0f, 3.5f, 3.5f, 0.f, 0.f, 0.f, 0.f},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
51
|
+
|
|
52
|
+
} // namespace detail
|
|
53
|
+
|
|
54
|
+
// Ring state at `phase`, in cycles. Values outside [0,1) wrap, so a caller can
|
|
55
|
+
// hand over raw elapsed time divided by the period without normalizing.
|
|
56
|
+
inline Frame at(float phase) {
|
|
57
|
+
const float p = phase - std::floor(phase);
|
|
58
|
+
float start = 0.f;
|
|
59
|
+
for (const detail::Stage& s : detail::kStages) {
|
|
60
|
+
// The last stage takes anything left over, which also catches a `p` that
|
|
61
|
+
// rounded up to 1 on the way in.
|
|
62
|
+
if (p < s.end || s.end >= 1.f) {
|
|
63
|
+
const float span = s.end - start;
|
|
64
|
+
const float t = span > 0.f ? (p - start) / span : 0.f;
|
|
65
|
+
return Frame{detail::lerp(s.start_radius, s.end_radius, t),
|
|
66
|
+
detail::lerp(s.start_fill, s.end_fill, t),
|
|
67
|
+
detail::lerp(s.start_stroke, s.end_stroke, t)};
|
|
68
|
+
}
|
|
69
|
+
start = s.end;
|
|
70
|
+
}
|
|
71
|
+
return Frame{1.f, 0.f, 0.f}; // unreachable: the table ends at 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
} // namespace vroom::tip_pulse
|
package/lib/index.d.mts
CHANGED
|
@@ -107,6 +107,33 @@ type VroomTheme = {
|
|
|
107
107
|
* the bottom of the price pane. Defaults to 0.28; set to 0 to disable the fill.
|
|
108
108
|
*/
|
|
109
109
|
lineGradientOpacity?: number;
|
|
110
|
+
/**
|
|
111
|
+
* How much to round the line chart's corners, from 0 (straight segments
|
|
112
|
+
* between closes) to 1 (fully smooth). Defaults to 0.
|
|
113
|
+
*
|
|
114
|
+
* The curve is monotone-limited, so smoothing can never overshoot into a price
|
|
115
|
+
* that didn't trade: every peak and trough stays on an actual close, and the
|
|
116
|
+
* curve never leaves the range of the two closes it connects. Applies to the
|
|
117
|
+
* gradient fill beneath the line as well, so the two stay flush.
|
|
118
|
+
*/
|
|
119
|
+
lineTension?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Mark the line chart's newest end with a dot. Defaults to `true`.
|
|
122
|
+
*
|
|
123
|
+
* Takes its color from `line` and its radius from `lineWidth`, wrapped in a 2px
|
|
124
|
+
* ring of `background` that separates it from the line itself. Only drawn in
|
|
125
|
+
* line mode, and it crossfades along with the line during a candle↔line
|
|
126
|
+
* transition.
|
|
127
|
+
*/
|
|
128
|
+
lineTipDot?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Pulse a ring outward from the tip dot, once every 2.6s. Defaults to `false`.
|
|
131
|
+
*
|
|
132
|
+
* Ignored when `lineTipDot` is off, and suppressed when the OS asks for reduced
|
|
133
|
+
* motion. Because the ring never stops, enabling it keeps the chart repainting
|
|
134
|
+
* continuously — leave it off for charts that should be able to go idle.
|
|
135
|
+
*/
|
|
136
|
+
lineTipPulse?: boolean;
|
|
110
137
|
};
|
|
111
138
|
/** A time window over the candle data, as Unix epoch milliseconds. */
|
|
112
139
|
type VisibleRange = {
|
|
@@ -134,7 +161,7 @@ type ChartType = 'candles' | 'line';
|
|
|
134
161
|
*/
|
|
135
162
|
type TransitionEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
136
163
|
/** Active drawing tool while in `draw` mode. `null` draws nothing. */
|
|
137
|
-
type DrawTool = null | 'line' | 'box' | 'pencil';
|
|
164
|
+
type DrawTool = null | 'line' | 'box' | 'pencil' | 'path';
|
|
138
165
|
/** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
|
|
139
166
|
type DrawPoint = {
|
|
140
167
|
/** Anchor time as Unix epoch milliseconds (not snapped to a candle slot). */
|
|
@@ -176,16 +203,27 @@ type PencilDrawing = DrawingBase & {
|
|
|
176
203
|
/** The path's points in draw order (at least 2), in data space. */
|
|
177
204
|
points: DrawPoint[];
|
|
178
205
|
};
|
|
206
|
+
/**
|
|
207
|
+
* A multi-segment path: straight segments through `points`, in order, ending in
|
|
208
|
+
* an arrowhead on the last vertex. Like a pencil stroke it holds a variable
|
|
209
|
+
* number of points, but every one was placed deliberately (one click each), so
|
|
210
|
+
* each is an individually draggable handle once the path is committed.
|
|
211
|
+
*/
|
|
212
|
+
type PathDrawing = DrawingBase & {
|
|
213
|
+
type: 'path';
|
|
214
|
+
/** The path's vertices in draw order (at least 2), in data space. */
|
|
215
|
+
points: DrawPoint[];
|
|
216
|
+
};
|
|
179
217
|
/**
|
|
180
218
|
* A committed drawing. Pass an array of these via the `drawings` prop to render
|
|
181
219
|
* persisted annotations; the chart appends a new one (via `onDrawingComplete`)
|
|
182
220
|
* each time the user finishes drawing.
|
|
183
221
|
*
|
|
184
222
|
* This is a discriminated union on `type` — narrow on it before reading
|
|
185
|
-
* `points[1]`, since
|
|
223
|
+
* `points[1]`, since `'pencil'` and `'path'` have variable-length arrays while
|
|
186
224
|
* `'line'` and `'box'` are always exactly two points.
|
|
187
225
|
*/
|
|
188
|
-
type Drawing = LineDrawing | BoxDrawing | PencilDrawing;
|
|
226
|
+
type Drawing = LineDrawing | BoxDrawing | PencilDrawing | PathDrawing;
|
|
189
227
|
/**
|
|
190
228
|
* Storage adapter for **managed** drawing persistence. Provide it via the
|
|
191
229
|
* `drawingStore` prop and the chart owns the drawings array itself — loading and
|
|
@@ -783,4 +821,35 @@ declare global {
|
|
|
783
821
|
*/
|
|
784
822
|
declare function VroomChart(props: VroomChartProps): React.JSX.Element;
|
|
785
823
|
|
|
786
|
-
|
|
824
|
+
/**
|
|
825
|
+
* How a new `candles` array relates to the one the chart already holds:
|
|
826
|
+
* `'initial'` is the first data, `'stream'` a live update to the same series,
|
|
827
|
+
* `'timeframe'` the same asset re-bucketed into a different interval, and
|
|
828
|
+
* `'reset'` a different series entirely.
|
|
829
|
+
*/
|
|
830
|
+
type DataTransition = 'initial' | 'stream' | 'timeframe' | 'reset';
|
|
831
|
+
/**
|
|
832
|
+
* The candle period in ms, inferred as the median of the first few intervals
|
|
833
|
+
* (robust to a single gap). Null when there are fewer than two candles.
|
|
834
|
+
*/
|
|
835
|
+
declare function inferStepMs(candles: Candle[]): number | null;
|
|
836
|
+
/**
|
|
837
|
+
* Classify a candles-prop change. `prev` is the previously rendered array
|
|
838
|
+
* (null on first render); `seriesKeyChanged` forces `reset` regardless of the
|
|
839
|
+
* data (the explicit escape hatch).
|
|
840
|
+
*
|
|
841
|
+
* Constraint: detection compares two immutable snapshots. An array mutated in
|
|
842
|
+
* place (same reference) never reaches this code — React props must change
|
|
843
|
+
* identity to re-render.
|
|
844
|
+
*/
|
|
845
|
+
declare function classifyTransition(prev: Candle[] | null, next: Candle[], seriesKeyChanged: boolean): DataTransition;
|
|
846
|
+
/**
|
|
847
|
+
* The visible window to apply after a timeframe switch so each candle keeps
|
|
848
|
+
* the exact pixel width it had before: the visible slot count is preserved and
|
|
849
|
+
* the right edge re-anchors on the newest candle (any future-gap overshoot is
|
|
850
|
+
* carried over in slots, clamped to the core's 3/4-window cap). The new start
|
|
851
|
+
* may precede the first candle — that gap is intentional, width wins.
|
|
852
|
+
*/
|
|
853
|
+
declare function timeframeWindow(oldWindow: VisibleRange, oldStepMs: number, oldLastMs: number, newStepMs: number, newLastMs: number): VisibleRange;
|
|
854
|
+
|
|
855
|
+
export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type DataTransition, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme, classifyTransition, inferStepMs, timeframeWindow };
|
package/lib/index.d.ts
CHANGED
|
@@ -107,6 +107,33 @@ type VroomTheme = {
|
|
|
107
107
|
* the bottom of the price pane. Defaults to 0.28; set to 0 to disable the fill.
|
|
108
108
|
*/
|
|
109
109
|
lineGradientOpacity?: number;
|
|
110
|
+
/**
|
|
111
|
+
* How much to round the line chart's corners, from 0 (straight segments
|
|
112
|
+
* between closes) to 1 (fully smooth). Defaults to 0.
|
|
113
|
+
*
|
|
114
|
+
* The curve is monotone-limited, so smoothing can never overshoot into a price
|
|
115
|
+
* that didn't trade: every peak and trough stays on an actual close, and the
|
|
116
|
+
* curve never leaves the range of the two closes it connects. Applies to the
|
|
117
|
+
* gradient fill beneath the line as well, so the two stay flush.
|
|
118
|
+
*/
|
|
119
|
+
lineTension?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Mark the line chart's newest end with a dot. Defaults to `true`.
|
|
122
|
+
*
|
|
123
|
+
* Takes its color from `line` and its radius from `lineWidth`, wrapped in a 2px
|
|
124
|
+
* ring of `background` that separates it from the line itself. Only drawn in
|
|
125
|
+
* line mode, and it crossfades along with the line during a candle↔line
|
|
126
|
+
* transition.
|
|
127
|
+
*/
|
|
128
|
+
lineTipDot?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Pulse a ring outward from the tip dot, once every 2.6s. Defaults to `false`.
|
|
131
|
+
*
|
|
132
|
+
* Ignored when `lineTipDot` is off, and suppressed when the OS asks for reduced
|
|
133
|
+
* motion. Because the ring never stops, enabling it keeps the chart repainting
|
|
134
|
+
* continuously — leave it off for charts that should be able to go idle.
|
|
135
|
+
*/
|
|
136
|
+
lineTipPulse?: boolean;
|
|
110
137
|
};
|
|
111
138
|
/** A time window over the candle data, as Unix epoch milliseconds. */
|
|
112
139
|
type VisibleRange = {
|
|
@@ -134,7 +161,7 @@ type ChartType = 'candles' | 'line';
|
|
|
134
161
|
*/
|
|
135
162
|
type TransitionEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
136
163
|
/** Active drawing tool while in `draw` mode. `null` draws nothing. */
|
|
137
|
-
type DrawTool = null | 'line' | 'box' | 'pencil';
|
|
164
|
+
type DrawTool = null | 'line' | 'box' | 'pencil' | 'path';
|
|
138
165
|
/** A drawing anchor in data space, so it stays glued to the candles on pan/zoom. */
|
|
139
166
|
type DrawPoint = {
|
|
140
167
|
/** Anchor time as Unix epoch milliseconds (not snapped to a candle slot). */
|
|
@@ -176,16 +203,27 @@ type PencilDrawing = DrawingBase & {
|
|
|
176
203
|
/** The path's points in draw order (at least 2), in data space. */
|
|
177
204
|
points: DrawPoint[];
|
|
178
205
|
};
|
|
206
|
+
/**
|
|
207
|
+
* A multi-segment path: straight segments through `points`, in order, ending in
|
|
208
|
+
* an arrowhead on the last vertex. Like a pencil stroke it holds a variable
|
|
209
|
+
* number of points, but every one was placed deliberately (one click each), so
|
|
210
|
+
* each is an individually draggable handle once the path is committed.
|
|
211
|
+
*/
|
|
212
|
+
type PathDrawing = DrawingBase & {
|
|
213
|
+
type: 'path';
|
|
214
|
+
/** The path's vertices in draw order (at least 2), in data space. */
|
|
215
|
+
points: DrawPoint[];
|
|
216
|
+
};
|
|
179
217
|
/**
|
|
180
218
|
* A committed drawing. Pass an array of these via the `drawings` prop to render
|
|
181
219
|
* persisted annotations; the chart appends a new one (via `onDrawingComplete`)
|
|
182
220
|
* each time the user finishes drawing.
|
|
183
221
|
*
|
|
184
222
|
* This is a discriminated union on `type` — narrow on it before reading
|
|
185
|
-
* `points[1]`, since
|
|
223
|
+
* `points[1]`, since `'pencil'` and `'path'` have variable-length arrays while
|
|
186
224
|
* `'line'` and `'box'` are always exactly two points.
|
|
187
225
|
*/
|
|
188
|
-
type Drawing = LineDrawing | BoxDrawing | PencilDrawing;
|
|
226
|
+
type Drawing = LineDrawing | BoxDrawing | PencilDrawing | PathDrawing;
|
|
189
227
|
/**
|
|
190
228
|
* Storage adapter for **managed** drawing persistence. Provide it via the
|
|
191
229
|
* `drawingStore` prop and the chart owns the drawings array itself — loading and
|
|
@@ -783,4 +821,35 @@ declare global {
|
|
|
783
821
|
*/
|
|
784
822
|
declare function VroomChart(props: VroomChartProps): React.JSX.Element;
|
|
785
823
|
|
|
786
|
-
|
|
824
|
+
/**
|
|
825
|
+
* How a new `candles` array relates to the one the chart already holds:
|
|
826
|
+
* `'initial'` is the first data, `'stream'` a live update to the same series,
|
|
827
|
+
* `'timeframe'` the same asset re-bucketed into a different interval, and
|
|
828
|
+
* `'reset'` a different series entirely.
|
|
829
|
+
*/
|
|
830
|
+
type DataTransition = 'initial' | 'stream' | 'timeframe' | 'reset';
|
|
831
|
+
/**
|
|
832
|
+
* The candle period in ms, inferred as the median of the first few intervals
|
|
833
|
+
* (robust to a single gap). Null when there are fewer than two candles.
|
|
834
|
+
*/
|
|
835
|
+
declare function inferStepMs(candles: Candle[]): number | null;
|
|
836
|
+
/**
|
|
837
|
+
* Classify a candles-prop change. `prev` is the previously rendered array
|
|
838
|
+
* (null on first render); `seriesKeyChanged` forces `reset` regardless of the
|
|
839
|
+
* data (the explicit escape hatch).
|
|
840
|
+
*
|
|
841
|
+
* Constraint: detection compares two immutable snapshots. An array mutated in
|
|
842
|
+
* place (same reference) never reaches this code — React props must change
|
|
843
|
+
* identity to re-render.
|
|
844
|
+
*/
|
|
845
|
+
declare function classifyTransition(prev: Candle[] | null, next: Candle[], seriesKeyChanged: boolean): DataTransition;
|
|
846
|
+
/**
|
|
847
|
+
* The visible window to apply after a timeframe switch so each candle keeps
|
|
848
|
+
* the exact pixel width it had before: the visible slot count is preserved and
|
|
849
|
+
* the right edge re-anchors on the newest candle (any future-gap overshoot is
|
|
850
|
+
* carried over in slots, clamped to the core's 3/4-window cap). The new start
|
|
851
|
+
* may precede the first candle — that gap is intentional, width wins.
|
|
852
|
+
*/
|
|
853
|
+
declare function timeframeWindow(oldWindow: VisibleRange, oldStepMs: number, oldLastMs: number, newStepMs: number, newLastMs: number): VisibleRange;
|
|
854
|
+
|
|
855
|
+
export { type BollingerBandsConfig, type Candle, type ChartType, type CrosshairEvent, type DataTransition, type MACDConfig, type MAKind, type MASource, type MovingAverageOverlay, type PriceLine, type PriceLinesStyle, type RSIConfig, type TransitionEasing, type VWAPConfig, type VisibleRange, type VolumeConfig, VroomChart, type VroomChartProps, type VroomColor, type VroomTheme, classifyTransition, inferStepMs, timeframeWindow };
|
package/lib/index.js
CHANGED
|
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
VroomChart: () => VroomChart
|
|
33
|
+
VroomChart: () => VroomChart,
|
|
34
|
+
classifyTransition: () => classifyTransition,
|
|
35
|
+
inferStepMs: () => inferStepMs,
|
|
36
|
+
timeframeWindow: () => timeframeWindow
|
|
34
37
|
});
|
|
35
38
|
module.exports = __toCommonJS(index_exports);
|
|
36
39
|
|
|
@@ -48,6 +51,86 @@ var import_react = require("react");
|
|
|
48
51
|
var import_react_native = require("react-native");
|
|
49
52
|
var NativeVroomChart_default = import_react_native.TurboModuleRegistry.getEnforcing("VroomChartModule");
|
|
50
53
|
|
|
54
|
+
// src/dataTransitions.ts
|
|
55
|
+
var STEP_TOLERANCE = 0.01;
|
|
56
|
+
var MAX_SAME_ASSET_CLOSE_RATIO = 1.25;
|
|
57
|
+
var MAX_END_DRIFT_STEPS = 3;
|
|
58
|
+
var MAX_STREAM_ADVANCE_STEPS = 5;
|
|
59
|
+
function inferStepMs(candles) {
|
|
60
|
+
if (candles.length < 2) return null;
|
|
61
|
+
const k = Math.min(candles.length - 1, 8);
|
|
62
|
+
const diffs = [];
|
|
63
|
+
for (let i = 0; i < k; i++) diffs.push(candles[i + 1].timeMs - candles[i].timeMs);
|
|
64
|
+
diffs.sort((a, b) => a - b);
|
|
65
|
+
const median = diffs[Math.floor(diffs.length / 2)];
|
|
66
|
+
return median > 0 ? median : null;
|
|
67
|
+
}
|
|
68
|
+
function indexByTime(candles, t) {
|
|
69
|
+
let lo = 0;
|
|
70
|
+
let hi = candles.length - 1;
|
|
71
|
+
while (lo <= hi) {
|
|
72
|
+
const mid = lo + hi >>> 1;
|
|
73
|
+
const v = candles[mid].timeMs;
|
|
74
|
+
if (v === t) return mid;
|
|
75
|
+
if (v < t) lo = mid + 1;
|
|
76
|
+
else hi = mid - 1;
|
|
77
|
+
}
|
|
78
|
+
return -1;
|
|
79
|
+
}
|
|
80
|
+
function classifyTransition(prev, next, seriesKeyChanged) {
|
|
81
|
+
if (!prev || prev.length === 0) return "initial";
|
|
82
|
+
if (next.length === 0) return "stream";
|
|
83
|
+
if (seriesKeyChanged) return "reset";
|
|
84
|
+
const prevStep = inferStepMs(prev);
|
|
85
|
+
const nextStep = inferStepMs(next);
|
|
86
|
+
if (prevStep == null || nextStep == null) return "reset";
|
|
87
|
+
const prevLast = prev[prev.length - 1];
|
|
88
|
+
const nextLast = next[next.length - 1];
|
|
89
|
+
if (Math.abs(nextStep - prevStep) <= prevStep * STEP_TOLERANCE) {
|
|
90
|
+
const idx = indexByTime(next, prevLast.timeMs);
|
|
91
|
+
const aligned = idx >= 0;
|
|
92
|
+
const sharedBarRatio = aligned && next[idx].close > 0 && prevLast.close > 0 ? Math.max(next[idx].close / prevLast.close, prevLast.close / next[idx].close) : Infinity;
|
|
93
|
+
const advanced = nextLast.timeMs >= prevLast.timeMs && nextLast.timeMs - prevLast.timeMs <= MAX_STREAM_ADVANCE_STEPS * nextStep;
|
|
94
|
+
return sharedBarRatio <= MAX_SAME_ASSET_CLOSE_RATIO && advanced ? "stream" : "reset";
|
|
95
|
+
}
|
|
96
|
+
const closeRatio = prevLast.close > 0 && nextLast.close > 0 ? Math.max(nextLast.close / prevLast.close, prevLast.close / nextLast.close) : Infinity;
|
|
97
|
+
const prevEnd = prevLast.timeMs + prevStep;
|
|
98
|
+
const nextEnd = nextLast.timeMs + nextStep;
|
|
99
|
+
const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
|
|
100
|
+
return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
|
|
101
|
+
}
|
|
102
|
+
function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
|
|
103
|
+
const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
|
|
104
|
+
const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
|
|
105
|
+
const offsetSlots = Math.min(Math.max(offsetRaw, 0), slots * 0.75);
|
|
106
|
+
const endMs = Math.round(newLastMs + offsetSlots * newStepMs);
|
|
107
|
+
return { startMs: Math.round(endMs - slots * newStepMs), endMs };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/easing.ts
|
|
111
|
+
function ease(kind, p) {
|
|
112
|
+
switch (kind) {
|
|
113
|
+
case "linear":
|
|
114
|
+
return p;
|
|
115
|
+
case "ease-in":
|
|
116
|
+
return p * p;
|
|
117
|
+
case "ease-out":
|
|
118
|
+
return p * (2 - p);
|
|
119
|
+
default:
|
|
120
|
+
return p * p * (3 - 2 * p);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
var EASINGS = [
|
|
124
|
+
"linear",
|
|
125
|
+
"ease-in",
|
|
126
|
+
"ease-out",
|
|
127
|
+
"ease-in-out"
|
|
128
|
+
];
|
|
129
|
+
function easingIndex(kind) {
|
|
130
|
+
const i = kind ? EASINGS.indexOf(kind) : -1;
|
|
131
|
+
return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
|
|
132
|
+
}
|
|
133
|
+
|
|
51
134
|
// src/packCandles.ts
|
|
52
135
|
var BYTES_PER_CANDLE = 48;
|
|
53
136
|
function packCandles(candles) {
|
|
@@ -106,12 +189,18 @@ var FLOAT_KEYS = {
|
|
|
106
189
|
// VROOM_FLOAT_VOLUME_RADIUS_PX
|
|
107
190
|
lineWidth: 11,
|
|
108
191
|
// VROOM_FLOAT_LINE_WIDTH_PX
|
|
109
|
-
lineGradientOpacity: 12
|
|
192
|
+
lineGradientOpacity: 12,
|
|
110
193
|
// VROOM_FLOAT_LINE_GRADIENT_OPACITY
|
|
194
|
+
lineTension: 13
|
|
195
|
+
// VROOM_FLOAT_LINE_TENSION
|
|
111
196
|
};
|
|
197
|
+
var FLOAT_LINE_TIP_PULSE = 15;
|
|
112
198
|
var BOOL_KEYS = {
|
|
113
|
-
wickRoundCap: 9
|
|
199
|
+
wickRoundCap: 9,
|
|
114
200
|
// VROOM_FLOAT_WICK_ROUND_CAP
|
|
201
|
+
lineTipDot: 14,
|
|
202
|
+
// VROOM_FLOAT_LINE_TIP_DOT
|
|
203
|
+
lineTipPulse: FLOAT_LINE_TIP_PULSE
|
|
115
204
|
};
|
|
116
205
|
function parseColor(value) {
|
|
117
206
|
if (typeof value === "number") {
|
|
@@ -282,15 +371,53 @@ function ensureInstalled() {
|
|
|
282
371
|
}
|
|
283
372
|
installed = true;
|
|
284
373
|
}
|
|
285
|
-
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines) {
|
|
374
|
+
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
|
|
286
375
|
const handleRef = (0, import_react.useRef)(null);
|
|
287
376
|
const defaultWidthAppliedRef = (0, import_react.useRef)(false);
|
|
288
377
|
const volumeCollapseRef = (0, import_react.useRef)(null);
|
|
378
|
+
const prevDataRef = (0, import_react.useRef)(null);
|
|
379
|
+
const intervalMorphRaf = (0, import_react.useRef)(null);
|
|
289
380
|
const [picture, setPicture] = (0, import_react.useState)(null);
|
|
290
381
|
if (!handleRef.current && size.width > 0 && size.height > 0) {
|
|
291
382
|
ensureInstalled();
|
|
292
383
|
handleRef.current = globalThis.VroomChartJSI.create();
|
|
293
384
|
}
|
|
385
|
+
const animRef = (0, import_react.useRef)({ ms: 300, easing: void 0, reduceMotion: false });
|
|
386
|
+
animRef.current = {
|
|
387
|
+
ms: Math.max(0, transition?.transitionMs ?? 300),
|
|
388
|
+
easing: transition?.transitionEasing,
|
|
389
|
+
reduceMotion: transition?.reduceMotion ?? false
|
|
390
|
+
};
|
|
391
|
+
const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
|
|
392
|
+
onFrameRef.current = transition?.onFrame;
|
|
393
|
+
const seriesKey = transition?.seriesKey;
|
|
394
|
+
const endIntervalMorph = (0, import_react.useCallback)(() => {
|
|
395
|
+
if (intervalMorphRaf.current != null) {
|
|
396
|
+
cancelAnimationFrame(intervalMorphRaf.current);
|
|
397
|
+
intervalMorphRaf.current = null;
|
|
398
|
+
}
|
|
399
|
+
handleRef.current?.setIntervalMorph(1);
|
|
400
|
+
}, []);
|
|
401
|
+
const startIntervalMorph = (0, import_react.useCallback)((h) => {
|
|
402
|
+
const { ms, easing } = animRef.current;
|
|
403
|
+
const start = performance.now();
|
|
404
|
+
const step = (now) => {
|
|
405
|
+
const p = Math.min(1, (now - start) / ms);
|
|
406
|
+
h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
|
|
407
|
+
const pic = h.render();
|
|
408
|
+
if (pic) onFrameRef.current?.(pic);
|
|
409
|
+
intervalMorphRaf.current = p < 1 ? requestAnimationFrame(step) : null;
|
|
410
|
+
};
|
|
411
|
+
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
412
|
+
}, []);
|
|
413
|
+
(0, import_react.useEffect)(() => {
|
|
414
|
+
return () => {
|
|
415
|
+
if (intervalMorphRaf.current != null) {
|
|
416
|
+
cancelAnimationFrame(intervalMorphRaf.current);
|
|
417
|
+
intervalMorphRaf.current = null;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}, []);
|
|
294
421
|
const explicit = visibleRange != null;
|
|
295
422
|
const startMs = visibleRange?.startMs ?? 0;
|
|
296
423
|
const endMs = visibleRange?.endMs ?? 0;
|
|
@@ -310,8 +437,54 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
310
437
|
h.setDefaultCandleWidth(defaultCandleWidth);
|
|
311
438
|
defaultWidthAppliedRef.current = true;
|
|
312
439
|
}
|
|
440
|
+
let morphing = false;
|
|
313
441
|
if (candles.length > 0) {
|
|
314
|
-
|
|
442
|
+
const prev = prevDataRef.current;
|
|
443
|
+
const freshHandle = prev == null || prev.handle !== h;
|
|
444
|
+
if (freshHandle || prev.candles !== candles || prev.seriesKey !== seriesKey) {
|
|
445
|
+
const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
|
|
446
|
+
let tfArgs = null;
|
|
447
|
+
let prevEnvelope = null;
|
|
448
|
+
if (transitionKind === "timeframe" && prev != null) {
|
|
449
|
+
const oldWindow = h.getVisibleRange();
|
|
450
|
+
const oldStepMs = inferStepMs(prev.candles);
|
|
451
|
+
if (oldWindow.endMs > oldWindow.startMs && oldStepMs != null) {
|
|
452
|
+
tfArgs = {
|
|
453
|
+
oldWindow,
|
|
454
|
+
oldStepMs,
|
|
455
|
+
oldLastMs: prev.candles[prev.candles.length - 1].timeMs
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
prevEnvelope = h.getVisiblePriceEnvelope();
|
|
459
|
+
morphing = animRef.current.ms > 0 && !animRef.current.reduceMotion && onFrameRef.current != null;
|
|
460
|
+
if (morphing) {
|
|
461
|
+
endIntervalMorph();
|
|
462
|
+
h.beginIntervalMorph();
|
|
463
|
+
}
|
|
464
|
+
} else if (transitionKind === "initial" || transitionKind === "reset") {
|
|
465
|
+
endIntervalMorph();
|
|
466
|
+
}
|
|
467
|
+
h.setCandles(packCandles(candles));
|
|
468
|
+
if (transitionKind === "timeframe") {
|
|
469
|
+
const newStepMs = inferStepMs(candles);
|
|
470
|
+
if (tfArgs && newStepMs != null) {
|
|
471
|
+
const w = timeframeWindow(
|
|
472
|
+
tfArgs.oldWindow,
|
|
473
|
+
tfArgs.oldStepMs,
|
|
474
|
+
tfArgs.oldLastMs,
|
|
475
|
+
newStepMs,
|
|
476
|
+
candles[candles.length - 1].timeMs
|
|
477
|
+
);
|
|
478
|
+
h.setVisibleRange(w.startMs, w.endMs);
|
|
479
|
+
}
|
|
480
|
+
if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
|
|
481
|
+
else h.resetPriceScale();
|
|
482
|
+
if (morphing) startIntervalMorph(h);
|
|
483
|
+
} else if (transitionKind === "reset") {
|
|
484
|
+
h.resetView();
|
|
485
|
+
}
|
|
486
|
+
prevDataRef.current = { handle: h, candles, seriesKey };
|
|
487
|
+
}
|
|
315
488
|
}
|
|
316
489
|
if (explicit) {
|
|
317
490
|
h.setVisibleRange(startMs, endMs);
|
|
@@ -319,6 +492,9 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
319
492
|
if (theme) {
|
|
320
493
|
applyTheme(h, theme);
|
|
321
494
|
}
|
|
495
|
+
if (animRef.current.reduceMotion) {
|
|
496
|
+
h.setFloat(FLOAT_LINE_TIP_PULSE, 0);
|
|
497
|
+
}
|
|
322
498
|
h.setRSI(rsiToSpec(rsi));
|
|
323
499
|
h.setMACD(macdToSpec(macd));
|
|
324
500
|
h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
|
|
@@ -330,39 +506,16 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
330
506
|
h.setPriceLines(
|
|
331
507
|
priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
|
|
332
508
|
);
|
|
333
|
-
setPicture(h.render());
|
|
334
|
-
}, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey]);
|
|
509
|
+
if (!morphing) setPicture(h.render());
|
|
510
|
+
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
|
|
335
511
|
return { handle: handleRef.current, picture, volumeCollapseRef };
|
|
336
512
|
}
|
|
337
513
|
|
|
338
|
-
// src/easing.ts
|
|
339
|
-
function ease(kind, p) {
|
|
340
|
-
switch (kind) {
|
|
341
|
-
case "linear":
|
|
342
|
-
return p;
|
|
343
|
-
case "ease-in":
|
|
344
|
-
return p * p;
|
|
345
|
-
case "ease-out":
|
|
346
|
-
return p * (2 - p);
|
|
347
|
-
default:
|
|
348
|
-
return p * p * (3 - 2 * p);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
var EASINGS = [
|
|
352
|
-
"linear",
|
|
353
|
-
"ease-in",
|
|
354
|
-
"ease-out",
|
|
355
|
-
"ease-in-out"
|
|
356
|
-
];
|
|
357
|
-
function easingIndex(kind) {
|
|
358
|
-
const i = kind ? EASINGS.indexOf(kind) : -1;
|
|
359
|
-
return i < 0 ? EASINGS.indexOf("ease-in-out") : i;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
514
|
// src/VroomChart.tsx
|
|
363
515
|
function VroomChart(props) {
|
|
364
516
|
const {
|
|
365
517
|
candles,
|
|
518
|
+
seriesKey,
|
|
366
519
|
width: widthProp,
|
|
367
520
|
height: heightProp,
|
|
368
521
|
style,
|
|
@@ -405,6 +558,19 @@ function VroomChart(props) {
|
|
|
405
558
|
} : void 0,
|
|
406
559
|
[priceLines, priceLinesStyle, onPriceLineClose]
|
|
407
560
|
);
|
|
561
|
+
const emptyPicture = (0, import_react2.useMemo)(() => {
|
|
562
|
+
const rec = import_react_native_skia.Skia.PictureRecorder();
|
|
563
|
+
rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
|
|
564
|
+
return rec.finishRecordingAsPicture();
|
|
565
|
+
}, []);
|
|
566
|
+
const pictureSV = (0, import_react_native_reanimated.useSharedValue)(emptyPicture);
|
|
567
|
+
const reduceMotion = (0, import_react_native_reanimated.useReducedMotion)();
|
|
568
|
+
const onFrame = (0, import_react2.useCallback)(
|
|
569
|
+
(p) => {
|
|
570
|
+
pictureSV.value = p;
|
|
571
|
+
},
|
|
572
|
+
[pictureSV]
|
|
573
|
+
);
|
|
408
574
|
const { handle, picture, volumeCollapseRef } = useChartCore(
|
|
409
575
|
candles,
|
|
410
576
|
{ width, height },
|
|
@@ -418,19 +584,11 @@ function VroomChart(props) {
|
|
|
418
584
|
vwap,
|
|
419
585
|
bollingerBands,
|
|
420
586
|
volume,
|
|
421
|
-
priceLinesProp
|
|
587
|
+
priceLinesProp,
|
|
588
|
+
{ seriesKey, transitionMs, transitionEasing, reduceMotion, onFrame }
|
|
422
589
|
);
|
|
423
|
-
const emptyPicture = (0, import_react2.useMemo)(() => {
|
|
424
|
-
const rec = import_react_native_skia.Skia.PictureRecorder();
|
|
425
|
-
rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
|
|
426
|
-
return rec.finishRecordingAsPicture();
|
|
427
|
-
}, []);
|
|
428
|
-
const pictureSV = (0, import_react_native_reanimated.useSharedValue)(emptyPicture);
|
|
429
590
|
const crosshairActive = (0, import_react2.useRef)(false);
|
|
430
591
|
const lastCrosshairTime = (0, import_react2.useRef)(null);
|
|
431
|
-
(0, import_react2.useEffect)(() => {
|
|
432
|
-
if (picture) pictureSV.value = picture;
|
|
433
|
-
}, [picture, pictureSV]);
|
|
434
592
|
const decayRaf = (0, import_react2.useRef)(null);
|
|
435
593
|
const cancelDecay = (0, import_react2.useCallback)(() => {
|
|
436
594
|
if (decayRaf.current != null) {
|
|
@@ -462,6 +620,10 @@ function VroomChart(props) {
|
|
|
462
620
|
}
|
|
463
621
|
};
|
|
464
622
|
}, []);
|
|
623
|
+
(0, import_react2.useEffect)(() => {
|
|
624
|
+
if (picture) pictureSV.value = picture;
|
|
625
|
+
maybeStartAnim();
|
|
626
|
+
}, [picture, pictureSV, maybeStartAnim]);
|
|
465
627
|
const morphRaf = (0, import_react2.useRef)(null);
|
|
466
628
|
const morphFade = (0, import_react2.useRef)(null);
|
|
467
629
|
const morphHandle = (0, import_react2.useRef)(null);
|
|
@@ -498,7 +660,7 @@ function VroomChart(props) {
|
|
|
498
660
|
const prog = Math.min(1, (now - startTs) / dur);
|
|
499
661
|
const fade = from + (target - from) * ease(easingRef.current, prog);
|
|
500
662
|
morphFade.current = fade;
|
|
501
|
-
handle.setMorph(fade, fade);
|
|
663
|
+
handle.setMorph(reduceMotion ? 0 : fade, fade);
|
|
502
664
|
const p = handle.render();
|
|
503
665
|
if (p) pictureSV.value = p;
|
|
504
666
|
if (prog < 1) {
|
|
@@ -518,7 +680,7 @@ function VroomChart(props) {
|
|
|
518
680
|
morphRaf.current = null;
|
|
519
681
|
}
|
|
520
682
|
};
|
|
521
|
-
}, [handle, chartType, transitionMs, pictureSV]);
|
|
683
|
+
}, [handle, chartType, transitionMs, reduceMotion, pictureSV]);
|
|
522
684
|
const volumeRaf = (0, import_react2.useRef)(null);
|
|
523
685
|
const volumeHandle = (0, import_react2.useRef)(null);
|
|
524
686
|
(0, import_react2.useEffect)(() => {
|
|
@@ -536,7 +698,7 @@ function VroomChart(props) {
|
|
|
536
698
|
volumeRaf.current = null;
|
|
537
699
|
}
|
|
538
700
|
const dur = Math.max(0, transitionMs ?? 300);
|
|
539
|
-
if (dur === 0) {
|
|
701
|
+
if (dur === 0 || reduceMotion) {
|
|
540
702
|
volumeCollapseRef.current = { t: target, easing };
|
|
541
703
|
handle.setVolumeCollapse(target, easing);
|
|
542
704
|
const p = handle.render();
|
|
@@ -563,7 +725,7 @@ function VroomChart(props) {
|
|
|
563
725
|
volumeRaf.current = null;
|
|
564
726
|
}
|
|
565
727
|
};
|
|
566
|
-
}, [handle, volume?.enabled, transitionMs, pictureSV, volumeCollapseRef]);
|
|
728
|
+
}, [handle, volume?.enabled, transitionMs, reduceMotion, pictureSV, volumeCollapseRef]);
|
|
567
729
|
const hitAxis = (0, import_react2.useCallback)(
|
|
568
730
|
(x, y) => {
|
|
569
731
|
if (!handle) return "chart";
|
|
@@ -775,6 +937,9 @@ function VroomChart(props) {
|
|
|
775
937
|
}
|
|
776
938
|
// Annotate the CommonJS export names for ESM import in node:
|
|
777
939
|
0 && (module.exports = {
|
|
778
|
-
VroomChart
|
|
940
|
+
VroomChart,
|
|
941
|
+
classifyTransition,
|
|
942
|
+
inferStepMs,
|
|
943
|
+
timeframeWindow
|
|
779
944
|
});
|
|
780
945
|
//# sourceMappingURL=index.js.map
|