react-native-vroom-chart 0.6.0 → 0.8.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.
Files changed (49) hide show
  1. package/cpp/VroomChartHostObject.cpp +281 -33
  2. package/cpp/_core_include/vroom/vroom_chart.h +231 -36
  3. package/cpp/_core_src/bollinger.h +1 -1
  4. package/cpp/_core_src/candles.cpp +138 -41
  5. package/cpp/_core_src/candles.h +11 -1
  6. package/cpp/_core_src/chart.cpp +91 -40
  7. package/cpp/_core_src/chart.h +69 -29
  8. package/cpp/_core_src/chart_facade.cpp +247 -69
  9. package/cpp/_core_src/drawings.cpp +147 -4
  10. package/cpp/_core_src/drawings.h +10 -5
  11. package/cpp/_core_src/gradient.cpp +46 -0
  12. package/cpp/_core_src/gradient.h +31 -0
  13. package/cpp/_core_src/labels.cpp +49 -16
  14. package/cpp/_core_src/labels.h +33 -4
  15. package/cpp/_core_src/liquidity.cpp +3 -37
  16. package/cpp/_core_src/ma_overlay.cpp +174 -12
  17. package/cpp/_core_src/ma_overlay.h +55 -3
  18. package/cpp/_core_src/macd.cpp +13 -42
  19. package/cpp/_core_src/macd.h +11 -8
  20. package/cpp/_core_src/macd_pane.cpp +57 -27
  21. package/cpp/_core_src/price_line_layout.h +1 -1
  22. package/cpp/_core_src/rsi.cpp +4 -18
  23. package/cpp/_core_src/rsi.h +6 -6
  24. package/cpp/_core_src/rsi_pane.cpp +34 -19
  25. package/cpp/_core_src/series_ma.cpp +64 -0
  26. package/cpp/_core_src/series_ma.h +30 -0
  27. package/cpp/_core_src/style_inherit.h +35 -0
  28. package/cpp/_core_src/theme.cpp +2 -1
  29. package/cpp/_core_src/viewport.cpp +36 -12
  30. package/cpp/_core_src/viewport.h +50 -0
  31. package/cpp/_core_src/volume.cpp +33 -8
  32. package/cpp/_core_src/volume.h +12 -1
  33. package/cpp/_core_src/volume_anim.cpp +32 -0
  34. package/cpp/_core_src/volume_anim.h +41 -0
  35. package/lib/index.d.mts +206 -31
  36. package/lib/index.d.ts +206 -31
  37. package/lib/index.js +324 -44
  38. package/lib/index.js.map +1 -1
  39. package/lib/index.mjs +329 -52
  40. package/lib/index.mjs.map +1 -1
  41. package/package.json +1 -1
  42. package/src/VroomChart.tsx +100 -17
  43. package/src/dataTransitions.ts +148 -0
  44. package/src/easing.ts +40 -0
  45. package/src/index.ts +9 -0
  46. package/src/jsi.d.ts +126 -19
  47. package/src/theme.ts +1 -0
  48. package/src/types.ts +3 -0
  49. package/src/useChartCore.ts +273 -30
@@ -0,0 +1,148 @@
1
+ // Mirror of packages/react/src/dataTransitions.ts — the platform packages don't
2
+ // depend on each other, and @vroomchart/types carries types only.
3
+ //
4
+ // Classifies how a new `candles` prop relates to the previous one so the chart
5
+ // can react appropriately: leave the viewport alone for streaming updates,
6
+ // re-anchor the time window for a timeframe switch, or fully reset the view
7
+ // for a different asset. Pure functions, no React — see useChartCore for the
8
+ // orchestration.
9
+
10
+ import type { Candle, VisibleRange } from '@vroomchart/types';
11
+
12
+ /**
13
+ * How a new `candles` array relates to the one the chart already holds:
14
+ * `'initial'` is the first data, `'stream'` a live update to the same series,
15
+ * `'timeframe'` the same asset re-bucketed into a different interval, and
16
+ * `'reset'` a different series entirely.
17
+ */
18
+ export type DataTransition = 'initial' | 'stream' | 'timeframe' | 'reset';
19
+
20
+ // A step change below this ratio is treated as the same timeframe. Real steps
21
+ // are exact integer ms; the tolerance only absorbs rounding/DST quirks (the
22
+ // smallest real timeframe jump, 1m -> 2m, is 100% apart).
23
+ const STEP_TOLERANCE = 0.01;
24
+
25
+ // Same-asset check for a timeframe switch: both series end "now", so their
26
+ // last closes must be close. No asset moves 25% between two consecutive prop
27
+ // pushes; distinct assets within 25% of each other are what `seriesKey` is for.
28
+ const MAX_SAME_ASSET_CLOSE_RATIO = 1.25;
29
+
30
+ // A coarser bucketing can shift the final bar's open by up to one coarse bar;
31
+ // allow that plus an in-flight bar when checking the two series end together.
32
+ const MAX_END_DRIFT_STEPS = 3;
33
+
34
+ // Streaming pushes may batch a few bars (e.g. a throttled background tab), but
35
+ // a jump of more than this many steps means the data was re-fetched elsewhere.
36
+ const MAX_STREAM_ADVANCE_STEPS = 5;
37
+
38
+ /**
39
+ * The candle period in ms, inferred as the median of the first few intervals
40
+ * (robust to a single gap). Null when there are fewer than two candles.
41
+ */
42
+ export function inferStepMs(candles: Candle[]): number | null {
43
+ if (candles.length < 2) return null;
44
+ const k = Math.min(candles.length - 1, 8);
45
+ const diffs: number[] = [];
46
+ for (let i = 0; i < k; i++) diffs.push(candles[i + 1].timeMs - candles[i].timeMs);
47
+ diffs.sort((a, b) => a - b);
48
+ const median = diffs[Math.floor(diffs.length / 2)];
49
+ return median > 0 ? median : null;
50
+ }
51
+
52
+ // Index of the candle whose timeMs exactly equals `t`, or -1. Binary search over
53
+ // the ascending-by-time series, so it tolerates interior gaps (missing bars from
54
+ // downtime / illiquid periods) — unlike a uniform-grid index computed from the
55
+ // step, which assumes a hole-free grid.
56
+ function indexByTime(candles: Candle[], t: number): number {
57
+ let lo = 0;
58
+ let hi = candles.length - 1;
59
+ while (lo <= hi) {
60
+ const mid = (lo + hi) >>> 1;
61
+ const v = candles[mid].timeMs;
62
+ if (v === t) return mid;
63
+ if (v < t) lo = mid + 1;
64
+ else hi = mid - 1;
65
+ }
66
+ return -1;
67
+ }
68
+
69
+ /**
70
+ * Classify a candles-prop change. `prev` is the previously rendered array
71
+ * (null on first render); `seriesKeyChanged` forces `reset` regardless of the
72
+ * data (the explicit escape hatch).
73
+ *
74
+ * Constraint: detection compares two immutable snapshots. An array mutated in
75
+ * place (same reference) never reaches this code — React props must change
76
+ * identity to re-render.
77
+ */
78
+ export function classifyTransition(
79
+ prev: Candle[] | null,
80
+ next: Candle[],
81
+ seriesKeyChanged: boolean,
82
+ ): DataTransition {
83
+ if (!prev || prev.length === 0) return 'initial';
84
+ if (next.length === 0) return 'stream'; // nothing to reframe against
85
+ if (seriesKeyChanged) return 'reset';
86
+
87
+ const prevStep = inferStepMs(prev);
88
+ const nextStep = inferStepMs(next);
89
+ if (prevStep == null || nextStep == null) return 'reset'; // too little data to reason
90
+
91
+ const prevLast = prev[prev.length - 1];
92
+ const nextLast = next[next.length - 1];
93
+
94
+ if (Math.abs(nextStep - prevStep) <= prevStep * STEP_TOLERANCE) {
95
+ // Same step: streaming iff prev's last bar still appears in next (covers
96
+ // append, update-last, and rolling buffers that drop old bars from the
97
+ // front) and the series only advanced by a few bars. Locate that bar by
98
+ // timestamp, not by a step-derived index — real series have interior gaps
99
+ // (downtime / illiquid periods), so a uniform-grid index would miss it and
100
+ // misread a harmless in-place tick as a reset.
101
+ // Time alignment alone isn't enough: two assets on the same exchange share
102
+ // the bar grid, so the bar at the shared timestamp must also be (nearly) the
103
+ // same bar — update-last moves the close, but never by the same-asset ratio.
104
+ const idx = indexByTime(next, prevLast.timeMs);
105
+ const aligned = idx >= 0;
106
+ const sharedBarRatio =
107
+ aligned && next[idx].close > 0 && prevLast.close > 0
108
+ ? Math.max(next[idx].close / prevLast.close, prevLast.close / next[idx].close)
109
+ : Infinity;
110
+ const advanced =
111
+ nextLast.timeMs >= prevLast.timeMs &&
112
+ nextLast.timeMs - prevLast.timeMs <= MAX_STREAM_ADVANCE_STEPS * nextStep;
113
+ return sharedBarRatio <= MAX_SAME_ASSET_CLOSE_RATIO && advanced ? 'stream' : 'reset';
114
+ }
115
+
116
+ // Step changed: a timeframe switch iff it still looks like the same asset —
117
+ // last closes near each other and both series ending around the same time.
118
+ const closeRatio =
119
+ prevLast.close > 0 && nextLast.close > 0
120
+ ? Math.max(nextLast.close / prevLast.close, prevLast.close / nextLast.close)
121
+ : Infinity;
122
+ const prevEnd = prevLast.timeMs + prevStep;
123
+ const nextEnd = nextLast.timeMs + nextStep;
124
+ const endsTogether =
125
+ Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
126
+ return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? 'timeframe' : 'reset';
127
+ }
128
+
129
+ /**
130
+ * The visible window to apply after a timeframe switch so each candle keeps
131
+ * the exact pixel width it had before: the visible slot count is preserved and
132
+ * the right edge re-anchors on the newest candle (any future-gap overshoot is
133
+ * carried over in slots, clamped to the core's 3/4-window cap). The new start
134
+ * may precede the first candle — that gap is intentional, width wins.
135
+ */
136
+ export function timeframeWindow(
137
+ oldWindow: VisibleRange,
138
+ oldStepMs: number,
139
+ oldLastMs: number,
140
+ newStepMs: number,
141
+ newLastMs: number,
142
+ ): VisibleRange {
143
+ const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
144
+ const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
145
+ const offsetSlots = Math.min(Math.max(offsetRaw, 0), slots * 0.75);
146
+ const endMs = Math.round(newLastMs + offsetSlots * newStepMs);
147
+ return { startMs: Math.round(endMs - slots * newStepMs), endMs };
148
+ }
package/src/easing.ts ADDED
@@ -0,0 +1,40 @@
1
+ // Mirror of packages/react/src/easing.ts — the platform packages don't depend on
2
+ // each other, and @vroomchart/types carries types only.
3
+
4
+ import type { TransitionEasing } from '@vroomchart/types';
5
+
6
+ /**
7
+ * Maps linear animation progress (0..1) to eased progress (0..1) for the chart's
8
+ * transitions. Unknown values fall back to `'ease-in-out'`, which is a
9
+ * smoothstep — the curve the candle↔line transition has always used.
10
+ */
11
+ export function ease(kind: TransitionEasing | undefined, p: number): number {
12
+ switch (kind) {
13
+ case 'linear':
14
+ return p;
15
+ case 'ease-in':
16
+ return p * p;
17
+ case 'ease-out':
18
+ return p * (2 - p);
19
+ default:
20
+ return p * p * (3 - 2 * p);
21
+ }
22
+ }
23
+
24
+ // Index order matches VroomEasing in vroom_chart.h.
25
+ const EASINGS: readonly TransitionEasing[] = [
26
+ 'linear',
27
+ 'ease-in',
28
+ 'ease-out',
29
+ 'ease-in-out',
30
+ ];
31
+
32
+ /**
33
+ * The curve as a `VroomEasing` index, for the animations the core paces itself
34
+ * (see `setVolumeCollapse`) rather than taking pre-eased progress. Falls back to
35
+ * `'ease-in-out'`, matching {@link ease}.
36
+ */
37
+ export function easingIndex(kind: TransitionEasing | undefined): number {
38
+ const i = kind ? EASINGS.indexOf(kind) : -1;
39
+ return i < 0 ? EASINGS.indexOf('ease-in-out') : i;
40
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  export { VroomChart } from './VroomChart';
2
+ export {
3
+ classifyTransition,
4
+ inferStepMs,
5
+ timeframeWindow,
6
+ type DataTransition,
7
+ } from './dataTransitions';
2
8
  export type {
3
9
  VroomChartProps,
4
10
  Candle,
@@ -9,10 +15,13 @@ export type {
9
15
  RSIConfig,
10
16
  MACDConfig,
11
17
  MASource,
18
+ MAKind,
12
19
  MovingAverageOverlay,
13
20
  VWAPConfig,
14
21
  BollingerBandsConfig,
22
+ VolumeConfig,
15
23
  ChartType,
24
+ TransitionEasing,
16
25
  PriceLine,
17
26
  PriceLinesStyle,
18
27
  } from './types';
package/src/jsi.d.ts CHANGED
@@ -27,6 +27,57 @@ export interface ChartHandle {
27
27
  setChartType(mode: number): void;
28
28
  /** Candle↔line morph blend: collapse folds candles to close, fade crossfades. */
29
29
  setMorph(collapse: number, fade: number): void;
30
+ /** The current visible time window. {startMs: 0, endMs: 0} = uninitialized. */
31
+ getVisibleRange(): { startMs: number; endMs: number };
32
+ /**
33
+ * Reset to the fresh-mount view: frame the most recent ~80 candles and
34
+ * re-enable continuous y auto-fit (the price range follows the visible
35
+ * candles until the next manual y gesture). Use when the data series is
36
+ * wholesale replaced — e.g. switching assets.
37
+ */
38
+ resetView(): void;
39
+ /**
40
+ * Re-enable continuous y auto-fit only; the time window is untouched. Use
41
+ * after repositioning the window for a same-asset data swap (e.g. a
42
+ * timeframe switch) so the price scale re-fits the newly visible candles.
43
+ */
44
+ resetPriceScale(): void;
45
+ /**
46
+ * The visible price *envelope* — the min low / max high across the currently
47
+ * visible candles, i.e. the extent the candles occupy rather than the (wider)
48
+ * axis range. Null when no candles are visible.
49
+ */
50
+ getVisiblePriceEnvelope(): { low: number; high: number } | null;
51
+ /**
52
+ * Scale lock for a same-asset data swap that re-buckets the same price action
53
+ * into a different high-low span (a timeframe switch). Rescales a *manual*
54
+ * price range so the visible envelope keeps the exact pixel height and
55
+ * position the given pre-swap envelope had — so candles don't suddenly shrink
56
+ * or grow when the interval changes.
57
+ *
58
+ * Call after setCandles + setVisibleRange, passing the envelope read by
59
+ * getVisiblePriceEnvelope before the swap. A no-op in auto-y mode (auto-fit is
60
+ * already span-invariant); falls back to resetPriceScale when either envelope
61
+ * is degenerate.
62
+ */
63
+ preservePriceEnvelope(prevLow: number, prevHigh: number): void;
64
+ /**
65
+ * Capture the visible candle geometry so the next data swap can animate as a
66
+ * reshape rather than a jump: each candle's wick and body slide and stretch
67
+ * into the shape of its counterpart in the new data.
68
+ *
69
+ * Candles are paired by *slot* — position counting back from the right edge of
70
+ * the visible window, which a timeframe switch preserves. Call before
71
+ * setCandles, then drive setIntervalMorph from 0 to 1.
72
+ */
73
+ beginIntervalMorph(): void;
74
+ /**
75
+ * Advance the interval morph started by beginIntervalMorph. `t` (clamped to
76
+ * 0..1) is the eased progress: 0 renders the captured geometry pixel-
77
+ * identically to the pre-swap frame, 1 renders the new candles and releases
78
+ * the capture. Driven per-frame by the host animation loop.
79
+ */
80
+ setIntervalMorph(t: number): void;
30
81
  /** Shifts the visible range by `dx`/`dy` pixels and returns a fresh picture. */
31
82
  pan(dx: number, dy: number): SkPicture | null;
32
83
  /**
@@ -102,22 +153,53 @@ export interface ChartHandle {
102
153
  } | null;
103
154
  } | null;
104
155
  /**
105
- * Configures the RSI pane: enable, period (>=2), overbought/oversold band
106
- * levels (0..100), and the RSI-based MA trendline (toggle + length >=1).
156
+ * Configures the RSI pane. maKind mirrors setOverlays' kind encoding; colors
157
+ * are packed 0xAARRGGBB where 0 means inherit, and a non-positive width
158
+ * inherits the default stroke.
107
159
  */
108
- setRSI(
109
- enabled: boolean,
110
- period: number,
111
- upperBand: number,
112
- lowerBand: number,
113
- maEnabled: boolean,
114
- maPeriod: number,
115
- ): void;
160
+ setRSI(spec: {
161
+ enabled: boolean;
162
+ period: number;
163
+ upperBand: number;
164
+ lowerBand: number;
165
+ maPeriod: number;
166
+ maKind: number;
167
+ maVisible: boolean;
168
+ lineColor: number;
169
+ lineWidth: number;
170
+ lineVisible: boolean;
171
+ maColor: number;
172
+ maWidth: number;
173
+ bandColor: number;
174
+ bandsVisible: boolean;
175
+ }): void;
116
176
  /**
117
- * Configures the MACD pane: enable, fast/slow EMA lengths (slow forced > fast)
118
- * and the signal-line length. Defaults 12/26/9.
177
+ * Configures the MACD pane. source/maKind mirror setOverlays' encodings;
178
+ * colors are packed 0xAARRGGBB where 0 means inherit, and a non-positive
179
+ * width inherits the default stroke.
119
180
  */
120
- setMACD(enabled: boolean, fast: number, slow: number, signal: number): void;
181
+ setMACD(spec: {
182
+ enabled: boolean;
183
+ fast: number;
184
+ slow: number;
185
+ signal: number;
186
+ source: number;
187
+ maKind: number;
188
+ signalMaKind: number;
189
+ lineColor: number;
190
+ lineWidth: number;
191
+ lineVisible: boolean;
192
+ signalColor: number;
193
+ signalWidth: number;
194
+ signalVisible: boolean;
195
+ histVisible: boolean;
196
+ histUpColor: number;
197
+ histUpFadingColor: number;
198
+ histDownColor: number;
199
+ histDownFadingColor: number;
200
+ zeroColor: number;
201
+ zeroVisible: boolean;
202
+ }): void;
121
203
  /**
122
204
  * Replaces the full set of MA/EMA overlay lines drawn on the price pane.
123
205
  * kind: 0=SMA, 1=EMA; source: 0=close,1=open,2=high,3=low,4=hl2,5=hlc3,6=ohlc4;
@@ -136,12 +218,12 @@ export interface ChartHandle {
136
218
  * Configures the session VWAP overlay. `resetOffsetMin` shifts the session
137
219
  * boundary from UTC midnight (minutes); `color` is packed 0xAARRGGBB.
138
220
  */
139
- setVWAP(
140
- enabled: boolean,
141
- resetOffsetMin: number,
142
- color: number,
143
- width: number,
144
- ): void;
221
+ setVWAP(spec: {
222
+ enabled: boolean;
223
+ resetOffsetMin: number;
224
+ color: number;
225
+ width: number;
226
+ }): void;
145
227
  /**
146
228
  * Configures the Bollinger Bands overlay (three price-pane lines + optional
147
229
  * fill between the bands). source/basisKind mirror setOverlays' encodings;
@@ -162,6 +244,31 @@ export interface ChartHandle {
162
244
  fillEnabled: boolean;
163
245
  fillOpacity: number;
164
246
  }): void;
247
+ /**
248
+ * Configures the volume bars under the candles. `heightFrac` is the tallest
249
+ * bar as a fraction of the price pane. The style fields carry an inherit
250
+ * sentinel: a negative number or a transparent color falls back to the
251
+ * matching theme key.
252
+ */
253
+ setVolume(spec: {
254
+ enabled: boolean;
255
+ heightFrac: number;
256
+ opacity: number;
257
+ radiusPx: number;
258
+ upColor: number;
259
+ downColor: number;
260
+ }): void;
261
+ /**
262
+ * Staggered volume-bar collapse: 0 = full height, 1 = all bars flat. Bars fall
263
+ * tallest-first and land together; drive 1 → 0 to reveal them, which plays the
264
+ * cascade in reverse (shortest bar home first).
265
+ *
266
+ * Unlike setMorph, `t` must be **linear** progress — the core eases each bar
267
+ * over its own slice of the timeline, so the curve is applied there. `easing`
268
+ * indexes `linear | ease-in | ease-out | ease-in-out`. setVolume snaps this to
269
+ * match its `enabled`, so it's only needed while animating.
270
+ */
271
+ setVolumeCollapse(t: number, easing: number): void;
165
272
  /**
166
273
  * The continuous data coordinate at pixel (x, y) — not snapped to a candle
167
274
  * slot. Null when there are no candles or the viewport is degenerate. Cheap to
package/src/theme.ts CHANGED
@@ -27,6 +27,7 @@ export const FLOAT_KEYS: Partial<Record<keyof VroomTheme, number>> = {
27
27
  candleRadius: 8, // VROOM_FLOAT_CANDLE_RADIUS_PX
28
28
  volumeRadius: 10, // VROOM_FLOAT_VOLUME_RADIUS_PX
29
29
  lineWidth: 11, // VROOM_FLOAT_LINE_WIDTH_PX
30
+ lineGradientOpacity: 12, // VROOM_FLOAT_LINE_GRADIENT_OPACITY
30
31
  };
31
32
 
32
33
  // Maps each boolean VroomTheme field to its VroomFloatKey index (pushed as 0/1).
package/src/types.ts CHANGED
@@ -12,11 +12,14 @@ export type {
12
12
  VisibleRange,
13
13
  RSIConfig,
14
14
  MASource,
15
+ MAKind,
15
16
  MovingAverageOverlay,
16
17
  VWAPConfig,
17
18
  BollingerBandsConfig,
19
+ VolumeConfig,
18
20
  MACDConfig,
19
21
  ChartType,
22
+ TransitionEasing,
20
23
  PriceLine,
21
24
  PriceLinesStyle,
22
25
  } from '@vroomchart/types';