toBucketsFromSamples function
Implementation
List<BucketWithSpikes> toBucketsFromSamples(
List<HrSample> samples, {
Duration interval = const Duration(minutes: 60),
double iqrK = 1.5,
bool includeSpikesInBars = false,
}) {
if (samples.isEmpty) return [];
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final grouped = <DateTime, List<int>>{};
for (final s in samples) {
final diff = s.time.difference(startOfDay).inMinutes;
final idx = diff ~/ interval.inMinutes;
final bucketStart =
startOfDay.add(Duration(minutes: idx * interval.inMinutes));
grouped.putIfAbsent(bucketStart, () => []).add(s.bpm.round());
}
List<BucketWithSpikes> out = [];
for (final e in grouped.entries) {
final vals = List<int>.from(e.value)..sort();
if (vals.isEmpty) continue;
num median(List<int> xs) => xs.length.isOdd
? xs[xs.length ~/ 2]
: ((xs[xs.length ~/ 2 - 1] + xs[xs.length ~/ 2]) / 2);
List<int> lower(List<int> xs) => xs.sublist(0, xs.length ~/ 2);
List<int> upper(List<int> xs) => xs.length.isOdd
? xs.sublist(xs.length ~/ 2 + 1)
: xs.sublist(xs.length ~/ 2);
final q1 = median(lower(vals));
final q3 = median(upper(vals));
final iqr = (q3 - q1).abs();
final lowerFence = iqr > 0 ? (q1 - iqrK * iqr) : -1e9;
final upperFence = iqr > 0 ? (q3 + iqrK * iqr) : 1e9;
final normals =
vals.where((v) => v >= lowerFence && v <= upperFence).toList()..sort();
final useVals = includeSpikesInBars || normals.isEmpty ? vals : normals;
final spikes = samples
.where((s) {
final diff = s.time.difference(startOfDay).inMinutes;
final idx = diff ~/ interval.inMinutes;
final st =
startOfDay.add(Duration(minutes: idx * interval.inMinutes));
if (st != e.key) return false;
final v = s.bpm.toDouble();
return v < lowerFence || v > upperFence;
})
.map((s) => HeartSpike(s.time, s.bpm.round()))
.toList();
out.add(BucketWithSpikes(
HeartRateBucket(
time: e.key,
minBpm: useVals.first,
maxBpm: useVals.last,
medianBpm: median(vals).round(),
),
spikes,
));
}
out.sort((a, b) => a.bucket.time.compareTo(b.bucket.time));
return out;
}