|
|
|
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
|
|
|
|
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
|
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
|
|
|
|
|
|
|
|
import '../../../../r.dart';
|
|
|
|
import '../../report_common/utils/report_localization.dart';
|
|
|
|
import '../../report_common/widgets/chart_selection_line_overlay.dart';
|
|
|
|
import '../../report_common/widgets/health_report_subject_scope.dart';
|
|
|
|
import '../../report_common/utils/report_localization.dart';
|
|
|
|
import '../models/hrv_report_models.dart';
|
|
|
|
|
|
|
|
class HrvWeekReportView extends StatelessWidget {
|
|
...
|
...
|
@@ -167,6 +167,8 @@ class _HrvBarChartState extends State<_HrvBarChart> { |
|
|
|
static const _plotLeft = _axisLabelWidth;
|
|
|
|
static const _plotRight = 0.0;
|
|
|
|
static const _bottomTitleHeight = 31.0;
|
|
|
|
static const _weekBarWidth = 16.0;
|
|
|
|
static const _weekBarGap = 22.0;
|
|
|
|
static const _tooltipBackground = Color(0xFFF3F3F3);
|
|
|
|
static const _tooltipDateColor = Color(0xFF78787D);
|
|
|
|
static const _tooltipMetaColor = Color(0xFFB0B0B6);
|
|
...
|
...
|
@@ -185,7 +187,7 @@ class _HrvBarChartState extends State<_HrvBarChart> { |
|
|
|
barRods: [
|
|
|
|
BarChartRodData(
|
|
|
|
toY: day.averageHrv ?? 0,
|
|
|
|
width: widget.isMonth ? 4 : 13,
|
|
|
|
width: widget.isMonth ? 4 : _weekBarWidth,
|
|
|
|
color: day.level == null
|
|
|
|
? Colors.transparent
|
|
|
|
: Color(day.level!.colorValue),
|
|
...
|
...
|
@@ -207,7 +209,10 @@ class _HrvBarChartState extends State<_HrvBarChart> { |
|
|
|
BarChartData(
|
|
|
|
minY: 0,
|
|
|
|
maxY: 110,
|
|
|
|
alignment: BarChartAlignment.spaceAround,
|
|
|
|
alignment: widget.isMonth
|
|
|
|
? BarChartAlignment.spaceAround
|
|
|
|
: BarChartAlignment.center,
|
|
|
|
groupsSpace: widget.isMonth ? 16 : _weekBarGap,
|
|
|
|
barGroups: bars,
|
|
|
|
borderData: FlBorderData(show: false),
|
|
|
|
gridData: FlGridData(
|
|
...
|
...
|
@@ -302,6 +307,8 @@ class _HrvBarChartState extends State<_HrvBarChart> { |
|
|
|
child: _TrendXAxisLabels(
|
|
|
|
report: widget.report,
|
|
|
|
isMonth: widget.isMonth,
|
|
|
|
weekBarWidth: _weekBarWidth,
|
|
|
|
weekBarGap: _weekBarGap,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
...
|
...
|
@@ -369,6 +376,15 @@ class _HrvBarChartState extends State<_HrvBarChart> { |
|
|
|
final plotStart = _plotLeft;
|
|
|
|
final plotWidth = width - plotStart - _plotRight;
|
|
|
|
if (plotWidth <= 0) return null;
|
|
|
|
if (!widget.isMonth) {
|
|
|
|
final groupCount = widget.report.days.length;
|
|
|
|
final chartWidth =
|
|
|
|
groupCount * _weekBarWidth + (groupCount - 1) * _weekBarGap;
|
|
|
|
final chartLeft = plotStart + (plotWidth - chartWidth) / 2;
|
|
|
|
return chartLeft +
|
|
|
|
_weekBarWidth / 2 +
|
|
|
|
index * (_weekBarWidth + _weekBarGap);
|
|
|
|
}
|
|
|
|
return plotStart + plotWidth * ((index + 0.5) / widget.report.days.length);
|
|
|
|
}
|
|
|
|
}
|
|
...
|
...
|
@@ -397,6 +413,7 @@ class _TrendMetric extends StatelessWidget { |
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final difference = hasData ? currentValue - (previousValue ?? 0) : null;
|
|
|
|
final isGoodChange = _isGoodChange(difference);
|
|
|
|
final comparison = hasData
|
|
|
|
? isMonth
|
|
|
|
? _monthComparisonText(context, difference!)
|
|
...
|
...
|
@@ -427,12 +444,12 @@ class _TrendMetric extends StatelessWidget { |
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
if (difference != null && difference != 0) ...[
|
|
|
|
Icon(
|
|
|
|
difference > 0
|
|
|
|
? Icons.keyboard_arrow_up_rounded
|
|
|
|
: Icons.keyboard_arrow_down_rounded,
|
|
|
|
size: 13,
|
|
|
|
color: comparisonColor,
|
|
|
|
Image.asset(
|
|
|
|
isGoodChange
|
|
|
|
? R.assetsImagesHealthTrendUp
|
|
|
|
: R.assetsImagesHealthTrendDown,
|
|
|
|
width: 12,
|
|
|
|
height: 12,
|
|
|
|
),
|
|
|
|
const SizedBox(width: 1),
|
|
|
|
],
|
|
...
|
...
|
@@ -481,11 +498,17 @@ class _TrendMetric extends StatelessWidget { |
|
|
|
if (difference == null || difference == 0) {
|
|
|
|
return const Color(0xFFB0B0B6);
|
|
|
|
}
|
|
|
|
final isGoodChange = switch (metricType) {
|
|
|
|
return _isGoodChange(difference)
|
|
|
|
? const Color(0xFF3BD49D)
|
|
|
|
: const Color(0xFFFF5279);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _isGoodChange(int? difference) {
|
|
|
|
if (difference == null || difference == 0) return false;
|
|
|
|
return switch (metricType) {
|
|
|
|
_TrendMetricType.relaxed => difference > 0,
|
|
|
|
_TrendMetricType.stressed => difference < 0,
|
|
|
|
};
|
|
|
|
return isGoodChange ? const Color(0xFF3BD49D) : const Color(0xFFFF5279);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -530,10 +553,14 @@ class _TrendXAxisLabels extends StatelessWidget { |
|
|
|
const _TrendXAxisLabels({
|
|
|
|
required this.report,
|
|
|
|
required this.isMonth,
|
|
|
|
required this.weekBarWidth,
|
|
|
|
required this.weekBarGap,
|
|
|
|
});
|
|
|
|
|
|
|
|
final HrvPeriodReport report;
|
|
|
|
final bool isMonth;
|
|
|
|
final double weekBarWidth;
|
|
|
|
final double weekBarGap;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
...
|
...
|
@@ -563,9 +590,12 @@ class _TrendXAxisLabels extends StatelessWidget { |
|
|
|
for (final label in labels)
|
|
|
|
if (label.index < groupCount)
|
|
|
|
Positioned(
|
|
|
|
left: constraints.maxWidth *
|
|
|
|
((label.index + 0.5) / groupCount) -
|
|
|
|
labelWidth / 2,
|
|
|
|
left: _labelLeft(
|
|
|
|
constraints.maxWidth,
|
|
|
|
groupCount,
|
|
|
|
label.index,
|
|
|
|
labelWidth,
|
|
|
|
),
|
|
|
|
top: 5,
|
|
|
|
width: labelWidth,
|
|
|
|
child: Text(
|
|
...
|
...
|
@@ -583,6 +613,24 @@ class _TrendXAxisLabels extends StatelessWidget { |
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
double _labelLeft(
|
|
|
|
double width,
|
|
|
|
int groupCount,
|
|
|
|
int index,
|
|
|
|
double labelWidth,
|
|
|
|
) {
|
|
|
|
if (isMonth) {
|
|
|
|
return width * ((index + 0.5) / groupCount) - labelWidth / 2;
|
|
|
|
}
|
|
|
|
final chartWidth =
|
|
|
|
groupCount * weekBarWidth + (groupCount - 1) * weekBarGap;
|
|
|
|
final chartLeft = (width - chartWidth) / 2;
|
|
|
|
return chartLeft +
|
|
|
|
weekBarWidth / 2 +
|
|
|
|
index * (weekBarWidth + weekBarGap) -
|
|
|
|
labelWidth / 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DistributionCard extends StatelessWidget {
|
|
...
|
...
|
@@ -737,32 +785,167 @@ class _DistributionBar extends StatelessWidget { |
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Container(
|
|
|
|
width: 33,
|
|
|
|
height: 180,
|
|
|
|
clipBehavior: Clip.antiAlias,
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: const Color(0xFFF3F3F3),
|
|
|
|
borderRadius: BorderRadius.circular(7)),
|
|
|
|
child: report.hasData
|
|
|
|
? Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
|
|
children: [
|
|
|
|
for (final level in HrvStressLevel.values)
|
|
|
|
if (report.countFor(level) > 0)
|
|
|
|
Expanded(
|
|
|
|
flex: report.countFor(level),
|
|
|
|
child: Container(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: Color(level.colorValue),
|
|
|
|
border: Border.all(color: Colors.white, width: .5),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
const radius = 7.0;
|
|
|
|
final segments = [
|
|
|
|
for (final level in HrvStressLevel.values)
|
|
|
|
if (report.countFor(level) > 0)
|
|
|
|
_DistributionBarSegment(
|
|
|
|
color: Color(level.colorValue),
|
|
|
|
flex: report.countFor(level),
|
|
|
|
),
|
|
|
|
];
|
|
|
|
final hasSegments = report.hasData && segments.isNotEmpty;
|
|
|
|
final height = hasSegments
|
|
|
|
? _DistributionBarPainter.visibleHeightFor(segments)
|
|
|
|
: 180.0;
|
|
|
|
final paintSize = Size(44.w, height);
|
|
|
|
|
|
|
|
return SizedBox.fromSize(
|
|
|
|
size: const Size(33, 180),
|
|
|
|
child: OverflowBox(
|
|
|
|
alignment: Alignment.bottomLeft,
|
|
|
|
minWidth: paintSize.width,
|
|
|
|
maxWidth: paintSize.width,
|
|
|
|
minHeight: paintSize.height,
|
|
|
|
maxHeight: paintSize.height,
|
|
|
|
child: Transform.translate(
|
|
|
|
offset: const Offset(0, -12),
|
|
|
|
child: SizedBox.fromSize(
|
|
|
|
size: paintSize,
|
|
|
|
child: CustomPaint(
|
|
|
|
painter: _DistributionBarPainter(
|
|
|
|
segments: hasSegments ? segments : const [],
|
|
|
|
backgroundColor: const Color(0xFFF3F3F3),
|
|
|
|
radius: radius,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DistributionBarSegment {
|
|
|
|
const _DistributionBarSegment({required this.color, required this.flex});
|
|
|
|
|
|
|
|
final Color color;
|
|
|
|
final int flex;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DistributionBarPainter extends CustomPainter {
|
|
|
|
const _DistributionBarPainter({
|
|
|
|
required this.segments,
|
|
|
|
required this.backgroundColor,
|
|
|
|
required this.radius,
|
|
|
|
});
|
|
|
|
|
|
|
|
final List<_DistributionBarSegment> segments;
|
|
|
|
final Color backgroundColor;
|
|
|
|
final double radius;
|
|
|
|
static const maxVisibleHeight = 220.0;
|
|
|
|
static const _borderWidth = 1.0;
|
|
|
|
static const _overlapHeight = 16.0;
|
|
|
|
static const _weekDayCount = 7;
|
|
|
|
|
|
|
|
static double visibleHeightFor(List<_DistributionBarSegment> segments) {
|
|
|
|
if (segments.isEmpty) return 0;
|
|
|
|
final totalFlex =
|
|
|
|
segments.fold<int>(0, (sum, segment) => sum + segment.flex);
|
|
|
|
return maxVisibleHeight * totalFlex / _weekDayCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
final clip = RRect.fromRectAndRadius(
|
|
|
|
Offset.zero & size,
|
|
|
|
Radius.circular(radius),
|
|
|
|
);
|
|
|
|
canvas.save();
|
|
|
|
canvas.clipRRect(clip);
|
|
|
|
|
|
|
|
final total = segments.fold<int>(0, (sum, segment) => sum + segment.flex);
|
|
|
|
if (total == 0) {
|
|
|
|
canvas.drawRRect(
|
|
|
|
clip,
|
|
|
|
Paint()
|
|
|
|
..color = backgroundColor
|
|
|
|
..style = PaintingStyle.fill,
|
|
|
|
);
|
|
|
|
canvas.restore();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
final visibleHeights = _visibleSegmentHeightsFor(size.height, segments);
|
|
|
|
var bottom = 0.0;
|
|
|
|
for (var index = 0; index < segments.length; index++) {
|
|
|
|
final segment = segments[index];
|
|
|
|
final isBottomSegment = index == segments.length - 1;
|
|
|
|
final drawTop = index == 0 ? 0.0 : bottom - _overlapHeight;
|
|
|
|
final drawBottom = drawTop +
|
|
|
|
visibleHeights[index] +
|
|
|
|
(isBottomSegment ? 0 : _overlapHeight);
|
|
|
|
_drawLayeredSegment(
|
|
|
|
canvas,
|
|
|
|
Rect.fromLTRB(0, drawTop, size.width, drawBottom),
|
|
|
|
color: segment.color,
|
|
|
|
radius: radius,
|
|
|
|
);
|
|
|
|
bottom = drawBottom;
|
|
|
|
}
|
|
|
|
canvas.restore();
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<double> _visibleSegmentHeightsFor(
|
|
|
|
double visibleHeight,
|
|
|
|
List<_DistributionBarSegment> segments,
|
|
|
|
) {
|
|
|
|
final totalFlex =
|
|
|
|
segments.fold<int>(0, (sum, segment) => sum + segment.flex);
|
|
|
|
final dayHeight = visibleHeight / totalFlex;
|
|
|
|
return [
|
|
|
|
for (final segment in segments) segment.flex * dayHeight,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
void _drawLayeredSegment(
|
|
|
|
Canvas canvas,
|
|
|
|
Rect rect, {
|
|
|
|
required Color color,
|
|
|
|
required double radius,
|
|
|
|
}) {
|
|
|
|
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(radius));
|
|
|
|
canvas.drawRRect(
|
|
|
|
rrect,
|
|
|
|
Paint()
|
|
|
|
..color = color
|
|
|
|
..style = PaintingStyle.fill,
|
|
|
|
);
|
|
|
|
canvas.drawRRect(
|
|
|
|
rrect.deflate(_borderWidth / 2),
|
|
|
|
Paint()
|
|
|
|
..color = Colors.white
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
..strokeWidth = _borderWidth,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool shouldRepaint(covariant _DistributionBarPainter oldDelegate) {
|
|
|
|
if (backgroundColor != oldDelegate.backgroundColor ||
|
|
|
|
radius != oldDelegate.radius ||
|
|
|
|
segments.length != oldDelegate.segments.length) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
for (var i = 0; i < segments.length; i++) {
|
|
|
|
final segment = segments[i];
|
|
|
|
final oldSegment = oldDelegate.segments[i];
|
|
|
|
if (segment.color != oldSegment.color ||
|
|
|
|
segment.flex != oldSegment.flex) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
...
|
...
|
|