react-native-livechart 4.18.0 → 4.20.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/dist/components/CrosshairOverlay.d.ts +3 -1
- package/dist/components/CrosshairOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +4 -2
- package/dist/components/DotOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts +2 -2
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts +2 -2
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +5 -3
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +19 -3
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/draw/candle.d.ts +21 -0
- package/dist/draw/candle.d.ts.map +1 -1
- package/dist/draw/grid.d.ts +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/line.d.ts +7 -1
- package/dist/draw/line.d.ts.map +1 -1
- package/dist/hooks/resolveChartLayout.d.ts +8 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +3 -1
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +3 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useDegen.d.ts +1 -3
- package/dist/hooks/useDegen.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts +3 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePinchZoom.d.ts +7 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -1
- package/dist/hooks/useSeriesIndicatorOpacity.d.ts +13 -0
- package/dist/hooks/useSeriesIndicatorOpacity.d.ts.map +1 -0
- package/dist/hooks/useTradeStream.d.ts.map +1 -1
- package/dist/hooks/useYAxis.d.ts +1 -1
- package/dist/hooks/useYAxis.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/simplify.d.ts +20 -0
- package/dist/math/simplify.d.ts.map +1 -0
- package/dist/types.d.ts +101 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/CrosshairOverlay.tsx +14 -9
- package/src/components/DotOverlay.tsx +17 -1
- package/src/components/LiveChart.tsx +227 -67
- package/src/components/LiveChartSeries.tsx +56 -16
- package/src/components/MultiSeriesDots.tsx +22 -3
- package/src/constants.ts +7 -0
- package/src/core/resolveConfig.ts +72 -14
- package/src/core/useLiveChartEngine.ts +6 -1
- package/src/draw/candle.ts +78 -0
- package/src/draw/grid.ts +34 -4
- package/src/draw/line.ts +20 -7
- package/src/hooks/resolveChartLayout.ts +30 -18
- package/src/hooks/useCandlePaths.ts +9 -1
- package/src/hooks/useChartPaths.ts +37 -7
- package/src/hooks/useDegen.ts +2 -6
- package/src/hooks/useMarkers.ts +6 -2
- package/src/hooks/useMultiSeriesLinePaths.ts +31 -4
- package/src/hooks/usePinchZoom.ts +21 -0
- package/src/hooks/useSeriesIndicatorOpacity.ts +56 -0
- package/src/hooks/useTradeStream.ts +6 -1
- package/src/hooks/useYAxis.ts +2 -0
- package/src/index.ts +2 -0
- package/src/math/simplify.ts +197 -0
- package/src/types.ts +103 -16
|
@@ -7,6 +7,10 @@ import type {
|
|
|
7
7
|
SingleEngineState,
|
|
8
8
|
} from "../core/useLiveChartEngine";
|
|
9
9
|
import { buildLinePoints, type ChartPadding } from "../draw/line";
|
|
10
|
+
import {
|
|
11
|
+
makeLineSimplifyScratch,
|
|
12
|
+
simplifyLinePoints,
|
|
13
|
+
} from "../math/simplify";
|
|
10
14
|
import { drawSpline, makeSplineScratch } from "../math/spline";
|
|
11
15
|
import { sampleThresholdYAt, thresholdSampleSpanX } from "../math/threshold";
|
|
12
16
|
import { blendPtsY, squigglifyPts } from "../math/squiggly";
|
|
@@ -52,6 +56,8 @@ export function useChartPaths(
|
|
|
52
56
|
* `samples[]` (so band geometry matches the shader exactly). Takes precedence
|
|
53
57
|
* over `thresholdY`, the constant (horizontal) case. */
|
|
54
58
|
thresholdSamples?: SharedValue<number[]>,
|
|
59
|
+
/** Screen-space path simplification tolerance in pixels. `0` disables it. */
|
|
60
|
+
simplifyTolerance = 0,
|
|
55
61
|
) {
|
|
56
62
|
const lineBuilder = usePathBuilder();
|
|
57
63
|
const fillBuilder = usePathBuilder();
|
|
@@ -61,24 +67,28 @@ export function useChartPaths(
|
|
|
61
67
|
emptyPath: SkPath;
|
|
62
68
|
ptsA: number[];
|
|
63
69
|
ptsB: number[];
|
|
70
|
+
rawPts: number[];
|
|
64
71
|
ptsTick: boolean;
|
|
65
72
|
squigglePts: number[];
|
|
66
73
|
morphA: number[];
|
|
67
74
|
morphB: number[];
|
|
68
75
|
morphTick: boolean;
|
|
69
76
|
scratch: ReturnType<typeof makeSplineScratch>;
|
|
77
|
+
simplifyScratch: ReturnType<typeof makeLineSimplifyScratch>;
|
|
70
78
|
} | null>(null);
|
|
71
79
|
if (cacheRef.current === null) {
|
|
72
80
|
cacheRef.current = {
|
|
73
81
|
emptyPath: Skia.Path.Make(),
|
|
74
82
|
ptsA: [] as number[],
|
|
75
83
|
ptsB: [] as number[],
|
|
84
|
+
rawPts: [] as number[],
|
|
76
85
|
ptsTick: false,
|
|
77
86
|
squigglePts: [] as number[],
|
|
78
87
|
morphA: [] as number[],
|
|
79
88
|
morphB: [] as number[],
|
|
80
89
|
morphTick: false,
|
|
81
90
|
scratch: makeSplineScratch(),
|
|
91
|
+
simplifyScratch: makeLineSimplifyScratch(),
|
|
82
92
|
};
|
|
83
93
|
}
|
|
84
94
|
|
|
@@ -94,32 +104,52 @@ export function useChartPaths(
|
|
|
94
104
|
engine.edgeValue.get(),
|
|
95
105
|
engine.viewEnd.get(),
|
|
96
106
|
);
|
|
107
|
+
const simplify =
|
|
108
|
+
Number.isFinite(simplifyTolerance) && simplifyTolerance > 0
|
|
109
|
+
? simplifyTolerance
|
|
110
|
+
: 0;
|
|
111
|
+
const now = engine.timestamp.get();
|
|
112
|
+
const windowSecs = engine.displayWindow.get();
|
|
113
|
+
const canvasWidth = engine.canvasWidth.get();
|
|
97
114
|
const realPts = buildLinePoints(
|
|
98
115
|
engine.data.get(),
|
|
99
116
|
tipValue,
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
now,
|
|
118
|
+
windowSecs,
|
|
102
119
|
engine.displayMin.get(),
|
|
103
120
|
engine.displayMax.get(),
|
|
104
|
-
|
|
121
|
+
canvasWidth,
|
|
105
122
|
engine.canvasHeight.get(),
|
|
106
123
|
padding,
|
|
107
|
-
buf,
|
|
124
|
+
simplify > 0 ? cache.rawPts : buf,
|
|
108
125
|
// Only a live-following chart may extend the line to the right edge; a
|
|
109
126
|
// parked (scrolled-back / overscrolled) window ends at its last real
|
|
110
127
|
// point rather than fabricating a flat line into dataless space.
|
|
111
128
|
engine.viewEnd.get() != null,
|
|
112
129
|
);
|
|
130
|
+
const chartWidth = canvasWidth - padding.left - padding.right;
|
|
131
|
+
const absoluteXOffset =
|
|
132
|
+
windowSecs > 0 ? (now - windowSecs) * (chartWidth / windowSecs) : 0;
|
|
133
|
+
const renderPts =
|
|
134
|
+
simplify > 0
|
|
135
|
+
? simplifyLinePoints(
|
|
136
|
+
realPts,
|
|
137
|
+
simplify,
|
|
138
|
+
buf,
|
|
139
|
+
cache.simplifyScratch,
|
|
140
|
+
absoluteXOffset,
|
|
141
|
+
)
|
|
142
|
+
: realPts;
|
|
113
143
|
|
|
114
144
|
// Skip blending when fully revealed or no morphT provided
|
|
115
145
|
const t = morphT?.get() ?? 1;
|
|
116
|
-
if (t >= 1 ||
|
|
146
|
+
if (t >= 1 || renderPts.length === 0) return renderPts;
|
|
117
147
|
|
|
118
148
|
// Compute squiggly Y values at the same X positions as the real line
|
|
119
149
|
const centerY =
|
|
120
150
|
(engine.canvasHeight.get() - padding.bottom + padding.top) / 2;
|
|
121
151
|
const squigglyPts = squigglifyPts(
|
|
122
|
-
|
|
152
|
+
renderPts,
|
|
123
153
|
engine.timestamp.get(),
|
|
124
154
|
centerY,
|
|
125
155
|
squiggleAmplitude,
|
|
@@ -131,7 +161,7 @@ export function useChartPaths(
|
|
|
131
161
|
cache.morphTick = !cache.morphTick;
|
|
132
162
|
return blendPtsY(
|
|
133
163
|
squigglyPts,
|
|
134
|
-
|
|
164
|
+
renderPts,
|
|
135
165
|
t,
|
|
136
166
|
padding,
|
|
137
167
|
engine.canvasWidth.get(),
|
package/src/hooks/useDegen.ts
CHANGED
|
@@ -20,8 +20,6 @@ export function useDegen(
|
|
|
20
20
|
momentumSV: SharedValue<Momentum>,
|
|
21
21
|
cfg: ResolvedDegenConfig | null,
|
|
22
22
|
onShake?: (payload: DegenShakePayload) => void,
|
|
23
|
-
/** Static charts run no loops: register the frame callback without starting it. */
|
|
24
|
-
isStatic = false,
|
|
25
23
|
): {
|
|
26
24
|
pack: SharedValue<Float64Array<ArrayBuffer>>;
|
|
27
25
|
packRevision: SharedValue<number>;
|
|
@@ -154,6 +152,8 @@ export function useDegen(
|
|
|
154
152
|
speedMaxSV,
|
|
155
153
|
]);
|
|
156
154
|
|
|
155
|
+
// ChartWithDegen only mounts while the resolved effect is enabled; static
|
|
156
|
+
// charts force that config to null and unmount this callback entirely.
|
|
157
157
|
useFrameCallback(
|
|
158
158
|
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
159
159
|
"worklet";
|
|
@@ -250,10 +250,6 @@ export function useDegen(
|
|
|
250
250
|
}
|
|
251
251
|
prevActiveCount.set(activeCount);
|
|
252
252
|
},
|
|
253
|
-
// Static charts register the callback but never start it, so no degen frame
|
|
254
|
-
// loop runs. Live charts keep the default (autostart) loop unchanged. The
|
|
255
|
-
// controller also forces `cfg` to null in static, so degen is doubly off.
|
|
256
|
-
!isStatic,
|
|
257
253
|
);
|
|
258
254
|
|
|
259
255
|
const shakeTransform = useDerivedValue(() => {
|
package/src/hooks/useMarkers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef } from "react";
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
2
|
import { Gesture } from "react-native-gesture-handler";
|
|
3
3
|
import {
|
|
4
4
|
useFrameCallback,
|
|
@@ -77,7 +77,7 @@ export function useMarkers(
|
|
|
77
77
|
onMarkerPress?.(event);
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
useFrameCallback(
|
|
80
|
+
const markerFrameCallback = useFrameCallback(
|
|
81
81
|
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ () => {
|
|
82
82
|
"worklet";
|
|
83
83
|
const cache = cacheRef.current!;
|
|
@@ -111,6 +111,10 @@ export function useMarkers(
|
|
|
111
111
|
},
|
|
112
112
|
autostart,
|
|
113
113
|
);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
// Reanimated does not reactively apply later `autostart` values.
|
|
116
|
+
markerFrameCallback.setActive(autostart);
|
|
117
|
+
}, [autostart, markerFrameCallback]);
|
|
114
118
|
|
|
115
119
|
const tapGesture = Gesture.Tap().onEnd(
|
|
116
120
|
/* istanbul ignore next -- gesture worklet runs on UI thread, not in Jest */ (
|
|
@@ -4,6 +4,10 @@ import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
|
4
4
|
import { MAX_MULTI_SERIES } from "../constants";
|
|
5
5
|
import type { MultiEngineState } from "../core/useLiveChartEngine";
|
|
6
6
|
import { buildLinePoints, type ChartPadding } from "../draw/line";
|
|
7
|
+
import {
|
|
8
|
+
makeLineSimplifyScratch,
|
|
9
|
+
simplifyLinePoints,
|
|
10
|
+
} from "../math/simplify";
|
|
7
11
|
import { drawSpline, makeSplineScratch } from "../math/spline";
|
|
8
12
|
import { usePathBuilders } from "./usePathBuilder";
|
|
9
13
|
|
|
@@ -21,25 +25,31 @@ export function useMultiSeriesLinePaths(
|
|
|
21
25
|
engine: MultiEngineState,
|
|
22
26
|
padding: ChartPadding,
|
|
23
27
|
activeSeriesCount = MAX_MULTI_SERIES,
|
|
28
|
+
/** Shared screen-space simplification tolerance; a series can override it. */
|
|
29
|
+
simplifyTolerance = 0,
|
|
24
30
|
): SharedValue<SkPath[]> {
|
|
25
31
|
const builders = usePathBuilders(MAX_MULTI_SERIES);
|
|
26
32
|
|
|
27
33
|
const poolRef = useRef<{
|
|
28
34
|
empty: SkPath;
|
|
29
35
|
ptsBuf: number[];
|
|
36
|
+
simplifiedPts: number[];
|
|
30
37
|
pathsA: SkPath[];
|
|
31
38
|
pathsB: SkPath[];
|
|
32
39
|
pathsTick: boolean;
|
|
33
40
|
scratch: ReturnType<typeof makeSplineScratch>;
|
|
41
|
+
simplifyScratch: ReturnType<typeof makeLineSimplifyScratch>;
|
|
34
42
|
} | null>(null);
|
|
35
43
|
if (poolRef.current === null) {
|
|
36
44
|
poolRef.current = {
|
|
37
45
|
empty: Skia.Path.Make(),
|
|
38
46
|
ptsBuf: [] as number[],
|
|
47
|
+
simplifiedPts: [] as number[],
|
|
39
48
|
pathsA: [] as SkPath[],
|
|
40
49
|
pathsB: [] as SkPath[],
|
|
41
50
|
pathsTick: false,
|
|
42
51
|
scratch: makeSplineScratch(),
|
|
52
|
+
simplifyScratch: makeLineSimplifyScratch(),
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -52,19 +62,36 @@ export function useMultiSeriesLinePaths(
|
|
|
52
62
|
const out = pool.pathsTick ? pool.pathsA : pool.pathsB;
|
|
53
63
|
out.length = 0;
|
|
54
64
|
const count = Math.min(activeSeriesCount, s.length, MAX_MULTI_SERIES);
|
|
65
|
+
const now = engine.timestamp.get();
|
|
66
|
+
const windowSecs = engine.displayWindow.get();
|
|
67
|
+
const canvasWidth = engine.canvasWidth.get();
|
|
68
|
+
const chartWidth = canvasWidth - padding.left - padding.right;
|
|
69
|
+
const absoluteXOffset =
|
|
70
|
+
windowSecs > 0 ? (now - windowSecs) * (chartWidth / windowSecs) : 0;
|
|
55
71
|
for (let i = 0; i < count; i++) {
|
|
56
|
-
const
|
|
72
|
+
const rawPts = buildLinePoints(
|
|
57
73
|
s[i].data,
|
|
58
74
|
displays[i] ?? s[i].value,
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
now,
|
|
76
|
+
windowSecs,
|
|
61
77
|
engine.displayMin.get(),
|
|
62
78
|
engine.displayMax.get(),
|
|
63
|
-
|
|
79
|
+
canvasWidth,
|
|
64
80
|
engine.canvasHeight.get(),
|
|
65
81
|
padding,
|
|
66
82
|
pool.ptsBuf,
|
|
67
83
|
);
|
|
84
|
+
const seriesTolerance = s[i].simplify ?? simplifyTolerance;
|
|
85
|
+
const pts =
|
|
86
|
+
Number.isFinite(seriesTolerance) && seriesTolerance > 0
|
|
87
|
+
? simplifyLinePoints(
|
|
88
|
+
rawPts,
|
|
89
|
+
seriesTolerance,
|
|
90
|
+
pool.simplifiedPts,
|
|
91
|
+
pool.simplifyScratch,
|
|
92
|
+
absoluteXOffset,
|
|
93
|
+
)
|
|
94
|
+
: rawPts;
|
|
68
95
|
const n = pts.length >> 1;
|
|
69
96
|
if (n >= 2) {
|
|
70
97
|
const b = slots[i];
|
|
@@ -26,6 +26,27 @@ export interface PinchZoomEngineRefs {
|
|
|
26
26
|
canvasWidth: SharedValue<number>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** The pinch state cleared by the public chart handle's `resetZoom()` method. */
|
|
30
|
+
export type PinchZoomResetRefs = Pick<
|
|
31
|
+
PinchZoomEngineRefs,
|
|
32
|
+
"viewWindow" | "viewEnd"
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clear the focal-anchored pinch overrides so the engine follows its configured
|
|
37
|
+
* `timeWindow` and live edge again. Safe to call when the chart is already reset.
|
|
38
|
+
*/
|
|
39
|
+
export function resetPinchZoom({
|
|
40
|
+
viewWindow,
|
|
41
|
+
viewEnd,
|
|
42
|
+
}: PinchZoomResetRefs): void {
|
|
43
|
+
"worklet";
|
|
44
|
+
cancelAnimation(viewWindow);
|
|
45
|
+
cancelAnimation(viewEnd);
|
|
46
|
+
viewWindow.set(null);
|
|
47
|
+
viewEnd.set(null);
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
export interface UsePinchZoomOptions {
|
|
30
51
|
engine: PinchZoomEngineRefs;
|
|
31
52
|
padding: ChartPadding;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Easing,
|
|
3
|
+
useAnimatedReaction,
|
|
4
|
+
useSharedValue,
|
|
5
|
+
withTiming,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
|
|
9
|
+
/** Short independent fade used when replacement data dims the plotted series. */
|
|
10
|
+
export const SERIES_INDICATOR_FADE_MS = 180;
|
|
11
|
+
|
|
12
|
+
/** Treat tiny animation residue at the top end as fully restored. */
|
|
13
|
+
const SERIES_OPACITY_VISIBLE_EPSILON = 0.001;
|
|
14
|
+
|
|
15
|
+
/** Worklet-safe target for live dots and their pulse rings. */
|
|
16
|
+
export function seriesIndicatorOpacityTarget(seriesOpacity: number): 0 | 1 {
|
|
17
|
+
"worklet";
|
|
18
|
+
return seriesOpacity >= 1 - SERIES_OPACITY_VISIBLE_EPSILON ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fades live dots and pulse rings away while `seriesOpacity` dims replacement
|
|
23
|
+
* data, then restores them once the plotted series is fully opaque again.
|
|
24
|
+
* Keeping this as a separate opacity avoids compositing a translucent dot over
|
|
25
|
+
* its own line, where the stroke would otherwise shine through.
|
|
26
|
+
*/
|
|
27
|
+
export function useSeriesIndicatorOpacity(
|
|
28
|
+
seriesOpacity: SharedValue<number>,
|
|
29
|
+
duration: number = SERIES_INDICATOR_FADE_MS,
|
|
30
|
+
): SharedValue<number> {
|
|
31
|
+
// Fully visible is the common mount state. The reaction snaps an initially
|
|
32
|
+
// dimmed series to 0 on its first UI-thread run so it never flashes a dot.
|
|
33
|
+
const opacity = useSharedValue(1);
|
|
34
|
+
|
|
35
|
+
useAnimatedReaction(
|
|
36
|
+
() => seriesIndicatorOpacityTarget(seriesOpacity.get()),
|
|
37
|
+
(target, previous) => {
|
|
38
|
+
"worklet";
|
|
39
|
+
if (previous === null || previous === undefined || duration === 0) {
|
|
40
|
+
opacity.set(target);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (target !== previous) {
|
|
44
|
+
opacity.set(
|
|
45
|
+
withTiming(target, {
|
|
46
|
+
duration,
|
|
47
|
+
easing: Easing.inOut(Easing.ease),
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[seriesOpacity, duration],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return opacity;
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import {
|
|
2
3
|
useFrameCallback,
|
|
3
4
|
useSharedValue,
|
|
@@ -31,7 +32,7 @@ export function useTradeStream(
|
|
|
31
32
|
const markers = useSharedValue<TradeMarker[]>([]);
|
|
32
33
|
const state = useSharedValue<TradeStreamState>(createTradeStreamState());
|
|
33
34
|
|
|
34
|
-
useFrameCallback(
|
|
35
|
+
const tradeFrameCallback = useFrameCallback(
|
|
35
36
|
/* istanbul ignore next -- worklet runs on UI thread, not in Jest */ (
|
|
36
37
|
frameInfo,
|
|
37
38
|
) => {
|
|
@@ -62,6 +63,10 @@ export function useTradeStream(
|
|
|
62
63
|
},
|
|
63
64
|
autostart,
|
|
64
65
|
);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
// Reanimated treats `autostart` as mount-time state; synchronize changes.
|
|
68
|
+
tradeFrameCallback.setActive(autostart);
|
|
69
|
+
}, [autostart, tradeFrameCallback]);
|
|
65
70
|
|
|
66
71
|
return markers;
|
|
67
72
|
}
|
package/src/hooks/useYAxis.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function useYAxis(
|
|
|
20
20
|
minGap = 36,
|
|
21
21
|
gridMetrics: GridMetrics = GRID_METRICS_DEFAULTS,
|
|
22
22
|
count = 0,
|
|
23
|
+
intervalScale = 1,
|
|
23
24
|
) {
|
|
24
25
|
const prevInterval = useSharedValue(0);
|
|
25
26
|
const labelAlphas = useSharedValue<Record<number, number>>({});
|
|
@@ -41,6 +42,7 @@ export function useYAxis(
|
|
|
41
42
|
minGap,
|
|
42
43
|
gridMetrics,
|
|
43
44
|
count,
|
|
45
|
+
intervalScale,
|
|
44
46
|
);
|
|
45
47
|
|
|
46
48
|
prevInterval.set(result.interval);
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type {
|
|
|
49
49
|
ChartSegment,
|
|
50
50
|
DegenOptions,
|
|
51
51
|
DegenShakePayload,
|
|
52
|
+
DotGlowConfig,
|
|
52
53
|
EmptyStateMetrics,
|
|
53
54
|
FontConfig,
|
|
54
55
|
FontWeight,
|
|
@@ -60,6 +61,7 @@ export type {
|
|
|
60
61
|
LegendStyle,
|
|
61
62
|
LineConfig,
|
|
62
63
|
LiveChartCoreProps,
|
|
64
|
+
LiveChartHandle,
|
|
63
65
|
LiveChartMetrics,
|
|
64
66
|
LiveChartMetricsOverride,
|
|
65
67
|
LiveChartPalette,
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/** Reusable buffers for {@link simplifyLinePoints}. */
|
|
2
|
+
export interface LineSimplifyScratch {
|
|
3
|
+
keep: number[];
|
|
4
|
+
stack: number[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Allocate simplification scratch once, outside the per-frame worklet. */
|
|
8
|
+
export function makeLineSimplifyScratch(): LineSimplifyScratch {
|
|
9
|
+
return { keep: [], stack: [] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum number of points simplified as one Ramer-Douglas-Peucker range.
|
|
14
|
+
*
|
|
15
|
+
* The visible line is already bounded to roughly two points per horizontal
|
|
16
|
+
* pixel. Splitting that bounded input into overlapping ranges also bounds the
|
|
17
|
+
* simplifier's worst-case work: a pathological zig-zag cannot turn a frame into
|
|
18
|
+
* an O(n²) scan. Range endpoints are retained, so chunking can only keep a few
|
|
19
|
+
* extra points; it cannot erase additional structure.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_RDP_RANGE_POINTS = 64;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Width of each RDP range on the stable, absolute screen-pixel grid.
|
|
25
|
+
*
|
|
26
|
+
* Dense line input contains at most two representatives per pixel bucket. A
|
|
27
|
+
* 30 px range therefore stays within the 64-point work bound even when the
|
|
28
|
+
* decimator and simplifier grids have different phases. Fixed-width ranges
|
|
29
|
+
* also keep their membership stable as the viewport advances.
|
|
30
|
+
*/
|
|
31
|
+
const RDP_RANGE_WIDTH_PX = 30;
|
|
32
|
+
|
|
33
|
+
function pointSegmentDistanceSq(
|
|
34
|
+
px: number,
|
|
35
|
+
py: number,
|
|
36
|
+
ax: number,
|
|
37
|
+
ay: number,
|
|
38
|
+
bx: number,
|
|
39
|
+
by: number,
|
|
40
|
+
): number {
|
|
41
|
+
"worklet";
|
|
42
|
+
const dx = bx - ax;
|
|
43
|
+
const dy = by - ay;
|
|
44
|
+
const lengthSq = dx * dx + dy * dy;
|
|
45
|
+
if (lengthSq === 0) {
|
|
46
|
+
const ex = px - ax;
|
|
47
|
+
const ey = py - ay;
|
|
48
|
+
return ex * ex + ey * ey;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let t = ((px - ax) * dx + (py - ay) * dy) / lengthSq;
|
|
52
|
+
if (t < 0) t = 0;
|
|
53
|
+
else if (t > 1) t = 1;
|
|
54
|
+
const ex = px - (ax + t * dx);
|
|
55
|
+
const ey = py - (ay + t * dy);
|
|
56
|
+
return ex * ex + ey * ey;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function simplifyRdpGroup(
|
|
60
|
+
points: number[],
|
|
61
|
+
keep: number[],
|
|
62
|
+
stack: number[],
|
|
63
|
+
toleranceSq: number,
|
|
64
|
+
rangeStart: number,
|
|
65
|
+
rangeEnd: number,
|
|
66
|
+
): void {
|
|
67
|
+
"worklet";
|
|
68
|
+
if (rangeStart === rangeEnd) {
|
|
69
|
+
keep[rangeStart] = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Oversized fixed-X groups are split into overlapping subranges. The group
|
|
74
|
+
// contents are themselves stable, so this point-count guard cannot move
|
|
75
|
+
// until that absolute group reaches a viewport edge.
|
|
76
|
+
for (let chunkStart = rangeStart; chunkStart < rangeEnd; ) {
|
|
77
|
+
const chunkEnd = Math.min(
|
|
78
|
+
chunkStart + MAX_RDP_RANGE_POINTS - 1,
|
|
79
|
+
rangeEnd,
|
|
80
|
+
);
|
|
81
|
+
keep[chunkStart] = 1;
|
|
82
|
+
keep[chunkEnd] = 1;
|
|
83
|
+
stack.push(chunkStart, chunkEnd);
|
|
84
|
+
|
|
85
|
+
while (stack.length >= 2) {
|
|
86
|
+
const end = stack.pop()!;
|
|
87
|
+
const start = stack.pop()!;
|
|
88
|
+
if (end <= start + 1) continue;
|
|
89
|
+
|
|
90
|
+
const ax = points[start * 2];
|
|
91
|
+
const ay = points[start * 2 + 1];
|
|
92
|
+
const bx = points[end * 2];
|
|
93
|
+
const by = points[end * 2 + 1];
|
|
94
|
+
let furthest = -1;
|
|
95
|
+
let maxDistanceSq = toleranceSq;
|
|
96
|
+
|
|
97
|
+
for (let i = start + 1; i < end; i++) {
|
|
98
|
+
const distanceSq = pointSegmentDistanceSq(
|
|
99
|
+
points[i * 2],
|
|
100
|
+
points[i * 2 + 1],
|
|
101
|
+
ax,
|
|
102
|
+
ay,
|
|
103
|
+
bx,
|
|
104
|
+
by,
|
|
105
|
+
);
|
|
106
|
+
if (distanceSq > maxDistanceSq) {
|
|
107
|
+
maxDistanceSq = distanceSq;
|
|
108
|
+
furthest = i;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (furthest >= 0) {
|
|
113
|
+
keep[furthest] = 1;
|
|
114
|
+
stack.push(start, furthest, furthest, end);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
chunkStart = chunkEnd;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Simplify flat screen-space points (`[x0, y0, x1, y1, ...]`) with a bounded
|
|
124
|
+
* Ramer-Douglas-Peucker pass.
|
|
125
|
+
*
|
|
126
|
+
* `tolerance` is the maximum removable deviation in pixels. The first and last
|
|
127
|
+
* point, larger peaks/valleys, and the original point order are retained. Pass
|
|
128
|
+
* distinct reusable `out` and `scratch` buffers to avoid per-frame allocation.
|
|
129
|
+
* `absoluteXOffset` maps the viewport-relative X coordinates back onto a fixed
|
|
130
|
+
* screen-pixel grid; live chart callers should pass `winStart * xScale` so
|
|
131
|
+
* historical range membership does not change as the window advances.
|
|
132
|
+
*/
|
|
133
|
+
export function simplifyLinePoints(
|
|
134
|
+
points: number[],
|
|
135
|
+
tolerance: number,
|
|
136
|
+
out: number[],
|
|
137
|
+
scratch: LineSimplifyScratch,
|
|
138
|
+
absoluteXOffset = 0,
|
|
139
|
+
): number[] {
|
|
140
|
+
"worklet";
|
|
141
|
+
if (out === points) return points;
|
|
142
|
+
|
|
143
|
+
out.length = 0;
|
|
144
|
+
const count = points.length >> 1;
|
|
145
|
+
if (!(tolerance > 0) || !Number.isFinite(tolerance) || count <= 2) {
|
|
146
|
+
for (let i = 0; i < count * 2; i++) out.push(points[i]);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const keep = scratch.keep;
|
|
151
|
+
keep.length = count;
|
|
152
|
+
for (let i = 0; i < count; i++) keep[i] = 0;
|
|
153
|
+
|
|
154
|
+
const stack = scratch.stack;
|
|
155
|
+
stack.length = 0;
|
|
156
|
+
const toleranceSq = tolerance * tolerance;
|
|
157
|
+
const stableXOffset = Number.isFinite(absoluteXOffset) ? absoluteXOffset : 0;
|
|
158
|
+
|
|
159
|
+
// Partition on a fixed absolute-X grid rather than by the first visible
|
|
160
|
+
// point's array index. Dropping a point from the left edge then affects only
|
|
161
|
+
// that edge range; fully visible historical ranges keep identical endpoints
|
|
162
|
+
// and RDP choices while translating across the viewport.
|
|
163
|
+
let groupStart = 0;
|
|
164
|
+
let groupKey = Math.floor(
|
|
165
|
+
(points[0] + stableXOffset) / RDP_RANGE_WIDTH_PX,
|
|
166
|
+
);
|
|
167
|
+
for (let i = 1; i < count; i++) {
|
|
168
|
+
const nextKey = Math.floor(
|
|
169
|
+
(points[i * 2] + stableXOffset) / RDP_RANGE_WIDTH_PX,
|
|
170
|
+
);
|
|
171
|
+
if (nextKey !== groupKey) {
|
|
172
|
+
simplifyRdpGroup(
|
|
173
|
+
points,
|
|
174
|
+
keep,
|
|
175
|
+
stack,
|
|
176
|
+
toleranceSq,
|
|
177
|
+
groupStart,
|
|
178
|
+
i - 1,
|
|
179
|
+
);
|
|
180
|
+
groupStart = i;
|
|
181
|
+
groupKey = nextKey;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
simplifyRdpGroup(
|
|
185
|
+
points,
|
|
186
|
+
keep,
|
|
187
|
+
stack,
|
|
188
|
+
toleranceSq,
|
|
189
|
+
groupStart,
|
|
190
|
+
count - 1,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < count; i++) {
|
|
194
|
+
if (keep[i] === 1) out.push(points[i * 2], points[i * 2 + 1]);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|