You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			279 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			279 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Dart
		
	
| 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';
 | |
| /// A customizable line graph widget using `fl_chart`.
 | |
| ///
 | |
| /// Displays a line chart with configurable axis labels, colors, and data points.
 | |
| /// Useful for visualizing time series or other sequential data.
 | |
| ///
 | |
| /// **Parameters:**
 | |
| /// - [dataPoints]: List of `DataPoint` objects to plot.
 | |
| /// - [leftLabelFormatter]: Function to build left axis labels.
 | |
| /// - [bottomLabelFormatter]: Function to build bottom axis labels.
 | |
| /// - [width]: Optional width of the chart.
 | |
| /// - [height]: Required height of the chart.
 | |
| /// - [maxY], [maxX], [minX]: Axis bounds.
 | |
| /// - [spotColor]: Color of the touched spot marker.
 | |
| /// - [graphColor]: Color of the line.
 | |
| /// - [graphShadowColor]: Color of the area below the line.
 | |
| /// - [graphGridColor]: Color of the grid lines.
 | |
| /// - [bottomLabelColor]: Color of bottom axis labels.
 | |
| /// - [bottomLabelSize]: Font size for bottom axis labels.
 | |
| /// - [bottomLabelFontWeight]: Font weight for bottom axis labels.
 | |
| /// - [leftLabelInterval]: Interval between left axis labels.
 | |
| /// - [leftLabelReservedSize]: Reserved space for left axis labels.
 | |
| /// - [scrollDirection]: Axis direction for scrolling.
 | |
| /// - [showBottomTitleDates]: Whether to show bottom axis labels.
 | |
| /// - [isFullScreeGraph]: Whether the graph is fullscreen.
 | |
| /// - [makeGraphBasedOnActualValue]: Use `actualValue` for plotting.
 | |
| ///
 | |
| /// Example usage:
 | |
| /// ```dart
 | |
| /// CustomGraph(
 | |
| ///   dataPoints: sampleData,
 | |
| ///   leftLabelFormatter: (value) => ...,
 | |
| ///   bottomLabelFormatter: (value, dataPoints) => ...,
 | |
| ///   height: 200,
 | |
| ///   scrollDirection: Axis.horizontal,
 | |
| ///   maxY: 100,
 | |
| ///   maxX: 2.5,
 | |
| /// )
 | |
| class CustomGraph extends StatelessWidget {
 | |
|   final List<DataPoint> dataPoints;
 | |
|   final double? width;
 | |
|   final double height;
 | |
|   final double? maxY;
 | |
|   final double? maxX;
 | |
|   final double? minX;
 | |
|   final Color spotColor;
 | |
|   final Color graphColor;
 | |
|   final Color graphShadowColor;
 | |
|   final Color graphGridColor;
 | |
|   final Color bottomLabelColor;
 | |
|   final double? bottomLabelSize;
 | |
|   final FontWeight? bottomLabelFontWeight;
 | |
|   final double? leftLabelInterval;
 | |
|   final double? leftLabelReservedSize;
 | |
| 
 | |
|   ///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) leftLabelFormatter;
 | |
|   final Widget Function(double , List<DataPoint>) bottomLabelFormatter;
 | |
| 
 | |
| 
 | |
|   final Axis scrollDirection;
 | |
|   final bool showBottomTitleDates;
 | |
|   final bool isFullScreeGraph;
 | |
|   final bool makeGraphBasedOnActualValue;
 | |
| 
 | |
|   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,
 | |
|     this.leftLabelInterval,
 | |
|     this.leftLabelReservedSize,
 | |
|     this.makeGraphBasedOnActualValue =  false,
 | |
|     required this.bottomLabelFormatter,
 | |
|   this.minX,
 | |
|   });
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     // var maxY = 0.0;
 | |
|     double interval = 20;
 | |
| 
 | |
|     return Material(
 | |
|         color: Colors.white,
 | |
|         child: SizedBox(
 | |
|           width: width,
 | |
|           height: height,
 | |
|           child: LineChart(
 | |
|             LineChartData(
 | |
|               minY: 0,
 | |
|               // maxY: ((maxY?.ceilToDouble() ?? 0.0) + interval).floorToDouble(),
 | |
|               maxY: maxY,
 | |
|               // minX: dataPoints.first.labelValue - 1,
 | |
|               maxX: maxX,
 | |
|               minX: minX ,
 | |
|               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.actualValue} ${dataPoint.unitOfMeasurement??""} - ${dataPoint.displayTime}',
 | |
|                           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: leftLabelReservedSize??80,
 | |
|                     interval: leftLabelInterval ?? .1, // Let fl_chart handle it
 | |
|                     getTitlesWidget: (value, _) {
 | |
|                       return leftLabelFormatter(value);
 | |
|                     },
 | |
|                   ),
 | |
|                 ),
 | |
|                 bottomTitles: AxisTitles(
 | |
|                   axisNameSize: 20,
 | |
|                   sideTitles: SideTitles(
 | |
|                     showTitles: showBottomTitleDates,
 | |
|                     reservedSize: 20,
 | |
|                     getTitlesWidget: (value, _) {
 | |
|                      return bottomLabelFormatter(value, dataPoints, );
 | |
|                     },
 | |
|                     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: 40,
 | |
|                 checkToShowHorizontalLine: (value) =>
 | |
|                     value >= 0 && value <= 100,
 | |
|                 getDrawingHorizontalLine: (value) {
 | |
|                   return FlLine(
 | |
|                     color: graphGridColor,
 | |
|                     strokeWidth: 1,
 | |
|                     dashArray: [5, 5],
 | |
|                   );
 | |
|                 },
 | |
|               ),
 | |
|             ),
 | |
|           ),
 | |
|         ));
 | |
|   }
 | |
| 
 | |
|   List<LineChartBarData> _buildColoredLineSegments(List<DataPoint> dataPoints) {
 | |
|     final List<FlSpot> allSpots = dataPoints.asMap().entries.map((entry) {
 | |
|       double value = (makeGraphBasedOnActualValue)?double.tryParse(entry.value.actualValue)??0.0:entry.value.value;
 | |
|       debugPrint("the value is $value");
 | |
|       return FlSpot(entry.key.toDouble(), 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.white,
 | |
|             ],
 | |
|             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,
 | |
|   //     ),
 | |
|   //   );
 | |
|   // }
 | |
| 
 | |
| 
 | |
| }
 | |
| 
 | |
| // final List<DataPoint> sampleData = [
 | |
| //   DataPoint(
 | |
| //     value: 20,
 | |
| //     label: 'Jan 2024',
 | |
| //   ),
 | |
| //   DataPoint(
 | |
| //     value: 36,
 | |
| //     label: 'Feb 2024',
 | |
| //   ),
 | |
| //   DataPoint(
 | |
| //     value: 80,
 | |
| //     label: 'This result',
 | |
| //   ),
 | |
| // ];
 |