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.js
CHANGED
|
@@ -99,6 +99,13 @@ function classifyTransition(prev, next, seriesKeyChanged) {
|
|
|
99
99
|
const endsTogether = Math.abs(nextEnd - prevEnd) <= MAX_END_DRIFT_STEPS * Math.max(prevStep, nextStep);
|
|
100
100
|
return closeRatio <= MAX_SAME_ASSET_CLOSE_RATIO && endsTogether ? "timeframe" : "reset";
|
|
101
101
|
}
|
|
102
|
+
function classifyStream(prev, next) {
|
|
103
|
+
if (prev.length === 0 || next.length === 0) return "tick";
|
|
104
|
+
return next[next.length - 1].timeMs > prev[prev.length - 1].timeMs ? "append" : "tick";
|
|
105
|
+
}
|
|
106
|
+
function isPinnedToLatest(window, lastMs, stepMs) {
|
|
107
|
+
return window.endMs >= lastMs + stepMs;
|
|
108
|
+
}
|
|
102
109
|
function timeframeWindow(oldWindow, oldStepMs, oldLastMs, newStepMs, newLastMs) {
|
|
103
110
|
const slots = (oldWindow.endMs - oldWindow.startMs) / oldStepMs;
|
|
104
111
|
const offsetRaw = (oldWindow.endMs - oldLastMs) / oldStepMs;
|
|
@@ -244,6 +251,7 @@ var MA_SOURCES = [
|
|
|
244
251
|
"hlc3",
|
|
245
252
|
"ohlc4"
|
|
246
253
|
];
|
|
254
|
+
var ATR_SMOOTHINGS = ["rma", "sma", "ema"];
|
|
247
255
|
var inheritColor = (v) => (v != null ? parseColor(v) : null) ?? 0;
|
|
248
256
|
function overlayToNumeric(o) {
|
|
249
257
|
const srcIdx = o.source ? MA_SOURCES.indexOf(o.source) : 0;
|
|
@@ -270,7 +278,8 @@ function rsiToSpec(cfg) {
|
|
|
270
278
|
maColor: inheritColor(cfg?.maColor),
|
|
271
279
|
maWidth: cfg?.maWidth ?? -1,
|
|
272
280
|
bandColor: inheritColor(cfg?.bandColor),
|
|
273
|
-
bandsVisible: cfg?.bandsVisible ?? true
|
|
281
|
+
bandsVisible: cfg?.bandsVisible ?? true,
|
|
282
|
+
extremeFill: cfg?.extremeFill ?? true
|
|
274
283
|
};
|
|
275
284
|
}
|
|
276
285
|
function vwapToSpec(cfg) {
|
|
@@ -301,6 +310,79 @@ function bollingerToSpec(cfg) {
|
|
|
301
310
|
fillOpacity: cfg?.fillOpacity ?? 0.1
|
|
302
311
|
};
|
|
303
312
|
}
|
|
313
|
+
var DEFAULT_ICH_GREEN = 4280723098;
|
|
314
|
+
var DEFAULT_ICH_RED = 4293874512;
|
|
315
|
+
var DEFAULT_ICH_BLUE = 4280902399;
|
|
316
|
+
var DEFAULT_ICH_ORANGE = 4294929664;
|
|
317
|
+
var DEFAULT_ICH_TEAL = 4278238420;
|
|
318
|
+
function ichimokuToSpec(cfg) {
|
|
319
|
+
const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
|
|
320
|
+
return {
|
|
321
|
+
enabled: cfg?.enabled ?? false,
|
|
322
|
+
tenkanPeriod: cfg?.tenkanPeriod ?? 9,
|
|
323
|
+
kijunPeriod: cfg?.kijunPeriod ?? 26,
|
|
324
|
+
senkouBPeriod: cfg?.senkouBPeriod ?? 52,
|
|
325
|
+
displacement: cfg?.displacement ?? 26,
|
|
326
|
+
tenkanColor: color(cfg?.tenkanColor, DEFAULT_ICH_BLUE),
|
|
327
|
+
tenkanWidth: cfg?.tenkanWidth ?? 1,
|
|
328
|
+
tenkanEnabled: cfg?.tenkanVisible ?? true,
|
|
329
|
+
kijunColor: color(cfg?.kijunColor, DEFAULT_ICH_RED),
|
|
330
|
+
kijunWidth: cfg?.kijunWidth ?? 1,
|
|
331
|
+
kijunEnabled: cfg?.kijunVisible ?? true,
|
|
332
|
+
senkouAColor: color(cfg?.senkouAColor, DEFAULT_ICH_GREEN),
|
|
333
|
+
senkouAWidth: cfg?.senkouAWidth ?? 1,
|
|
334
|
+
senkouAEnabled: cfg?.senkouAVisible ?? true,
|
|
335
|
+
senkouBColor: color(cfg?.senkouBColor, DEFAULT_ICH_ORANGE),
|
|
336
|
+
senkouBWidth: cfg?.senkouBWidth ?? 1,
|
|
337
|
+
senkouBEnabled: cfg?.senkouBVisible ?? true,
|
|
338
|
+
chikouColor: color(cfg?.chikouColor, DEFAULT_ICH_TEAL),
|
|
339
|
+
chikouWidth: cfg?.chikouWidth ?? 1,
|
|
340
|
+
chikouEnabled: cfg?.chikouVisible ?? true,
|
|
341
|
+
cloudEnabled: cfg?.cloudVisible ?? true,
|
|
342
|
+
bullishCloudColor: color(cfg?.bullishCloudColor, DEFAULT_ICH_GREEN),
|
|
343
|
+
bearishCloudColor: color(cfg?.bearishCloudColor, DEFAULT_ICH_RED),
|
|
344
|
+
cloudOpacity: cfg?.cloudOpacity ?? 0.15
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
var DEFAULT_FVG_GREEN = 4280723098;
|
|
348
|
+
var DEFAULT_FVG_RED = 4293874512;
|
|
349
|
+
var FVG_FILL_TYPES = ["close", "wick"];
|
|
350
|
+
var FVG_BORDER_STYLES = ["solid", "dotted", "dashed"];
|
|
351
|
+
function fvgToSpec(cfg) {
|
|
352
|
+
const color = (v, fallback) => (v != null ? parseColor(v) : null) ?? fallback;
|
|
353
|
+
const bullish = color(cfg?.bullishColor, DEFAULT_FVG_GREEN);
|
|
354
|
+
const bearish = color(cfg?.bearishColor, DEFAULT_FVG_RED);
|
|
355
|
+
return {
|
|
356
|
+
enabled: cfg?.enabled ?? false,
|
|
357
|
+
maxBarsBack: cfg?.maxBarsBack ?? 300,
|
|
358
|
+
waitForClose: cfg?.waitForClose ?? false,
|
|
359
|
+
fillType: Math.max(0, FVG_FILL_TYPES.indexOf(cfg?.fillType ?? "close")),
|
|
360
|
+
deleteAfterFill: cfg?.deleteAfterFill ?? true,
|
|
361
|
+
extendBoxes: cfg?.extendBoxes ?? false,
|
|
362
|
+
boxLength: cfg?.boxLength ?? 20,
|
|
363
|
+
bullishColor: bullish,
|
|
364
|
+
bearishColor: bearish,
|
|
365
|
+
opacity: cfg?.opacity ?? 0.15,
|
|
366
|
+
borderEnabled: cfg?.borderVisible ?? true,
|
|
367
|
+
borderStyle: Math.max(
|
|
368
|
+
0,
|
|
369
|
+
FVG_BORDER_STYLES.indexOf(cfg?.borderStyle ?? "solid")
|
|
370
|
+
),
|
|
371
|
+
borderWidth: cfg?.borderWidth ?? 1,
|
|
372
|
+
bullishBorderColor: color(cfg?.bullishBorderColor, bullish),
|
|
373
|
+
bearishBorderColor: color(cfg?.bearishBorderColor, bearish),
|
|
374
|
+
labelsEnabled: cfg?.showLabels ?? true,
|
|
375
|
+
label: cfg?.label ?? "FVG",
|
|
376
|
+
labelDistance: cfg?.labelDistance ?? 10,
|
|
377
|
+
// Alpha 0 is the core's "inherit the border color" sentinel.
|
|
378
|
+
labelColor: color(cfg?.labelColor, 0),
|
|
379
|
+
labelFontSize: cfg?.labelFontSize ?? 0,
|
|
380
|
+
showInverse: cfg?.showInverse ?? false,
|
|
381
|
+
inverseBullishColor: color(cfg?.inverseBullishColor, bullish),
|
|
382
|
+
inverseBearishColor: color(cfg?.inverseBearishColor, bearish),
|
|
383
|
+
inverseLabel: cfg?.inverseLabel ?? "iFVG"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
304
386
|
function macdToSpec(cfg) {
|
|
305
387
|
const srcIdx = cfg?.source ? MA_SOURCES.indexOf(cfg.source) : 0;
|
|
306
388
|
return {
|
|
@@ -326,6 +408,15 @@ function macdToSpec(cfg) {
|
|
|
326
408
|
zeroVisible: cfg?.zeroLineVisible ?? true
|
|
327
409
|
};
|
|
328
410
|
}
|
|
411
|
+
function atrToSpec(cfg) {
|
|
412
|
+
return {
|
|
413
|
+
enabled: cfg?.enabled ?? false,
|
|
414
|
+
period: cfg?.period ?? 14,
|
|
415
|
+
smoothing: Math.max(0, ATR_SMOOTHINGS.indexOf(cfg?.smoothing ?? "rma")),
|
|
416
|
+
lineColor: inheritColor(cfg?.lineColor),
|
|
417
|
+
lineWidth: cfg?.lineWidth ?? -1
|
|
418
|
+
};
|
|
419
|
+
}
|
|
329
420
|
function volumeToSpec(cfg) {
|
|
330
421
|
return {
|
|
331
422
|
enabled: cfg?.enabled ?? true,
|
|
@@ -363,6 +454,29 @@ function priceLinesToSpec(cfg) {
|
|
|
363
454
|
};
|
|
364
455
|
}
|
|
365
456
|
var EMPTY_PRICE_LINES = priceLinesToSpec({ lines: [], hasCloseHandler: false });
|
|
457
|
+
var DEFAULT_FOOTPRINT_HOVER_BOOST = 1.25;
|
|
458
|
+
var FOOTPRINT_BUY = 0;
|
|
459
|
+
var FOOTPRINT_SELL = 1;
|
|
460
|
+
var UNBUCKETABLE_MS = -864e13;
|
|
461
|
+
function footprintsToSpec(cfg) {
|
|
462
|
+
return {
|
|
463
|
+
// Index alignment is load-bearing: the core reports hits as indices into this
|
|
464
|
+
// array and the gesture layer maps them straight back to the consumer's
|
|
465
|
+
// `footprints`. So a non-finite time — which can't be bucketed and would
|
|
466
|
+
// reach the native side as a garbage int64 — is neutralized *in place* rather
|
|
467
|
+
// than filtered out, which would shift every index after it onto the wrong
|
|
468
|
+
// trade.
|
|
469
|
+
prints: cfg.prints.map((f) => ({
|
|
470
|
+
timeMs: Number.isFinite(f.timeMs) ? f.timeMs : UNBUCKETABLE_MS,
|
|
471
|
+
side: f.side === "sell" ? FOOTPRINT_SELL : FOOTPRINT_BUY
|
|
472
|
+
})),
|
|
473
|
+
radiusPx: cfg.style?.radius ?? 0,
|
|
474
|
+
gapPx: cfg.style?.gap ?? 0,
|
|
475
|
+
marginPx: cfg.style?.margin ?? 0,
|
|
476
|
+
hoverBoost: cfg.style?.hoverBoost ?? DEFAULT_FOOTPRINT_HOVER_BOOST
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
var EMPTY_FOOTPRINTS = footprintsToSpec({ prints: [] });
|
|
366
480
|
var installed = false;
|
|
367
481
|
function ensureInstalled() {
|
|
368
482
|
if (installed) return;
|
|
@@ -373,23 +487,37 @@ function ensureInstalled() {
|
|
|
373
487
|
}
|
|
374
488
|
installed = true;
|
|
375
489
|
}
|
|
376
|
-
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, movingAverages, vwap, bollingerBands, volume, priceLines, transition) {
|
|
490
|
+
function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType, theme, rsi, macd, atr, movingAverages, vwap, bollingerBands, ichimoku, fairValueGaps, volume, priceLines, footprints, transition) {
|
|
377
491
|
const handleRef = (0, import_react.useRef)(null);
|
|
378
492
|
const defaultWidthAppliedRef = (0, import_react.useRef)(false);
|
|
379
493
|
const volumeCollapseRef = (0, import_react.useRef)(null);
|
|
380
494
|
const prevDataRef = (0, import_react.useRef)(null);
|
|
381
495
|
const intervalMorphRaf = (0, import_react.useRef)(null);
|
|
496
|
+
const streamRaf = (0, import_react.useRef)(null);
|
|
497
|
+
const streamWindowRef = (0, import_react.useRef)(null);
|
|
498
|
+
const streamMorphRef = (0, import_react.useRef)(false);
|
|
382
499
|
const [picture, setPicture] = (0, import_react.useState)(null);
|
|
383
500
|
if (!handleRef.current && size.width > 0 && size.height > 0) {
|
|
384
501
|
ensureInstalled();
|
|
385
502
|
handleRef.current = globalThis.VroomChartJSI.create();
|
|
386
503
|
}
|
|
387
|
-
const animRef = (0, import_react.useRef)({
|
|
504
|
+
const animRef = (0, import_react.useRef)({
|
|
505
|
+
ms: 300,
|
|
506
|
+
easing: void 0,
|
|
507
|
+
reduceMotion: false,
|
|
508
|
+
interval: "transform",
|
|
509
|
+
stream: "none",
|
|
510
|
+
streamMs: 150
|
|
511
|
+
});
|
|
388
512
|
animRef.current = {
|
|
389
513
|
ms: Math.max(0, transition?.transitionMs ?? 300),
|
|
390
514
|
easing: transition?.transitionEasing,
|
|
391
515
|
reduceMotion: transition?.reduceMotion ?? false,
|
|
392
|
-
interval: transition?.intervalTransition === "fade" ? "fade" : "transform"
|
|
516
|
+
interval: transition?.intervalTransition === "fade" ? "fade" : "transform",
|
|
517
|
+
stream: transition?.streamTransition === "transform" ? "transform" : "none",
|
|
518
|
+
// Shorter than transitionMs by default: ticks can land faster than a 300ms
|
|
519
|
+
// curve, and every one that does interrupts the last.
|
|
520
|
+
streamMs: Math.max(0, transition?.streamTransitionMs ?? 150)
|
|
393
521
|
};
|
|
394
522
|
const onFrameRef = (0, import_react.useRef)(transition?.onFrame);
|
|
395
523
|
onFrameRef.current = transition?.onFrame;
|
|
@@ -413,12 +541,70 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
413
541
|
};
|
|
414
542
|
intervalMorphRaf.current = requestAnimationFrame(step);
|
|
415
543
|
}, []);
|
|
544
|
+
const settleStream = (0, import_react.useCallback)((keepMorph = false) => {
|
|
545
|
+
if (streamRaf.current != null) {
|
|
546
|
+
cancelAnimationFrame(streamRaf.current);
|
|
547
|
+
streamRaf.current = null;
|
|
548
|
+
}
|
|
549
|
+
const h = handleRef.current;
|
|
550
|
+
const target = streamWindowRef.current;
|
|
551
|
+
streamWindowRef.current = null;
|
|
552
|
+
if (target) h?.setVisibleRange(target.startMs, target.endMs);
|
|
553
|
+
if (streamMorphRef.current && !keepMorph) {
|
|
554
|
+
streamMorphRef.current = false;
|
|
555
|
+
h?.setIntervalMorph(1);
|
|
556
|
+
}
|
|
557
|
+
}, []);
|
|
558
|
+
const startStreamAnim = (0, import_react.useCallback)(
|
|
559
|
+
(h, morphing, window) => {
|
|
560
|
+
const { streamMs, easing } = animRef.current;
|
|
561
|
+
let from = window ? h.getVisibleRange() : null;
|
|
562
|
+
let applied = null;
|
|
563
|
+
streamWindowRef.current = window;
|
|
564
|
+
streamMorphRef.current = morphing;
|
|
565
|
+
const start = performance.now();
|
|
566
|
+
const step = (now) => {
|
|
567
|
+
if (from && applied) {
|
|
568
|
+
const now_w = h.getVisibleRange();
|
|
569
|
+
if (now_w.startMs !== applied.startMs || now_w.endMs !== applied.endMs) {
|
|
570
|
+
from = null;
|
|
571
|
+
streamWindowRef.current = null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const p = Math.min(1, (now - start) / streamMs);
|
|
575
|
+
const e = p < 1 ? ease(easing, p) : 1;
|
|
576
|
+
if (morphing) h.setIntervalMorph(e);
|
|
577
|
+
if (from && window) {
|
|
578
|
+
applied = {
|
|
579
|
+
startMs: Math.round(from.startMs + (window.startMs - from.startMs) * e),
|
|
580
|
+
endMs: Math.round(from.endMs + (window.endMs - from.endMs) * e)
|
|
581
|
+
};
|
|
582
|
+
h.setVisibleRange(applied.startMs, applied.endMs);
|
|
583
|
+
}
|
|
584
|
+
const pic = h.render();
|
|
585
|
+
if (pic) onFrameRef.current?.(pic);
|
|
586
|
+
if (p < 1) {
|
|
587
|
+
streamRaf.current = requestAnimationFrame(step);
|
|
588
|
+
} else {
|
|
589
|
+
streamRaf.current = null;
|
|
590
|
+
streamWindowRef.current = null;
|
|
591
|
+
streamMorphRef.current = false;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
streamRaf.current = requestAnimationFrame(step);
|
|
595
|
+
},
|
|
596
|
+
[]
|
|
597
|
+
);
|
|
416
598
|
(0, import_react.useEffect)(() => {
|
|
417
599
|
return () => {
|
|
418
600
|
if (intervalMorphRaf.current != null) {
|
|
419
601
|
cancelAnimationFrame(intervalMorphRaf.current);
|
|
420
602
|
intervalMorphRaf.current = null;
|
|
421
603
|
}
|
|
604
|
+
if (streamRaf.current != null) {
|
|
605
|
+
cancelAnimationFrame(streamRaf.current);
|
|
606
|
+
streamRaf.current = null;
|
|
607
|
+
}
|
|
422
608
|
};
|
|
423
609
|
}, []);
|
|
424
610
|
const explicit = visibleRange != null;
|
|
@@ -427,15 +613,20 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
427
613
|
const themeKey = theme ? JSON.stringify(theme) : "";
|
|
428
614
|
const rsiKey = rsi ? JSON.stringify(rsi) : "";
|
|
429
615
|
const macdKey = macd ? JSON.stringify(macd) : "";
|
|
616
|
+
const atrKey = atr ? JSON.stringify(atr) : "";
|
|
430
617
|
const maKey = movingAverages ? JSON.stringify(movingAverages) : "";
|
|
431
618
|
const vwapKey = vwap ? JSON.stringify(vwap) : "";
|
|
432
619
|
const bollingerKey = bollingerBands ? JSON.stringify(bollingerBands) : "";
|
|
620
|
+
const ichimokuKey = ichimoku ? JSON.stringify(ichimoku) : "";
|
|
621
|
+
const fvgKey = fairValueGaps ? JSON.stringify(fairValueGaps) : "";
|
|
433
622
|
const volumeKey = volume ? JSON.stringify(volume) : "";
|
|
434
623
|
const priceLinesKey = priceLines ? JSON.stringify(priceLines) : "";
|
|
624
|
+
const footprintsKey = footprints ? JSON.stringify(footprints) : "";
|
|
435
625
|
(0, import_react.useEffect)(() => {
|
|
436
626
|
const h = handleRef.current;
|
|
437
627
|
if (!h) return;
|
|
438
628
|
h.setSize(size.width, size.height, size.pxRatio ?? 1);
|
|
629
|
+
h.setIchimoku(ichimokuToSpec(ichimoku));
|
|
439
630
|
if (!defaultWidthAppliedRef.current && !explicit && defaultCandleWidth != null && defaultCandleWidth > 0) {
|
|
440
631
|
h.setDefaultCandleWidth(defaultCandleWidth);
|
|
441
632
|
defaultWidthAppliedRef.current = true;
|
|
@@ -448,6 +639,36 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
448
639
|
const transitionKind = freshHandle ? "initial" : explicit ? "stream" : classifyTransition(prev.candles, candles, seriesKey !== prev.seriesKey);
|
|
449
640
|
let tfArgs = null;
|
|
450
641
|
let prevEnvelope = null;
|
|
642
|
+
let stream = null;
|
|
643
|
+
if (transitionKind === "stream" && prev != null && !explicit) {
|
|
644
|
+
const { stream: mode, streamMs, reduceMotion } = animRef.current;
|
|
645
|
+
const stepMs = inferStepMs(candles);
|
|
646
|
+
if (mode === "transform" && streamMs > 0 && stepMs != null && !reduceMotion && onFrameRef.current != null) {
|
|
647
|
+
const lastMs = candles[candles.length - 1].timeMs;
|
|
648
|
+
const prevLastMs = prev.candles[prev.candles.length - 1].timeMs;
|
|
649
|
+
if (classifyStream(prev.candles, candles) === "append") {
|
|
650
|
+
const w = h.getVisibleRange();
|
|
651
|
+
const prevStepMs = inferStepMs(prev.candles) ?? stepMs;
|
|
652
|
+
if (isPinnedToLatest(w, prevLastMs, prevStepMs)) {
|
|
653
|
+
const by = lastMs - prevLastMs;
|
|
654
|
+
stream = {
|
|
655
|
+
morph: false,
|
|
656
|
+
window: { startMs: w.startMs + by, endMs: w.endMs + by }
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
stream = { morph: true, window: null };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (stream?.morph) {
|
|
664
|
+
settleStream(true);
|
|
665
|
+
h.beginStreamMorph();
|
|
666
|
+
} else {
|
|
667
|
+
settleStream();
|
|
668
|
+
}
|
|
669
|
+
} else if (transitionKind === "stream") {
|
|
670
|
+
settleStream();
|
|
671
|
+
}
|
|
451
672
|
if (transitionKind === "timeframe" && prev != null) {
|
|
452
673
|
const oldWindow = h.getVisibleRange();
|
|
453
674
|
const oldStepMs = inferStepMs(prev.candles);
|
|
@@ -483,6 +704,8 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
483
704
|
if (prevEnvelope) h.preservePriceEnvelope(prevEnvelope.low, prevEnvelope.high);
|
|
484
705
|
else h.resetPriceScale();
|
|
485
706
|
if (morphing) startIntervalMorph(h);
|
|
707
|
+
} else if (stream) {
|
|
708
|
+
startStreamAnim(h, stream.morph, stream.window);
|
|
486
709
|
} else if (transitionKind === "reset") {
|
|
487
710
|
h.resetView();
|
|
488
711
|
}
|
|
@@ -500,21 +723,27 @@ function useChartCore(candles, size, visibleRange, defaultCandleWidth, chartType
|
|
|
500
723
|
}
|
|
501
724
|
h.setRSI(rsiToSpec(rsi));
|
|
502
725
|
h.setMACD(macdToSpec(macd));
|
|
726
|
+
h.setATR(atrToSpec(atr));
|
|
503
727
|
h.setOverlays((movingAverages ?? []).map(overlayToNumeric));
|
|
504
728
|
h.setVWAP(vwapToSpec(vwap));
|
|
505
729
|
h.setBollinger(bollingerToSpec(bollingerBands));
|
|
730
|
+
h.setFairValueGaps(fvgToSpec(fairValueGaps));
|
|
506
731
|
h.setVolume(volumeToSpec(volume));
|
|
507
732
|
const collapse = volumeCollapseRef.current;
|
|
508
733
|
if (collapse) h.setVolumeCollapse(collapse.t, collapse.easing);
|
|
509
734
|
h.setPriceLines(
|
|
510
735
|
priceLines?.lines.length ? priceLinesToSpec(priceLines) : EMPTY_PRICE_LINES
|
|
511
736
|
);
|
|
737
|
+
h.setFootprints(
|
|
738
|
+
footprints?.prints.length ? footprintsToSpec(footprints) : EMPTY_FOOTPRINTS
|
|
739
|
+
);
|
|
512
740
|
if (!morphing) setPicture(h.render());
|
|
513
|
-
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, bollingerKey, volumeKey, priceLinesKey, startIntervalMorph, endIntervalMorph]);
|
|
741
|
+
}, [candles, seriesKey, size.width, size.height, size.pxRatio, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, atrKey, maKey, vwapKey, bollingerKey, ichimokuKey, fvgKey, volumeKey, priceLinesKey, footprintsKey, startIntervalMorph, endIntervalMorph, startStreamAnim, settleStream]);
|
|
514
742
|
return { handle: handleRef.current, picture, volumeCollapseRef };
|
|
515
743
|
}
|
|
516
744
|
|
|
517
745
|
// src/VroomChart.tsx
|
|
746
|
+
var FOOTPRINT_SELL2 = 1;
|
|
518
747
|
function isSkImage(frame) {
|
|
519
748
|
return typeof frame.getImageInfo === "function";
|
|
520
749
|
}
|
|
@@ -531,12 +760,17 @@ function VroomChart(props) {
|
|
|
531
760
|
transitionMs,
|
|
532
761
|
transitionEasing,
|
|
533
762
|
intervalTransition,
|
|
763
|
+
streamTransition,
|
|
764
|
+
streamTransitionMs,
|
|
534
765
|
theme,
|
|
535
766
|
rsi,
|
|
536
767
|
macd,
|
|
768
|
+
atr,
|
|
537
769
|
movingAverages,
|
|
538
770
|
vwap,
|
|
539
771
|
bollingerBands,
|
|
772
|
+
ichimoku,
|
|
773
|
+
fairValueGaps,
|
|
540
774
|
volume,
|
|
541
775
|
crosshairOffset = 40,
|
|
542
776
|
onCrosshair,
|
|
@@ -545,7 +779,10 @@ function VroomChart(props) {
|
|
|
545
779
|
priceLinesStyle,
|
|
546
780
|
onPriceLineDrag,
|
|
547
781
|
onPriceLineDragEnd,
|
|
548
|
-
onPriceLineClose
|
|
782
|
+
onPriceLineClose,
|
|
783
|
+
footprints,
|
|
784
|
+
footprintsStyle,
|
|
785
|
+
onFootprint
|
|
549
786
|
} = props;
|
|
550
787
|
const [measured, setMeasured] = (0, import_react2.useState)({ width: 0, height: 0 });
|
|
551
788
|
const width = widthProp ?? measured.width;
|
|
@@ -565,6 +802,10 @@ function VroomChart(props) {
|
|
|
565
802
|
} : void 0,
|
|
566
803
|
[priceLines, priceLinesStyle, onPriceLineClose]
|
|
567
804
|
);
|
|
805
|
+
const footprintsProp = (0, import_react2.useMemo)(
|
|
806
|
+
() => footprints ? { prints: footprints, style: footprintsStyle } : void 0,
|
|
807
|
+
[footprints, footprintsStyle]
|
|
808
|
+
);
|
|
568
809
|
const emptyPicture = (0, import_react2.useMemo)(() => {
|
|
569
810
|
const rec = import_react_native_skia.Skia.PictureRecorder();
|
|
570
811
|
rec.beginRecording(import_react_native_skia.Skia.XYWHRect(0, 0, 1, 1));
|
|
@@ -608,14 +849,28 @@ function VroomChart(props) {
|
|
|
608
849
|
theme,
|
|
609
850
|
rsi,
|
|
610
851
|
macd,
|
|
852
|
+
atr,
|
|
611
853
|
movingAverages,
|
|
612
854
|
vwap,
|
|
613
855
|
bollingerBands,
|
|
856
|
+
ichimoku,
|
|
857
|
+
fairValueGaps,
|
|
614
858
|
volume,
|
|
615
859
|
priceLinesProp,
|
|
616
|
-
|
|
860
|
+
footprintsProp,
|
|
861
|
+
{
|
|
862
|
+
seriesKey,
|
|
863
|
+
transitionMs,
|
|
864
|
+
transitionEasing,
|
|
865
|
+
intervalTransition,
|
|
866
|
+
streamTransition,
|
|
867
|
+
streamTransitionMs,
|
|
868
|
+
reduceMotion,
|
|
869
|
+
onFrame
|
|
870
|
+
}
|
|
617
871
|
);
|
|
618
872
|
const crosshairActive = (0, import_react2.useRef)(false);
|
|
873
|
+
const footprintActive = (0, import_react2.useRef)(false);
|
|
619
874
|
const lastCrosshairTime = (0, import_react2.useRef)(null);
|
|
620
875
|
const decayRaf = (0, import_react2.useRef)(null);
|
|
621
876
|
const cancelDecay = (0, import_react2.useCallback)(() => {
|
|
@@ -875,6 +1130,24 @@ function VroomChart(props) {
|
|
|
875
1130
|
null
|
|
876
1131
|
);
|
|
877
1132
|
const panMode = (0, import_react2.useRef)("chart");
|
|
1133
|
+
const dismissFootprint = (redraw = true) => {
|
|
1134
|
+
if (!handle || !footprintActive.current) return;
|
|
1135
|
+
footprintActive.current = false;
|
|
1136
|
+
handle.setFootprintHover(0, -1);
|
|
1137
|
+
if (redraw) {
|
|
1138
|
+
const frame = handle.render();
|
|
1139
|
+
if (frame) applyFrame(frame);
|
|
1140
|
+
}
|
|
1141
|
+
onFootprint?.({
|
|
1142
|
+
active: false,
|
|
1143
|
+
reason: "hide",
|
|
1144
|
+
side: null,
|
|
1145
|
+
timeMs: null,
|
|
1146
|
+
footprints: [],
|
|
1147
|
+
badge: null,
|
|
1148
|
+
pane: null
|
|
1149
|
+
});
|
|
1150
|
+
};
|
|
878
1151
|
const pan = import_react_native_gesture_handler.Gesture.Pan().runOnJS(true).maxPointers(1).onStart((e) => {
|
|
879
1152
|
cancelDecay();
|
|
880
1153
|
panMode.current = hitAxis(e.x, e.y);
|
|
@@ -889,6 +1162,7 @@ function VroomChart(props) {
|
|
|
889
1162
|
if (p) applyFrame(p);
|
|
890
1163
|
}
|
|
891
1164
|
}
|
|
1165
|
+
if (panMode.current !== "price-line") dismissFootprint();
|
|
892
1166
|
}).onChange((e) => {
|
|
893
1167
|
if (!handle) return;
|
|
894
1168
|
let next = null;
|
|
@@ -971,6 +1245,7 @@ function VroomChart(props) {
|
|
|
971
1245
|
});
|
|
972
1246
|
const pinch = import_react_native_gesture_handler.Gesture.Pinch().runOnJS(true).onTouchesDown((e) => {
|
|
973
1247
|
if (e.numberOfTouches < 2) return;
|
|
1248
|
+
dismissFootprint();
|
|
974
1249
|
const [a, b] = e.allTouches;
|
|
975
1250
|
const spanX = Math.abs(a.x - b.x);
|
|
976
1251
|
const spanY = Math.abs(a.y - b.y);
|
|
@@ -1011,6 +1286,7 @@ function VroomChart(props) {
|
|
|
1011
1286
|
if (hitAxis(e.x, e.y) !== "chart") return;
|
|
1012
1287
|
if (hitPriceLine(e.x, e.y)) return;
|
|
1013
1288
|
cancelDecay();
|
|
1289
|
+
dismissFootprint(false);
|
|
1014
1290
|
crosshairActive.current = true;
|
|
1015
1291
|
const ch = handle.setCrosshair(e.x, e.y - crosshairOffset);
|
|
1016
1292
|
if (ch) applyFrame(ch);
|
|
@@ -1027,6 +1303,28 @@ function VroomChart(props) {
|
|
|
1027
1303
|
});
|
|
1028
1304
|
const tap = import_react_native_gesture_handler.Gesture.Tap().runOnJS(true).onStart((e) => {
|
|
1029
1305
|
if (!handle) return;
|
|
1306
|
+
const prints = footprints ?? [];
|
|
1307
|
+
const fp = prints.length ? handle.hitTestFootprint(e.x, e.y) : null;
|
|
1308
|
+
if (fp) {
|
|
1309
|
+
handle.setFootprintHover(fp.candleTimeMs, fp.side);
|
|
1310
|
+
const frame = handle.render();
|
|
1311
|
+
if (frame) applyFrame(frame);
|
|
1312
|
+
const wasActive = footprintActive.current;
|
|
1313
|
+
footprintActive.current = true;
|
|
1314
|
+
onFootprint?.({
|
|
1315
|
+
active: true,
|
|
1316
|
+
reason: wasActive ? "move" : "show",
|
|
1317
|
+
side: fp.side === FOOTPRINT_SELL2 ? "sell" : "buy",
|
|
1318
|
+
timeMs: fp.candleTimeMs,
|
|
1319
|
+
// The core reports indices into the array we last pushed, which is this
|
|
1320
|
+
// same prop — so this rejoins each badge to the consumer's own objects.
|
|
1321
|
+
footprints: fp.indices.map((i) => prints[i]).filter((f) => f != null),
|
|
1322
|
+
badge: { x: fp.x, y: fp.y, radius: fp.radius },
|
|
1323
|
+
pane: fp.pane
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
dismissFootprint();
|
|
1030
1328
|
const pl = hitPriceLine(e.x, e.y);
|
|
1031
1329
|
if (pl && pl.part === 1) {
|
|
1032
1330
|
onPriceLineClose?.(pl.line.id);
|