Skip to main content

Chart Components

The Chart Components provide data visualization capabilities for the WorkPayCore Frontend application. These components handle various chart types including pie charts, line charts, bar charts, and column charts with consistent styling and data presentation patterns.

Overview

This document covers chart and data visualization components that display analytics, statistics, and metrics throughout the application using Google Charts and React Charts libraries.

Components Overview

Core Chart Components

Chart Types

Chart Patterns


NotDataFound

A component for displaying empty states in charts when no data is available.

Component Location

import NotDataFound from 'components/Charts/NotDataFound';

Props

PropTypeRequiredDefaultDescription
messagestring-Message to display when no data

TypeScript Interface

interface NotDataFoundProps {
message: string;
}

Usage Examples

Basic Chart Empty State

import NotDataFound from 'components/Charts/NotDataFound';

function SalesChart({ data }) {
if (!data || data.length === 0) {
return (
<NotDataFound message='No sales data available for the selected period' />
);
}

return <Chart data={data} />;
}

Conditional Chart Display

function DashboardWidget({ chartData, isLoading }) {
if (isLoading) {
return <Skeleton height='300px' />;
}

if (!chartData || chartData.length === 0) {
return <NotDataFound message='No data found for the selected filters' />;
}

return (
<Chart
chartType='PieChart'
data={chartData}
options={chartOptions}
width='100%'
height='300px'
/>
);
}

Google Charts Integration

The application uses React Google Charts for most data visualization needs.

Installation

npm install react-google-charts

Basic Usage

import { Chart } from 'react-google-charts';

function BasicChart() {
const data = [
['Task', 'Hours per Day'],
['Work', 11],
['Eat', 2],
['Commute', 2],
['Watch TV', 2],
['Sleep', 7],
];

const options = {
title: 'My Daily Activities',
pieHole: 0.4,
};

return (
<Chart
chartType='PieChart'
data={data}
options={options}
width='100%'
height='400px'
/>
);
}

Chart Types

Pie Charts

Circular charts for showing proportional data.

Basic Pie Chart

import { Chart } from 'react-google-charts';

function EmployeeStatusChart({ statusData }) {
const data = [
['Status', 'Count'],
...statusData.map(item => [item.status, item.count]),
];

const options = {
title: 'Employee Status Distribution',
pieHole: 0,
legend: { position: 'bottom' },
chartArea: { width: '80%' },
};

return (
<Chart
chartType='PieChart'
data={data}
options={options}
width='100%'
height='400px'
/>
);
}

Donut Chart

function WalletStatistics({ walletData }) {
const pieData = [
['Category', 'Amount'],
...walletData.map(item => [item.category, item.amount]),
];

const options = {
pieHole: 0.8,
legend: 'bottom',
tooltip: { text: 'value' },
chartArea: { width: '100%' },
pieSliceText: 'none',
pieSliceBorderColor: 'transparent',
};

return (
<Chart
chartType='PieChart'
data={pieData}
options={options}
width='100%'
height='365px'
loader={
<Center
flexDir='column'
padding='6'
align='center'
width='full'
height='full'
>
<SkeletonCircle size='20' />
<SkeletonText mt='4' noOfLines={1} spacing='4' w='50%' />
</Center>
}
/>
);
}

Line Charts

Time series and trend visualization.

Basic Line Chart

function PayrollSummaryChart({ payrollData }) {
const chartData = [
['Month', 'Amount'],
...payrollData.map(item => [item.month, item.amount]),
];

const options = {
title: 'Payroll Summary',
curveType: 'function',
legend: { position: 'bottom' },
chartArea: { width: '90%', height: '70%' },
colors: ['#62a446'],
};

return (
<Chart
chartType='LineChart'
width='100%'
height='400px'
data={chartData}
options={options}
/>
);
}

Growth Rate Chart

function GrowthRateChart({ growthData }) {
const chartData = [
['Period', 'Growth Rate'],
...growthData.map(item => [item.period, item.rate]),
];

const options = {
title: 'Employee Growth Rate',
curveType: 'function',
legend: { position: 'bottom' },
chartArea: { width: '90%', height: '70%' },
vAxis: { format: 'percent' },
colors: ['#62a446'],
};

return (
<Chart
chartType='LineChart'
width='100%'
height='400px'
data={chartData}
options={options}
/>
);
}

Bar Charts

Horizontal bar charts for categorical data.

Basic Bar Chart

function RetentionRateChart({ retentionData }) {
const chartData = [
['Department', 'Retention Rate'],
...retentionData.map(item => [item.department, item.rate]),
];

const options = {
title: 'Department Retention Rates',
chartArea: { width: '90%' },
legend: { position: 'none' },
bar: { groupWidth: '40%' },
colors: ['#62a446'],
};

return (
<Chart
chartType='Bar'
width='100%'
height='400px'
data={chartData}
options={options}
/>
);
}

Multi-Series Bar Chart

function AgeGroupDepartmentChart({ ageGroupData }) {
const chartData = [
['Department', 'Gen Z', 'Millennials', 'Gen X', 'Boomers'],
...ageGroupData.map(item => [
item.department,
item.genZ,
item.millennials,
item.genX,
item.boomers,
]),
];

const options = {
title: 'Age Group Distribution by Department',
chartArea: { width: '90%' },
legend: { position: 'bottom' },
colors: ['#f9dba1', '#deebff', '#a0c68f', '#f3b744', '#62a446'],
bar: { groupWidth: '75%' },
};

return (
<Chart
chartType='Bar'
width='100%'
height='400px'
data={chartData}
options={options}
/>
);
}

Column Charts

Vertical bar charts for categorical data.

Basic Column Chart

function DepartmentSummaryChart({ departmentData }) {
const chartData = [
['Department', 'Employee Count'],
...departmentData.map(item => [item.name, item.count]),
];

const options = {
title: 'Employees by Department',
chartArea: { width: '90%' },
legend: { position: 'none' },
colors: ['#62a446'],
};

return (
<Chart
chartType='ColumnChart'
width='100%'
height='400px'
data={chartData}
options={options}
/>
);
}

Age Demographics Column Chart

function AgeSummaryChart({ ageData }) {
const chartData = [
['Age Group', 'Count'],
...ageData.map(item => [item.ageGroup, item.count]),
];

const options = {
title: 'Employee Age Distribution',
chartArea: { width: '90%' },
legend: { position: 'none' },
bar: { groupWidth: '60%' },
colors: ['#62a446'],
};

return (
<Chart
chartType='ColumnChart'
width='100%'
height='450px'
data={chartData}
options={options}
/>
);
}

React Charts Integration

Some components use React Charts for more advanced interactions.

Installation

npm install react-charts

Basic Usage

import { Chart } from 'react-charts';

function PaymentDashboard({ paymentData }) {
const data = React.useMemo(
() => [
{
label: 'Payments',
data: paymentData.map(item => [item.date, item.amount]),
},
],
[paymentData],
);

const axes = React.useMemo(
() => [
{ primary: true, type: 'time', position: 'bottom' },
{ type: 'linear', position: 'left' },
],
[],
);

const series = React.useMemo(
() => ({
type: 'line',
}),
[],
);

return (
<Box height='400px'>
<Chart data={data} series={series} axes={axes} tooltip />
</Box>
);
}

Chart Patterns

Dashboard Charts

Common patterns for dashboard visualizations.

Stat Card with Chart

