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.
314 lines
10 KiB
Dart
314 lines
10 KiB
Dart
import 'package:diplomaticquarterapp/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? maxX;
|
|
final Axis scrollDirection;
|
|
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});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
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);
|
|
}).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: 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) {
|
|
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),
|
|
);
|
|
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
}
|
|
|
|
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--) {
|
|
if (value >= thresholds[i].value) {
|
|
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.value);
|
|
final color2 = getColor(dp2.value);
|
|
|
|
segments.add(LineChartBarData(
|
|
spots: [spot1, spot2],
|
|
isCurved: false,
|
|
barWidth: 2,
|
|
gradient: LinearGradient(
|
|
colors: [color1, color2],
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
),
|
|
dotData: FlDotData(show: false), // We'll handle dots separately
|
|
));
|
|
}
|
|
|
|
// 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");
|
|
|
|
var data = [
|
|
...segments,
|
|
LineChartBarData(
|
|
spots: allSpots,
|
|
isCurved: false,
|
|
barWidth: 0, // invisible line
|
|
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,
|
|
);
|
|
},
|
|
),
|
|
)
|
|
];
|
|
|
|
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; // e.g., "This Result"
|
|
final DateTime date;
|
|
bool isStringResource;
|
|
|
|
DataPoint(
|
|
{required this.value,
|
|
required this.label,
|
|
required this.date,
|
|
this.isStringResource = false,
|
|
this.labelValue = 0.0});
|
|
}
|
|
|
|
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)})';
|
|
}
|
|
}
|