@obitrain/charts 0.11.0 → 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 +2 -2
- package/lib/commonjs/Charts/BottomAxis/AxisLine.js +1 -1
- package/lib/commonjs/Charts/BottomAxis/BottomAxis.js +1 -1
- package/lib/commonjs/Charts/BottomAxis/Tick.js +2 -2
- package/lib/commonjs/Charts/BottomAxis/Tick.js.map +1 -1
- package/lib/commonjs/Charts/Cursor.js +15 -7
- package/lib/commonjs/Charts/Cursor.js.map +1 -1
- package/lib/commonjs/Charts/Dot.js +1 -1
- package/lib/commonjs/Charts/Dots.js +3 -3
- package/lib/commonjs/Charts/Dots.js.map +1 -1
- package/lib/commonjs/Charts/Linechart/index.js +1 -1
- package/lib/commonjs/Charts/YAxis.js +38 -25
- package/lib/commonjs/Charts/YAxis.js.map +1 -1
- 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 +23 -14
- package/lib/commonjs/Charts/gesture.js.map +1 -1
- package/lib/commonjs/Charts/graphUtils.js +66 -16
- package/lib/commonjs/Charts/graphUtils.js.map +1 -1
- package/lib/commonjs/Charts/index.js +6 -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 +1 -1
- package/lib/module/Charts/BottomAxis/BottomAxis.js.map +1 -1
- package/lib/module/Charts/BottomAxis/Tick.js +3 -3
- package/lib/module/Charts/BottomAxis/Tick.js.map +1 -1
- package/lib/module/Charts/Cursor.js +16 -8
- package/lib/module/Charts/Cursor.js.map +1 -1
- package/lib/module/Charts/Dot.js +1 -1
- package/lib/module/Charts/Dot.js.map +1 -1
- package/lib/module/Charts/Dots.js +3 -3
- package/lib/module/Charts/Dots.js.map +1 -1
- 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 +39 -25
- package/lib/module/Charts/YAxis.js.map +1 -1
- 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 +23 -14
- package/lib/module/Charts/gesture.js.map +1 -1
- package/lib/module/Charts/graphUtils.js +68 -18
- package/lib/module/Charts/graphUtils.js.map +1 -1
- package/lib/module/Charts/index.js +1 -1
- 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 +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/BottomAxis.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/Tick.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/BottomAxis/Tick.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/Cursor.d.ts +2 -2
- package/lib/typescript/commonjs/src/Charts/Cursor.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/Dot.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/Dot.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/Dots.d.ts +1 -1
- package/lib/typescript/commonjs/src/Charts/Dots.d.ts.map +1 -1
- 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 +3 -4
- package/lib/typescript/commonjs/src/Charts/YAxis.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Charts/ZoomableLinechart/ScalablePath.d.ts +1 -1
- 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.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 +1 -1
- 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 +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/BottomAxis.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/Tick.d.ts +1 -1
- package/lib/typescript/module/src/Charts/BottomAxis/Tick.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/Cursor.d.ts +2 -2
- package/lib/typescript/module/src/Charts/Cursor.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/Dot.d.ts +1 -1
- package/lib/typescript/module/src/Charts/Dot.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/Dots.d.ts +1 -1
- package/lib/typescript/module/src/Charts/Dots.d.ts.map +1 -1
- 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 +3 -4
- package/lib/typescript/module/src/Charts/YAxis.d.ts.map +1 -1
- package/lib/typescript/module/src/Charts/ZoomableLinechart/ScalablePath.d.ts +1 -1
- 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.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 +1 -1
- 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 +1 -1
- package/src/Charts/BottomAxis/Tick.tsx +7 -5
- package/src/Charts/Cursor.tsx +17 -9
- package/src/Charts/Dot.tsx +1 -1
- package/src/Charts/Dots.tsx +3 -3
- package/src/Charts/Linechart/index.tsx +1 -1
- package/src/Charts/YAxis.tsx +43 -36
- package/src/Charts/ZoomableLinechart/ScalablePath.tsx +7 -8
- package/src/Charts/ZoomableLinechart/index.tsx +1 -1
- package/src/Charts/gesture.ts +23 -20
- package/src/Charts/graphUtils.ts +75 -16
- package/src/Charts/index.tsx +1 -0
- package/src/Charts/maths.ts +107 -16
package/src/Charts/Cursor.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Circle, type PathCommand } from '
|
|
2
|
-
import type
|
|
1
|
+
import { Circle, type PathCommand } from 'react-native-skia';
|
|
2
|
+
import { useEffect, type FC } from 'react';
|
|
3
3
|
import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
|
|
4
|
-
import {
|
|
4
|
+
import { commandsToBezier, getYForXOnBeziers } from './maths';
|
|
5
5
|
|
|
6
6
|
const CURSOR_SIZE = 10;
|
|
7
7
|
|
|
@@ -27,16 +27,24 @@ const Cursor: FC<CursorProps> = function ({
|
|
|
27
27
|
translateY,
|
|
28
28
|
size = CURSOR_SIZE,
|
|
29
29
|
}) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const conflictingProps = currentValue !== undefined && translateY !== undefined;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (conflictingProps)
|
|
33
|
+
console.warn('currentValue has no effect when translateY is set');
|
|
34
|
+
}, [conflictingProps]);
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
// Normalized once per path change, not on every cursor move
|
|
37
|
+
const beziers = useDerivedValue(() => {
|
|
35
38
|
const _commands = commands?.value;
|
|
36
|
-
|
|
39
|
+
return _commands === undefined ? undefined : commandsToBezier(_commands);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const derivedTranslateY = useDerivedValue(() => {
|
|
43
|
+
const _beziers = beziers.value;
|
|
44
|
+
if (_beziers === undefined) {
|
|
37
45
|
return 0;
|
|
38
46
|
}
|
|
39
|
-
const _value =
|
|
47
|
+
const _value = getYForXOnBeziers(_beziers, positionX.value) ?? 0;
|
|
40
48
|
if (translateY === undefined && currentValue !== undefined) {
|
|
41
49
|
currentValue.value = _value;
|
|
42
50
|
}
|
package/src/Charts/Dot.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Circle, Group, type Color } from '
|
|
1
|
+
import { Circle, Group, type Color } from 'react-native-skia';
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
|
|
4
4
|
import { getPositionWl } from './gesture';
|
package/src/Charts/Dots.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Path, Skia, type Color } from '
|
|
1
|
+
import { Path, Skia, type Color } from 'react-native-skia';
|
|
2
2
|
import { type FC } from 'react';
|
|
3
3
|
import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
|
|
4
4
|
import { getPositionWl } from './gesture';
|
|
@@ -43,7 +43,7 @@ const Dots: FC<DotsProps> = function (props) {
|
|
|
43
43
|
} = props;
|
|
44
44
|
|
|
45
45
|
const path = useDerivedValue(() => {
|
|
46
|
-
const p = Skia.
|
|
46
|
+
const p = Skia.PathBuilder.Make();
|
|
47
47
|
const margin = r + strokeWidth;
|
|
48
48
|
for (const dot of dots) {
|
|
49
49
|
if (dot.opacity.value < 0.5) continue;
|
|
@@ -74,7 +74,7 @@ const Dots: FC<DotsProps> = function (props) {
|
|
|
74
74
|
p.addCircle(x, y, r);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
return p;
|
|
77
|
+
return p.build();
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
return (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Canvas, Group, Path, type SkPath } from '
|
|
1
|
+
import { Canvas, Group, Path, type SkPath } from 'react-native-skia';
|
|
2
2
|
import type { FC } from 'react';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import type { StyleProp, ViewStyle } from 'react-native';
|
package/src/Charts/YAxis.tsx
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
vec,
|
|
6
6
|
type Color,
|
|
7
7
|
type SkFont,
|
|
8
|
-
} from '
|
|
9
|
-
import { type FC } from 'react';
|
|
8
|
+
} from 'react-native-skia';
|
|
9
|
+
import { memo, useMemo, type FC } from 'react';
|
|
10
10
|
import { StyleSheet } from 'react-native';
|
|
11
11
|
|
|
12
12
|
export type YAxisProps = {
|
|
@@ -52,45 +52,52 @@ const YAxis: FC<YAxisProps> = function (props) {
|
|
|
52
52
|
formatLabel = defaultFormatLabel,
|
|
53
53
|
} = props;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
// Measured once per input change: parents re-render while panning
|
|
56
|
+
const rows = useMemo(() => {
|
|
57
|
+
// A flat range would divide by zero
|
|
58
|
+
const span = maxY - minY || 1;
|
|
59
|
+
const values =
|
|
60
|
+
props.values ??
|
|
61
|
+
Array.from(
|
|
62
|
+
{ length: nbTicks },
|
|
63
|
+
(_, i) => minY + ((i + 1) * (maxY - minY)) / nbTicks
|
|
64
|
+
);
|
|
65
|
+
return values.map((value) => {
|
|
66
|
+
const label = formatLabel(value);
|
|
67
|
+
const labelWidth = font
|
|
68
|
+
.getGlyphWidths(font.getGlyphIDs(label))
|
|
69
|
+
.reduce((a, b) => a + b, 0);
|
|
70
|
+
return {
|
|
71
|
+
label,
|
|
72
|
+
labelWidth,
|
|
73
|
+
transform: [{ translateY: height - ((value - minY) * height) / span }],
|
|
74
|
+
p1: vec(0, 0),
|
|
75
|
+
p2: vec(showLabels ? width - 10 - labelWidth : width, 0),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}, [props.values, nbTicks, minY, maxY, height, width, font, showLabels, formatLabel]);
|
|
61
79
|
|
|
62
80
|
return (
|
|
63
81
|
<Group>
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<Line
|
|
75
|
-
p1={vec(0, 0)}
|
|
76
|
-
p2={vec(lineWidth, 0)}
|
|
77
|
-
color={color}
|
|
78
|
-
strokeWidth={strokeWidth}
|
|
82
|
+
{rows.map((row, i) => (
|
|
83
|
+
<Group key={`YTick-${i}`} transform={row.transform}>
|
|
84
|
+
<Line p1={row.p1} p2={row.p2} color={color} strokeWidth={strokeWidth} />
|
|
85
|
+
{showLabels ? (
|
|
86
|
+
<Text
|
|
87
|
+
text={row.label}
|
|
88
|
+
x={width - row.labelWidth}
|
|
89
|
+
y={font.getSize() / 2.5}
|
|
90
|
+
font={font}
|
|
91
|
+
color={labelColor ?? color}
|
|
79
92
|
/>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
x={width - labelWidth}
|
|
84
|
-
y={font.getSize() / 2.5}
|
|
85
|
-
font={font}
|
|
86
|
-
color={labelColor ?? color}
|
|
87
|
-
/>
|
|
88
|
-
) : null}
|
|
89
|
-
</Group>
|
|
90
|
-
);
|
|
91
|
-
})}
|
|
93
|
+
) : null}
|
|
94
|
+
</Group>
|
|
95
|
+
))}
|
|
92
96
|
</Group>
|
|
93
97
|
);
|
|
94
98
|
};
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
const MemoYAxis = memo(YAxis);
|
|
101
|
+
MemoYAxis.displayName = 'YAxis';
|
|
102
|
+
|
|
103
|
+
export { MemoYAxis as YAxis };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Path, Skia, type SkPath } from '
|
|
1
|
+
import { Path, Skia, type SkPath } from 'react-native-skia';
|
|
2
2
|
import { type FC } from 'react';
|
|
3
3
|
import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
|
|
4
4
|
|
|
@@ -16,13 +16,12 @@ const ScalablePath: FC<ScalablePathProps> = function (props) {
|
|
|
16
16
|
|
|
17
17
|
const animatedPath = useDerivedValue(() => {
|
|
18
18
|
// Affine equivalent of getPositionWl: x' = scale * x + focalX * (1 - scale) + offsetX
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return _path;
|
|
19
|
+
// Plain 3x3 matrix: a Skia.Matrix() would allocate a native object per frame
|
|
20
|
+
const s = scale.value;
|
|
21
|
+
const tx = focalX.value * (1 - s) + offsetX.value;
|
|
22
|
+
return Skia.PathBuilder.MakeFromPath(path.value)
|
|
23
|
+
.transform([s, 0, tx, 0, 1, 0, 0, 0, 1])
|
|
24
|
+
.build();
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
return (
|
package/src/Charts/gesture.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clamp } from '
|
|
1
|
+
import { clamp } from 'react-native-skia';
|
|
2
2
|
import { useCallback, useMemo, useRef } from 'react';
|
|
3
3
|
import { Gesture, type PanGesture, type PinchGesture } from 'react-native-gesture-handler';
|
|
4
4
|
import {
|
|
@@ -277,25 +277,28 @@ export const useCursorGesture = function (props: UseCursorGestureProps) {
|
|
|
277
277
|
const xPosition = useSharedValue(0);
|
|
278
278
|
const yPosition = useSharedValue(height);
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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]);
|
|
299
302
|
|
|
300
303
|
// Re-snap the cursor when the data points change (e.g. switching graphs)
|
|
301
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,15 +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
|
},
|
|
202
237
|
// Explicit deps: without them the reaction re-registers and re-fires on
|
|
203
238
|
// every parent render, restarting every dot animation on the UI thread
|
|
204
|
-
[currentGraph, path, dataPoints, dots, opacityWl, translateWl]
|
|
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]
|
|
205
264
|
);
|
|
206
265
|
};
|
package/src/Charts/index.tsx
CHANGED
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 =>
|