Analytics & Reports
DWallet provides comprehensive analytics to help users understand their financial patterns through visual charts and detailed breakdowns.
Overview
Section titled “Overview”File: lib/app/pages/client/analytics/analytics_view.dart
Analytics features include:
- Net worth tracking over time (line chart)
- Spending breakdown by category (donut chart)
- Income vs Expense comparison
- Time-based filtering (Weekly, Monthly, Yearly)
Charts
Section titled “Charts”Net Worth Chart
Section titled “Net Worth Chart”File: lib/app/pages/client/analytics/components/_line_chart.dart
Custom painted line chart showing balance trends:
NetWorthChart( dataPoints: netWorthData, lineColor: theme.colorScheme.primary, fillColor: theme.colorScheme.primary.withOpacity(0.1),)Features:
- Smooth line curves
- Gradient fill below line
- Interactive points
- Animated transitions
- Date labels on X-axis
Spending Breakdown
Section titled “Spending Breakdown”File: lib/app/pages/client/analytics/components/_pi_chart.dart
Custom painted donut chart for category breakdown:
PiChart( segments: spendingByCategory, centerText: 'Total\n\$2,450',)Features:
- Color-coded categories
- Percentage display
- Interactive segments
- Center summary text
- Legend with category names
Data Calculation
Section titled “Data Calculation”Net Worth Over Time
Section titled “Net Worth Over Time”List<DataPoint> calculateNetWorthHistory( List<Transaction> transactions, DateTime startDate, DateTime endDate,) { final dataPoints = <DataPoint>[]; var runningBalance = 0.0;
// Sort transactions by date final sortedTransactions = transactions .sorted((a, b) => a.date.compareTo(b.date));
// Calculate daily balances for (var date = startDate; date.isBefore(endDate); date = date.add(Duration(days: 1))) {
// Add transactions for this day final dayTransactions = sortedTransactions .where((t) => isSameDay(t.date, date));
for (final transaction in dayTransactions) { if (transaction.type == TransactionType.income) { runningBalance += transaction.amount; } else { runningBalance -= transaction.amount; } }
dataPoints.add(DataPoint( date: date, value: runningBalance, )); }
return dataPoints;}Spending by Category
Section titled “Spending by Category”Map<Category, double> calculateSpendingByCategory( List<Transaction> transactions, DateTime startDate, DateTime endDate,) { final categoryTotals = <Category, double>{};
final filteredTransactions = transactions .where((t) => t.type == TransactionType.expense) .where((t) => t.date.isAfter(startDate) && t.date.isBefore(endDate));
for (final transaction in filteredTransactions) { categoryTotals.update( transaction.category, (value) => value + transaction.amount, ifAbsent: () => transaction.amount, ); }
return categoryTotals;}Income vs Expense Summary
Section titled “Income vs Expense Summary”class FinancialSummary { final double totalIncome; final double totalExpense; final double netSavings;
double get savingsRate => totalIncome > 0 ? (netSavings / totalIncome) * 100 : 0;}
FinancialSummary calculateSummary( List<Transaction> transactions, DateTimeRange range,) { final filtered = transactions.where( (t) => t.date.isAfter(range.start) && t.date.isBefore(range.end) );
final income = filtered .where((t) => t.type == TransactionType.income) .fold(0.0, (sum, t) => sum + t.amount);
final expense = filtered .where((t) => t.type == TransactionType.expense) .fold(0.0, (sum, t) => sum + t.amount);
return FinancialSummary( totalIncome: income, totalExpense: expense, netSavings: income - expense, );}Time Period Filtering
Section titled “Time Period Filtering”Users can filter analytics by time period:
enum TimePeriod { weekly, monthly, yearly,}
class TimePeriodFilter { static DateTimeRange getRange(TimePeriod period) { final now = DateTime.now();
switch (period) { case TimePeriod.weekly: return DateTimeRange( start: now.subtract(Duration(days: 7)), end: now, ); case TimePeriod.monthly: return DateTimeRange( start: DateTime(now.year, now.month - 1, now.day), end: now, ); case TimePeriod.yearly: return DateTimeRange( start: DateTime(now.year - 1, now.month, now.day), end: now, ); } }}UI Components
Section titled “UI Components”Period Selector
Section titled “Period Selector”ShadToggleGroup( children: [ ShadToggleOption(value: TimePeriod.weekly, child: Text('Week')), ShadToggleOption(value: TimePeriod.monthly, child: Text('Month')), ShadToggleOption(value: TimePeriod.yearly, child: Text('Year')), ], onValueChanged: (value) => setState(() => _selectedPeriod = value),)Chart Cards
Section titled “Chart Cards”ShadCard( title: Text('Net Worth Trend'), child: SizedBox( height: 200, child: NetWorthChart(dataPoints: netWorthData), ),)Customization
Section titled “Customization”Change Chart Colors
Section titled “Change Chart Colors”NetWorthChart( dataPoints: data, lineColor: Colors.green, // Custom color fillGradient: LinearGradient( colors: [Colors.green.withOpacity(0.3), Colors.transparent], ),)Custom Time Ranges
Section titled “Custom Time Ranges”Add custom periods:
enum TimePeriod { daily, weekly, monthly, quarterly, yearly, custom,}Export Data
Section titled “Export Data”Add export functionality:
Future<void> exportAnalytics() async { final csvData = _convertToCSV(transactions); await _saveToFile(csvData, 'analytics.csv');}Best Practices
Section titled “Best Practices”- Cache calculations: Don’t recalculate on every build
- Lazy loading: Load data when tab is selected
- Smooth animations: Animate chart updates
- Empty states: Show message when no data
- Interactive charts: Allow tap for details
- Compare periods: Show previous period comparison
- Drill down: Tap category to see transactions
Performance
Section titled “Performance”Optimize Large Datasets
Section titled “Optimize Large Datasets”// Aggregate daily data instead of showing every transactionList<DataPoint> aggregateByDay(List<Transaction> transactions) { final dailyTotals = <DateTime, double>{};
for (final transaction in transactions) { final date = DateTime( transaction.date.year, transaction.date.month, transaction.date.day, );
dailyTotals.update( date, (value) => value + transaction.signedAmount, ifAbsent: () => transaction.signedAmount, ); }
return dailyTotals.entries .map((e) => DataPoint(date: e.key, value: e.value)) .sorted((a, b) => a.date.compareTo(b.date)) .toList();}Debounce Updates
Section titled “Debounce Updates”final _updateSubject = PublishSubject<void>();
void initState() { super.initState(); _updateSubject .debounceTime(Duration(milliseconds: 300)) .listen((_) => _recalculateAnalytics());}Testing
Section titled “Testing”test('Net worth calculation is correct', () { final transactions = [ TransactionModel(amount: 1000, type: income, date: jan1), TransactionModel(amount: 500, type: expense, date: jan2), TransactionModel(amount: 2000, type: income, date: jan3), ];
final history = calculateNetWorthHistory(transactions, jan1, jan4);
expect(history[0].value, 1000); // After first transaction expect(history[1].value, 500); // After expense expect(history[2].value, 2500); // After second income});