253 lines
8.8 KiB
Dart
253 lines
8.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import '../models/chart_data_point.dart';
|
|
|
|
class HistoryGrafikCard extends StatelessWidget {
|
|
final List<ChartDataPoint> dataPoints;
|
|
final VoidCallback? onBookmarkTap;
|
|
|
|
const HistoryGrafikCard({
|
|
super.key,
|
|
required this.dataPoints,
|
|
this.onBookmarkTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (dataPoints.isEmpty) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(32),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.show_chart, size: 48, color: Colors.grey[400]),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Belum ada data history',
|
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final hours = dataPoints.map((p) => p.hour.toDouble()).toList();
|
|
final drops = dataPoints.map((p) => p.dropsPerMinute.toDouble()).toList();
|
|
|
|
final minX = hours.reduce((a, b) => a < b ? a : b);
|
|
final maxX = hours.reduce((a, b) => a > b ? a : b);
|
|
final minY = (drops.reduce((a, b) => a < b ? a : b) - 5.0).clamp(
|
|
0.0,
|
|
double.infinity,
|
|
);
|
|
final maxY = drops.reduce((a, b) => a > b ? a : b) + 5.0;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.1),
|
|
spreadRadius: 0,
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE3F2FD),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(
|
|
Icons.insert_chart_outlined,
|
|
size: 20,
|
|
color: Color(0xFF2196F3),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
const Text(
|
|
'Grafik Infus Pasien',
|
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
SizedBox(
|
|
height: 180,
|
|
child: LineChart(
|
|
LineChartData(
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: (maxY - minY) / 4,
|
|
getDrawingHorizontalLine: (value) {
|
|
return FlLine(color: Colors.grey[200]!, strokeWidth: 1);
|
|
},
|
|
),
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 30,
|
|
interval: dataPoints.length > 5 ? 2.0 : 1.0,
|
|
getTitlesWidget: (double value, TitleMeta meta) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Text(
|
|
'${value.toInt()}:00',
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
interval: (maxY - minY) / 4,
|
|
reservedSize: 35,
|
|
getTitlesWidget: (double value, TitleMeta meta) {
|
|
return Text(
|
|
'${value.toInt()}',
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 11,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: Border(
|
|
left: BorderSide(color: Colors.grey[300]!),
|
|
bottom: BorderSide(color: Colors.grey[300]!),
|
|
),
|
|
),
|
|
minX: minX,
|
|
maxX: maxX,
|
|
minY: minY,
|
|
maxY: maxY,
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
spots: dataPoints
|
|
.map(
|
|
(point) => FlSpot(
|
|
point.hour.toDouble(),
|
|
point.dropsPerMinute.toDouble(),
|
|
),
|
|
)
|
|
.toList(),
|
|
isCurved: true,
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF2196F3), Color(0xFF1976D2)],
|
|
),
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
dotData: FlDotData(
|
|
show: true,
|
|
getDotPainter: (spot, percent, barData, index) {
|
|
return FlDotCirclePainter(
|
|
radius: 4,
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
strokeColor: const Color(0xFF2196F3),
|
|
);
|
|
},
|
|
),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
const Color(0xFF2196F3).withOpacity(0.3),
|
|
const Color(0xFF2196F3).withOpacity(0.0),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
lineTouchData: LineTouchData(
|
|
enabled: true,
|
|
touchTooltipData: LineTouchTooltipData(
|
|
getTooltipColor: (touchedSpot) => const Color(0xFF2196F3),
|
|
tooltipBorder: const BorderSide(
|
|
color: Color(0xFF1976D2),
|
|
width: 1,
|
|
),
|
|
tooltipPadding: const EdgeInsets.all(8),
|
|
tooltipMargin: 8,
|
|
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
|
|
return touchedBarSpots.map((barSpot) {
|
|
return LineTooltipItem(
|
|
'${barSpot.y.toInt()} tpm\n${barSpot.x.toInt()}:00',
|
|
const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 12,
|
|
height: 3,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF2196F3),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'Tetes per Menit',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.grey[600],
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |