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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useDerivedValue } from "react-native-reanimated";
|
|
2
|
+
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
3
|
+
import type { ChartPadding } from "../draw/line";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derive the live dot position (right edge of the chart, mapped to current value).
|
|
7
|
+
* Returns `{ dotX, dotY }` as shared values. Coordinates are set to `-100`
|
|
8
|
+
* (off-screen sentinel) when canvas dimensions are unavailable.
|
|
9
|
+
*/
|
|
10
|
+
export function useLiveDot(engine: SingleEngineState, padding: ChartPadding) {
|
|
11
|
+
const dotX = useDerivedValue(() => {
|
|
12
|
+
const w = engine.canvasWidth.value;
|
|
13
|
+
if (w === 0) return -100;
|
|
14
|
+
return w - padding.right;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const dotY = useDerivedValue(() => {
|
|
18
|
+
const h = engine.canvasHeight.value;
|
|
19
|
+
if (h === 0) return -100;
|
|
20
|
+
const chartH = h - padding.top - padding.bottom;
|
|
21
|
+
const dMin = engine.displayMin.value;
|
|
22
|
+
const dMax = engine.displayMax.value;
|
|
23
|
+
const valRange = dMax - dMin;
|
|
24
|
+
if (valRange === 0) return padding.top + chartH / 2;
|
|
25
|
+
return (
|
|
26
|
+
padding.top + ((dMax - engine.displayValue.value) / valRange) * chartH
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return { dotX, dotY } as const;
|
|
31
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { Gesture } from "react-native-gesture-handler";
|
|
3
|
+
import {
|
|
4
|
+
useFrameCallback,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import { runOnJS } from "react-native-worklets";
|
|
9
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
10
|
+
import type { ChartPadding } from "../draw/line";
|
|
11
|
+
import { projectMarkers, type ProjectedMarker } from "../math/markers";
|
|
12
|
+
import { nearestMarkerIndex } from "../math/markers";
|
|
13
|
+
import type {
|
|
14
|
+
LiveChartPoint,
|
|
15
|
+
Marker,
|
|
16
|
+
MarkerHoverEvent,
|
|
17
|
+
SeriesConfig,
|
|
18
|
+
} from "../types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Projects markers to screen positions each frame and builds a tap gesture that
|
|
22
|
+
* hit-tests against them, firing `onMarkerHover` (or `null` on a miss).
|
|
23
|
+
*/
|
|
24
|
+
export function useMarkers(
|
|
25
|
+
engine: ChartEngineLayout,
|
|
26
|
+
padding: ChartPadding,
|
|
27
|
+
markers: SharedValue<Marker[]>,
|
|
28
|
+
active: boolean,
|
|
29
|
+
hitRadius: number,
|
|
30
|
+
onMarkerHover?: (event: MarkerHoverEvent | null) => void,
|
|
31
|
+
seriesSV?: SharedValue<SeriesConfig[]>,
|
|
32
|
+
lineData?: SharedValue<LiveChartPoint[]>,
|
|
33
|
+
): { projected: SharedValue<ProjectedMarker[]>; tapGesture: ReturnType<typeof Gesture.Tap> } {
|
|
34
|
+
const projected = useSharedValue<ProjectedMarker[]>([]);
|
|
35
|
+
const cacheRef = useRef<{
|
|
36
|
+
a: ProjectedMarker[];
|
|
37
|
+
b: ProjectedMarker[];
|
|
38
|
+
tick: boolean;
|
|
39
|
+
} | null>(null);
|
|
40
|
+
if (cacheRef.current === null) {
|
|
41
|
+
cacheRef.current = { a: [] as ProjectedMarker[], b: [] as ProjectedMarker[], tick: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const emitHover =
|
|
45
|
+
/* istanbul ignore next -- invoked only from the UI-thread tap worklet */
|
|
46
|
+
(event: MarkerHoverEvent | null) => {
|
|
47
|
+
onMarkerHover?.(event);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
useFrameCallback(
|
|
51
|
+
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
52
|
+
"worklet";
|
|
53
|
+
const cache = cacheRef.current!;
|
|
54
|
+
if (!active) {
|
|
55
|
+
if (projected.get().length > 0) projected.set([]);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
cache.tick = !cache.tick;
|
|
59
|
+
const buf = cache.tick ? cache.a : cache.b;
|
|
60
|
+
projectMarkers(markers.get(), buf, {
|
|
61
|
+
canvasWidth: engine.canvasWidth.get(),
|
|
62
|
+
canvasHeight: engine.canvasHeight.get(),
|
|
63
|
+
padTop: padding.top,
|
|
64
|
+
padBottom: padding.bottom,
|
|
65
|
+
padLeft: padding.left,
|
|
66
|
+
padRight: padding.right,
|
|
67
|
+
timestamp: engine.timestamp.get(),
|
|
68
|
+
displayWindow: engine.displayWindow.get(),
|
|
69
|
+
displayMin: engine.displayMin.get(),
|
|
70
|
+
displayMax: engine.displayMax.get(),
|
|
71
|
+
series: seriesSV?.get(),
|
|
72
|
+
lineData: lineData?.get(),
|
|
73
|
+
});
|
|
74
|
+
projected.set(buf);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const tapGesture = Gesture.Tap().onEnd(
|
|
79
|
+
/* istanbul ignore next -- gesture worklet runs on UI thread, not in Jest */ (
|
|
80
|
+
e,
|
|
81
|
+
) => {
|
|
82
|
+
"worklet";
|
|
83
|
+
const idx = nearestMarkerIndex(projected.get(), e.x, e.y, hitRadius);
|
|
84
|
+
if (idx < 0) {
|
|
85
|
+
runOnJS(emitHover)(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const m = markers.get()[idx];
|
|
89
|
+
const p = projected.get()[idx];
|
|
90
|
+
runOnJS(emitHover)({ marker: m, point: { x: p.x, y: p.y } });
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return { projected, tapGesture };
|
|
95
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Easing,
|
|
4
|
+
useDerivedValue,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
type SharedValue,
|
|
8
|
+
} from "react-native-reanimated";
|
|
9
|
+
|
|
10
|
+
const MODE_BLEND_DURATION_MS = 300;
|
|
11
|
+
|
|
12
|
+
export interface ModeBlendState {
|
|
13
|
+
/** 0 = fully line, 1 = fully candle */
|
|
14
|
+
modeBlend: SharedValue<number>;
|
|
15
|
+
/** line opacity = reveal.lineOpacity * (1 - modeBlend) */
|
|
16
|
+
lineGroupOpacity: SharedValue<number>;
|
|
17
|
+
/** candle opacity = reveal.lineOpacity * modeBlend */
|
|
18
|
+
candleGroupOpacity: SharedValue<number>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Animated crossfade between line and candle chart layers.
|
|
23
|
+
*
|
|
24
|
+
* `modeBlend` animates 0→1 when `isCandle` becomes true, 1→0 when false.
|
|
25
|
+
* The derived opacities multiply by `lineOpacity` from `useChartReveal`
|
|
26
|
+
* so the reveal and mode-switch animations compose correctly.
|
|
27
|
+
*/
|
|
28
|
+
export function useModeBlend(
|
|
29
|
+
isCandle: boolean,
|
|
30
|
+
lineOpacity: SharedValue<number>,
|
|
31
|
+
): ModeBlendState {
|
|
32
|
+
const modeBlend = useSharedValue(isCandle ? 1 : 0);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
modeBlend.set(
|
|
36
|
+
withTiming(isCandle ? 1 : 0, {
|
|
37
|
+
duration: MODE_BLEND_DURATION_MS,
|
|
38
|
+
easing: Easing.inOut(Easing.ease),
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}, [isCandle, modeBlend]);
|
|
42
|
+
|
|
43
|
+
const lineGroupOpacity = useDerivedValue(
|
|
44
|
+
() => lineOpacity.get() * (1 - modeBlend.get()),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const candleGroupOpacity = useDerivedValue(
|
|
48
|
+
() => lineOpacity.get() * modeBlend.get(),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return { modeBlend, lineGroupOpacity, candleGroupOpacity };
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
2
|
+
import type { SingleEngineState } from "../core/useLiveChartEngine";
|
|
3
|
+
import { detectMomentum } from "../math/momentum";
|
|
4
|
+
import type { Momentum, MomentumConfig } from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the momentum prop to a detected or forced momentum value.
|
|
8
|
+
* - `true` = auto-detect from data with defaults
|
|
9
|
+
* - `false` = disabled, always 'flat'
|
|
10
|
+
* - `'up'` / `'down'` / `'flat'` = forced value
|
|
11
|
+
* - `MomentumConfig` = auto-detect with custom threshold/lookback
|
|
12
|
+
*/
|
|
13
|
+
export function resolveMomentumProp(
|
|
14
|
+
prop: boolean | Momentum | MomentumConfig,
|
|
15
|
+
data: readonly { time: number; value: number }[],
|
|
16
|
+
): Momentum {
|
|
17
|
+
"worklet";
|
|
18
|
+
if (prop === false) return "flat";
|
|
19
|
+
if (prop === true)
|
|
20
|
+
return detectMomentum(data as { time: number; value: number }[]);
|
|
21
|
+
if (typeof prop === "object")
|
|
22
|
+
return detectMomentum(
|
|
23
|
+
data as { time: number; value: number }[],
|
|
24
|
+
prop.lookback,
|
|
25
|
+
prop.threshold,
|
|
26
|
+
);
|
|
27
|
+
return prop;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useMomentum(
|
|
31
|
+
engine: SingleEngineState,
|
|
32
|
+
momentumProp: boolean | Momentum | MomentumConfig = true,
|
|
33
|
+
): SharedValue<Momentum> {
|
|
34
|
+
return useDerivedValue(() =>
|
|
35
|
+
resolveMomentumProp(momentumProp, engine.data.value),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useDerivedValue,
|
|
4
|
+
useFrameCallback,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
type DerivedValue,
|
|
7
|
+
type SharedValue,
|
|
8
|
+
} from "react-native-reanimated";
|
|
9
|
+
import { runOnJS } from "react-native-worklets";
|
|
10
|
+
import { DEGEN_STRIDE } from "../constants";
|
|
11
|
+
import type { ResolvedDegenConfig } from "../core/resolveConfig";
|
|
12
|
+
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
13
|
+
import type { ChartPadding } from "../draw/line";
|
|
14
|
+
import { computeShake, spawnBurst, tickParticles } from "../math/degenTick";
|
|
15
|
+
import { detectMomentum } from "../math/momentum";
|
|
16
|
+
import type { DegenShakePayload } from "../types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Multi-series degen: **every** visible series sparks a burst off its own
|
|
20
|
+
* endpoint dot on an up-momentum swing — in that series' color (its particles
|
|
21
|
+
* carry the series index as their `colorIndex`) — plus one shared chart shake
|
|
22
|
+
* re-armed from the strongest swing each frame. Reuses the single-series
|
|
23
|
+
* particle/shake math (`math/degenTick`).
|
|
24
|
+
*/
|
|
25
|
+
export function useMultiSeriesDegen(
|
|
26
|
+
engine: MultiEngineState,
|
|
27
|
+
padding: ChartPadding,
|
|
28
|
+
cfg: ResolvedDegenConfig | null,
|
|
29
|
+
onShake?: (payload: DegenShakePayload) => void,
|
|
30
|
+
): {
|
|
31
|
+
pack: SharedValue<Float64Array<ArrayBuffer>>;
|
|
32
|
+
packRevision: SharedValue<number>;
|
|
33
|
+
shakeTransform: DerivedValue<
|
|
34
|
+
[{ translateX: number }, { translateY: number }]
|
|
35
|
+
>;
|
|
36
|
+
} {
|
|
37
|
+
const MAX_SLOTS = 80;
|
|
38
|
+
const pack = useSharedValue(new Float64Array(MAX_SLOTS * DEGEN_STRIDE));
|
|
39
|
+
const packRevision = useSharedValue(0);
|
|
40
|
+
// Per-series previous momentum (0 = flat, 1 = up, 2 = down), index-aligned to series.
|
|
41
|
+
const prevMoms = useSharedValue<number[]>([]);
|
|
42
|
+
const writeRot = useSharedValue(0);
|
|
43
|
+
const enabledSV = useSharedValue(0);
|
|
44
|
+
const scaleSV = useSharedValue(1);
|
|
45
|
+
const downSV = useSharedValue(0);
|
|
46
|
+
const shakeEnabledSV = useSharedValue(0);
|
|
47
|
+
const shakeIntensitySV = useSharedValue(1);
|
|
48
|
+
const shakeDurationSecSV = useSharedValue(0.45);
|
|
49
|
+
const slotCountSV = useSharedValue(60);
|
|
50
|
+
const burstParticleCountSV = useSharedValue(20);
|
|
51
|
+
const particleBurstDurationSecSV = useSharedValue(1.0);
|
|
52
|
+
const dragSV = useSharedValue(0.95);
|
|
53
|
+
const sizeMinSV = useSharedValue(1);
|
|
54
|
+
const sizeMaxSV = useSharedValue(2.2);
|
|
55
|
+
const spreadAngleSV = useSharedValue(Math.PI * 1.2);
|
|
56
|
+
const jitterXSV = useSharedValue(24);
|
|
57
|
+
const jitterYSV = useSharedValue(8);
|
|
58
|
+
const speedMinSV = useSharedValue(60);
|
|
59
|
+
const speedMaxSV = useSharedValue(160);
|
|
60
|
+
const shakeX = useSharedValue(0);
|
|
61
|
+
const shakeY = useSharedValue(0);
|
|
62
|
+
const shakeStart = useSharedValue(0);
|
|
63
|
+
const prevTimestamp = useSharedValue(0);
|
|
64
|
+
const prevActiveCount = useSharedValue(0);
|
|
65
|
+
const hasOnShakeListenerSV = useSharedValue(0);
|
|
66
|
+
|
|
67
|
+
const onShakeRef = useRef(onShake);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
onShakeRef.current = onShake;
|
|
70
|
+
});
|
|
71
|
+
const emitShake =
|
|
72
|
+
/* istanbul ignore next -- invoked only from the UI-thread frame worklet */
|
|
73
|
+
(direction: "up" | "down") => {
|
|
74
|
+
onShakeRef.current?.({ direction });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const degenOff = cfg === null;
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
// Mirror the resolved config into SharedValues for the UI-thread worklet.
|
|
81
|
+
// Everything is set unconditionally — no prop-derived `if` branch — so this
|
|
82
|
+
// is a plain external-store sync, not a disguised event handler. When degen
|
|
83
|
+
// is off, enabledSV is 0 and the frame worklet early-returns (zeroing the
|
|
84
|
+
// particle buffer and shake itself), so the remaining values are inert; the
|
|
85
|
+
// `?? default` fallbacks stand in for the null-config case.
|
|
86
|
+
enabledSV.set(degenOff ? 0 : 1);
|
|
87
|
+
shakeEnabledSV.set(cfg?.shake ? 1 : 0);
|
|
88
|
+
hasOnShakeListenerSV.set(!degenOff && onShake != null ? 1 : 0);
|
|
89
|
+
scaleSV.set(cfg?.scale ?? 1);
|
|
90
|
+
downSV.set(cfg?.downMomentum ? 1 : 0);
|
|
91
|
+
shakeIntensitySV.set(cfg?.shakeIntensity ?? 1);
|
|
92
|
+
shakeDurationSecSV.set(cfg?.shakeDurationSec ?? 0.45);
|
|
93
|
+
slotCountSV.set(cfg?.particleSlotCount ?? 60);
|
|
94
|
+
burstParticleCountSV.set(cfg?.burstParticleCount ?? 20);
|
|
95
|
+
particleBurstDurationSecSV.set(cfg?.particleBurstDurationSec ?? 1.0);
|
|
96
|
+
dragSV.set(cfg?.drag ?? 0.95);
|
|
97
|
+
sizeMinSV.set(cfg?.particleSizeMin ?? 1);
|
|
98
|
+
sizeMaxSV.set(cfg?.particleSizeMax ?? 2.2);
|
|
99
|
+
spreadAngleSV.set(cfg?.spreadAngle ?? Math.PI * 1.2);
|
|
100
|
+
jitterXSV.set(cfg?.positionJitterX ?? 24);
|
|
101
|
+
jitterYSV.set(cfg?.positionJitterY ?? 8);
|
|
102
|
+
speedMinSV.set(cfg?.speedMin ?? 60);
|
|
103
|
+
speedMaxSV.set(cfg?.speedMax ?? 160);
|
|
104
|
+
}, [
|
|
105
|
+
degenOff,
|
|
106
|
+
onShake,
|
|
107
|
+
cfg,
|
|
108
|
+
// SharedValue refs are stable (useSharedValue), so listing them satisfies
|
|
109
|
+
// exhaustive-deps without ever re-running the effect.
|
|
110
|
+
enabledSV,
|
|
111
|
+
shakeEnabledSV,
|
|
112
|
+
hasOnShakeListenerSV,
|
|
113
|
+
shakeStart,
|
|
114
|
+
shakeX,
|
|
115
|
+
shakeY,
|
|
116
|
+
scaleSV,
|
|
117
|
+
downSV,
|
|
118
|
+
shakeIntensitySV,
|
|
119
|
+
shakeDurationSecSV,
|
|
120
|
+
slotCountSV,
|
|
121
|
+
burstParticleCountSV,
|
|
122
|
+
particleBurstDurationSecSV,
|
|
123
|
+
dragSV,
|
|
124
|
+
sizeMinSV,
|
|
125
|
+
sizeMaxSV,
|
|
126
|
+
spreadAngleSV,
|
|
127
|
+
jitterXSV,
|
|
128
|
+
jitterYSV,
|
|
129
|
+
speedMinSV,
|
|
130
|
+
speedMaxSV,
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
useFrameCallback(
|
|
134
|
+
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
135
|
+
"worklet";
|
|
136
|
+
const now = engine.timestamp.get();
|
|
137
|
+
const buf = pack.get();
|
|
138
|
+
const slots = slotCountSV.get();
|
|
139
|
+
|
|
140
|
+
const dtSec = prevTimestamp.get() > 0 ? now - prevTimestamp.get() : 0.016;
|
|
141
|
+
prevTimestamp.set(now);
|
|
142
|
+
|
|
143
|
+
const moms = prevMoms.get();
|
|
144
|
+
|
|
145
|
+
if (enabledSV.get() < 0.5) {
|
|
146
|
+
for (let i = 0; i < slots; i++) buf[i * DEGEN_STRIDE + 5] = 0;
|
|
147
|
+
moms.length = 0;
|
|
148
|
+
prevActiveCount.set(0);
|
|
149
|
+
shakeStart.set(0);
|
|
150
|
+
shakeX.set(0);
|
|
151
|
+
shakeY.set(0);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const s = engine.series.get();
|
|
156
|
+
const displays = engine.displaySeriesValues.get();
|
|
157
|
+
const ops = engine.seriesOpacities.get();
|
|
158
|
+
const n = s.length;
|
|
159
|
+
while (moms.length < n) moms.push(0);
|
|
160
|
+
if (moms.length > n) moms.length = n;
|
|
161
|
+
|
|
162
|
+
const cw = engine.canvasWidth.get();
|
|
163
|
+
const ch = engine.canvasHeight.get();
|
|
164
|
+
const canSpawn = cw >= 1 && ch >= 1;
|
|
165
|
+
const dMin = engine.displayMin.get();
|
|
166
|
+
const dMax = engine.displayMax.get();
|
|
167
|
+
const valRange = dMax - dMin;
|
|
168
|
+
const chartH = ch - padding.top - padding.bottom;
|
|
169
|
+
const ox = cw - padding.right;
|
|
170
|
+
const allowDown = downSV.get() > 0.5;
|
|
171
|
+
|
|
172
|
+
// Each visible series fires a burst off its own dot on a fresh swing.
|
|
173
|
+
let firedDir = 0; // 0 none, 1 up, 2 down
|
|
174
|
+
for (let i = 0; i < n; i++) {
|
|
175
|
+
const visible = (ops[i] ?? 0) >= 0.5;
|
|
176
|
+
const m = visible ? detectMomentum(s[i].data) : "flat";
|
|
177
|
+
const code = m === "up" ? 1 : m === "down" ? 2 : 0;
|
|
178
|
+
const prev = moms[i];
|
|
179
|
+
moms[i] = code;
|
|
180
|
+
if (!canSpawn || code === 0 || code === prev) continue;
|
|
181
|
+
const isUp = code === 1;
|
|
182
|
+
if (!isUp && !allowDown) continue;
|
|
183
|
+
const v = displays[i] ?? s[i].value;
|
|
184
|
+
const oy =
|
|
185
|
+
valRange === 0
|
|
186
|
+
? padding.top + chartH / 2
|
|
187
|
+
: padding.top + ((dMax - v) / valRange) * chartH;
|
|
188
|
+
writeRot.set(
|
|
189
|
+
spawnBurst(buf, {
|
|
190
|
+
ox,
|
|
191
|
+
oy,
|
|
192
|
+
sc: scaleSV.get(),
|
|
193
|
+
burst: burstParticleCountSV.get(),
|
|
194
|
+
baseAngle: isUp ? -Math.PI / 2 : Math.PI / 2,
|
|
195
|
+
spread: spreadAngleSV.get(),
|
|
196
|
+
jx: jitterXSV.get(),
|
|
197
|
+
jy: jitterYSV.get(),
|
|
198
|
+
sMin: speedMinSV.get(),
|
|
199
|
+
sMax: speedMaxSV.get(),
|
|
200
|
+
szMin: sizeMinSV.get(),
|
|
201
|
+
szMax: sizeMaxSV.get(),
|
|
202
|
+
now,
|
|
203
|
+
baseRot: writeRot.get(),
|
|
204
|
+
slots,
|
|
205
|
+
colorIndex: i, // color this burst with series i's color
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
// Up swings win the shared shake; otherwise a down swing arms it.
|
|
209
|
+
if (firedDir !== 1) firedDir = isUp ? 1 : 2;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (firedDir !== 0 && shakeEnabledSV.get() > 0.5) {
|
|
213
|
+
shakeStart.set(now);
|
|
214
|
+
if (hasOnShakeListenerSV.get() > 0.5) {
|
|
215
|
+
runOnJS(emitShake)(firedDir === 1 ? "up" : "down");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (shakeStart.get() > 0 && shakeEnabledSV.get() > 0.5) {
|
|
220
|
+
const r = computeShake(
|
|
221
|
+
now - shakeStart.get(),
|
|
222
|
+
shakeDurationSecSV.get(),
|
|
223
|
+
scaleSV.get(),
|
|
224
|
+
shakeIntensitySV.get(),
|
|
225
|
+
);
|
|
226
|
+
shakeX.set(r.x);
|
|
227
|
+
shakeY.set(r.y);
|
|
228
|
+
if (!r.active) shakeStart.set(0);
|
|
229
|
+
} else if (shakeEnabledSV.get() < 0.5) {
|
|
230
|
+
shakeStart.set(0);
|
|
231
|
+
shakeX.set(0);
|
|
232
|
+
shakeY.set(0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const activeCount = tickParticles(
|
|
236
|
+
buf,
|
|
237
|
+
slots,
|
|
238
|
+
now,
|
|
239
|
+
particleBurstDurationSecSV.get(),
|
|
240
|
+
dragSV.get(),
|
|
241
|
+
dtSec,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (activeCount > 0 || prevActiveCount.get() > 0) {
|
|
245
|
+
packRevision.set(packRevision.get() + 1);
|
|
246
|
+
}
|
|
247
|
+
prevActiveCount.set(activeCount);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const shakeTransform = useDerivedValue(() => {
|
|
252
|
+
"worklet";
|
|
253
|
+
return [{ translateX: shakeX.get() }, { translateY: shakeY.get() }] as [
|
|
254
|
+
{ translateX: number },
|
|
255
|
+
{ translateY: number },
|
|
256
|
+
];
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return { pack, packRevision, shakeTransform };
|
|
260
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Skia, type SkPath } from "@shopify/react-native-skia";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
4
|
+
import { MAX_MULTI_SERIES } from "../constants";
|
|
5
|
+
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
6
|
+
import { buildLinePoints, type ChartPadding } from "../draw/line";
|
|
7
|
+
import { drawSpline, makeSplineScratch } from "../math/spline";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One derived shared value holding up to `MAX_MULTI_SERIES` Skia paths (unused slots are empty paths).
|
|
11
|
+
*
|
|
12
|
+
* Each slot ping-pongs between two persistent `SkPath`s so `MultiSeriesStroke`'s
|
|
13
|
+
* per-slot `useDerivedValue(() => paths.value[index])` sees a fresh reference each
|
|
14
|
+
* tick (otherwise Reanimated's setter short-circuits and Skia never repaints).
|
|
15
|
+
* No new JSI-backed `SkPath` is allocated per frame. See {@link useChartPaths}
|
|
16
|
+
* for the memory rationale.
|
|
17
|
+
*/
|
|
18
|
+
export function useMultiSeriesLinePaths(
|
|
19
|
+
engine: MultiEngineState,
|
|
20
|
+
padding: ChartPadding,
|
|
21
|
+
): SharedValue<ReturnType<typeof Skia.Path.Make>[]> {
|
|
22
|
+
const poolRef = useRef<{
|
|
23
|
+
a: SkPath[];
|
|
24
|
+
b: SkPath[];
|
|
25
|
+
tick: boolean;
|
|
26
|
+
ptsBuf: number[];
|
|
27
|
+
scratch: ReturnType<typeof makeSplineScratch>;
|
|
28
|
+
} | null>(null);
|
|
29
|
+
if (poolRef.current === null) {
|
|
30
|
+
const a: SkPath[] = [];
|
|
31
|
+
const b: SkPath[] = [];
|
|
32
|
+
for (let i = 0; i < MAX_MULTI_SERIES; i++) {
|
|
33
|
+
a.push(Skia.Path.Make());
|
|
34
|
+
b.push(Skia.Path.Make());
|
|
35
|
+
}
|
|
36
|
+
// One point buffer + spline scratch, reused across slots (built sequentially)
|
|
37
|
+
// and across frames — no per-frame, per-series array allocation.
|
|
38
|
+
poolRef.current = { a, b, tick: false, ptsBuf: [] as number[], scratch: makeSplineScratch() };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return useDerivedValue(() => {
|
|
42
|
+
const pool = poolRef.current!;
|
|
43
|
+
pool.tick = !pool.tick;
|
|
44
|
+
const slots = pool.tick ? pool.a : pool.b;
|
|
45
|
+
const s = engine.series.get();
|
|
46
|
+
const displays = engine.displaySeriesValues.get();
|
|
47
|
+
const out: ReturnType<typeof Skia.Path.Make>[] = [];
|
|
48
|
+
for (let i = 0; i < MAX_MULTI_SERIES; i++) {
|
|
49
|
+
const path = slots[i];
|
|
50
|
+
path.reset();
|
|
51
|
+
if (i >= s.length) {
|
|
52
|
+
out.push(path);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const pts = buildLinePoints(
|
|
56
|
+
s[i].data,
|
|
57
|
+
displays[i] ?? s[i].value,
|
|
58
|
+
engine.timestamp.get(),
|
|
59
|
+
engine.displayWindow.get(),
|
|
60
|
+
engine.displayMin.get(),
|
|
61
|
+
engine.displayMax.get(),
|
|
62
|
+
engine.canvasWidth.get(),
|
|
63
|
+
engine.canvasHeight.get(),
|
|
64
|
+
padding,
|
|
65
|
+
pool.ptsBuf,
|
|
66
|
+
);
|
|
67
|
+
const n = pts.length >> 1;
|
|
68
|
+
if (n >= 2) {
|
|
69
|
+
path.moveTo(pts[0], pts[1]);
|
|
70
|
+
drawSpline(path, pts, pool.scratch);
|
|
71
|
+
}
|
|
72
|
+
out.push(path);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
});
|
|
76
|
+
}
|