diff --git a/lib/core/common_models/data_points.dart b/lib/core/common_models/data_points.dart new file mode 100644 index 0000000..af7c473 --- /dev/null +++ b/lib/core/common_models/data_points.dart @@ -0,0 +1,14 @@ + + +///class used to provide value for the [DynamicResultChart] to plot the values +class DataPoint { + ///values that is displayed on the graph and dot is plotted on this + final double value; + ///label shown on the bottom of the graph + String label; + + DataPoint( + {required this.value, + required this.label, + }); +} diff --git a/lib/core/common_models/threshold.dart b/lib/core/common_models/threshold.dart new file mode 100644 index 0000000..c8e897c --- /dev/null +++ b/lib/core/common_models/threshold.dart @@ -0,0 +1,21 @@ +import 'dart:ui' show Color; + +class ThresholdRange { + final String label; + final double value; + final Color color; + final Color lineColor; + final String? actualValue; + + ThresholdRange( + {required this.label, + required this.value, + required this.color, + required this.lineColor, + this.actualValue}); + + @override + String toString() { + return 'ThresholdRange(label: $label, value: $value, color: ${color.value.toRadixString(16)}, lineColor: ${lineColor.value.toRadixString(16)})'; + } +} \ No newline at end of file diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 0c7da58..2627a12 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -62,4 +62,7 @@ static const Color greyLightColor = Color(0xFFEFEFF0); static const Color bottomNAVBorder = Color(0xFFEEEEEE); static const Color quickLoginColor = Color(0xFF666666); + +static const Color tooltipTextColor = Color(0xFF414D55); +static const Color graphGridColor = Color(0x4D18C273); } diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart new file mode 100644 index 0000000..d66d281 --- /dev/null +++ b/lib/widgets/graph/custom_graph.dart @@ -0,0 +1,287 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +/// +/// CustomGraph(dataPoints: sampleData, scrollDirection: Axis.horizontal,height: 200,maxY: 100, maxX:2.5, +/// leftLabelFormatter: (value){ +/// Widget buildLabel(String label) { +/// return Padding( +/// padding: const EdgeInsets.only(right: 8), +/// child: Text( +/// label, +/// style: TextStyle( +/// fontSize: 8.fSize, color: AppColors.textColor, +/// fontFamily: +/// FontUtils.getFontFamilyForLanguage(false) +/// ), +/// textAlign: TextAlign.right, +/// ), +/// ); +/// } +/// switch (value.toInt()) { +/// +/// case 20: +/// return buildLabel("Critical Low"); +/// case 40: +/// return buildLabel("Low"); +/// case 60: +/// return buildLabel("Normal"); +/// case 80: +/// return buildLabel("High"); +/// case 100: +/// return buildLabel("Critical High"); +/// } +/// return const SizedBox.shrink(); +/// }, +/// +/// ), +class CustomGraph extends StatelessWidget { + final List dataPoints; + final double? width; + final double height; + final double? maxY; + final double? maxX; + final Color spotColor; + final Color graphColor; + final Color graphShadowColor; + final Color graphGridColor; + final Color bottomLabelColor; + final double? bottomLabelSize; + final FontWeight? bottomLabelFontWeight; + + ///creates the left label and provide it to the chart as it will be used by other part of the application so the label will be different for every chart + final Widget Function(double value) leftLabelFormatter; + + final Axis scrollDirection; + final bool showBottomTitleDates; + final bool isFullScreeGraph; + + const CustomGraph({ + super.key, + required this.dataPoints, + required this.leftLabelFormatter, + this.width, + required this.scrollDirection, + required this.height, + this.maxY, + this.maxX, + this.showBottomTitleDates = true, + this.isFullScreeGraph = false, + this.spotColor = AppColors.bgGreenColor, + this.graphColor = AppColors.bgGreenColor, + this.graphShadowColor = AppColors.graphGridColor, + this.graphGridColor = AppColors.graphGridColor, + this.bottomLabelColor = AppColors.textColor, + this.bottomLabelFontWeight = FontWeight.w500, + this.bottomLabelSize, + }); + + @override + Widget build(BuildContext context) { + // var maxY = 0.0; + double interval = 20; + if ((maxY ?? 0) > 10 && (maxY ?? 0) <= 20) { + interval = 2; + } else if ((maxY ?? 0) > 5 && (maxY ?? 0) <= 10) { + interval = 1; + } else if ((maxY ?? 0) >= 0 && (maxY ?? 0) <= 5) { + interval = .4; + } + return Material( + color: Colors.white, + child: SizedBox( + width: width, + height: height, + child: Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8), + child: LineChart( + LineChartData( + minY: 0, + maxY: + ((maxY?.ceilToDouble() ?? 0.0) + interval).floorToDouble(), + // minX: dataPoints.first.labelValue - 1, + maxX: maxX, + minX: -0.2, + lineTouchData: LineTouchData( + getTouchLineEnd: (_, __) => 0, + getTouchedSpotIndicator: (barData, indicators) { + // Only show custom marker for touched spot + return indicators.map((int index) { + return TouchedSpotIndicatorData( + FlLine(color: Colors.transparent), + FlDotData( + show: true, + getDotPainter: (spot, percent, barData, idx) { + return FlDotCirclePainter( + radius: 8, + color: spotColor, + strokeWidth: 2, + strokeColor: Colors.white, + ); + }, + ), + ); + }).toList(); + }, + enabled: true, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (_) => Colors.white, + getTooltipItems: (touchedSpots) { + if (touchedSpots.isEmpty) return []; + // Only show tooltip for the first touched spot, hide others + return touchedSpots.map((spot) { + if (spot == touchedSpots.first) { + final dataPoint = dataPoints[spot.x.toInt()]; + + return LineTooltipItem( + // '${dataPoint.label} ${spot.y.toStringAsFixed(2)}', + '${dataPoint.value} ', + TextStyle( + color: Colors.black, + fontSize: 12.fSize, + fontWeight: FontWeight.w500), + ); + } + return null; // hides the rest + }).toList(); + }, + ), + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 77, + interval: .1, // Let fl_chart handle it + getTitlesWidget: (value, _) { + return leftLabelFormatter(value); + }, + ), + ), + bottomTitles: AxisTitles( + axisNameSize: 60, + sideTitles: SideTitles( + showTitles: showBottomTitleDates, + reservedSize: 50, + getTitlesWidget: (value, _) { + if ((value.toDouble() >= 0) && + (value.toDouble() < (maxX ?? dataPoints.length))) { + var label = dataPoints[value.toInt()].label; + + return buildBottomLabel(label); + } + return const SizedBox.shrink(); + }, + interval: 1, // ensures 1:1 mapping with spots + ), + ), + topTitles: AxisTitles(), + rightTitles: AxisTitles(), + ), + borderData: FlBorderData( + show: true, + border: const Border( + bottom: BorderSide.none, + left: BorderSide(color: Colors.grey, width: .5), + right: BorderSide.none, + top: BorderSide.none, + ), + ), + lineBarsData: _buildColoredLineSegments(dataPoints), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 20, + checkToShowHorizontalLine: (value) => + value >= 0 && value <= 100, + getDrawingHorizontalLine: (value) { + return FlLine( + color: AppColors.graphGridColor, + strokeWidth: 1, + dashArray: [5, 5], + ); + }, + ), + ), + ), + ), + )); + } + + List _buildColoredLineSegments(List dataPoints) { + final List allSpots = dataPoints.asMap().entries.map((entry) { + return FlSpot(entry.key.toDouble(), entry.value.value); + }).toList(); + + var data = [ + LineChartBarData( + spots: allSpots, + isCurved: true, + isStrokeCapRound: true, + isStrokeJoinRound: true, + barWidth: 4, + gradient: LinearGradient( + colors: [graphColor, graphColor], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + dotData: FlDotData( + show: false, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + graphShadowColor, + Colors.transparent, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ) + ]; + + return data; + } + + // Widget buildLabel(String label) { + // return Padding( + // padding: const EdgeInsets.only(right: 8), + // child: Text( + // label, + // style: TextStyle( + // fontSize: leftLabelSize ?? 8.fSize, color: leftLabelColor), + // textAlign: TextAlign.right, + // ), + // ); + // } + + Widget buildBottomLabel(String label) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + label, + style: TextStyle( + fontSize: bottomLabelSize ?? 8.fSize, color: bottomLabelColor), + ), + ); + } +} + +final List sampleData = [ + DataPoint( + value: 20, + label: 'Jan 2024', + ), + DataPoint( + value: 36, + label: 'Feb 2024', + ), + DataPoint( + value: 80, + label: 'This result', + ), +]; diff --git a/pubspec.yaml b/pubspec.yaml index ea41d43..0466933 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,7 @@ dependencies: uuid: ^4.5.1 health: ^13.1.3 # health: 12.0.1 - fl_chart: ^1.0.0 + fl_chart: ^1.1.1 geolocator: ^14.0.2 dropdown_search: ^6.0.2 google_maps_flutter: ^2.12.3