diff --git a/lib/main.dart b/lib/main.dart index f0093ed..bd3e7d5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:klimatologiot/simple_bloc_observer.dart'; import 'package:klimatologiot/app.dart'; import 'package:user_repository/user_repository.dart'; import 'package:monitoring_repository/monitoring_repository.dart'; @@ -14,7 +12,6 @@ void main() async { options: DefaultFirebaseOptions.currentPlatform, ); await initializeDateFormatting('id_ID', null); - Bloc.observer = SimpleBlocObserver(); runApp(MyApp( FirebaseUserRepo(), FirebaseMonitoringRepo(), diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart index 2e829f5..a561758 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart @@ -82,18 +82,16 @@ class EvaporasiChartWidget extends StatelessWidget { const tempMin = 0.0; const tempMax = 30.0; - // Deduplicate X by index for evaporasi - final evapSpotsAll = dailyValues.asMap().entries.map((e) { - return FlSpot(e.key.toDouble(), _safeValue(e.value)); - }).toList(); + // Evaporasi mm biasanya >= 0, kita pakai min 0 agar estetik. + final evapMin = 0.0; + final evapMax = _clampDouble(evapMaxRaw * 1.15, 8.0, 50.0); final Map evapByX = {}; for (final s in evapSpotsAll) { evapByX[s.x.toInt()] = s.y; } - final dedupEvapSpots = evapByX.entries - .toList() + final dedupEvapSpots = evapByX.entries.toList() ..sort((a, b) => a.key.compareTo(b.key)); final tempByX = {}; @@ -114,12 +112,8 @@ class EvaporasiChartWidget extends StatelessWidget { return FlSpot(x, y); }).toList(); - final evapSpots = dedupEvapSpots - .map((e) => FlSpot( - e.key.toDouble(), - e.value.clamp(evapMin, evapMax), - )) - .toList(); + final evapSpots = + dedupEvapSpots.map((e) => FlSpot(e.key.toDouble(), e.value)).toList(); double getRightTitle(double y) { return _evapScaleToTemp( @@ -135,7 +129,17 @@ class EvaporasiChartWidget extends StatelessWidget { LineChartData( minY: evapMin, maxY: evapMax, - gridData: FlGridData(show: false), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: (evapMax - evapMin) / 4, + getDrawingHorizontalLine: (value) { + return FlLine( + color: Colors.grey.withOpacity(0.15), + strokeWidth: 1, + ); + }, + ), borderData: FlBorderData(show: false), @@ -144,7 +148,11 @@ class EvaporasiChartWidget extends StatelessWidget { sideTitles: SideTitles( showTitles: true, reservedSize: 32, - interval: 1, + interval: period == 'Hari Ini' + ? 3 + : period == 'Minggu Ini' + ? 1 + : 5, getTitlesWidget: (value, meta) { final index = value.toInt(); return Padding( @@ -200,16 +208,34 @@ class EvaporasiChartWidget extends StatelessWidget { getTooltipItems: (spots) { return spots.map((spot) { - final temp = _evapScaleToTemp( - yEvap: spot.y, - evapMin: evapMin, - evapMax: evapMax, - tempMin: tempMin, - tempMax: tempMax, - ); + final isTemp = spot.bar.color == Colors.orange.shade700; + + if (isTemp) { + final temp = _evapScaleToTemp( + yEvap: spot.y, + evapMin: evapMin, + evapMax: evapMax, + tempMin: tempMin, + tempMax: tempMax, + ); + + return LineTooltipItem( + '🌡 Suhu\n${temp.toStringAsFixed(1)} °C', + const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ); + } + return LineTooltipItem( - 'Evap: ${spot.y.toStringAsFixed(1)} mm\nSuhu: ${temp.toStringAsFixed(1)} °C', - const TextStyle(color: Colors.white, fontSize: 12), + '💧 Evaporasi\n${spot.y.toStringAsFixed(1)} mm', + const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), ); }).toList(); }, @@ -219,27 +245,47 @@ class EvaporasiChartWidget extends StatelessWidget { if (evapSpots.isNotEmpty) LineChartBarData( spots: evapSpots, - isCurved: false, + isCurved: true, + curveSmoothness: 0.35, + isStrokeCapRound: true, color: Colors.blue.shade700, - barWidth: 2, - dotData: FlDotData(show: false), - belowBarData: BarAreaData(show: false), - - + barWidth: 3, + showingIndicators: [0], + dotData: FlDotData( + show: false, + checkToShowDot: (spot, barData) => false, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + Colors.blue.withAlpha(64), + Colors.blue.withAlpha(13), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), ), if (tempSpots.isNotEmpty) LineChartBarData( spots: tempSpots, - isCurved: false, + isCurved: true, + curveSmoothness: 0.35, + isStrokeCapRound: true, color: Colors.orange.shade700, - barWidth: 2, - dotData: FlDotData(show: false), - belowBarData: BarAreaData(show: false), - + barWidth: 3, + showingIndicators: [0], + dotData: FlDotData( + show: false, + checkToShowDot: (spot, barData) => false, + ), ), ], ), + duration: const Duration(milliseconds: 350), + curve: Curves.easeOutCubic, ); return Container( @@ -258,16 +304,20 @@ class EvaporasiChartWidget extends StatelessWidget { ), child: Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // Legend + Wrap( + spacing: 16, + runSpacing: 8, + alignment: WrapAlignment.center, children: [ Row( + mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, - decoration: const BoxDecoration( - color: Colors.blue, + decoration: BoxDecoration( + color: Colors.blue.shade700, shape: BoxShape.circle, ), ), @@ -283,12 +333,13 @@ class EvaporasiChartWidget extends StatelessWidget { ], ), Row( + mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, - decoration: const BoxDecoration( - color: Colors.orange, + decoration: BoxDecoration( + color: Colors.orange.shade700, shape: BoxShape.circle, ), ), @@ -305,6 +356,19 @@ class EvaporasiChartWidget extends StatelessWidget { ), ], ), + + Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Text( + 'Monitoring Evaporasi & Suhu', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.blueGrey.shade700, + ), + ), + ), + const SizedBox(height: 8), Expanded( child: ClipRRect( @@ -317,4 +381,3 @@ class EvaporasiChartWidget extends StatelessWidget { ); } } - diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart index 2d5e17c..0b3d742 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_period_selector.dart @@ -19,21 +19,26 @@ class EvaporasiPeriodSelector extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(15), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildTab(context, "Hari Ini", selectedPeriod, viewMode), - _buildTab(context, "Minggu Ini", selectedPeriod, viewMode), - _buildTab(context, "Bulan Ini", selectedPeriod, viewMode), - const SizedBox(width: 8), - // Date picker button - _buildDatePickerButton(context, viewMode, selectedDate), - ], + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTab(context, "Hari Ini", selectedPeriod, viewMode), + _buildTab(context, "Minggu Ini", selectedPeriod, viewMode), + _buildTab(context, "Bulan Ini", selectedPeriod, viewMode), + const SizedBox(width: 8), + // Date picker button + _buildDatePickerButton(context, viewMode, selectedDate), + ], + ), ), ); } - Widget _buildTab(BuildContext context, String label, String current, EvaporasiViewMode viewMode) { + Widget _buildTab(BuildContext context, String label, String current, + EvaporasiViewMode viewMode) { // If in customDate mode, show period tabs as inactive bool isActive = viewMode == EvaporasiViewMode.period && label == current; @@ -59,7 +64,8 @@ class EvaporasiPeriodSelector extends StatelessWidget { ); } -Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode, DateTime? selectedDate) { + Widget _buildDatePickerButton(BuildContext context, + EvaporasiViewMode viewMode, DateTime? selectedDate) { final isActive = viewMode == EvaporasiViewMode.customDate; return GestureDetector( @@ -67,8 +73,8 @@ Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode, // Set mode ke customDate SEBELUM membuka date picker final bloc = context.read(); bloc.add( - const EvaporasiViewModeChanged(EvaporasiViewMode.customDate), - ); + const EvaporasiViewModeChanged(EvaporasiViewMode.customDate), + ); showModalBottomSheet( context: context, @@ -79,7 +85,6 @@ Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode, child: const EvaporasiDatePicker(), ), ); - }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -116,4 +121,3 @@ Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode, return "${date.day}/${date.month}"; } } - diff --git a/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart index 060d254..f7cf78c 100644 --- a/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart +++ b/packages/monitoring_repository/lib/src/firebase_monitoring_repo.dart @@ -52,7 +52,6 @@ class FirebaseMonitoringRepo implements MonitoringRepository { final Map data = snapshot.value as Map; final list = data.values.map((item) { - print(item); return mapper(item as Map); }).toList();