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%' },
},
};