├── .gitignore
├── .npmignore
├── .release-it.json
├── CHANGELOG.md
├── README.md
├── assets
└── thumbnail.png
├── babel.config.js
├── index.js
├── package.json
├── src
├── index.tsx
└── packages
│ ├── array
│ ├── index.tsx
│ └── sum.tsx
│ ├── coordinate
│ ├── CartesianCoordinate.tsx
│ ├── Coordinate.tsx
│ └── PolarCoordinate.tsx
│ ├── math
│ ├── Degree.test.js
│ ├── Degree.tsx
│ ├── LinearInterpolation.test.js
│ ├── LinearInterpolation.tsx
│ ├── Radian.test.js
│ ├── Radian.tsx
│ └── index.tsx
│ ├── shape
│ ├── Circle.tsx
│ ├── Square.tsx
│ └── index.tsx
│ └── svg
│ ├── Arc.tsx
│ ├── ViewBox.tsx
│ └── index.tsx
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # generated files by bob
2 | lib/
3 | dist/
4 | node_modules/
5 | .expo/
6 | .DS_Store
7 | npm-debug.*
8 | package-lock.json
9 | .idea
10 | .vscode
11 | yarn.lock
12 | yarn-error.log
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | npm-debug.*
4 | /promo
5 | /assets
6 | .babelrc
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "@release-it/conventional-changelog": {
4 | "preset": "angular",
5 | "infile": "CHANGELOG.md"
6 | }
7 | },
8 | "git": {
9 | "changelog": "npx auto-changelog --stdout --commit-limit false -u --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs",
10 | "commitMessage": "chore: release v${version}",
11 | "requireCleanWorkingDir": false
12 | },
13 | "hooks": {
14 | "after:bump": "npx auto-changelog -p"
15 | },
16 | "github": {
17 | "release": true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.0.9](https://github.com/Novsochetra/react-native-circular-chart/compare/v1.0.8...v1.0.9) (2023-01-25)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * undefined is not an object donutItemListeners[i].removeAllListeners ([4b40ca9](https://github.com/Novsochetra/react-native-circular-chart/commit/4b40ca907d6962c92e069281effbd4509d826210))
7 |
8 | ### Changelog
9 |
10 | All notable changes to this project will be documented in this file. Dates are displayed in UTC.
11 |
12 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
13 |
14 | #### [v1.0.9](https://github.com/Novsochetra/react-native-circular-chart/compare/v1.0.8...v1.0.9)
15 |
16 | - fix: undefined is not an object donutItemListeners[i].removeAllListeners [`#3`](https://github.com/Novsochetra/react-native-circular-chart/pull/3)
17 | - chore: adding script prepare:local [`ad6004a`](https://github.com/Novsochetra/react-native-circular-chart/commit/ad6004a550706a12d2962de6e51466110d00bdad)
18 |
19 | #### [v1.0.8](https://github.com/Novsochetra/react-native-circular-chart/compare/v1.0.7...v1.0.8)
20 |
21 | > 11 January 2022
22 |
23 | - fix: wrong package name in package.json [`7aedcdc`](https://github.com/Novsochetra/react-native-circular-chart/commit/7aedcdc5e972cdd93ca507014b6bbdb8ee142dce)
24 | - chore: release v1.0.8 [`1aea7e8`](https://github.com/Novsochetra/react-native-circular-chart/commit/1aea7e8be5da03a85731856619a0e9d450eb09a9)
25 |
26 | #### [v1.0.7](https://github.com/Novsochetra/react-native-circular-chart/compare/v1.0.7-0...v1.0.7)
27 |
28 | > 11 January 2022
29 |
30 | #### [v1.0.7-0](https://github.com/Novsochetra/react-native-circular-chart/compare/v1.0.6...v1.0.7-0)
31 |
32 | > 11 January 2022
33 |
34 | #### [v1.0.6](https://github.com/Novsochetra/react-native-circular-chart/compare/v1.0.5...v1.0.6)
35 |
36 | > 11 January 2022
37 |
38 | #### [v1.0.5](https://github.com/Novsochetra/react-native-circular-chart/compare/v1.0.2...v1.0.5)
39 |
40 | > 11 January 2022
41 |
42 | - chore: init release it [`8e06a35`](https://github.com/Novsochetra/react-native-circular-chart/commit/8e06a35fa878463f20f88dad4752617176c2ee0c)
43 | - chore: set up release it [`0ec5f46`](https://github.com/Novsochetra/react-native-circular-chart/commit/0ec5f4673892cb4a2ddd081d004e73f2387f7f3e)
44 | - chore: release v1.0.5 [`3edde81`](https://github.com/Novsochetra/react-native-circular-chart/commit/3edde8120a693490ecfe525d48f18b8bad37a01a)
45 |
46 | #### v1.0.2
47 |
48 | > 11 January 2022
49 |
50 | - new: donut chart [`f1cd4e1`](https://github.com/Novsochetra/react-native-circular-chart/commit/f1cd4e1bdc213fb89d20c672a2d8126f5dbfdf6b)
51 | - doc: readme.md file [`83ae59d`](https://github.com/Novsochetra/react-native-circular-chart/commit/83ae59da7d575f813fd3ee99005bc8ded5dfc056)
52 | - fix: remove listener only when animation type == 'slide' [`81a938e`](https://github.com/Novsochetra/react-native-circular-chart/commit/81a938e24ff49a5bc158e804da4049ef7ab5c8a3)
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [
](image.png)
2 |
3 | [video demo](https://user-images.githubusercontent.com/20807120/109374979-d3250b00-78eb-11eb-8135-9c7cc338ce43.mov)
4 |
5 |
6 | ## React Native Circular Chart Documentation
7 |
8 | ### Import components
9 |
10 | 1. `yarn add react-native-circular-chart`
11 | 2. `yarn add react-native-svg` install peer dependencies
12 | 3. use with ES6 syntax to import components `import { DonutChart } from "react-native-circular-chart";`
13 |
14 | ### Quick Example
15 | ```js
16 | import { DonutChart } from "react-native-circular-chart";
17 |
18 |
19 |
30 |
31 |
32 | const styles = StyleSheet.create({
33 | sectionWrapper: {
34 | justifyContent: "center",
35 | alignItems: "center",
36 | borderWidth: 1,
37 | borderRadius: 8,
38 | borderColor: "lightgray",
39 | backgroundColor: "#ffffff",
40 | marginVertical: 8,
41 |
42 | shadowColor: "#000",
43 | shadowOffset: {
44 | width: 0,
45 | height: 1,
46 | },
47 | shadowOpacity: 0.2,
48 | shadowRadius: 1.41,
49 |
50 | elevation: 2,
51 | },
52 | });
53 |
54 | ```
55 |
56 | ### Circule Props
57 |
58 | | Property | Type | Description |
59 | | ----------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------ |
60 | | data | Array<{name: string; value: number; color: string;}> | Defines the data for circular |
61 | | containerWidth | number | Defines the width of container |
62 | | containerHeight | number | Defines the height of container |
63 | | radius | number | Defines the radius of circular |
64 | | startAngle | number (degree) | Defines the start point of the circular. default start from -115 deg |
65 | | endAngle | number (degree) | Defines the start point of the circular. default start from 115 deg |
66 | | strokeWidth | number | Defines the thickness of circular item |
67 | | type | 'butt', 'round' | Defines the type of circular item |
68 | | animationType | 'fade', 'slide' | Defines the animation type for chart item |
69 | | labelValueStyle | StyleProp | Defines the style for data.value |
70 | | labelTitleStyle | StyleProp | Defines the style for data.name |
71 | | labelWrapperStyle | StyleProp | Defines the style for wrapper of data.value and data.name |
72 | | containerStyle | StyleProp | Defines the style for container of chart |
73 |
74 | ### More information
75 | This library is built on top of the following open-source projects:
76 | - react-native-svg (https://github.com/react-native-svg/react-native-svg)
77 |
--------------------------------------------------------------------------------
/assets/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Novsochetra/react-native-circular-chart/dea302956f05ec1b3b1ee42980feba04b953deea/assets/thumbnail.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["module:metro-react-native-babel-preset"],
3 | };
4 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { DonutChart } from "./src/index";
2 |
3 | export { DonutChart };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-circular-chart",
3 | "version": "1.0.9",
4 | "description": "circular chart for react-native.",
5 | "main": "index.js",
6 | "module": "index.js",
7 | "react-native": "index.js",
8 | "types": "./dist/index.d.ts",
9 | "files": [
10 | "src",
11 | "dist",
12 | "!**/__tests__",
13 | "!**/__fixtures__",
14 | "!**/__mocks__"
15 | ],
16 | "@react-native-community/bob": {
17 | "source": "src",
18 | "output": "dist",
19 | "targets": [
20 | "commonjs",
21 | "module",
22 | "typescript"
23 | ]
24 | },
25 | "scripts": {
26 | "test": "echo \"Error: no test specified\" && exit 1",
27 | "prepublish": "yarn build",
28 | "prepare": "bob build",
29 | "prepare:local": "yarn pack",
30 | "build": "tsc",
31 | "release:patch": "release-it patch",
32 | "release": "release-it"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "git+https://github.com/Novsochetra/react-native-circular-chart.git"
37 | },
38 | "keywords": [
39 | "donut-chart",
40 | "circle-chart",
41 | "circular-chart",
42 | "react-native-donut-chart",
43 | "chart",
44 | "react-native"
45 | ],
46 | "peerDependencies": {
47 | "react": "*",
48 | "react-native": ">=0.60.0",
49 | "react-native-svg": ">=9"
50 | },
51 | "author": "sochetra NOV ",
52 | "license": "ISC",
53 | "bugs": {
54 | "url": "https://github.com/Novsochetra/react-native-circular-chart/issues"
55 | },
56 | "homepage": "https://github.com/Novsochetra/react-native-circular-chart#readme",
57 | "devDependencies": {
58 | "@react-native-community/bob": "^0.17.1",
59 | "@release-it/conventional-changelog": "^4.0.0",
60 | "@types/react": "^17.0.2",
61 | "@types/react-native": "^0.63.50",
62 | "@typescript-eslint/eslint-plugin": "^4.15.1",
63 | "@typescript-eslint/parser": "^4.15.1",
64 | "eslint": "^7.20.0",
65 | "eslint-config-prettier": "^7.2.0",
66 | "eslint-config-standard": "^16.0.2",
67 | "eslint-config-standard-with-typescript": "^20.0.0",
68 | "eslint-plugin-import": "^2.22.1",
69 | "eslint-plugin-node": "^11.1.0",
70 | "eslint-plugin-prettier": "^3.3.1",
71 | "eslint-plugin-promise": "^4.3.1",
72 | "eslint-plugin-react": "^7.22.0",
73 | "eslint-plugin-react-hooks": "^4.2.0",
74 | "eslint-plugin-react-native": "^3.10.0",
75 | "eslint-plugin-standard": "^5.0.0",
76 | "husky": "^5.0.9",
77 | "lint-staged": "^10.5.4",
78 | "metro-react-native-babel-preset": "^0.65.1",
79 | "prettier": "^2.2.1",
80 | "react-native-svg": "^12.1.0",
81 | "release-it": "^14.12.1",
82 | "typescript": "^4.1.5"
83 | },
84 | "husky": {
85 | "hooks": {
86 | "pre-commit": "tsc && lint-staged"
87 | }
88 | },
89 | "lint-staged": {
90 | "*.{ts,tsx}": [
91 | "eslint --fix",
92 | "prettier --write",
93 | "git add"
94 | ]
95 | },
96 | "dependencies": {},
97 | "eslintIgnore": [
98 | "node_modules/",
99 | "dist/"
100 | ]
101 | }
102 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
2 | import {
3 | StyleProp,
4 | Text,
5 | View,
6 | ViewStyle,
7 | Animated,
8 | StyleSheet,
9 | TextStyle,
10 | Easing,
11 | } from "react-native";
12 |
13 | import { Svg, Path } from "react-native-svg";
14 | import { Square } from "./packages/shape";
15 | import { Arc, ArcParams, ViewBox } from "./packages/svg";
16 | import { sum } from "./packages/array";
17 | import { LinearInterpolation } from "./packages/math";
18 |
19 | export type DonutItem = {
20 | name: string;
21 | value: number;
22 | color: string;
23 | };
24 |
25 | export type DonutAnimationType = "fade" | "slide";
26 |
27 | export type IDonutProps = {
28 | data: DonutItem[];
29 | containerWidth: number;
30 | containerHeight: number;
31 | radius: number;
32 | startAngle?: number;
33 | endAngle?: number;
34 | strokeWidth?: number;
35 | type?: "butt" | "round";
36 | labelValueStyle?: StyleProp;
37 | labelTitleStyle?: StyleProp;
38 | labelWrapperStyle?: StyleProp;
39 | containerStyle?: StyleProp;
40 |
41 | animationType?: DonutAnimationType;
42 | };
43 |
44 | const AnimatedPath = Animated.createAnimatedComponent(Path);
45 |
46 | export const DonutChart = ({
47 | data,
48 | containerWidth,
49 | containerHeight,
50 | radius,
51 | startAngle = -125,
52 | endAngle = startAngle * -1,
53 | strokeWidth = 10,
54 | type = "round",
55 | animationType = "slide",
56 |
57 | labelWrapperStyle,
58 | labelValueStyle,
59 | labelTitleStyle,
60 | containerStyle,
61 | }: IDonutProps) => {
62 | let donutItemListeners: any = [];
63 | const viewBox = new ViewBox({
64 | width: containerWidth,
65 | height: containerHeight,
66 | });
67 | const squareInCircle = new Square({ diameter: radius * 2 });
68 |
69 | const animateOpacity = useRef(new Animated.Value(0)).current;
70 | const animateContainerOpacity = useRef(new Animated.Value(0)).current;
71 | const animatedStrokeWidths = useRef(
72 | data.map(() => new Animated.Value(strokeWidth))
73 | ).current;
74 | const pathRefs = useRef([]);
75 | const animatedPaths = useRef>([]).current;
76 |
77 | const [displayValue, setDisplayValue] = useState(data[0]);
78 |
79 | // TODO:
80 | // remove WTF is this variable ?
81 | const [rotationPaths, setRotationPath] = useState<
82 | Array<{ from: number; to: number }>
83 | >([]);
84 |
85 | const defaultInterpolateConfig = (): {
86 | inputRange: [number, number];
87 | outputRange: [number, number];
88 | } => ({ inputRange: [0, 100], outputRange: [startAngle, endAngle] });
89 |
90 | const sumOfDonutItemValue = useMemo(
91 | (): number =>
92 | data
93 | .map((d) => d.value)
94 | .reduce((total: number, prev: number) => total + prev),
95 | [data]
96 | );
97 |
98 | const donutItemValueToPercentage = useMemo(
99 | () => data.map((d) => (d.value / sumOfDonutItemValue) * 100),
100 | [sumOfDonutItemValue, data]
101 | );
102 |
103 | useMemo(() => {
104 | const rotationRange: Array<{ from: number; to: number }> = [];
105 |
106 | data.forEach((_, idx) => {
107 | const fromValues = sum(donutItemValueToPercentage.slice(0, idx));
108 | const toValues = sum(donutItemValueToPercentage.slice(0, idx + 1));
109 |
110 | animatedPaths.push(
111 | new Animated.Value(
112 | LinearInterpolation({
113 | value: fromValues,
114 | ...defaultInterpolateConfig(),
115 | })
116 | )
117 | );
118 |
119 | rotationRange[idx] = {
120 | from: LinearInterpolation({
121 | value: fromValues,
122 | ...defaultInterpolateConfig(),
123 | }),
124 | to: LinearInterpolation({
125 | value: toValues,
126 | ...defaultInterpolateConfig(),
127 | }),
128 | };
129 | });
130 |
131 | setRotationPath(rotationRange);
132 | }, [data]);
133 |
134 | useEffect(() => {
135 | switch (animationType) {
136 | case "slide":
137 | animateContainerOpacity.setValue(1);
138 | slideAnimation();
139 | break;
140 |
141 | default:
142 | fadeAnimation();
143 | break;
144 | }
145 | }, []);
146 |
147 | const slideAnimation = () => {
148 | const animations: Animated.CompositeAnimation[] = data.map((_, i) => {
149 | const ani = Animated.timing(animatedPaths[i], {
150 | toValue: rotationPaths[i].to,
151 | duration: 3000,
152 | easing: Easing.bezier(0.075, 0.82, 0.165, 1),
153 | useNativeDriver: true,
154 | });
155 |
156 | return ani;
157 | });
158 | Animated.parallel(animations).start();
159 | };
160 |
161 | const fadeAnimation = () => {
162 | Animated.timing(animateContainerOpacity, {
163 | toValue: 1,
164 | duration: 5000,
165 | easing: Easing.bezier(0.075, 0.82, 0.165, 1),
166 | useNativeDriver: true,
167 | }).start();
168 | };
169 |
170 | useEffect(() => {
171 | data.forEach((_, i) => {
172 | const element = pathRefs.current[i];
173 | donutItemListeners[i] = addListener({
174 | element,
175 | animatedValue: animatedPaths[i],
176 | startValue: rotationPaths[i].from,
177 | });
178 | });
179 | }, []);
180 |
181 | useEffect(() => {
182 | return () => {
183 | if (animationType === "slide") {
184 | data.forEach((_, i) => {
185 | if (
186 | donutItemListeners?.[i] &&
187 | donutItemListeners?.[i].removeAllListeners
188 | ) {
189 | donutItemListeners?.[i].removeAllListeners?.();
190 | }
191 | });
192 | }
193 | };
194 | }, []);
195 |
196 | const addListener = ({
197 | element,
198 | animatedValue,
199 | startValue,
200 | }: {
201 | element: any;
202 | animatedValue: Animated.Value;
203 | startValue: number;
204 | }) => {
205 | animatedValue.addListener((angle) => {
206 | const arcParams: ArcParams = {
207 | coordX: viewBox.getCenterCoord().x,
208 | coordY: viewBox.getCenterCoord().y,
209 | radius: radius,
210 | startAngle: startValue,
211 | endAngle: angle.value,
212 | };
213 | const drawPath = new Arc(arcParams).getDrawPath();
214 |
215 | if (element) {
216 | element.setNativeProps({ d: drawPath });
217 | }
218 | });
219 | };
220 |
221 | useEffect(() => {
222 | animateOpacity.setValue(0);
223 | Animated.timing(animateOpacity, {
224 | toValue: 1,
225 | duration: 500,
226 | easing: Easing.bezier(0.075, 0.82, 0.165, 1),
227 | useNativeDriver: true,
228 | }).start();
229 | }, []);
230 |
231 | const onUpdateDisplayValue = (value: DonutItem, index: number) => {
232 | setDisplayValue(value);
233 | animateOpacity.setValue(0);
234 |
235 | Animated.parallel([
236 | Animated.timing(animateOpacity, {
237 | toValue: 1,
238 | duration: 500,
239 | useNativeDriver: true,
240 | }),
241 | ]).start();
242 | };
243 |
244 | const onPressIn = (value: DonutItem, index: number) => {
245 | Animated.timing(animatedStrokeWidths[index], {
246 | toValue: strokeWidth + 2,
247 | duration: 500,
248 | useNativeDriver: true,
249 | easing: Easing.bezier(0.075, 0.82, 0.165, 1),
250 | }).start();
251 | };
252 |
253 | const onPressOut = (index: number) => {
254 | Animated.timing(animatedStrokeWidths[index], {
255 | toValue: strokeWidth,
256 | duration: 500,
257 | useNativeDriver: true,
258 | easing: Easing.bezier(0.075, 0.82, 0.165, 1),
259 | }).start();
260 | };
261 |
262 | const _getContainerStyle = (): StyleProp => [
263 | styles.defaultContainer,
264 | containerStyle,
265 | { width: containerWidth, height: containerHeight },
266 | ];
267 |
268 | const _getLabelValueStyle = (color: string): StyleProp => [
269 | styles.defaultLabelValue,
270 | { color },
271 | labelValueStyle,
272 | ];
273 |
274 | const _getLabelTitleStyle = (color: string): StyleProp => [
275 | styles.defaultLabelTitle,
276 | { color },
277 | labelTitleStyle,
278 | ];
279 |
280 | const _getLabelWrapperStyle = (): Animated.WithAnimatedArray => [
281 | styles.defaultLabelWrapper,
282 | {
283 | width: squareInCircle.getCorner() - strokeWidth,
284 | height: squareInCircle.getCorner() - strokeWidth,
285 | opacity: animateOpacity,
286 | },
287 | labelWrapperStyle,
288 | ];
289 |
290 | return (
291 |
292 |
293 |
321 |
322 |
323 | {displayValue?.value}
324 |
325 |
326 | {displayValue?.name}
327 |
328 |
329 |
330 |
331 | );
332 | };
333 | const styles = StyleSheet.create({
334 | defaultContainer: {
335 | display: "flex",
336 | justifyContent: "center",
337 | alignItems: "center",
338 | },
339 |
340 | defaultLabelWrapper: {
341 | position: "absolute",
342 | justifyContent: "center",
343 | alignItems: "center",
344 | },
345 |
346 | defaultLabelValue: {
347 | fontSize: 32,
348 | fontWeight: "bold",
349 | },
350 |
351 | defaultLabelTitle: {
352 | fontSize: 16,
353 | },
354 | });
355 |
--------------------------------------------------------------------------------
/src/packages/array/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./sum";
2 |
--------------------------------------------------------------------------------
/src/packages/array/sum.tsx:
--------------------------------------------------------------------------------
1 | export function sum(arrays: Array): number {
2 | if (arrays.length == 0) {
3 | return 0;
4 | }
5 | return arrays.reduce((total, prev) => (total += prev));
6 | }
7 |
--------------------------------------------------------------------------------
/src/packages/coordinate/CartesianCoordinate.tsx:
--------------------------------------------------------------------------------
1 | import { Coordinate } from "./Coordinate";
2 |
3 | export class CartesianCoordinate extends Coordinate {
4 | x: number = 0;
5 | y: number = 0;
6 |
7 | constructor() {
8 | super();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/packages/coordinate/Coordinate.tsx:
--------------------------------------------------------------------------------
1 | export class Coordinate {}
2 |
--------------------------------------------------------------------------------
/src/packages/coordinate/PolarCoordinate.tsx:
--------------------------------------------------------------------------------
1 | import { Degree } from "../math";
2 | import { CartesianCoordinate } from "./CartesianCoordinate";
3 | import { Coordinate } from "./Coordinate";
4 |
5 | // https://www.mathsisfun.com/polar-cartesian-coordinates.html
6 | export class PolarCoordinate extends Coordinate {
7 | coordX: number = 0;
8 | coordY: number = 0;
9 | radius: number = 0;
10 | angle: number = 0;
11 |
12 | constructor({
13 | coordX,
14 | coordY,
15 | radius,
16 | angle,
17 | }: {
18 | coordX: number;
19 | coordY: number;
20 | radius: number;
21 | angle: number;
22 | }) {
23 | super();
24 | this.coordX = coordX;
25 | this.coordY = coordY;
26 | this.angle = angle;
27 | this.radius = radius;
28 | }
29 |
30 | toCartesian = (): CartesianCoordinate => {
31 | const startAngle = this.angle - 90
32 | const angleInRadians = new Degree(startAngle).toRadian();
33 |
34 | return {
35 | x: this.coordX + this.radius * Math.cos(angleInRadians),
36 | y: this.coordY + this.radius * Math.sin(angleInRadians),
37 | };
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/src/packages/math/Degree.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | // import { sum } from "../../src/utils/Array";
3 | import { Degree } from "./Degree";
4 |
5 | describe("Converter From Degree To Radian", (): void => {
6 | it("90 deg == π/2 rad", () => {
7 | const degree = 90;
8 | const radian = new Degree(degree).toRadian();
9 |
10 | expect(Number(radian.toFixed(13))).toBe(1.5707963267949);
11 | });
12 |
13 | it("45 deg == π/4", () => {
14 | const degree = 45;
15 | const radian = new Degree(degree).toRadian();
16 |
17 | expect(Number(radian.toFixed(13))).toBe(0.7853981633974);
18 | });
19 |
20 | it("180 deg == π", () => {
21 | const degree = 180;
22 | const radian = new Degree(degree).toRadian();
23 |
24 | const result = Number(radian.toFixed(13));
25 | const actualResult = Number(Math.PI.toFixed(13));
26 |
27 | expect(result).toBe(actualResult);
28 | });
29 |
30 | it("270 deg == 3π/2", () => {
31 | const degree = 270;
32 | const radian = new Degree(degree).toRadian();
33 |
34 | const result = Number(radian.toFixed(13));
35 | const actualResult = Number(((3 * Math.PI) / 2).toFixed(13));
36 |
37 | expect(result).toBe(actualResult);
38 | });
39 |
40 | it("360 deg == 2π", () => {
41 | const degree = 360;
42 | const radian = new Degree(degree).toRadian();
43 |
44 | const result = Number(radian.toFixed(13));
45 | const actualResult = Number((2 * Math.PI).toFixed(13));
46 |
47 | expect(result).toBe(actualResult);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/src/packages/math/Degree.tsx:
--------------------------------------------------------------------------------
1 | export class Degree {
2 | _value = 0;
3 |
4 | constructor(value: number) {
5 | this._value = value;
6 | }
7 |
8 | // degree = radian * 180 / Math.PI => radian = degree * Math.PI / 180
9 | toRadian = (): number => (this._value * Math.PI) / 180;
10 | }
11 |
--------------------------------------------------------------------------------
/src/packages/math/LinearInterpolation.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { LinearInterpolation } from "./LinearInterpolation";
3 |
4 | describe("LinearInterpolation: ", (): void => {
5 | it("should equal 50, when value: 50, inputRange: [0, 100], outputRange: [0, 100]", () => {
6 | const result = LinearInterpolation({
7 | value: 50,
8 | inputRange: [0, 100],
9 | outputRange: [0, 100],
10 | });
11 | const expectedResult = 50;
12 |
13 | expect(result).toBe(expectedResult);
14 | });
15 |
16 | it("should equal 0, when value: 0, inputRange: [0, 100], outputRange: [0, 100]", () => {
17 | const result = LinearInterpolation({
18 | value: 0,
19 | inputRange: [0, 100],
20 | outputRange: [0, 100],
21 | });
22 | const expectedResult = 0;
23 |
24 | expect(result).toBe(expectedResult);
25 | });
26 |
27 | it("should equal 100, when value: 100, inputRange: [0, 100], outputRange: [0, 100]", () => {
28 | const result = LinearInterpolation({
29 | value: 100,
30 | inputRange: [0, 100],
31 | outputRange: [0, 100],
32 | });
33 | const expectedResult = 100;
34 |
35 | expect(result).toBe(expectedResult);
36 | });
37 |
38 | it("should equal 10, when value: 100, inputRange: [0, 100], outputRange: [0, 10]", () => {
39 | const result = LinearInterpolation({
40 | value: 100,
41 | inputRange: [0, 100],
42 | outputRange: [0, 10],
43 | });
44 | const expectedResult = 10;
45 |
46 | expect(result).toBe(expectedResult);
47 | });
48 |
49 | it("should equal 9, when value: 90, inputRange: [0, 100], outputRange: [0, 10]", () => {
50 | const result = LinearInterpolation({
51 | value: 90,
52 | inputRange: [0, 100],
53 | outputRange: [0, 10],
54 | });
55 | const expectedResult = 9;
56 |
57 | expect(result).toBe(expectedResult);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/src/packages/math/LinearInterpolation.tsx:
--------------------------------------------------------------------------------
1 | // this linear interpolation is suppoprt only clamp.
2 | export function LinearInterpolation({
3 | value,
4 | inputRange,
5 | outputRange,
6 | }: {
7 | value: number;
8 | inputRange: [number, number];
9 | outputRange: [number, number];
10 | }) {
11 | const minInputRange = Math.min(...inputRange);
12 | const maxInputRange = Math.max(...inputRange);
13 | const minOutPutRange = Math.min(...outputRange);
14 | const maxOutPutRange = Math.max(...outputRange);
15 |
16 | if (value > maxInputRange) {
17 | return maxOutPutRange;
18 | } else if (value < minInputRange) {
19 | return minOutPutRange;
20 | }
21 |
22 | const percentage = getPercentageRange({
23 | value,
24 | min: minInputRange,
25 | max: maxInputRange,
26 | });
27 |
28 | // formula: (1 - percentage) * min + percentage * max; 😎
29 | return (1 - percentage) * minOutPutRange + percentage * maxOutPutRange;
30 | }
31 |
32 | function getPercentageRange({
33 | value,
34 | min,
35 | max,
36 | }: {
37 | value: number;
38 | min: number;
39 | max: number;
40 | }): number {
41 | //formula calclate percentange by range ((input - min) * 100) / (max - min) 😎
42 |
43 | // return between 0 -> 1
44 | return ((value - min) * 100) / (max - min) / 100;
45 | }
46 |
--------------------------------------------------------------------------------
/src/packages/math/Radian.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | // import { sum } from "../../src/utils/Array";
3 | import { Radian } from "./Radian";
4 |
5 | describe("Converter From Radian To Radian", (): void => {
6 | it("π/2 == 90 deg", () => {
7 | const radian = Math.PI / 2;
8 | const degree = new Radian(radian).toDegree();
9 |
10 | const result = Number(degree.toFixed(13));
11 | const actualResult = 90;
12 |
13 | expect(result).toBe(actualResult);
14 | });
15 |
16 | it("π/4 == 45 deg", () => {
17 | const radian = Math.PI / 4;
18 | const degree = new Radian(radian).toDegree();
19 |
20 | const result = Number(degree.toFixed(13));
21 | const actualResult = 45;
22 |
23 | expect(result).toBe(actualResult);
24 | });
25 |
26 | it("π == 180deg", () => {
27 | const radian = Math.PI;
28 | const degree = new Radian(radian).toDegree();
29 |
30 | const result = Number(degree.toFixed(13));
31 | const actualResult = 180;
32 |
33 | expect(result).toBe(actualResult);
34 | });
35 |
36 | it("3π/2 == 270 deg", () => {
37 | const radian = (3 * Math.PI) / 2;
38 | const degree = new Radian(radian).toDegree();
39 |
40 | const result = Number(degree.toFixed(13));
41 | const actualResult = 270;
42 |
43 | expect(result).toBe(actualResult);
44 | });
45 |
46 | it("2π == 360 deg", () => {
47 | const radian = 2 * Math.PI;
48 | const degree = new Radian(radian).toDegree();
49 |
50 | const result = Number(degree.toFixed(13));
51 | const actualResult = 360;
52 |
53 | expect(result).toBe(actualResult);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/packages/math/Radian.tsx:
--------------------------------------------------------------------------------
1 | export class Radian {
2 | _value = 0;
3 |
4 | constructor(value: number) {
5 | this._value = value;
6 | }
7 |
8 | // degree = radian * 180 / Math.PI
9 | toDegree = (): number => (this._value * 180) / Math.PI;
10 | }
11 |
--------------------------------------------------------------------------------
/src/packages/math/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Degree";
2 | export * from "./Radian";
3 | export * from "./LinearInterpolation";
4 |
--------------------------------------------------------------------------------
/src/packages/shape/Circle.tsx:
--------------------------------------------------------------------------------
1 | export class Circle {
2 | radius = 20;
3 |
4 | constructor({ r }: { r: number }) {
5 | this.radius = r ?? 50;
6 | }
7 |
8 | circumference = () => this.radius * 2 * Math.PI;
9 |
10 | getArcByPercentage = (percentage: number) => {
11 | const degreeInPercentage = 360 * percentage;
12 | return (degreeInPercentage / 360) * this.circumference();
13 | };
14 |
15 | getAngleByPercentange = (percentage: number) => {
16 | return (
17 | (this.getArcByPercentage(percentage) * 360) / 2 / Math.PI / this.radius
18 | );
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/src/packages/shape/Square.tsx:
--------------------------------------------------------------------------------
1 | export class Square {
2 | diameter = 20;
3 |
4 | constructor({ diameter }: { diameter: number }) {
5 | this.diameter = diameter;
6 | }
7 |
8 | getDiameter = () => this.diameter;
9 |
10 | getCorner = () => this.diameter / Math.SQRT2;
11 | }
12 |
--------------------------------------------------------------------------------
/src/packages/shape/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Circle";
2 | export * from "./Square";
3 |
--------------------------------------------------------------------------------
/src/packages/svg/Arc.tsx:
--------------------------------------------------------------------------------
1 | import { PolarCoordinate } from "../coordinate/PolarCoordinate";
2 |
3 | // For more info:
4 | // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
5 |
6 | export type ArcParams = Pick<
7 | Arc,
8 | "coordX" | "coordY" | "startAngle" | "endAngle" | "radius"
9 | >;
10 |
11 | export class Arc {
12 | coordX: number = 0;
13 | coordY: number = 0;
14 | radius: number = 0;
15 | startAngle: number = 0;
16 | endAngle: number = 0;
17 |
18 | constructor(props: ArcParams) {
19 | this.coordX = props.coordX;
20 | this.coordY = props.coordY;
21 | this.radius = props.radius;
22 | this.startAngle = props.startAngle;
23 | this.endAngle = props.endAngle;
24 | }
25 |
26 | getDrawPath(): string {
27 | const start = new PolarCoordinate({
28 | coordX: this.coordX,
29 | coordY: this.coordY,
30 | radius: this.radius,
31 | angle: this.endAngle,
32 | }).toCartesian();
33 |
34 | const end = new PolarCoordinate({
35 | coordX: this.coordX,
36 | coordY: this.coordY,
37 | radius: this.radius,
38 | angle: this.startAngle,
39 | }).toCartesian();
40 |
41 | const largeArcFlag = this.endAngle - this.startAngle <= 180 ? "0" : "1";
42 |
43 | const d = [
44 | "M",
45 | start.x,
46 | start.y,
47 | "A",
48 | this.radius,
49 | this.radius,
50 | 0,
51 | largeArcFlag,
52 | 0,
53 | end.x,
54 | end.y,
55 | ].join(" ");
56 |
57 | return d;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/packages/svg/ViewBox.tsx:
--------------------------------------------------------------------------------
1 | export class ViewBox {
2 | width = 50;
3 | height = 50;
4 |
5 | constructor({ width, height }: { width: number; height: number }) {
6 | this.width = width;
7 | this.height = height;
8 | }
9 |
10 | getWidth = () => this.width;
11 |
12 | getHeight = () => this.height;
13 |
14 | getCenterCoord = () => ({ x: this.width / 2, y: this.height / 2 });
15 | }
16 |
--------------------------------------------------------------------------------
/src/packages/svg/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Arc";
2 | export * from "./ViewBox";
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "es5",
5 | "es6",
6 | "es7",
7 | "es2015",
8 | "es2016",
9 | "es2017",
10 | "es2018",
11 | "esnext"
12 | ],
13 | "target": "es5",
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "rootDir": "./src",
17 | "outDir": "./dist",
18 | "declaration": true,
19 | "declarationMap": true,
20 | "inlineSourceMap": true,
21 | "inlineSources": true,
22 | "esModuleInterop": true,
23 | "noErrorTruncation": true,
24 | "jsx": "react-native",
25 | "skipLibCheck": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------