Commit c6bf1c5

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-08-15 18:04:50
implement graph decorations
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 526c6e5
Changed files (1)
app
lib
features
statistics
app/lib/features/statistics/value_graph.dart
@@ -0,0 +1,195 @@
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+import 'package:health_data_store/health_data_store.dart';
+import 'package:intl/intl.dart';
+
+void main() => runApp(Tmp());
+
+class Tmp extends StatelessWidget {
+  const Tmp({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      theme: ThemeData.dark(),
+      home: Scaffold(
+        appBar: AppBar(),
+        body: ClipRect(
+          child: SizedBox(
+            height: 250,
+            width: 600,
+            child: BloodPressureValueGraph(
+              records: [],
+              range: DateTimeRange(
+                start: DateTime.now().subtract(Duration(days: 265*3)),
+                end: DateTime.now(),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+
+class BloodPressureValueGraph extends StatelessWidget {
+  const BloodPressureValueGraph({super.key, required this.records, required this.range});
+
+  /// Data that make up graph lines.
+  ///
+  /// Data outside [range] will be ignored.
+  final List<BloodPressureRecord> records;
+
+  final DateTimeRange range;
+
+  @override
+  Widget build(BuildContext context) => CustomPaint(
+      painter: _ValueGraphPainter(
+        brightness: Brightness.dark,
+        labelStyle: TextStyle(),
+        records: records,
+        range: range,
+      ),
+    );
+}
+
+class _ValueGraphPainter extends CustomPainter {
+  _ValueGraphPainter({super.repaint,
+    required this.brightness,
+    required this.labelStyle,
+    required this.records,
+    required this.range,
+  });
+
+  final Brightness brightness;
+
+  final TextStyle labelStyle;
+
+  final List<BloodPressureRecord> records;
+
+  final DateTimeRange range;
+
+  void _paintDecorations(Canvas canvas, Size size, DateTimeRange range, double minY, double maxY) {
+    const leftLegendWidth = 35.0;
+    const bottomLegendHeight = 50.0;
+    final graphBorderLinesPaint = Paint()
+      ..color = brightness == Brightness.dark ? Colors.white : Colors.black
+      ..strokeCap = ui.StrokeCap.round;
+    final graphDecoLinesPaint = Paint()
+      ..color = brightness == Brightness.dark ? Colors.white60 : Colors.black45
+      ..strokeCap = ui.StrokeCap.round
+      ..strokeJoin = ui.StrokeJoin.round;
+
+    assert(size.width > leftLegendWidth && size.height > bottomLegendHeight);
+
+    // draw border
+    final bottomLeftOfGraph = Offset(leftLegendWidth, size.height - bottomLegendHeight);
+    canvas.drawLine(bottomLeftOfGraph, Offset(size.width, size.height - bottomLegendHeight), graphBorderLinesPaint);
+    canvas.drawLine(bottomLeftOfGraph, Offset(leftLegendWidth, 0.0), graphBorderLinesPaint);
+
+    final labelTextHeight = ((labelStyle.height ?? 1.0) * (labelStyle.fontSize ?? 14.0));
+    (){
+    // calculate horizontal decoration positions
+    final double drawHeight = size.height - bottomLegendHeight;
+
+    final leftLabelHeight = labelTextHeight + 4.0; // padding
+    final leftLegendLabelCount = drawHeight / leftLabelHeight;
+
+    // draw horizontal decorations
+    for (int i = 0; i < leftLegendLabelCount; i += 2) {
+      final h = (size.height - bottomLegendHeight) - i * leftLabelHeight;
+      canvas.drawLine(
+        Offset(leftLegendWidth, h),
+        Offset(size.width, h),
+        graphDecoLinesPaint,
+      );
+      canvas.drawLine(
+        Offset(leftLegendWidth, h),
+        Offset(leftLegendWidth - 5.0, h),
+        graphBorderLinesPaint,
+      );
+      final label = minY + ((maxY - minY) / leftLegendLabelCount) * i;
+      final paragraphBuilder = ui.ParagraphBuilder(labelStyle.getParagraphStyle(
+          textAlign: ui.TextAlign.end
+      ))
+        ..pushStyle(labelStyle.getTextStyle())
+        ..addText(label.round().toString());
+      final paragraph = paragraphBuilder.build()
+        ..layout(ui.ParagraphConstraints(width: leftLegendWidth - 6.0 - 2.0));
+      canvas.drawParagraph(paragraph, ui.Offset(2.0, h - (leftLabelHeight / 2)));
+    }
+    }();
+
+    // calculate vertical decoration positions
+    final double drawWidth = size.width - leftLegendWidth;
+    final bottomLabelHeight = labelTextHeight + 4.0;
+    int bottomLabelCount = 20;
+    Duration? stepDuration;
+    late DateFormat format;
+    while (stepDuration == null && bottomLabelCount > 4) {
+      final duration = range.duration ~/ bottomLabelCount;
+      print(duration);
+      format = (){
+        switch (duration) {
+          case < const Duration(hours: 8):
+            return DateFormat('H:m');
+          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
+      final totalWidthOccupiedByLabels = bottomLabelCount * paragraph.minIntrinsicWidth;
+      if (totalWidthOccupiedByLabels < drawWidth) {
+        stepDuration = duration;
+      } else {
+        bottomLabelCount--;
+      }
+    }
+
+    // draw vertical decorations
+    for (int i = 0; i < bottomLabelCount; i += 2) {
+      final x = leftLegendWidth + i * (drawWidth / bottomLabelCount);
+      canvas.drawLine(
+        Offset(x, 0),
+        Offset(x, size.height - bottomLegendHeight + 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 - bottomLegendHeight + (bottomLabelHeight / 2)));
+    }
+  }
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    _paintDecorations(canvas, size, range, 40, 135);
+    // TODO: implement paint
+  }
+
+  @override
+  bool shouldRepaint(covariant CustomPainter oldDelegate) {
+    // TODO: implement shouldRepaint
+    return true;
+  }
+
+}