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