Search code examples
reactjstypescriptreact-typescriptrecharts

Generate data into recharts bar chart using List of DTO


I have this DTO object generated from Rest API using Typescript:

export interface BillingSummaryDTO {
    paid?: number,
    outstanding?: number,
    pastDue?: number,
    cancelled?: number,
    createdAt?: Moment | null,
}

export async function getBillingSummary(): Promise<AxiosResponse<BillingSummaryDTO[]>> {
  return await axios.get<BillingSummaryDTO[]>(
      `${baseUrl}/management/billing/summary`
  );
}

Example chart:

import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
} from "recharts";
import {Box} from "@material-ui/core";

const data = [
  {
    name: "Jan",
    Chargebacks: 4000,
    Transactions: 2400,
    USD: 2400,
  },
  {
    name: "Feb",
    Chargebacks: 3000,
    Transactions: 1398,
    USD: 2210,
  },
  {
    name: "Mar",
    Chargebacks: 2000,
    Transactions: 9800,
    USD: 2290,
  },
  {
    name: "Apr",
    Chargebacks: 2780,
    Transactions: 3908,
    USD: 2000,
  },
  {
    name: "May",
    Chargebacks: 1890,
    Transactions: 4800,
    USD: 2181,
  },
  {
    name: "Jun",
    Chargebacks: 2390,
    Transactions: 3800,
    USD: 2500,
  },
  {
    name: "Jul",
    Chargebacks: 3490,
    Transactions: 4300,
    USD: 2100,
  },
  {
    name: "Aug",
    Chargebacks: 3490,
    Transactions: 4300,
    USD: 2100,
  },
  {
    name: "Sep",
    Chargebacks: 3490,
    Transactions: 4300,
    USD: 2100,
  },
];

const useStyles = makeStyles((theme) =>
  createStyles({
    root: {
      flexGrow: 1,
    },
    paper: {
      padding: theme.spacing(2),
      textAlign: "center",
      color: theme.palette.text.secondary,
    },
  })
);

const usePaperStyles = makeStyles((theme) =>
  createStyles({
    root: {
      display: "flex",
      flexWrap: "wrap",
      "& > *": {
        margin: theme.spacing(1),
        width: theme.spacing(16),
        height: theme.spacing(16),
      },
    },
  })
);

const useTimelineStyles = makeStyles((theme) => ({
  paper: {
    padding: "6px 16px",
  },
  secondaryTail: {
    backgroundColor: theme.palette.secondary.main,
  },
}));

export default function Billing() {
  const [click, setClick] = useState(false);
  const closeMobileMenu = () => setClick(false);
  const classes = useStyles();
  const classesPaper = usePaperStyles();
  const classesTimeline = useTimelineStyles();

  return (
    <>
      <Grid container justifyContent="center" alignItems="center">
        <Grid item xs={11}>
          {/*Padding on the top*/}
          <Box m="5rem" />
        </Grid>

        <Grid item xs={12} >
          <h4 className="chart-label">Invoices Summary</h4>
          <BarChart
            width={1000}
            height={300}
            data={data}
            margin={{
              top: 5,
              right: 30,
              left: 20,
              bottom: 5,
            }}
            barSize={30}
          >
            <XAxis
              dataKey="name"
              scale="point"
              padding={{ left: 10, right: 10 }}
            />
            <YAxis />
            <Tooltip />
            <Legend />
            <CartesianGrid strokeDasharray="3 3" />
            <Bar
              dataKey="Transactions"
              fill="#8884d8"
              background={{ fill: "#eee" }}
            />
          </BarChart>
        </Grid>
      </Grid>
    </>
  );
}

How I can use data from BillingSummaryDTO[] to generate the chart?


Solution

  • Making a function that maps over BillingSummaryDTO[] and converts it into the data model (as visible in the variable data) that you have specified for consumption by the <BarChart/> is a valid approach.

    export interface BarChartDataModel {
      name: string,
      Chargebacks: number,
      Transactions: number,
      USD: number,
    }
    
    const data : BarChartDataModel []
    

    This data will be used in <BarChart/> as

    
    <BarChart
                data={data}
                // ....
              >
             // ...
     </BarChart>
    

    Such a function will follow the map reduce approach with a little help from a JS date management library like momentjs

    4 step solution

    1. Arrange all bills chronologically (oldest bill first) for easier map-reduce later
    2. Bucket bills by year into buckets called 'yearwise-buckets'
    3. Bucket bills in each yearwise-bucket into buckets called 'monthwise-bucket'
    4. Reduce all bills in each monthwise-bucket to an object following the BarChartDataModel interface. A collection of objects following the BarChartDataModel interface is the data we pass to our <BarChart/> component
    5. Consume this object collection inn our <BarChart/> component

    Your code for the same would be :

    export interface BillingSummaryDTO {
        paid?: number,
        outstanding?: number,
        pastDue?: number,
        cancelled?: number,
        createdAt?: Moment | null,
    }
    
    export interface BarChartDataModel {
      name: string,
      Chargebacks: number,
      Transactions: number,
      USD: number,
    }
    
    export async function getBillingSummary(): Promise<AxiosResponse<BillingSummaryDTO[]>> {
      const response = await axios.get<BillingSummaryDTO[]>(
          `${baseUrl}/management/billing/summary`
      );
      // STEP 1 : Chronological ordering. Oldest bills will show first
      const chronologicallyOrdered = response.sort((a:BillingSummaryDTO,b:BillingSummaryDTO)=> a.createdAt - b.createdAt )
    
      // STEP 2 : Bucket by year
      const groupByYear = chronologicallyOrdered.reduce((yearwiseBills : any, bill:BillingSummaryDTO, currIdx:number)) => 
         {
            const year = moment(bill.createdAt).year().toString()
            if(!yearwiseBills[year]){ 
              yearwiseBills[year] = []
            } 
            yearwiseBills[year].push(bill)
            return yearwiseBills
         }
      ,{})
    
      // STEP 3 : In each yearwise bucket -> bucket by month
      const groupByMonth = Object.keys(groupByYear).map((year, yearwiseBills) => yearwiseBills.reduce((monthwiseBills: any, bill:BillingSummaryDTO, currIdx:number)) => 
         {
            const moment = moment(bill.createdAt).month().toString()
            if(!yearAcc[month]){ 
              monthwiseBills[month] = []
            } 
            monthwiseBills[month].push(bill)
            return monthwiseBills
         }
      ,{}) );
    
     // STEP 4 : Reduce all bills in a monthwise bucket into a monthlyReport object and store all monthlyReport objects in an monthlyReportArray
     const monthlyReportArray:BarChartDataModel[] = Object.keys(groupByMonth).map((year, yearwiseBills) => 
       Object.keys(bills).map((month, monthwiseBills) => monthwiseBills.reduce((monthlyReport:BarChartDataModel,bill:BillingSummaryDTO) => {
        if(bill.cancelled){
          monthlyReport.Chargebacks++
        }else{
          monthlyReport.Transactions++,
          monthlyReport.USD += bill.paid
        }
        return monthlyReport
      },{
        name : moment(month, 'M').format('MMM')
        Transactions : 0,
        USD : 0,
        Chargebacks:0
       } )
     )
    
    
     // STEP 5 : Consume this as the "data" for the "<BarChart/>" component
     return monthlyReportArray
    }
     
    
    

    I bucketed by year first and then bucketed by month instead of directly bucketing by month because we don't want to combine the monthly reports of say "May of 1997" and "May of 1998" into just "May" for our chart. We would want them to be separate