Merge branch 'branch' of https://github.com/kleponijo/klimatologi into branch

This commit is contained in:
Mochamad ongki ramadani 2026-05-13 18:50:51 +07:00
commit d4d07399a0
4 changed files with 124 additions and 61 deletions

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.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:klimatologiot/app.dart';
import 'package:user_repository/user_repository.dart'; import 'package:user_repository/user_repository.dart';
import 'package:monitoring_repository/monitoring_repository.dart'; import 'package:monitoring_repository/monitoring_repository.dart';
@ -14,7 +12,6 @@ void main() async {
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
await initializeDateFormatting('id_ID', null); await initializeDateFormatting('id_ID', null);
Bloc.observer = SimpleBlocObserver();
runApp(MyApp( runApp(MyApp(
FirebaseUserRepo(), FirebaseUserRepo(),
FirebaseMonitoringRepo(), FirebaseMonitoringRepo(),

View File

@ -82,18 +82,16 @@ class EvaporasiChartWidget extends StatelessWidget {
const tempMin = 0.0; const tempMin = 0.0;
const tempMax = 30.0; const tempMax = 30.0;
// Deduplicate X by index for evaporasi // Evaporasi mm biasanya >= 0, kita pakai min 0 agar estetik.
final evapSpotsAll = dailyValues.asMap().entries.map((e) { final evapMin = 0.0;
return FlSpot(e.key.toDouble(), _safeValue(e.value)); final evapMax = _clampDouble(evapMaxRaw * 1.15, 8.0, 50.0);
}).toList();
final Map<int, double> evapByX = {}; final Map<int, double> evapByX = {};
for (final s in evapSpotsAll) { for (final s in evapSpotsAll) {
evapByX[s.x.toInt()] = s.y; evapByX[s.x.toInt()] = s.y;
} }
final dedupEvapSpots = evapByX.entries final dedupEvapSpots = evapByX.entries.toList()
.toList()
..sort((a, b) => a.key.compareTo(b.key)); ..sort((a, b) => a.key.compareTo(b.key));
final tempByX = <int, double>{}; final tempByX = <int, double>{};
@ -114,12 +112,8 @@ class EvaporasiChartWidget extends StatelessWidget {
return FlSpot(x, y); return FlSpot(x, y);
}).toList(); }).toList();
final evapSpots = dedupEvapSpots final evapSpots =
.map((e) => FlSpot( dedupEvapSpots.map((e) => FlSpot(e.key.toDouble(), e.value)).toList();
e.key.toDouble(),
e.value.clamp(evapMin, evapMax),
))
.toList();
double getRightTitle(double y) { double getRightTitle(double y) {
return _evapScaleToTemp( return _evapScaleToTemp(
@ -135,7 +129,17 @@ class EvaporasiChartWidget extends StatelessWidget {
LineChartData( LineChartData(
minY: evapMin, minY: evapMin,
maxY: evapMax, 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), borderData: FlBorderData(show: false),
@ -144,7 +148,11 @@ class EvaporasiChartWidget extends StatelessWidget {
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
reservedSize: 32, reservedSize: 32,
interval: 1, interval: period == 'Hari Ini'
? 3
: period == 'Minggu Ini'
? 1
: 5,
getTitlesWidget: (value, meta) { getTitlesWidget: (value, meta) {
final index = value.toInt(); final index = value.toInt();
return Padding( return Padding(
@ -200,6 +208,9 @@ class EvaporasiChartWidget extends StatelessWidget {
getTooltipItems: (spots) { getTooltipItems: (spots) {
return spots.map((spot) { return spots.map((spot) {
final isTemp = spot.bar.color == Colors.orange.shade700;
if (isTemp) {
final temp = _evapScaleToTemp( final temp = _evapScaleToTemp(
yEvap: spot.y, yEvap: spot.y,
evapMin: evapMin, evapMin: evapMin,
@ -207,9 +218,24 @@ class EvaporasiChartWidget extends StatelessWidget {
tempMin: tempMin, tempMin: tempMin,
tempMax: tempMax, tempMax: tempMax,
); );
return LineTooltipItem( return LineTooltipItem(
'Evap: ${spot.y.toStringAsFixed(1)} mm\nSuhu: ${temp.toStringAsFixed(1)} °C', '🌡 Suhu\n${temp.toStringAsFixed(1)} °C',
const TextStyle(color: Colors.white, fontSize: 12), const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
);
}
return LineTooltipItem(
'💧 Evaporasi\n${spot.y.toStringAsFixed(1)} mm',
const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
); );
}).toList(); }).toList();
}, },
@ -219,27 +245,47 @@ class EvaporasiChartWidget extends StatelessWidget {
if (evapSpots.isNotEmpty) if (evapSpots.isNotEmpty)
LineChartBarData( LineChartBarData(
spots: evapSpots, spots: evapSpots,
isCurved: false, isCurved: true,
curveSmoothness: 0.35,
isStrokeCapRound: true,
color: Colors.blue.shade700, color: Colors.blue.shade700,
barWidth: 2, barWidth: 3,
dotData: FlDotData(show: false), showingIndicators: [0],
belowBarData: BarAreaData(show: false), 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) if (tempSpots.isNotEmpty)
LineChartBarData( LineChartBarData(
spots: tempSpots, spots: tempSpots,
isCurved: false, isCurved: true,
curveSmoothness: 0.35,
isStrokeCapRound: true,
color: Colors.orange.shade700, color: Colors.orange.shade700,
barWidth: 2, barWidth: 3,
dotData: FlDotData(show: false), showingIndicators: [0],
belowBarData: BarAreaData(show: false), dotData: FlDotData(
show: false,
checkToShowDot: (spot, barData) => false,
),
), ),
], ],
), ),
duration: const Duration(milliseconds: 350),
curve: Curves.easeOutCubic,
); );
return Container( return Container(
@ -258,16 +304,20 @@ class EvaporasiChartWidget extends StatelessWidget {
), ),
child: Column( child: Column(
children: [ children: [
Row( // Legend
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Wrap(
spacing: 16,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 10, width: 10,
height: 10, height: 10,
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.blue, color: Colors.blue.shade700,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
@ -283,12 +333,13 @@ class EvaporasiChartWidget extends StatelessWidget {
], ],
), ),
Row( Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 10, width: 10,
height: 10, height: 10,
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.orange, color: Colors.orange.shade700,
shape: BoxShape.circle, 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), const SizedBox(height: 8),
Expanded( Expanded(
child: ClipRRect( child: ClipRRect(
@ -317,4 +381,3 @@ class EvaporasiChartWidget extends StatelessWidget {
); );
} }
} }

View File

@ -19,6 +19,9 @@ class EvaporasiPeriodSelector extends StatelessWidget {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -30,10 +33,12 @@ class EvaporasiPeriodSelector extends StatelessWidget {
_buildDatePickerButton(context, viewMode, selectedDate), _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 // If in customDate mode, show period tabs as inactive
bool isActive = viewMode == EvaporasiViewMode.period && label == current; 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; final isActive = viewMode == EvaporasiViewMode.customDate;
return GestureDetector( return GestureDetector(
@ -79,7 +85,6 @@ Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode,
child: const EvaporasiDatePicker(), child: const EvaporasiDatePicker(),
), ),
); );
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@ -116,4 +121,3 @@ Widget _buildDatePickerButton(BuildContext context, EvaporasiViewMode viewMode,
return "${date.day}/${date.month}"; return "${date.day}/${date.month}";
} }
} }

View File

@ -52,7 +52,6 @@ class FirebaseMonitoringRepo implements MonitoringRepository {
final Map<dynamic, dynamic> data = snapshot.value as Map; final Map<dynamic, dynamic> data = snapshot.value as Map;
final list = data.values.map((item) { final list = data.values.map((item) {
print(item);
return mapper(item as Map<dynamic, dynamic>); return mapper(item as Map<dynamic, dynamic>);
}).toList(); }).toList();