Skip to content

Analytics & Reports

DWallet provides comprehensive analytics to help users understand their financial patterns through visual charts and detailed breakdowns.

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)

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

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
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;
}
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;
}
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,
);
}

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,
);
}
}
}
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),
)
ShadCard(
title: Text('Net Worth Trend'),
child: SizedBox(
height: 200,
child: NetWorthChart(dataPoints: netWorthData),
),
)
NetWorthChart(
dataPoints: data,
lineColor: Colors.green, // Custom color
fillGradient: LinearGradient(
colors: [Colors.green.withOpacity(0.3), Colors.transparent],
),
)

Add custom periods:

enum TimePeriod {
daily,
weekly,
monthly,
quarterly,
yearly,
custom,
}

Add export functionality:

Future<void> exportAnalytics() async {
final csvData = _convertToCSV(transactions);
await _saveToFile(csvData, 'analytics.csv');
}
  1. Cache calculations: Don’t recalculate on every build
  2. Lazy loading: Load data when tab is selected
  3. Smooth animations: Animate chart updates
  4. Empty states: Show message when no data
  5. Interactive charts: Allow tap for details
  6. Compare periods: Show previous period comparison
  7. Drill down: Tap category to see transactions
// Aggregate daily data instead of showing every transaction
List<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();
}
final _updateSubject = PublishSubject<void>();
void initState() {
super.initState();
_updateSubject
.debounceTime(Duration(milliseconds: 300))
.listen((_) => _recalculateAnalytics());
}
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
});