lab result graph is changed to curved and calculated flag overlay position changed
parent
18b56d6496
commit
92018c0963
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,72 @@
|
||||
import 'package:hmg_patient_app/core/viewModels/medical/labs_view_model.dart';
|
||||
import 'package:hmg_patient_app/pages/base/base_view.dart';
|
||||
import 'package:hmg_patient_app/uitl/translations_delegate_base.dart';
|
||||
import 'package:hmg_patient_app/widgets/data_display/medical/LabResult/lab_result_graph.dart';
|
||||
import 'package:hmg_patient_app/widgets/others/app_scaffold_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FullScreenGraph extends StatelessWidget {
|
||||
final List<DataPoint> completeeGraphValues;
|
||||
|
||||
final List<ThresholdRange> threshold;
|
||||
|
||||
final double maxY;
|
||||
|
||||
final String appBarTitle;
|
||||
|
||||
const FullScreenGraph({super.key, required this.completeeGraphValues, required this.threshold, required this.maxY, required this.appBarTitle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
isShowAppBar: true,
|
||||
// appBarTitle: TranslationBase.of(context).labResult,
|
||||
appBarTitle: appBarTitle,
|
||||
showNewAppBar: true,
|
||||
showNewAppBarTitle: true,
|
||||
showHomeAppBarIcon: false,
|
||||
backgroundColor: Color(0xffF8F8F8),
|
||||
body: RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: SizedBox(
|
||||
// width: MediaQuery.sizeOf(context).height,
|
||||
height: MediaQuery.sizeOf(context).width,
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DynamicResultChart(
|
||||
dataPoints: completeeGraphValues,
|
||||
thresholds: threshold,
|
||||
maxY: maxY,
|
||||
width: ((completeeGraphValues.length <= 2)
|
||||
? MediaQuery.sizeOf(context).height
|
||||
: (MediaQuery.sizeOf(context).height * (completeeGraphValues.length <= 15 ? 1 : (completeeGraphValues.length / 15)))) -
|
||||
100,
|
||||
maxX: completeeGraphValues.length + 1,
|
||||
// width: MediaQuery.sizeOf(context).height - 100,
|
||||
scrollDirection: Axis.horizontal,
|
||||
height: MediaQuery.sizeOf(context).width,
|
||||
showBottomTitleDates: true,
|
||||
isFullScreeGraph: true,
|
||||
// isFullScreenGraph: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// double getMax(List<DataPoint> dataPoints) {
|
||||
// double max = double.negativeInfinity;
|
||||
// for (var point in dataPoints) {
|
||||
// if (point.value > max) {
|
||||
// max = point.y;
|
||||
// }
|
||||
// }
|
||||
// return max;
|
||||
// }
|
||||
}
|
||||
@ -0,0 +1,337 @@
|
||||
import 'package:hmg_patient_app/uitl/translations_delegate_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DynamicResultChart extends StatelessWidget {
|
||||
final List<DataPoint> dataPoints;
|
||||
final List<ThresholdRange> thresholds;
|
||||
final double? width;
|
||||
final double height;
|
||||
final double? maxY;
|
||||
final double? maxX;
|
||||
|
||||
final Axis scrollDirection;
|
||||
final bool showBottomTitleDates;
|
||||
final bool isFullScreeGraph;
|
||||
|
||||
DynamicResultChart(
|
||||
{super.key, required this.dataPoints, required this.thresholds, this.width, required this.scrollDirection, required this.height, this.maxY,this.maxX, this.showBottomTitleDates = true, this.isFullScreeGraph = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var minY = 0.0;
|
||||
// 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;
|
||||
// else if((maxY??0)>=100) interval = 20;
|
||||
|
||||
// if (thresholds.isNotEmpty) {
|
||||
// print("the thresholds are $thresholds");
|
||||
// minY = thresholds.first.value;
|
||||
// maxY = thresholds.last.value + 1;
|
||||
// } else {
|
||||
// return SizedBox.shrink();
|
||||
// }
|
||||
|
||||
final spots = dataPoints.asMap().entries.map((entry) {
|
||||
return FlSpot(entry.key.toDouble(), entry.value.value);
|
||||
}).toList();
|
||||
final widthPerPoint = 30.0; // Customize as needed
|
||||
final chartWidth = (dataPoints.length + 1) * widthPerPoint;
|
||||
|
||||
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,
|
||||
lineTouchData: LineTouchData(touchTooltipData: LineTouchTooltipData(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()];
|
||||
|
||||
if (dataPoint.isStringResource) {
|
||||
return LineTooltipItem(
|
||||
'${double.parse(dataPoint.actualValue).toStringAsFixed(2)}',
|
||||
const TextStyle(color: Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
return LineTooltipItem(
|
||||
// '${dataPoint.label} ${spot.y.toStringAsFixed(2)}',
|
||||
'${dataPoint.label} ${double.parse(dataPoint.actualValue).toStringAsFixed(2)}',
|
||||
const TextStyle(color: Colors.white),
|
||||
);
|
||||
}
|
||||
return null; // hides the rest
|
||||
}).toList();
|
||||
})),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
|
||||
sideTitles : SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 50,
|
||||
interval: interval,
|
||||
getTitlesWidget: (value, meta) {
|
||||
// Skip the last (maxY) label
|
||||
if (value == ((maxY?.ceilToDouble()??0.0)+interval ).floorToDouble()) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
value.toStringAsFixed(1),
|
||||
style: const TextStyle(fontSize: 10),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
),
|
||||
// leftTitles: AxisTitles(
|
||||
// sideTitles: SideTitles(
|
||||
// showTitles: true,
|
||||
// reservedSize: 77,
|
||||
// interval: .1, // Let fl_chart handle it
|
||||
// getTitlesWidget: (value, _) {
|
||||
// // print("the value is ======== ${value}");
|
||||
//
|
||||
// // Compare with 2-decimal precision to avoid close duplicates
|
||||
// final matchingThreshold = thresholds.firstWhere(
|
||||
// (t) => t.value.toStringAsFixed(1) == value.toStringAsFixed(1),
|
||||
// orElse: () => ThresholdRange(label: '', value: 0, color: Colors.transparent, lineColor: Colors.transparent),
|
||||
// );
|
||||
//
|
||||
// var actualValue = (matchingThreshold.actualValue != null)
|
||||
// ? "${TranslationBase.of(context).getTranslation(matchingThreshold.label)} (${matchingThreshold.actualValue})"
|
||||
// : '${TranslationBase.of(context).getTranslation(matchingThreshold.label)}';
|
||||
// if (matchingThreshold.label.isNotEmpty) {
|
||||
// return Text(
|
||||
// "${value.toStringAsFixed(0)}",
|
||||
// style: const TextStyle(fontSize: 10),
|
||||
// );
|
||||
// }
|
||||
// return const SizedBox.shrink();
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameSize: 60,
|
||||
sideTitles: SideTitles(
|
||||
showTitles: showBottomTitleDates,
|
||||
reservedSize: 50,
|
||||
getTitlesWidget: (value, _) {
|
||||
if (value.toInt() >= 0 && value.toInt() < dataPoints.length) {
|
||||
var label = dataPoints[value.toInt()].label;
|
||||
if (dataPoints[value.toInt()].isStringResource) {
|
||||
label = TranslationBase.of(context).getTranslation(dataPoints[value.toInt()].label);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
top: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
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(
|
||||
color: Colors.grey,
|
||||
width: 0.5,
|
||||
),
|
||||
left: BorderSide(
|
||||
color: Colors.grey,
|
||||
width: .5
|
||||
),
|
||||
right:BorderSide.none,
|
||||
top: BorderSide.none,
|
||||
),),
|
||||
lineBarsData: _buildColoredLineSegments(dataPoints, thresholds),
|
||||
gridData: FlGridData(show: true, drawVerticalLine: false, getDrawingHorizontalLine:(value)=> FlLine(color: Colors.grey, strokeWidth: 0.5)),
|
||||
// extraLinesData: ExtraLinesData(
|
||||
// horizontalLines: [
|
||||
// HorizontalLine(
|
||||
// color: Colors.black,
|
||||
// strokeWidth: 2,
|
||||
// dashArray: [5, 5], // optional: dashed line
|
||||
// label: HorizontalLineLabel(
|
||||
// show: true,
|
||||
// alignment: Alignment.centerRight,
|
||||
// style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
|
||||
// labelResolver: (line) => 'Threshold ${line.y.toInt()}',
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// rangeAnnotations: RangeAnnotations(
|
||||
// horizontalRangeAnnotations: _buildRangeShades(thresholds),
|
||||
// ),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
List<LineChartBarData> _buildColoredLineSegments(List<DataPoint> dataPoints, List<ThresholdRange> thresholds) {
|
||||
List<LineChartBarData> segments = [];
|
||||
Color getColor(String value) {
|
||||
for (int i = thresholds.length - 1; i >= 0; i--) {
|
||||
if (value == thresholds[i].label) {
|
||||
return thresholds[i].lineColor;
|
||||
}
|
||||
}
|
||||
return Colors.grey;
|
||||
}
|
||||
|
||||
for (int i = 0; i < dataPoints.length - 1; i++) {
|
||||
final dp1 = dataPoints[i];
|
||||
final dp2 = dataPoints[i + 1];
|
||||
|
||||
final spot1 = FlSpot(i.toDouble(), dp1.value);
|
||||
final spot2 = FlSpot((i + 1).toDouble(), dp2.value);
|
||||
|
||||
final color1 = getColor(dp1.referenceRangeValue);
|
||||
final color2 = getColor(dp2.referenceRangeValue);
|
||||
|
||||
segments.add(LineChartBarData(
|
||||
spots: [spot1, spot2],
|
||||
isCurved: true,
|
||||
isStrokeCapRound: true,
|
||||
isStrokeJoinRound: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [color1, color2],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
// dotData: FlDotData(
|
||||
// show: true,
|
||||
// getDotPainter: (spot, _, __, index) {
|
||||
// final value = spot.y;
|
||||
// final color = getColor(value);
|
||||
// return FlDotCirclePainter(
|
||||
// radius: 4,
|
||||
// color: color,
|
||||
// strokeWidth: 1,
|
||||
// strokeColor: Colors.white,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Add dot markers separately so they are on top
|
||||
|
||||
final List<FlSpot> allSpots = dataPoints.asMap().entries.map((entry) {
|
||||
return FlSpot(entry.key.toDouble(), entry.value.value);
|
||||
}).toList();
|
||||
|
||||
var data = [
|
||||
// ...segments
|
||||
LineChartBarData(
|
||||
spots: allSpots,
|
||||
isCurved: true,
|
||||
isStrokeCapRound: true,
|
||||
isStrokeJoinRound: true,
|
||||
barWidth: 2,
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFF5dc36b), Color(0xFF5dc36b)],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, _, __, index) {
|
||||
final value = spot.y;
|
||||
final color = Color(0xFF5dc36b);
|
||||
return FlDotCirclePainter(
|
||||
radius: 4,
|
||||
color: color,
|
||||
strokeWidth: 1,
|
||||
strokeColor: Colors.white,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
// var max = LineChartHelper.calculateMaxAxisValues(data).maxX;
|
||||
// print("the maxX is the -------> $max");
|
||||
return data;
|
||||
}
|
||||
|
||||
List<HorizontalRangeAnnotation> _buildRangeShades(List<ThresholdRange> thresholds) {
|
||||
List<HorizontalRangeAnnotation> ranges = [];
|
||||
for (int i = 0; i < thresholds.length - 1; i++) {
|
||||
ranges.add(HorizontalRangeAnnotation(
|
||||
y1: thresholds[i].value,
|
||||
y2: thresholds[i + 1].value,
|
||||
color: thresholds[i].color,
|
||||
));
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
}
|
||||
|
||||
final List<DataPoint> sampleData = [
|
||||
DataPoint(value: 1.4, label: 'Jan 2024', date: DateTime(2024, 1, 1)),
|
||||
DataPoint(value: 3.6, label: 'Feb 2024', date: DateTime(2024, 2, 1)),
|
||||
DataPoint(value: 1.96, label: 'This result', date: DateTime(2024, 3, 1)),
|
||||
];
|
||||
final List<ThresholdRange> thresholdLevels = [
|
||||
ThresholdRange(label: 'Critical Low', value: 1.4, color: Color(0xfff6e9e9), lineColor: Color(0xFFe9a2a4)),
|
||||
ThresholdRange(label: 'Low', value: 3.6, color: Color(0xFFf2fbf5), lineColor: Color(0xFFeecd94)),
|
||||
ThresholdRange(label: 'Normal', value: 5.96, color: Color(0xFFf2fbf5), lineColor: Color(0xFF5dc36b)),
|
||||
ThresholdRange(label: 'High', value: 7.15, color: Color(0xfff6e9e9), lineColor: Color(0xFFeecd94)),
|
||||
ThresholdRange(label: 'Critical High', value: 10.15, color: Color(0xfff6e9e9), lineColor: Color(0xFFe9a2a4)),
|
||||
];
|
||||
|
||||
class DataPoint {
|
||||
final double value;
|
||||
double labelValue;
|
||||
String label;
|
||||
String actualValue;
|
||||
final DateTime date;
|
||||
bool isStringResource;
|
||||
String referenceRangeValue;
|
||||
|
||||
DataPoint({required this.value, required this.label, required this.date, this.isStringResource = false, this.labelValue = 0.0, this.actualValue = "", this.referenceRangeValue= ""});
|
||||
}
|
||||
|
||||
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)})';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
import 'package:hmg_patient_app/core/model/labs/lab_result.dart';
|
||||
import 'package:hmg_patient_app/core/model/labs/patient_lab_orders.dart';
|
||||
import 'package:hmg_patient_app/theme/colors.dart';
|
||||
import 'package:hmg_patient_app/uitl/translations_delegate_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:readmore/readmore.dart';
|
||||
|
||||
import '../FlowChartPage.dart';
|
||||
import 'labWidgets.dart';
|
||||
|
||||
class LabItem extends StatefulWidget {
|
||||
final LabResultList item;
|
||||
PatientLabOrders? patientLabOrders;
|
||||
|
||||
LabItem({super.key, required this.item, this.patientLabOrders});
|
||||
|
||||
@override
|
||||
State<LabItem> createState() => _LabItemState();
|
||||
}
|
||||
|
||||
class _LabItemState extends State<LabItem> {
|
||||
bool _isShowMoreGeneral = true;
|
||||
bool showBottomSheet = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (showBottomSheet) {
|
||||
print('the bottom sheet is showing');
|
||||
openFlowChart(context, '');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isShowMoreGeneral = !_isShowMoreGeneral;
|
||||
});
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.filterName ?? '',
|
||||
style: TextStyle(fontSize: 21, fontWeight: FontWeight.bold, color: Color(0xff2B353E), letterSpacing: -0.64, height: 25 / 16),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
_isShowMoreGeneral ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
|
||||
color: Color(0xff2B353E),
|
||||
size: 26,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ReadMoreText(
|
||||
widget.item.description ?? '',
|
||||
trimMode: TrimMode.Line,
|
||||
trimLines: 3,
|
||||
style: const TextStyle(color: Color(0xff575757), fontSize: 11),
|
||||
colorClickableText: CustomColors.accentColor,
|
||||
trimCollapsedText: '${TranslationBase.of(context).readMore}',
|
||||
trimExpandedText: ' show less',
|
||||
),
|
||||
SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (_isShowMoreGeneral)
|
||||
...List.generate(widget.item.patientLabResultList?.length ?? 0, (index) {
|
||||
var data = widget.item.patientLabResultList?[index];
|
||||
return Column(
|
||||
children: [
|
||||
ItemResultCardWidgetWithParams(
|
||||
title: data?.description ?? '',
|
||||
subTitle: data?.resultValue ?? '',
|
||||
referenceRange: data?.referanceRange ?? '',
|
||||
percentage: data?.percentage ?? 0.0,
|
||||
note: data?.notes ?? '',
|
||||
source: "",
|
||||
buttonText: "",
|
||||
type: data?.calculatedResultFlag?.getType() ?? ResultTypes.unknown,
|
||||
shouldShowResultBarAndGraph: data?.shouldShowResultBarAndGraph() ?? true,
|
||||
onButtonPressed: () {
|
||||
openFlowChart(context, data?.description ?? '');
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 13,
|
||||
),
|
||||
],
|
||||
);
|
||||
})
|
||||
// Expanded(
|
||||
// child: ListView.builder(
|
||||
// itemCount: widget.item.patientLabResultList?.length ?? 0,
|
||||
// itemBuilder: (context, index) {
|
||||
// var data = widget.item.patientLabResultList?[index];
|
||||
// Column(
|
||||
// children: [
|
||||
// EllipsisTextWithMore(
|
||||
// text: data?.packageShortDescription ?? '',
|
||||
// ),
|
||||
// SizedBox(
|
||||
// height: 8,
|
||||
// ),
|
||||
// ItemResultCardWidgetWithParams(
|
||||
// title: data?.description ?? '',
|
||||
// subTitle: data?.resultValue ?? '',
|
||||
// referenceRange: data?.referanceRange ?? '',
|
||||
// percentage: 20,
|
||||
// note: data?.testShortDescription ?? '',
|
||||
// source: "",
|
||||
// buttonText: "",
|
||||
// type: data?.calculatedResultFlag?.getType() ??
|
||||
// ResultTypes.unknown),
|
||||
// ],
|
||||
// );
|
||||
// }))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
openFlowChart(BuildContext context, String procedure) {
|
||||
print('openFlowChart: the bottom sheet is showing');
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
scrollControlDisabledMaxHeightRatio: .75,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20))),
|
||||
builder: (context) {
|
||||
return FlowChartPage(
|
||||
filterName: procedure,
|
||||
patientLabOrder: widget.patientLabOrders,
|
||||
);
|
||||
}).then((value) {
|
||||
setState(() {
|
||||
showBottomSheet = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,530 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:hmg_patient_app/uitl/translations_delegate_base.dart';
|
||||
import 'package:hmg_patient_app/uitl/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///@author
|
||||
///
|
||||
enum ResultTypes { lowCriticalLow, criticalLow, low, normal, high, criticalHigh, highCriticalHigh, IRR, unknown }
|
||||
|
||||
class ItemResultCardWidget extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const ItemResultCardWidget({
|
||||
Key? key,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 12),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomResultProgressBar extends StatelessWidget {
|
||||
final num percentage;
|
||||
final String value;
|
||||
final ResultTypes type;
|
||||
|
||||
CustomResultProgressBar({Key? key, required this.percentage, required this.value, required this.type}) : super(key: key);
|
||||
|
||||
final GlobalKey lcl = GlobalKey();
|
||||
|
||||
final GlobalKey cl = GlobalKey();
|
||||
|
||||
final GlobalKey l = GlobalKey();
|
||||
|
||||
final GlobalKey n = GlobalKey();
|
||||
|
||||
final GlobalKey h = GlobalKey();
|
||||
|
||||
final GlobalKey ch = GlobalKey();
|
||||
|
||||
final GlobalKey hch = GlobalKey();
|
||||
|
||||
OverlayEntry? overlayEntry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final double totalWidth = constraints.maxWidth;
|
||||
final double spacing = 2;
|
||||
final int spacingCount = 4; // between 5 bars
|
||||
final double flexWidth = totalWidth - 50;
|
||||
|
||||
|
||||
final double tooltipPosition =
|
||||
(flexWidth * (percentage / 100)).clamp(0, flexWidth);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_buildResultBar(
|
||||
key: cl,
|
||||
flex: 3,
|
||||
color: Color(0xFFDE7676),
|
||||
title: TranslationBase.of(context).criticalLow,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
_buildResultBar(
|
||||
key: l,
|
||||
flex: 2,
|
||||
color: Color(0xFFEAB157),
|
||||
title: TranslationBase.of(context).low,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
_buildResultBar(
|
||||
key: n,
|
||||
flex: 5,
|
||||
color: Color(0xFF09AA28),
|
||||
title: TranslationBase.of(context).normal,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
_buildResultBar(
|
||||
key: h,
|
||||
flex: 2,
|
||||
color: Color(0xFFEAB157),
|
||||
title: TranslationBase.of(context).high,
|
||||
),
|
||||
SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
_buildResultBar(
|
||||
key: ch,
|
||||
flex: 3,
|
||||
color: Color(0xFFDE7676),
|
||||
title: TranslationBase.of(context).criticalHigh,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!percentage.isNegative)
|
||||
Positioned(
|
||||
left: tooltipPosition,
|
||||
top: 33,
|
||||
child: Column(
|
||||
children: [
|
||||
TextCloud(
|
||||
text: "${value}",
|
||||
color: getColorForResultType(type),
|
||||
width: 75,
|
||||
height: 20,
|
||||
padding: EdgeInsets.zero,
|
||||
axisDirection: AxisDirection.up,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Color getColorForResultType(ResultTypes type) {
|
||||
switch (type) {
|
||||
case ResultTypes.lowCriticalLow:
|
||||
case ResultTypes.highCriticalHigh:
|
||||
case ResultTypes.criticalLow:
|
||||
case ResultTypes.criticalHigh:
|
||||
return Color(0xFFDE7676);
|
||||
case ResultTypes.low:
|
||||
case ResultTypes.high:
|
||||
return Color(0xFFEAB157);
|
||||
case ResultTypes.normal:
|
||||
return Color(0xFF09AA28);
|
||||
default:
|
||||
return Color(0xFF09AA28);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildResultBar({required int flex, required Color color, required String title, Key? key}) {
|
||||
return Expanded(
|
||||
key: key,
|
||||
flex: flex,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
fontFamily: 'Poppins',
|
||||
letterSpacing: -0.3,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Container(
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TextCloud extends StatelessWidget {
|
||||
final String text; // write here box content
|
||||
final Color color; // Set box Color
|
||||
final EdgeInsets padding; // Set content padding
|
||||
final double width; // Box width
|
||||
final double height; // Box Height
|
||||
final AxisDirection axisDirection; // Set triangle location up,left,right,down
|
||||
final double locationOfArrow; // set between 0 and 1, If 0.5 is set triangle position will be centered
|
||||
const TextCloud({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.color = Colors.white,
|
||||
this.padding = const EdgeInsets.all(10),
|
||||
this.width = 200,
|
||||
this.height = 100,
|
||||
this.axisDirection = AxisDirection.down,
|
||||
this.locationOfArrow = 0.5,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size arrowSize = const Size(12, 12);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(color: Colors.white, fontSize: 9, letterSpacing: -.3),
|
||||
)),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
double angle = 0;
|
||||
switch (axisDirection) {
|
||||
case AxisDirection.left:
|
||||
angle = pi * -0.5;
|
||||
break;
|
||||
case AxisDirection.up:
|
||||
angle = pi * -2;
|
||||
break;
|
||||
case AxisDirection.right:
|
||||
angle = pi * 0.5;
|
||||
break;
|
||||
case AxisDirection.down:
|
||||
angle = pi;
|
||||
break;
|
||||
default:
|
||||
angle = 0;
|
||||
}
|
||||
return Positioned(
|
||||
left: axisDirection == AxisDirection.left
|
||||
? -arrowSize.width + 5
|
||||
: (axisDirection == AxisDirection.up || axisDirection == AxisDirection.down ? width * locationOfArrow - arrowSize.width / 2 : null),
|
||||
right: axisDirection == AxisDirection.right ? -arrowSize.width + 5 : null,
|
||||
top: axisDirection == AxisDirection.up
|
||||
? -arrowSize.width + 5
|
||||
: (axisDirection == AxisDirection.right || axisDirection == AxisDirection.left ? height * locationOfArrow - arrowSize.width / 2 : null),
|
||||
bottom: axisDirection == AxisDirection.down ? -arrowSize.width + 5 : null,
|
||||
child: Transform.rotate(
|
||||
angle: angle,
|
||||
child: CustomPaint(
|
||||
size: arrowSize,
|
||||
painter: ArrowPaint(color: color),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArrowPaint extends CustomPainter {
|
||||
final Color color;
|
||||
|
||||
ArrowPaint({required this.color});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
Path path_0 = Path();
|
||||
path_0.moveTo(size.width * 0.5745375, size.height * 0.06573708);
|
||||
path_0.lineTo(size.width * 0.9813667, size.height * 0.8794000);
|
||||
path_0.cubicTo(size.width * 1.001950, size.height * 0.9205625, size.width * 0.9852625, size.height * 0.9706208, size.width * 0.9441000, size.height * 0.9912000);
|
||||
path_0.cubicTo(size.width * 0.9325250, size.height * 0.9969875, size.width * 0.9197667, size.height, size.width * 0.9068292, size.height);
|
||||
path_0.lineTo(size.width * 0.09316958, size.height);
|
||||
path_0.cubicTo(size.width * 0.04714583, size.height, size.width * 0.009836208, size.height * 0.9626917, size.width * 0.009836208, size.height * 0.9166667);
|
||||
path_0.cubicTo(size.width * 0.009836208, size.height * 0.9037292, size.width * 0.01284829, size.height * 0.8909708, size.width * 0.01863392, size.height * 0.8794000);
|
||||
path_0.lineTo(size.width * 0.4254625, size.height * 0.06573708);
|
||||
path_0.cubicTo(size.width * 0.4460458, size.height * 0.02457225, size.width * 0.4961042, size.height * 0.007886875, size.width * 0.5372667, size.height * 0.02846929);
|
||||
path_0.cubicTo(size.width * 0.5533958, size.height * 0.03653296, size.width * 0.5664708, size.height * 0.04961000, size.width * 0.5745375, size.height * 0.06573708);
|
||||
path_0.close();
|
||||
|
||||
Paint paint_0_fill = Paint()..style = PaintingStyle.fill;
|
||||
paint_0_fill.color = color;
|
||||
canvas.drawPath(path_0, paint_0_fill);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ItemResultCardWidgetWithParams extends StatelessWidget {
|
||||
final String title;
|
||||
final String subTitle;
|
||||
final String note;
|
||||
final String source;
|
||||
final String referenceRange;
|
||||
final num percentage;
|
||||
final String buttonText;
|
||||
final ResultTypes type;
|
||||
final bool isArabic;
|
||||
final Function()? onButtonPressed;
|
||||
final bool shouldShowResultBarAndGraph;
|
||||
|
||||
const ItemResultCardWidgetWithParams({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.subTitle,
|
||||
required this.referenceRange,
|
||||
required this.percentage,
|
||||
required this.note,
|
||||
required this.source,
|
||||
required this.buttonText,
|
||||
required this.type,
|
||||
this.onButtonPressed,
|
||||
this.isArabic = false,
|
||||
this.shouldShowResultBarAndGraph = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fontFamily = isArabic ? 'Cairo' : 'Poppins';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 21),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: BorderDirectional(
|
||||
start: BorderSide(
|
||||
color: getColorForResultType(type),
|
||||
width: MediaQuery.of(context).size.width * 0.015,
|
||||
),
|
||||
),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
// Title Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 16, fontFamily: fontFamily, fontWeight: FontWeight.w600, color: const Color(0xff2E303A), letterSpacing: -.64),
|
||||
),
|
||||
),
|
||||
// GestureDetector(
|
||||
// onTap: onButtonPressed,
|
||||
// child: Utils.tableColumnValueWithUnderLine(TranslationBase.of(context).viewFlowChart, isLast: true, isCapitable: false),
|
||||
// ),
|
||||
Visibility(
|
||||
visible: shouldShowResultBarAndGraph,
|
||||
child: GestureDetector(
|
||||
onTap: onButtonPressed,
|
||||
child: Utils.tableColumnValueWithUnderLine(TranslationBase.of(context).viewFlowChart, isLast: true, isCapitable: false),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ResultStatusWidget(type: type),
|
||||
SizedBox(width: 5),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
subTitle,
|
||||
style: TextStyle(fontSize: 14, fontFamily: fontFamily, fontWeight: FontWeight.w600, color: const Color(0xff2E303A), letterSpacing: -0.56),
|
||||
),
|
||||
Text(
|
||||
"${TranslationBase.of(context).referenceRange} \n $referenceRange",
|
||||
style: TextStyle(
|
||||
overflow: TextOverflow.clip,
|
||||
fontSize: 10,
|
||||
fontFamily: fontFamily,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xff2E303A),
|
||||
letterSpacing: -0.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Progress Bar
|
||||
Visibility(
|
||||
visible: shouldShowResultBarAndGraph,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 55,
|
||||
child: CustomResultProgressBar(
|
||||
percentage: percentage,
|
||||
value: subTitle,
|
||||
type: type,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Note Section
|
||||
if (note.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(color: Color(0xFFEFEFEF), thickness: 1),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"${TranslationBase.of(context).notes}:",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF2E303A),
|
||||
letterSpacing: -0.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
note,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF575757),
|
||||
letterSpacing: -0.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color getColorForResultType(ResultTypes type) {
|
||||
switch (type) {
|
||||
case ResultTypes.criticalLow:
|
||||
case ResultTypes.criticalHigh:
|
||||
case ResultTypes.lowCriticalLow:
|
||||
case ResultTypes.highCriticalHigh:
|
||||
return Color(0xFFDE7676);
|
||||
case ResultTypes.low:
|
||||
case ResultTypes.high:
|
||||
return Color(0xFFEAB157);
|
||||
case ResultTypes.normal:
|
||||
return Color(0xFF09AA28);
|
||||
//todo handle irr here
|
||||
default:
|
||||
return Color(0xFF09AA28);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResultStatusWidget extends StatelessWidget {
|
||||
final ResultTypes type;
|
||||
|
||||
const ResultStatusWidget({
|
||||
Key? key,
|
||||
required this.type,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color? color;
|
||||
IconData? icon;
|
||||
|
||||
switch (type) {
|
||||
case ResultTypes.criticalLow:
|
||||
case ResultTypes.lowCriticalLow:
|
||||
color = const Color(0xFFDE7676);
|
||||
icon = Icons.arrow_circle_down;
|
||||
break;
|
||||
case ResultTypes.low:
|
||||
color = const Color(0xFFEAB157);
|
||||
icon = Icons.arrow_circle_down;
|
||||
break;
|
||||
case ResultTypes.criticalHigh:
|
||||
case ResultTypes.highCriticalHigh:
|
||||
color = const Color(0xFFDE7676);
|
||||
icon = Icons.arrow_circle_up;
|
||||
break;
|
||||
case ResultTypes.high:
|
||||
color = const Color(0xFFEAB157);
|
||||
icon = Icons.arrow_circle_up;
|
||||
break;
|
||||
case ResultTypes.normal:
|
||||
color = const Color(0xFF09AA28);
|
||||
icon = Icons.check_circle;
|
||||
break;
|
||||
case ResultTypes.unknown:
|
||||
case ResultTypes.IRR:
|
||||
color = Color(0xFF09AA28);
|
||||
icon = Icons.check_circle;
|
||||
break;
|
||||
}
|
||||
|
||||
// Return the icon if defined, otherwise a placeholder widget
|
||||
return icon != null
|
||||
? Icon(
|
||||
icon,
|
||||
color: color,
|
||||
)
|
||||
: const SizedBox(); // Use SizedBox to keep the layout consistent
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue