|
|
|
|
@ -216,6 +216,14 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _historyCard(BuildContext context, {required List<DataPoint> history}) {
|
|
|
|
|
// For blood pressure, we need both systolic and diastolic series
|
|
|
|
|
List<DataPoint>? secondaryHistory;
|
|
|
|
|
if (args.metric == VitalSignMetric.bloodPressure) {
|
|
|
|
|
secondaryHistory = _buildBloodPressureDiastolicSeries(
|
|
|
|
|
context.read<HmgServicesViewModel>().vitalSignList,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
|
|
|
|
|
color: AppColors.whiteColor,
|
|
|
|
|
@ -282,7 +290,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
if (history.isEmpty)
|
|
|
|
|
Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true)
|
|
|
|
|
else if (_isGraphVisible)
|
|
|
|
|
_buildHistoryGraph(history)
|
|
|
|
|
_buildHistoryGraph(history, secondaryHistory: secondaryHistory)
|
|
|
|
|
else
|
|
|
|
|
_buildHistoryList(context, history),
|
|
|
|
|
],
|
|
|
|
|
@ -290,31 +298,25 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildHistoryGraph(List<DataPoint> history) {
|
|
|
|
|
final minY = _minY(history);
|
|
|
|
|
final maxY = _maxY(history);
|
|
|
|
|
Widget _buildHistoryGraph(List<DataPoint> history, {List<DataPoint>? secondaryHistory}) {
|
|
|
|
|
final minY = _minY(history, secondaryHistory: secondaryHistory);
|
|
|
|
|
final maxY = _maxY(history, secondaryHistory: secondaryHistory);
|
|
|
|
|
final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title);
|
|
|
|
|
|
|
|
|
|
return CustomGraph(
|
|
|
|
|
dataPoints: history,
|
|
|
|
|
secondaryDataPoints: secondaryHistory,
|
|
|
|
|
makeGraphBasedOnActualValue: true,
|
|
|
|
|
leftLabelReservedSize: 40,
|
|
|
|
|
showGridLines: true,
|
|
|
|
|
showShadow: true,
|
|
|
|
|
leftLabelInterval: _leftInterval(history),
|
|
|
|
|
leftLabelInterval: _leftInterval(history, secondaryHistory: secondaryHistory),
|
|
|
|
|
maxY: maxY,
|
|
|
|
|
minY: minY,
|
|
|
|
|
maxX: history.length.toDouble() - .75,
|
|
|
|
|
horizontalInterval: _leftInterval(history),
|
|
|
|
|
horizontalInterval: _leftInterval(history, secondaryHistory: secondaryHistory),
|
|
|
|
|
leftLabelFormatter: (value) {
|
|
|
|
|
// Show labels at interval points
|
|
|
|
|
if (args.high != null && (value - args.high!).abs() < 0.1) {
|
|
|
|
|
return _axisLabel('High');
|
|
|
|
|
}
|
|
|
|
|
if (args.low != null && (value - args.low!).abs() < 0.1) {
|
|
|
|
|
return _axisLabel('Low');
|
|
|
|
|
}
|
|
|
|
|
// Show numeric labels at regular intervals
|
|
|
|
|
// Show only numeric labels at regular intervals
|
|
|
|
|
return _axisLabel(value.toStringAsFixed(0));
|
|
|
|
|
},
|
|
|
|
|
getDrawingHorizontalLine: (value) {
|
|
|
|
|
@ -341,6 +343,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
graphColor: AppColors.bgGreenColor,
|
|
|
|
|
secondaryGraphColor: AppColors.blueColor,
|
|
|
|
|
graphShadowColor: AppColors.lightGreenColor.withOpacity(.4),
|
|
|
|
|
graphGridColor: scheme.iconFg,
|
|
|
|
|
bottomLabelFormatter: (value, data) {
|
|
|
|
|
@ -350,7 +353,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label);
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
},
|
|
|
|
|
rangeAnnotations: _rangeAnnotations(history),
|
|
|
|
|
rangeAnnotations: _rangeAnnotations(history, secondaryHistory: secondaryHistory),
|
|
|
|
|
minX: (history.length == 1) ? null : -.2,
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
height: 180.h,
|
|
|
|
|
@ -361,6 +364,15 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
Widget _buildHistoryList(BuildContext context, List<DataPoint> history) {
|
|
|
|
|
final items = history.reversed.toList();
|
|
|
|
|
final height = items.length < 3 ? items.length * 64.0 : 180.h;
|
|
|
|
|
|
|
|
|
|
// Get diastolic values if this is blood pressure
|
|
|
|
|
List<DataPoint>? secondaryItems;
|
|
|
|
|
if (args.metric == VitalSignMetric.bloodPressure) {
|
|
|
|
|
final viewModel = context.read<HmgServicesViewModel>();
|
|
|
|
|
final secondaryHistory = _buildBloodPressureDiastolicSeries(viewModel.vitalSignList);
|
|
|
|
|
secondaryItems = secondaryHistory.reversed.toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return SizedBox(
|
|
|
|
|
height: height,
|
|
|
|
|
child: ListView.separated(
|
|
|
|
|
@ -372,13 +384,25 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
),
|
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
|
final dp = items[index];
|
|
|
|
|
|
|
|
|
|
// Build the value text based on metric type
|
|
|
|
|
String valueText;
|
|
|
|
|
if (args.metric == VitalSignMetric.bloodPressure && secondaryItems != null && index < secondaryItems.length) {
|
|
|
|
|
// Show systolic/diastolic for blood pressure
|
|
|
|
|
final diastolic = secondaryItems[index];
|
|
|
|
|
valueText = '${dp.actualValue}/${diastolic.actualValue} ${dp.unitOfMeasurement ?? ''}';
|
|
|
|
|
} else {
|
|
|
|
|
// Show single value for other metrics
|
|
|
|
|
valueText = '${dp.actualValue} ${dp.unitOfMeasurement ?? ''}';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500),
|
|
|
|
|
('${dp.actualValue} ${dp.unitOfMeasurement ?? ''}').toText12(
|
|
|
|
|
valueText.toText12(
|
|
|
|
|
color: AppColors.textColor,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
),
|
|
|
|
|
@ -390,34 +414,46 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double _minY(List<DataPoint> points) {
|
|
|
|
|
double _minY(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
|
|
|
|
|
// IMPORTANT: y-axis uses actual numeric values (from actualValue).
|
|
|
|
|
final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList();
|
|
|
|
|
|
|
|
|
|
// Include secondary data points if provided (for blood pressure)
|
|
|
|
|
if (secondaryHistory != null && secondaryHistory.isNotEmpty) {
|
|
|
|
|
values.addAll(secondaryHistory.map((e) => double.tryParse(e.actualValue) ?? 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final min = values.reduce((a, b) => a < b ? a : b);
|
|
|
|
|
final double boundLow = args.low ?? min;
|
|
|
|
|
return (min < boundLow ? min : boundLow) - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double _maxY(List<DataPoint> points) {
|
|
|
|
|
double _maxY(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
|
|
|
|
|
// IMPORTANT: y-axis uses actual numeric values (from actualValue).
|
|
|
|
|
final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList();
|
|
|
|
|
|
|
|
|
|
// Include secondary data points if provided (for blood pressure)
|
|
|
|
|
if (secondaryHistory != null && secondaryHistory.isNotEmpty) {
|
|
|
|
|
values.addAll(secondaryHistory.map((e) => double.tryParse(e.actualValue) ?? 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final max = values.reduce((a, b) => a > b ? a : b);
|
|
|
|
|
final double boundHigh = args.high ?? max;
|
|
|
|
|
return (max > boundHigh ? max : boundHigh) + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double _leftInterval(List<DataPoint> points) {
|
|
|
|
|
double _leftInterval(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
|
|
|
|
|
// Keep it stable; graph will mostly show just two labels.
|
|
|
|
|
final range = (_maxY(points) - _minY(points)).abs();
|
|
|
|
|
final range = (_maxY(points, secondaryHistory: secondaryHistory) - _minY(points, secondaryHistory: secondaryHistory)).abs();
|
|
|
|
|
if (range <= 0) return 1;
|
|
|
|
|
return (range / 4).clamp(1, 20);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RangeAnnotations? _rangeAnnotations(List<DataPoint> points) {
|
|
|
|
|
RangeAnnotations? _rangeAnnotations(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
|
|
|
|
|
if (args.low == null && args.high == null) return null;
|
|
|
|
|
|
|
|
|
|
final minY = _minY(points);
|
|
|
|
|
final maxY = _maxY(points);
|
|
|
|
|
final minY = _minY(points, secondaryHistory: secondaryHistory);
|
|
|
|
|
final maxY = _maxY(points, secondaryHistory: secondaryHistory);
|
|
|
|
|
|
|
|
|
|
final List<HorizontalRangeAnnotation> ranges = [];
|
|
|
|
|
|
|
|
|
|
@ -480,7 +516,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
case VitalSignMetric.respiratoryRate:
|
|
|
|
|
return _toDouble(v.respirationBeatPerMinute);
|
|
|
|
|
case VitalSignMetric.bloodPressure:
|
|
|
|
|
// Graph only systolic for now (simple single-series).
|
|
|
|
|
// Graph systolic for primary series
|
|
|
|
|
return _toDouble(v.bloodPressureHigher);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -513,6 +549,46 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build diastolic blood pressure series for dual-line graph
|
|
|
|
|
List<DataPoint> _buildBloodPressureDiastolicSeries(List<VitalSignResModel> vitals) {
|
|
|
|
|
final List<DataPoint> points = [];
|
|
|
|
|
|
|
|
|
|
// Build a chronological series (oldest -> newest), skipping null/zero values.
|
|
|
|
|
final sorted = List<VitalSignResModel>.from(vitals);
|
|
|
|
|
sorted.sort((a, b) {
|
|
|
|
|
final ad = a.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
|
|
|
final bd = b.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0);
|
|
|
|
|
return ad.compareTo(bd);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
|
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
|
|
|
|
|
|
|
|
double index = 0;
|
|
|
|
|
for (final v in sorted) {
|
|
|
|
|
final diastolic = _toDouble(v.bloodPressureLower);
|
|
|
|
|
if (diastolic == null) continue;
|
|
|
|
|
if (diastolic == 0) continue;
|
|
|
|
|
|
|
|
|
|
final dt = v.vitalSignDate ?? DateTime.now();
|
|
|
|
|
final label = '${monthNames[dt.month - 1]}, ${dt.year}';
|
|
|
|
|
|
|
|
|
|
points.add(
|
|
|
|
|
DataPoint(
|
|
|
|
|
value: index,
|
|
|
|
|
label: label,
|
|
|
|
|
actualValue: diastolic.toStringAsFixed(0),
|
|
|
|
|
time: dt,
|
|
|
|
|
displayTime: '${dt.day}/${dt.month}/${dt.year}',
|
|
|
|
|
unitOfMeasurement: args.unit,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
index += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double? _toDouble(dynamic v) {
|
|
|
|
|
if (v == null) return null;
|
|
|
|
|
if (v is num) return v.toDouble();
|
|
|
|
|
|