pembaruan evaporasi grafik dan overflowed tanggal

This commit is contained in:
kleponijo 2026-05-13 10:57:54 +07:00
parent 6f2f7b91a5
commit 0315ec1d31
4 changed files with 135 additions and 56 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

@ -1,11 +1,7 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
const _tooltipBgColor = Colors.black87;
class EvaporasiChartWidget extends StatelessWidget {
final List<double> dailyValues;
final List<double> dailyTemperatures;
final String period;
@ -114,7 +110,7 @@ class EvaporasiChartWidget extends StatelessWidget {
// Evaporasi mm biasanya >= 0, kita pakai min 0 agar estetik.
final evapMin = 0.0;
final evapMax = _clampDouble(evapMaxRaw * 1.3, 10.0, 100.0);
final evapMax = _clampDouble(evapMaxRaw * 1.15, 8.0, 50.0);
// Suhu bisa saja 0 jika data kosong; tetap aman.
final tempMin = tempMinRaw;
@ -128,9 +124,8 @@ class EvaporasiChartWidget extends StatelessWidget {
evapByX[s.x.toInt()] = s.y;
}
final dedupEvapSpots = evapByX.entries
.toList()
..sort((a, b) => a.key.compareTo(b.key));
final dedupEvapSpots = evapByX.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
final Map<int, double> tempByX = {};
for (final entry in dailyTemperatures.asMap().entries) {
@ -149,9 +144,8 @@ class EvaporasiChartWidget extends StatelessWidget {
return FlSpot(x, y);
}).toList();
final evapSpots = dedupEvapSpots
.map((e) => FlSpot(e.key.toDouble(), e.value))
.toList();
final evapSpots =
dedupEvapSpots.map((e) => FlSpot(e.key.toDouble(), e.value)).toList();
if (evapSpots.isEmpty && tempSpots.isEmpty) {
return const SizedBox.shrink();
@ -167,19 +161,32 @@ class EvaporasiChartWidget extends StatelessWidget {
);
}
final chart = LineChart(
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),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
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(
@ -228,21 +235,35 @@ class EvaporasiChartWidget extends StatelessWidget {
handleBuiltInTouches: true,
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) {
return spots.map((spot) {
// fl_chart tidak mengekspos warna ke tooltip spot pada tipe LineBarSpot
// jadi kita tampilkan dua informasi sekaligus untuk memudahkan dosen.
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();
},
@ -253,9 +274,15 @@ class EvaporasiChartWidget extends StatelessWidget {
LineChartBarData(
spots: evapSpots,
isCurved: true,
curveSmoothness: 0.35,
isStrokeCapRound: true,
color: Colors.blue.shade700,
barWidth: 3,
dotData: FlDotData(show: false),
showingIndicators: [0],
dotData: FlDotData(
show: false,
checkToShowDot: (spot, barData) => false,
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
@ -272,12 +299,20 @@ class EvaporasiChartWidget extends StatelessWidget {
LineChartBarData(
spots: tempSpots,
isCurved: true,
curveSmoothness: 0.35,
isStrokeCapRound: true,
color: Colors.orange.shade700,
barWidth: 3,
dotData: FlDotData(show: false),
showingIndicators: [0],
dotData: FlDotData(
show: false,
checkToShowDot: (spot, barData) => false,
),
),
],
),
duration: const Duration(milliseconds: 350),
curve: Curves.easeOutCubic,
);
return Container(
@ -297,25 +332,70 @@ class EvaporasiChartWidget extends StatelessWidget {
child: Column(
children: [
// Legend
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Wrap(
spacing: 16,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.blue.shade700, shape: BoxShape.circle)),
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: Colors.blue.shade700,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text('Evaporasi (mm)', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.blueGrey)),
const Text(
'Evaporasi (mm)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.blueGrey,
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.orange.shade700, shape: BoxShape.circle)),
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: Colors.orange.shade700,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text('Suhu (°C)', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.brown)),
const Text(
'Suhu (°C)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.brown,
),
),
],
),
],
),
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(
@ -328,4 +408,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();