Skip to content

A lightweight, customizable Flutter widget for visualizing sleep stage data — **perfectly compatible with the official [`health`](https://pub.dev/packages/health) plugin** (Apple Health / Google Fit).

License

Notifications You must be signed in to change notification settings

manburenshenglu/sleep_stages_chart

Repository files navigation

sleep_stages_chart

A lightweight, customizable Flutter widget for visualizing sleep stage data — perfectly compatible with the official health plugin (Apple Health / Google Fit).

中文说明:见 README_ZH.md preview


🧠 Overview

sleep_stages_chart is a Flutter package designed purely for sleep visualization.
It does not depend on the health plugin, but can easily integrate with it (or any other data source) through simple data mapping.


✨ Features

  • Deep / Light / REM / Awake stages
  • Works perfectly with data from HealthKit / Google Fit
  • Fully customizable colors, tick lines, fonts, and radii
  • Supports custom labels and stage order
  • Utility functions for merging and clipping sleep data
  • Lightweight and platform-agnostic

📦 Installation

flutter pub add sleep_stages_chart

💤 Integration Example (with health plugin)

⚠️ Note: The health plugin is not required by this package.
The following example only demonstrates how you could integrate HealthKit/Google Fit data into the sleep_stages_chart visualization.

import 'package:collection/collection.dart';
import 'package:health/health.dart';
import 'package:sleep_stages_chart/sleep_stages_chart.dart';

class SleepRepository {
  final _health = Health();

  /// Request permission and fetch all sleep segments for a given day.
  /// Automatically handles cross-midnight sessions.
  Future<List<SleepSegment>> fetchDay(DateTime date) async {
    // Request permission: read sleep data only
    final types = <HealthDataType>[
      HealthDataType.SLEEP_ASLEEP,
      HealthDataType.SLEEP_DEEP,
      HealthDataType.SLEEP_REM,
      HealthDataType.SLEEP_AWAKE,
      HealthDataType.SLEEP_LIGHT, // iOS 17+ only
      // HealthDataType.SLEEP_ASLEEP_CORE, // Some devices/versions use "core" for light sleep
      HealthDataType.NUTRITION,
    ];

    // Query window: [00:00, next 00:00), extended ±6 hours to avoid truncation
    final dayStart = DateTime(date.year, date.month, date.day);
    final windowStart = dayStart.subtract(const Duration(hours: 6));
    final windowEnd = dayStart.add(const Duration(days: 1, hours: 6));

    final raw = await _health.getHealthDataFromTypes(
      startTime: windowStart,
      endTime: windowEnd,
      types: types,
    );

    // Map HealthKit / Fit data to unified SleepStage segments
    List<SleepSegment> segments = raw
        .map((d) {
          final stage = _mapToStage(d.type);
          if (stage == null) return null;
          return SleepSegment(start: d.dateFrom, end: d.dateTo, stage: stage);
        })
        .whereNotNull()
        .toList();

    // Clip to the day range, sort, and merge overlaps
    segments = _clipToDay(segments, dayStart, dayStart.add(const Duration(days: 1)));
    segments.sort((a, b) => a.start.compareTo(b.start));
    segments = _mergeOverlaps(segments);

    return segments;
  }

  /// Convert HealthKit/Google Fit types to unified SleepStage
  SleepStage? _mapToStage(HealthDataType type) {
    switch (type) {
      case HealthDataType.SLEEP_DEEP:
        return SleepStage.deep;
      case HealthDataType.SLEEP_REM:
        return SleepStage.rem;
      case HealthDataType.SLEEP_AWAKE:
        return SleepStage.awake;
      case HealthDataType.SLEEP_LIGHT:
      case HealthDataType.SLEEP_ASLEEP:
        return SleepStage.light;
      default:
        return null;
    }
  }

  /// Clip segments into [from, to)
  List<SleepSegment> _clipToDay(List<SleepSegment> segs, DateTime from, DateTime to) {
    final List<SleepSegment> out = [];
    for (final s in segs) {
      final start = s.start.isBefore(from) ? from : s.start;
      final end = s.end.isAfter(to) ? to : s.end;
      if (end.isAfter(start)) {
        out.add(SleepSegment(start: start, end: end, stage: s.stage));
      }
    }
    return out;
  }

  /// Merge overlaps: if different stages overlap, keep the one with longer duration.
  List<SleepSegment> _mergeOverlaps(List<SleepSegment> segs) {
    if (segs.isEmpty) return segs;
    final result = <SleepSegment>[];

    // Gather all boundaries
    final points = <DateTime>{};
    for (final s in segs) {
      points.add(s.start);
      points.add(s.end);
    }
    final borders = points.toList()..sort();

    for (var i = 0; i < borders.length - 1; i++) {
      final a = borders[i];
      final b = borders[i + 1];
      if (!b.isAfter(a)) continue;

      final candidates = segs.where((s) => s.start.isBefore(b) && s.end.isAfter(a)).toList();
      if (candidates.isEmpty) continue;

      candidates.sort((x, y) {
        final dx = x.end.difference(x.start).inMilliseconds;
        final dy = y.end.difference(y.start).inMilliseconds;
        return dy.compareTo(dx);
      });
      final top = candidates.first.stage;

      if (result.isNotEmpty &&
          result.last.stage == top &&
          result.last.end.isAtSameMomentAs(a)) {
        result[result.length - 1] =
            SleepSegment(start: result.last.start, end: b, stage: top);
      } else {
        result.add(SleepSegment(start: a, end: b, stage: top));
      }
    }
    return result;
  }
}

Then, simply visualize:

final segments = await SleepRepository().fetchDay(DateTime.now());

SleepStagesChart(
  day: DateTime.now(),
  segments: segments,
);

🎨 Customization — SleepStagesStyle

final style = SleepStagesStyle(
  stageColors: const {
    SleepStage.deep: Color(0xFF004D40),
    SleepStage.light: Color(0xFF26A69A),
    SleepStage.rem: Color(0xFF29B6F6),
    SleepStage.awake: Color(0xFFE57373),
  },
  barRadius: 10.0,
  barOpacity: 0.9,
  verticalTickCount: 6,
  legendSwatchSize: 14,
);

SleepStagesChart(
  day: DateTime.now(),
  segments: segments,
  style: style,
  timeLabelerAtTick: (t) => '${t.hour}:00',
);

⚙️ Data Utilities

Utilities from sleep_stages_utils.dart help preprocess raw sleep data:

final day = DateTime(2025, 1, 1);
final clipped = clipSegmentsToDay(rawSegments, day);
final merged = mergeOverlaps(clipped);
final cleaned = mergeAdjacentSameStage(
  merged,
  tolerance: const Duration(seconds: 30),
);

📘 Example App

See /example/lib/main.dart for a runnable demo.


🪪 License

MIT © 2025 Your Name
This package is open source and free for personal and commercial use.

About

A lightweight, customizable Flutter widget for visualizing sleep stage data — **perfectly compatible with the official [`health`](https://pub.dev/packages/health) plugin** (Apple Health / Google Fit).

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages