Compare commits
2 Commits
e79c521ea9
...
c09170b506
| Author | SHA1 | Date |
|---|---|---|
|
|
c09170b506 | 4 months ago |
|
|
5461d78a1b | 4 months ago |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,297 @@
|
||||
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;
|
||||
|
||||
const DynamicResultChart(
|
||||
{super.key, required this.dataPoints, required this.thresholds});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final minY = thresholds.first.value;
|
||||
final maxY = thresholds.last.value + 1;
|
||||
|
||||
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(
|
||||
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 [];
|
||||
|
||||
// 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}");
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
),
|
||||
),
|
||||
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");
|
||||
|
||||
return [
|
||||
...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,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
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)})';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue