import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../services/api_service.dart'; import '../models/sensor_data.dart'; class ChartScreen extends StatefulWidget { const ChartScreen({super.key}); @override State createState() => _ChartScreenState(); } class _ChartScreenState extends State { final ApiService _apiService = ApiService(); List historyData = []; bool isLoading = true; String selectedPeriod = '50'; // 50, 100, 200 @override void initState() { super.initState(); _loadHistoryData(); } Future _loadHistoryData() async { setState(() { isLoading = true; }); try { final data = await _apiService.getDataHistory(limit: int.parse(selectedPeriod)); if (mounted) { setState(() { historyData = data; isLoading = false; }); } } catch (e) { print('Error loading history: $e'); if (mounted) { setState(() { isLoading = false; }); } } } List _getSuhuSpots() { return historyData.asMap().entries.map((entry) { return FlSpot(entry.key.toDouble(), entry.value.suhu); }).toList(); } List _getBeratSpots() { return historyData.asMap().entries.map((entry) { return FlSpot(entry.key.toDouble(), entry.value.berat); }).toList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Grafik History'), backgroundColor: Colors.blue, foregroundColor: Colors.white, actions: [ PopupMenuButton( icon: const Icon(Icons.filter_list), onSelected: (value) { setState(() { selectedPeriod = value; _loadHistoryData(); }); }, itemBuilder: (context) => [ const PopupMenuItem(value: '50', child: Text('50 Data Terakhir')), const PopupMenuItem(value: '100', child: Text('100 Data Terakhir')), const PopupMenuItem(value: '200', child: Text('200 Data Terakhir')), ], ), ], ), body: isLoading ? const Center(child: CircularProgressIndicator()) : historyData.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox, size: 64, color: Colors.grey.shade400), const SizedBox(height: 16), Text( 'Belum ada data history', style: TextStyle( fontSize: 16, color: Colors.grey.shade600, ), ), ], ), ) : RefreshIndicator( onRefresh: _loadHistoryData, child: ListView( padding: const EdgeInsets.all(16), children: [ // Info card _buildInfoCard(), const SizedBox(height: 16), // Temperature chart _buildTemperatureChart(), const SizedBox(height: 16), // Weight chart _buildWeightChart(), ], ), ), ); } Widget _buildInfoCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue.shade400, Colors.blue.shade600], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ const Icon(Icons.info_outline, color: Colors.white, size: 32), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'History Data', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 4), Text( '$selectedPeriod data terakhir dari database', style: const TextStyle( fontSize: 12, color: Colors.white70, ), ), ], ), ), IconButton( onPressed: _loadHistoryData, icon: const Icon(Icons.refresh, color: Colors.white), ), ], ), ); } Widget _buildTemperatureChart() { final suhuSpots = _getSuhuSpots(); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.thermostat, color: Colors.orange, size: 24), ), const SizedBox(width: 12), const Expanded( child: Text( 'Grafik Suhu', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 16), SizedBox( height: 200, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 10, getDrawingHorizontalLine: (value) => FlLine( color: Colors.grey.shade200, 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: suhuSpots.length > 20 ? 20 : 10, getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < historyData.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( '#${value.toInt()}', style: const TextStyle(fontSize: 10, color: Colors.grey), ), ); } return const Text(''); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 10, reservedSize: 40, getTitlesWidget: (value, meta) => Text( '${value.toInt()}°C', style: const TextStyle(fontSize: 10, color: Colors.grey), ), ), ), ), borderData: FlBorderData( show: true, border: Border( bottom: BorderSide(color: Colors.grey.shade300), left: BorderSide(color: Colors.grey.shade300), ), ), minX: 0, maxX: suhuSpots.isNotEmpty ? suhuSpots.length.toDouble() - 1 : 10, minY: 20, maxY: 60, lineBarsData: [ LineChartBarData( spots: suhuSpots, isCurved: true, color: Colors.orange, barWidth: 3, dotData: const FlDotData(show: false), belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: [ Colors.orange.withOpacity(0.3), Colors.orange.withOpacity(0.1), Colors.transparent, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipColor: (touchedSpot) => Colors.black87, tooltipRoundedRadius: 8, getTooltipItems: (spots) { return spots.map((spot) { final index = spot.x.toInt(); if (index >= 0 && index < historyData.length) { final data = historyData[index]; return LineTooltipItem( '${spot.y.toStringAsFixed(1)}°C\n', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), children: [ TextSpan( text: data.timestamp, style: const TextStyle( fontSize: 10, color: Colors.white70, ), ), ], ); } return null; }).toList(); }, ), ), ), ), ), ], ), ); } Widget _buildWeightChart() { final beratSpots = _getBeratSpots(); final avgTarget = historyData.isNotEmpty ? historyData.map((e) => e.target).reduce((a, b) => a + b) / historyData.length : 0.0; // Filter data yang valid untuk ditampilkan (ganti nilai 0 dengan nilai sebelumnya atau null) List displaySpots = []; double? lastValidValue; for (var spot in beratSpots) { if (spot.y > 10) { // Data valid, gunakan nilai asli displaySpots.add(spot); lastValidValue = spot.y; } else if (lastValidValue != null) { // Data invalid (0), gunakan nilai terakhir yang valid displaySpots.add(FlSpot(spot.x, lastValidValue)); } // Jika lastValidValue null dan spot.y invalid, skip point ini } // Jika masih kosong setelah filter, gunakan data asli if (displaySpots.isEmpty) { displaySpots = beratSpots; } // Hitung minY dan maxY yang aman untuk menghindari overflow double minY = 0; double maxY = 200; if (displaySpots.isNotEmpty) { double dataMin = displaySpots.map((e) => e.y).reduce((a, b) => a < b ? a : b); double dataMax = displaySpots.map((e) => e.y).reduce((a, b) => a > b ? a : b); // Sertakan target dalam perhitungan if (avgTarget > 0) { dataMin = dataMin < avgTarget ? dataMin : avgTarget; dataMax = dataMax > avgTarget ? dataMax : avgTarget; } // Tambah padding 25% double range = dataMax - dataMin; double padding = range > 0 ? range * 0.25 : 50; minY = (dataMin - padding).clamp(0, double.infinity); maxY = dataMax + padding; // Pastikan range minimal 100 if (maxY - minY < 100) { double center = (maxY + minY) / 2; minY = (center - 50).clamp(0, double.infinity); maxY = center + 50; } } else if (avgTarget > 0) { minY = 0; maxY = (avgTarget * 1.5).clamp(100, double.infinity); } return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.scale, color: Colors.blue, size: 24), ), const SizedBox(width: 12), const Expanded( child: Text( 'Grafik Berat', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 16), SizedBox( height: 200, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 50, getDrawingHorizontalLine: (value) { if ((value - avgTarget).abs() < 10) { return FlLine( color: Colors.green.shade300, strokeWidth: 2, dashArray: [5, 5], ); } return FlLine( color: Colors.grey.shade200, 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: beratSpots.length > 20 ? 20 : 10, getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < historyData.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( '#${value.toInt()}', style: const TextStyle(fontSize: 10, color: Colors.grey), ), ); } return const Text(''); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 50, reservedSize: 45, getTitlesWidget: (value, meta) { final isTarget = (value - avgTarget).abs() < 10; return Text( '${value.toInt()}g', style: TextStyle( fontSize: 10, color: isTarget ? Colors.green : Colors.grey, fontWeight: isTarget ? FontWeight.bold : FontWeight.normal, ), ); }, ), ), ), borderData: FlBorderData( show: true, border: Border( bottom: BorderSide(color: Colors.grey.shade300), left: BorderSide(color: Colors.grey.shade300), ), ), minX: 0, maxX: beratSpots.isNotEmpty ? beratSpots.length.toDouble() - 1 : 10, minY: minY, maxY: maxY, extraLinesData: ExtraLinesData( horizontalLines: [ if (avgTarget > 0) HorizontalLine( y: avgTarget, color: Colors.green.shade400, strokeWidth: 2, dashArray: [8, 4], label: HorizontalLineLabel( show: true, alignment: Alignment.topRight, style: TextStyle( color: Colors.green.shade700, fontSize: 10, fontWeight: FontWeight.bold, ), labelResolver: (line) => 'Target', ), ), ], ), lineBarsData: [ LineChartBarData( spots: displaySpots, isCurved: true, color: Colors.blue, barWidth: 3, dotData: const FlDotData(show: false), belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: [ Colors.blue.withOpacity(0.3), Colors.blue.withOpacity(0.1), Colors.transparent, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipColor: (touchedSpot) => Colors.black87, tooltipRoundedRadius: 8, getTooltipItems: (spots) { return spots.map((spot) { final index = spot.x.toInt(); if (index >= 0 && index < historyData.length) { final data = historyData[index]; return LineTooltipItem( '${spot.y.toStringAsFixed(0)}g\n', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), children: [ TextSpan( text: 'Target: ${data.target.toStringAsFixed(0)}g\n', style: const TextStyle( fontSize: 10, color: Colors.green, ), ), TextSpan( text: data.timestamp, style: const TextStyle( fontSize: 10, color: Colors.white70, ), ), ], ); } return null; }).toList(); }, ), ), ), ), ), ], ), ); } Widget _buildStatisticsCard() { if (historyData.isEmpty) return const SizedBox.shrink(); final avgSuhu = historyData.map((e) => e.suhu).reduce((a, b) => a + b) / historyData.length; final maxSuhu = historyData.map((e) => e.suhu).reduce((a, b) => a > b ? a : b); final minSuhu = historyData.map((e) => e.suhu).reduce((a, b) => a < b ? a : b); final avgBerat = historyData.map((e) => e.berat).reduce((a, b) => a + b) / historyData.length; final maxBerat = historyData.map((e) => e.berat).reduce((a, b) => a > b ? a : b); final minBerat = historyData.map((e) => e.berat).reduce((a, b) => a < b ? a : b); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Statistik', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildStatItem( icon: Icons.thermostat, iconColor: Colors.orange, label: 'Rata-rata Suhu', value: '${avgSuhu.toStringAsFixed(1)}°C', ), ), const SizedBox(width: 12), Expanded( child: _buildStatItem( icon: Icons.scale, iconColor: Colors.blue, label: 'Rata-rata Berat', value: '${avgBerat.toStringAsFixed(0)}g', ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildStatItem( icon: Icons.arrow_upward, iconColor: Colors.red, label: 'Max Suhu', value: '${maxSuhu.toStringAsFixed(1)}°C', ), ), const SizedBox(width: 12), Expanded( child: _buildStatItem( icon: Icons.arrow_downward, iconColor: Colors.green, label: 'Min Suhu', value: '${minSuhu.toStringAsFixed(1)}°C', ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildStatItem( icon: Icons.arrow_upward, iconColor: Colors.red, label: 'Max Berat', value: '${maxBerat.toStringAsFixed(0)}g', ), ), const SizedBox(width: 12), Expanded( child: _buildStatItem( icon: Icons.arrow_downward, iconColor: Colors.green, label: 'Min Berat', value: '${minBerat.toStringAsFixed(0)}g', ), ), ], ), ], ), ); } Widget _buildStatItem({ required IconData icon, required Color iconColor, required String label, required String value, }) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Icon(icon, color: iconColor, size: 24), const SizedBox(height: 8), Text( value, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( label, style: const TextStyle( fontSize: 11, color: Colors.grey, ), ), ], ), ); } }