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
- NotDataFound - Empty state component for charts with no data
- Google Charts Integration - React Google Charts implementation
- React Charts Integration - React Charts implementation
Chart Types
- Pie Charts - Circular data visualization
- Line Charts - Time series and trend visualization
- Bar Charts - Horizontal and vertical bar charts
- Column Charts - Vertical bar charts
- Donut Charts - Pie charts with center hole
Chart Patterns
- Dashboard Charts - Common dashboard chart patterns
- Analytics Charts - People analytics and HR metrics
- Financial Charts - Payment and wallet statistics
NotDataFound
A component for displaying empty states in charts when no data is available.
Component Location
import NotDataFound from 'components/Charts/NotDataFound';
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| message | string | ✓ | - | 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'>
<HStack>
{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'>
<FormLabel>Department</FormLabel>
<DepartmentFilter
value={department}
onChange={handleDepartmentChange}
isMulti
/>
</FormControl>
<FormControl w='200px'>
<FormLabel>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
-
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]); -
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
-
Memoize Chart Data
const chartData = React.useMemo(() => formatChartData(rawData), [rawData]); -
Optimize Chart Options
const chartOptions = React.useMemo(
() => ({
title: 'Chart Title',
legend: { position: 'bottom' },
chartArea: { width: '90%' },
}),
[],
);
Accessibility
-
Provide Alternative Text
<Chart
chartType='PieChart'
data={data}
options={options}
width='100%'
height='400px'
aria-label='Employee distribution by department'
/> -
Color Accessibility
const accessibleColors = [
'#1f77b4',
'#ff7f0e',
'#2ca02c',
'#d62728',
'#9467bd',
'#8c564b',
'#e377c2',
'#7f7f7f',
'#bcbd22',
'#17becf',
];
Error Handling
-
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' />;
}
} -
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
-
Install Dependencies
npm install react-google-charts -
Update Imports
// Old
import Chart from 'some-chart-library';
// New
import { Chart } from 'react-google-charts'; -
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%' },
},
};