This commit is contained in:
parent
dbb6a25733
commit
76d8760703
|
|
@ -51,21 +51,24 @@ class TimeSeriesMapper {
|
|||
required double Function(T) getValue,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
final sums = List<double>.filled(24, 0.0);
|
||||
final counts = List<int>.filled(24, 0);
|
||||
final values = List<double>.filled(24, -1.0);
|
||||
final latestTimes = List<DateTime?>.filled(24, null);
|
||||
|
||||
for (final item in data) {
|
||||
final time = getTime(item);
|
||||
if (_isSameDay(time, now)) {
|
||||
final hour = time.toLocal().hour;
|
||||
final localTime = time.toLocal();
|
||||
final hour = localTime.hour;
|
||||
if (hour >= 0 && hour < 24) {
|
||||
sums[hour] += getValue(item);
|
||||
counts[hour]++;
|
||||
final existingTime = latestTimes[hour];
|
||||
if (existingTime == null || localTime.isAfter(existingTime)) {
|
||||
values[hour] = getValue(item);
|
||||
latestTimes[hour] = localTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final raw = List<double>.generate(24, (i) =>
|
||||
counts[i] == 0 ? -1.0 : sums[i] / counts[i]);
|
||||
return _interpolate(_filterSpike(raw));
|
||||
return _interpolate(_filterSpike(values));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -132,21 +135,24 @@ class TimeSeriesMapper {
|
|||
required double Function(T) getValue,
|
||||
required DateTime targetDate,
|
||||
}) {
|
||||
final sums = List<double>.filled(24, 0.0);
|
||||
final counts = List<int>.filled(24, 0);
|
||||
final values = List<double>.filled(24, -1.0);
|
||||
final latestTimes = List<DateTime?>.filled(24, null);
|
||||
|
||||
for (final item in data) {
|
||||
final time = getTime(item);
|
||||
if (_isSameDay(time, targetDate)) {
|
||||
final hour = time.toLocal().hour;
|
||||
final localTime = time.toLocal();
|
||||
final hour = localTime.hour;
|
||||
if (hour >= 0 && hour < 24) {
|
||||
sums[hour] += getValue(item);
|
||||
counts[hour]++;
|
||||
final existingTime = latestTimes[hour];
|
||||
if (existingTime == null || localTime.isAfter(existingTime)) {
|
||||
values[hour] = getValue(item);
|
||||
latestTimes[hour] = localTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final raw = List<double>.generate(24, (i) =>
|
||||
counts[i] == 0 ? -1.0 : sums[i] / counts[i]);
|
||||
return _interpolate(_filterSpike(raw));
|
||||
return _interpolate(_filterSpike(values));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -72,7 +72,25 @@ class _DashboardChartsState extends State<DashboardCharts> {
|
|||
void _onPeriodChanged(String p) {
|
||||
setState(() => _period = p);
|
||||
context.read<WindSpeedBloc>().add(WindSpeedPeriodChanged(p));
|
||||
context.read<EvaporasiBloc>().add(EvaporasiPeriodChanged(p));
|
||||
final now = DateTime.now();
|
||||
DateTime startDate;
|
||||
DateTime endDate = now;
|
||||
|
||||
switch (p) {
|
||||
case "Minggu Ini":
|
||||
startDate = now.subtract(Duration(days: now.weekday - 1));
|
||||
break;
|
||||
case "Bulan Ini":
|
||||
startDate = DateTime(now.year, now.month, 1);
|
||||
break;
|
||||
default:
|
||||
startDate = now;
|
||||
break;
|
||||
}
|
||||
|
||||
context.read<EvaporasiBloc>().add(
|
||||
EvaporasiDateRangeChanged(startDate: startDate, endDate: endDate),
|
||||
);
|
||||
// AtmosphericBloc tidak punya period (hanya realtime)
|
||||
}
|
||||
|
||||
|
|
@ -132,11 +150,6 @@ class _DashboardChartsState extends State<DashboardCharts> {
|
|||
// 2. Evaporasi
|
||||
BlocBuilder<EvaporasiBloc, EvaporasiState>(
|
||||
builder: (context, state) {
|
||||
final data = switch (_period) {
|
||||
"Minggu Ini" => state.dailyValues, // weekly jika ada
|
||||
"Bulan Ini" => state.dailyValues,
|
||||
_ => state.dailyValues,
|
||||
};
|
||||
return _SensorChartCard(
|
||||
title: 'Evaporasi',
|
||||
unit: 'mm',
|
||||
|
|
@ -144,7 +157,7 @@ class _DashboardChartsState extends State<DashboardCharts> {
|
|||
bgColor: Colors.teal.shade50,
|
||||
icon: Icons.water_drop_outlined,
|
||||
currentValue: state.currentValue,
|
||||
data: data,
|
||||
data: state.chartValues,
|
||||
period: _period,
|
||||
isLoading: state.isLoading,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class SensorGrid extends StatelessWidget {
|
|||
? '—'
|
||||
: state.currentValue.toStringAsFixed(1),
|
||||
unit: 'mm',
|
||||
status: state.currentData?.status,
|
||||
isLoading: state.isLoading,
|
||||
),
|
||||
),
|
||||
|
|
@ -79,6 +80,7 @@ class SensorCard extends StatelessWidget {
|
|||
final String label;
|
||||
final String value;
|
||||
final String unit;
|
||||
final String? status;
|
||||
final bool isLoading;
|
||||
|
||||
const SensorCard({
|
||||
|
|
@ -90,6 +92,7 @@ class SensorCard extends StatelessWidget {
|
|||
required this.label,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
this.status,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
|
|
@ -162,6 +165,32 @@ class SensorCard extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
if (status != null && !isLoading) ...[
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: (status == 'Tinggi'
|
||||
? Colors.red.shade50
|
||||
: (status == 'Rendah' ? Colors.blue.shade50 : Colors.green.shade50)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status!,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: (status == 'Tinggi'
|
||||
? Colors.red.shade600
|
||||
: (status == 'Rendah' ? Colors.blue.shade600 : Colors.green.shade600)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
// Label
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
|||
(json) => Evaporasi.fromJson(json),
|
||||
),
|
||||
)..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
// Default: tampilkan hari ini (per jam)
|
||||
|
|
@ -88,10 +87,9 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
|||
await _subscription?.cancel();
|
||||
_subscription = _repository
|
||||
.getSensorStream(
|
||||
'Monitoring',
|
||||
'Monitoring/realtime',
|
||||
(json) {
|
||||
final f = Map<dynamic, dynamic>.from(json)..remove('History');
|
||||
return Evaporasi.fromJson(f);
|
||||
return Evaporasi.fromJson(json);
|
||||
},
|
||||
)
|
||||
.listen((data) => add(_EvaporasiRealtimeUpdated(data)));
|
||||
|
|
@ -104,8 +102,6 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
|||
_EvaporasiRealtimeUpdated event,
|
||||
Emitter<EvaporasiState> emit,
|
||||
) {
|
||||
if (event.data.timestamp.millisecondsSinceEpoch == 0) return;
|
||||
|
||||
final updatedHistory = List<Evaporasi>.from(state.history);
|
||||
final dupIdx = updatedHistory.indexWhere(
|
||||
(e) => e.timestamp.toUtc() == event.data.timestamp.toUtc(),
|
||||
|
|
@ -117,26 +113,45 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
|||
}
|
||||
updatedHistory.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
// Update chart jika sedang tampil hari ini per jam
|
||||
List<double> updatedChart = state.chartValues;
|
||||
List<double> updatedTemp = state.chartTemperatures;
|
||||
// Regenerasikan data chart secara dinamis agar selalu sinkron dengan data real-time
|
||||
List<double> updatedChart;
|
||||
List<double> updatedTemp;
|
||||
List<String> updatedLabels = state.chartLabels;
|
||||
|
||||
if (state.isSingleDay) {
|
||||
final eventTime = event.data.timestamp.toLocal();
|
||||
final now = DateTime.now();
|
||||
final isToday = eventTime.year == now.year &&
|
||||
eventTime.month == now.month &&
|
||||
eventTime.day == now.day;
|
||||
final isSingle = _isSameDay(state.startDate, state.endDate);
|
||||
|
||||
if (isToday && dupIdx < 0) {
|
||||
updatedChart = List<double>.from(state.chartValues);
|
||||
updatedTemp = List<double>.from(state.chartTemperatures);
|
||||
final hour = eventTime.hour;
|
||||
if (hour >= 0 && hour < 24) {
|
||||
updatedChart[hour] = event.data.evaporasi;
|
||||
updatedTemp[hour] = event.data.suhu;
|
||||
}
|
||||
}
|
||||
if (isSingle) {
|
||||
updatedChart = TimeSeriesMapper.toSpecificDate(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.evaporasi,
|
||||
targetDate: state.startDate,
|
||||
);
|
||||
updatedTemp = TimeSeriesMapper.toSpecificDate(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.suhu,
|
||||
targetDate: state.startDate,
|
||||
);
|
||||
updatedLabels = List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00');
|
||||
} else {
|
||||
final evapResult = TimeSeriesMapper.toDateRange(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.evaporasi,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
);
|
||||
final tempResult = TimeSeriesMapper.toDateRange(
|
||||
data: updatedHistory,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.suhu,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
);
|
||||
updatedChart = evapResult.values;
|
||||
updatedTemp = tempResult.values;
|
||||
updatedLabels = evapResult.labels;
|
||||
}
|
||||
|
||||
final (status, willRain) = _computeStatus(event.data.evaporasi);
|
||||
|
|
@ -155,6 +170,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
|||
waterLevel: event.data.tinggiAir,
|
||||
chartValues: updatedChart,
|
||||
chartTemperatures: updatedTemp,
|
||||
chartLabels: updatedLabels,
|
||||
weatherStatus: status,
|
||||
willRain: willRain,
|
||||
currentData: event.data,
|
||||
|
|
|
|||
|
|
@ -165,23 +165,46 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
}
|
||||
|
||||
Widget _infoRow(EvaporasiState state) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_miniCard('Suhu Air',
|
||||
'${state.temperature.toStringAsFixed(1)} °C',
|
||||
Icons.thermostat, Colors.orange),
|
||||
_miniCard('Tinggi Air',
|
||||
'${state.waterLevel.toStringAsFixed(1)} cm',
|
||||
Icons.water, Colors.blue),
|
||||
],
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final cardWidth = (constraints.maxWidth - 12) / 2;
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
_miniCard(
|
||||
cardWidth,
|
||||
'Suhu Air',
|
||||
state.temperature < 0
|
||||
? '- °C'
|
||||
: '${state.temperature.toStringAsFixed(1)} °C',
|
||||
Icons.thermostat,
|
||||
Colors.orange,
|
||||
),
|
||||
_miniCard(
|
||||
cardWidth,
|
||||
'Tinggi Air',
|
||||
'${state.waterLevel.toStringAsFixed(1)} cm',
|
||||
Icons.water,
|
||||
Colors.blue,
|
||||
),
|
||||
_miniCard(
|
||||
cardWidth,
|
||||
'Acuan Air Pagi',
|
||||
'${(state.currentData?.acuanPagi ?? 0.0).toStringAsFixed(1)} cm',
|
||||
Icons.wb_sunny_outlined,
|
||||
Colors.amber.shade700,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _miniCard(
|
||||
String title, String value, IconData icon, Color color) {
|
||||
double width, String title, String value, IconData icon, Color color) {
|
||||
return Container(
|
||||
width: 160,
|
||||
width: width,
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
|
|
@ -191,15 +214,26 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
children: [
|
||||
Icon(icon, color: color),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Colors.grey)),
|
||||
Text(value,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -86,11 +86,13 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
List<FlSpot> _buildEvapSpots(double evapMin, double evapMax) {
|
||||
if (dailyValues.isEmpty) return const [];
|
||||
|
||||
return dailyValues.asMap().entries.map((e) {
|
||||
final x = e.key.toDouble();
|
||||
final y = _safeValue(e.value).clamp(evapMin, evapMax);
|
||||
return FlSpot(x, y);
|
||||
}).toList();
|
||||
return dailyValues.asMap().entries
|
||||
.where((e) => e.value >= 0) // Hanya tampilkan data yang ada / valid
|
||||
.map((e) {
|
||||
final x = e.key.toDouble();
|
||||
final y = _safeValue(e.value).clamp(evapMin, evapMax);
|
||||
return FlSpot(x, y);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -120,27 +122,32 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
final tempMinRaw = _minOf(dailyTemperatures);
|
||||
final tempMaxRaw = _maxOf(dailyTemperatures);
|
||||
|
||||
final tempMin = tempMinRaw;
|
||||
final tempMax = tempMaxRaw == tempMinRaw ? tempMinRaw + 1 : tempMaxRaw;
|
||||
// ── FIXED COORD SYSTEM SUHU MALFUNCTION ──
|
||||
// Jika data suhu flat 0 atau sangat rendah, kita kunci range visualnya dari 0 sampai 40 derajat Celsius
|
||||
final tempMin = tempMaxRaw < 2.0 ? 0.0 : tempMinRaw;
|
||||
final tempMax = tempMaxRaw < 2.0 ? 40.0 : (tempMaxRaw == tempMinRaw ? tempMinRaw + 1 : tempMaxRaw);
|
||||
|
||||
// Evaporasi: batasi maksimum supaya tetap masuk akal
|
||||
final evapMax = evapMaxRaw < 1e-9 ? 20.0 : (evapMaxRaw > 50 ? 50.0 : evapMaxRaw);
|
||||
// Evaporasi: Berikan buffer / ruang kosong (+ 20%) di bagian atas grafik agar tidak terpotong rata
|
||||
final double evapMaxBase = evapMaxRaw < 1e-9 ? 20.0 : (evapMaxRaw > 50 ? 50.0 : evapMaxRaw);
|
||||
final evapMax = evapMaxBase * 1.2; // Tambahan padding atas 20%
|
||||
|
||||
final evapSpots = _buildEvapSpots(evapMin, evapMax);
|
||||
|
||||
// Suhu diproyeksikan ke skala evaporasi
|
||||
final tempSpots = dailyTemperatures.asMap().entries.map((entry) {
|
||||
final x = entry.key.toDouble();
|
||||
final temp = _safeValue(entry.value);
|
||||
final y = _tempToEvapScale(
|
||||
temp: temp,
|
||||
evapMin: evapMin,
|
||||
evapMax: evapMax,
|
||||
tempMin: tempMin,
|
||||
tempMax: tempMax,
|
||||
);
|
||||
return FlSpot(x, y.clamp(evapMin, evapMax));
|
||||
}).toList();
|
||||
final tempSpots = dailyTemperatures.asMap().entries
|
||||
.where((entry) => entry.value >= 0) // Hanya tampilkan data yang ada / valid
|
||||
.map((entry) {
|
||||
final x = entry.key.toDouble();
|
||||
final temp = _safeValue(entry.value);
|
||||
final y = _tempToEvapScale(
|
||||
temp: temp,
|
||||
evapMin: evapMin,
|
||||
evapMax: evapMax,
|
||||
tempMin: tempMin,
|
||||
tempMax: tempMax,
|
||||
);
|
||||
return FlSpot(x, y.clamp(evapMin, evapMax));
|
||||
}).toList();
|
||||
|
||||
double getRightTitle(double y) {
|
||||
if (tempSpots.isEmpty) return tempMin;
|
||||
|
|
@ -163,7 +170,8 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
maxX: (chartLabels.isNotEmpty ? chartLabels.length - 1 : 23)
|
||||
.toDouble() +
|
||||
0.5,
|
||||
clipData: const FlClipData.all(),
|
||||
// Diubah ke clipData false agar lengkungan ujung titik teratas tidak ter-crop kaku
|
||||
clipData: const FlClipData.none(),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
|
|
@ -232,8 +240,10 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
interval: (evapMax - evapMin) / 4,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final t = getRightTitle(value);
|
||||
// Menampilkan label dengan 1 angka desimal jika nilainya kecil,
|
||||
// atau bulat murni jika data menggunakan fallback sistem (0-40)
|
||||
return Text(
|
||||
t.toStringAsFixed(0),
|
||||
t > 5 ? t.toStringAsFixed(0) : t.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
color: Colors.brown,
|
||||
fontSize: 10,
|
||||
|
|
@ -249,36 +259,48 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
lineTouchData: LineTouchData(
|
||||
handleBuiltInTouches: true,
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
tooltipPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
showOnTopOfTheChartBoxArea: true,
|
||||
fitInsideHorizontally: true,
|
||||
fitInsideVertically: true,
|
||||
getTooltipItems: (touchedSpots) {
|
||||
if (touchedSpots.isEmpty) return const <LineTooltipItem>[];
|
||||
|
||||
final items = <LineTooltipItem>[];
|
||||
for (int i = 0; i < touchedSpots.length; i++) {
|
||||
final spot = touchedSpots[i];
|
||||
final y = _safeValue(spot.y).clamp(evapMin, evapMax);
|
||||
// Ambil spot pertama yang disentuh untuk mencari index X
|
||||
final firstSpot = touchedSpots.first;
|
||||
final idx = firstSpot.x.round();
|
||||
|
||||
if (i == 0) {
|
||||
items.add(LineTooltipItem(
|
||||
'Evap: ${y.toStringAsFixed(1)} mm',
|
||||
const TextStyle(color: Colors.white, fontSize: 12),
|
||||
));
|
||||
} else {
|
||||
final tempOnly = _evapScaleToTemp(
|
||||
yEvap: y,
|
||||
evapMin: evapMin,
|
||||
evapMax: evapMax,
|
||||
tempMin: tempMin,
|
||||
tempMax: tempMax,
|
||||
);
|
||||
|
||||
items.add(LineTooltipItem(
|
||||
'Suhu: ${tempOnly.toStringAsFixed(1)} °C',
|
||||
const TextStyle(color: Colors.white, fontSize: 12),
|
||||
));
|
||||
}
|
||||
// Validasi batas index
|
||||
if (idx < 0 || idx >= dailyValues.length) {
|
||||
return const <LineTooltipItem>[];
|
||||
}
|
||||
return items;
|
||||
|
||||
// Ambil data evaporasi dan suhu langsung dari array data berdasarkan index X
|
||||
final evapVal = dailyValues[idx];
|
||||
final tempVal = idx < dailyTemperatures.length ? dailyTemperatures[idx] : 0.0;
|
||||
|
||||
final tooltipText = 'Evap: ${evapVal.toStringAsFixed(1)} mm\n'
|
||||
'Suhu: ${tempVal.toStringAsFixed(1)} °C';
|
||||
|
||||
return touchedSpots.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
if (index == 0) {
|
||||
return LineTooltipItem(
|
||||
tooltipText,
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.4,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const LineTooltipItem(
|
||||
'',
|
||||
TextStyle(color: Colors.transparent, fontSize: 0),
|
||||
);
|
||||
}
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -322,8 +344,6 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
children: [
|
||||
const Text(
|
||||
'',
|
||||
// NOTE: placeholder; judul seragam dengan AtmosphericScreen.
|
||||
// Jika ingin judul Evaporasi, ganti sesuai kebutuhan.
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -331,7 +351,11 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
height: 220,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: chart,
|
||||
// Mengubah ClipBehavior ke none agar garis di titik maksimum visual luar aman tidak ter-crop
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 10, right: 4),
|
||||
child: chart,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -378,5 +402,4 @@ class EvaporasiChartWidget extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,12 +4,16 @@ class Evaporasi {
|
|||
final double evaporasi;
|
||||
final double suhu;
|
||||
final double tinggiAir;
|
||||
final double acuanPagi;
|
||||
final String status;
|
||||
final DateTime timestamp;
|
||||
|
||||
Evaporasi({
|
||||
required this.evaporasi,
|
||||
required this.suhu,
|
||||
required this.tinggiAir,
|
||||
required this.acuanPagi,
|
||||
required this.status,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
|
|
@ -17,6 +21,8 @@ class Evaporasi {
|
|||
evaporasi: 0.0,
|
||||
suhu: 0.0,
|
||||
tinggiAir: 0.0,
|
||||
acuanPagi: 0.0,
|
||||
status: "Normal",
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
|
||||
|
|
@ -56,7 +62,7 @@ class Evaporasi {
|
|||
json['temp'] ??
|
||||
json['temperature'],
|
||||
);
|
||||
final suhuVal = (suhuRaw < -50 || suhuRaw > 100) ? 0.0 : suhuRaw;
|
||||
final suhuVal = (suhuRaw < -50 || suhuRaw > 100) ? -1.0 : suhuRaw;
|
||||
|
||||
// ── Tinggi Air (cm) ──────────────────────────────────
|
||||
final tinggiVal = toDoubleSafe(
|
||||
|
|
@ -76,6 +82,14 @@ class Evaporasi {
|
|||
final tinggiFiltered =
|
||||
(tinggiVal < 0 || tinggiVal > 100) ? 0.0 : tinggiVal;
|
||||
|
||||
// ── Acuan Pagi (cm) ──────────────────────────────────
|
||||
final acuanPagiVal = toDoubleSafe(
|
||||
json['acuan_pagi_cm'] ?? json['acuan_pagi'] ?? 0.0,
|
||||
);
|
||||
|
||||
// ── Status ───────────────────────────────────────────
|
||||
final statusVal = json['status']?.toString() ?? "Normal";
|
||||
|
||||
// ── Parse Timestamp ──────────────────────────────────
|
||||
// FIX: Tambahkan offset +07:00 (WIB) jika string tidak punya info timezone,
|
||||
// agar tidak terjadi mismatch 7 jam antara Firebase dan Flutter.
|
||||
|
|
@ -157,6 +171,8 @@ class Evaporasi {
|
|||
evaporasi: evaporasiFiltered,
|
||||
suhu: suhuVal,
|
||||
tinggiAir: tinggiFiltered,
|
||||
acuanPagi: acuanPagiVal,
|
||||
status: statusVal,
|
||||
timestamp: timestamp,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue