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.f,
|
|
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',
|
|
// ),
|
|
// ];
|