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.
- package/cpp/VroomChartHostObject.cpp +281 -33
- package/cpp/_core_include/vroom/vroom_chart.h +231 -36
- package/cpp/_core_src/bollinger.h +1 -1
- package/cpp/_core_src/candles.cpp +138 -41
- package/cpp/_core_src/candles.h +11 -1
- package/cpp/_core_src/chart.cpp +91 -40
- package/cpp/_core_src/chart.h +69 -29
- package/cpp/_core_src/chart_facade.cpp +247 -69
- package/cpp/_core_src/drawings.cpp +147 -4
- package/cpp/_core_src/drawings.h +10 -5
- package/cpp/_core_src/gradient.cpp +46 -0
- package/cpp/_core_src/gradient.h +31 -0
- package/cpp/_core_src/labels.cpp +49 -16
- package/cpp/_core_src/labels.h +33 -4
- package/cpp/_core_src/liquidity.cpp +3 -37
- package/cpp/_core_src/ma_overlay.cpp +174 -12
- package/cpp/_core_src/ma_overlay.h +55 -3
- package/cpp/_core_src/macd.cpp +13 -42
- package/cpp/_core_src/macd.h +11 -8
- package/cpp/_core_src/macd_pane.cpp +57 -27
- package/cpp/_core_src/price_line_layout.h +1 -1
- package/cpp/_core_src/rsi.cpp +4 -18
- package/cpp/_core_src/rsi.h +6 -6
- package/cpp/_core_src/rsi_pane.cpp +34 -19
- package/cpp/_core_src/series_ma.cpp +64 -0
- package/cpp/_core_src/series_ma.h +30 -0
- package/cpp/_core_src/style_inherit.h +35 -0
- package/cpp/_core_src/theme.cpp +2 -1
- package/cpp/_core_src/viewport.cpp +36 -12
- package/cpp/_core_src/viewport.h +50 -0
- package/cpp/_core_src/volume.cpp +33 -8
- package/cpp/_core_src/volume.h +12 -1
- package/cpp/_core_src/volume_anim.cpp +32 -0
- package/cpp/_core_src/volume_anim.h +41 -0
- package/lib/index.d.mts +206 -31
- package/lib/index.d.ts +206 -31
- package/lib/index.js +324 -44
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +329 -52
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +100 -17
- package/src/dataTransitions.ts +148 -0
- package/src/easing.ts +40 -0
- package/src/index.ts +9 -0
- package/src/jsi.d.ts +126 -19
- package/src/theme.ts +1 -0
- package/src/types.ts +3 -0
- package/src/useChartCore.ts +273 -30
package/src/useChartCore.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { MutableRefObject } from 'react';
|
|
2
3
|
import type { SkPicture } from '@shopify/react-native-skia';
|
|
3
4
|
|
|
4
5
|
import NativeVroomChart from './NativeVroomChart';
|
|
6
|
+
import type { DataTransition } from './dataTransitions';
|
|
7
|
+
import { classifyTransition, inferStepMs, timeframeWindow } from './dataTransitions';
|
|
8
|
+
import { ease } from './easing';
|
|
5
9
|
import type { ChartHandle } from './jsi.d';
|
|
6
10
|
import { packCandles } from './packCandles';
|
|
7
11
|
import { applyTheme, parseColor } from './theme';
|
|
@@ -14,7 +18,9 @@ import type {
|
|
|
14
18
|
PriceLine,
|
|
15
19
|
PriceLinesStyle,
|
|
16
20
|
RSIConfig,
|
|
21
|
+
TransitionEasing,
|
|
17
22
|
VisibleRange,
|
|
23
|
+
VolumeConfig,
|
|
18
24
|
VroomTheme,
|
|
19
25
|
VWAPConfig,
|
|
20
26
|
} from './types';
|
|
@@ -30,17 +36,49 @@ const MA_SOURCES = [
|
|
|
30
36
|
'ohlc4',
|
|
31
37
|
] as const;
|
|
32
38
|
|
|
39
|
+
// An unset style color marshals as the core's transparent inherit sentinel.
|
|
40
|
+
const inheritColor = (v: string | number | undefined): number =>
|
|
41
|
+
(v != null ? parseColor(v) : null) ?? 0;
|
|
42
|
+
|
|
33
43
|
function overlayToNumeric(o: MovingAverageOverlay) {
|
|
34
44
|
const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
|
|
35
45
|
return {
|
|
36
|
-
kind: o.
|
|
37
|
-
period: o.
|
|
46
|
+
kind: o.maType === 'ema' ? 1 : 0,
|
|
47
|
+
period: o.period,
|
|
38
48
|
source: srcIdx < 0 ? 0 : srcIdx,
|
|
39
49
|
color: (o.color != null ? parseColor(o.color) : null) ?? 0xff2962ff,
|
|
40
50
|
width: o.width ?? 1.5,
|
|
41
51
|
};
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
function rsiToSpec(cfg: RSIConfig | undefined) {
|
|
55
|
+
return {
|
|
56
|
+
enabled: cfg?.enabled ?? false,
|
|
57
|
+
period: cfg?.period ?? 14,
|
|
58
|
+
upperBand: cfg?.upperBand ?? 70,
|
|
59
|
+
lowerBand: cfg?.lowerBand ?? 30,
|
|
60
|
+
maPeriod: cfg?.maPeriod ?? 14,
|
|
61
|
+
maKind: cfg?.maType === 'ema' ? 1 : 0,
|
|
62
|
+
maVisible: cfg?.maVisible ?? true,
|
|
63
|
+
lineColor: inheritColor(cfg?.lineColor),
|
|
64
|
+
lineWidth: cfg?.lineWidth ?? -1,
|
|
65
|
+
lineVisible: cfg?.lineVisible ?? true,
|
|
66
|
+
maColor: inheritColor(cfg?.maColor),
|
|
67
|
+
maWidth: cfg?.maWidth ?? -1,
|
|
68
|
+
bandColor: inheritColor(cfg?.bandColor),
|
|
69
|
+
bandsVisible: cfg?.bandsVisible ?? true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function vwapToSpec(cfg: VWAPConfig | undefined) {
|
|
74
|
+
return {
|
|
75
|
+
enabled: cfg?.enabled ?? false,
|
|
76
|
+
resetOffsetMin: cfg?.resetMinutes ?? 0,
|
|
77
|
+
color: inheritColor(cfg?.color),
|
|
78
|
+
width: cfg?.width ?? -1,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
44
82
|
// Bollinger defaults: blue bands / orange basis, matching the repo palette.
|
|
45
83
|
const DEFAULT_BB_BAND_COLOR = 0xff2962ff;
|
|
46
84
|
const DEFAULT_BB_BASIS_COLOR = 0xffff6d00;
|
|
@@ -52,7 +90,7 @@ function bollingerToSpec(cfg: BollingerBandsConfig | undefined) {
|
|
|
52
90
|
period: cfg?.period ?? 20,
|
|
53
91
|
mult: cfg?.stdDev ?? 2,
|
|
54
92
|
source: srcIdx < 0 ? 0 : srcIdx,
|
|
55
|
-
basisKind: cfg?.
|
|
93
|
+
basisKind: cfg?.maType === 'ema' ? 1 : 0,
|
|
56
94
|
upperColor:
|
|
57
95
|
(cfg?.upperColor != null ? parseColor(cfg.upperColor) : null) ??
|
|
58
96
|
DEFAULT_BB_BAND_COLOR,
|
|
@@ -65,11 +103,51 @@ function bollingerToSpec(cfg: BollingerBandsConfig | undefined) {
|
|
|
65
103
|
(cfg?.lowerColor != null ? parseColor(cfg.lowerColor) : null) ??
|
|
66
104
|
DEFAULT_BB_BAND_COLOR,
|
|
67
105
|
lowerWidth: cfg?.lowerWidth ?? 1,
|
|
68
|
-
fillEnabled: cfg?.
|
|
106
|
+
fillEnabled: cfg?.fillVisible ?? true,
|
|
69
107
|
fillOpacity: cfg?.fillOpacity ?? 0.1,
|
|
70
108
|
};
|
|
71
109
|
}
|
|
72
110
|
|
|
111
|
+
function macdToSpec(cfg: MACDConfig | undefined) {
|
|
112
|
+
const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
|
|
113
|
+
return {
|
|
114
|
+
enabled: cfg?.enabled ?? false,
|
|
115
|
+
fast: cfg?.fast ?? 12,
|
|
116
|
+
slow: cfg?.slow ?? 26,
|
|
117
|
+
signal: cfg?.signal ?? 9,
|
|
118
|
+
source: srcIdx < 0 ? 0 : srcIdx,
|
|
119
|
+
maKind: cfg?.maType === 'sma' ? 0 : 1,
|
|
120
|
+
signalMaKind: cfg?.signalMaType === 'sma' ? 0 : 1,
|
|
121
|
+
lineColor: inheritColor(cfg?.lineColor),
|
|
122
|
+
lineWidth: cfg?.lineWidth ?? -1,
|
|
123
|
+
lineVisible: cfg?.lineVisible ?? true,
|
|
124
|
+
signalColor: inheritColor(cfg?.signalColor),
|
|
125
|
+
signalWidth: cfg?.signalWidth ?? -1,
|
|
126
|
+
signalVisible: cfg?.signalVisible ?? true,
|
|
127
|
+
histVisible: cfg?.histogramVisible ?? true,
|
|
128
|
+
histUpColor: inheritColor(cfg?.histogramUpColor),
|
|
129
|
+
histUpFadingColor: inheritColor(cfg?.histogramUpFadingColor),
|
|
130
|
+
histDownColor: inheritColor(cfg?.histogramDownColor),
|
|
131
|
+
histDownFadingColor: inheritColor(cfg?.histogramDownFadingColor),
|
|
132
|
+
zeroColor: inheritColor(cfg?.zeroLineColor),
|
|
133
|
+
zeroVisible: cfg?.zeroLineVisible ?? true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Unset style fields go down as the core's inherit sentinels (negative float,
|
|
138
|
+
// transparent color) rather than as literal defaults, so the theme keys stay in
|
|
139
|
+
// charge of anything the consumer didn't set.
|
|
140
|
+
function volumeToSpec(cfg: VolumeConfig | undefined) {
|
|
141
|
+
return {
|
|
142
|
+
enabled: cfg?.enabled ?? true,
|
|
143
|
+
heightFrac: cfg?.height ?? -1,
|
|
144
|
+
opacity: cfg?.opacity ?? -1,
|
|
145
|
+
radiusPx: cfg?.radius ?? -1,
|
|
146
|
+
upColor: (cfg?.upColor != null ? parseColor(cfg.upColor) : null) ?? 0,
|
|
147
|
+
downColor: (cfg?.downColor != null ? parseColor(cfg.downColor) : null) ?? 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
73
151
|
// Price-line defaults: a soft red dotted rule with a dark translucent label,
|
|
74
152
|
// close in weight to the current-price indicator it sits beside.
|
|
75
153
|
const DEFAULT_PRICE_LINE_COLOR = 0xffef5350;
|
|
@@ -136,10 +214,38 @@ function ensureInstalled(): void {
|
|
|
136
214
|
installed = true;
|
|
137
215
|
}
|
|
138
216
|
|
|
217
|
+
/** Progress + curve of the staggered volume-bar collapse. See setVolumeCollapse. */
|
|
218
|
+
export type VolumeCollapse = { t: number; easing: number };
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* How a data swap should animate, plus where its frames go. The interval morph
|
|
222
|
+
* has to be started from inside the data effect (it needs the pre-swap capture),
|
|
223
|
+
* but it repaints at 60fps — far too often for React state — so the host passes
|
|
224
|
+
* a sink that writes straight into the picture SharedValue.
|
|
225
|
+
*/
|
|
226
|
+
export type TransitionOptions = {
|
|
227
|
+
/** Identity of the series; a change forces a full view reset. */
|
|
228
|
+
seriesKey?: string;
|
|
229
|
+
/** Duration of the interval morph in ms. 0 snaps. Default 300. */
|
|
230
|
+
transitionMs?: number;
|
|
231
|
+
/** Curve applied to the morph's progress. Default 'ease-in-out'. */
|
|
232
|
+
transitionEasing?: TransitionEasing;
|
|
233
|
+
/** OS reduced-motion preference: skips the capture and snaps. */
|
|
234
|
+
reduceMotion?: boolean;
|
|
235
|
+
/** Receives every morph frame. Without one, data swaps snap. */
|
|
236
|
+
onFrame?: (picture: SkPicture) => void;
|
|
237
|
+
};
|
|
238
|
+
|
|
139
239
|
export type ChartCoreState = {
|
|
140
240
|
handle: ChartHandle | null;
|
|
141
241
|
/** Picture freshly rendered after the latest data/size/range push. */
|
|
142
242
|
picture: SkPicture | null;
|
|
243
|
+
/**
|
|
244
|
+
* The last volume collapse handed to the core, or null before the first push.
|
|
245
|
+
* VroomChart's animation loop owns this — it lives here only so the data effect
|
|
246
|
+
* can restore it, since setVolume snaps the scalar (see below).
|
|
247
|
+
*/
|
|
248
|
+
volumeCollapseRef: MutableRefObject<VolumeCollapse | null>;
|
|
143
249
|
};
|
|
144
250
|
|
|
145
251
|
// Owns a ChartHandle and produces an "initial" picture whenever data, size,
|
|
@@ -158,13 +264,24 @@ export function useChartCore(
|
|
|
158
264
|
movingAverages?: MovingAverageOverlay[],
|
|
159
265
|
vwap?: VWAPConfig,
|
|
160
266
|
bollingerBands?: BollingerBandsConfig,
|
|
267
|
+
volume?: VolumeConfig,
|
|
161
268
|
priceLines?: PriceLinesProp,
|
|
269
|
+
transition?: TransitionOptions,
|
|
162
270
|
): ChartCoreState {
|
|
163
271
|
const handleRef = useRef<ChartHandle | null>(null);
|
|
164
272
|
// Push setDefaultCandleWidth only once (first load): setCandles re-runs on
|
|
165
273
|
// every data change, and the core setter re-frames when candles are present,
|
|
166
274
|
// so re-pushing would snap the view away from the user's pan/zoom.
|
|
167
275
|
const defaultWidthAppliedRef = useRef(false);
|
|
276
|
+
const volumeCollapseRef = useRef<VolumeCollapse | null>(null);
|
|
277
|
+
// What the core currently holds, for classifying the next data change. Keyed
|
|
278
|
+
// by handle so a recreated core is treated as a fresh initial load.
|
|
279
|
+
const prevDataRef = useRef<{
|
|
280
|
+
handle: ChartHandle;
|
|
281
|
+
candles: Candle[];
|
|
282
|
+
seriesKey?: string;
|
|
283
|
+
} | null>(null);
|
|
284
|
+
const intervalMorphRaf = useRef<number | null>(null);
|
|
168
285
|
const [picture, setPicture] = useState<SkPicture | null>(null);
|
|
169
286
|
|
|
170
287
|
if (!handleRef.current && size.width > 0 && size.height > 0) {
|
|
@@ -172,6 +289,56 @@ export function useChartCore(
|
|
|
172
289
|
handleRef.current = globalThis.VroomChartJSI!.create();
|
|
173
290
|
}
|
|
174
291
|
|
|
292
|
+
// Animation config and frame sink in refs, refreshed each render, so changing
|
|
293
|
+
// the duration, curve or callback identity doesn't re-run the data effect
|
|
294
|
+
// below (which would re-push every candle).
|
|
295
|
+
const animRef = useRef<{
|
|
296
|
+
ms: number;
|
|
297
|
+
easing: TransitionEasing | undefined;
|
|
298
|
+
reduceMotion: boolean;
|
|
299
|
+
}>({ ms: 300, easing: undefined, reduceMotion: false });
|
|
300
|
+
animRef.current = {
|
|
301
|
+
ms: Math.max(0, transition?.transitionMs ?? 300),
|
|
302
|
+
easing: transition?.transitionEasing,
|
|
303
|
+
reduceMotion: transition?.reduceMotion ?? false,
|
|
304
|
+
};
|
|
305
|
+
const onFrameRef = useRef(transition?.onFrame);
|
|
306
|
+
onFrameRef.current = transition?.onFrame;
|
|
307
|
+
const seriesKey = transition?.seriesKey;
|
|
308
|
+
|
|
309
|
+
// Stop an in-flight interval morph and land the core on the new candles.
|
|
310
|
+
const endIntervalMorph = useCallback(() => {
|
|
311
|
+
if (intervalMorphRaf.current != null) {
|
|
312
|
+
cancelAnimationFrame(intervalMorphRaf.current);
|
|
313
|
+
intervalMorphRaf.current = null;
|
|
314
|
+
}
|
|
315
|
+
handleRef.current?.setIntervalMorph(1);
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
318
|
+
// Runs the interval morph clock. The core holds the pre-swap geometry (see
|
|
319
|
+
// beginIntervalMorph) and reshapes each candle slot toward its new counterpart.
|
|
320
|
+
const startIntervalMorph = useCallback((h: ChartHandle) => {
|
|
321
|
+
const { ms, easing } = animRef.current;
|
|
322
|
+
const start = performance.now();
|
|
323
|
+
const step = (now: number) => {
|
|
324
|
+
const p = Math.min(1, (now - start) / ms);
|
|
325
|
+
h.setIntervalMorph(p < 1 ? ease(easing, p) : 1);
|
|
326
|
+
const pic = h.render();
|
|
327
|
+
if (pic) onFrameRef.current?.(pic);
|
|
328
|
+
intervalMorphRaf.current = p < 1 ? requestAnimationFrame(step) : null;
|
|
329
|
+
};
|
|
330
|
+
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
331
|
+
}, []);
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
return () => {
|
|
335
|
+
if (intervalMorphRaf.current != null) {
|
|
336
|
+
cancelAnimationFrame(intervalMorphRaf.current);
|
|
337
|
+
intervalMorphRaf.current = null;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}, []);
|
|
341
|
+
|
|
175
342
|
// When no visibleRange is provided, leave the range entirely to the C++
|
|
176
343
|
// side (which defaults to a sensible recent window on first setCandles).
|
|
177
344
|
// Only push setVisibleRange when the caller is actively controlling it,
|
|
@@ -188,6 +355,7 @@ export function useChartCore(
|
|
|
188
355
|
const maKey = movingAverages ? JSON.stringify(movingAverages) : '';
|
|
189
356
|
const vwapKey = vwap ? JSON.stringify(vwap) : '';
|
|
190
357
|
const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : '';
|
|
358
|
+
const volumeKey = volume ? JSON.stringify(volume) : '';
|
|
191
359
|
const priceLinesKey = priceLines ? JSON.stringify(priceLines) : '';
|
|
192
360
|
|
|
193
361
|
useEffect(() => {
|
|
@@ -206,8 +374,89 @@ export function useChartCore(
|
|
|
206
374
|
h.setDefaultCandleWidth(defaultCandleWidth);
|
|
207
375
|
defaultWidthAppliedRef.current = true;
|
|
208
376
|
}
|
|
377
|
+
// How the new candles relate to what the core holds decides what happens to
|
|
378
|
+
// the viewport: a stream leaves it alone, a timeframe switch re-anchors and
|
|
379
|
+
// morphs into it, a different asset resets it.
|
|
380
|
+
let morphing = false;
|
|
209
381
|
if (candles.length > 0) {
|
|
210
|
-
|
|
382
|
+
const prev = prevDataRef.current;
|
|
383
|
+
const freshHandle = prev == null || prev.handle !== h;
|
|
384
|
+
if (freshHandle || prev.candles !== candles || prev.seriesKey !== seriesKey) {
|
|
385
|
+
// A fresh core frames itself (its window starts at 0/0); an explicit
|
|
386
|
+
// visibleRange prop overrides any auto behavior, so treat the change
|
|
387
|
+
// like a stream and let the range application below win.
|
|
388
|
+
const transitionKind: DataTransition = freshHandle
|
|
389
|
+
? 'initial'
|
|
390
|
+
: explicit
|
|
391
|
+
? 'stream'
|
|
392
|
+
: classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
|
|
393
|
+
|
|
394
|
+
// Capture the outgoing view before setCandles re-infers the candle
|
|
395
|
+
// period from the new data.
|
|
396
|
+
let tfArgs: {
|
|
397
|
+
oldWindow: VisibleRange;
|
|
398
|
+
oldStepMs: number;
|
|
399
|
+
oldLastMs: number;
|
|
400
|
+
} | null = null;
|
|
401
|
+
// The pre-swap candle envelope, used to scale-lock the y-axis below.
|
|
402
|
+
let prevEnvelope: { low: number; high: number } | null = null;
|
|
403
|
+
if (transitionKind === 'timeframe' && prev != null) {
|
|
404
|
+
const oldWindow = h.getVisibleRange();
|
|
405
|
+
const oldStepMs = inferStepMs(prev.candles);
|
|
406
|
+
if (oldWindow.endMs > oldWindow.startMs && oldStepMs != null) {
|
|
407
|
+
tfArgs = {
|
|
408
|
+
oldWindow,
|
|
409
|
+
oldStepMs,
|
|
410
|
+
oldLastMs: prev.candles[prev.candles.length - 1].timeMs,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
prevEnvelope = h.getVisiblePriceEnvelope();
|
|
414
|
+
// Capture the outgoing candle geometry, but only when it will actually
|
|
415
|
+
// be animated so a disabled animation costs no snapshot. A switch
|
|
416
|
+
// during a morph restarts from the data the core currently holds.
|
|
417
|
+
morphing =
|
|
418
|
+
animRef.current.ms > 0 &&
|
|
419
|
+
!animRef.current.reduceMotion &&
|
|
420
|
+
onFrameRef.current != null;
|
|
421
|
+
if (morphing) {
|
|
422
|
+
endIntervalMorph();
|
|
423
|
+
h.beginIntervalMorph();
|
|
424
|
+
}
|
|
425
|
+
} else if (transitionKind === 'initial' || transitionKind === 'reset') {
|
|
426
|
+
// Wholesale reframing — the slot pairing no longer holds, so land any
|
|
427
|
+
// in-flight morph rather than reshaping into unrelated data.
|
|
428
|
+
endIntervalMorph();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
h.setCandles(packCandles(candles));
|
|
432
|
+
|
|
433
|
+
if (transitionKind === 'timeframe') {
|
|
434
|
+
const newStepMs = inferStepMs(candles);
|
|
435
|
+
if (tfArgs && newStepMs != null) {
|
|
436
|
+
const w = timeframeWindow(
|
|
437
|
+
tfArgs.oldWindow,
|
|
438
|
+
tfArgs.oldStepMs,
|
|
439
|
+
tfArgs.oldLastMs,
|
|
440
|
+
newStepMs,
|
|
441
|
+
candles[candles.length - 1].timeMs,
|
|
442
|
+
);
|
|
443
|
+
h.setVisibleRange(w.startMs, w.endMs);
|
|
444
|
+
}
|
|
445
|
+
// Scale-lock the y-axis: the same price action re-buckets into a
|
|
446
|
+
// smaller/larger high-low span, so a manual price range is rescaled to
|
|
447
|
+
// keep the candle envelope at the pixel height it just had instead of
|
|
448
|
+
// snapping back to auto-fit. A no-op in auto-y mode, which is already
|
|
449
|
+
// span-invariant.
|
|
450
|
+
if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
|
|
451
|
+
else h.resetPriceScale();
|
|
452
|
+
// Started after the new bounds are in place: the snapshot is in band
|
|
453
|
+
// fractions, so frame 0 still matches the pre-switch pixels exactly.
|
|
454
|
+
if (morphing) startIntervalMorph(h);
|
|
455
|
+
} else if (transitionKind === 'reset') {
|
|
456
|
+
h.resetView();
|
|
457
|
+
}
|
|
458
|
+
prevDataRef.current = { handle: h, candles, seriesKey };
|
|
459
|
+
}
|
|
211
460
|
}
|
|
212
461
|
if (explicit) {
|
|
213
462
|
h.setVisibleRange(startMs, endMs);
|
|
@@ -217,38 +466,32 @@ export function useChartCore(
|
|
|
217
466
|
if (theme) {
|
|
218
467
|
applyTheme(h, theme);
|
|
219
468
|
}
|
|
220
|
-
h.setRSI(
|
|
221
|
-
|
|
222
|
-
rsi?.period ?? 14,
|
|
223
|
-
rsi?.upperBand ?? 70,
|
|
224
|
-
rsi?.lowerBand ?? 30,
|
|
225
|
-
rsi?.maEnabled ?? true,
|
|
226
|
-
rsi?.maPeriod ?? 14,
|
|
227
|
-
);
|
|
228
|
-
h.setMACD(
|
|
229
|
-
macd?.enabled ?? false,
|
|
230
|
-
macd?.fast ?? 12,
|
|
231
|
-
macd?.slow ?? 26,
|
|
232
|
-
macd?.signal ?? 9,
|
|
233
|
-
);
|
|
469
|
+
h.setRSI(rsiToSpec(rsi));
|
|
470
|
+
h.setMACD(macdToSpec(macd));
|
|
234
471
|
h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
|
|
235
|
-
h.setVWAP(
|
|
236
|
-
vwap?.enabled ?? false,
|
|
237
|
-
vwap?.resetMinutes ?? 0,
|
|
238
|
-
(vwap?.color != null ? parseColor(vwap.color) : null) ?? 0xff00bcd4,
|
|
239
|
-
vwap?.width ?? 1.5,
|
|
240
|
-
);
|
|
472
|
+
h.setVWAP(vwapToSpec(vwap));
|
|
241
473
|
h.setBollinger(bollingerToSpec(bollingerBands));
|
|
474
|
+
h.setVolume(volumeToSpec(volume));
|
|
475
|
+
// setVolume snaps the collapse scalar to its `enabled`, which would cut a
|
|
476
|
+
// toggle animation short whenever this effect re-runs (a streaming candle, a
|
|
477
|
+
// resize). Hand the in-flight value back; VroomChart's loop drives it from
|
|
478
|
+
// there.
|
|
479
|
+
const collapse = volumeCollapseRef.current;
|
|
480
|
+
if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
|
|
242
481
|
h.setPriceLines(
|
|
243
482
|
priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES,
|
|
244
483
|
);
|
|
245
484
|
// TODO(rn-parity): mirror the web `liquidity` overlay here (setLiquidity +
|
|
246
485
|
// the VroomBand structs in the JSI handle) — web-only for now.
|
|
247
|
-
|
|
248
|
-
//
|
|
486
|
+
// A just-started morph is already pushing frames straight to the host sink;
|
|
487
|
+
// this snapshot would land on top of them a frame or two later. The morph's
|
|
488
|
+
// frame 0 is pixel-identical to what's on screen, so there's nothing to show
|
|
489
|
+
// in the meantime anyway.
|
|
490
|
+
if (!morphing) setPicture(h.render());
|
|
491
|
+
// theme/rsi/macd/movingAverages/vwap/bollingerBands/volume/priceLines are
|
|
249
492
|
// represented by their *Key deps.
|
|
250
493
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
251
|
-
}, [candles, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, priceLinesKey]);
|
|
494
|
+
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
|
|
252
495
|
|
|
253
|
-
return { handle: handleRef.current, picture };
|
|
496
|
+
return { handle: handleRef.current, picture, volumeCollapseRef };
|
|
254
497
|
}
|