A lightweight, customizable Flutter widget for visualizing sleep stage data — perfectly compatible with the official health plugin (Apple Health / Google Fit).
中文说明:见 README_ZH.md

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.
- 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
flutter pub add sleep_stages_chart
⚠️ Note: Thehealthplugin is not required by this package.
The following example only demonstrates how you could integrate HealthKit/Google Fit data into thesleep_stages_chartvisualization.
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,
);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',
);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),
);See /example/lib/main.dart for a runnable demo.
MIT © 2025 Your Name
This package is open source and free for personal and commercial use.