function PayrollStatCard({ payrollData }) {
const chartData = React.useMemo(
() => payrollData.map(item => [item.month, item.amount]),
[payrollData],
);

const latestTotal = payrollData[payrollData.length - 1]?.total;
const latestPercentage = calculatePercentageChange(payrollData);

return (
<StatCardBluePrint
header={<StatCardHeader heading='Payroll Summary' />}
body={
<StatCardBody px={5} py={2} w='full' justify='flex-start'>
<VStack w='full' align='flex-start'>
<HStack w='full' justify='space-between'>
&lt;HStack&gt;
{latestTotal && (
<Text color='onyx' fontWeight='bold' fontSize='lg'>
{latestTotal}
</Text>
)}
{latestPercentage && (
<Text color='#387E1B' bg='#e7f1e3' fontSize='sm' p={1}>
{latestPercentage}
</Text>
)}
</HStack>
</HStack>
<HStack w='full' h='full' justify='center' align='center'>
{chartData?.length > 0 ? (
<Chart
chartType='LineChart'
width='100%'
height='400px'
data={chartData}
options={options}
/>
) : (
<NotDataFound message='No Data found' />
)}
</HStack>
</VStack>
</StatCardBody>
}
/>
);
}

Multi-Chart Dashboard

function PaymentDashboard({ data, isLoading }) {
if (isLoading) {
return <ChartsLoader />;
}

return (
<Stack direction='row' spacing={4} width='100%'>
<Stack width='35%' spacing={4}>
{/* Top-up Chart */}
<Stack borderRadius='lg' p={4} bgColor='white' spacing={4}>
<Stack spacing={0}>
<Text color='skeletonBorder' fontSize='10px' fontWeight='normal'>
AMOUNT TOPPED UP
</Text>
<Text color='green' fontSize='18px' fontWeight='bold'>
KES {data?.topup?.total}
</Text>
</Stack>
<Box height='200px'>
<Chart data={topUpData} series={series} axes={axes} tooltip />
</Box>
</Stack>

{/* Payout Chart */}
<Stack borderRadius='lg' p={4} bgColor='white' spacing={4}>
<Stack spacing={0}>
<Text color='skeletonBorder' fontSize='10px' fontWeight='normal'>
AMOUNT PAID
</Text>
<Text color='green' fontSize='18px' fontWeight='bold'>
KES {numberFormat(data?.payout?.total || 0)}
</Text>
</Stack>
<Box height='200px'>
<Chart data={amountPaidData} series={series} axes={axes} tooltip />
</Box>
</Stack>
</Stack>

{/* Transaction Charges Chart */}
<Stack borderRadius='lg' p={4} bgColor='white' spacing={4} width='65%'>
<Stack spacing={0}>
<Text color='skeletonBorder' fontSize='10px' fontWeight='normal'>
TRANSACTION CHARGES
</Text>
<Text color='green' fontSize='18px' fontWeight='bold'>
KES {numberFormat(transactionSummary)}
</Text>
</Stack>
<Box height='500px'>
<Chart data={transactionsData} series={series} axes={axes} tooltip />
</Box>
</Stack>
</Stack>
);
}

Analytics Charts

People analytics and HR metrics patterns.

Employee Statistics with Legend

function TotalEmployeesChart({ employeeData }) {
const chartData = React.useMemo(
() => formatTotalEmployees(employeeData?.items),
[employeeData],
);

const legendItems = [
{ color: '#5fa145', label: 'Male', icon: <GenderMaleOutline /> },
{ color: '#e5eaed', label: 'Female', icon: <GenderFemaleOutline /> },
{ color: '#f3b744', label: 'Unknown', icon: <GenderAmbiguousOutline /> },
];

const options = {
pieHole: 0.4,
legend: { position: 'none' },
colors: ['#5fa145', '#e5eaed', '#f3b744'],
pieSliceText: 'none',
};

return (
<StatCardBluePrint
header={<StatCardHeader heading='Total Employees' />}
body={
<StatCardBody px={5} py={2}>
<VStack w='full' align='flex-start'>
<Text color='#253545' fontSize='xl' fontWeight='400'>
{employeeData?.total}
</Text>
<HStack w='full' align='center'>
{chartData?.length > 0 ? (
<Chart
chartType='PieChart'
data={chartData}
options={options}
width='250px'
height='250px'
/>
) : (
<NotDataFound message='No Data Found' />
)}
</HStack>
<HStack
w='full'
align='space-between'
justify='space-between'
spacing={3}
>
{legendItems.map((item, index) => (
<HStack key={index} align='center'>
<Box bg={item.color} boxSize='10px' borderRadius='50%' />
{item.icon}
<Text fontSize='sm' color='charcoal'>
{item.label}
</Text>
</HStack>
))}
</HStack>
</VStack>
</StatCardBody>
}
/>
);
}

Filterable Analytics Chart

function RetentionRateInsight() {
const [department, setDepartment] = React.useState();
const [ageGroup, setAgeGroup] = React.useState();
const [filters, setFilters] = React.useState({
department_ids: 'ALL',
age_groups: '',
});

const retentionData = useGroupAgeRetentionAnalysis(filters);
const chartData = React.useMemo(
() => formatRetentionData(retentionData?.data?.items),
[retentionData],
);

const options = {
title: 'Retention Rate by Age Group',
colors: ['#f9dba1', '#deebff', '#a0c68f', '#f3b744', '#62a446'],
chartArea: { width: '90%' },
legend: { position: 'none' },
};

return (
<StatCardBluePrint
header={<StatCardHeader heading='Retention Rate Insight' />}
body={
<StatCardBody px={5} py={2} w='full' justify='flex-start'>
<VStack w='full' align='flex-start' spacing={4}>
{/* Filter Controls */}
<HStack w='full' justify='space-between' align='flex-end'>
<HStack spacing={4}>
<FormControl w='200px'>
&lt;FormLabel&gt;Department</FormLabel>
<DepartmentFilter
value={department}
onChange={handleDepartmentChange}
isMulti
/>
</FormControl>
<FormControl w='200px'>
&lt;FormLabel&gt;Age Group</FormLabel>
<AgeGroupFilter
value={ageGroup}
onChange={handleAgeGroupChange}
isMulti
/>
</FormControl>
</HStack>
<Button variant='outline' onClick={handleResetFilters}>
Reset Filters
</Button>
</HStack>

{/* Chart */}
<HStack w='full' justify='flex-start' overflowX='scroll'>
{retentionData?.isLoading ? (
<ChartSkeleton />
) : chartData?.length > 1 ? (
<Chart
chartType='Bar'
width='100%'
height='400px'
data={chartData}
options={options}
/>
) : (
<NotDataFound message='No Data Found' />
)}
</HStack>

{/* Legend */}
<HStack spacing={4}>
{legendColors.map((legend, i) => (
<HStack key={i} mr={2}>
<Box bg={legend.color} boxSize='10px' borderRadius='50%' />
<Text color='skeletonDark' fontSize='sm'>
{legend.age}
</Text>
</HStack>
))}
</HStack>
</VStack>
</StatCardBody>
}
/>
);
}

Financial Charts

Payment and wallet statistics patterns.

Country Statistics Pie Chart

function CountryStatChart({ type = 'nationality' }) {
const [chartType, setChartType] = useState(type);
const countryStat = useEmployeeCountryStat({ type: chartType });

const pieData = React.useMemo(() => {
const data = countryStat?.data?.data || [];
return data.reduce(
(acc, item) => {
const label =
chartType === 'gender'
? `${item.gender} ${item.employees_count}`
: `${item.country_name} ${item.employees_count}`;
acc.push([label, Number(item.employees_count)]);
return acc;
},
[[chartType, 'Employees']],
);
}, [countryStat?.data?.data, chartType]);

const options = {
pieHole: 0.8,
legend: 'bottom',
tooltip: { text: 'value' },
chartArea: { width: '100%' },
pieSliceText: 'none',
pieSliceBorderColor: 'transparent',
};

return (
<StatCardBluePrint
header={<StatCardHeader heading={`Employee ${chartType}`} />}
body={
<StatCardBody px={5} py={2} w='full' justify='center'>
<VStack w='full' align='center'>
<Text color='charcoal' fontSize='2xl' fontWeight='bold'>
{countryStat?.data?.data?.reduce(
(acc, item) => acc + Number(item.employees_count),
0,
)}
</Text>
<Box height='300px' width='100%'>
<Chart
chartType='PieChart'
data={pieData}
options={options}
width='100%'
height='100%'
/>
</Box>
</VStack>
</StatCardBody>
}
/>
);
}

Loading States

Chart Skeleton

function ChartSkeleton() {
return (
<VStack w='full' align='center' justify='center' spacing={8}>
<HStack w='full' justify='flex-start' align='flex-end' spacing={8}>
<Skeleton height='50px' w='8%' />
<Skeleton height='100px' w='8%' />
<Skeleton height='150px' w='8%' />
<Skeleton height='200px' w='8%' />
<Skeleton height='50px' w='8%' />
<Skeleton height='100px' w='8%' />
<Skeleton height='50px' w='8%' />
<Skeleton height='80px' w='8%' />
<Skeleton height='70px' w='8%' />
<Skeleton height='200px' w='8%' />
<Skeleton height='50px' w='8%' />
<Skeleton height='150px' w='8%' />
</HStack>
</VStack>
);
}

Pie Chart Skeleton

function PieChartSkeleton() {
return (
<VStack w='full' align='center'>
<HStack py={5} w='full' align='center' justify='center'>
<SkeletonCircle w='150px' h='150px' />
</HStack>
<HStack w='full' align='center' justify='center'>
{[...Array(3)].map((_, index) => (
<HStack key={index} w='20%'>
<SkeletonCircle w='10px' h='10px' />
<SkeletonText noOfLines={1} spacing='2' w='30%' />
</HStack>
))}
</HStack>
</VStack>
);
}

Charts Loader

function ChartsLoader() {
return (
<HStack width='full' spacing={4}>
<Stack width='35%' height='440px' spacing={4}>
<Skeleton width='full' borderRadius='lg' height='full' />
<Skeleton width='full' borderRadius='lg' height='full' />
</Stack>
<Stack width='65%' height='440px'>
<Skeleton width='full' borderRadius='lg' height='full' />
</Stack>
</HStack>
);
}

Best Practices

Data Formatting

  1. Consistent Data Structure

    // Good - consistent structure
    const chartData = [
    ['Category', 'Value'],
    ...data.map(item => [item.name, item.value]),
    ];

    // Bad - inconsistent structure
    const chartData = data.map(item => [item.name, item.value]);
  2. Handle Missing Data

    const formatChartData = data => {
    if (!data || data.length === 0) return [];

    return [
    ['Category', 'Value'],
    ...data.map(item => [item.name || 'Unknown', item.value || 0]),
    ];
    };

Performance Optimization

  1. Memoize Chart Data

    const chartData = React.useMemo(() => formatChartData(rawData), [rawData]);
  2. Optimize Chart Options

    const chartOptions = React.useMemo(
    () => ({
    title: 'Chart Title',
    legend: { position: 'bottom' },
    chartArea: { width: '90%' },
    }),
    [],
    );

Accessibility

  1. Provide Alternative Text

    <Chart
    chartType='PieChart'
    data={data}
    options={options}
    width='100%'
    height='400px'
    aria-label='Employee distribution by department'
    />
  2. Color Accessibility

    const accessibleColors = [
    '#1f77b4',
    '#ff7f0e',
    '#2ca02c',
    '#d62728',
    '#9467bd',
    '#8c564b',
    '#e377c2',
    '#7f7f7f',
    '#bcbd22',
    '#17becf',
    ];

Error Handling

  1. Graceful Fallbacks

    function ChartWithFallback({ data }) {
    if (!data || data.length === 0) {
    return <NotDataFound message='No data available' />;
    }

    try {
    return <Chart chartType='PieChart' data={data} options={options} />;
    } catch (error) {
    console.error('Chart rendering error:', error);
    return <NotDataFound message='Error loading chart' />;
    }
    }
  2. Loading States

    function ChartContainer({ isLoading, data }) {
    if (isLoading) {
    return <ChartSkeleton />;
    }

    if (!data || data.length === 0) {
    return <NotDataFound message='No data available' />;
    }

    return <Chart data={data} />;
    }

Testing

Unit Tests

import { render, screen } from '@testing-library/react';
import NotDataFound from 'components/Charts/NotDataFound';

describe('NotDataFound', () => {
it('renders message correctly', () => {
render(<NotDataFound message='Test message' />);
expect(screen.getByText('Test message')).toBeInTheDocument();
});

it('displays no data icon', () => {
render(<NotDataFound message='Test' />);
expect(screen.getByRole('img')).toBeInTheDocument();
});
});

Integration Tests

import { render, screen, waitFor } from '@testing-library/react';
import EmployeeChart from 'components/Charts/EmployeeChart';

describe('EmployeeChart', () => {
it('renders chart with data', async () => {
const mockData = [
{ name: 'Engineering', count: 10 },
{ name: 'Marketing', count: 5 },
];

render(<EmployeeChart data={mockData} />);

await waitFor(() => {
expect(screen.getByText('Engineering')).toBeInTheDocument();
expect(screen.getByText('Marketing')).toBeInTheDocument();
});
});

it('shows empty state when no data', () => {
render(<EmployeeChart data={[]} />);
expect(screen.getByText('No data available')).toBeInTheDocument();
});
});

Migration Guide

From Basic Charts to Google Charts

  1. Install Dependencies

    npm install react-google-charts
  2. Update Imports

    // Old
    import Chart from 'some-chart-library';

    // New
    import { Chart } from 'react-google-charts';
  3. Update Chart Configuration

    // Old
    <Chart
    data={data}
    type="pie"
    config={config}
    />

    // New
    <Chart
    chartType="PieChart"
    data={data}
    options={options}
    width="100%"
    height="400px"
    />

Data Format Migration

// Old format
const oldData = {
labels: ['A', 'B', 'C'],
datasets: [{ data: [1, 2, 3] }],
};

// New format for Google Charts
const newData = [
['Label', 'Value'],
['A', 1],
['B', 2],
['C', 3],
];

Common Chart Configurations

Standard Color Palette

const workPayColors = {
primary: '#62a446',
secondary: '#f3b744',
accent: '#deebff',
neutral: '#e5eaed',
warning: '#f9dba1',
error: '#fee6e9',
};

const chartColors = [
workPayColors.primary,
workPayColors.secondary,
workPayColors.accent,
workPayColors.neutral,
workPayColors.warning,
];

Responsive Chart Options

const responsiveOptions = {
responsive: true,
maintainAspectRatio: false,
chartArea: {
width: '90%',
height: '80%',
},
legend: {
position: 'bottom',
textStyle: { fontSize: 12 },
},
titleTextStyle: {
fontSize: 16,
bold: true,
},
};

Default Chart Configurations

const defaultChartConfigs = {
pieChart: {
pieHole: 0.4,
legend: { position: 'bottom' },
pieSliceText: 'percentage',
tooltip: { text: 'both' },
},
lineChart: {
curveType: 'function',
legend: { position: 'bottom' },
pointSize: 5,
lineWidth: 2,
},
barChart: {
legend: { position: 'none' },
bar: { groupWidth: '75%' },
chartArea: { width: '85%' },
},
columnChart: {
legend: { position: 'none' },
bar: { groupWidth: '60%' },
chartArea: { width: '90%' },
},
};