DEV Community

Cover image for Build Your Expense Tracker using chart.js - PART-1
Kirti
Kirti

Posted on

Build Your Expense Tracker using chart.js - PART-1

How cool would that be if we could just talk about our expenses and our app would calculate and show graphical representation of all our income and expenses with history of our previous records as well.
To add cherry on top let's make our very own Expense Tracker using context API ,Speechly and local Storage.
Let's start with the setup first go to terminal of your VScode and write

npx create-react-app my-app
cd my-app
npm start
Enter fullscreen mode Exit fullscreen mode

One more thing to add is our react version is 17 and we would need to degrade it because our speechly doesn't support the new version of react. so for downgrading write in your terminal.

npm i react@16.12.0 react-dom@16.12.0
Enter fullscreen mode Exit fullscreen mode

Now, let's just install all the other packages that we would be needing for our build.
for design purpose

npm i @material-ui/core @material-ui/icons @material-ui/lab 
Enter fullscreen mode Exit fullscreen mode

for graphical representation and unique id

npm i chart.js react-chartjs-2 uuid
Enter fullscreen mode Exit fullscreen mode

Now , for the special voice feature

npm i @speechly/react-client @speechly/react-ui 
Enter fullscreen mode Exit fullscreen mode

Let's start with setting up the background and basic structure of our app where we would have one component for expense and income and one as main components where we would be adding a form and voice feature to add our expense.
Make Details.js file and for structure have a look at the code below.

import React from 'react'
import {Card , CardHeader , CardContent , Typography } from '@material-ui/core';
// import { Doughnut } from 'react-chartjs-2';
import  useStyles from './styles';
const Details = ({title}) => {
    // We use useStyle hook for importing material-ui class
    const classes =useStyles();

    return (
        // In th below statement for our income use income css class else expense
        <Card className={title === 'Income' ? classes.income : classes.expense}>
            <CardHeader title={title}/>
                <CardContent>
                   <Typography varinat="h5"> $50</Typography>
                   {/* We will be adding data later */}
                   {/* <Doughnut data='DATA' /> */}
                </CardContent>
        </Card>
    )
}

export default Details

Enter fullscreen mode Exit fullscreen mode

Now Styles file

import { makeStyles } from '@material-ui/core/styles';

export default makeStyles(() => ({
  income: {
    borderBottom: '10px solid rgba(0, 255, 0, 0.5)',
  },
  expense: {
    borderBottom: '10px solid rgba(255, 0, 0, 0.5)',
  },
}));

Enter fullscreen mode Exit fullscreen mode
import React from 'react'
import {Card , CardHeader , CardContent , Typography , Grid , Divider, FormControl, InputLabel, Select, MenuItem} from '@material-ui/core';
import useStyles from './styles'
import Form from './Form/Form';
const Main = () => {
        // We use useStyle hook for importing material-ui class
    const classes = useStyles();
    return (
        <Card className={classes.root}>
            <CardHeader title ="Expense Tracker" subheader="Talk to Add your expenses"/>
            <CardContent>
                <Typography align="center" variant="h5"> Total Balance $100</Typography>
                <Typography variant="subtitle1" style={{lineHeight: '1.5em', marginTop: '20px'}}>
                </Typography>
                <Divider/>
                {/* Below is our Form component */}
                <Form/>
            </CardContent>

        </Card>
    )
}

export default Main

Enter fullscreen mode Exit fullscreen mode

Now styles file for main component

import { makeStyles } from '@material-ui/core/styles';

export default makeStyles((theme) => ({
  media: {
    height: 0,
    paddingTop: '56.25%', // 16:9
  },
  expand: {
    transform: 'rotate(0deg)',
    marginLeft: 'auto',
    transition: theme.transitions.create('transform', {
      duration: theme.transitions.duration.shortest,
    }),
  },
  expandOpen: {
    transform: 'rotate(180deg)',
  },
  cartContent: {
    paddingTop: 0,
  },
  divider: {
    margin: '20px 0',
  },
}));

Enter fullscreen mode Exit fullscreen mode

Now Form Component

import React from 'react'
import {TextField, Typography, Grid, Button, FormControl, InputLabel, Select, MenuItem} from '@material-ui/core';
import useStyles from './styles';
const Form = () => {
    const classes = useStyles();
    return (
        <Grid container spacing={2}>
                    <Grid item xs={12}>
                    <Typography align='center' variant='subtitle1' gutterBottom>
                        ...
                    </Typography>
                    </Grid>

                    {/* Another grid for Type */}
                    <Grid item xs={6}>
                    <FormControl fullWidth>
                        <InputLabel>Type</InputLabel>
                        <Select>
                            <MenuItem value="Income">Income</MenuItem>
                            <MenuItem value="Expense">Expense</MenuItem>
                        </Select>
                    </FormControl>
                    </Grid>
                    {/* Another grid for conatiner */}
                    <Grid item xs={6}>
                        <FormControl fullWidth>
                            <InputLabel> Category</InputLabel>
                            <Select>
                            <MenuItem value="Income">I</MenuItem>
                            <MenuItem value="Expense">E</MenuItem>
                        </Select>
                        </FormControl>
                    </Grid>

                    {/*Amount */}
                    <Grid item xs={6}>
                      <TextField type="number" label ="Amount" fullWidth/>
                    </Grid>

                    {/* DATE */}
                    <Grid item xs={6}>
                      <TextField type="date" label ="Date" fullWidth/>
                    </Grid>

                    {/*BUTTON TO SUBMIT OUR FORM */}
                    <Button className={classes.button} variant="outlined" color="primary" fullWidth>
                        Create
                    </Button>
            </Grid>
    )
}

export default Form

Enter fullscreen mode Exit fullscreen mode

Now style file for form

import { makeStyles } from '@material-ui/core/styles';

export default makeStyles(() => ({
  radioGroup: {
    display: 'flex',
    justifyContent: 'center',
    marginBottom: '-10px',
  },
  button: {
    marginTop: '20px',
  },
}));

Enter fullscreen mode Exit fullscreen mode

Now App.js file where we will call and arrange our all components to be rendered on the website.

import React from 'react'
import {Grid} from '@material-ui/core';
import Details from './components/Details/Details';
import useStyles from './styles';
import Main from './components/Main/Main';
const App = () => {

    const classes =useStyles();
    return (
        <div>
            <Grid className= {classes.grid} 
            container spacing={0} 
            alignItems= "center"
            justify="center"
            style={{height: '100vh'}}>
                <Grid item xs={12} sm={3}>
                    <Details title ="Income"/>
                </Grid>
                {/* Main component */}
                <Grid item xs={12} sm={3}>
                    <Main/>
                </Grid>
                {/* Expense component */}
                <Grid item xs={12} sm={3}>
                    <Details title="Expense"/>
                </Grid>
            </Grid>
        </div>
    )
}

export default App

Enter fullscreen mode Exit fullscreen mode

Now styling file for App file

#root, body , html {
    height: 100%;;
    margin: 0;
}

body{
    background: url(./Images/bg.jpg);
    background-size: cover;
}
Enter fullscreen mode Exit fullscreen mode

Now , Our structure is ready and after these lines of codes our app will look something like this.

APP

Now, let's talk about one of the important things which we will be using for our app is Context. Here, you can relate context to
the reducer where we had a store and all the components used to take data from. It is similar to that but simpler and with less boilerplate code. Here, we have two main things to focus on right now is adding transactions and deleting one. Create a folder name context and in that files named context.js and contextReducer.js and have a look at the below code I have added explanation there.
context.js

import React, {useReducer , createContext} from 'react';

import  contextReducer from './contextReducer';

const intitalState = [];

export const ExpenseTrackerContext = createContext(intitalState);

