ppew
This commit is contained in:
parent
6f2f7b91a5
commit
5a297ff456
14
TODO.md
14
TODO.md
|
|
@ -1,10 +1,10 @@
|
||||||
## TODO - Evaporasi (Dual Axis + History Firebase)
|
# TODO
|
||||||
|
|
||||||
### Checklist
|
- [ ] Plan & identifikasi penyebab garis merah pada `evaporasi_chart_widget.dart`.
|
||||||
- [x] Cek: List Evaporasi sudah mengambil `state.history` dari Firebase `getSensorHistory`.
|
- [ ] Periksa isi file `evaporasi_chart_widget.dart` untuk potensi error sintaks/typing.
|
||||||
- [x] Cek: Filter custom date pada list menggunakan `state.history` (data terdahulu sudah tersedia).
|
- [ ] Lakukan perbaikan kode sesuai penyebabnya.
|
||||||
- [x] Ubah grafik evaporasi menjadi **dual-axis** (melalui mapping skala + label kiri/kanan).
|
- [x] Analisis file `evaporasi_chart_widget.dart` untuk potensi error sintaks/typing yang memicu garis merah.
|
||||||
- [ ] Perbaiki mismatch chart & list dengan sumber history Firebase (sinkronisasi agregasi/label/period + timezone).
|
- [x] Perbaiki penyebab garis merah pada `evaporasi_chart_widget.dart` (syntax/indentation/typing).
|
||||||
- [ ] Jalankan `flutter analyze` dan minimal compile aplikasi.
|
- [ ] Jalankan `flutter format` dan `flutter analyze` (manual di terminal) untuk memastikan error hilang.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const _tooltipBgColor = Colors.black87;
|
|
||||||
|
|
||||||
|
|
||||||
class EvaporasiChartWidget extends StatelessWidget {
|
class EvaporasiChartWidget extends StatelessWidget {
|
||||||
|
|
||||||
final List<double> dailyValues;
|
final List<double> dailyValues;
|
||||||
final List<double> dailyTemperatures;
|
final List<double> dailyTemperatures;
|
||||||
final String period;
|
final String period;
|
||||||
|
|
@ -24,29 +20,6 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
return value < 0 ? 0.0 : value;
|
return value < 0 ? 0.0 : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
double _minOf(List<double> values) {
|
|
||||||
if (values.isEmpty) return 0.0;
|
|
||||||
return values.map(_safeValue).reduce((a, b) => a < b ? a : b);
|
|
||||||
}
|
|
||||||
|
|
||||||
double _maxOf(List<double> values) {
|
|
||||||
if (values.isEmpty) return 0.0;
|
|
||||||
return values.map(_safeValue).reduce((a, b) => a > b ? a : b);
|
|
||||||
}
|
|
||||||
|
|
||||||
double _clampDouble(double v, double minV, double maxV) {
|
|
||||||
if (v.isNaN || v.isInfinite) return minV;
|
|
||||||
return v.clamp(minV, maxV);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<FlSpot> _evapSpots() {
|
|
||||||
return dailyValues.asMap().entries.map((entry) {
|
|
||||||
return FlSpot(entry.key.toDouble(), _safeValue(entry.value));
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mapping suhu (°C) -> posisi Y internal agar bisa ditampilkan dalam chart
|
|
||||||
/// yang sama dengan skala evaporasi (mm).
|
|
||||||
double _tempToEvapScale({
|
double _tempToEvapScale({
|
||||||
required double temp,
|
required double temp,
|
||||||
required double evapMin,
|
required double evapMin,
|
||||||
|
|
@ -54,16 +27,13 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
required double tempMin,
|
required double tempMin,
|
||||||
required double tempMax,
|
required double tempMax,
|
||||||
}) {
|
}) {
|
||||||
// Hindari pembagian nol
|
|
||||||
final tempRange = (tempMax - tempMin);
|
final tempRange = (tempMax - tempMin);
|
||||||
if (tempRange.abs() < 1e-9) return evapMin;
|
if (tempRange.abs() < 1e-9) return evapMin;
|
||||||
|
|
||||||
final normalized = (temp - tempMin) / tempRange; // 0..1 (secara ideal)
|
final normalized = (temp - tempMin) / tempRange; // 0..1 ideal
|
||||||
final scaled = evapMin + normalized * (evapMax - evapMin);
|
return evapMin + normalized * (evapMax - evapMin);
|
||||||
return scaled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverse mapping Y internal (skala evaporasi) -> suhu asli (°C)
|
|
||||||
double _evapScaleToTemp({
|
double _evapScaleToTemp({
|
||||||
required double yEvap,
|
required double yEvap,
|
||||||
required double evapMin,
|
required double evapMin,
|
||||||
|
|
@ -99,30 +69,24 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
color: Colors.black.withAlpha(13),
|
color: Colors.black.withAlpha(13),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 5),
|
offset: const Offset(0, 5),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Text('Tidak ada data chart'),
|
child: const Text('Tidak ada data chart'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hitung range masing-masing agar axis kanan (°C) masuk akal.
|
// Sesuai permintaan: evaporasi 0..20 dan suhu 0..30
|
||||||
final evapMinRaw = _minOf(dailyValues);
|
const evapMin = 0.0;
|
||||||
final evapMaxRaw = _maxOf(dailyValues);
|
const evapMax = 20.0;
|
||||||
final tempMinRaw = _minOf(dailyTemperatures);
|
const tempMin = 0.0;
|
||||||
final tempMaxRaw = _maxOf(dailyTemperatures);
|
const tempMax = 30.0;
|
||||||
|
|
||||||
// Evaporasi mm biasanya >= 0, kita pakai min 0 agar estetik.
|
// Deduplicate X by index for evaporasi
|
||||||
final evapMin = 0.0;
|
final evapSpotsAll = dailyValues.asMap().entries.map((e) {
|
||||||
final evapMax = _clampDouble(evapMaxRaw * 1.3, 10.0, 100.0);
|
return FlSpot(e.key.toDouble(), _safeValue(e.value));
|
||||||
|
}).toList();
|
||||||
|
|
||||||
// Suhu bisa saja 0 jika data kosong; tetap aman.
|
|
||||||
final tempMin = tempMinRaw;
|
|
||||||
final tempMax = tempMaxRaw == tempMinRaw ? tempMinRaw + 1 : tempMaxRaw;
|
|
||||||
|
|
||||||
final evapSpotsAll = _evapSpots();
|
|
||||||
|
|
||||||
// Deduplicate X=hour agar garis tidak kelihatan dobel/acak.
|
|
||||||
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;
|
||||||
|
|
@ -130,12 +94,13 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
|
|
||||||
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 Map<int, double> tempByX = {};
|
final tempByX = <int, double>{};
|
||||||
for (final entry in dailyTemperatures.asMap().entries) {
|
for (final entry in dailyTemperatures.asMap().entries) {
|
||||||
tempByX[entry.key] = _safeValue(entry.value);
|
tempByX[entry.key] = _safeValue(entry.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
final tempSpots = tempByX.entries.map((entry) {
|
final tempSpots = tempByX.entries.map((entry) {
|
||||||
final x = entry.key.toDouble();
|
final x = entry.key.toDouble();
|
||||||
final temp = entry.value;
|
final temp = entry.value;
|
||||||
|
|
@ -150,14 +115,13 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final evapSpots = dedupEvapSpots
|
final evapSpots = dedupEvapSpots
|
||||||
.map((e) => FlSpot(e.key.toDouble(), e.value))
|
.map((e) => FlSpot(
|
||||||
|
e.key.toDouble(),
|
||||||
|
e.value.clamp(evapMin, evapMax),
|
||||||
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (evapSpots.isEmpty && tempSpots.isEmpty) {
|
double getRightTitle(double y) {
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
double _getRightTitle(double y) {
|
|
||||||
return _evapScaleToTemp(
|
return _evapScaleToTemp(
|
||||||
yEvap: y,
|
yEvap: y,
|
||||||
evapMin: evapMin,
|
evapMin: evapMin,
|
||||||
|
|
@ -167,13 +131,14 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final chart = LineChart(
|
final chart = LineChart(
|
||||||
LineChartData(
|
LineChartData(
|
||||||
minY: evapMin,
|
minY: evapMin,
|
||||||
maxY: evapMax,
|
maxY: evapMax,
|
||||||
gridData: FlGridData(show: false),
|
gridData: FlGridData(show: false),
|
||||||
borderData: FlBorderData(show: false),
|
borderData: FlBorderData(show: false),
|
||||||
|
|
||||||
|
|
||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
bottomTitles: AxisTitles(
|
bottomTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
|
|
@ -198,10 +163,12 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
reservedSize: 40,
|
reservedSize: 40,
|
||||||
interval: (evapMax - evapMin) / 4,
|
interval: (evapMax - evapMin) / 4,
|
||||||
getTitlesWidget: (value, meta) {
|
getTitlesWidget: (value, meta) {
|
||||||
final v = value;
|
|
||||||
return Text(
|
return Text(
|
||||||
'${v.toStringAsFixed(0)}',
|
value.toStringAsFixed(0),
|
||||||
style: const TextStyle(color: Colors.blueGrey, fontSize: 10),
|
style: const TextStyle(
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -212,27 +179,27 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
reservedSize: 40,
|
reservedSize: 40,
|
||||||
interval: (evapMax - evapMin) / 4,
|
interval: (evapMax - evapMin) / 4,
|
||||||
getTitlesWidget: (value, meta) {
|
getTitlesWidget: (value, meta) {
|
||||||
final t = _getRightTitle(value);
|
final t = getRightTitle(value);
|
||||||
return Text(
|
return Text(
|
||||||
'${t.toStringAsFixed(0)}',
|
t.toStringAsFixed(0),
|
||||||
style: const TextStyle(color: Colors.brown, fontSize: 10),
|
style: const TextStyle(
|
||||||
|
color: Colors.brown,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
topTitles: const AxisTitles(
|
topTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(showTitles: false),
|
sideTitles: SideTitles(showTitles: false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
lineTouchData: LineTouchData(
|
lineTouchData: LineTouchData(
|
||||||
handleBuiltInTouches: true,
|
handleBuiltInTouches: true,
|
||||||
touchTooltipData: LineTouchTooltipData(
|
touchTooltipData: LineTouchTooltipData(
|
||||||
|
|
||||||
getTooltipItems: (spots) {
|
getTooltipItems: (spots) {
|
||||||
|
|
||||||
|
|
||||||
return spots.map((spot) {
|
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(
|
final temp = _evapScaleToTemp(
|
||||||
yEvap: spot.y,
|
yEvap: spot.y,
|
||||||
evapMin: evapMin,
|
evapMin: evapMin,
|
||||||
|
|
@ -252,36 +219,31 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
if (evapSpots.isNotEmpty)
|
if (evapSpots.isNotEmpty)
|
||||||
LineChartBarData(
|
LineChartBarData(
|
||||||
spots: evapSpots,
|
spots: evapSpots,
|
||||||
isCurved: true,
|
isCurved: false,
|
||||||
color: Colors.blue.shade700,
|
color: Colors.blue.shade700,
|
||||||
barWidth: 3,
|
barWidth: 2,
|
||||||
dotData: FlDotData(show: false),
|
dotData: FlDotData(show: false),
|
||||||
belowBarData: BarAreaData(
|
belowBarData: BarAreaData(show: false),
|
||||||
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: true,
|
isCurved: false,
|
||||||
color: Colors.orange.shade700,
|
color: Colors.orange.shade700,
|
||||||
barWidth: 3,
|
barWidth: 2,
|
||||||
dotData: FlDotData(show: false),
|
dotData: FlDotData(show: false),
|
||||||
|
belowBarData: BarAreaData(show: false),
|
||||||
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 340,
|
height: 380,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
@ -291,27 +253,54 @@ class EvaporasiChartWidget extends StatelessWidget {
|
||||||
color: Colors.black.withAlpha(13),
|
color: Colors.black.withAlpha(13),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 5),
|
offset: const Offset(0, 5),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Legend
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.blue.shade700, shape: BoxShape.circle)),
|
Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.orange.shade700, shape: BoxShape.circle)),
|
Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.orange,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue