Commit 747d84d745315341439545e1da0b685ad30c9c0b

Authored by 刘宏哲
1 parent 243ed246

feat(app): bug fixed

... ... @@ -63,12 +63,12 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
weekStart: start,
weekEnd: start.add(const Duration(days: 6)),
days: _periodReports(start, 7, data),
totalActiveEnergyOverride: data.totalMove?.round(),
totalActiveEnergyOverride: data.totalMove?.toInt(),
totalExerciseMinutesOverride: _durationMinutes(data.totalExercise),
totalStandHoursOverride: _hours(data.totalStand),
averageDailyActiveEnergyOverride: data.avgMove?.round(),
activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(),
previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(),
averageDailyActiveEnergyOverride: data.avgMove?.toInt(),
activeEnergyGoalOverride: data.activityTargetInfo?.move?.toInt(),
previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.toInt(),
),
AppFailure() => WeeklyActivityBurnReport.empty(start),
};
... ... @@ -91,12 +91,12 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
monthStart: start,
monthEnd: end,
days: _periodReports(start, end.day, data),
totalActiveEnergyOverride: data.totalMove?.round(),
totalActiveEnergyOverride: data.totalMove?.toInt(),
totalExerciseMinutesOverride: _durationMinutes(data.totalExercise),
totalStandHoursOverride: _hours(data.totalStand),
averageDailyActiveEnergyOverride: data.avgMove?.round(),
activeEnergyGoalOverride: data.activityTargetInfo?.move?.round(),
previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.round(),
averageDailyActiveEnergyOverride: data.avgMove?.toInt(),
activeEnergyGoalOverride: data.activityTargetInfo?.move?.toInt(),
previousAverageDailyActiveEnergyOverride: data.qoqAvgMove?.toInt(),
),
AppFailure() => MonthlyActivityBurnReport.empty(start),
};
... ... @@ -247,7 +247,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
ActivityBurnMetric? _metric(num? value, num? goal) {
if (value == null) return null;
return ActivityBurnMetric(value: value.round(), goal: goal?.round() ?? 0);
return ActivityBurnMetric(value: value.toInt(), goal: goal?.toInt() ?? 0);
}
ActivityBurnMetric? _exerciseDurationMetric(num? seconds, num? target) {
... ... @@ -269,7 +269,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
int? _durationMinutes(num? seconds) =>
seconds == null ? null : _secondsToWholeMinutes(seconds);
int? _hours(num? hours) => hours?.round();
int? _hours(num? hours) => hours?.toInt();
int _secondsToWholeMinutes(num? seconds) {
if (seconds == null || seconds < 60) return 0;
... ... @@ -278,7 +278,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
int _exerciseTargetMinutes(num? target) {
if (target == null || target <= 0) return 0;
return target >= 60 ? _secondsToWholeMinutes(target) : target.round();
return target >= 60 ? _secondsToWholeMinutes(target) : target.toInt();
}
DateTime? _parseDateTime(Object? value, {DateTime? fallbackDate}) {
... ... @@ -289,7 +289,7 @@ class ApiActivityBurnReportDataSource implements ActivityBurnReportDataSource {
return DateTime.fromMillisecondsSinceEpoch(milliseconds);
}
if (fallbackDate != null && value >= 0 && value < 86400) {
return fallbackDate.add(Duration(seconds: value.round()));
return fallbackDate.add(Duration(seconds: value.toInt()));
}
}
final raw = value?.toString();
... ...
... ... @@ -126,7 +126,7 @@ class WeeklyActivityBurnReport {
int get averageDailyActiveEnergy {
if (averageDailyActiveEnergyOverride case final value?) return value;
if (daysWithData.isEmpty) return 0;
return (totalActiveEnergy / daysWithData.length).round();
return (totalActiveEnergy / daysWithData.length).toInt();
}
int get activeEnergyGoal =>
... ... @@ -141,7 +141,7 @@ class WeeklyActivityBurnReport {
int? get activeEnergyComparisonPercent {
final previous = previousAverageDailyActiveEnergy;
if (previous == null || previous <= 0) return null;
return (((averageDailyActiveEnergy - previous) / previous) * 100).round();
return (((averageDailyActiveEnergy - previous) / previous) * 100).toInt();
}
int get perfectRingDays => daysWithData
... ... @@ -232,7 +232,7 @@ class MonthlyActivityBurnReport {
int get averageDailyActiveEnergy {
if (averageDailyActiveEnergyOverride case final value?) return value;
if (daysWithData.isEmpty) return 0;
return (totalActiveEnergy / daysWithData.length).round();
return (totalActiveEnergy / daysWithData.length).toInt();
}
int get activeEnergyGoal =>
... ... @@ -247,7 +247,7 @@ class MonthlyActivityBurnReport {
int? get activeEnergyComparisonPercent {
final previous = previousAverageDailyActiveEnergy;
if (previous == null || previous <= 0) return null;
return (((averageDailyActiveEnergy - previous) / previous) * 100).round();
return (((averageDailyActiveEnergy - previous) / previous) * 100).toInt();
}
int get perfectRingDays => daysWithData
... ...
... ... @@ -400,13 +400,13 @@ class ActivityBurnHeartRateZoneCard extends StatelessWidget {
}
String? _bottomLabel(double value, DateTime start, double maxX) {
final rounded = value.round();
if (rounded % 360 != 0 || rounded < 0 || rounded > maxX.round()) {
final minutes = value.toInt();
if (minutes % 360 != 0 || minutes < 0 || minutes > maxX.toInt()) {
return null;
}
if (rounded == 1440) return '24:00';
if (minutes == 1440) return '24:00';
final time = start.add(Duration(minutes: rounded));
final time = start.add(Duration(minutes: minutes));
return DateFormat('HH:mm').format(time);
}
}
... ... @@ -484,7 +484,7 @@ class _HeartRateAxis {
}
String formatYLabel(double value) {
if (value == value.roundToDouble()) return value.round().toString();
if (value == value.truncateToDouble()) return value.toInt().toString();
return value.toStringAsFixed(1);
}
... ...
... ... @@ -683,7 +683,7 @@ class _MonthEnergyTrendCardState extends State<_MonthEnergyTrendCard> {
reservedSize: 36,
interval: 100,
getTitlesWidget: (value, meta) {
final isHundredTick = value.round() % 100 == 0;
final isHundredTick = value.toInt() % 100 == 0;
final isMax = (value - maxY).abs() < 0.001;
if (!isHundredTick && !isMax) return const SizedBox.shrink();
return Transform.translate(
... ...
... ... @@ -139,10 +139,12 @@ class _ActivityBurnRingPainter extends CustomPainter {
static const _track = Color(0xFF35101D);
static const _innerTrack = Color(0xFF13251F);
static const _center = Color(0xFF0F0F11);
static const _outerRadius = 88.0;
static const _middleRadius = 60.0;
static const _innerRadius = 32.0;
static const _ringWidth = 24.0;
static const _ringWidth = 20.0;
static const _ringGap = 4.0;
static const _outerEdgeRadius = 100.0;
static const _outerRadius = _outerEdgeRadius - _ringWidth / 2;
static const _middleRadius = _outerRadius - _ringWidth - _ringGap;
static const _innerRadius = _middleRadius - _ringWidth - _ringGap;
@override
void paint(Canvas canvas, Size size) {
... ...
... ... @@ -661,7 +661,7 @@ class _EnergyTrendCardState extends State<_EnergyTrendCard> {
reservedSize: 36,
interval: 100,
getTitlesWidget: (value, meta) {
final isHundredTick = value.round() % 100 == 0;
final isHundredTick = value.toInt() % 100 == 0;
final isMax = (value - maxY).abs() < 0.001;
if (!isHundredTick && !isMax) return const SizedBox.shrink();
return Transform.translate(
... ...
import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_models.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'
... ... @@ -7,6 +6,7 @@ import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:intl/intl.dart';
import '../models/friend_health_data.dart';
import '../models/friend_stress_state.dart';
abstract class FriendsRepository {
Future<List<FriendHealthData>> getFriends({bool withHealthData = true});
... ... @@ -64,7 +64,6 @@ class FriendsRepositoryImpl implements FriendsRepository {
FriendHealthData _mapFriend(api_models.FriendItem friend) {
final healthData = friend.healthData;
final stressValue = healthData?.latestHrv;
final nickname = friend.friendNickname?.trim();
final remark = friend.remarkName?.trim();
... ... @@ -81,10 +80,7 @@ class FriendsRepositoryImpl implements FriendsRepository {
steps: healthData?.totalSteps == null
? null
: l10n.friendsStepCount(healthData!.totalSteps!),
statusText: stressValue == null
? l10n.friendsWaitingForData
: HrvStressLevel.fromRealtimeStress(stressValue).label,
stressValue: stressValue,
stressState: FriendStressState.fromValue(healthData?.hrvState),
isOnWatchFace: friend.isShowInDial,
);
}
... ...
... ... @@ -2,6 +2,8 @@ import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'
as api_models;
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'friend_stress_state.dart';
class FriendHealthData {
const FriendHealthData({
required this.friendItem,
... ... @@ -12,8 +14,7 @@ class FriendHealthData {
required this.updatedAt,
required this.sleepQualityScore,
required this.steps,
required this.statusText,
required this.stressValue,
required this.stressState,
this.isOnWatchFace = false,
});
... ... @@ -25,8 +26,7 @@ class FriendHealthData {
final String updatedAt;
final int? sleepQualityScore;
final String? steps;
final String statusText;
final double? stressValue;
final FriendStressState stressState;
final bool isOnWatchFace;
String get displayName {
... ... @@ -44,8 +44,7 @@ class FriendHealthData {
String? updatedAt,
int? sleepQualityScore,
String? steps,
String? statusText,
double? stressValue,
FriendStressState? stressState,
bool? isOnWatchFace,
}) {
return FriendHealthData(
... ... @@ -57,8 +56,7 @@ class FriendHealthData {
updatedAt: updatedAt ?? this.updatedAt,
sleepQualityScore: sleepQualityScore ?? this.sleepQualityScore,
steps: steps ?? this.steps,
statusText: statusText ?? this.statusText,
stressValue: stressValue ?? this.stressValue,
stressState: stressState ?? this.stressState,
isOnWatchFace: isOnWatchFace ?? this.isOnWatchFace,
);
}
... ...
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/material.dart';
enum FriendStressState {
wait(0, '等待数据'),
stressful(1, '压力过载'),
slightStress(2, '注意压力'),
normal(3, '状态正常'),
energetic(4, '状态优秀');
const FriendStressState(this.value, this.label);
final int value;
final String label;
static FriendStressState fromValue(int? value) {
return switch (value) {
1 => FriendStressState.stressful,
2 => FriendStressState.slightStress,
3 => FriendStressState.normal,
4 => FriendStressState.energetic,
_ => FriendStressState.wait,
};
}
bool get hasData => this != FriendStressState.wait;
int? get segmentIndex {
return switch (this) {
FriendStressState.stressful => 0,
FriendStressState.slightStress => 1,
FriendStressState.normal => 2,
FriendStressState.energetic => 3,
FriendStressState.wait => null,
};
}
Color get color {
return switch (this) {
FriendStressState.stressful => const Color(0xFFFF5279),
FriendStressState.slightStress => const Color(0xFFFF9A6E),
FriendStressState.normal => const Color(0xFF7B9BFB),
FriendStressState.energetic => const Color(0xFF3BD49D),
FriendStressState.wait => const Color(0xFFF3F3F3),
};
}
String get iconPath {
return switch (this) {
FriendStressState.stressful => R.assetsImagesRealtimeStressOverloadIcon,
FriendStressState.slightStress =>
R.assetsImagesRealtimeStressAttentionIcon,
FriendStressState.normal => R.assetsImagesRealtimeStressNormalIcon,
FriendStressState.energetic => R.assetsImagesRealtimeStressExcellentIcon,
FriendStressState.wait => R.assetsImagesRealtimeStressNoDataIcon,
};
}
}
... ...
import 'package:doublefeel_flutter/app/modules/home/widgets/df_tab_bar.dart';
import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_models.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
... ... @@ -12,12 +11,13 @@ import 'package:intl/intl.dart';
import '../controllers/friends_controller.dart';
import '../models/friend_health_data.dart';
import '../models/friend_stress_state.dart';
import '../widgets/friend_health_card.dart';
class FriendsTab extends GetView<FriendsController> {
const FriendsTab({super.key});
static const double _floatingAddButtonGapAboveTabBar = 23;
static const double _floatingAddButtonBottomGap = 24;
@override
Widget build(BuildContext context) {
... ... @@ -49,9 +49,9 @@ class FriendsTab extends GetView<FriendsController> {
stressScore == null;
final isFriendsInitialLoading =
controller.isLoading.value && !hasFriends;
final stressValue = stressScore?.state == 0
? null
: stressScore?.comprehensiveScore?.toDouble();
final selfStressState = FriendStressState.fromValue(
stressScore?.comprehensiveScore,
);
final selfHealthCard = _SelfHealthCard(
name: currentUser?.nickname?.trim().isNotEmpty == true
? currentUser!.nickname!.trim()
... ... @@ -69,12 +69,7 @@ class FriendsTab extends GetView<FriendsController> {
: context.l10n.friendsStepCount(
healthData!.steps!,
),
statusText: stressValue == null
? context.l10n.friendsWaitingForData
: HrvStressLevel.fromRealtimeStress(
stressValue,
).label,
stressValue: stressValue,
stressState: selfStressState,
isLoading: isSelfInitialLoading,
);
... ... @@ -185,8 +180,7 @@ class FriendsTab extends GetView<FriendsController> {
updatedAt: friend.updatedAt,
sleepQualityScore: friend.sleepQualityScore,
steps: friend.steps,
statusText: friend.statusText,
stressValue: friend.stressValue,
stressState: friend.stressState,
isOnWatchFace: friend.isOnWatchFace,
onTap: () => _openFriendHome(friend),
onMoreSelected: (action) {
... ... @@ -225,7 +219,7 @@ class FriendsTab extends GetView<FriendsController> {
}
double _floatingAddButtonBottom(BuildContext context) {
return _tabBarAvoidanceBottom(context) + _floatingAddButtonGapAboveTabBar;
return MediaQuery.paddingOf(context).bottom + _floatingAddButtonBottomGap;
}
double _tabBarAvoidanceBottom(BuildContext context) {
... ... @@ -286,8 +280,7 @@ class _SelfHealthCard extends StatelessWidget {
required this.updatedAt,
required this.sleepQualityScore,
required this.steps,
required this.statusText,
required this.stressValue,
required this.stressState,
required this.isLoading,
});
... ... @@ -296,8 +289,7 @@ class _SelfHealthCard extends StatelessWidget {
final String updatedAt;
final int? sleepQualityScore;
final String? steps;
final String statusText;
final double? stressValue;
final FriendStressState stressState;
final bool isLoading;
@override
... ... @@ -311,8 +303,7 @@ class _SelfHealthCard extends StatelessWidget {
updatedAt: updatedAt,
sleepQualityScore: sleepQualityScore,
steps: steps,
statusText: statusText,
stressValue: stressValue,
stressState: stressState,
isSelf: true,
borderRadius: borderRadius,
contentPadding: const EdgeInsets.fromLTRB(36, 20, 35, 20),
... ...
... ... @@ -67,8 +67,7 @@ class _SelectFriendViewState extends State<SelectFriendView> {
updatedAt: friend.updatedAt,
sleepQualityScore: friend.sleepQualityScore,
steps: friend.steps,
statusText: friend.statusText,
stressValue: friend.stressValue,
stressState: friend.stressState,
isOnWatchFace: friend.isOnWatchFace,
isSelected: isSelected,
showCheckbox: true,
... ...
import 'dart:math' as math;
import 'package:doublefeel_flutter/app/modules/hrv_report/models/hrv_report_models.dart';
import 'package:doublefeel_flutter/app/modules/friends/models/friend_stress_state.dart';
import 'package:doublefeel_flutter/app/modules/sleep_report/models/sleep_report_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
... ... @@ -21,8 +21,7 @@ class FriendHealthCard extends StatelessWidget {
required this.updatedAt,
required this.sleepQualityScore,
required this.steps,
required this.statusText,
required this.stressValue,
required this.stressState,
this.isSelf = false,
this.isOnWatchFace = false,
this.isSelected = false,
... ... @@ -39,8 +38,7 @@ class FriendHealthCard extends StatelessWidget {
final String updatedAt;
final int? sleepQualityScore;
final String? steps;
final String statusText;
final double? stressValue;
final FriendStressState stressState;
final bool isSelf;
final bool isOnWatchFace;
final bool isSelected;
... ... @@ -92,9 +90,7 @@ class FriendHealthCard extends StatelessWidget {
children: [
Expanded(
child: _StatusFigure(
statusText: statusText,
stressValue: stressValue,
waiting: stressValue == null,
stressState: stressState,
),
),
const SizedBox(width: 18),
... ... @@ -138,7 +134,8 @@ class FriendHealthCard extends StatelessWidget {
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
transform: Matrix4.identity()..scale(isSelected ? 1.0 : 0.92),
transform: Matrix4.identity()
..scaleByDouble(isSelected ? 1.0 : 0.92, 1, 1, 1),
child: _FriendSelectMark(isSelected: isSelected),
),
),
... ... @@ -333,21 +330,13 @@ class _AvatarFallback extends StatelessWidget {
class _StatusFigure extends StatelessWidget {
const _StatusFigure({
required this.statusText,
required this.stressValue,
required this.waiting,
required this.stressState,
});
final String statusText;
final double? stressValue;
final bool waiting;
final FriendStressState stressState;
@override
Widget build(BuildContext context) {
final stressLevel = stressValue == null
? null
: HrvStressLevel.fromRealtimeStress(stressValue!);
return SizedBox(
height: 144,
child: Column(
... ... @@ -363,16 +352,14 @@ class _StatusFigure extends StatelessWidget {
CustomPaint(
size: const Size(124, 121),
painter: _StressStatusRingPainter(
stressValue: stressValue,
muted: waiting,
stressState: stressState,
),
),
Positioned(
left: 22,
top: 24,
child: Image.asset(
stressLevel?.realtimeStressIconPath ??
R.assetsImagesRealtimeStressNoDataIcon,
stressState.iconPath,
width: 72,
height: 72,
fit: BoxFit.contain,
... ... @@ -383,7 +370,7 @@ class _StatusFigure extends StatelessWidget {
),
const SizedBox(height: 3),
Text(
statusText,
stressState.label,
style: const TextStyle(
color: Color(0xFF0F0F11),
fontSize: 14,
... ... @@ -399,18 +386,16 @@ class _StatusFigure extends StatelessWidget {
class _StressStatusRingPainter extends CustomPainter {
const _StressStatusRingPainter({
required this.stressValue,
required this.muted,
required this.stressState,
});
final double? stressValue;
final bool muted;
final FriendStressState stressState;
static const _segments = [
_StressStatusSegment(80, 100, Color(0xFFFF5279)),
_StressStatusSegment(50, 80, Color(0xFFFF9A6E)),
_StressStatusSegment(20, 50, Color(0xFF7B9BFB)),
_StressStatusSegment(0, 20, Color(0xFF3BD49D)),
static const _segmentColors = [
Color(0xFFFF5279),
Color(0xFFFF9A6E),
Color(0xFF7B9BFB),
Color(0xFF3BD49D),
];
static const _arcSpecs = [
... ... @@ -419,6 +404,7 @@ class _StressStatusRingPainter extends CustomPainter {
_ArcSpec(270, 80),
_ArcSpec(-10, 55),
];
static const _arcGapDegrees = 5.0;
@override
void paint(Canvas canvas, Size size) {
... ... @@ -436,7 +422,7 @@ class _StressStatusRingPainter extends CustomPainter {
..strokeWidth = 12
..strokeCap = StrokeCap.round;
if (muted) {
if (!stressState.hasData) {
const mutedSpec = _ArcSpec(135, 270);
final startAngle = _degreesToRadians(mutedSpec.startDegrees);
final sweepAngle = _degreesToRadians(mutedSpec.sweepDegrees);
... ... @@ -453,8 +439,9 @@ class _StressStatusRingPainter extends CustomPainter {
const paintOrder = [3, 2, 1, 0];
for (final i in paintOrder) {
final startAngle = _degreesToRadians(_arcSpecs[i].startDegrees);
final sweepAngle = _degreesToRadians(_arcSpecs[i].sweepDegrees);
final visualSpec = _arcSpecs[i].withGap(_arcGapDegrees);
final startAngle = _degreesToRadians(visualSpec.startDegrees);
final sweepAngle = _degreesToRadians(visualSpec.sweepDegrees);
canvas.drawArc(
rect,
startAngle,
... ... @@ -463,7 +450,7 @@ class _StressStatusRingPainter extends CustomPainter {
borderPaint,
);
segmentPaint.color = _segments[i].color.withValues(alpha: 0.42);
segmentPaint.color = _segmentColors[i].withValues(alpha: 0.42);
canvas.drawArc(
rect,
startAngle,
... ... @@ -473,21 +460,15 @@ class _StressStatusRingPainter extends CustomPainter {
);
}
final value = stressValue;
if (value == null) return;
final segmentIndex =
_segments.indexWhere((segment) => segment.contains(value));
final safeSegmentIndex = segmentIndex < 0 ? 2 : segmentIndex;
final segment = _segments[safeSegmentIndex];
final spec = _arcSpecs[safeSegmentIndex];
final progress = segment.progressOf(value);
final segmentIndex = stressState.segmentIndex;
if (segmentIndex == null) return;
final spec = _arcSpecs[segmentIndex].withGap(_arcGapDegrees);
final indicatorAngle =
_degreesToRadians(spec.startDegrees + spec.sweepDegrees * progress);
_degreesToRadians(spec.startDegrees + spec.sweepDegrees / 2);
final indicatorPoint = _pointOnOval(rect, indicatorAngle);
final indicatorPaint = Paint()
..color = segment.color
..color = _segmentColors[segmentIndex]
..style = PaintingStyle.fill;
canvas.drawCircle(indicatorPoint, 10, indicatorPaint);
... ... @@ -508,24 +489,7 @@ class _StressStatusRingPainter extends CustomPainter {
@override
bool shouldRepaint(covariant _StressStatusRingPainter oldDelegate) {
return oldDelegate.stressValue != stressValue || oldDelegate.muted != muted;
}
}
class _StressStatusSegment {
const _StressStatusSegment(this.min, this.max, this.color);
final double min;
final double max;
final Color color;
bool contains(double value) {
final lowerMatched = min == 0 ? value >= min : value > min;
return lowerMatched && value <= max;
}
double progressOf(double value) {
return ((value.clamp(min, max) - min) / (max - min)).toDouble();
return oldDelegate.stressState != stressState;
}
}
... ... @@ -534,6 +498,14 @@ class _ArcSpec {
final double startDegrees;
final double sweepDegrees;
_ArcSpec withGap(double gapDegrees) {
final clampedGap = gapDegrees.clamp(0, sweepDegrees / 2).toDouble();
return _ArcSpec(
startDegrees + clampedGap,
sweepDegrees - clampedGap * 2,
);
}
}
class _MetricTile extends StatelessWidget {
... ...
... ... @@ -6,28 +6,6 @@ enum HrvStressLevel {
normal,
attention,
overload;
static HrvStressLevel fromHrvState(int hrvState) {
if (hrvState == 1) return HrvStressLevel.overload;
if (hrvState == 2) return HrvStressLevel.attention;
if (hrvState == 3) return HrvStressLevel.normal;
return HrvStressLevel.excellent;
}
static HrvStressLevel fromRealtimeStress(double value) {
final normalizedValue = value.clamp(0, 100);
if (normalizedValue > 80) return HrvStressLevel.overload;
if (normalizedValue > 50) return HrvStressLevel.attention;
if (normalizedValue > 20) return HrvStressLevel.normal;
return HrvStressLevel.excellent;
}
static HrvStressLevel fromAverageHrv(double value) {
if (value >= 75) return HrvStressLevel.excellent;
if (value >= 55) return HrvStressLevel.normal;
if (value >= 40) return HrvStressLevel.attention;
return HrvStressLevel.overload;
}
}
extension HrvStressLevelPresentation on HrvStressLevel {
... ... @@ -345,17 +323,15 @@ class YearlyHrvReport implements HrvPeriodReport {
return levels.first;
}
List<int> get monthsWithAverage => [
List<int> get monthsWithLevel => [
for (var month = 1; month <= 12; month++)
if (averageForMonth(month) != null) month,
if (levelForMonth(month) != null) month,
];
({List<int> mostStressed, List<int> leastStressed})
averageStressExtremeMonths() {
({List<int> mostStressed, List<int> leastStressed}) stressExtremeMonths() {
final values = [
for (final month in monthsWithAverage)
if (averageForMonth(month) case final average?)
MapEntry(month, average),
for (final month in monthsWithLevel)
if (levelForMonth(month) case final level?) MapEntry(month, level),
];
values.sort(_compareByStressDescending);
... ... @@ -367,7 +343,7 @@ class YearlyHrvReport implements HrvPeriodReport {
if (values.length == 1) {
final entry = values.single;
if (_isStressedAverage(entry.value)) {
if (_isStressedLevel(entry.value)) {
mostStressed.add(entry.key);
} else {
leastStressed.add(entry.key);
... ... @@ -380,7 +356,7 @@ class YearlyHrvReport implements HrvPeriodReport {
if (values.length == 3) {
final middle = values[1];
if (_isStressedAverage(middle.value)) {
if (_isStressedLevel(middle.value)) {
mostStressed.add(middle.key);
} else {
leastStressed.add(middle.key);
... ... @@ -396,19 +372,29 @@ class YearlyHrvReport implements HrvPeriodReport {
}
int _compareByStressDescending(
MapEntry<int, double> a,
MapEntry<int, double> b,
MapEntry<int, HrvStressLevel> a,
MapEntry<int, HrvStressLevel> b,
) {
final valueComparison = a.value.compareTo(b.value);
final valueComparison = _stressRank(b.value).compareTo(
_stressRank(a.value),
);
return valueComparison == 0 ? a.key.compareTo(b.key) : valueComparison;
}
bool _isStressedAverage(double average) {
final level = HrvStressLevel.fromAverageHrv(average);
bool _isStressedLevel(HrvStressLevel level) {
return level == HrvStressLevel.attention ||
level == HrvStressLevel.overload;
}
int _stressRank(HrvStressLevel level) {
return switch (level) {
HrvStressLevel.overload => 3,
HrvStressLevel.attention => 2,
HrvStressLevel.normal => 1,
HrvStressLevel.excellent => 0,
};
}
List<int> monthsAtExtreme({required bool maximum}) {
final values = [
for (var month = 1; month <= 12; month++)
... ...
... ... @@ -34,7 +34,7 @@ class _YearTrendCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final extremeMonths = report.averageStressExtremeMonths();
final extremeMonths = report.stressExtremeMonths();
return _Card(
padding: const EdgeInsets.fromLTRB(20, 17, 20, 18),
child: Column(
... ... @@ -269,12 +269,7 @@ class _YearBarChartState extends State<_YearBarChart> {
Color _colorForMonth(int month) {
final level = widget.report.levelForMonth(month);
if (level != null) return Color(level.colorValue);
return _colorForValue(widget.report.averageForMonth(month));
}
Color _colorForValue(double? value) {
if (value == null) return Colors.transparent;
return Color(HrvStressLevel.fromAverageHrv(value).colorValue);
return Colors.transparent;
}
double? _lineXForMonth(double width) {
... ...
/// Global selectable range for report dates.
abstract final class ReportDateRangeConfig {
static const int yearsBeforeCurrent = 2;
static const int yearsAfterCurrent = 1;
static int firstYear([DateTime? now]) =>
(now ?? DateTime.now()).year - yearsBeforeCurrent;
static int lastYear([DateTime? now]) =>
(now ?? DateTime.now()).year + yearsAfterCurrent;
static int lastYear([DateTime? now]) => (now ?? DateTime.now()).year;
static DateTime firstDate([DateTime? now]) => DateTime(firstYear(now));
static DateTime lastDate([DateTime? now]) =>
DateTime(lastYear(now), DateTime.december, 31);
static DateTime lastDate([DateTime? now]) {
final date = now ?? DateTime.now();
return DateTime(date.year, date.month, date.day);
}
static DateTime firstMonth([DateTime? now]) => DateTime(firstYear(now));
static DateTime lastMonth([DateTime? now]) =>
DateTime(lastYear(now), DateTime.december);
static DateTime lastMonth([DateTime? now]) {
final date = now ?? DateTime.now();
return DateTime(date.year, date.month);
}
static DateTime firstWeekStart([DateTime? now]) {
final first = firstDate(now);
... ...
... ... @@ -181,8 +181,8 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
date: date,
duration: durationSeconds == null
? report.duration
: SleepDuration(minutes: (durationSeconds / 60).round()),
qualityScore: score?.round() ?? report.qualityScore,
: SleepDuration(minutes: durationSeconds ~/ 60),
qualityScore: score?.toInt() ?? report.qualityScore,
heartRate: report.heartRate,
);
}
... ... @@ -276,9 +276,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
dailyTrend,
asleepTime,
points,
averageBpm: data.avgHr?.round(),
maxBpm: data.maxHr?.round(),
minBpm: data.minHr?.round(),
averageBpm: data.avgHr?.toInt(),
maxBpm: data.maxHr?.toInt(),
minBpm: data.minHr?.toInt(),
);
}
... ... @@ -317,9 +317,9 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
trends[date],
asleepTimes[date],
_sleepPoints(date, asleepTimes[date], pointsByDate),
averageBpm: isSingleDayResponse ? data.avgHr?.round() : null,
maxBpm: isSingleDayResponse ? data.maxHr?.round() : null,
minBpm: isSingleDayResponse ? data.minHr?.round() : null,
averageBpm: isSingleDayResponse ? data.avgHr?.toInt() : null,
maxBpm: isSingleDayResponse ? data.maxHr?.toInt() : null,
minBpm: isSingleDayResponse ? data.minHr?.toInt() : null,
),
};
}
... ... @@ -351,13 +351,13 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
final durationSeconds = trend?.totalTime;
final sleepEnd = asleepTime == null || durationSeconds == null
? null
: asleepTime.add(Duration(seconds: durationSeconds.round()));
: asleepTime.add(Duration(seconds: durationSeconds.toInt()));
return SleepReport(
date: date,
duration: durationSeconds == null
? null
: SleepDuration(minutes: (durationSeconds / 60).round()),
qualityScore: trend?.score?.round(),
: SleepDuration(minutes: durationSeconds ~/ 60),
qualityScore: trend?.score?.toInt(),
heartRate: _heartRateSummary(
points,
asleepTime,
... ... @@ -395,10 +395,10 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
if (point.bpm < minPoint.bpm) minPoint = point;
}
return SleepHeartRateSummary(
averageBpm: averageBpm ?? (total / points.length).round(),
maxBpm: maxBpm ?? maxPoint.bpm.round(),
averageBpm: averageBpm ?? (total / points.length).toInt(),
maxBpm: maxBpm ?? maxPoint.bpm.toInt(),
maxAt: maxPoint.time,
minBpm: minBpm ?? minPoint.bpm.round(),
minBpm: minBpm ?? minPoint.bpm.toInt(),
minAt: minPoint.time,
sleepStart: sleepStart ?? points.first.time,
sleepEnd: sleepEnd ?? points.last.time,
... ... @@ -419,7 +419,7 @@ class ApiSleepReportDataSource implements SleepReportDataSource {
return DateTime.fromMillisecondsSinceEpoch(milliseconds);
}
if (fallbackDate != null && value >= 0 && value < 86400) {
return fallbackDate.add(Duration(seconds: value.round()));
return fallbackDate.add(Duration(seconds: value.toInt()));
}
}
final raw = value?.toString();
... ... @@ -658,10 +658,10 @@ SleepHeartRateSummary _buildHeartRateSummary({
}
return SleepHeartRateSummary(
averageBpm: average.round(),
maxBpm: maxPoint.bpm.round(),
averageBpm: average.toInt(),
maxBpm: maxPoint.bpm.toInt(),
maxAt: maxPoint.time,
minBpm: minPoint.bpm.round(),
minBpm: minPoint.bpm.toInt(),
minAt: minPoint.time,
sleepStart: sleepStart,
sleepEnd: sleepEnd,
... ...
... ... @@ -166,11 +166,17 @@ class SleepReport {
this.heartRate = const SleepHeartRateSummary(),
});
bool get hasData =>
duration != null || qualityScore != null || heartRate.hasData;
bool get hasData => hasSleepData;
bool get hasSleepData => (duration?.minutes ?? 0) > 0;
int? get validQualityScore => hasSleepData ? qualityScore : null;
SleepHeartRateSummary get validHeartRate =>
hasSleepData ? heartRate : const SleepHeartRateSummary();
SleepQualityLevel get qualityLevel =>
sleepQualityLevelFromScore(qualityScore);
sleepQualityLevelFromScore(validQualityScore);
factory SleepReport.empty(DateTime date) {
return SleepReport(date: date);
... ... @@ -232,14 +238,12 @@ class WeeklySleepReport {
days.where((report) => report.hasData).toList();
bool get hasData =>
daysWithData.isNotEmpty ||
averageDurationSeconds != null ||
averageScore != null;
daysWithData.isNotEmpty || _hasPositiveSeconds(averageDurationSeconds);
int? get averageDurationMinutes {
final averageSeconds = _computedAverageDurationSeconds;
if (averageSeconds == null) return null;
return (averageSeconds / 60).round();
return averageSeconds ~/ 60;
}
num? get _computedAverageDurationSeconds {
... ... @@ -249,22 +253,26 @@ class WeeklySleepReport {
.where((minutes) => minutes > 0)
.toList();
if (values.isEmpty) {
return averageDurationSeconds;
return _hasPositiveSeconds(averageDurationSeconds)
? averageDurationSeconds
: null;
}
return values.reduce((sum, minutes) => sum + minutes) * 60 / values.length;
}
int? get averageQualityScore {
final average = _computedAverageScore;
return average?.round();
return average?.toInt();
}
num? get _computedAverageScore {
final values = daysWithData
.map((report) => report.qualityScore)
.map((report) => report.validQualityScore)
.whereType<int>()
.toList();
if (values.isEmpty) return averageScore;
if (values.isEmpty) {
return _hasPositiveSeconds(averageDurationSeconds) ? averageScore : null;
}
return values.reduce((sum, score) => sum + score) / values.length;
}
... ... @@ -281,7 +289,7 @@ class WeeklySleepReport {
SleepHeartRateSummary get heartRate {
final dailySummaries = daysWithData
.map((report) => report.heartRate)
.map((report) => report.validHeartRate)
.where((summary) => summary.hasData)
.toList();
if (dailySummaries.isEmpty) return const SleepHeartRateSummary();
... ... @@ -294,7 +302,7 @@ class WeeklySleepReport {
final average = averageValues.isEmpty
? null
: (averageValues.reduce((sum, bpm) => sum + bpm) / averageValues.length)
.round();
.toInt();
SleepHeartRatePoint? maxPoint;
SleepHeartRatePoint? minPoint;
... ... @@ -305,9 +313,9 @@ class WeeklySleepReport {
return SleepHeartRateSummary(
averageBpm: average,
maxBpm: maxPoint?.bpm.round(),
maxBpm: maxPoint?.bpm.toInt(),
maxAt: maxPoint?.time,
minBpm: minPoint?.bpm.round(),
minBpm: minPoint?.bpm.toInt(),
minAt: minPoint?.time,
sleepStart: points.isEmpty ? null : points.first.time,
sleepEnd: points.isEmpty ? null : points.last.time,
... ... @@ -360,14 +368,12 @@ class MonthlySleepReport {
days.where((report) => report.hasData).toList();
bool get hasData =>
daysWithData.isNotEmpty ||
averageDurationSeconds != null ||
averageScore != null;
daysWithData.isNotEmpty || _hasPositiveSeconds(averageDurationSeconds);
int? get averageDurationMinutes {
final averageSeconds = _computedAverageDurationSeconds;
if (averageSeconds == null) return null;
return (averageSeconds / 60).round();
return averageSeconds ~/ 60;
}
num? get _computedAverageDurationSeconds {
... ... @@ -377,22 +383,26 @@ class MonthlySleepReport {
.where((minutes) => minutes > 0)
.toList();
if (values.isEmpty) {
return averageDurationSeconds;
return _hasPositiveSeconds(averageDurationSeconds)
? averageDurationSeconds
: null;
}
return values.reduce((sum, minutes) => sum + minutes) * 60 / values.length;
}
int? get averageQualityScore {
final average = _computedAverageScore;
return average?.round();
return average?.toInt();
}
num? get _computedAverageScore {
final values = daysWithData
.map((report) => report.qualityScore)
.map((report) => report.validQualityScore)
.whereType<int>()
.toList();
if (values.isEmpty) return averageScore;
if (values.isEmpty) {
return _hasPositiveSeconds(averageDurationSeconds) ? averageScore : null;
}
return values.reduce((sum, score) => sum + score) / values.length;
}
... ... @@ -425,3 +435,5 @@ double? _comparisonPercent(num? current, num? previous) {
if (current == null || previous == null || previous == 0) return null;
return (current - previous) / previous * 100;
}
bool _hasPositiveSeconds(num? seconds) => seconds != null && seconds > 0;
... ...
... ... @@ -134,6 +134,7 @@ class _DailyReportContent extends StatelessWidget {
SleepHeartRateCard(
date: report?.date,
summary: report?.heartRate ?? const SleepHeartRateSummary(),
hasSleepData: (report?.duration?.minutes ?? 0) > 0,
),
],
);
... ...
... ... @@ -11,10 +11,12 @@ class SleepHeartRateCard extends StatelessWidget {
super.key,
this.date,
required this.summary,
this.hasSleepData = true,
});
final DateTime? date;
final SleepHeartRateSummary summary;
final bool hasSleepData;
static const _h1 = Color(0xFF0F0F11);
static const _h3 = Color(0xFFB0B0B6);
... ... @@ -46,14 +48,16 @@ class SleepHeartRateCard extends StatelessWidget {
),
),
const SizedBox(height: 24),
_MetricRow(summary: summary),
_MetricRow(
summary: hasSleepData ? summary : const SleepHeartRateSummary(),
),
const SizedBox(height: 18),
Expanded(
child: Builder(
builder: (context) {
final xAxis = _heartRateXAxis;
final chartPoints = _chartPoints(xAxis);
final hasChartData = chartPoints.isNotEmpty;
final hasChartData = hasSleepData && chartPoints.isNotEmpty;
return Stack(
alignment: Alignment.center,
... ...
... ... @@ -22,7 +22,7 @@ class _SleepQualityRingState extends State<SleepQualityRing>
late Animation<double> _progress;
double get _targetProgress =>
((widget.report?.qualityScore ?? 0).clamp(0, 100) / 100).toDouble();
((widget.report?.validQualityScore ?? 0).clamp(0, 100) / 100).toDouble();
@override
void initState() {
... ... @@ -38,7 +38,8 @@ class _SleepQualityRingState extends State<SleepQualityRing>
void didUpdateWidget(covariant SleepQualityRing oldWidget) {
super.didUpdateWidget(oldWidget);
final oldProgress =
((oldWidget.report?.qualityScore ?? 0).clamp(0, 100) / 100).toDouble();
((oldWidget.report?.validQualityScore ?? 0).clamp(0, 100) / 100)
.toDouble();
if (oldProgress != _targetProgress) {
_animateTo(_targetProgress, from: _progress.value);
}
... ... @@ -67,7 +68,7 @@ class _SleepQualityRingState extends State<SleepQualityRing>
builder: (context, child) => CustomPaint(
painter: _SleepQualityRingPainter(
progress: _progress.value,
hasData: widget.report?.qualityScore != null,
hasData: widget.report?.validQualityScore != null,
),
),
),
... ...
... ... @@ -20,8 +20,8 @@ class SleepSummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final duration = report?.duration;
final score = report?.qualityScore;
final duration = report?.hasSleepData == true ? report?.duration : null;
final score = report?.validQualityScore;
final quality = report?.qualityLevel ?? SleepQualityLevel.unknown;
return Container(
... ... @@ -147,7 +147,7 @@ class _DurationValue extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasData = duration != null;
final hasData = (duration?.minutes ?? 0) > 0;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
... ...
... ... @@ -29,7 +29,8 @@ class SleepWeekReportView extends StatelessWidget {
static TextStyle get titleTextStyle => SleepReportTextStyles.titleTextStyle;
static StrutStyle get titleStrutStyle => SleepReportTextStyles.titleStrutStyle;
static StrutStyle get titleStrutStyle =>
SleepReportTextStyles.titleStrutStyle;
static int minutesFromEvening(DateTime time) {
final minutes = time.hour * 60 + time.minute;
... ... @@ -202,7 +203,7 @@ class _SleepPeriodTrendContent extends StatelessWidget {
List<SleepReport> days, {
required bool longest,
}) {
final values = days.where((day) => day.duration != null);
final values = days.where((day) => day.hasSleepData);
if (values.isEmpty) return null;
return values.reduce((selected, day) {
final selectedValue = selected.duration!.minutes;
... ... @@ -216,11 +217,11 @@ class _SleepPeriodTrendContent extends StatelessWidget {
List<SleepReport> days, {
required bool best,
}) {
final values = days.where((day) => day.qualityScore != null);
final values = days.where((day) => day.validQualityScore != null);
if (values.isEmpty) return null;
return values.reduce((selected, day) {
final selectedValue = selected.qualityScore!;
final dayValue = day.qualityScore!;
final selectedValue = selected.validQualityScore!;
final dayValue = day.validQualityScore!;
if (best) return dayValue > selectedValue ? day : selected;
return dayValue < selectedValue ? day : selected;
});
... ... @@ -230,7 +231,9 @@ class _SleepPeriodTrendContent extends StatelessWidget {
List<SleepReport> days, {
required bool earliest,
}) {
final values = days.where((day) => day.heartRate.sleepStart != null);
final values = days.where(
(day) => day.hasSleepData && day.heartRate.sleepStart != null,
);
if (values.isEmpty) return null;
return values.reduce((selected, day) {
final selectedValue = SleepWeekReportView.minutesFromEvening(
... ... @@ -325,7 +328,7 @@ class _TopSummary extends StatelessWidget {
String _formatComparison(double? value) {
if (!hasData || value == null) return '-';
return value.abs().round().toString();
return value.abs().toInt().toString();
}
}
... ... @@ -466,7 +469,7 @@ class _DurationChartState extends State<_DurationChart> {
final values = <double>[
8,
for (final day in widget.days)
if (day.duration != null) day.duration!.minutes / 60,
if (day.hasSleepData) day.duration!.minutes / 60,
if (widget.averageDurationMinutes != null)
widget.averageDurationMinutes! / 60,
if (_targetDurationHours case final target?) target,
... ... @@ -477,7 +480,7 @@ class _DurationChartState extends State<_DurationChart> {
@override
Widget build(BuildContext context) {
final hasData = widget.days.any((day) => day.duration != null);
final hasData = widget.days.any((day) => day.hasSleepData);
return Column(
children: [
_ChartLegend(
... ... @@ -540,7 +543,7 @@ class _DurationChartState extends State<_DurationChart> {
barTouchData: _barTouchData(
days: widget.days,
dataBuilder: _ChartSummaryData.duration,
hasData: (report) => report.duration != null,
hasData: (report) => report.hasSleepData,
onSelected: (_, offset) => setState(() => _selectedOffset = offset),
),
gridData: const FlGridData(show: false),
... ... @@ -590,7 +593,7 @@ class _QualityChartState extends State<_QualityChart> {
@override
Widget build(BuildContext context) {
final hasData = widget.days.any((day) => day.qualityScore != null);
final hasData = widget.days.any((day) => day.validQualityScore != null);
return Column(
children: [
const _ChartLegend(
... ... @@ -646,7 +649,7 @@ class _QualityChartState extends State<_QualityChart> {
barTouchData: _barTouchData(
days: widget.days,
dataBuilder: _ChartSummaryData.quality,
hasData: (report) => report.qualityScore != null,
hasData: (report) => report.validQualityScore != null,
onSelected: (_, offset) => setState(() => _selectedOffset = offset),
),
gridData: const FlGridData(show: false),
... ... @@ -661,7 +664,7 @@ class _QualityChartState extends State<_QualityChart> {
x: i,
barRods: [
BarChartRodData(
toY: (widget.days[i].qualityScore ?? 0).toDouble(),
toY: (widget.days[i].validQualityScore ?? 0).toDouble(),
width: _barWidth(widget.compact),
color: SleepWeekReportView.blue,
borderRadius: const BorderRadius.vertical(
... ... @@ -686,7 +689,9 @@ class _BedtimeChart extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasData = days.any((day) => day.heartRate.sleepStart != null);
final hasData = days.any(
(day) => day.hasSleepData && day.heartRate.sleepStart != null,
);
final axis = _BedtimeAxis.fromReports(days);
return Stack(
alignment: Alignment.center,
... ... @@ -706,7 +711,7 @@ class _BedtimeChart extends StatelessWidget {
LineChartData _chartData(_BedtimeAxis axis) {
final points = <FlSpot>[
for (var i = 0; i < days.length; i++)
if (days[i].heartRate.sleepStart != null)
if (days[i].hasSleepData && days[i].heartRate.sleepStart != null)
FlSpot(
i.toDouble(),
_bedtimeY(days[i].heartRate.sleepStart!),
... ... @@ -758,7 +763,7 @@ class _BedtimeChart extends StatelessWidget {
}
String _timeAxisLabel(double value) {
final hour = value.round() % 24;
final hour = value.toInt() % 24;
return '${hour.toString().padLeft(2, '0')}:00';
}
}
... ... @@ -783,6 +788,7 @@ class _BedtimeAxis {
factory _BedtimeAxis.fromReports(List<SleepReport> days) {
final values = days
.where((day) => day.hasSleepData)
.map((day) => day.heartRate.sleepStart)
.whereType<DateTime>()
.map(SleepWeekReportView.minutesFromEvening)
... ... @@ -945,10 +951,12 @@ LineTouchTooltipData _lineTooltipData({
fitInsideVertically: false,
getTooltipItems: (spots) {
return spots.map((spot) {
final index = spot.x.round();
final index = spot.x.toInt();
if (index < 0 || index >= days.length) return null;
final report = days[index];
if (report.heartRate.sleepStart == null) return null;
if (!report.hasSleepData || report.heartRate.sleepStart == null) {
return null;
}
return _lineTooltipItem(dataBuilder(report));
}).toList();
},
... ... @@ -1016,7 +1024,7 @@ class _ChartSummaryData {
}
factory _ChartSummaryData.quality(SleepReport report) {
final score = report.qualityScore!;
final score = report.validQualityScore!;
return _ChartSummaryData(
title: l10n.sleepQualityScore(
report.qualityLevel.shortText,
... ... @@ -1244,7 +1252,7 @@ class _HorizontalDashedGridPainter extends CustomPainter {
final paint = Paint()
..color = SleepWeekReportView.grid
..strokeWidth = 1;
final steps = ((maxY - minY) / interval).round();
final steps = ((maxY - minY) / interval).ceil();
for (var index = 0; index <= steps; index++) {
final y = size.height * (1 - index / steps);
for (var x = 0.0; x < size.width; x += 4) {
... ... @@ -1323,7 +1331,7 @@ class _RightAxisLabels extends StatelessWidget {
return IgnorePointer(
child: LayoutBuilder(
builder: (context, constraints) {
final steps = ((maxY - minY) / interval).round();
final steps = ((maxY - minY) / interval).ceil();
return Stack(
clipBehavior: Clip.none,
children: [
... ... @@ -1454,10 +1462,11 @@ class _ExtremeMetric extends StatelessWidget {
required Color color,
required SleepReport? report,
}) {
final score = report?.validQualityScore;
return _ExtremeMetric(
title: title,
color: color,
value: report?.qualityScore == null ? '-' : '${report!.qualityScore}',
value: score == null ? '-' : '$score',
unit: l10n.reportUnitScore,
date: report?.date,
);
... ... @@ -1468,7 +1477,8 @@ class _ExtremeMetric extends StatelessWidget {
required Color color,
required SleepReport? report,
}) {
final start = report?.heartRate.sleepStart;
final start =
report?.hasSleepData == true ? report?.heartRate.sleepStart : null;
return _ExtremeMetric(
title: title,
color: color,
... ...