From 5a297ff456c07ec61ee14a2c3cef95dfa2659498 Mon Sep 17 00:00:00 2001 From: Mochamad ongki ramadani Date: Wed, 13 May 2026 18:48:44 +0700 Subject: [PATCH] ppew --- TODO.md | 14 +- .../views/widgets/evaporasi_chart_widget.dart | 173 ++++++++---------- 2 files changed, 88 insertions(+), 99 deletions(-) diff --git a/TODO.md b/TODO.md index 5d401e8..0ea3373 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,10 @@ -## TODO - Evaporasi (Dual Axis + History Firebase) +# TODO -### Checklist -- [x] Cek: List Evaporasi sudah mengambil `state.history` dari Firebase `getSensorHistory`. -- [x] Cek: Filter custom date pada list menggunakan `state.history` (data terdahulu sudah tersedia). -- [x] Ubah grafik evaporasi menjadi **dual-axis** (melalui mapping skala + label kiri/kanan). -- [ ] Perbaiki mismatch chart & list dengan sumber history Firebase (sinkronisasi agregasi/label/period + timezone). -- [ ] Jalankan `flutter analyze` dan minimal compile aplikasi. +- [ ] Plan & identifikasi penyebab garis merah pada `evaporasi_chart_widget.dart`. +- [ ] Periksa isi file `evaporasi_chart_widget.dart` untuk potensi error sintaks/typing. +- [ ] Lakukan perbaikan kode sesuai penyebabnya. +- [x] Analisis file `evaporasi_chart_widget.dart` untuk potensi error sintaks/typing yang memicu garis merah. +- [x] Perbaiki penyebab garis merah pada `evaporasi_chart_widget.dart` (syntax/indentation/typing). +- [ ] Jalankan `flutter format` dan `flutter analyze` (manual di terminal) untuk memastikan error hilang. diff --git a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart index 209b0a4..2e829f5 100644 --- a/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart +++ b/lib/screens/monitoring/evaporasi/views/widgets/evaporasi_chart_widget.dart @@ -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 dailyValues; final List dailyTemperatures; final String period; @@ -24,29 +20,6 @@ class EvaporasiChartWidget extends StatelessWidget { return value < 0 ? 0.0 : value; } - double _minOf(List values) { - if (values.isEmpty) return 0.0; - return values.map(_safeValue).reduce((a, b) => a < b ? a : b); - } - - double _maxOf(List 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 _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({ required double temp, required double evapMin, @@ -54,16 +27,13 @@ class EvaporasiChartWidget extends StatelessWidget { required double tempMin, required double tempMax, }) { - // Hindari pembagian nol final tempRange = (tempMax - tempMin); if (tempRange.abs() < 1e-9) return evapMin; - final normalized = (temp - tempMin) / tempRange; // 0..1 (secara ideal) - final scaled = evapMin + normalized * (evapMax - evapMin); - return scaled; + final normalized = (temp - tempMin) / tempRange; // 0..1 ideal + return evapMin + normalized * (evapMax - evapMin); } - /// Reverse mapping Y internal (skala evaporasi) -> suhu asli (°C) double _evapScaleToTemp({ required double yEvap, required double evapMin, @@ -99,30 +69,24 @@ class EvaporasiChartWidget extends StatelessWidget { color: Colors.black.withAlpha(13), blurRadius: 10, offset: const Offset(0, 5), - ) + ), ], ), child: const Text('Tidak ada data chart'), ); } - // Hitung range masing-masing agar axis kanan (°C) masuk akal. - final evapMinRaw = _minOf(dailyValues); - final evapMaxRaw = _maxOf(dailyValues); - final tempMinRaw = _minOf(dailyTemperatures); - final tempMaxRaw = _maxOf(dailyTemperatures); + // Sesuai permintaan: evaporasi 0..20 dan suhu 0..30 + const evapMin = 0.0; + const evapMax = 20.0; + const tempMin = 0.0; + const tempMax = 30.0; - // Evaporasi mm biasanya >= 0, kita pakai min 0 agar estetik. - final evapMin = 0.0; - final evapMax = _clampDouble(evapMaxRaw * 1.3, 10.0, 100.0); + // Deduplicate X by index for evaporasi + final evapSpotsAll = dailyValues.asMap().entries.map((e) { + 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 evapByX = {}; for (final s in evapSpotsAll) { evapByX[s.x.toInt()] = s.y; @@ -130,12 +94,13 @@ class EvaporasiChartWidget extends StatelessWidget { final dedupEvapSpots = evapByX.entries .toList() - ..sort((a, b) => a.key.compareTo(b.key)); + ..sort((a, b) => a.key.compareTo(b.key)); - final Map tempByX = {}; + final tempByX = {}; for (final entry in dailyTemperatures.asMap().entries) { tempByX[entry.key] = _safeValue(entry.value); } + final tempSpots = tempByX.entries.map((entry) { final x = entry.key.toDouble(); final temp = entry.value; @@ -150,14 +115,13 @@ class EvaporasiChartWidget extends StatelessWidget { }).toList(); final evapSpots = dedupEvapSpots - .map((e) => FlSpot(e.key.toDouble(), e.value)) + .map((e) => FlSpot( + e.key.toDouble(), + e.value.clamp(evapMin, evapMax), + )) .toList(); - if (evapSpots.isEmpty && tempSpots.isEmpty) { - return const SizedBox.shrink(); - } - - double _getRightTitle(double y) { + double getRightTitle(double y) { return _evapScaleToTemp( yEvap: y, evapMin: evapMin, @@ -167,13 +131,14 @@ class EvaporasiChartWidget extends StatelessWidget { ); } - final chart = LineChart( LineChartData( minY: evapMin, maxY: evapMax, gridData: FlGridData(show: false), borderData: FlBorderData(show: false), + + titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( @@ -198,10 +163,12 @@ class EvaporasiChartWidget extends StatelessWidget { reservedSize: 40, interval: (evapMax - evapMin) / 4, getTitlesWidget: (value, meta) { - final v = value; return Text( - '${v.toStringAsFixed(0)}', - style: const TextStyle(color: Colors.blueGrey, fontSize: 10), + value.toStringAsFixed(0), + style: const TextStyle( + color: Colors.blueGrey, + fontSize: 10, + ), ); }, ), @@ -212,27 +179,27 @@ class EvaporasiChartWidget extends StatelessWidget { reservedSize: 40, interval: (evapMax - evapMin) / 4, getTitlesWidget: (value, meta) { - final t = _getRightTitle(value); + final t = getRightTitle(value); return Text( - '${t.toStringAsFixed(0)}', - style: const TextStyle(color: Colors.brown, fontSize: 10), + t.toStringAsFixed(0), + style: const TextStyle( + color: Colors.brown, + fontSize: 10, + ), ); }, ), ), - topTitles: const AxisTitles( + topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), lineTouchData: LineTouchData( 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, @@ -252,36 +219,31 @@ class EvaporasiChartWidget extends StatelessWidget { if (evapSpots.isNotEmpty) LineChartBarData( spots: evapSpots, - isCurved: true, + isCurved: false, color: Colors.blue.shade700, - barWidth: 3, - dotData: FlDotData(show: false), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - colors: [ - Colors.blue.withAlpha(64), - Colors.blue.withAlpha(13), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), + barWidth: 2, + dotData: FlDotData(show: false), + belowBarData: BarAreaData(show: false), + + ), + if (tempSpots.isNotEmpty) LineChartBarData( spots: tempSpots, - isCurved: true, + isCurved: false, color: Colors.orange.shade700, - barWidth: 3, - dotData: FlDotData(show: false), + barWidth: 2, + dotData: FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ), ], ), ); return Container( - height: 340, + height: 380, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, @@ -291,27 +253,54 @@ class EvaporasiChartWidget extends StatelessWidget { color: Colors.black.withAlpha(13), blurRadius: 10, offset: const Offset(0, 5), - ) + ), ], ), child: Column( children: [ - // Legend Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row( 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 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( 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 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, + ), + ), ], ), ],