export const Provider = ({children}) => {
    const [transactions, dispatch] = useReducer(contextReducer, intitalState);

    // Action Creators
    //which one we want to delete can be known by the id only 
    const deleteTransactions = (id) => {
          dispatch({type: 'DELETE_TRANSACTION', payload: id});
    };
    // In here while creating we dont know the id so we need the whole transaction
    // dispatching means changing the state of the transition
    const addTransactions = (transaction) => {
        dispatch({type: 'ADD_TRANSACTION', payload: transaction});
    };

    return (
        // This line below means we are sending the delete and add transaction method to be used by the whole app
        <ExpenseTrackerContext.Provider value={{
            deleteTransactions,
            addTransactions,
            transactions,
        }}>
            {children}
        </ExpenseTrackerContext.Provider>
    );

};
Enter fullscreen mode Exit fullscreen mode

contextReducer.js

//logic after doing that action like how changing old state to new

const contextReducer = (state, action) => {
    let transactions;

    switch(action.type){

        case 'DELETE_TRANSACTION':
            transactions = state.filter((t) => t.id !== action.payload);
            return transactions;

        case 'ADD_TRANSACTION':
            transactions = [action.payload, ...state]; 
            return transactions;
        default:
            return state;
    }
}

export default contextReducer;
Enter fullscreen mode Exit fullscreen mode

Now, calling it in our List.jsx

import React, {useContext} from 'react';
import { List as MUIList, ListItem, ListItemAvatar, Avatar, ListItemText, ListItemSecondaryAction, IconButton, Slide } from '@material-ui/core';
import { Delete, MoneyOff } from '@material-ui/icons';
import {ExpenseTrackerContext} from '../../../context/context';
import useStyles from './styles';

const List = () => {
    const classes =useStyles();
    const {transactions , deleteTransactions} = useContext(ExpenseTrackerContext);

    return (
       <MUIList dense={false} className={classes.list}>
           {
               transactions.map((transaction) => (
                   <Slide direction="down" in mountOnEnter unmountOnExit key={transaction.id}>
                       <ListItem>
                           <ListItemAvatar>
                               <Avatar className={transaction.type === 'Income' ?
                               classes.avatarIncome : classes.avatarExpense
                               }>
                                   <MoneyOff/>
                               </Avatar>
                           </ListItemAvatar>
                           <ListItemText primary={transaction.category} 
                           secondary ={`$${transaction.amount} - ${transaction.date}`}></ListItemText>
                           <ListItemSecondaryAction>
                               <IconButton edge="end" aria-label="delete" 
                               onClick={() => deleteTransactions(transaction.id)}>
                                 <Delete/>
                               </IconButton>
                           </ListItemSecondaryAction>
                       </ListItem>

                   </Slide>
               ))
           }

       </MUIList>
    )
}
export default List
Enter fullscreen mode Exit fullscreen mode

Now we would also want to have a dynamic categories list for expenses and income and for that we would create constants and simply in there, we will be creating objects of type, amount, and color. Look at the picture below for a better understanding.

Image description
Categories.js

const incomeColors = ['#123123', '#154731', '#165f40', '#16784f', '#14915f', '#10ac6e', '#0bc77e', '#04e38d', '#00ff9d'];
const expenseColors = ['#b50d12', '#bf2f1f', '#c9452c', '#d3583a', '#dc6a48', '#e57c58', '#ee8d68', '#f79d79', '#ffae8a', '#cc474b', '#f55b5f'];

export const incomeCategories = [
  { type: 'Business', amount: 0, color: incomeColors[0] },
  { type: 'Investments', amount: 0, color: incomeColors[1] },
  { type: 'Extra income', amount: 0, color: incomeColors[2] },
  { type: 'Deposits', amount: 0, color: incomeColors[3] },
  { type: 'Lottery', amount: 0, color: incomeColors[4] },
  { type: 'Gifts', amount: 0, color: incomeColors[5] },
  { type: 'Salary', amount: 0, color: incomeColors[6] },
  { type: 'Savings', amount: 0, color: incomeColors[7] },
  { type: 'Rental income', amount: 0, color: incomeColors[8] },
];

