|
|
|
|
@ -13,19 +13,19 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
final bool isFullScreenGraph;
|
|
|
|
|
|
|
|
|
|
const DynamicResultChart(
|
|
|
|
|
{super.key, required this.dataPoints, required this.thresholds,required this.width,required this.scrollDirection, required this.height, this.maxX, this.isFullScreenGraph = false});
|
|
|
|
|
{super.key, required this.dataPoints, required this.thresholds, required this.width, required this.scrollDirection, required this.height, this.maxX, this.isFullScreenGraph = false});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
var minY = 0.0;
|
|
|
|
|
var maxY = 0.0;
|
|
|
|
|
if(thresholds.isNotEmpty) {
|
|
|
|
|
print("the thresholds are $thresholds");
|
|
|
|
|
var minY = 0.0;
|
|
|
|
|
var maxY = 0.0;
|
|
|
|
|
if (thresholds.isNotEmpty) {
|
|
|
|
|
print("the thresholds are $thresholds");
|
|
|
|
|
minY = thresholds.first.value;
|
|
|
|
|
maxY = thresholds.last.value + 1;
|
|
|
|
|
}else{
|
|
|
|
|
return SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return SizedBox.shrink();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final spots = dataPoints.asMap().entries.map((entry) {
|
|
|
|
|
return FlSpot(entry.key.toDouble(), entry.value.value);
|
|
|
|
|
@ -44,123 +44,118 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
LineChartData(
|
|
|
|
|
minY: minY,
|
|
|
|
|
maxY: maxY,
|
|
|
|
|
minX: dataPoints.first.labelValue-1,
|
|
|
|
|
minX: dataPoints.first.labelValue - 1,
|
|
|
|
|
maxX: maxX,
|
|
|
|
|
lineTouchData:
|
|
|
|
|
LineTouchData(touchTooltipData: LineTouchTooltipData(
|
|
|
|
|
getTooltipItems: (touchedSpots) {
|
|
|
|
|
if (touchedSpots.isEmpty) return [];
|
|
|
|
|
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()];
|
|
|
|
|
return LineTooltipItem(
|
|
|
|
|
'${dataPoint.label} ${spot.y.toStringAsFixed(2)}',
|
|
|
|
|
const TextStyle(color: Colors.white),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
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, _) {
|
|
|
|
|
// print("the value is ======== ${value}");
|
|
|
|
|
// Only show tooltip for the first touched spot, hide others
|
|
|
|
|
return touchedSpots.map((spot) {
|
|
|
|
|
if (spot == touchedSpots.first) {
|
|
|
|
|
final dataPoint = dataPoints[spot.x.toInt()];
|
|
|
|
|
|
|
|
|
|
// 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),
|
|
|
|
|
);
|
|
|
|
|
if (dataPoint.isStringResource) {
|
|
|
|
|
return LineTooltipItem(
|
|
|
|
|
'${double.parse(dataPoint.actualValue).toStringAsFixed(2)}',
|
|
|
|
|
const TextStyle(color: Colors.white),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
actualValue,
|
|
|
|
|
style: const TextStyle(fontSize: 10),
|
|
|
|
|
return LineTooltipItem(
|
|
|
|
|
// '${dataPoint.label} ${spot.y.toStringAsFixed(2)}',
|
|
|
|
|
'${dataPoint.label} ${double.parse(dataPoint.actualValue).toStringAsFixed(2)}',
|
|
|
|
|
const TextStyle(color: Colors.white),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
bottomTitles: AxisTitles(
|
|
|
|
|
axisNameSize: 60,
|
|
|
|
|
sideTitles: SideTitles(
|
|
|
|
|
showTitles: !isFullScreenGraph,
|
|
|
|
|
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 null; // hides the rest
|
|
|
|
|
}).toList();
|
|
|
|
|
})),
|
|
|
|
|
titlesData: FlTitlesData(
|
|
|
|
|
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),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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(
|
|
|
|
|
actualValue,
|
|
|
|
|
style: const TextStyle(fontSize: 10),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
bottomTitles: AxisTitles(
|
|
|
|
|
axisNameSize: 60,
|
|
|
|
|
sideTitles: SideTitles(
|
|
|
|
|
showTitles: !isFullScreenGraph,
|
|
|
|
|
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: false),
|
|
|
|
|
lineBarsData: _buildColoredLineSegments(dataPoints, thresholds),
|
|
|
|
|
gridData: FlGridData(show: false),
|
|
|
|
|
extraLinesData: ExtraLinesData(
|
|
|
|
|
horizontalLines: thresholds.map((t) {
|
|
|
|
|
return HorizontalLine(
|
|
|
|
|
y: t.value,
|
|
|
|
|
color: t.lineColor,
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
// label: HorizontalLineLabel(
|
|
|
|
|
// show: true,
|
|
|
|
|
// alignment: Alignment.centerLeft,
|
|
|
|
|
// style: const TextStyle(fontSize: 10),
|
|
|
|
|
// labelResolver: (_) => '${t.label} (${t.value.toStringAsFixed(2)})',
|
|
|
|
|
// ),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
rangeAnnotations: RangeAnnotations(
|
|
|
|
|
horizontalRangeAnnotations: _buildRangeShades(thresholds),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
topTitles: AxisTitles(),
|
|
|
|
|
rightTitles: AxisTitles(),
|
|
|
|
|
),
|
|
|
|
|
borderData: FlBorderData(show: false),
|
|
|
|
|
lineBarsData:
|
|
|
|
|
_buildColoredLineSegments(dataPoints, thresholds),
|
|
|
|
|
gridData: FlGridData(show: false),
|
|
|
|
|
extraLinesData: ExtraLinesData(
|
|
|
|
|
horizontalLines: thresholds.map((t) {
|
|
|
|
|
return HorizontalLine(
|
|
|
|
|
y: t.value,
|
|
|
|
|
color: t.lineColor,
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
// label: HorizontalLineLabel(
|
|
|
|
|
// show: true,
|
|
|
|
|
// alignment: Alignment.centerLeft,
|
|
|
|
|
// style: const TextStyle(fontSize: 10),
|
|
|
|
|
// labelResolver: (_) => '${t.label} (${t.value.toStringAsFixed(2)})',
|
|
|
|
|
// ),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
rangeAnnotations: RangeAnnotations(
|
|
|
|
|
horizontalRangeAnnotations: _buildRangeShades(thresholds),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<LineChartBarData> _buildColoredLineSegments(
|
|
|
|
|
List<DataPoint> dataPoints, List<ThresholdRange> thresholds) {
|
|
|
|
|
List<LineChartBarData> _buildColoredLineSegments(List<DataPoint> dataPoints, List<ThresholdRange> thresholds) {
|
|
|
|
|
List<LineChartBarData> segments = [];
|
|
|
|
|
Color getColor(double value) {
|
|
|
|
|
for (int i = thresholds.length - 1; i >= 0; i--) {
|
|
|
|
|
@ -225,11 +220,10 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
|
|
|
|
|
// var max = LineChartHelper.calculateMaxAxisValues(data).maxX;
|
|
|
|
|
// print("the maxX is the -------> $max");
|
|
|
|
|
return data;
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<HorizontalRangeAnnotation> _buildRangeShades(
|
|
|
|
|
List<ThresholdRange> thresholds) {
|
|
|
|
|
List<HorizontalRangeAnnotation> _buildRangeShades(List<ThresholdRange> thresholds) {
|
|
|
|
|
List<HorizontalRangeAnnotation> ranges = [];
|
|
|
|
|
for (int i = 0; i < thresholds.length - 1; i++) {
|
|
|
|
|
ranges.add(HorizontalRangeAnnotation(
|
|
|
|
|
@ -240,8 +234,6 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
}
|
|
|
|
|
return ranges;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final List<DataPoint> sampleData = [
|
|
|
|
|
@ -250,46 +242,22 @@ final List<DataPoint> sampleData = [
|
|
|
|
|
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)),
|
|
|
|
|
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; // e.g., "This Result"
|
|
|
|
|
String label;
|
|
|
|
|
String actualValue;
|
|
|
|
|
final DateTime date;
|
|
|
|
|
bool isStringResource;
|
|
|
|
|
|
|
|
|
|
DataPoint(
|
|
|
|
|
{required this.value,
|
|
|
|
|
required this.label,
|
|
|
|
|
required this.date,
|
|
|
|
|
this.isStringResource = false,
|
|
|
|
|
this.labelValue = 0.0});
|
|
|
|
|
DataPoint({required this.value, required this.label, required this.date, this.isStringResource = false, this.labelValue = 0.0, this.actualValue = ""});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ThresholdRange {
|
|
|
|
|
@ -299,12 +267,7 @@ class ThresholdRange {
|
|
|
|
|
final Color lineColor;
|
|
|
|
|
final String? actualValue;
|
|
|
|
|
|
|
|
|
|
ThresholdRange(
|
|
|
|
|
{required this.label,
|
|
|
|
|
required this.value,
|
|
|
|
|
required this.color,
|
|
|
|
|
required this.lineColor,
|
|
|
|
|
this.actualValue});
|
|
|
|
|
ThresholdRange({required this.label, required this.value, required this.color, required this.lineColor, this.actualValue});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String toString() {
|
|
|
|
|
|