Powerful analytics for your ffastdb database — group, rank, pivot, and compute statistics directly inside your Dart or Flutter app, with no server, no network, and no SQL required.
| I want to… | Use |
|---|---|
| Sum / average / count records by category | groupBy |
| Find the top-N items by a value | topN |
| Rank items with tie support | rank |
| Compute a moving average over time | rollingAvg |
| Build a running total | cumulativeSum |
| Generate a spreadsheet-style pivot table | pivot |
| Calculate a percentile (e.g. p95 latency) | percentile |
| Measure spread with standard deviation | stddev |
| See how values are distributed | histogram |
| Narrow down results before analysing | where |
Add to your pubspec.yaml:
dependencies:
ffastdb_analytics: ^0.1.0Then run:
dart pub getimport 'package:ffastdb/ffastdb.dart';
import 'package:ffastdb_analytics/ffastdb_analytics.dart';
final db = await FfastDb.init(MemoryStorageStrategy());
// Insert some documents
await db.insertMany([
{'category': 'Food', 'amount': 120.0, 'status': 'active'},
{'category': 'Food', 'amount': 80.0, 'status': 'active'},
{'category': 'Tech', 'amount': 500.0, 'status': 'inactive'},
]);
// Analyse the whole collection
final summary = await db.analytics.all.groupBy('category', {
'revenue': aggSum('amount'),
'orders': aggCount(),
'avg_ticket': aggAvg('amount'),
});
// → [{key: Food, revenue: 200.0, orders: 2, avg_ticket: 100.0}, ...]
// Or filter first, then analyse
final activeOnly = await db.analytics
.where((q) => q.where('status').equals('active').findIds())
.groupBy('category', {'total': aggSum('amount')});Tip: Every analytical method works the same way whether you use
.allor.where(...)— just pick the scope you need.
Use groupBy to split documents into groups and compute values for each group. Pass any combination of aggregation expressions:
| Expression | Description |
|---|---|
aggSum('field') |
Total of a numeric field |
aggAvg('field') |
Average of a numeric field |
aggCount() |
Number of documents in the group |
aggMin('field') |
Smallest value |
aggMax('field') |
Largest value |
final result = await db.analytics.all.groupBy('department', {
'headcount': aggCount(),
'total_salary': aggSum('salary'),
'avg_salary': aggAvg('salary'),
'min_salary': aggMin('salary'),
'max_salary': aggMax('salary'),
});Get the highest (or lowest) N documents by any field:
// Top 5 products by revenue
final top5 = await db.analytics.all.topN('revenue', n: 5);
// Bottom 3 (ascending)
final bottom3 = await db.analytics.all.topN('revenue', n: 3, ascending: true);Assign a rank to every document. Ties share the same rank:
final ranked = await db.analytics.all.rank('score');
// scores [100, 100, 80] → ranks [1, 1, 3]
for (final r in ranked) {
print('#${r.rank} ${r.document['name']} score=${r.value}');
}Window functions compute a value for each document based on a sliding window over an ordered sequence — like Excel's moving-average formula or SQL's OVER (...).
// 7-day moving average of daily revenue
final rolling = await db.analytics.all
.rollingAvg('amount', window: 7, orderBy: 'date');
for (final p in rolling) {
print('day ${p.index}: value=${p.value} 7-day avg=${p.rollingValue}');
}// Year-to-date running total for income entries
final ytd = await db.analytics
.where((q) => q.where('type').equals('INCOME').findIds())
.cumulativeSum('amount', orderBy: 'date');
print('YTD total: \$${ytd.last.cumSum}');Turn rows into a spreadsheet-style grid — perfect for comparing values across two dimensions:
final table = await db.analytics.all.pivot(
rowField: 'department',
colField: 'quarter',
valueField: 'budget',
aggregation: PivotAgg.sum, // sum | avg | count | min | max
);
// Read a single cell
final q1Eng = table.rows['Engineering']?['Q1']; // num?
// Print the whole grid
print(['', ...table.columnKeys].join('\t'));
for (final entry in table.rows.entries) {
final cells = table.columnKeys
.map((c) => table.rows[entry.key]?[c]?.toString() ?? '-')
.join('\t');
print('${entry.key}\t$cells');
}Find the value below which a given percentage of data falls — great for SLA thresholds:
final p95latency = await db.analytics.all.percentile('latency_ms', 0.95);
print('95th percentile latency: ${p95latency}ms');Measure how spread out values are — useful for detecting outliers:
final sigma = await db.analytics.all.stddev('amount');Understand the shape of your data by splitting it into equal-width buckets:
final hist = await db.analytics.all.histogram('price', bins: 10);
for (final bin in hist) {
print('[${bin.low.toStringAsFixed(2)} – ${bin.high.toStringAsFixed(2)}): '
'${bin.count} items');
}// 1. Balance grouped by account type
final balance = await db.analytics.all.groupBy('type', {
'total': aggSum('amount'),
'entries': aggCount(),
'avg_amount': aggAvg('amount'),
});
// 2. Revenue vs expenses per quarter
final revenue = await db.analytics
.where((q) => q.where('type').equals('INCOME').findIds())
.groupBy('quarter', {'total': aggSum('amount')});
final expenses = await db.analytics
.where((q) => q.where('type').equals('EXPENSE').findIds())
.groupBy('quarter', {'total': aggSum('amount')});
// 3. Expense breakdown as a pivot (category × quarter)
final breakdown = await db.analytics
.where((q) => q.where('type').equals('EXPENSE').findIds())
.pivot(
rowField: 'category',
colField: 'quarter',
valueField: 'amount',
aggregation: PivotAgg.sum,
);
// 4. Flag unusually large transactions (above p90 + 1σ)
final p90 = await db.analytics.all.percentile('amount', 0.90);
final sigma = await db.analytics.all.stddev('amount');
final threshold = (p90 ?? 0) + (sigma ?? 0);Every analytics operation follows the same two-step pattern:
- Scope — decide which documents to include using
.allor.where(filter). - Analyse — call any analytical method on that scope.
db.analytics
.where(...) ← optional filter (any ffastdb query)
.groupBy(...) ← or any other analytical method
Documents are loaded lazily from storage on each call, so memory usage stays low even for large collections.