|
...
|
...
|
@@ -25,12 +25,13 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
static const _red = Color(0xFFFF5279);
|
|
|
|
static const _chartLeftInset = 0.0;
|
|
|
|
static const _chartRightInset = 20.0;
|
|
|
|
static const _chartTopInset = 16.0;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Container(
|
|
|
|
height: 358,
|
|
|
|
padding: EdgeInsets.fromLTRB(20, 20.h, 20, 16),
|
|
|
|
padding: EdgeInsets.fromLTRB(20, 20.h, 20, 16),
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Colors.white,
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
...
|
...
|
@@ -66,6 +67,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
children: [
|
|
|
|
if (hasChartData) ...[
|
|
|
|
Positioned.fill(
|
|
|
|
top: _chartTopInset,
|
|
|
|
bottom: 20,
|
|
|
|
child: LayoutBuilder(
|
|
|
|
builder: (context, constraints) => LineChart(
|
|
...
|
...
|
@@ -77,7 +79,13 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const Positioned.fill(
|
|
|
|
top: _chartTopInset,
|
|
|
|
bottom: 20,
|
|
|
|
child: _HeartRateBoundaryGridLine(),
|
|
|
|
),
|
|
|
|
Positioned.fill(
|
|
|
|
top: _chartTopInset,
|
|
|
|
bottom: 20,
|
|
|
|
child: _HeartRateAxisLabels(
|
|
|
|
range: _HeartRateYAxisRange.fromPoints(chartPoints),
|
|
...
|
...
|
@@ -86,7 +94,42 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
Positioned.fill(
|
|
|
|
child: _HeartRateTimeLabels(axis: xAxis),
|
|
|
|
),
|
|
|
|
] else
|
|
|
|
] else ...[
|
|
|
|
Positioned.fill(
|
|
|
|
top: _chartTopInset,
|
|
|
|
bottom: 20,
|
|
|
|
child: LayoutBuilder(
|
|
|
|
builder: (context, constraints) => LineChart(
|
|
|
|
_chartData(
|
|
|
|
xAxis: xAxis,
|
|
|
|
chartPoints: chartPoints,
|
|
|
|
chartWidth: constraints.maxWidth,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const Positioned.fill(
|
|
|
|
top: _chartTopInset,
|
|
|
|
bottom: 20,
|
|
|
|
child: _HeartRateBoundaryGridLine(
|
|
|
|
drawTop: true,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Positioned.fill(
|
|
|
|
top: _chartTopInset,
|
|
|
|
bottom: 20,
|
|
|
|
child: _HeartRateAxisLabels(
|
|
|
|
range: _HeartRateYAxisRange.fromPoints(
|
|
|
|
chartPoints,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Positioned.fill(
|
|
|
|
child: _HeartRateTimeLabels(
|
|
|
|
axis: xAxis,
|
|
|
|
tickCount: 4,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Positioned.fill(
|
|
|
|
bottom: 20,
|
|
|
|
child: Center(
|
|
...
|
...
|
@@ -100,6 +143,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
...
|
...
|
@@ -138,7 +182,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
gridData: FlGridData(
|
|
|
|
show: true,
|
|
|
|
drawVerticalLine: false,
|
|
|
|
horizontalInterval: _HeartRateYAxisRange.interval,
|
|
|
|
horizontalInterval: yRange.interval,
|
|
|
|
getDrawingHorizontalLine: (_) => const FlLine(
|
|
|
|
color: _grid,
|
|
|
|
strokeWidth: 1,
|
|
...
|
...
|
@@ -208,10 +252,21 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
_HeartRateXAxis get _heartRateXAxis {
|
|
|
|
final points = _visibleDatePoints;
|
|
|
|
if (points.isEmpty) {
|
|
|
|
final start = _chartDayStart;
|
|
|
|
final sleepStart = summary.sleepStart;
|
|
|
|
final sleepEnd = summary.sleepEnd;
|
|
|
|
if (sleepStart != null &&
|
|
|
|
sleepEnd != null &&
|
|
|
|
sleepEnd.isAfter(sleepStart)) {
|
|
|
|
return _HeartRateXAxis(
|
|
|
|
start: sleepStart,
|
|
|
|
end: sleepEnd,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final start = _chartDayStart.add(const Duration(hours: 22));
|
|
|
|
return _HeartRateXAxis(
|
|
|
|
start: start,
|
|
|
|
end: start.add(const Duration(hours: 1)),
|
|
|
|
end: start.add(const Duration(hours: 8, minutes: 59)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -274,6 +329,65 @@ class SleepHeartRateCard extends StatelessWidget { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _HeartRateBoundaryGridLine extends StatelessWidget {
|
|
|
|
const _HeartRateBoundaryGridLine({
|
|
|
|
this.drawTop = false,
|
|
|
|
});
|
|
|
|
|
|
|
|
final bool drawTop;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return IgnorePointer(
|
|
|
|
child: CustomPaint(
|
|
|
|
painter: _HeartRateBoundaryGridLinePainter(drawTop: drawTop),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _HeartRateBoundaryGridLinePainter extends CustomPainter {
|
|
|
|
const _HeartRateBoundaryGridLinePainter({
|
|
|
|
required this.drawTop,
|
|
|
|
});
|
|
|
|
|
|
|
|
final bool drawTop;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
final paint = Paint()
|
|
|
|
..color = SleepHeartRateCard._grid
|
|
|
|
..strokeWidth = 1;
|
|
|
|
final right = size.width;
|
|
|
|
_drawDashedLine(canvas, paint, y: size.height - 0.5, width: right);
|
|
|
|
if (drawTop) {
|
|
|
|
_drawDashedLine(canvas, paint, y: 0.5, width: right);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void _drawDashedLine(
|
|
|
|
Canvas canvas,
|
|
|
|
Paint paint, {
|
|
|
|
required double y,
|
|
|
|
required double width,
|
|
|
|
}) {
|
|
|
|
const dashWidth = 2.0;
|
|
|
|
const dashSpace = 2.0;
|
|
|
|
var x = 0.0;
|
|
|
|
|
|
|
|
while (x < width) {
|
|
|
|
final end = (x + dashWidth).clamp(0.0, width);
|
|
|
|
canvas.drawLine(Offset(x, y), Offset(end, y), paint);
|
|
|
|
x += dashWidth + dashSpace;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool shouldRepaint(covariant _HeartRateBoundaryGridLinePainter oldDelegate) {
|
|
|
|
return drawTop != oldDelegate.drawTop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _HeartRateXAxis {
|
|
|
|
const _HeartRateXAxis({
|
|
|
|
required this.start,
|
|
...
|
...
|
@@ -325,18 +439,23 @@ class _HeartRateAxisLabels extends StatelessWidget { |
|
|
|
class _HeartRateTimeLabels extends StatelessWidget {
|
|
|
|
const _HeartRateTimeLabels({
|
|
|
|
required this.axis,
|
|
|
|
this.tickCount = 3,
|
|
|
|
});
|
|
|
|
|
|
|
|
final _HeartRateXAxis axis;
|
|
|
|
final int tickCount;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final totalMinutes = axis.end.difference(axis.start).inMinutes;
|
|
|
|
final ticks = <DateTime>[
|
|
|
|
axis.start,
|
|
|
|
axis.start.add(Duration(minutes: totalMinutes ~/ 2)),
|
|
|
|
axis.end,
|
|
|
|
];
|
|
|
|
final count = tickCount < 2 ? 2 : tickCount;
|
|
|
|
final ticks = List.generate(count, (index) {
|
|
|
|
if (index == 0) return axis.start;
|
|
|
|
if (index == count - 1) return axis.end;
|
|
|
|
return axis.start.add(
|
|
|
|
Duration(minutes: (totalMinutes * index / (count - 1)).round()),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
return IgnorePointer(
|
|
|
|
child: LayoutBuilder(
|
|
...
|
...
|
@@ -371,7 +490,8 @@ class _HeartRateTimeLabels extends StatelessWidget { |
|
|
|
final plotWidth =
|
|
|
|
(plotRight - plotLeft).clamp(0, double.infinity).toDouble();
|
|
|
|
final tickX = plotLeft + plotWidth * progress;
|
|
|
|
final left = tickX - labelWidth / 2;
|
|
|
|
final isFirstTick = index == 0;
|
|
|
|
final left = isFirstTick ? tickX : tickX - labelWidth / 2;
|
|
|
|
|
|
|
|
return Positioned(
|
|
|
|
left: left,
|
|
...
|
...
|
@@ -379,7 +499,7 @@ class _HeartRateTimeLabels extends StatelessWidget { |
|
|
|
width: labelWidth,
|
|
|
|
child: _TimeLabel(
|
|
|
|
_tickLabel(tick),
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
textAlign: isFirstTick ? TextAlign.left : TextAlign.center,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
...
|
...
|
@@ -417,12 +537,12 @@ class _HeartRateYAxisRange { |
|
|
|
const _HeartRateYAxisRange({
|
|
|
|
required this.minY,
|
|
|
|
required this.maxY,
|
|
|
|
this.interval = 20.0,
|
|
|
|
});
|
|
|
|
|
|
|
|
static const interval = 20.0;
|
|
|
|
|
|
|
|
final double minY;
|
|
|
|
final double maxY;
|
|
|
|
final double interval;
|
|
|
|
|
|
|
|
double get span => maxY - minY <= 0 ? interval : maxY - minY;
|
|
|
|
|
|
...
|
...
|
@@ -439,19 +559,24 @@ class _HeartRateYAxisRange { |
|
|
|
|
|
|
|
factory _HeartRateYAxisRange.fromPoints(List<SleepHeartRatePoint> points) {
|
|
|
|
if (points.isEmpty) {
|
|
|
|
return const _HeartRateYAxisRange(minY: 50, maxY: 80);
|
|
|
|
return const _HeartRateYAxisRange(
|
|
|
|
minY: 50,
|
|
|
|
maxY: 80,
|
|
|
|
interval: 10,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
final values = points.map((point) => point.bpm);
|
|
|
|
final minBpm = values.reduce((min, bpm) => bpm < min ? bpm : min);
|
|
|
|
final maxBpm = values.reduce((max, bpm) => bpm > max ? bpm : max);
|
|
|
|
final minY = minBpm;
|
|
|
|
var maxY = (maxBpm / 5).ceil() * 5.0;
|
|
|
|
if (minY == maxY) maxY += interval;
|
|
|
|
final minY = (minBpm / 10).floor() * 10.0;
|
|
|
|
var maxY = (maxBpm / 10).ceil() * 10.0;
|
|
|
|
if (minY == maxY) maxY += 10;
|
|
|
|
|
|
|
|
return _HeartRateYAxisRange(
|
|
|
|
minY: minY,
|
|
|
|
maxY: maxY,
|
|
|
|
interval: 10,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
...
|
...
|
|