Chart Components
OneLibro uses Recharts library for data visualization, providing interactive, responsive charts with a consistent dark theme. Chart components visualize spending patterns, budget progress, and financial trends.
Overview
Chart components in OneLibro:
- Built with Recharts - React-based charting library
- Responsive - Adapt to container size
- Interactive - Hover tooltips with formatted data
- Themed - Consistent dark theme styling
- Accessible - ARIA labels for screen readers
- Performance - Optimized for large datasets
SpendingChart
Line chart component for visualizing spending over time, showing daily or monthly spending trends.
Props
interface SpendingChartProps {
data: SpendingDataPoint[]; // Array of spending data points
title?: string; // Chart title (default: "Spending Overview")
loading?: boolean; // Show loading skeleton
}
interface SpendingDataPoint {
date: string; // Date label (e.g., "Jan 24")
amount: number; // Spending amount in cents
}
Usage
Basic Usage:
import SpendingChart from '@/components/finance/SpendingChart';
const spendingData = [
{ date: 'Jan 20', amount: 5000 }, // $50.00
{ date: 'Jan 21', amount: 12000 }, // $120.00
{ date: 'Jan 22', amount: 8500 }, // $85.00
// ... more data points
];
<SpendingChart
data={spendingData}
title="Spending Over Time (Last 30 Days)"
/>
With Loading State:
const { data: transactions, isLoading } = useTransactions();
const chartData = useMemo(() => {
return generateChartData(transactions);
}, [transactions]);
<SpendingChart
data={chartData}
loading={isLoading}
/>
Chart Structure
Visual Layout:
┌────────────────────────────────────┐
│ Spending Over Time (Last 30 Days) │
├────────────────────────────────────┤
│ │
│ $150 ┤ ● │
│ │ ● │
│ $100 ┤ ● │
│ │ ● │
│ $50 ┤ ● │
│ │ │
│ 0 └───────────────────────────►│
│ Jan 20 Jan 22 Jan 24 │
│ │
└────────────────────────────────────┘
Generating Chart Data
Convert transactions to chart data points:
function generateSpendingChartData(
transactions: Transaction[],
days: number = 30
): SpendingDataPoint[] {
const chartData: SpendingDataPoint[] = [];
// Generate date buckets for last N days
for (let i = days - 1; i >= 0; i--) {
const date = new Date();
date.setDate(date.setDate() - i);
const dateStr = date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
// Sum spending for this day
const daySpending = transactions
.filter((tx) => {
const txDate = new Date(tx.transaction_date);
return (
txDate.toDateString() === date.toDateString() &&
tx.amount > 0 // Expenses only
);
})
.reduce((sum, tx) => sum + tx.amount, 0);
chartData.push({
date: dateStr,
amount: daySpending,
});
}
return chartData;
}
Custom Tooltip
The chart displays a custom tooltip on hover:
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length) {
return (
<div className="bg-[#1a1a1a]/95 backdrop-blur-sm px-4 py-3 rounded-xl border border-[#10b981]/40 shadow-xl">
<p className="text-sm font-semibold text-[#e5e5e5] mb-1">
{payload[0].payload.date}
</p>
<p className="text-xs text-[#a3a3a3]">
Spent:{' '}
<span className="font-bold text-[#10b981]">
{formatCurrency(payload[0].value)}
</span>
</p>
</div>
);
}
return null;
};
Tooltip Features:
- Dark translucent background
- Border in primary color (#10b981)
- Date label
- Formatted currency amount
- Appears on hover
Recharts Configuration
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
{/* Grid */}
<CartesianGrid
strokeDasharray="3 3"
stroke="#2a2a2a"
vertical={false}
/>
{/* X Axis (Dates) */}
<XAxis
dataKey="date"
stroke="#6b7280"
fontSize={12}
tickLine={false}
/>
{/* Y Axis (Amounts) */}
<YAxis
stroke="#6b7280"
fontSize={12}
tickLine={false}
tickFormatter={(value) => `$${value / 100}`}
/>
{/* Tooltip */}
<Tooltip content={<CustomTooltip />} />
{/* Line */}
<Line
type="monotone"
dataKey="amount"
stroke="#10b981"
strokeWidth={3}
dot={{ fill: '#10b981', r: 4 }}
activeDot={{ r: 6, fill: '#10b981' }}
/>
</LineChart>
</ResponsiveContainer>
States
Loading State:
if (loading) {
return (
<div>
<h3 className="text-xl font-bold text-[#e5e5e5] mb-6">
{title}
</h3>
<div className="h-72 bg-[#e5e5e5]/5 rounded-xl animate-pulse" />
</div>
);
}
Empty State:
if (!data || data.length === 0) {
return (
<div className="text-center py-12">
<ChartLine className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No spending data available</p>
<p className="text-sm text-gray-600 mt-2">
Connect a bank account to see your spending trends
</p>
</div>
);
}
BudgetProgressChart
Bar chart comparing budget limits vs actual spending (component to be implemented).
Planned Props
interface BudgetProgressChartProps {
budgets: Array<{
name: string;
category: string;
limit: number; // Budget limit in cents
spent: number; // Amount spent in cents
percentage: number; // Percentage of budget used
}>;
showPercentages?: boolean; // Show % labels on bars
height?: number; // Chart height in pixels
}
Planned Usage
<BudgetProgressChart
budgets={[
{
name: 'Groceries',
category: 'Groceries',
limit: 50000, // $500
spent: 42500, // $425
percentage: 85,
},
{
name: 'Dining Out',
category: 'Food and Drink',
limit: 20000, // $200
spent: 19000, // $190
percentage: 95,
},
]}
showPercentages={true}
height={400}
/>
Planned Visual
Groceries ████████████████░░░░ 85% ($425 / $500)
Dining Out ███████████████████░ 95% ($190 / $200)
Transportation ███████░░░░░░░░░░░░░ 40% ($120 / $300)
CategoryPieChart
Pie chart showing spending breakdown by category (component to be implemented).
Planned Props
interface CategoryPieChartProps {
data: Array<{
category: string;
amount: number; // Amount in cents
percentage: number; // Percentage of total
color: string; // Hex color for slice
}>;
showLegend?: boolean;
innerRadius?: number; // For donut chart (0 = full pie)
}
Planned Usage
<CategoryPieChart
data={[
{ category: 'Food & Drink', amount: 45000, percentage: 45, color: '#ef4444' },
{ category: 'Shopping', amount: 30000, percentage: 30, color: '#3b82f6' },
{ category: 'Transportation', amount: 15000, percentage: 15, color: '#f59e0b' },
{ category: 'Other', amount: 10000, percentage: 10, color: '#6b7280' },
]}
showLegend={true}
innerRadius={60} // Donut chart
/>
Chart Theming
Color Palette
Primary Colors:
- Line/Bar Color:
#10b981(green) - Grid Lines:
#2a2a2a(dark gray) - Axis Labels:
#6b7280(medium gray) - Tooltip Background:
#1a1a1a(dark)
Category Colors (for pie/bar charts):
const categoryColors: Record<string, string> = {
'Food and Drink': '#ef4444', // Red
'Groceries': '#10b981', // Green
'Shopping': '#3b82f6', // Blue
'Transportation': '#f59e0b', // Orange
'Entertainment': '#8b5cf6', // Purple
'Bills & Utilities': '#06b6d4', // Cyan
'Healthcare': '#ec4899', // Pink
'Other': '#6b7280', // Gray
};
Budget Status Colors:
function getBudgetColor(percentage: number): string {
if (percentage < 70) return '#10b981'; // Green (safe)
if (percentage < 90) return '#f59e0b'; // Orange (warning)
return '#ef4444'; // Red (danger)
}
Typography
Chart Title:
text-xl font-bold text-[#e5e5e5] mb-6
Axis Labels:
fontSize: 12
stroke: #6b7280
Tooltip Text:
text-sm font-semibold text-[#e5e5e5] /* Date/label */
text-xs text-[#a3a3a3] /* Secondary info */
font-bold text-[#10b981] /* Value */
Responsive Design
Container Sizing
Charts use ResponsiveContainer from Recharts to adapt to parent size:
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
{/* Chart configuration */}
</LineChart>
</ResponsiveContainer>
Breakpoint Adjustments
Mobile (< 768px):
<ResponsiveContainer width="100%" height={250}>
<LineChart>
<XAxis
fontSize={10} // Smaller font
angle={-45} // Angled labels
textAnchor="end"
height={60} // More space for angled labels
/>
</LineChart>
</ResponsiveContainer>
Desktop (> 1024px):
<ResponsiveContainer width="100%" height={400}>
<LineChart>
<XAxis fontSize={12} />
</LineChart>
</ResponsiveContainer>
Performance Optimization
Memoization
Prevent unnecessary re-renders:
const MemoizedSpendingChart = React.memo(SpendingChart);
// Use in component
<MemoizedSpendingChart data={chartData} />
Data Transformation
Memoize expensive data transformations:
const chartData = useMemo(() => {
return generateSpendingChartData(transactions, 30);
}, [transactions]);
Lazy Loading
Lazy load chart components to reduce initial bundle size:
const SpendingChart = dynamic(
() => import('@/components/finance/SpendingChart'),
{
loading: () => (
<div className="h-72 bg-[#e5e5e5]/5 rounded-xl animate-pulse" />
),
ssr: false, // Disable SSR for Recharts (browser-only)
}
);
Accessibility
ARIA Labels
<div
role="img"
aria-label="Spending chart showing daily expenses for the last 30 days"
>
<ResponsiveContainer>
{/* Chart */}
</ResponsiveContainer>
</div>
Screen Reader Table
Provide data table alternative for screen readers:
<div className="sr-only">
<table>
<caption>Spending over last 30 days</caption>
<thead>
<tr>
<th>Date</th>
<th>Amount Spent</th>
</tr>
</thead>
<tbody>
{data.map((point) => (
<tr key={point.date}>
<td>{point.date}</td>
<td>{formatCurrency(point.amount)}</td>
</tr>
))}
</tbody>
</table>
</div>
Common Patterns
Chart with Date Range Selector
function DashboardWithChart() {
const [dateRange, setDateRange] = useState<'7d' | '30d' | '90d'>('30d');
const days = {
'7d': 7,
'30d': 30,
'90d': 90,
};
const chartData = useMemo(() => {
return generateSpendingChartData(transactions, days[dateRange]);
}, [transactions, dateRange]);
return (
<div>
<div className="flex gap-2 mb-4">
<button onClick={() => setDateRange('7d')}>Last 7 Days</button>
<button onClick={() => setDateRange('30d')}>Last 30 Days</button>
<button onClick={() => setDateRange('90d')}>Last 90 Days</button>
</div>
<SpendingChart data={chartData} />
</div>
);
}
Chart with Export
function ExportableChart() {
const exportToCSV = () => {
const csv = data
.map((point) => `${point.date},${point.amount / 100}`)
.join('\n');
const blob = new Blob([`Date,Amount\n${csv}`], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'spending-data.csv';
link.click();
};
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2>Spending Chart</h2>
<button onClick={exportToCSV}>
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
<SpendingChart data={chartData} />
</div>
);
}
Testing Charts
Unit Tests
import { render, screen } from '@testing-library/react';
import SpendingChart from './SpendingChart';
describe('SpendingChart', () => {
const mockData = [
{ date: 'Jan 20', amount: 5000 },
{ date: 'Jan 21', amount: 10000 },
];
it('renders chart with data', () => {
render(<SpendingChart data={mockData} />);
expect(screen.getByText(/Spending Overview/i)).toBeInTheDocument();
});
it('shows empty state when no data', () => {
render(<SpendingChart data={[]} />);
expect(screen.getByText(/No spending data/i)).toBeInTheDocument();
});
it('shows loading state', () => {
render(<SpendingChart data={[]} loading={true} />);
expect(screen.getByRole('status')).toBeInTheDocument();
});
});
Future Enhancements
Planned chart improvements:
- Budget Progress Bar Chart - Compare budgets vs actual spending
- Category Pie/Donut Chart - Spending breakdown by category
- Income vs Expenses Chart - Dual-line chart comparing income and expenses
- Net Worth Over Time - Area chart showing account balance trends
- Monthly Comparison Chart - Bar chart comparing spending across months
- Export to Image - Download chart as PNG/SVG
- Interactive Filters - Filter by category, account, date range
- Zoom and Pan - For large datasets
Related Documentation
- Data Display Components - Tables and cards for data
- Components Overview - All component documentation
Summary
Chart components in OneLibro provide:
- ✅ Interactive visualizations with Recharts
- ✅ Responsive design (mobile, tablet, desktop)
- ✅ Dark theme with consistent styling
- ✅ Custom tooltips with formatted data
- ✅ Loading and empty states
- ✅ Performance optimizations (memoization, lazy loading)
- ✅ Accessibility features (ARIA labels, screen reader tables)
- ✅ Easy data transformation utilities
Use these chart components to help users visualize and understand their financial data!