From 7c55e8bbf94f04a0694e68b0cba3ed81a750ec99 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 11:27:34 +0300 Subject: [PATCH 01/23] Prepared widgets for the aqi distribution chart. --- .../air_quality/views/air_quality_view.dart | 13 +++++-- .../widgets/aqi_distribution_chart.dart | 10 ++++++ .../widgets/aqi_distribution_chart_box.dart | 29 +++++++++++++++ .../widgets/aqi_distribution_chart_title.dart | 35 +++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart diff --git a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index 17ecbc22..b6d403eb 100644 --- a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart +++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart'; class AirQualityView extends StatelessWidget { @@ -23,8 +24,14 @@ class AirQualityView extends StatelessWidget { height: height * 1.2, child: const AirQualityEndSideWidget(), ), - SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()), - SizedBox(height: height * 0.5, child: const Placeholder()), + SizedBox( + height: height * 0.5, + child: const RangeOfAqiChartBox(), + ), + SizedBox( + height: height * 0.5, + child: const AqiDistributionChartBox(), + ), ], ), ); @@ -46,7 +53,7 @@ class AirQualityView extends StatelessWidget { spacing: 20, children: [ Expanded(child: RangeOfAqiChartBox()), - Expanded(child: Placeholder()), + Expanded(child: AqiDistributionChartBox()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart new file mode 100644 index 00000000..254727aa --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AqiDistributionChart extends StatelessWidget { + const AqiDistributionChart({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart new file mode 100644 index 00000000..77eacfa5 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiDistributionChartBox extends StatelessWidget { + const AqiDistributionChartBox({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AqiDistributionChartTitle(isLoading: false), + SizedBox(height: 10), + Divider(), + SizedBox(height: 20), + Expanded(child: AqiDistributionChart()), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart new file mode 100644 index 00000000..a1272a10 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; + +class AqiDistributionChartTitle extends StatelessWidget { + const AqiDistributionChartTitle({required this.isLoading, super.key}); + + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Row( + spacing: 11, + children: [ + const Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: ChartTitle( + title: Text('Distribution over Air Quality Index'), + ), + ), + ), + FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AqiTypeDropdown( + onChanged: (value) {}, + ), + ), + ], + ); + } +} From 5940e5282679ecb04f17ce24034d9e6336c6309b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 11:50:34 +0300 Subject: [PATCH 02/23] Implemented an initial version of `AqiDistributionChart`. --- .../models/air_quality_data_model.dart | 31 +++ .../widgets/aqi_distribution_chart.dart | 199 +++++++++++++++++- .../widgets/aqi_distribution_chart_box.dart | 44 +++- 3 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 lib/pages/analytics/models/air_quality_data_model.dart diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart new file mode 100644 index 00000000..639bcb2e --- /dev/null +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AirQualityDataModel { + const AirQualityDataModel({ + required this.date, + this.aqi, + this.pm25, + this.pm10, + this.hcho, + this.tvoc, + this.co2, + }); + + final DateTime date; + final double? aqi; + final double? pm25; + final double? pm10; + final double? hcho; + final double? tvoc; + final double? co2; + + static const Map metricColors = { + 'aqi': ColorsManager.goodGreen, + 'pm25': ColorsManager.moderateYellow, + 'pm10': ColorsManager.poorOrange, + 'hcho': ColorsManager.unhealthyRed, + 'tvoc': ColorsManager.severePink, + 'co2': ColorsManager.hazardousPurple, + }; +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 254727aa..1038aaa8 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -1,10 +1,205 @@ +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; class AqiDistributionChart extends StatelessWidget { - const AqiDistributionChart({super.key}); + const AqiDistributionChart({super.key, required this.chartData}); + final List chartData; + + static const _rodStackItemsSpacing = 4; @override Widget build(BuildContext context) { - return const Placeholder(); + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + barTouchData: _barTouchData(context), + titlesData: _titlesData(context), + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: 100, + ), + borderData: EnergyManagementChartsHelper.borderData(), + barGroups: _buildBarGroups(), + groupsSpace: 12, + ), + duration: Duration.zero, + ); + } + + List _buildBarGroups() { + return List.generate(chartData.length, (index) { + final data = chartData[index]; + final stackItems = []; + double currentY = 0; + + if (data.aqi != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.aqi!, + color: AirQualityDataModel.metricColors['aqi']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.aqi! + _rodStackItemsSpacing; + } + + if (data.pm25 != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.pm25!, + color: AirQualityDataModel.metricColors['pm25']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.pm25! + _rodStackItemsSpacing; + } + + if (data.pm10 != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.pm10!, + color: AirQualityDataModel.metricColors['pm10']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.pm10! + 2; + } + + if (data.hcho != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.hcho!, + color: AirQualityDataModel.metricColors['hcho']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.hcho! + 2; + } + + if (data.tvoc != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.tvoc!, + color: AirQualityDataModel.metricColors['tvoc']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.tvoc! + 2; + } + + if (data.co2 != null) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + data.co2!, + color: AirQualityDataModel.metricColors['co2']!, + borderRadius: BorderRadius.circular(10), + width: 20, + ), + ); + currentY += data.co2! + 2; + } + return BarChartGroupData( + x: index, + barRods: stackItems, + groupVertically: true, + ); + }); + } + + BarTouchData _barTouchData(BuildContext context) { + return BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (_) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final data = chartData[group.x.toInt()]; + final stackItems = rod.rodStackItems; + + return BarTooltipItem( + '${data.date.day}/${data.date.month}\n', + context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + fontSize: 14, + ), + children: stackItems.map((item) { + final metricName = AirQualityDataModel.metricColors.entries + .firstWhere((entry) => entry.value == item.color) + .key + .toUpperCase(); + return TextSpan( + text: '$metricName: ${item.toY - item.fromY}\n', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ); + }).toList(), + ); + }, + ), + ); + } + + FlTitlesData _titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + + return titlesData.copyWith( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + if (value.toInt() >= chartData.length) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsetsDirectional.only(top: 20.0), + child: Text( + chartData[value.toInt()].date.day.toString(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 12, + ), + ), + ); + }, + reservedSize: 32, + ), + ), + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + value.toInt().toString(), + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ), + ), + ), + ); } } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 77eacfa5..f1ccc0ab 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -13,15 +14,46 @@ class AqiDistributionChartBox extends StatelessWidget { decoration: subSectionContainerDecoration.copyWith( borderRadius: BorderRadius.circular(30), ), - child: const Column( + child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - AqiDistributionChartTitle(isLoading: false), - SizedBox(height: 10), - Divider(), - SizedBox(height: 20), - Expanded(child: AqiDistributionChart()), + const AqiDistributionChartTitle(isLoading: false), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded( + child: AqiDistributionChart( + chartData: [ + AirQualityDataModel( + date: DateTime.now(), + aqi: 50, + pm25: 30, + pm10: 40, + co2: 120, + hcho: 10, + tvoc: 50, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 50, + pm25: 25, + pm10: 40, + co2: 120, + hcho: 10, + tvoc: 50, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 50, + pm25: 25, + pm10: 40, + co2: 120, + hcho: 10, + tvoc: 50, + ), + ], + )), ], ), ); From 1998a629b637cb3d65f8b9c509de63aa78e91adf Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:20:08 +0300 Subject: [PATCH 03/23] added some opacity to metric colors. --- .../analytics/models/air_quality_data_model.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart index 639bcb2e..2f5b3b3b 100644 --- a/lib/pages/analytics/models/air_quality_data_model.dart +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -20,12 +20,12 @@ class AirQualityDataModel { final double? tvoc; final double? co2; - static const Map metricColors = { - 'aqi': ColorsManager.goodGreen, - 'pm25': ColorsManager.moderateYellow, - 'pm10': ColorsManager.poorOrange, - 'hcho': ColorsManager.unhealthyRed, - 'tvoc': ColorsManager.severePink, - 'co2': ColorsManager.hazardousPurple, + static final Map metricColors = { + 'aqi': ColorsManager.goodGreen.withValues(alpha: 0.7), + 'pm25': ColorsManager.moderateYellow.withValues(alpha: 0.7), + 'pm10': ColorsManager.poorOrange.withValues(alpha: 0.7), + 'hcho': ColorsManager.unhealthyRed.withValues(alpha: 0.7), + 'tvoc': ColorsManager.severePink.withValues(alpha: 0.7), + 'co2': ColorsManager.hazardousPurple.withValues(alpha: 0.7), }; } From 10f35d37477c8038796410db92b36ed706e8053a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:20:27 +0300 Subject: [PATCH 04/23] added more mock data to `AqiDistributionChart`. --- .../widgets/aqi_distribution_chart_box.dart | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index f1ccc0ab..eb0ab19e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -23,37 +23,38 @@ class AqiDistributionChartBox extends StatelessWidget { const Divider(), const SizedBox(height: 20), Expanded( - child: AqiDistributionChart( - chartData: [ - AirQualityDataModel( - date: DateTime.now(), - aqi: 50, - pm25: 30, - pm10: 40, - co2: 120, - hcho: 10, - tvoc: 50, - ), - AirQualityDataModel( - date: DateTime.now(), - aqi: 50, - pm25: 25, - pm10: 40, - co2: 120, - hcho: 10, - tvoc: 50, - ), - AirQualityDataModel( - date: DateTime.now(), - aqi: 50, - pm25: 25, - pm10: 40, - co2: 120, - hcho: 10, - tvoc: 50, - ), - ], - )), + child: AqiDistributionChart( + chartData: [ + AirQualityDataModel( + date: DateTime.now(), + aqi: 20, + pm25: 10, + pm10: 40, + co2: 10, + hcho: 0, + tvoc: 20, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 20, + pm25: 10, + pm10: 40, + co2: 10, + hcho: 0, + tvoc: 20, + ), + AirQualityDataModel( + date: DateTime.now(), + aqi: 20, + pm25: 10, + pm10: 40, + co2: 10, + hcho: 0, + tvoc: 20, + ), + ], + ), + ), ], ), ); From 7b31914e1ccd5f12f70a0f2aa0389365f858577f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:20:44 +0300 Subject: [PATCH 05/23] made progress towards aqi distribution chart. --- .../widgets/aqi_distribution_chart.dart | 125 ++++++++++-------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 1038aaa8..0575dfff 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -9,21 +9,22 @@ class AqiDistributionChart extends StatelessWidget { const AqiDistributionChart({super.key, required this.chartData}); final List chartData; - static const _rodStackItemsSpacing = 4; + static const _rodStackItemsSpacing = 0.4; + static const _barWidth = 20.0; + static final _barBorderRadius = BorderRadius.circular(22); @override Widget build(BuildContext context) { return BarChart( BarChartData( - alignment: BarChartAlignment.spaceAround, - barTouchData: _barTouchData(context), - titlesData: _titlesData(context), + maxY: 100.1, gridData: EnergyManagementChartsHelper.gridData( - horizontalInterval: 100, + horizontalInterval: 20, ), borderData: EnergyManagementChartsHelper.borderData(), + barTouchData: _barTouchData(context), + titlesData: _titlesData(context), barGroups: _buildBarGroups(), - groupsSpace: 12, ), duration: Duration.zero, ); @@ -41,8 +42,8 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.aqi!, color: AirQualityDataModel.metricColors['aqi']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); currentY += data.aqi! + _rodStackItemsSpacing; @@ -54,8 +55,8 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.pm25!, color: AirQualityDataModel.metricColors['pm25']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); currentY += data.pm25! + _rodStackItemsSpacing; @@ -67,11 +68,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.pm10!, color: AirQualityDataModel.metricColors['pm10']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.pm10! + 2; + currentY += data.pm10! + _rodStackItemsSpacing; } if (data.hcho != null) { @@ -80,11 +81,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.hcho!, color: AirQualityDataModel.metricColors['hcho']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.hcho! + 2; + currentY += data.hcho! + _rodStackItemsSpacing; } if (data.tvoc != null) { @@ -93,11 +94,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.tvoc!, color: AirQualityDataModel.metricColors['tvoc']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.tvoc! + 2; + currentY += data.tvoc! + _rodStackItemsSpacing; } if (data.co2 != null) { @@ -106,11 +107,11 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.co2!, color: AirQualityDataModel.metricColors['co2']!, - borderRadius: BorderRadius.circular(10), - width: 20, + borderRadius: _barBorderRadius, + width: _barWidth, ), ); - currentY += data.co2! + 2; + currentY += data.co2! + _rodStackItemsSpacing; } return BarChartGroupData( x: index, @@ -143,7 +144,7 @@ class AqiDistributionChart extends StatelessWidget { final metricName = AirQualityDataModel.metricColors.entries .firstWhere((entry) => entry.value == item.color) .key - .toUpperCase(); + .toLowerCase(); return TextSpan( text: '$metricName: ${item.toY - item.fromY}\n', style: context.textTheme.bodySmall?.copyWith( @@ -159,47 +160,55 @@ class AqiDistributionChart extends StatelessWidget { } FlTitlesData _titlesData(BuildContext context) { - final titlesData = EnergyManagementChartsHelper.titlesData(context); + final titlesData = EnergyManagementChartsHelper.titlesData( + context, + leftTitlesInterval: 20, + ); - return titlesData.copyWith( - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (value, meta) { - if (value.toInt() >= chartData.length) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsetsDirectional.only(top: 20.0), - child: Text( - chartData[value.toInt()].date.day.toString(), - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.lightGreyColor, - fontSize: 12, - ), - ), - ); - }, - reservedSize: 32, - ), - ), - leftTitles: titlesData.leftTitles.copyWith( - sideTitles: titlesData.leftTitles.sideTitles.copyWith( - reservedSize: 70, - getTitlesWidget: (value, meta) => Padding( - padding: const EdgeInsetsDirectional.only(end: 12), - child: FittedBox( - alignment: AlignmentDirectional.centerStart, - fit: BoxFit.scaleDown, - child: Text( - value.toInt().toString(), - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, - ), + final leftTitles = titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 20, + maxIncluded: false, + minIncluded: true, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + '${value.toStringAsFixed(0)}%', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, ), ), ), ), ), ); + + final bottomTitles = AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, _) => FittedBox( + alignment: AlignmentDirectional.bottomCenter, + fit: BoxFit.scaleDown, + child: Text( + chartData[value.toInt()].date.day.toString(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 8, + ), + ), + ), + reservedSize: 36, + ), + ); + + return titlesData.copyWith( + leftTitles: leftTitles, + bottomTitles: bottomTitles, + ); } } From ca1feb96009cf2f2962d0b5a486f9f38365d306c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:36:09 +0300 Subject: [PATCH 06/23] made charts based on states and not based on metrics. --- .../models/air_quality_data_model.dart | 36 ++++++------- .../widgets/aqi_distribution_chart.dart | 52 ++++++++++--------- .../widgets/aqi_distribution_chart_box.dart | 42 +++++++-------- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart index 2f5b3b3b..d65f1418 100644 --- a/lib/pages/analytics/models/air_quality_data_model.dart +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -4,28 +4,28 @@ import 'package:syncrow_web/utils/color_manager.dart'; class AirQualityDataModel { const AirQualityDataModel({ required this.date, - this.aqi, - this.pm25, - this.pm10, - this.hcho, - this.tvoc, - this.co2, + this.good, + this.moderate, + this.poor, + this.unhealthy, + this.severe, + this.hazardous, }); final DateTime date; - final double? aqi; - final double? pm25; - final double? pm10; - final double? hcho; - final double? tvoc; - final double? co2; + final double? good; + final double? moderate; + final double? poor; + final double? unhealthy; + final double? severe; + final double? hazardous; static final Map metricColors = { - 'aqi': ColorsManager.goodGreen.withValues(alpha: 0.7), - 'pm25': ColorsManager.moderateYellow.withValues(alpha: 0.7), - 'pm10': ColorsManager.poorOrange.withValues(alpha: 0.7), - 'hcho': ColorsManager.unhealthyRed.withValues(alpha: 0.7), - 'tvoc': ColorsManager.severePink.withValues(alpha: 0.7), - 'co2': ColorsManager.hazardousPurple.withValues(alpha: 0.7), + 'good': ColorsManager.goodGreen.withValues(alpha: 0.7), + 'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7), + 'poor': ColorsManager.poorOrange.withValues(alpha: 0.7), + 'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7), + 'severe': ColorsManager.severePink.withValues(alpha: 0.7), + 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), }; } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 0575dfff..d3cab467 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -36,83 +36,84 @@ class AqiDistributionChart extends StatelessWidget { final stackItems = []; double currentY = 0; - if (data.aqi != null) { + if (data.good != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.aqi!, - color: AirQualityDataModel.metricColors['aqi']!, + toY: currentY + data.good!, + color: AirQualityDataModel.metricColors['good']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.aqi! + _rodStackItemsSpacing; + currentY += data.good! + _rodStackItemsSpacing; } - if (data.pm25 != null) { + if (data.moderate != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.pm25!, - color: AirQualityDataModel.metricColors['pm25']!, + toY: currentY + data.moderate!, + color: AirQualityDataModel.metricColors['moderate']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.pm25! + _rodStackItemsSpacing; + currentY += data.moderate! + _rodStackItemsSpacing; } - if (data.pm10 != null) { + if (data.poor != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.pm10!, - color: AirQualityDataModel.metricColors['pm10']!, + toY: currentY + data.poor!, + color: AirQualityDataModel.metricColors['poor']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.pm10! + _rodStackItemsSpacing; + currentY += data.poor! + _rodStackItemsSpacing; } - if (data.hcho != null) { + if (data.unhealthy != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.hcho!, - color: AirQualityDataModel.metricColors['hcho']!, + toY: currentY + data.unhealthy!, + color: AirQualityDataModel.metricColors['unhealthy']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.hcho! + _rodStackItemsSpacing; + currentY += data.unhealthy! + _rodStackItemsSpacing; } - if (data.tvoc != null) { + if (data.severe != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.tvoc!, - color: AirQualityDataModel.metricColors['tvoc']!, + toY: currentY + data.severe!, + color: AirQualityDataModel.metricColors['severe']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.tvoc! + _rodStackItemsSpacing; + currentY += data.severe! + _rodStackItemsSpacing; } - if (data.co2 != null) { + if (data.hazardous != null) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.co2!, - color: AirQualityDataModel.metricColors['co2']!, + toY: currentY + data.hazardous!, + color: AirQualityDataModel.metricColors['hazardous']!, borderRadius: _barBorderRadius, width: _barWidth, ), ); - currentY += data.co2! + _rodStackItemsSpacing; + currentY += data.hazardous! + _rodStackItemsSpacing; } + return BarChartGroupData( x: index, barRods: stackItems, @@ -146,7 +147,8 @@ class AqiDistributionChart extends StatelessWidget { .key .toLowerCase(); return TextSpan( - text: '$metricName: ${item.toY - item.fromY}\n', + text: + '$metricName: ${(item.toY - item.fromY).toStringAsFixed(1)}%\n', style: context.textTheme.bodySmall?.copyWith( color: ColorsManager.blackColor, fontSize: 12, diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index eb0ab19e..ac770a4e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -26,31 +26,31 @@ class AqiDistributionChartBox extends StatelessWidget { child: AqiDistributionChart( chartData: [ AirQualityDataModel( - date: DateTime.now(), - aqi: 20, - pm25: 10, - pm10: 40, - co2: 10, - hcho: 0, - tvoc: 20, + date: DateTime(2025, 5, 1), + good: 30, + moderate: 25, + poor: 15, + unhealthy: 10, + severe: 15, + hazardous: 5, ), AirQualityDataModel( - date: DateTime.now(), - aqi: 20, - pm25: 10, - pm10: 40, - co2: 10, - hcho: 0, - tvoc: 20, + date: DateTime(2025, 5, 2), + good: 40, + moderate: 20, + poor: 20, + unhealthy: 10, + severe: 5, + hazardous: 5, ), AirQualityDataModel( - date: DateTime.now(), - aqi: 20, - pm25: 10, - pm10: 40, - co2: 10, - hcho: 0, - tvoc: 20, + date: DateTime(2025, 5, 3), + good: 35, + moderate: 30, + poor: 15, + unhealthy: 10, + severe: 5, + hazardous: 5, ), ], ), From 44c4648941f5c9d6178c759a8e3a3dc889aa16ed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 12:45:41 +0300 Subject: [PATCH 07/23] made the first element of the bar rods to have only a top sides radius to match the design. --- .../widgets/aqi_distribution_chart.dart | 50 ++++++++++++++++--- .../widgets/aqi_distribution_chart_box.dart | 20 ++++---- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index d3cab467..aab0a607 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -35,6 +35,7 @@ class AqiDistributionChart extends StatelessWidget { final data = chartData[index]; final stackItems = []; double currentY = 0; + bool isFirstElement = true; if (data.good != null) { stackItems.add( @@ -42,11 +43,18 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.good!, color: AirQualityDataModel.metricColors['good']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + // ignore: dead_code + : _barBorderRadius, width: _barWidth, ), ); currentY += data.good! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.moderate != null) { @@ -55,11 +63,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.moderate!, color: AirQualityDataModel.metricColors['moderate']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.moderate! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.poor != null) { @@ -68,11 +82,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.poor!, color: AirQualityDataModel.metricColors['poor']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.poor! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.unhealthy != null) { @@ -81,11 +101,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.unhealthy!, color: AirQualityDataModel.metricColors['unhealthy']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.unhealthy! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.severe != null) { @@ -94,11 +120,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.severe!, color: AirQualityDataModel.metricColors['severe']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.severe! + _rodStackItemsSpacing; + isFirstElement = false; } if (data.hazardous != null) { @@ -107,11 +139,17 @@ class AqiDistributionChart extends StatelessWidget { fromY: currentY, toY: currentY + data.hazardous!, color: AirQualityDataModel.metricColors['hazardous']!, - borderRadius: _barBorderRadius, + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, width: _barWidth, ), ); currentY += data.hazardous! + _rodStackItemsSpacing; + isFirstElement = false; } return BarChartGroupData( diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index ac770a4e..8d20db94 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -27,21 +27,21 @@ class AqiDistributionChartBox extends StatelessWidget { chartData: [ AirQualityDataModel( date: DateTime(2025, 5, 1), - good: 30, - moderate: 25, - poor: 15, - unhealthy: 10, - severe: 15, - hazardous: 5, + good: null, + moderate: 35, + poor: 20, + unhealthy: 15, + severe: 20, + hazardous: 10, ), AirQualityDataModel( date: DateTime(2025, 5, 2), - good: 40, + good: null, moderate: 20, poor: 20, - unhealthy: 10, - severe: 5, - hazardous: 5, + unhealthy: null, + severe: 30, + hazardous: 25, ), AirQualityDataModel( date: DateTime(2025, 5, 3), From 286dea3f5128b942c5d09dfa6eb4ebe4c65fb518 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:01:53 +0300 Subject: [PATCH 08/23] created a `GetAirQualityDistributionParam`. --- .../params/get_air_quality_distribution_param.dart | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/pages/analytics/params/get_air_quality_distribution_param.dart diff --git a/lib/pages/analytics/params/get_air_quality_distribution_param.dart b/lib/pages/analytics/params/get_air_quality_distribution_param.dart new file mode 100644 index 00000000..f1d3fe9f --- /dev/null +++ b/lib/pages/analytics/params/get_air_quality_distribution_param.dart @@ -0,0 +1,9 @@ +class GetAirQualityDistributionParam { + final DateTime date; + final String spaceUuid; + + const GetAirQualityDistributionParam({ + required this.date, + required this.spaceUuid, + }); +} From 4479ed04b79cc1befe448e6802742abdfc00bee1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:11 +0300 Subject: [PATCH 09/23] Created a `AirQualityDistributionService` along with its fake implementation. --- .../air_quality_distribution_service.dart | 8 ++++ ...fake_air_quality_distribution_service.dart | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart create mode 100644 lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart diff --git a/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart new file mode 100644 index 00000000..ef63856a --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; + +abstract interface class AirQualityDistributionService { + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ); +} diff --git a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart new file mode 100644 index 00000000..59f4947b --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart @@ -0,0 +1,38 @@ +import 'dart:math'; + +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; + +class FakeAirQualityDistributionService implements AirQualityDistributionService { + final _random = Random(); + + @override + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ) async { + return List.generate(30, (index) { + final date = DateTime(2025, 5, 1).add(Duration(days: index)); + + final values = _generateRandomPercentages(); + + return AirQualityDataModel( + date: date, + good: values[0], + moderate: values[1], + poor: values[2], + unhealthy: values[3], + severe: values[4], + hazardous: values[5], + ); + }); + } + + List _generateRandomPercentages() { + final values = List.generate(6, (_) => _random.nextDouble()); + + final sum = values.reduce((a, b) => a + b); + + return values.map((value) => (value / sum * 100).roundToDouble()).toList(); + } +} From 455d9c1f012d9924f9e8952131561db49e7c9cf8 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:25 +0300 Subject: [PATCH 10/23] Created `AirQualityDistributionBloc`. --- .../air_quality_distribution_bloc.dart | 54 +++++++++++++++++++ .../air_quality_distribution_event.dart | 21 ++++++++ .../air_quality_distribution_state.dart | 23 ++++++++ 3 files changed, 98 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart new file mode 100644 index 00000000..a81724a2 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -0,0 +1,54 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; + +part 'air_quality_distribution_event.dart'; +part 'air_quality_distribution_state.dart'; + +class AirQualityDistributionBloc + extends Bloc { + final AirQualityDistributionService _service; + + AirQualityDistributionBloc( + this._service, + ) : super(const AirQualityDistributionState()) { + on(_onLoadAirQualityDistribution); + on(_onClearAirQualityDistribution); + } + + Future _onLoadAirQualityDistribution( + LoadAirQualityDistribution event, + Emitter emit, + ) async { + try { + emit( + const AirQualityDistributionState( + status: AirQualityDistributionStatus.loading, + ), + ); + final result = await _service.getAirQualityDistribution(event.param); + emit( + AirQualityDistributionState( + status: AirQualityDistributionStatus.success, + chartData: result, + ), + ); + } catch (e) { + emit( + AirQualityDistributionState( + status: AirQualityDistributionStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + Future _onClearAirQualityDistribution( + ClearAirQualityDistribution event, + Emitter emit, + ) async { + emit(const AirQualityDistributionState()); + } +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart new file mode 100644 index 00000000..2e1d291f --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart @@ -0,0 +1,21 @@ +part of 'air_quality_distribution_bloc.dart'; + +sealed class AirQualityDistributionEvent extends Equatable { + const AirQualityDistributionEvent(); + + @override + List get props => []; +} + +final class LoadAirQualityDistribution extends AirQualityDistributionEvent { + final GetAirQualityDistributionParam param; + + const LoadAirQualityDistribution(this.param); + + @override + List get props => [param]; +} + +final class ClearAirQualityDistribution extends AirQualityDistributionEvent { + const ClearAirQualityDistribution(); +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart new file mode 100644 index 00000000..0db95e2d --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart @@ -0,0 +1,23 @@ +part of 'air_quality_distribution_bloc.dart'; + +enum AirQualityDistributionStatus { + initial, + loading, + success, + failure, +} + +class AirQualityDistributionState extends Equatable { + final AirQualityDistributionStatus status; + final List chartData; + final String? errorMessage; + + const AirQualityDistributionState({ + this.status = AirQualityDistributionStatus.initial, + this.chartData = const [], + this.errorMessage, + }); + + @override + List get props => [status, chartData, errorMessage]; +} From 736e0c3d9c72f9c98138009bd2862ae40e77e9bb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:40 +0300 Subject: [PATCH 11/23] Injected `AirQualityDistributionBloc` into `AnalyticsPage`. --- .../analytics/views/analytics_page.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 68a531c8..6fc0fc5c 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +<<<<<<< HEAD +======= +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; +>>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; @@ -18,6 +23,7 @@ import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_en import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart'; @@ -101,6 +107,21 @@ class _AnalyticsPageState extends State { FakeRangeOfAqiService(), ), ), +<<<<<<< HEAD +======= + BlocProvider( + create: (context) => DeviceLocationBloc( + RemoteDeviceLocationService( + Dio(BaseOptions(baseUrl: 'https://api.openweathermap.org/data/2.5')), + ), + ), + ), + BlocProvider( + create: (context) => AirQualityDistributionBloc( + FakeAirQualityDistributionService(), + ), + ), +>>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) ], child: const AnalyticsPageForm(), ); From accafb150e808722d70f51396b373a6ab8953ceb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 14:24:07 +0300 Subject: [PATCH 12/23] . --- .../modules/analytics/views/analytics_page.dart | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 6fc0fc5c..575aa862 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -<<<<<<< HEAD -======= import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; ->>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; @@ -18,12 +14,12 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart'; -import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart'; import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart'; @@ -107,21 +103,11 @@ class _AnalyticsPageState extends State { FakeRangeOfAqiService(), ), ), -<<<<<<< HEAD -======= - BlocProvider( - create: (context) => DeviceLocationBloc( - RemoteDeviceLocationService( - Dio(BaseOptions(baseUrl: 'https://api.openweathermap.org/data/2.5')), - ), - ), - ), BlocProvider( create: (context) => AirQualityDistributionBloc( FakeAirQualityDistributionService(), ), ), ->>>>>>> 449e1fd6 (Injected `AirQualityDistributionBloc` into `AnalyticsPage`.) ], child: const AnalyticsPageForm(), ); From 8dc7d2b3d016e044ed4f25d540e9432876db292b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:02:52 +0300 Subject: [PATCH 13/23] Connected `AirQualityDistributionBloc` into `AqiDistributionChartBox`. --- .../widgets/aqi_distribution_chart_box.dart | 76 +++++++------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 8d20db94..8347a15b 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; class AqiDistributionChartBox extends StatelessWidget { @@ -9,54 +11,32 @@ class AqiDistributionChartBox extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsetsDirectional.all(30), - decoration: subSectionContainerDecoration.copyWith( - borderRadius: BorderRadius.circular(30), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const AqiDistributionChartTitle(isLoading: false), - const SizedBox(height: 10), - const Divider(), - const SizedBox(height: 20), - Expanded( - child: AqiDistributionChart( - chartData: [ - AirQualityDataModel( - date: DateTime(2025, 5, 1), - good: null, - moderate: 35, - poor: 20, - unhealthy: 15, - severe: 20, - hazardous: 10, - ), - AirQualityDataModel( - date: DateTime(2025, 5, 2), - good: null, - moderate: 20, - poor: 20, - unhealthy: null, - severe: 30, - hazardous: 25, - ), - AirQualityDataModel( - date: DateTime(2025, 5, 3), - good: 35, - moderate: 30, - poor: 15, - unhealthy: 10, - severe: 5, - hazardous: 5, - ), - ], - ), + return BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), ), - ], - ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.errorMessage != null) ...[ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + ], + AqiDistributionChartTitle( + isLoading: state.status == AirQualityDistributionStatus.loading, + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded(child: AqiDistributionChart(chartData: state.chartData)), + ], + ), + ); + }, ); } } From c50ed693ae3de23e60213d769d2a1ccee4739035 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:10:56 +0300 Subject: [PATCH 14/23] loads and clears aqi distribution in `FetchAirQualityDataHelper`. --- .../fetch_air_quality_data_helper.dart | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 65e62365..55de65d3 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; @@ -28,6 +30,11 @@ abstract final class FetchAirQualityDataHelper { date: date, aqiType: AqiType.aqi, ); + loadAirQualityDistribution( + context, + spaceUuid: spaceUuid, + date: date, + ); } static void clearAllData(BuildContext context) { @@ -37,7 +44,9 @@ abstract final class FetchAirQualityDataHelper { context.read().add( const RealtimeDeviceChangesClosed(), ); - + context.read().add( + const ClearAirQualityDistribution(), + ); context.read().add(const ClearRangeOfAqiEvent()); } @@ -79,4 +88,16 @@ abstract final class FetchAirQualityDataHelper { ), ); } + + static void loadAirQualityDistribution( + BuildContext context, { + required String spaceUuid, + required DateTime date, + }) { + context.read().add( + LoadAirQualityDistribution( + GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date), + ), + ); + } } From 2e12d73151dd6d8f3af53b96934a5833cd6bbafa Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:11:29 +0300 Subject: [PATCH 15/23] randomize generated fake data in `FakeAirQualityDistributionService`. --- ...fake_air_quality_distribution_service.dart | 55 +++++++++++++++---- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart index 59f4947b..264addab 100644 --- a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart +++ b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart @@ -11,23 +11,54 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService Future> getAirQualityDistribution( GetAirQualityDistributionParam param, ) async { - return List.generate(30, (index) { - final date = DateTime(2025, 5, 1).add(Duration(days: index)); + return Future.delayed( + const Duration(milliseconds: 400), + () => List.generate(30, (index) { + final date = DateTime(2025, 5, 1).add(Duration(days: index)); - final values = _generateRandomPercentages(); + final values = _generateRandomPercentages(); + final nullMask = List.generate(6, (_) => _shouldBeNull()); - return AirQualityDataModel( - date: date, - good: values[0], - moderate: values[1], - poor: values[2], - unhealthy: values[3], - severe: values[4], - hazardous: values[5], - ); + // If all values are null, force at least one to be non-null + if (nullMask.every((isNull) => isNull)) { + nullMask[_random.nextInt(6)] = false; + } + + // Redistribute percentages among non-null values + final nonNullValues = _redistributePercentages(values, nullMask); + + return AirQualityDataModel( + date: date, + good: nullMask[0] ? null : nonNullValues[0], + moderate: nullMask[1] ? null : nonNullValues[1], + poor: nullMask[2] ? null : nonNullValues[2], + unhealthy: nullMask[3] ? null : nonNullValues[3], + severe: nullMask[4] ? null : nonNullValues[4], + hazardous: nullMask[5] ? null : nonNullValues[5], + ); + }), + ); + } + + List _redistributePercentages( + List originalValues, List nullMask) { + // Calculate total of non-null values + double nonNullSum = 0; + for (int i = 0; i < originalValues.length; i++) { + if (!nullMask[i]) { + nonNullSum += originalValues[i]; + } + } + + // Redistribute percentages to maintain 100% total + return List.generate(originalValues.length, (i) { + if (nullMask[i]) return 0; + return (originalValues[i] / nonNullSum * 100).roundToDouble(); }); } + bool _shouldBeNull() => _random.nextDouble() < 0.6; + List _generateRandomPercentages() { final values = List.generate(6, (_) => _random.nextDouble()); From 2be15e648ac7498b97098adc3c639c6e26621521 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:13:45 +0300 Subject: [PATCH 16/23] added loading widget to `AqiDistributionChartTitle`. --- .../air_quality/widgets/aqi_distribution_chart_title.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart index a1272a10..5045316b 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; class AqiDistributionChartTitle extends StatelessWidget { const AqiDistributionChartTitle({required this.isLoading, super.key}); @@ -10,12 +11,12 @@ class AqiDistributionChartTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - spacing: 11, children: [ + ChartsLoadingWidget(isLoading: isLoading), const Expanded( flex: 3, child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: ChartTitle( title: Text('Distribution over Air Quality Index'), From e28f3c3c0300c0d38a59db6d431ee9e977456873 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:16:12 +0300 Subject: [PATCH 17/23] reduced bar width size. --- .../modules/air_quality/widgets/aqi_distribution_chart.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index aab0a607..e23a4424 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -10,7 +10,7 @@ class AqiDistributionChart extends StatelessWidget { final List chartData; static const _rodStackItemsSpacing = 0.4; - static const _barWidth = 20.0; + static const _barWidth = 13.0; static final _barBorderRadius = BorderRadius.circular(22); @override From 066f967cd1561bf273c5700099c47b7d3ecec708 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 1 Jun 2025 13:27:35 +0300 Subject: [PATCH 18/23] shows tooltip with data. --- .../widgets/aqi_distribution_chart.dart | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index e23a4424..89b6dd1d 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -1,5 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -171,28 +172,64 @@ class AqiDistributionChart extends StatelessWidget { tooltipPadding: const EdgeInsets.all(8), getTooltipItem: (group, groupIndex, rod, rodIndex) { final data = chartData[group.x.toInt()]; - final stackItems = rod.rodStackItems; + + final List children = []; + + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ); + + if (data.good != null) { + children.add(TextSpan( + text: '\nGOOD: ${data.good!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.moderate != null) { + children.add(TextSpan( + text: '\nMODERATE: ${data.moderate!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.poor != null) { + children.add(TextSpan( + text: '\nPOOR: ${data.poor!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.unhealthy != null) { + children.add(TextSpan( + text: '\nUNHEALTHY: ${data.unhealthy!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.severe != null) { + children.add(TextSpan( + text: '\nSEVERE: ${data.severe!.toStringAsFixed(1)}%', + style: textStyle, + )); + } + + if (data.hazardous != null) { + children.add(TextSpan( + text: '\nHAZARDOUS: ${data.hazardous!.toStringAsFixed(1)}%', + style: textStyle, + )); + } return BarTooltipItem( - '${data.date.day}/${data.date.month}\n', + DateFormat('dd/MM/yyyy').format(data.date), context.textTheme.bodyMedium!.copyWith( color: ColorsManager.blackColor, - fontSize: 14, + fontSize: 16, + fontWeight: FontWeight.w600, ), - children: stackItems.map((item) { - final metricName = AirQualityDataModel.metricColors.entries - .firstWhere((entry) => entry.value == item.color) - .key - .toLowerCase(); - return TextSpan( - text: - '$metricName: ${(item.toY - item.fromY).toStringAsFixed(1)}%\n', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontSize: 12, - ), - ); - }).toList(), + children: children, ); }, ), From 57b6f0117756dea268c69923fedac0b68e906dba Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:26:47 +0300 Subject: [PATCH 19/23] SP-1593 Implemented the agreed upon api contract. --- lib/pages/analytics/models/range_of_aqi.dart | 27 +++++++--- .../blocs/range_of_aqi/range_of_aqi_bloc.dart | 50 ++++++++++++++++--- .../range_of_aqi/range_of_aqi_event.dart | 9 ++++ .../range_of_aqi/range_of_aqi_state.dart | 23 ++++++++- .../fetch_air_quality_data_helper.dart | 4 -- .../widgets/aqi_type_dropdown.dart | 15 +++--- .../widgets/range_of_aqi_chart.dart | 48 +++++++++++------- .../widgets/range_of_aqi_chart_box.dart | 17 +++++-- .../widgets/range_of_aqi_chart_title.dart | 22 ++++---- .../params/get_range_of_aqi_param.dart | 6 +-- .../fake_range_of_aqi_service.dart | 14 ++++-- 11 files changed, 170 insertions(+), 65 deletions(-) diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 759666c2..4cee813e 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -1,18 +1,31 @@ import 'package:equatable/equatable.dart'; class RangeOfAqi extends Equatable { - final double min; - final double avg; - final double max; final DateTime date; + final List data; const RangeOfAqi({ - required this.min, - required this.avg, - required this.max, + required this.data, required this.date, }); @override - List get props => [min, avg, max, date]; + List get props => [data, date]; +} + +class RangeOfAqiValue extends Equatable { + final String type; + final double min; + final double average; + final double max; + + const RangeOfAqiValue({ + required this.type, + required this.min, + required this.average, + required this.max, + }); + + @override + List get props => [type, min, average, max]; } diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart index febbcf58..88c3715e 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; @@ -11,6 +12,7 @@ class RangeOfAqiBloc extends Bloc { RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { on(_onLoadRangeOfAqiEvent); on(_onClearRangeOfAqiEvent); + on(_onUpdateAqiTypeEvent); } final RangeOfAqiService _rangeOfAqiService; @@ -20,19 +22,55 @@ class RangeOfAqiBloc extends Bloc { Emitter emit, ) async { emit( - RangeOfAqiState( - status: RangeOfAqiStatus.loading, - rangeOfAqi: state.rangeOfAqi, - ), + state.copyWith(status: RangeOfAqiStatus.loading), ); try { final rangeOfAqi = await _rangeOfAqiService.load(event.param); - emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); + emit( + state.copyWith( + status: RangeOfAqiStatus.loaded, + rangeOfAqi: rangeOfAqi, + filteredRangeOfAqi: _arrangeChartDataByType( + rangeOfAqi, + state.selectedAqiType, + ), + ), + ); } catch (e) { - emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e')); + emit( + state.copyWith( + status: RangeOfAqiStatus.failure, + errorMessage: '$e', + ), + ); } } + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedAqiType: event.aqiType, + filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType), + ), + ); + } + + List _arrangeChartDataByType( + List rangeOfAqi, + AqiType aqiType, + ) { + final filteredRangeOfAqi = rangeOfAqi.map( + (data) => RangeOfAqi( + date: data.date, + data: data.data.where((value) => value.type == aqiType.code).toList(), + ), + ); + return filteredRangeOfAqi.toList(); + } + void _onClearRangeOfAqiEvent( ClearRangeOfAqiEvent event, Emitter emit, diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart index 8a429587..6a08df5b 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart @@ -16,6 +16,15 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent { List get props => [param]; } +class UpdateAqiTypeEvent extends RangeOfAqiEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + class ClearRangeOfAqiEvent extends RangeOfAqiEvent { const ClearRangeOfAqiEvent(); } diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart index 392e98c1..9308020c 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart @@ -5,14 +5,35 @@ enum RangeOfAqiStatus { initial, loading, loaded, failure } final class RangeOfAqiState extends Equatable { const RangeOfAqiState({ this.rangeOfAqi = const [], + this.filteredRangeOfAqi = const [], this.status = RangeOfAqiStatus.initial, this.errorMessage, + this.selectedAqiType = AqiType.aqi, }); final RangeOfAqiStatus status; final List rangeOfAqi; + final List filteredRangeOfAqi; final String? errorMessage; + final AqiType selectedAqiType; + + RangeOfAqiState copyWith({ + RangeOfAqiStatus? status, + List? rangeOfAqi, + List? filteredRangeOfAqi, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return RangeOfAqiState( + status: status ?? this.status, + rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi, + filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } @override - List get props => [status, rangeOfAqi, errorMessage]; + List get props => + [status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType]; } diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 55de65d3..1919f518 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; @@ -28,7 +27,6 @@ abstract final class FetchAirQualityDataHelper { context, spaceUuid: spaceUuid, date: date, - aqiType: AqiType.aqi, ); loadAirQualityDistribution( context, @@ -76,14 +74,12 @@ abstract final class FetchAirQualityDataHelper { BuildContext context, { required String spaceUuid, required DateTime date, - required AqiType aqiType, }) { context.read().add( LoadRangeOfAqiEvent( GetRangeOfAqiParam( date: date, spaceUuid: spaceUuid, - aqiType: aqiType, ), ), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index c725d1fa..60a686ff 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -3,17 +3,18 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; enum AqiType { - aqi('AQI', ''), - pm25('PM2.5', 'µg/m³'), - pm10('PM10', 'µg/m³'), - hcho('HCHO', 'mg/m³'), - tvoc('TVOC', 'µg/m³'), - co2('CO2', 'ppm'); + aqi('AQI', '', 'aqi'), + pm25('PM2.5', 'µg/m³', 'pm25'), + pm10('PM10', 'µg/m³', 'pm10'), + hcho('HCHO', 'mg/m³', 'hcho'), + tvoc('TVOC', 'µg/m³', 'tvoc'), + co2('CO2', 'ppm', 'co2'); - const AqiType(this.value, this.unit); + const AqiType(this.value, this.unit, this.code); final String value; final String unit; + final String code; } class AqiTypeDropdown extends StatefulWidget { diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart index 08a036c0..fc63e413 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -13,23 +13,37 @@ class RangeOfAqiChart extends StatelessWidget { required this.chartData, }); - List<(List values, Color color, Color? dotColor)> get _lines => [ - ( - chartData.map((e) => e.max).toList(), - ColorsManager.maxPurple, - ColorsManager.maxPurpleDot, - ), - ( - chartData.map((e) => e.avg).toList(), - Colors.white, - null, - ), - ( - chartData.map((e) => e.min).toList(), - ColorsManager.minBlue, - ColorsManager.minBlueDot, - ), - ]; + List<(List values, Color color, Color? dotColor)> get _lines { + final sortedData = List.from(chartData) + ..sort((a, b) => a.date.compareTo(b.date)); + + return [ + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.max ?? 0; + }).toList(), + ColorsManager.maxPurple, + ColorsManager.maxPurpleDot, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.average ?? 0; + }).toList(), + Colors.white, + null, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.min ?? 0; + }).toList(), + ColorsManager.minBlue, + ColorsManager.minBlueDot, + ), + ]; + } @override Widget build(BuildContext context) { diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index 0fe4c4bd..fefb7a9c 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -26,13 +27,23 @@ class RangeOfAqiChartBox extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), ], - RangeOfAqiChartTitle( - isLoading: state.status == RangeOfAqiStatus.loading, + GestureDetector( + onTap: () { + context.read().add(LoadRangeOfAqiEvent( + GetRangeOfAqiParam( + spaceUuid: '123', + date: DateTime.now().subtract(const Duration(days: 30)), + ), + )); + }, + child: RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, + ), ), const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), + Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), ], ), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart index 04cefd6c..6c7aa235 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; -import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; class RangeOfAqiChartTitle extends StatelessWidget { - const RangeOfAqiChartTitle({required this.isLoading, super.key}); + const RangeOfAqiChartTitle({ + required this.isLoading, + super.key, + }); + final bool isLoading; static const List<(Color color, String title, bool hasBorder)> _colors = [ @@ -59,19 +62,16 @@ class RangeOfAqiChartTitle extends StatelessWidget { child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, - child: AqiTypeDropdown( + child: AqiTypeDropdown( onChanged: (value) { final spaceTreeState = context.read().state; final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; - if (spaceUuid == null) return; + // if (spaceUuid == null) return; - FetchAirQualityDataHelper.loadRangeOfAqi( - context, - spaceUuid: spaceUuid, - date: context.read().state.monthlyDate, - aqiType: value ?? AqiType.aqi, - ); + if (value != null) { + context.read().add(UpdateAqiTypeEvent(value)); + } }, ), ), diff --git a/lib/pages/analytics/params/get_range_of_aqi_param.dart b/lib/pages/analytics/params/get_range_of_aqi_param.dart index bbf24658..ef53fe76 100644 --- a/lib/pages/analytics/params/get_range_of_aqi_param.dart +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -1,16 +1,12 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; class GetRangeOfAqiParam extends Equatable { final DateTime date; final String spaceUuid; - final AqiType aqiType; - const GetRangeOfAqiParam( - { + const GetRangeOfAqiParam({ required this.date, required this.spaceUuid, - required this.aqiType, }); @override diff --git a/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart index 13173c94..01ad6fa1 100644 --- a/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart +++ b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart @@ -1,4 +1,5 @@ import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; @@ -18,10 +19,15 @@ class FakeRangeOfAqiService implements RangeOfAqiService { final avg = (min + avgDelta).clamp(0.0, 301.0); final max = (avg + maxDelta).clamp(0.0, 301.0); - return RangeOfAqi( - min: min, - avg: avg, - max: max, + return RangeOfAqi( + data: [ + RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max), + RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max), + ], date: date, ); }); From fa9210f387be966bf8521c18cde952210bf78908 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:28:50 +0300 Subject: [PATCH 20/23] added `fromJson` factory methods to `RangeOfAqi`, and to `RangeOfAqiValue` data models. --- lib/pages/analytics/models/range_of_aqi.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 4cee813e..0308d564 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -9,6 +9,15 @@ class RangeOfAqi extends Equatable { required this.date, }); + factory RangeOfAqi.fromJson(Map json) { + return RangeOfAqi( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map((e) => RangeOfAqiValue.fromJson(e as Map)) + .toList(), + ); + } + @override List get props => [data, date]; } @@ -26,6 +35,15 @@ class RangeOfAqiValue extends Equatable { required this.max, }); + factory RangeOfAqiValue.fromJson(Map json) { + return RangeOfAqiValue( + type: json['type'] as String, + min: (json['min'] as num).toDouble(), + average: (json['average'] as num).toDouble(), + max: (json['max'] as num).toDouble(), + ); + } + @override List get props => [type, min, average, max]; } From 97801872e06b9fba22255d8827cb9657f641b77a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:29:04 +0300 Subject: [PATCH 21/23] Implemented an initial remote implementation of `RangeOfAqiService`. --- .../remote_range_of_aqi_service.dart | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart diff --git a/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart new file mode 100644 index 00000000..1a80ef33 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart @@ -0,0 +1,34 @@ +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteRangeOfAqiService implements RangeOfAqiService { + const RemoteRangeOfAqiService(this._httpService); + + final HTTPService _httpService; + + @override + Future> load(GetRangeOfAqiParam param) async { + try { + final response = await _httpService.get( + path: 'endpoint', + queryParameters: { + 'spaceUuid': param.spaceUuid, + 'date': param.date.toIso8601String(), + }, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return RangeOfAqi.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per phase: $e'); + } + } +} From 7bc9079212baa5827e6b683bdbb392f7051dfeed Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 14:29:58 +0300 Subject: [PATCH 22/23] reverted a comment. --- .../modules/air_quality/widgets/range_of_aqi_chart_title.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart index 6c7aa235..1b0da288 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -62,12 +62,12 @@ class RangeOfAqiChartTitle extends StatelessWidget { child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, - child: AqiTypeDropdown( + child: AqiTypeDropdown( onChanged: (value) { final spaceTreeState = context.read().state; final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; - // if (spaceUuid == null) return; + if (spaceUuid == null) return; if (value != null) { context.read().add(UpdateAqiTypeEvent(value)); From 8e11749ed7b7e3e71f0958a1400cc258daaa9430 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 2 Jun 2025 16:13:58 +0300 Subject: [PATCH 23/23] Prepared for aqi distribution API Integration. --- .../models/air_quality_data_model.dart | 52 ++++-- .../air_quality_distribution_bloc.dart | 43 ++++- .../air_quality_distribution_event.dart | 9 + .../air_quality_distribution_state.dart | 30 +++- .../fetch_air_quality_data_helper.dart | 2 + .../widgets/aqi_distribution_chart.dart | 161 +++--------------- .../widgets/aqi_distribution_chart_box.dart | 4 +- .../widgets/aqi_distribution_chart_title.dart | 12 +- .../widgets/range_of_aqi_chart_box.dart | 15 +- .../air_quality_data_loading_strategy.dart | 2 + .../analytics_page_tabs_and_children.dart | 95 ++++++++--- ...fake_air_quality_distribution_service.dart | 48 ++++-- ...mote_air_quality_distribution_service.dart | 36 ++++ 13 files changed, 296 insertions(+), 213 deletions(-) create mode 100644 lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart index d65f1418..2eab2ddb 100644 --- a/lib/pages/analytics/models/air_quality_data_model.dart +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -1,24 +1,24 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -class AirQualityDataModel { +class AirQualityDataModel extends Equatable { const AirQualityDataModel({ required this.date, - this.good, - this.moderate, - this.poor, - this.unhealthy, - this.severe, - this.hazardous, + required this.data, }); final DateTime date; - final double? good; - final double? moderate; - final double? poor; - final double? unhealthy; - final double? severe; - final double? hazardous; + final List data; + + factory AirQualityDataModel.fromJson(Map json) { + return AirQualityDataModel( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map((e) => AirQualityPercentageData.fromJson(e as Map)) + .toList(), + ); + } static final Map metricColors = { 'good': ColorsManager.goodGreen.withValues(alpha: 0.7), @@ -28,4 +28,30 @@ class AirQualityDataModel { 'severe': ColorsManager.severePink.withValues(alpha: 0.7), 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), }; + + @override + List get props => [date, data]; +} + +class AirQualityPercentageData extends Equatable { + const AirQualityPercentageData({ + required this.type, + required this.name, + required this.percentage, + }); + + final String type; + final String name; + final double percentage; + + factory AirQualityPercentageData.fromJson(Map json) { + return AirQualityPercentageData( + type: json['type'] as String? ?? '', + name: json['name'] as String? ?? '', + percentage: (json['percentage'] as num?)?.toDouble() ?? 0, + ); + } + + @override + List get props => [type, name, percentage]; } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart index a81724a2..fb7e2352 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; @@ -9,13 +10,14 @@ part 'air_quality_distribution_state.dart'; class AirQualityDistributionBloc extends Bloc { - final AirQualityDistributionService _service; + final AirQualityDistributionService _aqiDistributionService; AirQualityDistributionBloc( - this._service, + this._aqiDistributionService, ) : super(const AirQualityDistributionState()) { on(_onLoadAirQualityDistribution); on(_onClearAirQualityDistribution); + on(_onUpdateAqiTypeEvent); } Future _onLoadAirQualityDistribution( @@ -23,16 +25,15 @@ class AirQualityDistributionBloc Emitter emit, ) async { try { - emit( - const AirQualityDistributionState( - status: AirQualityDistributionStatus.loading, - ), + emit(state.copyWith(status: AirQualityDistributionStatus.loading)); + final result = await _aqiDistributionService.getAirQualityDistribution( + event.param, ); - final result = await _service.getAirQualityDistribution(event.param); emit( - AirQualityDistributionState( + state.copyWith( status: AirQualityDistributionStatus.success, chartData: result, + filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType), ), ); } catch (e) { @@ -40,6 +41,7 @@ class AirQualityDistributionBloc AirQualityDistributionState( status: AirQualityDistributionStatus.failure, errorMessage: e.toString(), + selectedAqiType: state.selectedAqiType, ), ); } @@ -51,4 +53,29 @@ class AirQualityDistributionBloc ) async { emit(const AirQualityDistributionState()); } + + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedAqiType: event.aqiType, + filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType), + ), + ); + } + + List _arrangeChartDataByType( + List data, + AqiType aqiType, + ) { + final filteredData = data.map( + (data) => AirQualityDataModel( + date: data.date, + data: data.data.where((value) => value.type == aqiType.code).toList(), + ), + ); + return filteredData.toList(); + } } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart index 2e1d291f..b91dafe5 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart @@ -16,6 +16,15 @@ final class LoadAirQualityDistribution extends AirQualityDistributionEvent { List get props => [param]; } +final class UpdateAqiTypeEvent extends AirQualityDistributionEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + final class ClearAirQualityDistribution extends AirQualityDistributionEvent { const ClearAirQualityDistribution(); } diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart index 0db95e2d..65665882 100644 --- a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart @@ -8,16 +8,36 @@ enum AirQualityDistributionStatus { } class AirQualityDistributionState extends Equatable { - final AirQualityDistributionStatus status; - final List chartData; - final String? errorMessage; - const AirQualityDistributionState({ this.status = AirQualityDistributionStatus.initial, this.chartData = const [], + this.filteredChartData = const [], this.errorMessage, + this.selectedAqiType = AqiType.aqi, }); + final AirQualityDistributionStatus status; + final List chartData; + final List filteredChartData; + final String? errorMessage; + final AqiType selectedAqiType; + + AirQualityDistributionState copyWith({ + AirQualityDistributionStatus? status, + List? chartData, + List? filteredChartData, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return AirQualityDistributionState( + status: status ?? this.status, + chartData: chartData ?? this.chartData, + filteredChartData: filteredChartData ?? this.filteredChartData, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } + @override - List get props => [status, chartData, errorMessage]; + List get props => [status, chartData, errorMessage, selectedAqiType]; } diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 1919f518..e212dedf 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -14,8 +14,10 @@ abstract final class FetchAirQualityDataHelper { static void loadAirQualityData( BuildContext context, { + required DateTime date, required String communityUuid, required String spaceUuid, + bool shouldFetchAnalyticsDevices = true, }) { final date = context.read().state.monthlyDate; loadAnalyticsDevices( diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 89b6dd1d..373e36ca 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -16,6 +16,11 @@ class AqiDistributionChart extends StatelessWidget { @override Widget build(BuildContext context) { + final sortedData = List.from(chartData) + ..sort( + (a, b) => a.date.compareTo(b.date), + ); + return BarChart( BarChartData( maxY: 100.1, @@ -25,45 +30,29 @@ class AqiDistributionChart extends StatelessWidget { borderData: EnergyManagementChartsHelper.borderData(), barTouchData: _barTouchData(context), titlesData: _titlesData(context), - barGroups: _buildBarGroups(), + barGroups: _buildBarGroups(sortedData), ), duration: Duration.zero, ); } - List _buildBarGroups() { - return List.generate(chartData.length, (index) { - final data = chartData[index]; + List _buildBarGroups(List sortedData) { + return List.generate(sortedData.length, (index) { + final data = sortedData[index]; final stackItems = []; double currentY = 0; bool isFirstElement = true; - if (data.good != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.good!, - color: AirQualityDataModel.metricColors['good']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - // ignore: dead_code - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.good! + _rodStackItemsSpacing; - isFirstElement = false; - } + // Sort data by type to ensure consistent order + final sortedPercentageData = List.from(data.data) + ..sort((a, b) => a.type.compareTo(b.type)); - if (data.moderate != null) { + for (final percentageData in sortedPercentageData) { stackItems.add( BarChartRodData( fromY: currentY, - toY: currentY + data.moderate!, - color: AirQualityDataModel.metricColors['moderate']!, + toY: currentY + percentageData.percentage , + color: AirQualityDataModel.metricColors[percentageData.name]!, borderRadius: isFirstElement ? const BorderRadius.only( topLeft: Radius.circular(22), @@ -73,83 +62,7 @@ class AqiDistributionChart extends StatelessWidget { width: _barWidth, ), ); - currentY += data.moderate! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.poor != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.poor!, - color: AirQualityDataModel.metricColors['poor']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.poor! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.unhealthy != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.unhealthy!, - color: AirQualityDataModel.metricColors['unhealthy']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.unhealthy! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.severe != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.severe!, - color: AirQualityDataModel.metricColors['severe']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.severe! + _rodStackItemsSpacing; - isFirstElement = false; - } - - if (data.hazardous != null) { - stackItems.add( - BarChartRodData( - fromY: currentY, - toY: currentY + data.hazardous!, - color: AirQualityDataModel.metricColors['hazardous']!, - borderRadius: isFirstElement - ? const BorderRadius.only( - topLeft: Radius.circular(22), - topRight: Radius.circular(22), - ) - : _barBorderRadius, - width: _barWidth, - ), - ); - currentY += data.hazardous! + _rodStackItemsSpacing; + currentY += percentageData.percentage + _rodStackItemsSpacing; isFirstElement = false; } @@ -180,44 +93,14 @@ class AqiDistributionChart extends StatelessWidget { fontSize: 12, ); - if (data.good != null) { - children.add(TextSpan( - text: '\nGOOD: ${data.good!.toStringAsFixed(1)}%', - style: textStyle, - )); - } + // Sort data by type to ensure consistent order + final sortedPercentageData = List.from(data.data) + ..sort((a, b) => a.type.compareTo(b.type)); - if (data.moderate != null) { + for (final percentageData in sortedPercentageData) { children.add(TextSpan( - text: '\nMODERATE: ${data.moderate!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.poor != null) { - children.add(TextSpan( - text: '\nPOOR: ${data.poor!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.unhealthy != null) { - children.add(TextSpan( - text: '\nUNHEALTHY: ${data.unhealthy!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.severe != null) { - children.add(TextSpan( - text: '\nSEVERE: ${data.severe!.toStringAsFixed(1)}%', - style: textStyle, - )); - } - - if (data.hazardous != null) { - children.add(TextSpan( - text: '\nHAZARDOUS: ${data.hazardous!.toStringAsFixed(1)}%', + text: + '\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%', style: textStyle, )); } diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart index 8347a15b..8a57fe0b 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -32,7 +32,9 @@ class AqiDistributionChartBox extends StatelessWidget { const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded(child: AqiDistributionChart(chartData: state.chartData)), + Expanded( + child: AqiDistributionChart(chartData: state.filteredChartData), + ), ], ), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart index 5045316b..e32043c5 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; @@ -16,7 +18,7 @@ class AqiDistributionChartTitle extends StatelessWidget { const Expanded( flex: 3, child: FittedBox( - fit: BoxFit.scaleDown, + fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: ChartTitle( title: Text('Distribution over Air Quality Index'), @@ -27,7 +29,13 @@ class AqiDistributionChartTitle extends StatelessWidget { alignment: AlignmentDirectional.centerEnd, fit: BoxFit.scaleDown, child: AqiTypeDropdown( - onChanged: (value) {}, + onChanged: (value) { + if (value != null) { + context + .read() + .add(UpdateAqiTypeEvent(value)); + } + }, ), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index fefb7a9c..6548c696 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; -import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -27,18 +26,8 @@ class RangeOfAqiChartBox extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), ], - GestureDetector( - onTap: () { - context.read().add(LoadRangeOfAqiEvent( - GetRangeOfAqiParam( - spaceUuid: '123', - date: DateTime.now().subtract(const Duration(days: 30)), - ), - )); - }, - child: RangeOfAqiChartTitle( - isLoading: state.status == RangeOfAqiStatus.loading, - ), + RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, ), const SizedBox(height: 10), const Divider(), diff --git a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart index dc3b1c5e..5d62029f 100644 --- a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart +++ b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; @@ -39,6 +40,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg context, communityUuid: community.uuid, spaceUuid: space.uuid ?? '', + date: context.read().state.monthlyDate, ); } diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart index 5e9e347a..f6197e46 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; @@ -56,33 +57,16 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { const Spacer(), Visibility( key: ValueKey(selectedTab), - visible: selectedTab == AnalyticsPageTab.energyManagement, + visible: selectedTab == AnalyticsPageTab.energyManagement || + selectedTab == AnalyticsPageTab.airQuality, child: Expanded( flex: 2, child: FittedBox( fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AnalyticsDateFilterButton( - onDateSelected: (DateTime value) { - context.read().add( - UpdateAnalyticsDatePickerEvent(montlyDate: value), - ); - - final spaceTreeState = - context.read().state; - if (spaceTreeState.selectedSpaces.isNotEmpty) { - FetchEnergyManagementDataHelper - .loadEnergyManagementData( - context, - shouldFetchAnalyticsDevices: false, - selectedDate: value, - communityId: - spaceTreeState.selectedCommunities.firstOrNull ?? - '', - spaceId: - spaceTreeState.selectedSpaces.firstOrNull ?? '', - ); - } + onDateSelected: (value) { + _onDateChanged(context, value, selectedTab); }, selectedDate: context .watch() @@ -112,4 +96,73 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { child: child, ); } + + void _onDateChanged( + BuildContext context, + DateTime date, + AnalyticsPageTab selectedTab, + ) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + final spaceTreeState = context.read().state; + final communities = spaceTreeState.selectedCommunities; + final spaces = spaceTreeState.selectedSpaces; + if (spaceTreeState.selectedSpaces.isNotEmpty) { + switch (selectedTab) { + case AnalyticsPageTab.energyManagement: + _onEnergyManagementDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + break; + case AnalyticsPageTab.airQuality: + _onAirQualityDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + default: + break; + } + } + } + + void _onEnergyManagementDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + FetchEnergyManagementDataHelper.loadEnergyManagementData( + context, + shouldFetchAnalyticsDevices: false, + selectedDate: date, + communityId: communityUuid, + spaceId: spaceUuid, + ); + } + + void _onAirQualityDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + FetchAirQualityDataHelper.loadAirQualityData( + context, + date: date, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + shouldFetchAnalyticsDevices: false, + ); + } } diff --git a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart index 264addab..e0023f53 100644 --- a/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart +++ b/lib/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; @@ -19,30 +20,56 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService final values = _generateRandomPercentages(); final nullMask = List.generate(6, (_) => _shouldBeNull()); - // If all values are null, force at least one to be non-null if (nullMask.every((isNull) => isNull)) { nullMask[_random.nextInt(6)] = false; } - // Redistribute percentages among non-null values final nonNullValues = _redistributePercentages(values, nullMask); return AirQualityDataModel( date: date, - good: nullMask[0] ? null : nonNullValues[0], - moderate: nullMask[1] ? null : nonNullValues[1], - poor: nullMask[2] ? null : nonNullValues[2], - unhealthy: nullMask[3] ? null : nonNullValues[3], - severe: nullMask[4] ? null : nonNullValues[4], - hazardous: nullMask[5] ? null : nonNullValues[5], + data: [ + AirQualityPercentageData( + type: AqiType.aqi.code, + percentage: nonNullValues[0], + name: 'good', + ), + AirQualityPercentageData( + name: 'moderate', + type: AqiType.co2.code, + percentage: nonNullValues[1], + ), + AirQualityPercentageData( + name: 'poor', + percentage: nonNullValues[2], + type: AqiType.hcho.code, + + ), + AirQualityPercentageData( + name: 'unhealthy', + percentage: nonNullValues[3], + type: AqiType.pm10.code, + ), + AirQualityPercentageData( + name: 'severe', + type: AqiType.pm25.code, + percentage: nonNullValues[4], + ), + AirQualityPercentageData( + name: 'hazardous', + percentage: nonNullValues[5], + type: AqiType.co2.code, + ), + ], ); }), ); } List _redistributePercentages( - List originalValues, List nullMask) { - // Calculate total of non-null values + List originalValues, + List nullMask, + ) { double nonNullSum = 0; for (int i = 0; i < originalValues.length; i++) { if (!nullMask[i]) { @@ -50,7 +77,6 @@ class FakeAirQualityDistributionService implements AirQualityDistributionService } } - // Redistribute percentages to maintain 100% total return List.generate(originalValues.length, (i) { if (nullMask[i]) return 0; return (originalValues[i] / nonNullSum * 100).roundToDouble(); diff --git a/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart new file mode 100644 index 00000000..dcf00600 --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart @@ -0,0 +1,36 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class RemoteAirQualityDistributionService implements AirQualityDistributionService { + RemoteAirQualityDistributionService(this._httpService); + + final HTTPService _httpService; + + @override + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ) async { + try { + final response = await _httpService.get( + path: 'endpoint', + queryParameters: { + 'spaceUuid': param.spaceUuid, + 'date': param.date.toIso8601String(), + }, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return AirQualityDataModel.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per phase: $e'); + } + } +}