import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; class WeightChart extends StatelessWidget { final List dataPoints; final double targetWeight; final String title; final Color lineColor; final Color gradientStartColor; final Color gradientEndColor; final String unit; const WeightChart({ super.key, required this.dataPoints, required this.targetWeight, this.title = 'Berat Real-time', this.lineColor = Colors.blue, this.gradientStartColor = Colors.blue, this.gradientEndColor = Colors.cyan, this.unit = 'g', }); @override Widget build(BuildContext context) { // Filter data yang valid untuk ditampilkan (ganti nilai 0 dengan nilai sebelumnya) List displayPoints = []; double? lastValidValue; for (var spot in dataPoints) { if (spot.y > 10) { // Data valid, gunakan nilai asli displayPoints.add(spot); lastValidValue = spot.y; } else if (lastValidValue != null) { // Data invalid (0), gunakan nilai terakhir yang valid displayPoints.add(FlSpot(spot.x, lastValidValue)); } } // Jika masih kosong setelah filter, gunakan data asli if (displayPoints.isEmpty) { displayPoints = dataPoints; } // Hitung min dan max Y berdasarkan data yang akan ditampilkan double minY = 0; double maxY = 200; if (displayPoints.isNotEmpty) { // Cari nilai min dan max dari data yang akan ditampilkan double dataMin = displayPoints.map((e) => e.y).reduce((a, b) => a < b ? a : b); double dataMax = displayPoints.map((e) => e.y).reduce((a, b) => a > b ? a : b); // Jika ada target, sertakan dalam perhitungan if (targetWeight > 0) { dataMin = dataMin < targetWeight ? dataMin : targetWeight; dataMax = dataMax > targetWeight ? dataMax : targetWeight; } // Pastikan minY tidak negatif dan ada padding yang cukup double range = dataMax - dataMin; double padding = range > 0 ? range * 0.25 : 50; // 25% padding atau minimal 50 minY = (dataMin - padding).clamp(0, double.infinity); maxY = dataMax + padding; // Pastikan ada range minimal 100 if (maxY - minY < 100) { double center = (maxY + minY) / 2; minY = (center - 50).clamp(0, double.infinity); maxY = center + 50; } // Debug log print('📊 Weight Chart: min=$minY, max=$maxY, dataMin=$dataMin, dataMax=$dataMax, displayPoints=${displayPoints.length}/${dataPoints.length}'); } else if (targetWeight > 0) { // Kalau belum ada data, gunakan target minY = 0; maxY = (targetWeight * 1.5).clamp(100, double.infinity); } else { // Fallback default minY = 0; maxY = 200; } 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: lineColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(Icons.scale, color: lineColor, size: 20), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), ), Row( children: [ Text( dataPoints.isNotEmpty ? '${dataPoints.last.y.toStringAsFixed(0)} $unit' : '-- $unit', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), const SizedBox(width: 8), Icon(Icons.arrow_forward, size: 12, color: Colors.grey.shade400), const SizedBox(width: 4), Text( '${targetWeight.toStringAsFixed(0)} $unit', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.green.shade600, ), ), ], ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: lineColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon( Icons.fiber_manual_record, color: lineColor, size: 8, ), const SizedBox(width: 4), const Text( 'LIVE', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.black54, ), ), ], ), ), ], ), const SizedBox(height: 16), SizedBox( height: 200, // Naik dari 180 jadi 200 biar lebih lapang child: dataPoints.isEmpty ? Center( child: Text( 'Menunggu data...', style: TextStyle( fontSize: 12, color: Colors.grey.shade400, ), ), ) : Padding( padding: const EdgeInsets.only(left: 8, right: 8), // Tambah padding kiri kanan child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 50, getDrawingHorizontalLine: (value) { // Garis target lebih tebal if ((value - targetWeight).abs() < 5) { 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: 5, getTitlesWidget: (value, meta) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( '${value.toInt()}s', style: const TextStyle( fontSize: 10, color: Colors.grey, ), ), ); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 50, reservedSize: 50, // Turun ke 50, karena masalahnya bukan disini getTitlesWidget: (value, meta) { // Tandai target dengan warna hijau final isTarget = (value - targetWeight).abs() < 5; return Text( '${value.toInt()}$unit', 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: displayPoints.isNotEmpty ? displayPoints.first.x : 0, maxX: displayPoints.isNotEmpty ? displayPoints.last.x : 10, minY: minY, maxY: maxY, extraLinesData: ExtraLinesData( horizontalLines: [ HorizontalLine( y: targetWeight, color: Colors.green.shade400, strokeWidth: 2, dashArray: [8, 4], label: HorizontalLineLabel( show: true, alignment: Alignment.topRight, padding: const EdgeInsets.only(right: 5, bottom: 5), style: TextStyle( color: Colors.green.shade700, fontSize: 10, fontWeight: FontWeight.bold, ), labelResolver: (line) => 'Target', ), ), ], ), lineBarsData: [ LineChartBarData( spots: displayPoints, isCurved: true, curveSmoothness: 0.3, color: lineColor, barWidth: 3, isStrokeCapRound: true, dotData: FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { // Hanya tampilkan dot di titik terakhir if (index == displayPoints.length - 1) { return FlDotCirclePainter( radius: 5, color: Colors.white, strokeWidth: 3, strokeColor: lineColor, ); } return FlDotCirclePainter( radius: 0, color: Colors.transparent, ); }, ), belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: [ gradientStartColor.withOpacity(0.3), gradientEndColor.withOpacity(0.1), Colors.transparent, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), ), ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipColor: (touchedSpot) => Colors.black87, tooltipRoundedRadius: 8, fitInsideHorizontally: true, fitInsideVertically: true, tooltipPadding: const EdgeInsets.all(8), tooltipMargin: 8, getTooltipItems: (List touchedSpots) { return touchedSpots.map((LineBarSpot touchedSpot) { final remaining = touchedSpot.y - targetWeight; return LineTooltipItem( '${touchedSpot.y.toStringAsFixed(0)} $unit\n', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, ), children: [ TextSpan( text: remaining > 0 ? 'Sisa ${remaining.toStringAsFixed(0)} $unit' : 'Target tercapai!', style: TextStyle( color: remaining > 0 ? Colors.orange.shade200 : Colors.green.shade200, fontSize: 10, ), ), ], ); }).toList(); }, ), handleBuiltInTouches: true, ), ), ), ), ), ], ), ); } }