├── .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 | 294 | {rotationPaths.map((d, i) => { 295 | const arcParams: ArcParams = { 296 | coordX: viewBox.getCenterCoord().x, 297 | coordY: viewBox.getCenterCoord().y, 298 | radius: radius, 299 | startAngle: d.from, 300 | endAngle: d.to, 301 | }; 302 | const drawPath = new Arc(arcParams).getDrawPath(); 303 | 304 | return ( 305 | (pathRefs.current[i] = el)} 308 | onPress={() => onUpdateDisplayValue(data[i], i)} 309 | onPressIn={() => onPressIn(data[i], i)} 310 | onPressOut={() => onPressOut(i)} 311 | strokeLinecap={type} 312 | d={drawPath} 313 | opacity={animateContainerOpacity} 314 | fill="none" 315 | stroke={data[i].color} 316 | strokeWidth={animatedStrokeWidths[i]} 317 | /> 318 | ); 319 | })} 320 | 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 | --------------------------------------------------------------------------------