From 34ffd38c570242c23c508dad759b2e3a0247de7b Mon Sep 17 00:00:00 2001 From: Katy Date: Thu, 9 Jan 2020 17:28:16 +0800 Subject: [PATCH 1/7] update: move dateSlider to common folder --- src/{Stats => Common}/DateSlider.jsx | 2 +- src/Stats/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{Stats => Common}/DateSlider.jsx (96%) diff --git a/src/Stats/DateSlider.jsx b/src/Common/DateSlider.jsx similarity index 96% rename from src/Stats/DateSlider.jsx rename to src/Common/DateSlider.jsx index b038018..e051dff 100644 --- a/src/Stats/DateSlider.jsx +++ b/src/Common/DateSlider.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { View, Text } from 'react-native'; import PropTypes from 'prop-types'; import { Button } from 'react-native-elements'; -import styles from '../Common/themeStyle'; +import styles from './themeStyle'; export default function DateSlider(props) { const { viewSet, onPressBtn, viewType } = props; diff --git a/src/Stats/index.jsx b/src/Stats/index.jsx index 31d21ec..36856dc 100644 --- a/src/Stats/index.jsx +++ b/src/Stats/index.jsx @@ -5,7 +5,7 @@ import { useSelector } from 'react-redux'; import styles from '../Common/themeStyle'; import MainHeader from '../Common/MainHeader'; import utils from './utils'; -import DateSlider from './DateSlider'; +import DateSlider from '../Common/DateSlider'; import FilterBtn from './FilterBtn'; import TransList from './TransList'; import EmptyHistory from './EmptyHistory'; From 55beea863595e289707069f260e5cea99fcf4244 Mon Sep 17 00:00:00 2001 From: Katy Date: Thu, 9 Jan 2020 18:31:18 +0800 Subject: [PATCH 2/7] update: add files --- package-lock.json | 114 +++++++++++++++++++++++++++++++++++ package.json | 2 + src/Chart/LineGraph.jsx | 45 ++++++++++++++ src/Chart/chartUtils.js | 42 +++++++++++++ src/Chart/index.jsx | 68 +++++++++++++++++++++ src/Common/DateOverlay.jsx | 32 ++++++++++ src/Navigator/TabBarIcon.jsx | 6 +- src/Navigator/index.jsx | 7 ++- 8 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 src/Chart/LineGraph.jsx create mode 100644 src/Chart/chartUtils.js create mode 100644 src/Chart/index.jsx create mode 100644 src/Common/DateOverlay.jsx diff --git a/package-lock.json b/package-lock.json index 989d74f..5d10fbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3885,6 +3885,11 @@ "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.12.0.tgz", "integrity": "sha512-zo+HIdIhzojv6F1siQPqPFROyVy7C50KzHv/k/Iz+BtvtVzSHXiMXOpq2wCfNkeBqdCv+V8XOV96tsEt2W/3rQ==" }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "bplist-creator": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", @@ -4428,6 +4433,38 @@ } } }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-tree": { + "version": "1.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", + "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", + "requires": { + "mdn-data": "2.0.6", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "css-what": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", + "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==" + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -4631,11 +4668,32 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + } + } + }, "dom-walk": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -4645,6 +4703,15 @@ "webidl-conversions": "^4.0.2" } }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -4692,6 +4759,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" + }, "envinfo": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-5.12.1.tgz", @@ -9728,6 +9800,11 @@ "buffer-alloc": "^1.1.0" } }, + "mdn-data": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", + "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==" + }, "mem": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", @@ -10503,6 +10580,14 @@ "gauge": "~1.2.5" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, "nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -10953,6 +11038,11 @@ "pify": "^2.0.0" } }, + "paths-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/paths-js/-/paths-js-0.4.10.tgz", + "integrity": "sha512-JZoqlRSHtx+bc+xKI9o4bropEbqZBF4ZfYImiB1T9RYpHB73h5I8XZ7FfSBbHbBMtdD1c04ujjAPH8wUuu4+Gw==" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -11084,6 +11174,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "point-in-polygon": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.0.1.tgz", + "integrity": "sha1-1Ztk6P7kHElFiqyCtWcYxZV7Kvc=" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -11482,6 +11577,16 @@ "resolved": "https://registry.npmjs.org/react-native-branch/-/react-native-branch-3.0.1.tgz", "integrity": "sha512-vbcYxPZlpF5f39GAEUF8kuGQqCNeD3E6zEdvtOq8oCGZunHXlWlKgAS6dgBKCvsHvXgHuMtpvs39VgOp8DaKig==" }, + "react-native-chart-kit": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-4.3.0.tgz", + "integrity": "sha512-mVi0jT2bbKJOpmyqdn2+kGiInU+7P+ebi2ddQWm/Vfwbmd0InyPi7wj3F5UN9Ymr2tYQxuqpOJtHKmn1bT3/5w==", + "requires": { + "lodash": "^4.17.13", + "paths-js": "^0.4.10", + "point-in-polygon": "^1.0.1" + } + }, "react-native-drawer": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/react-native-drawer/-/react-native-drawer-2.5.1.tgz", @@ -11615,6 +11720,15 @@ "resolved": "https://registry.npmjs.org/react-native-status-bar-height/-/react-native-status-bar-height-2.4.0.tgz", "integrity": "sha512-pWvZFlyIHiuxLugLioq97vXiaGSovFXEyxt76wQtbq0gxv4dGXMPqYow46UmpwOgeJpBhqL1E0EKxnfJRrFz5w==" }, + "react-native-svg": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-10.0.0.tgz", + "integrity": "sha512-7x56ji3oP8+Bh1jVuBtrq/JGxqLBAVf43BCuAbPMKaBlq78TXCMuln7fiBxCUOD4Z5hoZZaVkILhrJPPszfENA==", + "requires": { + "css-select": "^2.0.2", + "css-tree": "^1.0.0-alpha.37" + } + }, "react-native-tab-view": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.11.0.tgz", diff --git a/package.json b/package.json index b12690a..3072fc9 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,12 @@ "react": "16.9.0", "react-dom": "16.9.0", "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz", + "react-native-chart-kit": "^4.3.0", "react-native-elements": "^1.2.7", "react-native-gesture-handler": "~1.3.0", "react-native-modal-datetime-picker": "^7.6.1", "react-native-reanimated": "~1.2.0", + "react-native-svg": "^10.0.0", "react-native-web": "^0.11.7", "react-navigation": "^4.0.10", "react-navigation-stack": "^1.10.3", diff --git a/src/Chart/LineGraph.jsx b/src/Chart/LineGraph.jsx new file mode 100644 index 0000000..6ae6e56 --- /dev/null +++ b/src/Chart/LineGraph.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { View, Dimensions } from 'react-native'; +import { LineChart } from 'react-native-chart-kit'; +import PropTypes from 'prop-types'; + +export default function LineGraph({ dataSet }) { + const screenWidth = Dimensions.get('window').width * 0.85; + + const chartConfig = { + backgroundGradientFrom: '#e96d9e', + backgroundGradientTo: '#ffaa8f', + color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + strokeWidth: 2, + }; + + const data = { + labels: dataSet.labels, + datasets: [{ + data: dataSet.total, + color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + strokeWidth: 2, + }], + }; + return ( + + + + ); +} + +LineGraph.propTypes = { + dataSet: PropTypes.shape({ labels: PropTypes.array, total: PropTypes.array }), +}; + +LineGraph.defaultProps = { + dataSet: {}, +}; diff --git a/src/Chart/chartUtils.js b/src/Chart/chartUtils.js new file mode 100644 index 0000000..5b27425 --- /dev/null +++ b/src/Chart/chartUtils.js @@ -0,0 +1,42 @@ +const moment = require('moment'); + +const convertToLineGraphDataset = (dataList, dataType) => { + const labels = []; + const total = []; + dataList.map((groupedRecordsByDate) => { + const filterData = groupedRecordsByDate.filter((item) => item.type === dataType); + const result = filterData.reduce((sum, { amount }) => { + const parsedAmount = parseFloat(amount); + return sum + parsedAmount; + }, 0); + if (filterData.length !== 0) { + const formatDate = moment(groupedRecordsByDate[0].date, 'MMMM Do YYYY').format('D'); + labels.push(formatDate); + total.push(result); + } + return null; + }); + return { labels: labels.reverse(), total: total.reverse() }; +}; + +const filterDataByPeriod = (dataList, range, view) => { + const monthRange = range.format('MMM YYYY'); + const yearRange = range.format('YYYY'); + let result; + switch (view) { + case 'month': + result = dataList.filter((value) => moment.unix(value.date).format('MMM YYYY') === monthRange); + break; + case 'year': + result = dataList.filter((value) => moment.unix(value.date).format('YYYY') === yearRange); + break; + default: + break; + } + return result; +}; + +export default { + convertToLineGraphDataset, + filterDataByPeriod, +}; diff --git a/src/Chart/index.jsx b/src/Chart/index.jsx new file mode 100644 index 0000000..6247689 --- /dev/null +++ b/src/Chart/index.jsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { View, ScrollView } from 'react-native'; +import { useSelector } from 'react-redux'; +import styles from '../Common/themeStyle'; +import MainHeader from '../Common/MainHeader'; +import DateSlider from '../Common/DateSlider'; +import DateOverlay from '../Common/DateOverlay'; +import utils from '../Stats/utils'; +import statsUtils from './chartUtils'; +import LineGraph from './LineGraph'; + +const moment = require('moment'); + +export default function Chart() { + const { transactions } = useSelector((state) => state.transactions); + const [view, setCurrentView] = useState('month'); + const [timePeriodOptions, setTimePeriod] = useState(utils.getDateSet(moment(), view)); + const [isOverlayVisible, setOverlayVisibility] = useState(false); + const updateHeaderView = (type) => { + setCurrentView(type); + setTimePeriod(utils.getDateSet(moment(), type)); + setOverlayVisibility(!isOverlayVisible); + }; + + const filterListView = (transactionRecords) => { + const current = timePeriodOptions[1]; + return utils.filterData(transactionRecords, current, view); + }; + + + const navBarFunc = [ + { + name: 'filter', + func: () => setOverlayVisibility(true), + }, + ]; + + const filteredDataByDate = filterListView(utils.groupTransactionsByDate(transactions)); + + const filterDataByPeriod = statsUtils.filterDataByPeriod( + transactions, timePeriodOptions[1], view, + ); + + const dataSetByDate = statsUtils.convertToLineGraphDataset(filteredDataByDate, 'Expense'); + + return ( + + updateHeaderView(viewValue)} + onPressClose={() => setOverlayVisibility(false)} + /> + + setTimePeriod(utils.getDateSet(value, type))} + viewType={view} + /> + + + {filterDataByPeriod.length !== 0 + ? + : } + + + + ); +} diff --git a/src/Common/DateOverlay.jsx b/src/Common/DateOverlay.jsx new file mode 100644 index 0000000..3e93c92 --- /dev/null +++ b/src/Common/DateOverlay.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { View } from 'react-native'; +import { ListItem, Overlay, Button } from 'react-native-elements'; +import PropTypes from 'prop-types'; + +export default function DateOverlay(props) { + const { isOverlayVisible, onPressClose, onPressBtn } = props; + + return ( + + + +