Skip to content

Commit f2bafdc

Browse files
committed
feat: add GeagueChart
1 parent 5b794bb commit f2bafdc

File tree

27 files changed

+461
-108
lines changed

27 files changed

+461
-108
lines changed

App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GestureHandlerRootView } from "react-native-gesture-handler";
2-
import { RootStackNavigation } from "./src/navigation";
32
import { NavigationContainer } from "@react-navigation/native";
3+
import { RootStackNavigation } from "./src/navigation";
44

55
export default function App() {
66
return (

bun.lockb

90 KB
Binary file not shown.

package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"start": "expo start",
77
"android": "expo start --android",
88
"ios": "expo start --ios",
9-
"web": "expo start --web"
9+
"web": "expo start --web",
10+
"lint": "eslint . --ext .ts,.tsx --fix",
11+
"tscheck": "tsc --noEmit",
12+
"lint:control": "yarn tscheck && yarn lint"
1013
},
1114
"dependencies": {
1215
"@react-native-masked-view/masked-view": "0.2.9",
@@ -29,8 +32,16 @@
2932
},
3033
"devDependencies": {
3134
"@babel/core": "^7.20.0",
35+
"@papyonlab/eslint-config": "^1.0.1",
36+
"@papyonlab/tsconfig": "^1.0.0",
3237
"@types/react": "~18.2.14",
3338
"typescript": "^5.1.3"
3439
},
35-
"private": true
40+
"private": false,
41+
"eslintConfig": {
42+
"root": true,
43+
"extends": [
44+
"@papyonlab/eslint-config/base"
45+
]
46+
}
3647
}

src/ComponentList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { StatusBar } from "expo-status-bar";
22
import { StyleSheet, Text, TouchableOpacity } from "react-native";
33

44
import { ScrollView } from "react-native-gesture-handler";
5-
import { useAppNavigation } from "./utils/useNavigation";
65
import { SafeAreaView } from "react-native-safe-area-context";
6+
import { useAppNavigation } from "./utils/useNavigation";
77
import { ComponentScreenList } from "./constants";
88

