|
@@ -25,12 +25,13 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -25,12 +25,13 @@ class SleepHeartRateCard extends StatelessWidget { |
|
25
|
static const _red = Color(0xFFFF5279);
|
25
|
static const _red = Color(0xFFFF5279);
|
|
26
|
static const _chartLeftInset = 0.0;
|
26
|
static const _chartLeftInset = 0.0;
|
|
27
|
static const _chartRightInset = 20.0;
|
27
|
static const _chartRightInset = 20.0;
|
|
|
|
28
|
+ static const _chartTopInset = 16.0;
|
|
28
|
|
29
|
|
|
29
|
@override
|
30
|
@override
|
|
30
|
Widget build(BuildContext context) {
|
31
|
Widget build(BuildContext context) {
|
|
31
|
return Container(
|
32
|
return Container(
|
|
32
|
height: 358,
|
33
|
height: 358,
|
|
33
|
- padding: EdgeInsets.fromLTRB(20, 20.h, 20, 16),
|
34
|
+ padding: EdgeInsets.fromLTRB(20, 20.h, 20, 16),
|
|
34
|
decoration: BoxDecoration(
|
35
|
decoration: BoxDecoration(
|
|
35
|
color: Colors.white,
|
36
|
color: Colors.white,
|
|
36
|
borderRadius: BorderRadius.circular(16),
|
37
|
borderRadius: BorderRadius.circular(16),
|
|
@@ -66,6 +67,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -66,6 +67,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
66
|
children: [
|
67
|
children: [
|
|
67
|
if (hasChartData) ...[
|
68
|
if (hasChartData) ...[
|
|
68
|
Positioned.fill(
|
69
|
Positioned.fill(
|
|
|
|
70
|
+ top: _chartTopInset,
|
|
69
|
bottom: 20,
|
71
|
bottom: 20,
|
|
70
|
child: LayoutBuilder(
|
72
|
child: LayoutBuilder(
|
|
71
|
builder: (context, constraints) => LineChart(
|
73
|
builder: (context, constraints) => LineChart(
|
|
@@ -77,7 +79,13 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -77,7 +79,13 @@ class SleepHeartRateCard extends StatelessWidget { |
|
77
|
),
|
79
|
),
|
|
78
|
),
|
80
|
),
|
|
79
|
),
|
81
|
),
|
|
|
|
82
|
+ const Positioned.fill(
|
|
|
|
83
|
+ top: _chartTopInset,
|
|
|
|
84
|
+ bottom: 20,
|
|
|
|
85
|
+ child: _HeartRateBoundaryGridLine(),
|
|
|
|
86
|
+ ),
|
|
80
|
Positioned.fill(
|
87
|
Positioned.fill(
|
|
|
|
88
|
+ top: _chartTopInset,
|
|
81
|
bottom: 20,
|
89
|
bottom: 20,
|
|
82
|
child: _HeartRateAxisLabels(
|
90
|
child: _HeartRateAxisLabels(
|
|
83
|
range: _HeartRateYAxisRange.fromPoints(chartPoints),
|
91
|
range: _HeartRateYAxisRange.fromPoints(chartPoints),
|
|
@@ -86,7 +94,42 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -86,7 +94,42 @@ class SleepHeartRateCard extends StatelessWidget { |
|
86
|
Positioned.fill(
|
94
|
Positioned.fill(
|
|
87
|
child: _HeartRateTimeLabels(axis: xAxis),
|
95
|
child: _HeartRateTimeLabels(axis: xAxis),
|
|
88
|
),
|
96
|
),
|
|
89
|
- ] else
|
97
|
+ ] else ...[
|
|
|
|
98
|
+ Positioned.fill(
|
|
|
|
99
|
+ top: _chartTopInset,
|
|
|
|
100
|
+ bottom: 20,
|
|
|
|
101
|
+ child: LayoutBuilder(
|
|
|
|
102
|
+ builder: (context, constraints) => LineChart(
|
|
|
|
103
|
+ _chartData(
|
|
|
|
104
|
+ xAxis: xAxis,
|
|
|
|
105
|
+ chartPoints: chartPoints,
|
|
|
|
106
|
+ chartWidth: constraints.maxWidth,
|
|
|
|
107
|
+ ),
|
|
|
|
108
|
+ ),
|
|
|
|
109
|
+ ),
|
|
|
|
110
|
+ ),
|
|
|
|
111
|
+ const Positioned.fill(
|
|
|
|
112
|
+ top: _chartTopInset,
|
|
|
|
113
|
+ bottom: 20,
|
|
|
|
114
|
+ child: _HeartRateBoundaryGridLine(
|
|
|
|
115
|
+ drawTop: true,
|
|
|
|
116
|
+ ),
|
|
|
|
117
|
+ ),
|
|
|
|
118
|
+ Positioned.fill(
|
|
|
|
119
|
+ top: _chartTopInset,
|
|
|
|
120
|
+ bottom: 20,
|
|
|
|
121
|
+ child: _HeartRateAxisLabels(
|
|
|
|
122
|
+ range: _HeartRateYAxisRange.fromPoints(
|
|
|
|
123
|
+ chartPoints,
|
|
|
|
124
|
+ ),
|
|
|
|
125
|
+ ),
|
|
|
|
126
|
+ ),
|
|
|
|
127
|
+ Positioned.fill(
|
|
|
|
128
|
+ child: _HeartRateTimeLabels(
|
|
|
|
129
|
+ axis: xAxis,
|
|
|
|
130
|
+ tickCount: 4,
|
|
|
|
131
|
+ ),
|
|
|
|
132
|
+ ),
|
|
90
|
Positioned.fill(
|
133
|
Positioned.fill(
|
|
91
|
bottom: 20,
|
134
|
bottom: 20,
|
|
92
|
child: Center(
|
135
|
child: Center(
|
|
@@ -100,6 +143,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -100,6 +143,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
100
|
),
|
143
|
),
|
|
101
|
),
|
144
|
),
|
|
102
|
),
|
145
|
),
|
|
|
|
146
|
+ ],
|
|
103
|
],
|
147
|
],
|
|
104
|
);
|
148
|
);
|
|
105
|
},
|
149
|
},
|
|
@@ -138,7 +182,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -138,7 +182,7 @@ class SleepHeartRateCard extends StatelessWidget { |
|
138
|
gridData: FlGridData(
|
182
|
gridData: FlGridData(
|
|
139
|
show: true,
|
183
|
show: true,
|
|
140
|
drawVerticalLine: false,
|
184
|
drawVerticalLine: false,
|
|
141
|
- horizontalInterval: _HeartRateYAxisRange.interval,
|
185
|
+ horizontalInterval: yRange.interval,
|
|
142
|
getDrawingHorizontalLine: (_) => const FlLine(
|
186
|
getDrawingHorizontalLine: (_) => const FlLine(
|
|
143
|
color: _grid,
|
187
|
color: _grid,
|
|
144
|
strokeWidth: 1,
|
188
|
strokeWidth: 1,
|
|
@@ -208,10 +252,21 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -208,10 +252,21 @@ class SleepHeartRateCard extends StatelessWidget { |
|
208
|
_HeartRateXAxis get _heartRateXAxis {
|
252
|
_HeartRateXAxis get _heartRateXAxis {
|
|
209
|
final points = _visibleDatePoints;
|
253
|
final points = _visibleDatePoints;
|
|
210
|
if (points.isEmpty) {
|
254
|
if (points.isEmpty) {
|
|
211
|
- final start = _chartDayStart;
|
255
|
+ final sleepStart = summary.sleepStart;
|
|
|
|
256
|
+ final sleepEnd = summary.sleepEnd;
|
|
|
|
257
|
+ if (sleepStart != null &&
|
|
|
|
258
|
+ sleepEnd != null &&
|
|
|
|
259
|
+ sleepEnd.isAfter(sleepStart)) {
|
|
|
|
260
|
+ return _HeartRateXAxis(
|
|
|
|
261
|
+ start: sleepStart,
|
|
|
|
262
|
+ end: sleepEnd,
|
|
|
|
263
|
+ );
|
|
|
|
264
|
+ }
|
|
|
|
265
|
+
|
|
|
|
266
|
+ final start = _chartDayStart.add(const Duration(hours: 22));
|
|
212
|
return _HeartRateXAxis(
|
267
|
return _HeartRateXAxis(
|
|
213
|
start: start,
|
268
|
start: start,
|
|
214
|
- end: start.add(const Duration(hours: 1)),
|
269
|
+ end: start.add(const Duration(hours: 8, minutes: 59)),
|
|
215
|
);
|
270
|
);
|
|
216
|
}
|
271
|
}
|
|
217
|
|
272
|
|
|
@@ -274,6 +329,65 @@ class SleepHeartRateCard extends StatelessWidget { |
|
@@ -274,6 +329,65 @@ class SleepHeartRateCard extends StatelessWidget { |
|
274
|
}
|
329
|
}
|
|
275
|
}
|
330
|
}
|
|
276
|
|
331
|
|
|
|
|
332
|
+class _HeartRateBoundaryGridLine extends StatelessWidget {
|
|
|
|
333
|
+ const _HeartRateBoundaryGridLine({
|
|
|
|
334
|
+ this.drawTop = false,
|
|
|
|
335
|
+ });
|
|
|
|
336
|
+
|
|
|
|
337
|
+ final bool drawTop;
|
|
|
|
338
|
+
|
|
|
|
339
|
+ @override
|
|
|
|
340
|
+ Widget build(BuildContext context) {
|
|
|
|
341
|
+ return IgnorePointer(
|
|
|
|
342
|
+ child: CustomPaint(
|
|
|
|
343
|
+ painter: _HeartRateBoundaryGridLinePainter(drawTop: drawTop),
|
|
|
|
344
|
+ ),
|
|
|
|
345
|
+ );
|
|
|
|
346
|
+ }
|
|
|
|
347
|
+}
|
|
|
|
348
|
+
|
|
|
|
349
|
+class _HeartRateBoundaryGridLinePainter extends CustomPainter {
|
|
|
|
350
|
+ const _HeartRateBoundaryGridLinePainter({
|
|
|
|
351
|
+ required this.drawTop,
|
|
|
|
352
|
+ });
|
|
|
|
353
|
+
|
|
|
|
354
|
+ final bool drawTop;
|
|
|
|
355
|
+
|
|
|
|
356
|
+ @override
|
|
|
|
357
|
+ void paint(Canvas canvas, Size size) {
|
|
|
|
358
|
+ final paint = Paint()
|
|
|
|
359
|
+ ..color = SleepHeartRateCard._grid
|
|
|
|
360
|
+ ..strokeWidth = 1;
|
|
|
|
361
|
+ final right = size.width;
|
|
|
|
362
|
+ _drawDashedLine(canvas, paint, y: size.height - 0.5, width: right);
|
|
|
|
363
|
+ if (drawTop) {
|
|
|
|
364
|
+ _drawDashedLine(canvas, paint, y: 0.5, width: right);
|
|
|
|
365
|
+ }
|
|
|
|
366
|
+ }
|
|
|
|
367
|
+
|
|
|
|
368
|
+ void _drawDashedLine(
|
|
|
|
369
|
+ Canvas canvas,
|
|
|
|
370
|
+ Paint paint, {
|
|
|
|
371
|
+ required double y,
|
|
|
|
372
|
+ required double width,
|
|
|
|
373
|
+ }) {
|
|
|
|
374
|
+ const dashWidth = 2.0;
|
|
|
|
375
|
+ const dashSpace = 2.0;
|
|
|
|
376
|
+ var x = 0.0;
|
|
|
|
377
|
+
|
|
|
|
378
|
+ while (x < width) {
|
|
|
|
379
|
+ final end = (x + dashWidth).clamp(0.0, width);
|
|
|
|
380
|
+ canvas.drawLine(Offset(x, y), Offset(end, y), paint);
|
|
|
|
381
|
+ x += dashWidth + dashSpace;
|
|
|
|
382
|
+ }
|
|
|
|
383
|
+ }
|
|
|
|
384
|
+
|
|
|
|
385
|
+ @override
|
|
|
|
386
|
+ bool shouldRepaint(covariant _HeartRateBoundaryGridLinePainter oldDelegate) {
|
|
|
|
387
|
+ return drawTop != oldDelegate.drawTop;
|
|
|
|
388
|
+ }
|
|
|
|
389
|
+}
|
|
|
|
390
|
+
|
|
277
|
class _HeartRateXAxis {
|
391
|
class _HeartRateXAxis {
|
|
278
|
const _HeartRateXAxis({
|
392
|
const _HeartRateXAxis({
|
|
279
|
required this.start,
|
393
|
required this.start,
|
|
@@ -325,18 +439,23 @@ class _HeartRateAxisLabels extends StatelessWidget { |
|
@@ -325,18 +439,23 @@ class _HeartRateAxisLabels extends StatelessWidget { |
|
325
|
class _HeartRateTimeLabels extends StatelessWidget {
|
439
|
class _HeartRateTimeLabels extends StatelessWidget {
|
|
326
|
const _HeartRateTimeLabels({
|
440
|
const _HeartRateTimeLabels({
|
|
327
|
required this.axis,
|
441
|
required this.axis,
|
|
|
|
442
|
+ this.tickCount = 3,
|
|
328
|
});
|
443
|
});
|
|
329
|
|
444
|
|
|
330
|
final _HeartRateXAxis axis;
|
445
|
final _HeartRateXAxis axis;
|
|
|
|
446
|
+ final int tickCount;
|
|
331
|
|
447
|
|
|
332
|
@override
|
448
|
@override
|
|
333
|
Widget build(BuildContext context) {
|
449
|
Widget build(BuildContext context) {
|
|
334
|
final totalMinutes = axis.end.difference(axis.start).inMinutes;
|
450
|
final totalMinutes = axis.end.difference(axis.start).inMinutes;
|
|
335
|
- final ticks = <DateTime>[
|
|
|
|
336
|
- axis.start,
|
|
|
|
337
|
- axis.start.add(Duration(minutes: totalMinutes ~/ 2)),
|
|
|
|
338
|
- axis.end,
|
|
|
|
339
|
- ];
|
451
|
+ final count = tickCount < 2 ? 2 : tickCount;
|
|
|
|
452
|
+ final ticks = List.generate(count, (index) {
|
|
|
|
453
|
+ if (index == 0) return axis.start;
|
|
|
|
454
|
+ if (index == count - 1) return axis.end;
|
|
|
|
455
|
+ return axis.start.add(
|
|
|
|
456
|
+ Duration(minutes: (totalMinutes * index / (count - 1)).round()),
|
|
|
|
457
|
+ );
|
|
|
|
458
|
+ });
|
|
340
|
|
459
|
|
|
341
|
return IgnorePointer(
|
460
|
return IgnorePointer(
|
|
342
|
child: LayoutBuilder(
|
461
|
child: LayoutBuilder(
|
|
@@ -371,7 +490,8 @@ class _HeartRateTimeLabels extends StatelessWidget { |
|
@@ -371,7 +490,8 @@ class _HeartRateTimeLabels extends StatelessWidget { |
|
371
|
final plotWidth =
|
490
|
final plotWidth =
|
|
372
|
(plotRight - plotLeft).clamp(0, double.infinity).toDouble();
|
491
|
(plotRight - plotLeft).clamp(0, double.infinity).toDouble();
|
|
373
|
final tickX = plotLeft + plotWidth * progress;
|
492
|
final tickX = plotLeft + plotWidth * progress;
|
|
374
|
- final left = tickX - labelWidth / 2;
|
493
|
+ final isFirstTick = index == 0;
|
|
|
|
494
|
+ final left = isFirstTick ? tickX : tickX - labelWidth / 2;
|
|
375
|
|
495
|
|
|
376
|
return Positioned(
|
496
|
return Positioned(
|
|
377
|
left: left,
|
497
|
left: left,
|
|
@@ -379,7 +499,7 @@ class _HeartRateTimeLabels extends StatelessWidget { |
|
@@ -379,7 +499,7 @@ class _HeartRateTimeLabels extends StatelessWidget { |
|
379
|
width: labelWidth,
|
499
|
width: labelWidth,
|
|
380
|
child: _TimeLabel(
|
500
|
child: _TimeLabel(
|
|
381
|
_tickLabel(tick),
|
501
|
_tickLabel(tick),
|
|
382
|
- textAlign: TextAlign.center,
|
502
|
+ textAlign: isFirstTick ? TextAlign.left : TextAlign.center,
|
|
383
|
),
|
503
|
),
|
|
384
|
);
|
504
|
);
|
|
385
|
}
|
505
|
}
|
|
@@ -417,12 +537,12 @@ class _HeartRateYAxisRange { |
|
@@ -417,12 +537,12 @@ class _HeartRateYAxisRange { |
|
417
|
const _HeartRateYAxisRange({
|
537
|
const _HeartRateYAxisRange({
|
|
418
|
required this.minY,
|
538
|
required this.minY,
|
|
419
|
required this.maxY,
|
539
|
required this.maxY,
|
|
|
|
540
|
+ this.interval = 20.0,
|
|
420
|
});
|
541
|
});
|
|
421
|
|
542
|
|
|
422
|
- static const interval = 20.0;
|
|
|
|
423
|
-
|
|
|
|
424
|
final double minY;
|
543
|
final double minY;
|
|
425
|
final double maxY;
|
544
|
final double maxY;
|
|
|
|
545
|
+ final double interval;
|
|
426
|
|
546
|
|
|
427
|
double get span => maxY - minY <= 0 ? interval : maxY - minY;
|
547
|
double get span => maxY - minY <= 0 ? interval : maxY - minY;
|
|
428
|
|
548
|
|
|
@@ -439,19 +559,24 @@ class _HeartRateYAxisRange { |
|
@@ -439,19 +559,24 @@ class _HeartRateYAxisRange { |
|
439
|
|
559
|
|
|
440
|
factory _HeartRateYAxisRange.fromPoints(List<SleepHeartRatePoint> points) {
|
560
|
factory _HeartRateYAxisRange.fromPoints(List<SleepHeartRatePoint> points) {
|
|
441
|
if (points.isEmpty) {
|
561
|
if (points.isEmpty) {
|
|
442
|
- return const _HeartRateYAxisRange(minY: 50, maxY: 80);
|
562
|
+ return const _HeartRateYAxisRange(
|
|
|
|
563
|
+ minY: 50,
|
|
|
|
564
|
+ maxY: 80,
|
|
|
|
565
|
+ interval: 10,
|
|
|
|
566
|
+ );
|
|
443
|
}
|
567
|
}
|
|
444
|
|
568
|
|
|
445
|
final values = points.map((point) => point.bpm);
|
569
|
final values = points.map((point) => point.bpm);
|
|
446
|
final minBpm = values.reduce((min, bpm) => bpm < min ? bpm : min);
|
570
|
final minBpm = values.reduce((min, bpm) => bpm < min ? bpm : min);
|
|
447
|
final maxBpm = values.reduce((max, bpm) => bpm > max ? bpm : max);
|
571
|
final maxBpm = values.reduce((max, bpm) => bpm > max ? bpm : max);
|
|
448
|
- final minY = minBpm;
|
|
|
|
449
|
- var maxY = (maxBpm / 5).ceil() * 5.0;
|
|
|
|
450
|
- if (minY == maxY) maxY += interval;
|
572
|
+ final minY = (minBpm / 10).floor() * 10.0;
|
|
|
|
573
|
+ var maxY = (maxBpm / 10).ceil() * 10.0;
|
|
|
|
574
|
+ if (minY == maxY) maxY += 10;
|
|
451
|
|
575
|
|
|
452
|
return _HeartRateYAxisRange(
|
576
|
return _HeartRateYAxisRange(
|
|
453
|
minY: minY,
|
577
|
minY: minY,
|
|
454
|
maxY: maxY,
|
578
|
maxY: maxY,
|
|
|
|
579
|
+ interval: 10,
|
|
455
|
);
|
580
|
);
|
|
456
|
}
|
581
|
}
|
|
457
|
}
|
582
|
}
|