@obitrain/charts 0.10.4 → 0.12.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/README.md +6 -4
- package/lib/commonjs/Charts/BottomAxis/AxisLine.js +1 -1
- package/lib/commonjs/Charts/BottomAxis/BottomAxis.js +54 -14
- package/lib/commonjs/Charts/BottomAxis/BottomAxis.js.map +1 -1
- package/lib/commonjs/Charts/BottomAxis/Tick.js +29 -13
- package/lib/commonjs/Charts/BottomAxis/Tick.js.map +1 -1
- package/lib/commonjs/Charts/BottomAxis/index.js.map +1 -1
- package/lib/commonjs/Charts/Cursor.js +33 -13
- package/lib/commonjs/Charts/Cursor.js.map +1 -1
- package/lib/commonjs/Charts/Dot.js +51 -0
- package/lib/commonjs/Charts/Dot.js.map +1 -0
- package/lib/commonjs/Charts/Dots.js +74 -0
- package/lib/commonjs/Charts/Dots.js.map +1 -0
- package/lib/commonjs/Charts/Linechart/index.js +1 -1
- package/lib/commonjs/Charts/YAxis.js +77 -0
- package/lib/commonjs/Charts/YAxis.js.map +1 -0
- package/lib/commonjs/Charts/ZoomableLinechart/ScalablePath.js +5 -4
- package/lib/commonjs/Charts/ZoomableLinechart/ScalablePath.js.map +1 -1
- package/lib/commonjs/Charts/ZoomableLinechart/index.js +1 -1
- package/lib/commonjs/Charts/gesture.js +90 -38
- package/lib/commonjs/Charts/gesture.js.map +1 -1
- package/lib/commonjs/Charts/graphUtils.js +69 -16
- package/lib/commonjs/Charts/graphUtils.js.map +1 -1
- package/lib/commonjs/Charts/index.js +39 -0
- package/lib/commonjs/Charts/index.js.map +1 -1
- package/lib/commonjs/Charts/maths.js +107 -16
- package/lib/commonjs/Charts/maths.js.map +1 -1
- package/lib/module/Charts/BottomAxis/AxisLine.js +1 -1
- package/lib/module/Charts/BottomAxis/AxisLine.js.map +1 -1
- package/lib/module/Charts/BottomAxis/BottomAxis.js +56 -15
- package/lib/module/Charts/BottomAxis/BottomAxis.js.map +1 -1
- package/lib/module/Charts/BottomAxis/Tick.js +29 -11
- package/lib/module/Charts/BottomAxis/Tick.js.map +1 -1
- package/lib/module/Charts/BottomAxis/index.js.map +1 -1
- package/lib/module/Charts/Cursor.js +35 -15
- package/lib/module/Charts/Cursor.js.map +1 -1
- package/lib/module/Charts/Dot.js +47 -0
- package/lib/module/Charts/Dot.js.map +1 -0
- package/lib/module/Charts/Dots.js +70 -0
- package/lib/module/Charts/Dots.js.map +1 -0
- package/lib/module/Charts/Linechart/index.js +1 -1
- package/lib/module/Charts/Linechart/index.js.map +1 -1
- package/lib/module/Charts/YAxis.js +73 -0
- package/lib/module/Charts/YAxis.js.map +1 -0
- package/lib/module/Charts/ZoomableLinechart/ScalablePath.js +5 -4
- package/lib/module/Charts/ZoomableLinechart/ScalablePath.js.map +1 -1
- package/lib/module/Charts/ZoomableLinechart/index.js +1 -1
- package/lib/module/Charts/ZoomableLinechart/index.js.map +1 -1
- package/lib/module/Charts/gesture.js +90 -39
- package/lib/module/Charts/gesture.js.map +1 -1
- package/lib/module/Charts/graphUtils.js +71 -18
- package/lib/module/Charts/graphUtils.js.map +1 -1
- package/lib/module/Charts/index.js +5 -2
- package/lib/module/Charts/index.js.map +1 -1
- package/lib/module/Charts/maths.js +101 -14
- package/lib/module/Charts/maths.js.map +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/AxisLine.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/AxisLine.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/BottomAxis.d.ts +26 -3
- package/lib/typescript/commonjs/src/Charts/BottomAxis/BottomAxis.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/Tick.d.ts +16 -3
- package/lib/typescript/commonjs/src/Charts/BottomAxis/Tick.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/index.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/Cursor.d.ts +5 -2
- package/lib/typescript/commonjs/src/Charts/Cursor.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/Dot.d.ts +22 -0
- package/lib/typescript/commonjs/src/Charts/Dot.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/Charts/Dots.d.ts +28 -0
- package/lib/typescript/commonjs/src/Charts/Dots.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/Charts/Linechart/index.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/Linechart/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/YAxis.d.ts +21 -0
- package/lib/typescript/commonjs/src/Charts/YAxis.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/ScalablePath.d.ts +2 -2
- package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/ScalablePath.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/index.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/gesture.d.ts +6 -0
- package/lib/typescript/commonjs/src/Charts/gesture.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/graphUtils.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/graphUtils.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/index.d.ts +9 -3
- package/lib/typescript/commonjs/src/Charts/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/maths.d.ts +15 -1
- package/lib/typescript/commonjs/src/Charts/maths.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/AxisLine.d.ts +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/AxisLine.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/BottomAxis.d.ts +26 -3
- package/lib/typescript/module/src/Charts/BottomAxis/BottomAxis.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/Tick.d.ts +16 -3
- package/lib/typescript/module/src/Charts/BottomAxis/Tick.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/index.d.ts +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/index.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/Cursor.d.ts +5 -2
- package/lib/typescript/module/src/Charts/Cursor.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/Dot.d.ts +22 -0
- package/lib/typescript/module/src/Charts/Dot.d.ts.map +1 -0
- package/lib/typescript/module/src/Charts/Dots.d.ts +28 -0
- package/lib/typescript/module/src/Charts/Dots.d.ts.map +1 -0
- package/lib/typescript/module/src/Charts/Linechart/index.d.ts +1 -1
- package/lib/typescript/module/src/Charts/Linechart/index.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/YAxis.d.ts +21 -0
- package/lib/typescript/module/src/Charts/YAxis.d.ts.map +1 -0
- package/lib/typescript/module/src/Charts/ZoomableLinechart/ScalablePath.d.ts +2 -2
- package/lib/typescript/module/src/Charts/ZoomableLinechart/ScalablePath.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/ZoomableLinechart/index.d.ts +1 -1
- package/lib/typescript/module/src/Charts/ZoomableLinechart/index.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/gesture.d.ts +6 -0
- package/lib/typescript/module/src/Charts/gesture.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/graphUtils.d.ts +1 -1
- package/lib/typescript/module/src/Charts/graphUtils.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/index.d.ts +9 -3
- package/lib/typescript/module/src/Charts/index.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/maths.d.ts +15 -1
- package/lib/typescript/module/src/Charts/maths.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/Charts/BottomAxis/AxisLine.tsx +1 -1
- package/src/Charts/BottomAxis/BottomAxis.tsx +100 -19
- package/src/Charts/BottomAxis/Tick.tsx +61 -15
- package/src/Charts/BottomAxis/index.tsx +5 -1
- package/src/Charts/Cursor.tsx +38 -10
- package/src/Charts/Dot.tsx +64 -0
- package/src/Charts/Dots.tsx +96 -0
- package/src/Charts/Linechart/index.tsx +1 -1
- package/src/Charts/YAxis.tsx +103 -0
- package/src/Charts/ZoomableLinechart/ScalablePath.tsx +8 -9
- package/src/Charts/ZoomableLinechart/index.tsx +1 -1
- package/src/Charts/gesture.ts +132 -71
- package/src/Charts/graphUtils.ts +78 -16
- package/src/Charts/index.tsx +9 -0
- package/src/Charts/maths.ts +107 -16
package/src/Charts/gesture.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { clamp } from '
|
|
2
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { clamp } from 'react-native-skia';
|
|
2
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
3
3
|
import { Gesture, type PanGesture, type PinchGesture } from 'react-native-gesture-handler';
|
|
4
4
|
import {
|
|
5
5
|
useAnimatedReaction,
|
|
6
6
|
useSharedValue,
|
|
7
|
-
withTiming,
|
|
8
7
|
type SharedValue,
|
|
9
8
|
} from 'react-native-reanimated';
|
|
10
9
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
@@ -24,6 +23,23 @@ export const getPositionWl = function (
|
|
|
24
23
|
return (position - focalX) * scale + focalX + offsetX;
|
|
25
24
|
};
|
|
26
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Offset range keeping the content edges pinned to the viewport:
|
|
28
|
+
* leftBound (position 0) <= startOffset and rightBound (position width)
|
|
29
|
+
* >= width + startOffset. Collapses to [startOffset, startOffset] at scale 1.
|
|
30
|
+
*/
|
|
31
|
+
export const getOffsetBoundsWl = function (
|
|
32
|
+
width: number,
|
|
33
|
+
startOffset: number,
|
|
34
|
+
focalX: number,
|
|
35
|
+
scale: number
|
|
36
|
+
): [min: number, max: number] {
|
|
37
|
+
'worklet';
|
|
38
|
+
const maxOffset = startOffset + focalX * (scale - 1);
|
|
39
|
+
const minOffset = startOffset + (1 - scale) * (width - focalX);
|
|
40
|
+
return [minOffset, maxOffset];
|
|
41
|
+
};
|
|
42
|
+
|
|
27
43
|
export type AxisGestureProps = {
|
|
28
44
|
width: number;
|
|
29
45
|
startOffset?: number;
|
|
@@ -55,6 +71,8 @@ export const useScalableGesture = (props: AxisGestureProps): ScalableGesture =>
|
|
|
55
71
|
// For panning
|
|
56
72
|
const offsetX = useSharedValue(startOffset);
|
|
57
73
|
const lastOffsetX = useSharedValue(startOffset);
|
|
74
|
+
// Pinch pivot bookkeeping (see pinchGesture)
|
|
75
|
+
const needsPivot = useSharedValue(true);
|
|
58
76
|
|
|
59
77
|
const reset = useCallback(() => {
|
|
60
78
|
scale.value = startScale;
|
|
@@ -74,54 +92,85 @@ export const useScalableGesture = (props: AxisGestureProps): ScalableGesture =>
|
|
|
74
92
|
startScale,
|
|
75
93
|
]);
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
95
|
+
// Memoized: handing a new gesture instance to GestureDetector mid-gesture
|
|
96
|
+
// resets the active pan and makes the chart jump
|
|
97
|
+
const panGesture = useMemo(
|
|
98
|
+
() =>
|
|
99
|
+
Gesture.Pan()
|
|
100
|
+
// Single-finger only: two fingers belong to the pinch, whose focal
|
|
101
|
+
// rebase would otherwise fight the pan over offsetX
|
|
102
|
+
.maxPointers(1)
|
|
103
|
+
// Incremental deltas (changeX) with a live clamp keep the content
|
|
104
|
+
// edges pinned: no overscroll, no snap-back to interrupt
|
|
105
|
+
.onChange((event) => {
|
|
106
|
+
const [minOffset, maxOffset] = getOffsetBoundsWl(
|
|
107
|
+
width,
|
|
108
|
+
startOffset,
|
|
109
|
+
focalX.value,
|
|
110
|
+
scale.value
|
|
111
|
+
);
|
|
112
|
+
offsetX.value = clamp(
|
|
113
|
+
offsetX.value + event.changeX,
|
|
114
|
+
minOffset,
|
|
115
|
+
maxOffset
|
|
116
|
+
);
|
|
117
|
+
})
|
|
118
|
+
.onEnd(() => {
|
|
119
|
+
lastScale.value = scale.value;
|
|
120
|
+
lastFocalX.value = focalX.value;
|
|
121
|
+
lastOffsetX.value = offsetX.value;
|
|
122
|
+
}),
|
|
123
|
+
[focalX, lastFocalX, lastOffsetX, lastScale, offsetX, scale, startOffset, width]
|
|
124
|
+
);
|
|
108
125
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
const pinchGesture = useMemo(
|
|
127
|
+
() =>
|
|
128
|
+
Gesture.Pinch()
|
|
129
|
+
.onStart(() => {
|
|
130
|
+
needsPivot.value = true;
|
|
131
|
+
})
|
|
132
|
+
.onUpdate((event) => {
|
|
133
|
+
// The pinch can keep firing in the single-finger tail of the
|
|
134
|
+
// gesture, tracking the remaining finger as its focal: ignore it
|
|
135
|
+
// (numberOfPointers is missing on web — let those through)
|
|
136
|
+
if ((event.numberOfPointers ?? 2) < 2) {
|
|
137
|
+
needsPivot.value = true;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const prevScale = scale.value;
|
|
141
|
+
const newScale = clamp(lastScale.value * event.scale, 1, Infinity);
|
|
142
|
+
if (needsPivot.value) {
|
|
143
|
+
// New pivot: rebase offsetX so re-anchoring under the fingers
|
|
144
|
+
// doesn't shift the content
|
|
145
|
+
const rebased =
|
|
146
|
+
offsetX.value + (focalX.value - event.focalX) * (1 - prevScale);
|
|
147
|
+
offsetX.value = (rebased * newScale) / prevScale;
|
|
148
|
+
needsPivot.value = false;
|
|
149
|
+
} else {
|
|
150
|
+
// Keep the content glued to both fingers: the offset follows the
|
|
151
|
+
// focal translation and rescales with the zoom
|
|
152
|
+
offsetX.value =
|
|
153
|
+
(event.focalX - focalX.value + offsetX.value / prevScale) *
|
|
154
|
+
newScale;
|
|
155
|
+
}
|
|
156
|
+
focalX.value = event.focalX;
|
|
157
|
+
scale.value = newScale;
|
|
158
|
+
// Zooming moves the bounds: keep the content edges pinned
|
|
159
|
+
const [minOffset, maxOffset] = getOffsetBoundsWl(
|
|
160
|
+
width,
|
|
161
|
+
startOffset,
|
|
162
|
+
focalX.value,
|
|
163
|
+
scale.value
|
|
164
|
+
);
|
|
165
|
+
offsetX.value = clamp(offsetX.value, minOffset, maxOffset);
|
|
166
|
+
})
|
|
167
|
+
.onEnd(() => {
|
|
168
|
+
lastScale.value = scale.value;
|
|
169
|
+
lastFocalX.value = focalX.value;
|
|
170
|
+
lastOffsetX.value = offsetX.value;
|
|
171
|
+
}),
|
|
172
|
+
[focalX, lastFocalX, lastOffsetX, lastScale, needsPivot, offsetX, scale, startOffset, width]
|
|
173
|
+
);
|
|
125
174
|
|
|
126
175
|
return {
|
|
127
176
|
scale,
|
|
@@ -143,6 +192,14 @@ export const useUpdateAxis = function (props: UpdateAxisProps) {
|
|
|
143
192
|
const { scale, scales, onScaleChange } = props;
|
|
144
193
|
const currentIndex = useSharedValue(0);
|
|
145
194
|
|
|
195
|
+
// Stable dispatcher so an inline onScaleChange doesn't re-register the
|
|
196
|
+
// reaction (and disturb the UI thread) on every render
|
|
197
|
+
const onScaleChangeRef = useRef(onScaleChange);
|
|
198
|
+
onScaleChangeRef.current = onScaleChange;
|
|
199
|
+
const dispatchScaleChange = useCallback((index: number) => {
|
|
200
|
+
onScaleChangeRef.current?.(index);
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
146
203
|
useAnimatedReaction(
|
|
147
204
|
() => scale.value,
|
|
148
205
|
(currentScale, _) => {
|
|
@@ -158,11 +215,12 @@ export const useUpdateAxis = function (props: UpdateAxisProps) {
|
|
|
158
215
|
currentIndex.value !== i
|
|
159
216
|
) {
|
|
160
217
|
currentIndex.value = i;
|
|
161
|
-
|
|
218
|
+
scheduleOnRN(dispatchScaleChange, i);
|
|
162
219
|
break;
|
|
163
220
|
}
|
|
164
221
|
}
|
|
165
|
-
}
|
|
222
|
+
},
|
|
223
|
+
[scales, dispatchScaleChange]
|
|
166
224
|
);
|
|
167
225
|
|
|
168
226
|
return { currentIndex };
|
|
@@ -219,25 +277,28 @@ export const useCursorGesture = function (props: UseCursorGestureProps) {
|
|
|
219
277
|
const xPosition = useSharedValue(0);
|
|
220
278
|
const yPosition = useSharedValue(height);
|
|
221
279
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
280
|
+
// Memoized: a new gesture instance on every render can reset an active pan
|
|
281
|
+
const { panGesture, tapGesture } = useMemo(() => {
|
|
282
|
+
const setPosition = (x: number, y: number) => {
|
|
283
|
+
'worklet';
|
|
284
|
+
yPosition.value = clamp(y, 0, height);
|
|
285
|
+
if (isContinuous || points === undefined) {
|
|
286
|
+
xPosition.value = clamp(x, 0, width);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const _closestDot = getClosestPoint(clamp(x, 0, width), points.value);
|
|
290
|
+
if (closestDataPoint !== undefined) closestDataPoint.value = _closestDot;
|
|
291
|
+
xPosition.value = _closestDot.x;
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
panGesture: Gesture.Pan()
|
|
295
|
+
.onBegin((event) => setPosition(event.x, event.y))
|
|
296
|
+
.onUpdate((event) => setPosition(event.x, event.y)),
|
|
297
|
+
tapGesture: Gesture.Tap().onBegin((event) =>
|
|
298
|
+
setPosition(event.x, event.y)
|
|
299
|
+
),
|
|
300
|
+
};
|
|
301
|
+
}, [width, height, isContinuous, points, closestDataPoint, xPosition, yPosition]);
|
|
241
302
|
|
|
242
303
|
// Re-snap the cursor when the data points change (e.g. switching graphs)
|
|
243
304
|
useAnimatedReaction(
|
package/src/Charts/graphUtils.ts
CHANGED
|
@@ -3,16 +3,23 @@ import {
|
|
|
3
3
|
Skia,
|
|
4
4
|
type PathCommand,
|
|
5
5
|
type SkPath,
|
|
6
|
-
} from '
|
|
6
|
+
} from 'react-native-skia';
|
|
7
|
+
import { useMemo, useRef } from 'react';
|
|
7
8
|
import { scaleLinear, type ScaleLinear } from 'd3-scale';
|
|
8
9
|
import * as shape from 'd3-shape';
|
|
9
10
|
import {
|
|
10
11
|
useAnimatedReaction,
|
|
12
|
+
useSharedValue,
|
|
11
13
|
withTiming,
|
|
12
14
|
type SharedValue,
|
|
13
15
|
} from 'react-native-reanimated';
|
|
14
16
|
import { getPositionWl } from './gesture';
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
commandsToBezier,
|
|
19
|
+
findBezierIndex,
|
|
20
|
+
getYBeyondPathEnds,
|
|
21
|
+
getYOnBezier,
|
|
22
|
+
} from './maths';
|
|
16
23
|
import type { AnimatedDot, DataPoint } from './types';
|
|
17
24
|
|
|
18
25
|
export type BuildGraphConfig = {
|
|
@@ -47,12 +54,23 @@ export const buildGraph = function (
|
|
|
47
54
|
if (data.length === 0)
|
|
48
55
|
throw new Error('buildGraph requires at least one data point');
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
// Loops rather than Math.min(...data): spreading a long series can exceed the engine's argument limit
|
|
58
|
+
let dataMinX = Infinity;
|
|
59
|
+
let dataMaxX = -Infinity;
|
|
60
|
+
let dataMinY = Infinity;
|
|
61
|
+
let dataMaxY = -Infinity;
|
|
62
|
+
for (const [x, y] of data) {
|
|
63
|
+
if (x < dataMinX) dataMinX = x;
|
|
64
|
+
if (x > dataMaxX) dataMaxX = x;
|
|
65
|
+
if (y < dataMinY) dataMinY = y;
|
|
66
|
+
if (y > dataMaxY) dataMaxY = y;
|
|
67
|
+
}
|
|
68
|
+
const minX = config?.minX ?? dataMinX;
|
|
69
|
+
const maxX = config?.maxX ?? dataMaxX;
|
|
52
70
|
const scaleX = scaleLinear().domain([minX, maxX]).range([0, width]);
|
|
53
71
|
|
|
54
|
-
const minY = config?.minY ??
|
|
55
|
-
const maxY = config?.maxY ??
|
|
72
|
+
const minY = config?.minY ?? dataMinY;
|
|
73
|
+
const maxY = config?.maxY ?? dataMaxY;
|
|
56
74
|
const scaleY = scaleLinear().domain([minY, maxY]).range([height, 0]);
|
|
57
75
|
|
|
58
76
|
const path = shape
|
|
@@ -173,18 +191,39 @@ export const useDotsTransition = function (props: UseDotAnimationProps) {
|
|
|
173
191
|
translateWl = defaultTranslateTransitionWl,
|
|
174
192
|
} = props;
|
|
175
193
|
|
|
194
|
+
// Normalized once per path change, not per dot per frame
|
|
195
|
+
const commands = useSharedValue<PathCommand[]>([]);
|
|
196
|
+
const visibleCount = useSharedValue(0);
|
|
197
|
+
// Bumped when the data or the dots are replaced, so they get retargeted
|
|
198
|
+
// even if the graph index is unchanged
|
|
199
|
+
const generationRef = useRef(0);
|
|
200
|
+
const generation = useMemo(
|
|
201
|
+
() => (generationRef.current += 1),
|
|
202
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
203
|
+
[dataPoints, dots]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Retarget on graph or data change only: re-assigning x while it animates
|
|
207
|
+
// would restart the animation every frame
|
|
176
208
|
useAnimatedReaction(
|
|
177
209
|
() => ({
|
|
178
210
|
_currentGraph: currentGraph.value,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// even when the path is swapped instantly or tweens on another duration
|
|
182
|
-
_xs: dots.map((d) => d.x.value),
|
|
211
|
+
_path: path.value,
|
|
212
|
+
_generation: generation,
|
|
183
213
|
}),
|
|
184
|
-
({ _currentGraph,
|
|
214
|
+
({ _currentGraph, _path, _generation }, previous) => {
|
|
215
|
+
commands.value = commandsToBezier(_path.toCmds());
|
|
216
|
+
if (
|
|
217
|
+
previous !== null &&
|
|
218
|
+
previous._currentGraph === _currentGraph &&
|
|
219
|
+
previous._generation === _generation
|
|
220
|
+
)
|
|
221
|
+
return;
|
|
222
|
+
|
|
185
223
|
const newDataPoints = dataPoints[_currentGraph];
|
|
186
224
|
if (newDataPoints === undefined)
|
|
187
225
|
throw new Error('Data points cannot be undefined');
|
|
226
|
+
visibleCount.value = newDataPoints.length;
|
|
188
227
|
dots.forEach((_dot, i) => {
|
|
189
228
|
const _newDot = newDataPoints[i];
|
|
190
229
|
if (!_newDot) {
|
|
@@ -192,12 +231,35 @@ export const useDotsTransition = function (props: UseDotAnimationProps) {
|
|
|
192
231
|
return;
|
|
193
232
|
}
|
|
194
233
|
_dot.x.value = translateWl(_newDot.x);
|
|
195
|
-
const newY = getYForX(_currentCommands, _dot.x.value);
|
|
196
|
-
if (newY !== undefined) {
|
|
197
|
-
_dot.y.value = newY;
|
|
198
|
-
}
|
|
199
234
|
_dot.opacity.value = opacityWl(1);
|
|
200
235
|
});
|
|
201
|
-
}
|
|
236
|
+
},
|
|
237
|
+
// Explicit deps: without them the reaction re-registers and re-fires on
|
|
238
|
+
// every parent render, restarting every dot animation on the UI thread
|
|
239
|
+
[currentGraph, path, dataPoints, dots, opacityWl, translateWl, generation]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Keep y glued to the curve while x animates or the path tweens
|
|
243
|
+
useAnimatedReaction(
|
|
244
|
+
() => ({ _cmds: commands.value, _xs: dots.map((d) => d.x.value) }),
|
|
245
|
+
({ _cmds, _xs }) => {
|
|
246
|
+
const count = Math.min(visibleCount.value, dots.length);
|
|
247
|
+
// Dots are in x order, so one forward walk over the segments serves them all
|
|
248
|
+
let segment = 0;
|
|
249
|
+
for (let i = 0; i < count; i++) {
|
|
250
|
+
const x = _xs[i]!;
|
|
251
|
+
let index = findBezierIndex(_cmds, x, segment);
|
|
252
|
+
if (index === -1 && segment > 1) index = findBezierIndex(_cmds, x);
|
|
253
|
+
let newY: number | undefined;
|
|
254
|
+
if (index === -1) {
|
|
255
|
+
newY = getYBeyondPathEnds(_cmds, x);
|
|
256
|
+
} else {
|
|
257
|
+
segment = index;
|
|
258
|
+
newY = getYOnBezier(_cmds, index, x);
|
|
259
|
+
}
|
|
260
|
+
if (newY !== undefined) dots[i]!.y.value = newY;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
[dots]
|
|
202
264
|
);
|
|
203
265
|
};
|
package/src/Charts/index.tsx
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
export { AxisLine, BottomAxis, Tick } from './BottomAxis';
|
|
2
2
|
export type {
|
|
3
3
|
AxisLineProps,
|
|
4
|
+
AxisTick,
|
|
4
5
|
BottomAxisProps,
|
|
5
6
|
TickProps,
|
|
6
7
|
} from './BottomAxis';
|
|
7
8
|
export { Cursor } from './Cursor';
|
|
8
9
|
export type { CursorProps } from './Cursor';
|
|
10
|
+
export { Dot } from './Dot';
|
|
11
|
+
export type { DotProps } from './Dot';
|
|
12
|
+
export { Dots } from './Dots';
|
|
13
|
+
export type { DotShape, DotsProps } from './Dots';
|
|
14
|
+
export { YAxis, getPaddedTicks } from './YAxis';
|
|
15
|
+
export type { YAxisProps } from './YAxis';
|
|
9
16
|
export { LineChart } from './Linechart';
|
|
10
17
|
export type { LineChartProps, LinePath } from './Linechart';
|
|
11
18
|
export { ScalablePath, ZoomableLineChart } from './ZoomableLinechart';
|
|
@@ -15,6 +22,7 @@ export type {
|
|
|
15
22
|
} from './ZoomableLinechart';
|
|
16
23
|
export {
|
|
17
24
|
getClosestPoint,
|
|
25
|
+
getOffsetBoundsWl,
|
|
18
26
|
getPositionWl,
|
|
19
27
|
useCursorGesture,
|
|
20
28
|
useScalableGesture,
|
|
@@ -44,6 +52,7 @@ export {
|
|
|
44
52
|
commandsToBezier,
|
|
45
53
|
cubicBezierYForX,
|
|
46
54
|
getYForX,
|
|
55
|
+
getYForXOnBeziers,
|
|
47
56
|
magnitude,
|
|
48
57
|
normalize,
|
|
49
58
|
selectCurve,
|
package/src/Charts/maths.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type {
|
|
|
2
2
|
PathCommand,
|
|
3
3
|
SkPoint,
|
|
4
4
|
Vector,
|
|
5
|
-
} from '
|
|
6
|
-
import { PathVerb
|
|
5
|
+
} from 'react-native-skia';
|
|
6
|
+
import { PathVerb } from 'react-native-skia';
|
|
7
7
|
|
|
8
8
|
// code from William Candillon
|
|
9
9
|
|
|
@@ -155,7 +155,10 @@ export const selectCurve = (
|
|
|
155
155
|
): Cubic | undefined => {
|
|
156
156
|
'worklet';
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
// Plain numbers while scanning: vec() allocates a JSI host object, which
|
|
159
|
+
// dominated the UI thread when called per dot per frame
|
|
160
|
+
let fromX = 0;
|
|
161
|
+
let fromY = 0;
|
|
159
162
|
for (let i = 0; i < cmds.length; i++) {
|
|
160
163
|
const cmd = cmds[i];
|
|
161
164
|
if (cmd == null) {
|
|
@@ -163,20 +166,21 @@ export const selectCurve = (
|
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
if (cmd[0] === PathVerb.Move) {
|
|
166
|
-
|
|
169
|
+
fromX = cmd[1]!;
|
|
170
|
+
fromY = cmd[2]!;
|
|
167
171
|
} else if (cmd[0] === PathVerb.Cubic) {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
if (x >= from.x && x <= to.x) {
|
|
172
|
+
const toX = cmd[5]!;
|
|
173
|
+
const toY = cmd[6]!;
|
|
174
|
+
if (x >= fromX && x <= toX) {
|
|
172
175
|
return {
|
|
173
|
-
from,
|
|
174
|
-
c1,
|
|
175
|
-
c2,
|
|
176
|
-
to,
|
|
176
|
+
from: { x: fromX, y: fromY },
|
|
177
|
+
c1: { x: cmd[1]!, y: cmd[2]! },
|
|
178
|
+
c2: { x: cmd[3]!, y: cmd[4]! },
|
|
179
|
+
to: { x: toX, y: toY },
|
|
177
180
|
};
|
|
178
181
|
}
|
|
179
|
-
|
|
182
|
+
fromX = toX;
|
|
183
|
+
fromY = toY;
|
|
180
184
|
}
|
|
181
185
|
}
|
|
182
186
|
|
|
@@ -229,13 +233,100 @@ export const getYForX = (
|
|
|
229
233
|
precision = 2
|
|
230
234
|
): number | undefined => {
|
|
231
235
|
'worklet';
|
|
232
|
-
const
|
|
233
|
-
const c = selectCurve(cmdsNorm, x);
|
|
236
|
+
const c = selectCurve(commandsToBezier(cmds), x);
|
|
234
237
|
if (c == null) return undefined;
|
|
235
|
-
|
|
236
238
|
return cubicBezierYForX(x, c.from, c.c1, c.c2, c.to, precision);
|
|
237
239
|
};
|
|
238
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Index of the cubic whose x range contains `x`, scanning from `start`; -1 if none.
|
|
243
|
+
* `cmds` must come from {@link commandsToBezier}.
|
|
244
|
+
*/
|
|
245
|
+
export const findBezierIndex = (
|
|
246
|
+
cmds: PathCommand[],
|
|
247
|
+
x: number,
|
|
248
|
+
start = 0
|
|
249
|
+
): number => {
|
|
250
|
+
'worklet';
|
|
251
|
+
for (let i = Math.max(start, 1); i < cmds.length; i++) {
|
|
252
|
+
const cmd = cmds[i]!;
|
|
253
|
+
if (cmd[0] !== PathVerb.Cubic) continue;
|
|
254
|
+
const prev = cmds[i - 1]!;
|
|
255
|
+
const fromX = prev[prev.length - 2]!;
|
|
256
|
+
if (x >= fromX && x <= cmd[5]!) return i;
|
|
257
|
+
}
|
|
258
|
+
return -1;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const EPSILON = 1e-6;
|
|
262
|
+
|
|
263
|
+
/** y of the cubic at `cmds[index]` for `x`, without allocating; undefined when x has no t in [0, 1]. */
|
|
264
|
+
export const getYOnBezier = (
|
|
265
|
+
cmds: PathCommand[],
|
|
266
|
+
index: number,
|
|
267
|
+
x: number
|
|
268
|
+
): number | undefined => {
|
|
269
|
+
'worklet';
|
|
270
|
+
const prev = cmds[index - 1]!;
|
|
271
|
+
const cmd = cmds[index]!;
|
|
272
|
+
const x0 = prev[prev.length - 2]!;
|
|
273
|
+
const y0 = prev[prev.length - 1]!;
|
|
274
|
+
const x1 = cmd[1]!;
|
|
275
|
+
const y1 = cmd[2]!;
|
|
276
|
+
const x2 = cmd[3]!;
|
|
277
|
+
const y2 = cmd[4]!;
|
|
278
|
+
const x3 = cmd[5]!;
|
|
279
|
+
const y3 = cmd[6]!;
|
|
280
|
+
|
|
281
|
+
// commandsToBezier turns lines into cubics with c1 = from and c2 = to
|
|
282
|
+
if (x1 === x0 && y1 === y0 && x2 === x3 && y2 === y3) {
|
|
283
|
+
return x3 === x0 ? y0 : y0 + ((y3 - y0) * (x - x0)) / (x3 - x0);
|
|
284
|
+
}
|
|
285
|
+
// A vertical curve has no single y for x: use its start, like a vertical line
|
|
286
|
+
if (x0 === x1 && x1 === x2 && x2 === x3) return y0;
|
|
287
|
+
|
|
288
|
+
const roots = solveCubic(
|
|
289
|
+
-x0 + 3 * x1 - 3 * x2 + x3,
|
|
290
|
+
3 * x0 - 6 * x1 + 3 * x2,
|
|
291
|
+
-3 * x0 + 3 * x1,
|
|
292
|
+
x0 - x
|
|
293
|
+
);
|
|
294
|
+
for (let i = 0; i < roots.length; i++) {
|
|
295
|
+
const t = roots[i]!;
|
|
296
|
+
if (t >= -EPSILON && t <= 1 + EPSILON) {
|
|
297
|
+
return cubicSolve(Math.min(1, Math.max(0, t)), y0, y1, y2, y3);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* y of the path's start when `x` is left of it, of its end when right of it, else undefined.
|
|
305
|
+
* Endpoints are stored as float32, so a point's exact x can fall a hair outside the path.
|
|
306
|
+
*/
|
|
307
|
+
export const getYBeyondPathEnds = (
|
|
308
|
+
cmds: PathCommand[],
|
|
309
|
+
x: number
|
|
310
|
+
): number | undefined => {
|
|
311
|
+
'worklet';
|
|
312
|
+
const first = cmds[0];
|
|
313
|
+
const last = cmds[cmds.length - 1];
|
|
314
|
+
if (first === undefined || last === undefined) return undefined;
|
|
315
|
+
if (x < first[1]!) return first[2];
|
|
316
|
+
if (x > last[last.length - 2]!) return last[last.length - 1];
|
|
317
|
+
return undefined;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/** Same as {@link getYForX} for commands already passed through {@link commandsToBezier}. */
|
|
321
|
+
export const getYForXOnBeziers = (
|
|
322
|
+
cmds: PathCommand[],
|
|
323
|
+
x: number
|
|
324
|
+
): number | undefined => {
|
|
325
|
+
'worklet';
|
|
326
|
+
const index = findBezierIndex(cmds, x);
|
|
327
|
+
return index === -1 ? undefined : getYOnBezier(cmds, index, x);
|
|
328
|
+
};
|
|
329
|
+
|
|
239
330
|
// See Path/getPath2D
|
|
240
331
|
|
|
241
332
|
export const magnitude = (p: SkPoint): number =>
|