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: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(),

View File

@ -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<int, double> 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 = <int, double>{};
@ -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 {
);
}
}

View File

@ -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<EvaporasiBloc>();
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}";
}
}

View File

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