|
|
|
|
@ -6,14 +6,26 @@ 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? maxX;
|
|
|
|
|
final Axis scrollDirection;
|
|
|
|
|
final bool isFullScreenGraph;
|
|
|
|
|
|
|
|
|
|
const DynamicResultChart(
|
|
|
|
|
{super.key, required this.dataPoints, required this.thresholds});
|
|
|
|
|
{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) {
|
|
|
|
|
final minY = thresholds.first.value;
|
|
|
|
|
final maxY = thresholds.last.value + 1;
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final spots = dataPoints.asMap().entries.map((entry) {
|
|
|
|
|
return FlSpot(entry.key.toDouble(), entry.value.value);
|
|
|
|
|
@ -24,130 +36,127 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
return Material(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
height: MediaQuery.sizeOf(context).height / 4,
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: MediaQuery.sizeOf(context).width - 77,
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.only(top: 8.0, bottom: 8),
|
|
|
|
|
child: LineChart(
|
|
|
|
|
LineChartData(
|
|
|
|
|
minY: minY,
|
|
|
|
|
maxY: maxY,
|
|
|
|
|
minX: dataPoints.first.labelValue - 1,
|
|
|
|
|
maxX: dataPoints.first.labelValue + 1,
|
|
|
|
|
lineTouchData:
|
|
|
|
|
LineTouchData(touchTooltipData: LineTouchTooltipData(
|
|
|
|
|
getTooltipItems: (touchedSpots) {
|
|
|
|
|
if (touchedSpots.isEmpty) return [];
|
|
|
|
|
width: width,
|
|
|
|
|
height: height,
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.only(top: 8.0, bottom: 8),
|
|
|
|
|
child: LineChart(
|
|
|
|
|
LineChartData(
|
|
|
|
|
minY: minY,
|
|
|
|
|
maxY: maxY,
|
|
|
|
|
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) {
|
|
|
|
|
return LineTooltipItem(
|
|
|
|
|
'${spot.y}',
|
|
|
|
|
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()];
|
|
|
|
|
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}");
|
|
|
|
|
|
|
|
|
|
// 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),
|
|
|
|
|
);
|
|
|
|
|
// 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(
|
|
|
|
|
actualValue,
|
|
|
|
|
style: const TextStyle(fontSize: 10),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
bottomTitles: AxisTitles(
|
|
|
|
|
axisNameSize: 60,
|
|
|
|
|
sideTitles: SideTitles(
|
|
|
|
|
showTitles: true,
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
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: const EdgeInsets.only(top: 8.0),
|
|
|
|
|
child: Text(
|
|
|
|
|
label,
|
|
|
|
|
style: const TextStyle(fontSize: 12),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
},
|
|
|
|
|
interval: 1, // ensures 1:1 mapping with spots
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: EdgeInsetsDirectional.only(top: 8.0,),
|
|
|
|
|
child: Text(
|
|
|
|
|
label,
|
|
|
|
|
style: const TextStyle(fontSize: 12),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<LineChartBarData> _buildColoredLineSegments(
|
|
|
|
|
@ -186,12 +195,13 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
print("the spots are $allSpots");
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
var data = [
|
|
|
|
|
...segments,
|
|
|
|
|
LineChartBarData(
|
|
|
|
|
spots: allSpots,
|
|
|
|
|
@ -212,6 +222,10 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
var max = LineChartHelper.calculateMaxAxisValues(data).maxX;
|
|
|
|
|
print("the maxX is the -------> $max");
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<HorizontalRangeAnnotation> _buildRangeShades(
|
|
|
|
|
@ -226,6 +240,8 @@ class DynamicResultChart extends StatelessWidget {
|
|
|
|
|
}
|
|
|
|
|
return ranges;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final List<DataPoint> sampleData = [
|
|
|
|
|
|