Skip to main content

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


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!