toBucketsFromSamples function

List<BucketWithSpikes> toBucketsFromSamples(
  1. List<HrSample> samples, {
  2. Duration interval = const Duration(minutes: 60),
  3. double iqrK = 1.5,
  4. bool includeSpikesInBars = false,
})

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