99
export function ComoponentList() {
@@ -14,6 +14,7 @@ export function ComoponentList() {
1414
<ScrollView>
1515
{Object.keys(ComponentScreenList).map((name) => (
1616
<TouchableOpacity
17+
accessibilityRole="button"
1718
key={name}
1819
style={styles.item}
1920
onPress={() => navgation.navigate(name as any)}

src/components/AppleLoading/AppleLoading.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const polarToCartesian = (
4141
centerX: number,
4242
centerY: number,
4343
radius: number,
44-
angleInDegrees: number
44+
angleInDegrees: number,
4545
) => {
4646
"worklet";
4747
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
@@ -53,7 +53,7 @@ const polarToCartesian = (
5353

5454
const moveNearestCirclePoint = (
5555
featureDot: { x: number; y: number; size: number },
56-
circle: { x: number; y: number; size: number }
56+
circle: { x: number; y: number; size: number },
5757
) => {
5858
"worklet";
5959
const radius = featureDot.size + circle.size;
@@ -64,7 +64,7 @@ const moveNearestCirclePoint = (
6464
radius,
6565
(Math.atan2(circle.y - featureDot.y, circle.x - featureDot.x) * 180) /
6666
Math.PI +
67-
90
67+
90,
6868
);
6969

7070
return pos;
@@ -156,7 +156,7 @@ function FeatureDot({
156156
SCREEN_WIDTH / 2,
157157
SCREEN_WIDTH / 2,
158158
dot.value.distance,
159-
rotation
159+
rotation,
160160
);
161161

162162
return pos;
@@ -237,13 +237,13 @@ function Dot({
237237
SCREEN_WIDTH / 2,
238238
SCREEN_WIDTH / 2,
239239
featureDot.value.distance,
240-
featureDot.value.angle + progress.value
240+
featureDot.value.angle + progress.value,
241241
);
242242
const dotPos = polarToCartesian(
243243
SCREEN_WIDTH / 2,
244244
SCREEN_WIDTH / 2,
245245
distance - size,
246-
angle
246+
angle,
247247
);
248248

249249
const pos = moveNearestCirclePoint(
@@ -256,21 +256,21 @@ function Dot({
256256
x: dotPos.x,
257257
y: dotPos.y,
258258
size: size,
259-
}
259+
},
260260
);
261261

262262
return {
263263
x: interpolate(
264264
featureScaleMultiplier.value,
265265
[0, 1],
266266
[dotPos.x, pos.x],
267-
Extrapolate.CLAMP
267+
Extrapolate.CLAMP,
268268
),
269269
y: interpolate(
270270
featureScaleMultiplier.value,
271271
[0, 1],
272272
[dotPos.y, pos.y],
273-
Extrapolate.CLAMP
273+
Extrapolate.CLAMP,
274274
),
275275
size: interpolate(featureScaleMultiplier.value, [1, 0], [5, size]),
276276
};
@@ -279,7 +279,7 @@ function Dot({
279279
SCREEN_WIDTH / 2,
280280
SCREEN_WIDTH / 2,
281281
distance - size,
282-
angle
282+
angle,
283283
);
284284

285285
return {
@@ -317,7 +317,7 @@ export function AppleLoading() {
317317
useEffect(() => {
318318
t.value = withRepeat(
319319
withTiming(360, { duration: 12000, easing: Easing.linear }),
320-
-1
320+
-1,
321321
);
322322
featureScaleMultiplier.value = withRepeat(
323323
withSequence(
@@ -326,17 +326,17 @@ export function AppleLoading() {
326326
withSpring(1, {
327327
damping: 50,
328328
stiffness: 100,
329-
})
329+
}),
330330
),
331331
withDelay(
332332
1000,
333333
withSpring(0, { damping: 50, stiffness: 100 }, () => {
334334
featureIndex.value = (featureIndex.value + 4) % LAYERS[0].dotCount;
335335
runOnJS(setImage)(APPS[featureIndex.value % APPS.length]);
336-
})
337-
)
336+
}),
337+
),
338338
),
339-
-1
339+
-1,
340340
);
341341
}, []);
342342

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { useEffect, useImperativeHandle } from "react";
2+
import { Dimensions, StyleSheet, View } from "react-native";
3+
import { Path, Svg } from "react-native-svg";
4+
import {
5+
useAnimatedStyle,
6+
useSharedValue,
7+
withSequence,
8+
withTiming,
9+
} from "react-native-reanimated";
10+
import { Neddle, NEDDLE_SIZE } from "./Neddle";
11+
12+
import { polarToCartesian, segmentPath } from "./math";
13+
14+
export const SIZE = Dimensions.get("window").width;
15+
const GAP = 10;
16+
const RADIUS = SIZE / 2 - GAP;
17+
const STROKE_WIDTH = 60;
18+
19+
type GaugeItem = {
20+
color: string;
21+
value: number;
22+
};
23+
24+
type Props = {
25+
values: GaugeItem[];
26+
value: number;
27+
};
28+
29+
export type GaugeChartHandle = {
30+
reRunAnimation: () => void;
31+
};
32+
33+
export const GaugeChart = React.forwardRef<GaugeChartHandle, Props>(
34+
({ values, value }, ref) => {
35+
const total = values.reduce((acc, item) => acc + item.value, 0);
36+
const animatedDegree = useSharedValue(-90);
37+
38+
useImperativeHandle(ref, () => ({
39+
reRunAnimation: () => {
40+
animatedDegree.value = withSequence(
41+
withTiming(-90, { duration: 0 }),
42+
withTiming(((value / total) * 180 - 90) % 360, {
43+
duration: 1000,
44+
}),
45+
);
46+
},
47+
}));
48+
49+
useEffect(() => {
50+
animatedDegree.value = withTiming(((value / total) * 180 - 90) % 360, {
51+
duration: 1000,
52+
});
53+
}, [animatedDegree, total, value]);
54+
55+
const segment = (n: number) => {
56+
const center = SIZE / 2;
57+
const degree = (values[n].value / total) * 180;
58+
59+
const start =
60+
n === 0
61+
? 0
62+
: values
63+
.slice(0, n)
64+
.reduce((acc, item) => acc + (item.value / total) * 180, 0);
65+
const end = start + degree;
66+
67+
const path = segmentPath(
68+
center,
69+
center,
70+
RADIUS,
71+
RADIUS - STROKE_WIDTH,
72+
start,
73+
end,
74+
);
75+
76+
const fill = values[n].color;
77+
return <Path key={n} d={path} fill={fill} />;
78+
};
79+
80+
const animatedNeddleStyle = useAnimatedStyle(() => {
81+
const center = SIZE / 2;
82+
// needle rotation degree
83+
const [x, y] = polarToCartesian(
84+
center,
85+
center,
86+
RADIUS - STROKE_WIDTH / 2 - 30,
87+
animatedDegree.value,
88+
);
89+
90+
return {
91+
transform: [
92+
{ translateX: x - NEDDLE_SIZE / 2 },
93+
{ translateY: y - NEDDLE_SIZE / 2 },
94+
{
95+
rotate: `${animatedDegree.value}deg`,
96+
},
97+
],
98+
};
99+
});
100+
101+
return (
102+
<View style={styles.container}>
103+
<Svg width={SIZE} height={SIZE / 2}>
104+
{values.map((item, i) => segment(i))}
105+
<Neddle
106+
style={[
107+
{ width: NEDDLE_SIZE, height: NEDDLE_SIZE },
108+
animatedNeddleStyle,
109+
]}
110+
/>
111+
</Svg>
112+
</View>
113+
);
114+
},
115+
);
116+
117+
const styles = StyleSheet.create({
118+
container: {
119+
position: "relative",
120+
},
121+
});

src/components/GaugeChart/Neddle.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from "react";
2+
import Svg, { SvgProps, Path } from "react-native-svg";
3+
import { ViewProps } from "react-native";
4+
import Animated, { AnimateProps } from "react-native-reanimated";
5+
6+
type Props = {
7+
svgProps?: SvgProps;
8+
} & AnimateProps<ViewProps>;
9+
10+
const NEDDLE_SIZE = 34;
11+
12+
const Neddle = ({ svgProps, ...rest }: Props) => (
13+
<Animated.View {...rest}>
14+
<Svg width={NEDDLE_SIZE} height={NEDDLE_SIZE} fill="none" {...svgProps}>
15+
<Path
16+
d="M17.898 2.094 32.007 30.81a1 1 0 0 1-.898 1.441H2.891a1 1 0 0 1-.898-1.441l14.11-28.715a1 1 0 0 1 1.794 0Z"
17+
fill="#003441"
18+
stroke="#fff"
19+
strokeWidth={2.5}
20+
/>
21+
</Svg>
22+
</Animated.View>
23+
);
24+
25+
export { Neddle, NEDDLE_SIZE };

src/components/GaugeChart/math.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
export const angelToRadian = (angleInDegrees: number) => {
2+
"worklet";
3+
return ((angleInDegrees - 90) * Math.PI) / 180;
4+
};
5+
6+
export const polarToCartesian = (
7+
centerX: number,
8+
centerY: number,
9+
radius: number,
10+
angleInDegrees: number,
11+
) => {
12+
"worklet";
13+
const angleInRadians = angelToRadian(angleInDegrees);
14+
15+
return [
16+
centerX + radius * Math.cos(angleInRadians),
17+
centerY + radius * Math.sin(angleInRadians),
18+
];
19+
};
20+
21+
export const segmentPath = (
22+
x: number,
23+
y: number,
24+
r0: number,
25+
r1: number,
26+
d0: number,
27+
d1: number,
28+
) => {
29+
const arc = Math.abs(d0 - d1) > 180 ? 1 : 0;
30+
const point = (radius: number, degree: number) =>
31+
polarToCartesian(x, y, radius, degree)
32+
.map((n) => n.toPrecision(5))
33+
.join(",");
34+
35+
const start = -90 + d0;
36+
const end = -90 + d1;
37+
38+
return [
39+
`M${point(r0, start)}`,
40+
`A${r0},${r0},0,${arc},1,${point(r0, end)}`,
41+
`L${point(r1, end)}`,
42+
`A${r1},${r1},0,${arc},0,${point(r1, start)}`,
43+
"Z",
44+
].join("");
45+
};
46+
47+
export const circlePath = (
48+
x: number,
49+
y: number,
50+
radius: number,
51+
startAngle: number,
52+
endAngle: number,
53+
) => {
54+
"worklet";
55+
const [startX, startY] = polarToCartesian(x, y, radius, endAngle * 0.9999);
56+
const [endX, endY] = polarToCartesian(x, y, radius, startAngle);
57+
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
58+
const d = [
59+
"M",
60+
startX,
61+
startY,
62+
"A",
63+
radius,
64+
radius,
65+
0,
66+
largeArcFlag,
67+
0,
68+
endX,
69+
endY,
70+
];
71+
return d.join(" ");
72+
};

0 commit comments

Comments
 (0)