react-native-livechart 1.0.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/LICENSE +33 -0
- package/README.md +198 -0
- package/dist/components/AnimatedLabel.d.ts +21 -0
- package/dist/components/AnimatedLabel.d.ts.map +1 -0
- package/dist/components/BadgeOverlay.d.ts +16 -0
- package/dist/components/BadgeOverlay.d.ts.map +1 -0
- package/dist/components/CrosshairLine.d.ts +20 -0
- package/dist/components/CrosshairLine.d.ts.map +1 -0
- package/dist/components/CrosshairOverlay.d.ts +29 -0
- package/dist/components/CrosshairOverlay.d.ts.map +1 -0
- package/dist/components/DegenParticlesOverlay.d.ts +16 -0
- package/dist/components/DegenParticlesOverlay.d.ts.map +1 -0
- package/dist/components/DotOverlay.d.ts +17 -0
- package/dist/components/DotOverlay.d.ts.map +1 -0
- package/dist/components/LeftEdgeFade.d.ts +9 -0
- package/dist/components/LeftEdgeFade.d.ts.map +1 -0
- package/dist/components/LiveChart.d.ts +3 -0
- package/dist/components/LiveChart.d.ts.map +1 -0
- package/dist/components/LiveChartSeries.d.ts +3 -0
- package/dist/components/LiveChartSeries.d.ts.map +1 -0
- package/dist/components/LiveChartTransition.d.ts +28 -0
- package/dist/components/LiveChartTransition.d.ts.map +1 -0
- package/dist/components/LoadingOverlay.d.ts +24 -0
- package/dist/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/components/MarkerOverlay.d.ts +25 -0
- package/dist/components/MarkerOverlay.d.ts.map +1 -0
- package/dist/components/MultiSeriesDots.d.ts +11 -0
- package/dist/components/MultiSeriesDots.d.ts.map +1 -0
- package/dist/components/MultiSeriesStroke.d.ts +14 -0
- package/dist/components/MultiSeriesStroke.d.ts.map +1 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts +10 -0
- package/dist/components/MultiSeriesTooltipStack.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLabels.d.ts +11 -0
- package/dist/components/MultiSeriesValueLabels.d.ts.map +1 -0
- package/dist/components/MultiSeriesValueLines.d.ts +10 -0
- package/dist/components/MultiSeriesValueLines.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +19 -0
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/SeriesToggleChips.d.ts +15 -0
- package/dist/components/SeriesToggleChips.d.ts.map +1 -0
- package/dist/components/TradeStreamOverlay.d.ts +14 -0
- package/dist/components/TradeStreamOverlay.d.ts.map +1 -0
- package/dist/components/ValueLineOverlay.d.ts +16 -0
- package/dist/components/ValueLineOverlay.d.ts.map +1 -0
- package/dist/components/ValueTextOverlay.d.ts +20 -0
- package/dist/components/ValueTextOverlay.d.ts.map +1 -0
- package/dist/components/XAxisOverlay.d.ts +14 -0
- package/dist/components/XAxisOverlay.d.ts.map +1 -0
- package/dist/components/YAxisOverlay.d.ts +23 -0
- package/dist/components/YAxisOverlay.d.ts.map +1 -0
- package/dist/components/seriesMetaSig.d.ts +4 -0
- package/dist/components/seriesMetaSig.d.ts.map +1 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core/liveChartEngineTick.d.ts +45 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts +38 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -0
- package/dist/core/multiSeriesLayout.d.ts +22 -0
- package/dist/core/multiSeriesLayout.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +188 -0
- package/dist/core/resolveConfig.d.ts.map +1 -0
- package/dist/core/useLiveChartEngine.d.ts +79 -0
- package/dist/core/useLiveChartEngine.d.ts.map +1 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts +68 -0
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -0
- package/dist/draw/candle.d.ts +25 -0
- package/dist/draw/candle.d.ts.map +1 -0
- package/dist/draw/grid.d.ts +24 -0
- package/dist/draw/grid.d.ts.map +1 -0
- package/dist/draw/line.d.ts +86 -0
- package/dist/draw/line.d.ts.map +1 -0
- package/dist/draw/trade.d.ts +38 -0
- package/dist/draw/trade.d.ts.map +1 -0
- package/dist/hooks/crosshairSeries.d.ts +13 -0
- package/dist/hooks/crosshairSeries.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +72 -0
- package/dist/hooks/crosshairShared.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +24 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +42 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -0
- package/dist/hooks/useBadge.d.ts +14 -0
- package/dist/hooks/useBadge.d.ts.map +1 -0
- package/dist/hooks/useCandlePaths.d.ts +19 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -0
- package/dist/hooks/useCanvasLayout.d.ts +12 -0
- package/dist/hooks/useCanvasLayout.d.ts.map +1 -0
- package/dist/hooks/useChartColors.d.ts +20 -0
- package/dist/hooks/useChartColors.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +19 -0
- package/dist/hooks/useChartPaths.d.ts.map +1 -0
- package/dist/hooks/useChartReveal.d.ts +30 -0
- package/dist/hooks/useChartReveal.d.ts.map +1 -0
- package/dist/hooks/useChartSkiaFont.d.ts +8 -0
- package/dist/hooks/useChartSkiaFont.d.ts.map +1 -0
- package/dist/hooks/useCrosshair.d.ts +20 -0
- package/dist/hooks/useCrosshair.d.ts.map +1 -0
- package/dist/hooks/useCrosshairSeries.d.ts +10 -0
- package/dist/hooks/useCrosshairSeries.d.ts.map +1 -0
- package/dist/hooks/useDegen.d.ts +17 -0
- package/dist/hooks/useDegen.d.ts.map +1 -0
- package/dist/hooks/useLiveChartHasData.d.ts +17 -0
- package/dist/hooks/useLiveChartHasData.d.ts.map +1 -0
- package/dist/hooks/useLiveDot.d.ts +12 -0
- package/dist/hooks/useLiveDot.d.ts.map +1 -0
- package/dist/hooks/useMarkers.d.ts +15 -0
- package/dist/hooks/useMarkers.d.ts.map +1 -0
- package/dist/hooks/useModeBlend.d.ts +18 -0
- package/dist/hooks/useModeBlend.d.ts.map +1 -0
- package/dist/hooks/useMomentum.d.ts +16 -0
- package/dist/hooks/useMomentum.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts +25 -0
- package/dist/hooks/useMultiSeriesDegen.d.ts.map +1 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +15 -0
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +32 -0
- package/dist/hooks/useReferenceLine.d.ts.map +1 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts +24 -0
- package/dist/hooks/useReverseMorphEngineInputs.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts +12 -0
- package/dist/hooks/useTradeStream.d.ts.map +1 -0
- package/dist/hooks/useXAxis.d.ts +32 -0
- package/dist/hooks/useXAxis.d.ts.map +1 -0
- package/dist/hooks/useYAxis.d.ts +13 -0
- package/dist/hooks/useYAxis.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/format.d.ts +5 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/measureFontTextWidth.d.ts +7 -0
- package/dist/lib/measureFontTextWidth.d.ts.map +1 -0
- package/dist/lib/monoFontFamily.d.ts +6 -0
- package/dist/lib/monoFontFamily.d.ts.map +1 -0
- package/dist/math/color.d.ts +5 -0
- package/dist/math/color.d.ts.map +1 -0
- package/dist/math/degenTick.d.ts +57 -0
- package/dist/math/degenTick.d.ts.map +1 -0
- package/dist/math/interpolate.d.ts +7 -0
- package/dist/math/interpolate.d.ts.map +1 -0
- package/dist/math/intervals.d.ts +3 -0
- package/dist/math/intervals.d.ts.map +1 -0
- package/dist/math/lerp.d.ts +7 -0
- package/dist/math/lerp.d.ts.map +1 -0
- package/dist/math/markers.d.ts +44 -0
- package/dist/math/markers.d.ts.map +1 -0
- package/dist/math/momentum.d.ts +8 -0
- package/dist/math/momentum.d.ts.map +1 -0
- package/dist/math/pickCandle.d.ts +8 -0
- package/dist/math/pickCandle.d.ts.map +1 -0
- package/dist/math/range.d.ts +10 -0
- package/dist/math/range.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +20 -0
- package/dist/math/referenceLines.d.ts.map +1 -0
- package/dist/math/spline.d.ts +45 -0
- package/dist/math/spline.d.ts.map +1 -0
- package/dist/math/squiggly.d.ts +36 -0
- package/dist/math/squiggly.d.ts.map +1 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/types.d.ts +720 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/components/AnimatedLabel.tsx +41 -0
- package/src/components/BadgeOverlay.tsx +39 -0
- package/src/components/CrosshairLine.tsx +90 -0
- package/src/components/CrosshairOverlay.tsx +170 -0
- package/src/components/DegenParticlesOverlay.tsx +141 -0
- package/src/components/DotOverlay.tsx +69 -0
- package/src/components/LeftEdgeFade.tsx +36 -0
- package/src/components/LiveChart.tsx +821 -0
- package/src/components/LiveChartSeries.tsx +607 -0
- package/src/components/LiveChartTransition.tsx +135 -0
- package/src/components/LoadingOverlay.tsx +301 -0
- package/src/components/MarkerOverlay.tsx +331 -0
- package/src/components/MultiSeriesDots.tsx +117 -0
- package/src/components/MultiSeriesStroke.tsx +79 -0
- package/src/components/MultiSeriesTooltipStack.tsx +78 -0
- package/src/components/MultiSeriesValueLabels.tsx +103 -0
- package/src/components/MultiSeriesValueLines.tsx +115 -0
- package/src/components/ReferenceLineOverlay.tsx +317 -0
- package/src/components/SeriesToggleChips.tsx +201 -0
- package/src/components/TradeStreamOverlay.tsx +117 -0
- package/src/components/ValueLineOverlay.tsx +63 -0
- package/src/components/ValueTextOverlay.tsx +43 -0
- package/src/components/XAxisOverlay.tsx +109 -0
- package/src/components/YAxisOverlay.tsx +135 -0
- package/src/components/seriesMetaSig.ts +13 -0
- package/src/constants.ts +44 -0
- package/src/core/liveChartEngineTick.ts +185 -0
- package/src/core/liveChartSeriesEngineTick.ts +163 -0
- package/src/core/multiSeriesLayout.ts +80 -0
- package/src/core/resolveConfig.ts +538 -0
- package/src/core/useLiveChartEngine.ts +207 -0
- package/src/core/useLiveChartSeriesEngine.ts +215 -0
- package/src/draw/candle.ts +210 -0
- package/src/draw/grid.ts +174 -0
- package/src/draw/line.ts +257 -0
- package/src/draw/trade.ts +202 -0
- package/src/hooks/crosshairSeries.ts +78 -0
- package/src/hooks/crosshairShared.ts +296 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/resolveChartLayout.ts +144 -0
- package/src/hooks/useBadge.ts +192 -0
- package/src/hooks/useCandlePaths.ts +165 -0
- package/src/hooks/useCanvasLayout.ts +22 -0
- package/src/hooks/useChartColors.ts +48 -0
- package/src/hooks/useChartPaths.ts +121 -0
- package/src/hooks/useChartReveal.ts +123 -0
- package/src/hooks/useChartSkiaFont.ts +34 -0
- package/src/hooks/useCrosshair.ts +250 -0
- package/src/hooks/useCrosshairSeries.ts +163 -0
- package/src/hooks/useDegen.ts +262 -0
- package/src/hooks/useLiveChartHasData.ts +31 -0
- package/src/hooks/useLiveDot.ts +31 -0
- package/src/hooks/useMarkers.ts +95 -0
- package/src/hooks/useModeBlend.ts +52 -0
- package/src/hooks/useMomentum.ts +37 -0
- package/src/hooks/useMultiSeriesDegen.ts +260 -0
- package/src/hooks/useMultiSeriesLinePaths.ts +76 -0
- package/src/hooks/useReferenceLine.ts +212 -0
- package/src/hooks/useReverseMorphEngineInputs.ts +154 -0
- package/src/hooks/useTradeStream.ts +64 -0
- package/src/hooks/useXAxis.ts +200 -0
- package/src/hooks/useYAxis.ts +48 -0
- package/src/index.ts +69 -0
- package/src/lib/format.ts +36 -0
- package/src/lib/measureFontTextWidth.ts +10 -0
- package/src/lib/monoFontFamily.ts +8 -0
- package/src/math/color.ts +31 -0
- package/src/math/degenTick.ts +151 -0
- package/src/math/interpolate.ts +32 -0
- package/src/math/intervals.ts +17 -0
- package/src/math/lerp.ts +17 -0
- package/src/math/markers.ts +175 -0
- package/src/math/momentum.ts +38 -0
- package/src/math/pickCandle.ts +42 -0
- package/src/math/range.ts +54 -0
- package/src/math/referenceLines.ts +56 -0
- package/src/math/spline.ts +183 -0
- package/src/math/squiggly.ts +108 -0
- package/src/theme.ts +231 -0
- package/src/types.ts +789 -0
package/src/draw/grid.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { lerp } from "../math/lerp";
|
|
2
|
+
|
|
3
|
+
export interface YAxisEntry {
|
|
4
|
+
y: number;
|
|
5
|
+
label: string;
|
|
6
|
+
alpha: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pick a nice Y-axis interval using TradingView's cycling divisor approach.
|
|
11
|
+
* Hysteresis: once chosen, sticks until pixel spacing falls outside [0.5x, 4x] of minGap.
|
|
12
|
+
*/
|
|
13
|
+
export function pickInterval(
|
|
14
|
+
valRange: number,
|
|
15
|
+
pxPerUnit: number,
|
|
16
|
+
minGap: number,
|
|
17
|
+
prev: number,
|
|
18
|
+
): number {
|
|
19
|
+
"worklet";
|
|
20
|
+
if (prev > 0) {
|
|
21
|
+
const px = prev * pxPerUnit;
|
|
22
|
+
if (px >= minGap * 0.5 && px <= minGap * 4) return prev;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const divisorSets = [
|
|
26
|
+
[2, 2.5, 2],
|
|
27
|
+
[2, 2, 2.5],
|
|
28
|
+
[2.5, 2, 2],
|
|
29
|
+
];
|
|
30
|
+
let best = Infinity;
|
|
31
|
+
for (let s = 0; s < divisorSets.length; s++) {
|
|
32
|
+
const divs = divisorSets[s];
|
|
33
|
+
let span = Math.pow(10, Math.ceil(Math.log10(valRange)));
|
|
34
|
+
let i = 0;
|
|
35
|
+
while ((span / divs[i % 3]) * pxPerUnit >= minGap) {
|
|
36
|
+
span /= divs[i % 3];
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
if (span < best) best = span;
|
|
40
|
+
}
|
|
41
|
+
if (best === Infinity) {
|
|
42
|
+
return valRange / 5;
|
|
43
|
+
}
|
|
44
|
+
return best;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Blend factor for fine (non-coarse) grid lines from pixel spacing.
|
|
49
|
+
* Exposed for unit tests; matches `computeGridEntries` behaviour.
|
|
50
|
+
*/
|
|
51
|
+
export function fineLineTargetAlpha(finePx: number, minGap = 36): number {
|
|
52
|
+
"worklet";
|
|
53
|
+
const fineMin = minGap * 1.1;
|
|
54
|
+
const fineMax = minGap * 1.7;
|
|
55
|
+
if (finePx < fineMin) return 0;
|
|
56
|
+
if (finePx >= fineMax) return 1;
|
|
57
|
+
return (finePx - fineMin) / (fineMax - fineMin);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function divisible(val: number, interval: number): boolean {
|
|
61
|
+
"worklet";
|
|
62
|
+
const ratio = val / interval;
|
|
63
|
+
// 1% tolerance for floating-point rounding in interval alignment
|
|
64
|
+
return Math.abs(ratio - Math.round(ratio)) < 0.01;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const FADE_IN = 0.18;
|
|
68
|
+
const FADE_OUT = 0.12;
|
|
69
|
+
|
|
70
|
+
/** Map data value → canvas Y (nested arrows inside worklets are not UI-thread-safe). */
|
|
71
|
+
function gridValueToY(
|
|
72
|
+
val: number,
|
|
73
|
+
displayMax: number,
|
|
74
|
+
valRange: number,
|
|
75
|
+
padTop: number,
|
|
76
|
+
chartH: number,
|
|
77
|
+
): number {
|
|
78
|
+
"worklet";
|
|
79
|
+
return padTop + ((displayMax - val) / valRange) * chartH;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compute grid entries with per-label alpha fading.
|
|
84
|
+
* `labelAlphas` is mutated in place (persistent state across frames).
|
|
85
|
+
*/
|
|
86
|
+
export function computeGridEntries(
|
|
87
|
+
displayMin: number,
|
|
88
|
+
displayMax: number,
|
|
89
|
+
canvasHeight: number,
|
|
90
|
+
padTop: number,
|
|
91
|
+
padBottom: number,
|
|
92
|
+
prevInterval: number,
|
|
93
|
+
labelAlphas: Record<number, number>,
|
|
94
|
+
formatValue: (v: number) => string,
|
|
95
|
+
dt: number,
|
|
96
|
+
minGap = 36,
|
|
97
|
+
): { entries: YAxisEntry[]; interval: number } {
|
|
98
|
+
"worklet";
|
|
99
|
+
const chartH = canvasHeight - padTop - padBottom;
|
|
100
|
+
if (chartH <= 0) return { entries: [], interval: prevInterval };
|
|
101
|
+
|
|
102
|
+
const valRange = displayMax - displayMin;
|
|
103
|
+
if (valRange <= 0) return { entries: [], interval: prevInterval };
|
|
104
|
+
|
|
105
|
+
const pxPerUnit = chartH / valRange;
|
|
106
|
+
|
|
107
|
+
const coarse = pickInterval(valRange, pxPerUnit, minGap, prevInterval);
|
|
108
|
+
const fine = coarse / 2;
|
|
109
|
+
const finePx = fine * pxPerUnit;
|
|
110
|
+
const fineTarget = fineLineTargetAlpha(finePx, minGap);
|
|
111
|
+
|
|
112
|
+
const fadeZone = 32;
|
|
113
|
+
|
|
114
|
+
// Phase 1: compute target alpha for every grid line
|
|
115
|
+
const targets: Record<number, number> = {};
|
|
116
|
+
const first = Math.ceil(displayMin / fine) * fine;
|
|
117
|
+
for (let val = first; val <= displayMax; val += fine) {
|
|
118
|
+
const y = gridValueToY(val, displayMax, valRange, padTop, chartH);
|
|
119
|
+
/* istanbul ignore next -- y stays in chart band for v in [first, displayMax] with consistent toY */
|
|
120
|
+
if (y < padTop - 2 || y > canvasHeight - padBottom + 2) continue;
|
|
121
|
+
const isCoarse = divisible(val, coarse);
|
|
122
|
+
|
|
123
|
+
const fromEdge = Math.min(y - padTop, canvasHeight - padBottom - y);
|
|
124
|
+
const edgeAlpha =
|
|
125
|
+
fromEdge >= fadeZone ? 1 : fromEdge <= 0 ? 0 : fromEdge / fadeZone;
|
|
126
|
+
|
|
127
|
+
const target = (isCoarse ? 1 : fineTarget) * edgeAlpha;
|
|
128
|
+
const key = Math.round(val * 1e10);
|
|
129
|
+
targets[key] = target;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Phase 2: update tracked label alphas
|
|
133
|
+
const keys = Object.keys(labelAlphas);
|
|
134
|
+
for (let i = 0; i < keys.length; i++) {
|
|
135
|
+
const key = Number(keys[i]);
|
|
136
|
+
const alpha = labelAlphas[key];
|
|
137
|
+
const target = targets[key] ?? 0;
|
|
138
|
+
const speed = target >= alpha ? FADE_IN : FADE_OUT;
|
|
139
|
+
let next = lerp(alpha, target, speed, dt);
|
|
140
|
+
if (Math.abs(next - target) < 0.02) next = target;
|
|
141
|
+
if (next < 0.01 && target === 0) {
|
|
142
|
+
delete labelAlphas[key];
|
|
143
|
+
} else {
|
|
144
|
+
labelAlphas[key] = next;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// New labels not yet tracked
|
|
149
|
+
const targetKeys = Object.keys(targets);
|
|
150
|
+
for (let i = 0; i < targetKeys.length; i++) {
|
|
151
|
+
const key = Number(targetKeys[i]);
|
|
152
|
+
/* istanbul ignore else -- key already tracked after phase 2 when targets repeat across frames */
|
|
153
|
+
if (labelAlphas[key] === undefined) {
|
|
154
|
+
/* istanbul ignore next -- targets keys always map to a number; ?? keeps types safe */
|
|
155
|
+
labelAlphas[key] = (targets[key] ?? 0) * FADE_IN;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Phase 3: build entries
|
|
160
|
+
const entries: YAxisEntry[] = [];
|
|
161
|
+
const allKeys = Object.keys(labelAlphas);
|
|
162
|
+
for (let i = 0; i < allKeys.length; i++) {
|
|
163
|
+
const key = Number(allKeys[i]);
|
|
164
|
+
const alpha = labelAlphas[key];
|
|
165
|
+
if (alpha < 0.02) continue;
|
|
166
|
+
const val = key / 1e10;
|
|
167
|
+
const y = gridValueToY(val, displayMax, valRange, padTop, chartH);
|
|
168
|
+
/* istanbul ignore next -- vertical clip; y stays in-band when val comes from tracked keys */
|
|
169
|
+
if (y < padTop - 10 || y > canvasHeight - padBottom + 10) continue;
|
|
170
|
+
entries.push({ y, label: formatValue(val), alpha });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { entries, interval: coarse };
|
|
174
|
+
}
|
package/src/draw/line.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BADGE_DOT_GAP,
|
|
3
|
+
BADGE_MARGIN_RIGHT,
|
|
4
|
+
BADGE_PILL_PAD_X,
|
|
5
|
+
BADGE_PILL_PAD_Y,
|
|
6
|
+
BADGE_TAIL_LEN,
|
|
7
|
+
} from "../constants";
|
|
8
|
+
import type { ChartInsets, LiveChartPoint } from "../types";
|
|
9
|
+
export {
|
|
10
|
+
BADGE_DOT_GAP,
|
|
11
|
+
BADGE_MARGIN_RIGHT,
|
|
12
|
+
BADGE_PILL_PAD_X,
|
|
13
|
+
BADGE_PILL_PAD_Y,
|
|
14
|
+
BADGE_TAIL_LEN
|
|
15
|
+
} from "../constants";
|
|
16
|
+
|
|
17
|
+
export interface ChartPadding {
|
|
18
|
+
top: number;
|
|
19
|
+
right: number;
|
|
20
|
+
bottom: number;
|
|
21
|
+
left: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_PADDING: ChartPadding = {
|
|
25
|
+
top: 12,
|
|
26
|
+
right: 12,
|
|
27
|
+
bottom: 28,
|
|
28
|
+
left: 12,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Combined tail + rounded-cap offset.
|
|
33
|
+
* The pill body starts `tl` px to the right of the gutter left edge (= dot x),
|
|
34
|
+
* so the tail spans the gap between the dot and the pill body.
|
|
35
|
+
*
|
|
36
|
+
* When `showTail` is false the tail spike is omitted and only the round cap
|
|
37
|
+
* radius is returned, letting callers shrink the right gutter.
|
|
38
|
+
*/
|
|
39
|
+
export function badgeTailAndCap(fontSize: number, showTail = true): number {
|
|
40
|
+
"worklet";
|
|
41
|
+
const pillH = fontSize + BADGE_PILL_PAD_Y * 2;
|
|
42
|
+
return (showTail ? BADGE_TAIL_LEN : 0) + pillH / 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Text left-edge X so the label is horizontally centered in the badge pill.
|
|
47
|
+
* Uses the ASYMMETRIC layout: tail gap (`tl`) on the left, `BADGE_MARGIN_RIGHT` on
|
|
48
|
+
* the right. Both `useBadge` and `GridOverlay` (when badge is shown) call this so
|
|
49
|
+
* the badge text and y-axis labels share the exact same horizontal position.
|
|
50
|
+
*
|
|
51
|
+
* Layout: |dot| tl |PAD_X| text |PAD_X| BADGE_MARGIN_RIGHT |canvas edge
|
|
52
|
+
* ↑ same x for grid labels and badge text
|
|
53
|
+
*/
|
|
54
|
+
export function pillTextLeftX(
|
|
55
|
+
canvasWidth: number,
|
|
56
|
+
paddingRight: number,
|
|
57
|
+
tl: number,
|
|
58
|
+
textWidth: number,
|
|
59
|
+
): number {
|
|
60
|
+
"worklet";
|
|
61
|
+
const bodyLeft = canvasWidth - paddingRight + tl;
|
|
62
|
+
const bodyRight = canvasWidth - BADGE_MARGIN_RIGHT;
|
|
63
|
+
return (bodyLeft + bodyRight - textWidth) / 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Symmetric gutter centering — used for y-axis labels when badge is NOT shown.
|
|
68
|
+
* When badge IS shown, use `pillTextLeftX` instead so labels align with badge text.
|
|
69
|
+
*/
|
|
70
|
+
export function gutterCenteredTextLeftX(
|
|
71
|
+
canvasWidth: number,
|
|
72
|
+
paddingRight: number,
|
|
73
|
+
textWidth: number,
|
|
74
|
+
): number {
|
|
75
|
+
"worklet";
|
|
76
|
+
return canvasWidth - paddingRight / 2 - textWidth / 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Right-aligned text positioning — used for y-axis labels when inline series
|
|
81
|
+
* labels occupy the left portion of the right gutter.
|
|
82
|
+
*/
|
|
83
|
+
export function gutterRightAlignedTextLeftX(
|
|
84
|
+
canvasWidth: number,
|
|
85
|
+
textWidth: number,
|
|
86
|
+
rightMargin = 4,
|
|
87
|
+
): number {
|
|
88
|
+
"worklet";
|
|
89
|
+
return canvasWidth - textWidth - rightMargin;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Minimum `padding.right` for the badge gutter.
|
|
94
|
+
*
|
|
95
|
+
* Layout: |dot| BADGE_DOT_GAP | tl | PAD_X | text | PAD_X | BADGE_MARGIN_RIGHT |canvas edge
|
|
96
|
+
*/
|
|
97
|
+
export function minPaddingRightForBadgeYAxisAlign(
|
|
98
|
+
fontSize: number,
|
|
99
|
+
textWidth: number,
|
|
100
|
+
showTail = true,
|
|
101
|
+
): number {
|
|
102
|
+
const tl = badgeTailAndCap(fontSize, showTail);
|
|
103
|
+
return Math.ceil(
|
|
104
|
+
BADGE_DOT_GAP + tl + 2 * BADGE_PILL_PAD_X + textWidth + BADGE_MARGIN_RIGHT,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Auto-right-padding: badge needs space for the pill, y-axis labels need less. */
|
|
109
|
+
export function resolveAutoRight(
|
|
110
|
+
yAxis: boolean,
|
|
111
|
+
badge: boolean,
|
|
112
|
+
showTail = true,
|
|
113
|
+
): number {
|
|
114
|
+
if (badge) return minPaddingRightForBadgeYAxisAlign(12, 49, showTail);
|
|
115
|
+
if (yAxis) return 44;
|
|
116
|
+
return DEFAULT_PADDING.right;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Minimum `padding.left` for a badge pill drawn in the left chart margin (label width + horizontal padding + dot gap).
|
|
121
|
+
* `resolveChartLayout` does not call this; it remains available for custom layouts or `resolvePadding(..., badgeOnLeft: true)`.
|
|
122
|
+
*/
|
|
123
|
+
export function minPaddingLeftForBadge(textWidth: number): number {
|
|
124
|
+
return Math.ceil(
|
|
125
|
+
BADGE_MARGIN_RIGHT + 2 * BADGE_PILL_PAD_X + textWidth + BADGE_DOT_GAP,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Default left inset, or a wider inset when `badgeOnLeft` is true (see `minPaddingLeftForBadge`). */
|
|
130
|
+
export function resolveAutoLeft(badgeOnLeft: boolean): number {
|
|
131
|
+
if (badgeOnLeft) return minPaddingLeftForBadge(49);
|
|
132
|
+
return DEFAULT_PADDING.left;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Minimum inset from the canvas edge so the live-dot pulse ring is not clipped.
|
|
137
|
+
* Matches `DotOverlay`: circle radius up to `maxRadius` with a centered stroke.
|
|
138
|
+
*
|
|
139
|
+
* Used by `resolveChartLayout` when `pulse` is set; keep in sync with `DotOverlay` pulse rendering.
|
|
140
|
+
*
|
|
141
|
+
* @see `resolveChartLayout` in `hooks/resolveChartLayout.ts`
|
|
142
|
+
* @see `DotOverlay` in `components/DotOverlay.tsx`
|
|
143
|
+
*/
|
|
144
|
+
export function pulseRadialOutset(
|
|
145
|
+
maxRadius: number,
|
|
146
|
+
strokeWidth: number,
|
|
147
|
+
): number {
|
|
148
|
+
return Math.ceil(maxRadius + strokeWidth / 2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Extra space beyond label width so the pulse ring does not touch glyphs. */
|
|
152
|
+
const PULSE_Y_AXIS_LABEL_GAP = 8;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Minimum `padding.right` when the live dot (and pulse) sit on the inner edge of the
|
|
156
|
+
* right gutter and y-axis labels are centered in that gutter (`gutterCenteredTextLeftX`).
|
|
157
|
+
* Otherwise the pulse can overlap labels to the right of the dot.
|
|
158
|
+
*/
|
|
159
|
+
export function minPaddingRightForYAxisWithPulse(
|
|
160
|
+
pulseOutlet: number,
|
|
161
|
+
yAxisLabelTextWidth: number,
|
|
162
|
+
gap = PULSE_Y_AXIS_LABEL_GAP,
|
|
163
|
+
): number {
|
|
164
|
+
return Math.ceil(2 * pulseOutlet + yAxisLabelTextWidth + gap);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function resolvePadding(
|
|
168
|
+
override?: ChartInsets,
|
|
169
|
+
yAxis = false,
|
|
170
|
+
badge = false,
|
|
171
|
+
badgeOnLeft = false,
|
|
172
|
+
xAxis = true,
|
|
173
|
+
showTail = true,
|
|
174
|
+
): ChartPadding {
|
|
175
|
+
const autoRight = resolveAutoRight(yAxis, badge && !badgeOnLeft, showTail);
|
|
176
|
+
const autoLeft = resolveAutoLeft(badgeOnLeft);
|
|
177
|
+
const autoBottom = xAxis ? DEFAULT_PADDING.bottom : 8;
|
|
178
|
+
if (!override) {
|
|
179
|
+
return {
|
|
180
|
+
...DEFAULT_PADDING,
|
|
181
|
+
right: autoRight,
|
|
182
|
+
left: autoLeft,
|
|
183
|
+
bottom: autoBottom,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
top: override.top ?? DEFAULT_PADDING.top,
|
|
188
|
+
right: override.right ?? autoRight,
|
|
189
|
+
bottom: override.bottom ?? autoBottom,
|
|
190
|
+
left: override.left ?? autoLeft,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build screen-space points as a flat number array [x0, y0, x1, y1, ...].
|
|
196
|
+
* Includes one point before the window for smooth left-edge entry,
|
|
197
|
+
* and appends a live tip at (now, displayValue).
|
|
198
|
+
*
|
|
199
|
+
* Flat layout avoids ~150 tuple object allocations per frame.
|
|
200
|
+
*
|
|
201
|
+
* Pass `out` to reuse a persistent array instead of allocating a fresh one each
|
|
202
|
+
* frame (it is cleared and refilled). The per-frame allocation otherwise scales
|
|
203
|
+
* with the visible point count, so pooling it cuts UI-thread GC pressure on
|
|
204
|
+
* busy / wide-window charts. Callers that pool `out` must ping-pong two buffers
|
|
205
|
+
* so the returned reference still changes each frame (Reanimated's value-equality
|
|
206
|
+
* check skips notifying subscribers when the reference is unchanged).
|
|
207
|
+
*/
|
|
208
|
+
export function buildLinePoints(
|
|
209
|
+
data: LiveChartPoint[],
|
|
210
|
+
displayValue: number,
|
|
211
|
+
now: number,
|
|
212
|
+
windowSecs: number,
|
|
213
|
+
displayMin: number,
|
|
214
|
+
displayMax: number,
|
|
215
|
+
canvasWidth: number,
|
|
216
|
+
canvasHeight: number,
|
|
217
|
+
padding: ChartPadding,
|
|
218
|
+
out?: number[],
|
|
219
|
+
): number[] {
|
|
220
|
+
"worklet";
|
|
221
|
+
const pts: number[] = out ?? [];
|
|
222
|
+
if (out) out.length = 0;
|
|
223
|
+
const chartW = canvasWidth - padding.left - padding.right;
|
|
224
|
+
const chartH = canvasHeight - padding.top - padding.bottom;
|
|
225
|
+
const valRange = displayMax - displayMin;
|
|
226
|
+
|
|
227
|
+
if (valRange === 0 || chartW <= 0 || chartH <= 0 || data.length === 0)
|
|
228
|
+
return pts;
|
|
229
|
+
|
|
230
|
+
const winStart = now - windowSecs;
|
|
231
|
+
|
|
232
|
+
// Binary search for first point >= winStart
|
|
233
|
+
let lo = 0;
|
|
234
|
+
let hi = data.length;
|
|
235
|
+
while (lo < hi) {
|
|
236
|
+
const mid = (lo + hi) >> 1;
|
|
237
|
+
if (data[mid].time < winStart) lo = mid + 1;
|
|
238
|
+
else hi = mid;
|
|
239
|
+
}
|
|
240
|
+
const startIdx = Math.max(0, lo - 1);
|
|
241
|
+
|
|
242
|
+
for (let i = startIdx; i < data.length; i++) {
|
|
243
|
+
if (data[i].time > now) break;
|
|
244
|
+
pts.push(
|
|
245
|
+
padding.left + ((data[i].time - winStart) / windowSecs) * chartW,
|
|
246
|
+
padding.top + ((displayMax - data[i].value) / valRange) * chartH,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Live tip at current time with smoothed value
|
|
251
|
+
pts.push(
|
|
252
|
+
padding.left + chartW,
|
|
253
|
+
padding.top + ((displayMax - displayValue) / valRange) * chartH,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return pts;
|
|
257
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { TradeEvent } from "../types";
|
|
2
|
+
|
|
3
|
+
// ── Output type consumed by the overlay ────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface TradeMarker {
|
|
6
|
+
y: number;
|
|
7
|
+
label: string;
|
|
8
|
+
green: boolean;
|
|
9
|
+
alpha: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Internal state for the scrolling tape ──────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface StreamLabel {
|
|
15
|
+
y: number;
|
|
16
|
+
text: string;
|
|
17
|
+
green: boolean;
|
|
18
|
+
life: number;
|
|
19
|
+
maxLife: number;
|
|
20
|
+
intensity: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TradeStreamState {
|
|
24
|
+
labels: StreamLabel[];
|
|
25
|
+
spawnTimer: number;
|
|
26
|
+
smoothSpeed: number;
|
|
27
|
+
lastSeenTime: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createTradeStreamState(): TradeStreamState {
|
|
31
|
+
return {
|
|
32
|
+
labels: [],
|
|
33
|
+
spawnTimer: 0,
|
|
34
|
+
smoothSpeed: BASE_SPEED,
|
|
35
|
+
lastSeenTime: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Trade stream label layout constants ────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export const MAX_LABELS = 50;
|
|
42
|
+
export const LABEL_LIFETIME = 6;
|
|
43
|
+
const SPAWN_INTERVAL = 40;
|
|
44
|
+
const MIN_LABEL_GAP = 22;
|
|
45
|
+
const BASE_SPEED = 60;
|
|
46
|
+
const MAX_SPEED = 160;
|
|
47
|
+
|
|
48
|
+
// ── Size formatter ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function formatSize(size: number): string {
|
|
51
|
+
"worklet";
|
|
52
|
+
if (size >= 10) return `$${Math.round(size)}`;
|
|
53
|
+
if (size >= 1) return `$${size.toFixed(1)}`;
|
|
54
|
+
return `$${size.toFixed(2)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Tick: spawn, move, expire ──────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Advance the trade stream state by one frame.
|
|
61
|
+
* Spawns labels from new trades at `chartBottom`, scrolls them upward with
|
|
62
|
+
* deceleration, and expires labels that reach the top or run out of life.
|
|
63
|
+
*
|
|
64
|
+
* Pure worklet — no Skia imports. Mutates `state` in place.
|
|
65
|
+
*/
|
|
66
|
+
export function tickTradeStream(
|
|
67
|
+
state: TradeStreamState,
|
|
68
|
+
trades: TradeEvent[],
|
|
69
|
+
dtMs: number,
|
|
70
|
+
chartTop: number,
|
|
71
|
+
chartBottom: number,
|
|
72
|
+
): void {
|
|
73
|
+
"worklet";
|
|
74
|
+
const dtSec = dtMs / 1000;
|
|
75
|
+
const range = chartBottom - chartTop;
|
|
76
|
+
if (range <= 0) return;
|
|
77
|
+
|
|
78
|
+
const newCount = trades.length;
|
|
79
|
+
const newestTime =
|
|
80
|
+
newCount > 0 ? trades[newCount - 1].time : /* istanbul ignore next */ 0;
|
|
81
|
+
const hasNewTrades = newestTime > state.lastSeenTime && newCount > 0;
|
|
82
|
+
|
|
83
|
+
// Count how many trades arrived since last seen time
|
|
84
|
+
let newTradesDelta = 0;
|
|
85
|
+
if (hasNewTrades) {
|
|
86
|
+
for (let i = newCount - 1; i >= 0; i--) {
|
|
87
|
+
if (trades[i].time <= state.lastSeenTime) break;
|
|
88
|
+
newTradesDelta++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const activity = Math.min(newTradesDelta / 5, 1);
|
|
92
|
+
|
|
93
|
+
// Smooth speed lerp (fast attack, slow decay)
|
|
94
|
+
const targetSpeed = BASE_SPEED + activity * (MAX_SPEED - BASE_SPEED);
|
|
95
|
+
const speedLerp = activity > 0 ? 0.3 : 0.05;
|
|
96
|
+
state.smoothSpeed += (targetSpeed - state.smoothSpeed) * speedLerp;
|
|
97
|
+
const speed = state.smoothSpeed;
|
|
98
|
+
|
|
99
|
+
// Find max size among recent trades for intensity normalisation
|
|
100
|
+
let maxSize = 0;
|
|
101
|
+
const recentStart = Math.max(0, newCount - 20);
|
|
102
|
+
for (let i = recentStart; i < newCount; i++) {
|
|
103
|
+
if (trades[i].size > maxSize) maxSize = trades[i].size;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Spawn new labels at bottom
|
|
107
|
+
state.spawnTimer += dtMs;
|
|
108
|
+
while (
|
|
109
|
+
state.spawnTimer >= SPAWN_INTERVAL &&
|
|
110
|
+
hasNewTrades &&
|
|
111
|
+
state.labels.length < MAX_LABELS
|
|
112
|
+
) {
|
|
113
|
+
state.spawnTimer -= SPAWN_INTERVAL;
|
|
114
|
+
|
|
115
|
+
// Overlap check
|
|
116
|
+
let tooClose = false;
|
|
117
|
+
for (let j = 0; j < state.labels.length; j++) {
|
|
118
|
+
if (Math.abs(state.labels[j].y - chartBottom) < MIN_LABEL_GAP) {
|
|
119
|
+
tooClose = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (tooClose) break;
|
|
124
|
+
|
|
125
|
+
// Pick a recent trade (weighted random by size)
|
|
126
|
+
const windowStart = Math.max(0, newCount - 10);
|
|
127
|
+
let totalWeight = 0;
|
|
128
|
+
for (let i = windowStart; i < newCount; i++) totalWeight += trades[i].size;
|
|
129
|
+
let r = Math.random() * totalWeight;
|
|
130
|
+
let picked = trades[newCount - 1];
|
|
131
|
+
for (let i = windowStart; i < newCount; i++) {
|
|
132
|
+
r -= trades[i].size;
|
|
133
|
+
if (r <= 0) {
|
|
134
|
+
picked = trades[i];
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sizeRatio =
|
|
140
|
+
maxSize > 0 ? picked.size / maxSize : /* istanbul ignore next */ 0.5;
|
|
141
|
+
|
|
142
|
+
const sym =
|
|
143
|
+
"symbol" in picked && typeof picked.symbol === "string" && picked.symbol
|
|
144
|
+
? `${picked.symbol} `
|
|
145
|
+
: "";
|
|
146
|
+
state.labels.push({
|
|
147
|
+
y: chartBottom,
|
|
148
|
+
text: `${sym}${picked.side === "buy" ? "+" : "-"} ${formatSize(picked.size)}`,
|
|
149
|
+
green: picked.side === "buy",
|
|
150
|
+
life: LABEL_LIFETIME,
|
|
151
|
+
maxLife: LABEL_LIFETIME,
|
|
152
|
+
intensity: 0.5 + sizeRatio * 0.5,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
state.lastSeenTime = newestTime;
|
|
157
|
+
|
|
158
|
+
// Move labels upward with deceleration + expire
|
|
159
|
+
let writeIdx = 0;
|
|
160
|
+
for (let i = 0; i < state.labels.length; i++) {
|
|
161
|
+
const l = state.labels[i];
|
|
162
|
+
l.life -= dtSec;
|
|
163
|
+
if (l.life <= 0) continue;
|
|
164
|
+
const yProgress = (l.y - chartTop) / range; // 1 at bottom, 0 at top
|
|
165
|
+
l.y -= speed * (0.7 + 0.3 * yProgress) * dtSec;
|
|
166
|
+
if (l.y < chartTop - 14) continue;
|
|
167
|
+
state.labels[writeIdx++] = l;
|
|
168
|
+
}
|
|
169
|
+
state.labels.length = writeIdx;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Project labels for the overlay ─────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Convert internal StreamLabel state to TradeMarker array for the overlay.
|
|
176
|
+
* Computes per-label alpha from fadeIn, fadeOut, and intensity.
|
|
177
|
+
*/
|
|
178
|
+
export function projectLabels(
|
|
179
|
+
state: TradeStreamState,
|
|
180
|
+
chartTop: number,
|
|
181
|
+
chartH: number,
|
|
182
|
+
): TradeMarker[] {
|
|
183
|
+
"worklet";
|
|
184
|
+
const out: TradeMarker[] = [];
|
|
185
|
+
for (let i = 0; i < state.labels.length; i++) {
|
|
186
|
+
const l = state.labels[i];
|
|
187
|
+
const lifeRatio = l.life / l.maxLife;
|
|
188
|
+
const fadeIn = Math.min((1 - lifeRatio) * 10, 1);
|
|
189
|
+
const yRatio =
|
|
190
|
+
chartH > 0 ? (l.y - chartTop) / chartH : /* istanbul ignore next */ 1;
|
|
191
|
+
const fadeOut = yRatio < 0.45 ? yRatio / 0.45 : 1;
|
|
192
|
+
const alpha = l.intensity * fadeIn * fadeOut;
|
|
193
|
+
|
|
194
|
+
out.push({
|
|
195
|
+
y: l.y,
|
|
196
|
+
label: l.text,
|
|
197
|
+
green: l.green,
|
|
198
|
+
alpha: Math.max(0, Math.min(1, alpha)),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type SkFont } from "@shopify/react-native-skia";
|
|
2
|
+
import { type ChartPadding } from "../draw/line";
|
|
3
|
+
import { interpolateAtTime } from "../math/interpolate";
|
|
4
|
+
import type { ScrubSeriesValue, SeriesConfig } from "../types";
|
|
5
|
+
import {
|
|
6
|
+
computeTooltipLayoutMulti,
|
|
7
|
+
HIDDEN_TOOLTIP,
|
|
8
|
+
type TooltipLayout,
|
|
9
|
+
} from "./crosshairShared";
|
|
10
|
+
|
|
11
|
+
export function interpolateSeriesAtTime(
|
|
12
|
+
series: SeriesConfig[],
|
|
13
|
+
time: number,
|
|
14
|
+
): { primary: number | null; seriesValues: ScrubSeriesValue[] } {
|
|
15
|
+
"worklet";
|
|
16
|
+
const seriesValues: ScrubSeriesValue[] = [];
|
|
17
|
+
let primary: number | null = null;
|
|
18
|
+
for (let i = 0; i < series.length; i++) {
|
|
19
|
+
if (series[i].visible === false) continue;
|
|
20
|
+
const v = interpolateAtTime(series[i].data, time);
|
|
21
|
+
if (v === null) continue;
|
|
22
|
+
seriesValues.push({
|
|
23
|
+
id: series[i].id,
|
|
24
|
+
label: series[i].label,
|
|
25
|
+
value: v,
|
|
26
|
+
});
|
|
27
|
+
if (primary === null) primary = v;
|
|
28
|
+
}
|
|
29
|
+
return { primary, seriesValues };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Series-chart scrub primary value at window time — extracted for tests. */
|
|
33
|
+
export function deriveScrubValueSeries(
|
|
34
|
+
scrubActive: boolean,
|
|
35
|
+
scrubTime: number,
|
|
36
|
+
series: SeriesConfig[],
|
|
37
|
+
): number | null {
|
|
38
|
+
"worklet";
|
|
39
|
+
if (!scrubActive || scrubTime < 0) return null;
|
|
40
|
+
return interpolateSeriesAtTime(series, scrubTime).primary;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Series-chart stacked tooltip at scrub time — extracted for tests. */
|
|
44
|
+
export function computeSeriesScrubTooltipLayout(
|
|
45
|
+
scrubActive: boolean,
|
|
46
|
+
scrubX: number,
|
|
47
|
+
scrubTime: number,
|
|
48
|
+
series: SeriesConfig[],
|
|
49
|
+
padding: ChartPadding,
|
|
50
|
+
canvasWidth: number,
|
|
51
|
+
formatValue: (v: number) => string,
|
|
52
|
+
formatTime: (t: number) => string,
|
|
53
|
+
font: SkFont,
|
|
54
|
+
): TooltipLayout {
|
|
55
|
+
"worklet";
|
|
56
|
+
if (!scrubActive || scrubTime < 0) return HIDDEN_TOOLTIP;
|
|
57
|
+
const r = interpolateSeriesAtTime(series, scrubTime);
|
|
58
|
+
if (r.primary === null) return HIDDEN_TOOLTIP;
|
|
59
|
+
const lineObjs: { text: string; dim: boolean }[] = [
|
|
60
|
+
{ text: formatTime(scrubTime), dim: true },
|
|
61
|
+
];
|
|
62
|
+
for (let k = 0; k < r.seriesValues.length; k++) {
|
|
63
|
+
const sv = r.seriesValues[k];
|
|
64
|
+
const label = sv.label ?? sv.id;
|
|
65
|
+
lineObjs.push({
|
|
66
|
+
text: `${label}: ${formatValue(sv.value)}`,
|
|
67
|
+
dim: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return computeTooltipLayoutMulti(
|
|
71
|
+
scrubActive,
|
|
72
|
+
scrubX,
|
|
73
|
+
lineObjs,
|
|
74
|
+
padding,
|
|
75
|
+
canvasWidth,
|
|
76
|
+
font,
|
|
77
|
+
);
|
|
78
|
+
}
|