react-native-vroom-chart 0.14.0 → 0.16.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 +289 -2
- package/cpp/_core_include/vroom/vroom_chart.h +221 -4
- package/cpp/_core_src/atr.cpp +67 -0
- package/cpp/_core_src/atr.h +44 -0
- package/cpp/_core_src/atr_pane.cpp +162 -0
- package/cpp/_core_src/atr_pane.h +48 -0
- package/cpp/_core_src/chart.cpp +477 -172
- package/cpp/_core_src/chart.h +173 -1
- package/cpp/_core_src/chart_facade.cpp +317 -23
- package/cpp/_core_src/fair_value_gaps.cpp +76 -0
- package/cpp/_core_src/fair_value_gaps.h +54 -0
- package/cpp/_core_src/footprints.cpp +226 -0
- package/cpp/_core_src/footprints.h +45 -0
- package/cpp/_core_src/footprints_layout.cpp +140 -0
- package/cpp/_core_src/footprints_layout.h +94 -0
- package/cpp/_core_src/fvg_overlay.cpp +262 -0
- package/cpp/_core_src/fvg_overlay.h +43 -0
- package/cpp/_core_src/ichimoku.cpp +65 -0
- package/cpp/_core_src/ichimoku.h +45 -0
- package/cpp/_core_src/labels.cpp +3 -0
- package/cpp/_core_src/line_morph.h +159 -0
- package/cpp/_core_src/ma_overlay.cpp +259 -36
- package/cpp/_core_src/ma_overlay.h +57 -2
- package/cpp/_core_src/macd.cpp +24 -1
- package/cpp/_core_src/macd.h +16 -0
- package/cpp/_core_src/macd_pane.cpp +71 -62
- package/cpp/_core_src/macd_pane.h +13 -1
- package/cpp/_core_src/pane_series.h +169 -0
- package/cpp/_core_src/rsi.cpp +4 -0
- package/cpp/_core_src/rsi.h +7 -0
- package/cpp/_core_src/rsi_pane.cpp +85 -29
- package/cpp/_core_src/rsi_pane.h +11 -1
- package/cpp/_core_src/tip_pulse.h +4 -4
- package/cpp/_core_src/viewport.h +35 -0
- package/lib/index.d.mts +385 -3
- package/lib/index.d.ts +385 -3
- package/lib/index.js +305 -7
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +305 -7
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +108 -7
- package/src/dataTransitions.ts +47 -0
- package/src/index.ts +10 -0
- package/src/jsi.d.ts +136 -1
- package/src/types.ts +10 -0
- package/src/useChartCore.ts +330 -5
package/lib/index.mjs
CHANGED
|
@@ -71,6 +71,13 @@ function classifyTransition(prev, next, seriesKeyChanged) {
|
|
|
71
71
|
const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
|
|
72
72
|
return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
|
|
73
73
|
}
|
|
74
|
+
function classifyStream(prev, next) {
|
|
75
|
+
if (prev.length === 0 || next.length === 0) return "tick";
|
|
76
|
+
return next[next.length - 1].timeMs > prev[prev.length - 1].timeMs ? "append" : "tick";
|
|
77
|
+
}
|
|
78
|
+
function isPinnedToLatest(window, lastMs, stepMs) {
|
|
79
|
+
return window.endMs >= lastMs + stepMs;
|
|
80
|
+
}
|
|
74
81
|
function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
|
|
75
82
|
const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
|
|
76
83
|
const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
|
|
@@ -216,6 +223,7 @@ var MA_SOURCES = [
|
|
|
216
223
|
"hlc3",
|
|
217
224
|
"ohlc4"
|
|
218
225
|
];
|
|
226
|
+
var ATR_SMOOTHINGS = ["rma", "sma", "ema"];
|
|
219
227
|
var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
|
|
220
228
|
function overlayToNumeric(o) {
|
|
221
229
|
const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
|
|
@@ -242,7 +250,8 @@ function rsiToSpec(cfg) {
|
|
|
242
250
|
maColor: inheritColor(cfg?.maColor),
|
|
243
251
|
maWidth: cfg?.maWidth ?? -1,
|
|
244
252
|
bandColor: inheritColor(cfg?.bandColor),
|
|
245
|
-
bandsVisible: cfg?.bandsVisible ?? true
|
|
253
|
+
bandsVisible: cfg?.bandsVisible ?? true,
|
|
254
|
+
extremeFill: cfg?.extremeFill ?? true
|
|
246
255
|
};
|
|
247
256
|
}
|
|
248
257
|
function vwapToSpec(cfg) {
|
|
@@ -273,6 +282,79 @@ function bollingerToSpec(cfg) {
|
|
|
273
282
|
fillOpacity: cfg?.fillOpacity ?? 0.1
|
|
274
283
|
};
|
|
275
284
|
}
|
|
285
|
+
var DEFAULT_ICH_GREEN = 4280723098;
|
|
286
|
+
var DEFAULT_ICH_RED = 4293874512;
|
|
287
|
+
var DEFAULT_ICH_BLUE = 4280902399;
|
|
288
|
+
var DEFAULT_ICH_ORANGE = 4294929664;
|
|
289
|
+
var DEFAULT_ICH_TEAL = 4278238420;
|
|
290
|
+
function ichimokuToSpec(cfg) {
|
|
291
|
+
const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
|
|
292
|
+
return {
|
|
293
|
+
enabled: cfg?.enabled ?? false,
|
|
294
|
+
tenkanPeriod: cfg?.tenkanPeriod ?? 9,
|
|
295
|
+
kijunPeriod: cfg?.kijunPeriod ?? 26,
|
|
296
|
+
senkouBPeriod: cfg?.senkouBPeriod ?? 52,
|
|
297
|
+
displacement: cfg?.displacement ?? 26,
|
|
298
|
+
tenkanColor: color(cfg?.tenkanColor, DEFAULT_ICH_BLUE),
|
|
299
|
+
tenkanWidth: cfg?.tenkanWidth ?? 1,
|
|
300
|
+
tenkanEnabled: cfg?.tenkanVisible ?? true,
|
|
301
|
+
kijunColor: color(cfg?.kijunColor, DEFAULT_ICH_RED),
|
|
302
|
+
kijunWidth: cfg?.kijunWidth ?? 1,
|
|
303
|
+
kijunEnabled: cfg?.kijunVisible ?? true,
|
|
304
|
+
senkouAColor: color(cfg?.senkouAColor, DEFAULT_ICH_GREEN),
|
|
305
|
+
senkouAWidth: cfg?.senkouAWidth ?? 1,
|
|
306
|
+
senkouAEnabled: cfg?.senkouAVisible ?? true,
|
|
307
|
+
senkouBColor: color(cfg?.senkouBColor, DEFAULT_ICH_ORANGE),
|
|
308
|
+
senkouBWidth: cfg?.senkouBWidth ?? 1,
|
|
309
|
+
senkouBEnabled: cfg?.senkouBVisible ?? true,
|
|
310
|
+
chikouColor: color(cfg?.chikouColor, DEFAULT_ICH_TEAL),
|
|
311
|
+
chikouWidth: cfg?.chikouWidth ?? 1,
|
|
312
|
+
chikouEnabled: cfg?.chikouVisible ?? true,
|
|
313
|
+
cloudEnabled: cfg?.cloudVisible ?? true,
|
|
314
|
+
bullishCloudColor: color(cfg?.bullishCloudColor, DEFAULT_ICH_GREEN),
|
|
315
|
+
bearishCloudColor: color(cfg?.bearishCloudColor, DEFAULT_ICH_RED),
|
|
316
|
+
cloudOpacity: cfg?.cloudOpacity ?? 0.15
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
var DEFAULT_FVG_GREEN = 4280723098;
|
|
320
|
+
var DEFAULT_FVG_RED = 4293874512;
|
|
321
|
+
var FVG_FILL_TYPES = ["close", "wick"];
|
|
322
|
+
var FVG_BORDER_STYLES = ["solid", "dotted", "dashed"];
|
|
323
|
+
function fvgToSpec(cfg) {
|
|
324
|
+
const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
|
|
325
|
+
const bullish = color(cfg?.bullishColor, DEFAULT_FVG_GREEN);
|
|
326
|
+
const bearish = color(cfg?.bearishColor, DEFAULT_FVG_RED);
|
|
327
|
+
return {
|
|
328
|
+
enabled: cfg?.enabled ?? false,
|
|
329
|
+
maxBarsBack: cfg?.maxBarsBack ?? 300,
|
|
330
|
+
waitForClose: cfg?.waitForClose ?? false,
|
|
331
|
+
fillType: Math.max(0, FVG_FILL_TYPES.indexOf(cfg?.fillType ?? "close")),
|
|
332
|
+
deleteAfterFill: cfg?.deleteAfterFill ?? true,
|
|
333
|
+
extendBoxes: cfg?.extendBoxes ?? false,
|
|
334
|
+
boxLength: cfg?.boxLength ?? 20,
|
|
335
|
+
bullishColor: bullish,
|
|
336
|
+
bearishColor: bearish,
|
|
337
|
+
opacity: cfg?.opacity ?? 0.15,
|
|
338
|
+
borderEnabled: cfg?.borderVisible ?? true,
|
|
339
|
+
borderStyle: Math.max(
|
|
340
|
+
0,
|
|
341
|
+
FVG_BORDER_STYLES.indexOf(cfg?.borderStyle ?? "solid")
|
|
342
|
+
),
|
|
343
|
+
borderWidth: cfg?.borderWidth ?? 1,
|
|
344
|
+
bullishBorderColor: color(cfg?.bullishBorderColor, bullish),
|
|
345
|
+
bearishBorderColor: color(cfg?.bearishBorderColor, bearish),
|
|
346
|
+
labelsEnabled: cfg?.showLabels ?? true,
|
|
347
|
+
label: cfg?.label ?? "FVG",
|
|
348
|
+
labelDistance: cfg?.labelDistance ?? 10,
|
|
349
|
+
// Alpha 0 is the core's "inherit the border color" sentinel.
|
|
350
|
+
labelColor: color(cfg?.labelColor, 0),
|
|
351
|
+
labelFontSize: cfg?.labelFontSize ?? 0,
|
|
352
|
+
showInverse: cfg?.showInverse ?? false,
|
|
353
|
+
inverseBullishColor: color(cfg?.inverseBullishColor, bullish),
|
|
354
|
+
inverseBearishColor: color(cfg?.inverseBearishColor, bearish),
|
|
355
|
+
inverseLabel: cfg?.inverseLabel ?? "iFVG"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
276
358
|
function macdToSpec(cfg) {
|
|
277
359
|
const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
|
|
278
360
|
return {
|
|
@@ -298,6 +380,15 @@ function macdToSpec(cfg) {
|
|
|
298
380
|
zeroVisible: cfg?.zeroLineVisible ?? true
|
|
299
381
|
};
|
|
300
382
|
}
|
|
383
|
+
function atrToSpec(cfg) {
|
|
384
|
+
return {
|
|
385
|
+
enabled: cfg?.enabled ?? false,
|
|
386
|
+
period: cfg?.period ?? 14,
|
|
387
|
+
smoothing: Math.max(0, ATR_SMOOTHINGS.indexOf(cfg?.smoothing ?? "rma")),
|
|
388
|
+
lineColor: inheritColor(cfg?.lineColor),
|
|
389
|
+
lineWidth: cfg?.lineWidth ?? -1
|
|
390
|
+
};
|
|
391
|
+
}
|
|
301
392
|
function volumeToSpec(cfg) {
|
|
302
393
|
return {
|
|
303
394
|
enabled: cfg?.enabled ?? true,
|
|
@@ -335,6 +426,29 @@ function priceLinesToSpec(cfg) {
|
|
|
335
426
|
};
|
|
336
427
|
}
|
|
337
428
|
var EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
|
|
429
|
+
var DEFAULT_FOOTPRINT_HOVER_BOOST = 1.25;
|
|
430
|
+
var FOOTPRINT_BUY = 0;
|
|
431
|
+
var FOOTPRINT_SELL = 1;
|
|
432
|
+
var UNBUCKETABLE_MS = -864e13;
|
|
433
|
+
function footprintsToSpec(cfg) {
|
|
434
|
+
return {
|
|
435
|
+
// Index alignment is load-bearing: the core reports hits as indices into this
|
|
436
|
+
// array and the gesture layer maps them straight back to the consumer's
|
|
437
|
+
// `footprints`. So a non-finite time — which can't be bucketed and would
|
|
438
|
+
// reach the native side as a garbage int64 — is neutralized *in place* rather
|
|
439
|
+
// than filtered out, which would shift every index after it onto the wrong
|
|
440
|
+
// trade.
|
|
441
|
+
prints: cfg.prints.map((f) => ({
|
|
442
|
+
timeMs: Number.isFinite(f.timeMs) ? f.timeMs : UNBUCKETABLE_MS,
|
|
443
|
+
side: f.side === "sell" ? FOOTPRINT_SELL : FOOTPRINT_BUY
|
|
444
|
+
})),
|
|
445
|
+
radiusPx: cfg.style?.radius ?? 0,
|
|
446
|
+
gapPx: cfg.style?.gap ?? 0,
|
|
447
|
+
marginPx: cfg.style?.margin ?? 0,
|
|
448
|
+
hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_FOOTPRINT_HOVER_BOOST
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
var EMPTY_FOOTPRINTS = footprintsToSpec({ prints: [] });
|
|
338
452
|
var installed = false;
|
|
339
453
|
function ensureInstalled() {
|
|
340
454
|
if (installed) return;
|
|
@@ -345,23 +459,37 @@ function ensureInstalled() {
|
|
|
345
459
|
}
|
|
346
460
|
installed = true;
|
|
347
461
|
}
|
|
348
|
-
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
|
|
462
|
+
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition) {
|
|
349
463
|
const handleRef = useRef(null);
|
|
350
464
|
const defaultWidthAppliedRef = useRef(false);
|
|
351
465
|
const volumeCollapseRef = useRef(null);
|
|
352
466
|
const prevDataRef = useRef(null);
|
|
353
467
|
const intervalMorphRaf = useRef(null);
|
|
468
|
+
const streamRaf = useRef(null);
|
|
469
|
+
const streamWindowRef = useRef(null);
|
|
470
|
+
const streamMorphRef = useRef(false);
|
|
354
471
|
const [picture, setPicture] = useState(null);
|
|
355
472
|
if (!handleRef.current && size.width > 0 && size.height > 0) {
|
|
356
473
|
ensureInstalled();
|
|
357
474
|
handleRef.current = globalThis.VroomChartJSI.create();
|
|
358
475
|
}
|
|
359
|
-
const animRef = useRef({
|
|
476
|
+
const animRef = useRef({
|
|
477
|
+
ms: 300,
|
|
478
|
+
easing: void 0,
|
|
479
|
+
reduceMotion: false,
|
|
480
|
+
interval: "transform",
|
|
481
|
+
stream: "none",
|
|
482
|
+
streamMs: 150
|
|
483
|
+
});
|
|
360
484
|
animRef.current = {
|
|
361
485
|
ms: Math.max(0, transition?.transitionMs ?? 300),
|
|
362
486
|
easing: transition?.transitionEasing,
|
|
363
487
|
reduceMotion: transition?.reduceMotion ?? false,
|
|
364
|
-
interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
|
|
488
|
+
interval: transition?.intervalTransition === "fade" ? "fade" : "transform",
|
|
489
|
+
stream: transition?.streamTransition === "transform" ? "transform" : "none",
|
|
490
|
+
// Shorter than transitionMs by default: ticks can land faster than a 300ms
|
|
491
|
+
// curve, and every one that does interrupts the last.
|
|
492
|
+
streamMs: Math.max(0, transition?.streamTransitionMs ?? 150)
|
|
365
493
|
};
|
|
366
494
|
const onFrameRef = useRef(transition?.onFrame);
|
|
367
495
|
onFrameRef.current = transition?.onFrame;
|
|
@@ -385,12 +513,70 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
385
513
|
};
|
|
386
514
|
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
387
515
|
}, []);
|
|
516
|
+
const settleStream = useCallback((keepMorph = false) => {
|
|
517
|
+
if (streamRaf.current != null) {
|
|
518
|
+
cancelAnimationFrame(streamRaf.current);
|
|
519
|
+
streamRaf.current = null;
|
|
520
|
+
}
|
|
521
|
+
const h = handleRef.current;
|
|
522
|
+
const target = streamWindowRef.current;
|
|
523
|
+
streamWindowRef.current = null;
|
|
524
|
+
if (target) h?.setVisibleRange(target.startMs, target.endMs);
|
|
525
|
+
if (streamMorphRef.current && !keepMorph) {
|
|
526
|
+
streamMorphRef.current = false;
|
|
527
|
+
h?.setIntervalMorph(1);
|
|
528
|
+
}
|
|
529
|
+
}, []);
|
|
530
|
+
const startStreamAnim = useCallback(
|
|
531
|
+
(h, morphing, window) => {
|
|
532
|
+
const { streamMs, easing } = animRef.current;
|
|
533
|
+
let from = window ? h.getVisibleRange() : null;
|
|
534
|
+
let applied = null;
|
|
535
|
+
streamWindowRef.current = window;
|
|
536
|
+
streamMorphRef.current = morphing;
|
|
537
|
+
const start = performance.now();
|
|
538
|
+
const step = (now) => {
|
|
539
|
+
if (from && applied) {
|
|
540
|
+
const now_w = h.getVisibleRange();
|
|
541
|
+
if (now_w.startMs !== applied.startMs || now_w.endMs !== applied.endMs) {
|
|
542
|
+
from = null;
|
|
543
|
+
streamWindowRef.current = null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const p = Math.min(1, (now - start) / streamMs);
|
|
547
|
+
const e = p < 1 ? ease(easing, p) : 1;
|
|
548
|
+
if (morphing) h.setIntervalMorph(e);
|
|
549
|
+
if (from && window) {
|
|
550
|
+
applied = {
|
|
551
|
+
startMs: Math.round(from.startMs + (window.startMs - from.startMs) * e),
|
|
552
|
+
endMs: Math.round(from.endMs + (window.endMs - from.endMs) * e)
|
|
553
|
+
};
|
|
554
|
+
h.setVisibleRange(applied.startMs, applied.endMs);
|
|
555
|
+
}
|
|
556
|
+
const pic = h.render();
|
|
557
|
+
if (pic) onFrameRef.current?.(pic);
|
|
558
|
+
if (p < 1) {
|
|
559
|
+
streamRaf.current = requestAnimationFrame(step);
|
|
560
|
+
} else {
|
|
561
|
+
streamRaf.current = null;
|
|
562
|
+
streamWindowRef.current = null;
|
|
563
|
+
streamMorphRef.current = false;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
streamRaf.current = requestAnimationFrame(step);
|
|
567
|
+
},
|
|
568
|
+
[]
|
|
569
|
+
);
|
|
388
570
|
useEffect(() => {
|
|
389
571
|
return () => {
|
|
390
572
|
if (intervalMorphRaf.current != null) {
|
|
391
573
|
cancelAnimationFrame(intervalMorphRaf.current);
|
|
392
574
|
intervalMorphRaf.current = null;
|
|
393
575
|
}
|
|
576
|
+
if (streamRaf.current != null) {
|
|
577
|
+
cancelAnimationFrame(streamRaf.current);
|
|
578
|
+
streamRaf.current = null;
|
|
579
|
+
}
|
|
394
580
|
};
|
|
395
581
|
}, []);
|
|
396
582
|
const explicit = visibleRange != null;
|
|
@@ -399,15 +585,20 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
399
585
|
const themeKey = theme ? JSON.stringify(theme) : "";
|
|
400
586
|
const rsiKey = rsi ? JSON.stringify(rsi) : "";
|
|
401
587
|
const macdKey = macd ? JSON.stringify(macd) : "";
|
|
588
|
+
const atrKey = atr ? JSON.stringify(atr) : "";
|
|
402
589
|
const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
|
|
403
590
|
const vwapKey = vwap ? JSON.stringify(vwap) : "";
|
|
404
591
|
const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
|
|
592
|
+
const ichimokuKey = ichimoku ? JSON.stringify(ichimoku) : "";
|
|
593
|
+
const fvgKey = fairValueGaps ? JSON.stringify(fairValueGaps) : "";
|
|
405
594
|
const volumeKey = volume ? JSON.stringify(volume) : "";
|
|
406
595
|
const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
|
|
596
|
+
const footprintsKey = footprints ? JSON.stringify(footprints) : "";
|
|
407
597
|
useEffect(() => {
|
|
408
598
|
const h = handleRef.current;
|
|
409
599
|
if (!h) return;
|
|
410
600
|
h.setSize(size.width, size.height, size.pxRatio ?? 1);
|
|
601
|
+
h.setIchimoku(ichimokuToSpec(ichimoku));
|
|
411
602
|
if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
|
|
412
603
|
h.setDefaultCandleWidth(defaultCandleWidth);
|
|
413
604
|
defaultWidthAppliedRef.current = true;
|
|
@@ -420,6 +611,36 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
420
611
|
const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
|
|
421
612
|
let tfArgs = null;
|
|
422
613
|
let prevEnvelope = null;
|
|
614
|
+
let stream = null;
|
|
615
|
+
if (transitionKind === "stream" && prev != null && !explicit) {
|
|
616
|
+
const { stream: mode, streamMs, reduceMotion } = animRef.current;
|
|
617
|
+
const stepMs = inferStepMs(candles);
|
|
618
|
+
if (mode === "transform" && streamMs > 0 && stepMs != null && !reduceMotion && onFrameRef.current != null) {
|
|
619
|
+
const lastMs = candles[candles.length - 1].timeMs;
|
|
620
|
+
const prevLastMs = prev.candles[prev.candles.length - 1].timeMs;
|
|
621
|
+
if (classifyStream(prev.candles, candles) === "append") {
|
|
622
|
+
const w = h.getVisibleRange();
|
|
623
|
+
const prevStepMs = inferStepMs(prev.candles) ?? stepMs;
|
|
624
|
+
if (isPinnedToLatest(w, prevLastMs, prevStepMs)) {
|
|
625
|
+
const by = lastMs - prevLastMs;
|
|
626
|
+
stream = {
|
|
627
|
+
morph: false,
|
|
628
|
+
window: { startMs: w.startMs + by, endMs: w.endMs + by }
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
stream = { morph: true, window: null };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (stream?.morph) {
|
|
636
|
+
settleStream(true);
|
|
637
|
+
h.beginStreamMorph();
|
|
638
|
+
} else {
|
|
639
|
+
settleStream();
|
|
640
|
+
}
|
|
641
|
+
} else if (transitionKind === "stream") {
|
|
642
|
+
settleStream();
|
|
643
|
+
}
|
|
423
644
|
if (transitionKind === "timeframe" && prev != null) {
|
|
424
645
|
const oldWindow = h.getVisibleRange();
|
|
425
646
|
const oldStepMs = inferStepMs(prev.candles);
|
|
@@ -455,6 +676,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
455
676
|
if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
|
|
456
677
|
else h.resetPriceScale();
|
|
457
678
|
if (morphing) startIntervalMorph(h);
|
|
679
|
+
} else if (stream) {
|
|
680
|
+
startStreamAnim(h, stream.morph, stream.window);
|
|
458
681
|
} else if (transitionKind === "reset") {
|
|
459
682
|
h.resetView();
|
|
460
683
|
}
|
|
@@ -472,21 +695,27 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
472
695
|
}
|
|
473
696
|
h.setRSI(rsiToSpec(rsi));
|
|
474
697
|
h.setMACD(macdToSpec(macd));
|
|
698
|
+
h.setATR(atrToSpec(atr));
|
|
475
699
|
h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
|
|
476
700
|
h.setVWAP(vwapToSpec(vwap));
|
|
477
701
|
h.setBollinger(bollingerToSpec(bollingerBands));
|
|
702
|
+
h.setFairValueGaps(fvgToSpec(fairValueGaps));
|
|
478
703
|
h.setVolume(volumeToSpec(volume));
|
|
479
704
|
const collapse = volumeCollapseRef.current;
|
|
480
705
|
if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
|
|
481
706
|
h.setPriceLines(
|
|
482
707
|
priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
|
|
483
708
|
);
|
|
709
|
+
h.setFootprints(
|
|
710
|
+
footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
|
|
711
|
+
);
|
|
484
712
|
if (!morphing) setPicture(h.render());
|
|
485
|
-
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
|
|
713
|
+
}, [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]);
|
|
486
714
|
return { handle: handleRef.current, picture, volumeCollapseRef };
|
|
487
715
|
}
|
|
488
716
|
|
|
489
717
|
// src/VroomChart.tsx
|
|
718
|
+
var FOOTPRINT_SELL2 = 1;
|
|
490
719
|
function isSkImage(frame) {
|
|
491
720
|
return typeof frame.getImageInfo === "function";
|
|
492
721
|
}
|
|
@@ -503,12 +732,17 @@ function VroomChart(props) {
|
|
|
503
732
|
transitionMs,
|
|
504
733
|
transitionEasing,
|
|
505
734
|
intervalTransition,
|
|
735
|
+
streamTransition,
|
|
736
|
+
streamTransitionMs,
|
|
506
737
|
theme,
|
|
507
738
|
rsi,
|
|
508
739
|
macd,
|
|
740
|
+
atr,
|
|
509
741
|
movingAverages,
|
|
510
742
|
vwap,
|
|
511
743
|
bollingerBands,
|
|
744
|
+
ichimoku,
|
|
745
|
+
fairValueGaps,
|
|
512
746
|
volume,
|
|
513
747
|
crosshairOffset = 40,
|
|
514
748
|
onCrosshair,
|
|
@@ -517,7 +751,10 @@ function VroomChart(props) {
|
|
|
517
751
|
priceLinesStyle,
|
|
518
752
|
onPriceLineDrag,
|
|
519
753
|
onPriceLineDragEnd,
|
|
520
|
-
onPriceLineClose
|
|
754
|
+
onPriceLineClose,
|
|
755
|
+
footprints,
|
|
756
|
+
footprintsStyle,
|
|
757
|
+
onFootprint
|
|
521
758
|
} = props;
|
|
522
759
|
const [measured, setMeasured] = useState2({ width: 0, height: 0 });
|
|
523
760
|
const width = widthProp ?? measured.width;
|
|
@@ -537,6 +774,10 @@ function VroomChart(props) {
|
|
|
537
774
|
} : void 0,
|
|
538
775
|
[priceLines, priceLinesStyle, onPriceLineClose]
|
|
539
776
|
);
|
|
777
|
+
const footprintsProp = useMemo(
|
|
778
|
+
() => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
|
|
779
|
+
[footprints, footprintsStyle]
|
|
780
|
+
);
|
|
540
781
|
const emptyPicture = useMemo(() => {
|
|
541
782
|
const rec = Skia.PictureRecorder();
|
|
542
783
|
rec.beginRecording(Skia.XYWHRect(0, 0, 1, 1));
|
|
@@ -580,14 +821,28 @@ function VroomChart(props) {
|
|
|
580
821
|
theme,
|
|
581
822
|
rsi,
|
|
582
823
|
macd,
|
|
824
|
+
atr,
|
|
583
825
|
movingAverages,
|
|
584
826
|
vwap,
|
|
585
827
|
bollingerBands,
|
|
828
|
+
ichimoku,
|
|
829
|
+
fairValueGaps,
|
|
586
830
|
volume,
|
|
587
831
|
priceLinesProp,
|
|
588
|
-
|
|
832
|
+
footprintsProp,
|
|
833
|
+
{
|
|
834
|
+
seriesKey,
|
|
835
|
+
transitionMs,
|
|
836
|
+
transitionEasing,
|
|
837
|
+
intervalTransition,
|
|
838
|
+
streamTransition,
|
|
839
|
+
streamTransitionMs,
|
|
840
|
+
reduceMotion,
|
|
841
|
+
onFrame
|
|
842
|
+
}
|
|
589
843
|
);
|
|
590
844
|
const crosshairActive = useRef2(false);
|
|
845
|
+
const footprintActive = useRef2(false);
|
|
591
846
|
const lastCrosshairTime = useRef2(null);
|
|
592
847
|
const decayRaf = useRef2(null);
|
|
593
848
|
const cancelDecay = useCallback2(() => {
|
|
@@ -847,6 +1102,24 @@ function VroomChart(props) {
|
|
|
847
1102
|
null
|
|
848
1103
|
);
|
|
849
1104
|
const panMode = useRef2("chart");
|
|
1105
|
+
const dismissFootprint = (redraw = true) => {
|
|
1106
|
+
if (!handle || !footprintActive.current) return;
|
|
1107
|
+
footprintActive.current = false;
|
|
1108
|
+
handle.setFootprintHover(0, -1);
|
|
1109
|
+
if (redraw) {
|
|
1110
|
+
const frame = handle.render();
|
|
1111
|
+
if (frame) applyFrame(frame);
|
|
1112
|
+
}
|
|
1113
|
+
onFootprint?.({
|
|
1114
|
+
active: false,
|
|
1115
|
+
reason: "hide",
|
|
1116
|
+
side: null,
|
|
1117
|
+
timeMs: null,
|
|
1118
|
+
footprints: [],
|
|
1119
|
+
badge: null,
|
|
1120
|
+
pane: null
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
850
1123
|
const pan = Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
|
|
851
1124
|
cancelDecay();
|
|
852
1125
|
panMode.current = hitAxis(e.x, e.y);
|
|
@@ -861,6 +1134,7 @@ function VroomChart(props) {
|
|
|
861
1134
|
if (p) applyFrame(p);
|
|
862
1135
|
}
|
|
863
1136
|
}
|
|
1137
|
+
if (panMode.current !== "price-line") dismissFootprint();
|
|
864
1138
|
}).onChange((e) => {
|
|
865
1139
|
if (!handle) return;
|
|
866
1140
|
let next = null;
|
|
@@ -943,6 +1217,7 @@ function VroomChart(props) {
|
|
|
943
1217
|
});
|
|
944
1218
|
const pinch = Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
|
|
945
1219
|
if (e.numberOfTouches < 2) return;
|
|
1220
|
+
dismissFootprint();
|
|
946
1221
|
const [a, b] = e.allTouches;
|
|
947
1222
|
const spanX = Math.abs(a.x - b.x);
|
|
948
1223
|
const spanY = Math.abs(a.y - b.y);
|
|
@@ -983,6 +1258,7 @@ function VroomChart(props) {
|
|
|
983
1258
|
if (hitAxis(e.x, e.y) !== "chart") return;
|
|
984
1259
|
if (hitPriceLine(e.x, e.y)) return;
|
|
985
1260
|
cancelDecay();
|
|
1261
|
+
dismissFootprint(false);
|
|
986
1262
|
crosshairActive.current = true;
|
|
987
1263
|
const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
|
|
988
1264
|
if (ch) applyFrame(ch);
|
|
@@ -999,6 +1275,28 @@ function VroomChart(props) {
|
|
|
999
1275
|
});
|
|
1000
1276
|
const tap = Gesture.Tap().runOnJS(true).onStart((e) => {
|
|
1001
1277
|
if (!handle) return;
|
|
1278
|
+
const prints = footprints ?? [];
|
|
1279
|
+
const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
|
|
1280
|
+
if (fp) {
|
|
1281
|
+
handle.setFootprintHover(fp.candleTimeMs, fp.side);
|
|
1282
|
+
const frame = handle.render();
|
|
1283
|
+
if (frame) applyFrame(frame);
|
|
1284
|
+
const wasActive = footprintActive.current;
|
|
1285
|
+
footprintActive.current = true;
|
|
1286
|
+
onFootprint?.({
|
|
1287
|
+
active: true,
|
|
1288
|
+
reason: wasActive ? "move" : "show",
|
|
1289
|
+
side: fp.side === FOOTPRINT_SELL2 ? "sell" : "buy",
|
|
1290
|
+
timeMs: fp.candleTimeMs,
|
|
1291
|
+
// The core reports indices into the array we last pushed, which is this
|
|
1292
|
+
// same prop — so this rejoins each badge to the consumer's own objects.
|
|
1293
|
+
footprints: fp.indices.map((i) => prints[i]).filter((f) => f != null),
|
|
1294
|
+
badge: { x: fp.x, y: fp.y, radius: fp.radius },
|
|
1295
|
+
pane: fp.pane
|
|
1296
|
+
});
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
dismissFootprint();
|
|
1002
1300
|
const pl = hitPriceLine(e.x, e.y);
|
|
1003
1301
|
if (pl && pl.part === 1) {
|
|
1004
1302
|
onPriceLineClose?.(pl.line.id);
|