import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; /// πŸ“Š [곡톡 μœ„μ ―] λͺ¨λ“  μ„Όμ„œ(μ˜¨λ„/μŠ΅λ„/CO2 λ“±)κ°€ κ³΅μœ ν•˜λŠ” μ‹€μ‹œκ°„ 차트 μΉ΄λ“œ. /// /// DashboardGrid 의 `_buildSensorCard` μŠ€νƒ€μΌ(λ°°κ²½ #1F242C, λΌμš΄λ“œ 16, /// white10 ν…Œλ‘λ¦¬, 헀더 + κ°•μ‘° 색상)을 κ·ΈλŒ€λ‘œ λ”°λ₯΄λ©°, ν•˜λ‹¨μ— fl_chart /// 라인 차트λ₯Ό λΆ™μ—¬ [stream] 으둜 ν˜λŸ¬μ˜€λŠ” 데이터λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ κ·Έλ¦°λ‹€. /// /// μ‚¬μš© 예: /// ```dart /// SensorChartCard( /// title: 'μ˜¨λ„', /// icon: Icons.thermostat, /// accentColor: Colors.orangeAccent, /// unit: 'Β°C', /// stream: tempService.stream, /// ) /// ``` class SensorChartCard extends StatelessWidget { const SensorChartCard({ super.key, required this.title, required this.icon, required this.accentColor, required this.unit, required this.stream, }); /// μΉ΄λ“œ 제λͺ© (예: 'μ˜¨λ„', 'μŠ΅λ„', 'μ΄μ‚°ν™”νƒ„μ†Œ') final String title; /// 헀더 μš°μΈ‘μ— ν‘œμ‹œν•  μ•„μ΄μ½˜ final IconData icon; /// 라인/μ•„μ΄μ½˜/ν˜„μž¬κ°’μ— 쓰일 κ°•μ‘° 색상 final Color accentColor; /// κ°’ 뒀에 λΆ™λŠ” λ‹¨μœ„ (예: 'Β°C', '%', 'ppm') final String unit; /// 갱신될 λ•Œλ§ˆλ‹€ "전체 μΈ‘μ •κ°’ 리슀트"κ°€ ν˜λŸ¬λ‚˜μ˜€λŠ” 슀트림 final Stream> stream; static const Color _cardColor = Color(0xFF1F242C); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: _cardColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white10), ), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), child: StreamBuilder>( stream: stream, builder: (context, snapshot) { final data = snapshot.data ?? const []; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(data), const SizedBox(height: 20), Expanded(child: _buildChart(data)), ], ); }, ), ); } // 🏷️ 헀더: 제λͺ© + ν˜„μž¬κ°’ + μ•„μ΄μ½˜ (μ„Όμ„œ μΉ΄λ“œμ™€ λ™μΌν•œ 톀) Widget _buildHeader(List data) { final hasData = data.isNotEmpty; final latest = hasData ? data.last : null; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 16, color: Colors.white70, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( hasData ? latest!.toStringAsFixed(1) : '--', style: const TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(width: 4), Text( unit, style: const TextStyle(fontSize: 14, color: Colors.white38), ), ], ), ], ), Icon(icon, color: accentColor.withOpacity(0.8), size: 22), ], ); } // πŸ“ˆ 라인 차트 (fl_chart) Widget _buildChart(List data) { if (data.isEmpty) { return const Center( child: SizedBox( height: 24, width: 24, child: CircularProgressIndicator( strokeWidth: 2, color: Color(0xFFB4E49C), ), ), ); } final spots = [ for (var i = 0; i < data.length; i++) FlSpot(i.toDouble(), data[i]), ]; final minY = data.reduce((a, b) => a < b ? a : b); final maxY = data.reduce((a, b) => a > b ? a : b); // μœ„μ•„λž˜λ‘œ μ•½κ°„ 여백을 λ‘¬μ„œ 선이 ν…Œλ‘λ¦¬μ— λΆ™μ§€ μ•Šκ²Œ ν•œλ‹€. final padding = ((maxY - minY).abs() * 0.15).clamp(1.0, double.infinity); return LineChart( LineChartData( minX: 0, maxX: (data.length - 1).toDouble(), minY: minY - padding, maxY: maxY + padding, gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: ((maxY + padding) - (minY - padding)) / 4, getDrawingHorizontalLine: (value) => const FlLine(color: Colors.white10, strokeWidth: 1), ), titlesData: const FlTitlesData(show: false), borderData: FlBorderData(show: false), lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipColor: (_) => _cardColor.withOpacity(0.9), getTooltipItems: (touchedSpots) => [ for (final s in touchedSpots) LineTooltipItem( '${s.y.toStringAsFixed(1)} $unit', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12, ), ), ], ), ), lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, color: accentColor, barWidth: 2.5, dotData: const FlDotData(show: false), belowBarData: BarAreaData( show: true, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ accentColor.withOpacity(0.25), accentColor.withOpacity(0.0), ], ), ), ), ], ), ); } }