export const expenseCategories = [
  { type: 'Bills', amount: 0, color: expenseColors[0] },
  { type: 'Car', amount: 0, color: expenseColors[1] },
  { type: 'Clothes', amount: 0, color: expenseColors[2] },
  { type: 'Travel', amount: 0, color: expenseColors[3] },
  { type: 'Food', amount: 0, color: expenseColors[4] },
  { type: 'Shopping', amount: 0, color: expenseColors[5] },
  { type: 'House', amount: 0, color: expenseColors[6] },
  { type: 'Entertainment', amount: 0, color: expenseColors[7] },
  { type: 'Phone', amount: 0, color: expenseColors[8] },
  { type: 'Pets', amount: 0, color: expenseColors[9] },
  { type: 'Other', amount: 0, color: expenseColors[10] },
];
// Reset function to reset all the categories if you want to clean them
export const resetCategories = () => {
  incomeCategories.forEach((c) => c.amount = 0);
  expenseCategories.forEach((c) => c.amount = 0);
};
Enter fullscreen mode Exit fullscreen mode

Now, let's call everything in our Form.jsx
Form.jsx

import React, {useContext, useState} from 'react';
import {TextField, Typography, Grid, Button, FormControl, InputLabel, Select, MenuItem} from '@material-ui/core';
import useStyles from './styles';
import {ExpenseTrackerContext} from '../../../context/context';
import {v4 as uuidv4} from 'uuid';
import {incomeCategories , expenseCategories} from '../../../constants/categories';
import formatDate from '../../../Utils/formatDate';


const initialState = {
    amount: '',
    category: '',
    type: 'Income',
    date: formatDate(new Date()),
};

const Form = () => {
    const classes = useStyles();
    const {addTransactions} = useContext(ExpenseTrackerContext);
    const [formData , setFormData] =useState(initialState);


    const createTransaction = () => {
        // For id we will be using uuid
        const transaction = {...formData, amount: Number(formData.amount), id: uuidv4()}
        addTransactions(transaction);
        setFormData(initialState);
    }

    const selectedCategories = formData.type === 'Income' ? incomeCategories : expenseCategories;
    return (
        <Grid container spacing={2}>
                    <Grid item xs={12}>
                    <Typography align='center' variant='subtitle1' gutterBottom>
                        ...
                    </Typography>
                    </Grid>

                    {/* Another grid for Type */}
                    <Grid item xs={6}>
                    <FormControl fullWidth>
                        <InputLabel>Type</InputLabel>
                        <Select value={formData.type} 
                        onChange={(e) => setFormData({...formData, type: e.target.value})}>
                            <MenuItem value="Income">Income</MenuItem>
                            <MenuItem value="Expense">Expense</MenuItem>
                        </Select>
                    </FormControl>
                    </Grid>

                    {/* Another grid for conatiner */}
                    <Grid item xs={6}>
                        <FormControl fullWidth>
                            <InputLabel>Category</InputLabel>
                            <Select value={formData.category} 
                            onChange={ (e) => setFormData({...formData, category: e.target.value})}>
                                    {
                                        selectedCategories.map((c) => 
                                        <MenuItem key ={c.type} value={c.type}>
                                            {c.type}
                                        </MenuItem>
                                        )
                                    }
                        </Select>
                        </FormControl>
                    </Grid>

                    {/*Amount */}
                    <Grid item xs={6}>
                      <TextField type="number" label ="Amount" fullWidth value={formData.amount}
                      onChange={(e) => {setFormData({...formData , amount: e.target.value})}}/>
                    </Grid>

                    {/* DATE */}
                    <Grid item xs={6}>
                      <TextField type="date" label ="Date" fullWidth value={formData.date}
                      onChange={(e)=> {setFormData({...formData, date: formatDate(e.target.value)})}}/>
                    </Grid>

                    {/*BUTTON TO SUBMIT OUR FORM */}
                    <Button className={classes.button} variant="outlined" color="primary" fullWidth
                    onClick={createTransaction}>
                        Create
                    </Button>
            </Grid>
    )
}

export default Form;
Enter fullscreen mode Exit fullscreen mode

For the change in date Format have a look at the below code or you can also use moment library.
util.js

export default (date) => {
    const d = new Date(date);
    let month = `${d.getMonth() + 1}`;
    let day = `${d.getDate()}`;
    const year = d.getFullYear();

    if (month.length < 2) { month = `0${month}`; }
    if (day.length < 2) { day = `0${day}`; }

    return [year, month, day].join('-');
  };

Enter fullscreen mode Exit fullscreen mode

Next part is to add charts the graphical representation of our income and expenses. For that we would be using custom hook(A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks).

UseTransaction.js

import { useContext } from 'react';
import { ExpenseTrackerContext } from './context/context';

import { incomeCategories, expenseCategories, resetCategories } from './constants/categories';

const useTransactions = (title) => {
  resetCategories();
  const { transactions } = useContext(ExpenseTrackerContext);
  const rightTransactions = transactions.filter((t) => t.type === title);
  const total = rightTransactions.reduce((acc, currVal) => acc += currVal.amount, 0);
  const categories = title === 'Income' ? incomeCategories : expenseCategories;

  rightTransactions.forEach((t) => {
    const category = categories.find((c) => c.type === t.category);

    if (category) category.amount += t.amount;
  });

  const filteredCategories = categories.filter((sc) => sc.amount > 0);

  const chartData = {
    datasets: [{
      data: filteredCategories.map((c) => c.amount),
      backgroundColor: filteredCategories.map((c) => c.color),
    }],
    labels: filteredCategories.map((c) => c.type),
  };

  return { filteredCategories, total, chartData };
};

export default useTransactions;
Enter fullscreen mode Exit fullscreen mode

Let's simply call this hook in our details file and one more thing to notice we do need to downgrade out chart.js package fr doing that put these commands in the terminal

npm i chart.js@2.9.4  react-chartjs-2@2.11.1
Enter fullscreen mode Exit fullscreen mode

Now , calling our hook in details.jsx

import React from 'react'
import {Card , CardHeader , CardContent , Typography } from '@material-ui/core';
 import { Doughnut } from 'react-chartjs-2';

import  useStyles from './styles';
import useTransactions from '../../useTransactions';

const Details = ({title, subheader}) => {
    // We use useStyle hook for importing material-ui class
     const {total, chartData} =useTransactions(title);
    const classes =useStyles();


    return (
        // In th below statement for our income use income css class else expense
        <Card className={title === 'Income' ? classes.income : classes.expense}>
            <CardHeader title={title} subheader ={subheader}/>
                <CardContent>
                   <Typography varinat="h5">${total}</Typography>
                   {/* We will be adding data later */}
                   {/* console.log(chartData) */}
                   <Doughnut data={chartData} /> 
                </CardContent>
        </Card>
    );
}
export default Details;
Enter fullscreen mode Exit fullscreen mode

Now, our App looks something like this

Image description
I have also added voice feature with local storage in it as well if you all want me to write an article on that part then please write in comments.
All the code is available here on github
And the app link
Github (leave a start ⭐)
Track_Your_Expenses

Thank You!!

Discussion (3)

Collapse
parimal20 profile image
Parimal Prajapati

Can you help me for my issue?
I want to begin from zero on the y-axis but after zero directly want to show dataset value on the y-axis as per I have attached image.
dev-to-uploads.s3.amazonaws.com/up...

Collapse
kirtisingh3008 profile image
Kirti Author
options: {
    scales: {
        yAxes: [{
            ticks: {
                beginAtZero: true
            }
        }]
    }
}
Enter fullscreen mode Exit fullscreen mode

Did you try this?

Collapse
parimal20 profile image
Parimal Prajapati

Yes. I tried beginAtZero but it automatically creating stepsize. It doesn't giving a desired result. I have attached image link of using beginAtZero.
dev-to-uploads.s3.amazonaws.com/up...

if you know how can i achieve desired result please let me know.