Commit 855d891
Changed files (4)
app
lib
features
statistics
test
features
app/lib/features/statistics/value_graph.dart
@@ -107,6 +107,7 @@ class _ValueGraphPainter extends CustomPainter {
final graphBorderLinesPaint = Paint()
..color = brightness == Brightness.dark ? Colors.white : Colors.black
+ ..strokeWidth = 1.0
..strokeCap = ui.StrokeCap.round;
final graphDecoLinesPaint = Paint()
..color = brightness == Brightness.dark ? Colors.white60 : Colors.black45
@@ -124,78 +125,53 @@ class _ValueGraphPainter extends CustomPainter {
final double drawHeight = size.height - _kBottomLegendHeight;
final leftLabelHeight = labelTextHeight + 4.0; // padding
+ final leftLabelWidth = _kLeftLegendWidth - 6.0 - 2.0;
final leftLegendLabelCount = drawHeight / leftLabelHeight;
// draw horizontal decorations
for (int i = 0; i < leftLegendLabelCount; i += 2) {
final h = (size.height - _kBottomLegendHeight) - i * leftLabelHeight;
canvas.drawLine(
- Offset(_kLeftLegendWidth, h),
+ Offset(_kLeftLegendWidth - 5.0, h),
Offset(size.width, h),
graphDecoLinesPaint,
);
- canvas.drawLine(
- Offset(_kLeftLegendWidth, h),
- Offset(_kLeftLegendWidth - 5.0, h),
- graphBorderLinesPaint,
- );
final labelY = minY + ((maxY - minY) / leftLegendLabelCount) * i;
- final paragraphBuilder = ui.ParagraphBuilder(labelStyle.getParagraphStyle(
- textAlign: ui.TextAlign.end,
- ))
- ..pushStyle(labelStyle.getTextStyle())
- ..addText(labelY.round().toString());
- final paragraph = paragraphBuilder.build()
- ..layout(ui.ParagraphConstraints(width: _kLeftLegendWidth - 6.0 - 2.0));
+
+ final paragraph = _paragraph(ui.TextAlign.end, labelY.round().toString(), leftLabelWidth);
canvas.drawParagraph(paragraph, ui.Offset(2.0, h - (leftLabelHeight / 2)));
}
}();
- // calculate vertical decoration positions
+ // calculate horizontal decoration positions
final double drawWidth = size.width - _kLeftLegendWidth;
- final bottomLabelHeight = labelTextHeight + 4.0;
int bottomLabelCount = 20;
Duration? stepDuration;
late DateFormat format;
while (stepDuration == null && bottomLabelCount > 4) {
- final duration = range.duration ~/ bottomLabelCount;
- format = (){
- switch (duration) {
- case < const Duration(hours: 4):
- return DateFormat('H:m EEE');
- case < const Duration(days: 1):
- return DateFormat('EEE');
- case < const Duration(days: 5):
- return DateFormat('dd');
- case < const Duration(days: 30):
- return DateFormat('MMM, dd');
- case < const Duration(days: 30*6):
- return DateFormat('MMM yyyy');
- default:
- return DateFormat('yyyy');
- }
- }();
- final paragraphBuilder = ui.ParagraphBuilder(labelStyle.getParagraphStyle(
- textAlign: ui.TextAlign.end
- ))..pushStyle(labelStyle.getTextStyle())
- ..addText(format.format(range.start));
- final paragraph = paragraphBuilder.build()
- ..layout(ui.ParagraphConstraints(width: drawWidth));
- // Not 100% accurate but avoids expensive text layout
+ stepDuration = range.duration ~/ bottomLabelCount;
+ format = switch(stepDuration) {
+ < const Duration(hours: 4) => DateFormat('H:m EEE'),
+ < const Duration(days: 1) => DateFormat('EEE'),
+ < const Duration(days: 5) => DateFormat('dd'),
+ < const Duration(days: 30) => DateFormat('MMM, dd'),
+ < const Duration(days: 30*6) => DateFormat('MMM yyyy'),
+ _ => DateFormat('yyyy'),
+ };
+ // Approximate total width needed with duration
+ final paragraph = _paragraph(ui.TextAlign.center, format.format(range.start), drawWidth);
final totalWidthOccupiedByLabels = bottomLabelCount * paragraph.minIntrinsicWidth;
- if (totalWidthOccupiedByLabels < drawWidth) {
- stepDuration = duration;
- } else {
+ if (totalWidthOccupiedByLabels > drawWidth) {
+ stepDuration = null;
bottomLabelCount--;
}
}
- if (stepDuration == null) {
- // graph to small to draw labels
- return;
- }
+ // -> Graph to small to draw labels
+ if (stepDuration == null) return;
- // draw vertical decorations
+ // draw horizontal decorations
+ final labelWidth = drawWidth / bottomLabelCount + 8.0;
for (int i = 0; i < bottomLabelCount; i += 2) {
final x = _kLeftLegendWidth + i * (drawWidth / bottomLabelCount);
canvas.drawLine(
@@ -203,13 +179,12 @@ class _ValueGraphPainter extends CustomPainter {
Offset(x, size.height - _kBottomLegendHeight + 4.0),
graphDecoLinesPaint,
);
- final paragraphBuilder = ui.ParagraphBuilder(labelStyle.getParagraphStyle(
- textAlign: ui.TextAlign.center
- ))..pushStyle(labelStyle.getTextStyle())
- ..addText(format.format(range.start.add(stepDuration! * i)));
- final paragraph = paragraphBuilder.build()
- ..layout(ui.ParagraphConstraints(width: drawWidth / bottomLabelCount + 8.0));
- canvas.drawParagraph(paragraph, ui.Offset(x - ((drawWidth / bottomLabelCount + 8.0) / 2), size.height - _kBottomLegendHeight + (bottomLabelHeight / 2)));
+ final text = format.format(range.start.add(stepDuration * i));
+ final paragraph = _paragraph(ui.TextAlign.center, text, labelWidth);
+ canvas.drawParagraph(paragraph, ui.Offset(
+ x - (labelWidth / 2),
+ size.height - _kBottomLegendHeight + (labelTextHeight / 2),
+ ));
}
}
@@ -276,7 +251,6 @@ class _ValueGraphPainter extends CustomPainter {
final double meanX = xValues.sum / data.length;
final double meanY = yValues.sum / data.length;
- // Calculate slope
final slopeTop = data.fold(0.0, (double last, (DateTime, double) e) {
final xErr = e.$1.millisecondsSinceEpoch - meanX;
final yErr = e.$2 - meanY;
@@ -287,15 +261,12 @@ class _ValueGraphPainter extends CustomPainter {
return last + xErr * xErr;
});
final slope = slopeTop / slopeBtm;
-
- // Calculate where the function intercepts the Y axis
final yIntercept = meanY - slope * meanX;
// Convert data points to canvas coordinates
final minX = xValues.reduce(math.min);
final maxX = xValues.reduce(math.max);
- // Draw the regression line from the first point to the last point
final start = ui.Offset(
_kLeftLegendWidth,
_transformY(size, slope * minX + yIntercept, minY, maxY),
@@ -317,7 +288,7 @@ class _ValueGraphPainter extends CustomPainter {
final path = Path();
double x = _kLeftLegendWidth;
bool drawNext = true;
- while (x < size.width) { // Dotted
+ while (x < size.width) { // Create dotted line
if (drawNext) {
final newX = x + 10;
path.moveTo(x, y);
@@ -451,6 +422,19 @@ class _ValueGraphPainter extends CustomPainter {
final offset = x.millisecondsSinceEpoch - range.start.millisecondsSinceEpoch;
return _kLeftLegendWidth + offset * factorX;
}
+
+ /// Create and align a paragraph builder
+ ui.Paragraph _paragraph(ui.TextAlign textAlign, String text, double width) {
+ final paragraphBuilder = ui.ParagraphBuilder(
+ labelStyle.getParagraphStyle(textAlign: textAlign),
+ )
+ ..pushStyle(labelStyle.getTextStyle())
+ ..addText(text);
+
+ final paragraph = paragraphBuilder.build();
+ paragraph.layout(ui.ParagraphConstraints(width: width));
+ return paragraph;
+ }
}
/// Create graph data from a list of blood pressure records.
app/test/features/statistics/full_graph-years.png
Binary file
app/test/features/statistics/value_graph_test.dart
@@ -100,7 +100,7 @@ void main() {
final localizations = await AppLocalizations.delegate.load(const Locale('en'));
expect(find.text(localizations.errNotEnoughDataToGraph), findsNothing);
- expect(find.byType(BloodPressureValueGraph), matchesGoldenFile('full_graph-years.png'));
+ await expectLater(find.byType(BloodPressureValueGraph), matchesGoldenFile('full_graph-years.png'));
});
}
.gitignore
@@ -1,2 +1,3 @@
/.idea/
-/fastlane/metadata/android/en-US/images/phoneScreenshots/resizeAll.sh
\ No newline at end of file
+/fastlane/metadata/android/en-US/images/phoneScreenshots/resizeAll.sh
+**/failures/*.png
\ No newline at end of file