pembaruan evaporasi grafik dan overflowed tanggal
This commit is contained in:
parent
6f2f7b91a5
commit
0315ec1d31
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue