From c8fe4e3baa6ba86934ae42bfdf568cabffb88822 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 12:01:26 +0300 Subject: [PATCH 01/40] Created an initial version of `RangeOfAqiChart`. --- .../air_quality/views/air_quality_view.dart | 5 +- .../widgets/range_of_aqi_chart.dart | 105 ++++++++++++++++++ .../widgets/range_of_aqi_chart_box.dart | 58 ++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart create mode 100644 lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.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 38f62cd7..be3b9b04 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/range_of_aqi_chart_box.dart'; class AirQualityView extends StatelessWidget { const AirQualityView({super.key}); @@ -22,7 +23,7 @@ class AirQualityView extends StatelessWidget { height: height * 1.2, child: const AirQualityEndSideWidget(), ), - SizedBox(height: height * 0.5, child: const Placeholder()), + SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()), SizedBox(height: height * 0.5, child: const Placeholder()), ], ), @@ -44,7 +45,7 @@ class AirQualityView extends StatelessWidget { child: Column( spacing: 20, children: [ - Expanded(child: Placeholder()), + Expanded(child: RangeOfAqiChartBox()), Expanded(child: Placeholder()), ], ), 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 new file mode 100644 index 00000000..6e8974f7 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -0,0 +1,105 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RangeOfAqiChart extends StatelessWidget { + final List minValues; + final List avgValues; + final List maxValues; + + const RangeOfAqiChart({ + super.key, + required this.minValues, + required this.avgValues, + required this.maxValues, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Fixed gradient background + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Color(0xFF0CEC16), + Color(0xFFFAC96C), + Color(0xFFEC7400), + Color(0xFFD40000), + Color(0xFFD40094), + Color(0xFFBA01FD), + ], + ), + ), + ), + ), + LineChart( + LineChartData( + minY: 0, + maxY: 320, + gridData: EnergyManagementChartsHelper.gridData(), + titlesData: EnergyManagementChartsHelper.titlesData(context), + borderData: FlBorderData(show: false), + lineBarsData: [ + // Max line (top, purple) + LineChartBarData( + spots: List.generate( + maxValues.length, + (i) => FlSpot(i.toDouble(), maxValues[i]), + ), + isCurved: true, + color: const Color(0xFF962DFF), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF5F00BD)), + belowBarData: BarAreaData(show: false), + ), + // Avg line (middle, white) + LineChartBarData( + spots: List.generate( + avgValues.length, + (i) => FlSpot(i.toDouble(), avgValues[i]), + ), + isCurved: true, + color: Colors.white, + barWidth: 3, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ), + // Min line (bottom, blue) + LineChartBarData( + spots: List.generate( + minValues.length, + (i) => FlSpot(i.toDouble(), minValues[i]), + ), + isCurved: true, + color: const Color(0xFF93AAFD), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF023DFE)), + belowBarData: BarAreaData(show: false), + ), + ], + ), + ), + ], + ); + } + + FlDotData _buildDotData(Color color) { + return FlDotData( + show: true, + getDotPainter: (spot, percent, bar, index) => FlDotCirclePainter( + radius: 2, + color: ColorsManager.whiteColors, + strokeWidth: 2, + strokeColor: color, + ), + ); + } +} 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 new file mode 100644 index 00000000..a8b173d7 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RangeOfAqiChartBox extends StatelessWidget { + const RangeOfAqiChartBox({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsetsDirectional.all(20), + decoration: secondarySection, + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 20, + children: [ + ChartTitle(title: Text('Range of AQI')), + Expanded( + child: RangeOfAqiChart( + avgValues: [ + 120, + 60, + 110, + 100, + 90, + 70, + 80, + 90, + 100, + 110, + 120, + 150, + 160 + ], + minValues: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130], + maxValues: [ + 130, + 140, + 150, + 160, + 170, + 180, + 190, + 200, + 210, + 220, + 230, + 240, + ], + ), + ), + ], + ), + ); + } +} From 39351a710d1a756f44234a530c0260e63ad19586 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Sun, 25 May 2025 12:18:09 +0300 Subject: [PATCH 02/40] Added aqi info colors to `ColorsManager`. --- lib/utils/color_manager.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 5a892aa6..c2e4e60d 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -73,4 +73,10 @@ abstract class ColorsManager { static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); static const Color grey700 = Color(0xFF2D3748); + static const Color goodGreen = Color(0xFF0CEC16); + static const Color moderateYellow = Color(0xFFFAC96C); + static const Color poorOrange = Color(0xFFEC7400); + static const Color unhealthyRed = Color(0xFFD40000); + static const Color severePink = Color(0xFFD40094); + static const Color hazardousPurple = Color(0xFFBA01FD); } From cd2eb46f4958c34efecceb150348b7df2804de0c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 09:50:53 +0300 Subject: [PATCH 03/40] Implemented the overall design of `RangeOfAqiChart`, whats left is 100% matching it with the figma design. --- .../widgets/range_of_aqi_chart.dart | 132 ++++++++++-------- 1 file changed, 73 insertions(+), 59 deletions(-) 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 6e8974f7..934e4c6e 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 @@ -15,75 +15,45 @@ class RangeOfAqiChart extends StatelessWidget { required this.maxValues, }); + static const _gradientColors = [ + ColorsManager.goodGreen, + ColorsManager.moderateYellow, + ColorsManager.poorOrange, + ColorsManager.unhealthyRed, + ColorsManager.severePink, + ColorsManager.hazardousPurple, + ]; + @override Widget build(BuildContext context) { return Stack( children: [ - // Fixed gradient background - Positioned.fill( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [ - Color(0xFF0CEC16), - Color(0xFFFAC96C), - Color(0xFFEC7400), - Color(0xFFD40000), - Color(0xFFD40094), - Color(0xFFBA01FD), - ], - ), - ), - ), - ), LineChart( LineChartData( minY: 0, - maxY: 320, + maxY: 301, gridData: EnergyManagementChartsHelper.gridData(), titlesData: EnergyManagementChartsHelper.titlesData(context), - borderData: FlBorderData(show: false), + borderData: EnergyManagementChartsHelper.borderData(), + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 2, + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: _gradientColors + .map( + (e) => e.withValues(alpha: 0.8), + ) + .toList(), + ), + ), + ], lineBarsData: [ - // Max line (top, purple) - LineChartBarData( - spots: List.generate( - maxValues.length, - (i) => FlSpot(i.toDouble(), maxValues[i]), - ), - isCurved: true, - color: const Color(0xFF962DFF), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF5F00BD)), - belowBarData: BarAreaData(show: false), - ), - // Avg line (middle, white) - LineChartBarData( - spots: List.generate( - avgValues.length, - (i) => FlSpot(i.toDouble(), avgValues[i]), - ), - isCurved: true, - color: Colors.white, - barWidth: 3, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData(show: false), - ), - // Min line (bottom, blue) - LineChartBarData( - spots: List.generate( - minValues.length, - (i) => FlSpot(i.toDouble(), minValues[i]), - ), - isCurved: true, - color: const Color(0xFF93AAFD), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF023DFE)), - belowBarData: BarAreaData(show: false), - ), + _buildMaxLine(), + _buildAverageLine(), + _buildMinLine(), ], ), ), @@ -91,6 +61,50 @@ class RangeOfAqiChart extends StatelessWidget { ); } + LineChartBarData _buildMinLine() { + return LineChartBarData( + spots: List.generate( + minValues.length, + (i) => FlSpot(i.toDouble(), minValues[i]), + ), + isCurved: true, + color: const Color(0xFF93AAFD), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF023DFE)), + belowBarData: BarAreaData(show: false), + ); + } + + LineChartBarData _buildAverageLine() { + return LineChartBarData( + spots: List.generate( + avgValues.length, + (i) => FlSpot(i.toDouble(), avgValues[i]), + ), + isCurved: true, + color: Colors.white, + barWidth: 3, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ); + } + + LineChartBarData _buildMaxLine() { + return LineChartBarData( + spots: List.generate( + maxValues.length, + (i) => FlSpot(i.toDouble(), maxValues[i]), + ), + isCurved: true, + color: const Color(0xFF962DFF), + barWidth: 3, + isStrokeCapRound: true, + dotData: _buildDotData(const Color(0xFF5F00BD)), + belowBarData: BarAreaData(show: false), + ); + } + FlDotData _buildDotData(Color color) { return FlDotData( show: true, From 82006e9aaf13ec44bc5d3917d968913f8030d9f3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:12:52 +0300 Subject: [PATCH 04/40] Implemented the side titles of `RangeOfAqiChart`. --- .../widgets/range_of_aqi_chart.dart | 36 +++++++++++++++++-- .../energy_management_charts_helper.dart | 6 ++-- 2 files changed, 38 insertions(+), 4 deletions(-) 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 934e4c6e..c9c56bdd 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 @@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.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 RangeOfAqiChart extends StatelessWidget { final List minValues; @@ -32,8 +33,39 @@ class RangeOfAqiChart extends StatelessWidget { LineChartData( minY: 0, maxY: 301, - gridData: EnergyManagementChartsHelper.gridData(), - titlesData: EnergyManagementChartsHelper.titlesData(context), + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: 50, + ), + titlesData: EnergyManagementChartsHelper.titlesData(context).copyWith( + leftTitles: AxisTitles( + sideTitles: SideTitles( + reservedSize: 70, + interval: 51, + showTitles: true, + maxIncluded: true, + getTitlesWidget: (value, meta) { + String text; + if (value >= 300) { + text = '300+'; + } else if (value == 255) { + text = '300'; + } else { + text = ((value / 50).round() * 50).toInt().toString(); + } + return Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: Text( + text, + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ); + }, + ), + ), + ), borderData: EnergyManagementChartsHelper.borderData(), betweenBarsData: [ BetweenBarsData( diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart index 567e03ed..5938c77d 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -93,12 +93,14 @@ abstract final class EnergyManagementChartsHelper { ); } - static FlGridData gridData() { + static FlGridData gridData({ + double horizontalInterval = 250, + }) { return FlGridData( show: true, drawVerticalLine: false, drawHorizontalLine: true, - horizontalInterval: 250, + horizontalInterval: horizontalInterval, getDrawingHorizontalLine: (value) { return FlLine( color: ColorsManager.greyColor, From 24e3eb2311d827aa68f1d70bacc5f4d72f86eaea Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:18:15 +0300 Subject: [PATCH 05/40] extracted titlesData into a private factory method to enahnce readability. --- .../widgets/range_of_aqi_chart.dart | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) 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 c9c56bdd..a271d154 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 @@ -36,36 +36,7 @@ class RangeOfAqiChart extends StatelessWidget { gridData: EnergyManagementChartsHelper.gridData( horizontalInterval: 50, ), - titlesData: EnergyManagementChartsHelper.titlesData(context).copyWith( - leftTitles: AxisTitles( - sideTitles: SideTitles( - reservedSize: 70, - interval: 51, - showTitles: true, - maxIncluded: true, - getTitlesWidget: (value, meta) { - String text; - if (value >= 300) { - text = '300+'; - } else if (value == 255) { - text = '300'; - } else { - text = ((value / 50).round() * 50).toInt().toString(); - } - return Padding( - padding: const EdgeInsetsDirectional.only(end: 12), - child: Text( - text, - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, - ), - ), - ); - }, - ), - ), - ), + titlesData: _titlesData(context), borderData: EnergyManagementChartsHelper.borderData(), betweenBarsData: [ BetweenBarsData( @@ -148,4 +119,41 @@ class RangeOfAqiChart extends StatelessWidget { ), ); } + + FlTitlesData _titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + return titlesData.copyWith( + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 51, + maxIncluded: true, + getTitlesWidget: (value, meta) { + String text; + if (value >= 300) { + text = '300+'; + } else if (value == 255) { + text = '300'; + } else { + text = ((value / 50).round() * 50).toInt().toString(); + } + return Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + text, + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ); + }, + ), + ), + ); + } } From 791b71276a6887488496d68c8d4e211e9bc3f2ba Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:29:38 +0300 Subject: [PATCH 06/40] populated linear data for `RangeOfAqiChart`, for a more pleasant dev experience and debugging. --- .../widgets/range_of_aqi_chart_box.dart | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) 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 a8b173d7..685f1665 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 @@ -9,46 +9,65 @@ class RangeOfAqiChartBox extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsetsDirectional.all(20), - decoration: secondarySection, + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), child: const Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - spacing: 20, children: [ ChartTitle(title: Text('Range of AQI')), + SizedBox(height: 10), + Divider(), + SizedBox(height: 20), Expanded( child: RangeOfAqiChart( avgValues: [ - 120, - 60, - 110, - 100, - 90, + 50, 70, - 80, 90, - 100, 110, - 120, - 150, - 160 - ], - minValues: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130], - maxValues: [ 130, - 140, 150, - 160, 170, - 180, 190, - 200, 210, - 220, 230, + 250, + 270, + 290, + ], + minValues: [ + 0, + 20, + 40, + 60, + 80, + 100, + 120, + 140, + 160, + 180, + 200, + 220, 240, ], + maxValues: [ + 100, + 120, + 140, + 160, + 180, + 200, + 220, + 240, + 260, + 280, + 300, + 301, + 301, + ], ), ), ], From 563a3e1cf5a41f6d49b6646609c706a2878ff20c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:31:21 +0300 Subject: [PATCH 07/40] Refactored `RangeOfAqiChart` to consolidate line chart creation into a reusable method, improving code maintainability and reducing duplication. --- .../widgets/range_of_aqi_chart.dart | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) 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 a271d154..e0a8107f 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 @@ -65,46 +65,26 @@ class RangeOfAqiChart extends StatelessWidget { } LineChartBarData _buildMinLine() { - return LineChartBarData( - spots: List.generate( - minValues.length, - (i) => FlSpot(i.toDouble(), minValues[i]), - ), - isCurved: true, + return _buildLine( + values: minValues, color: const Color(0xFF93AAFD), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF023DFE)), - belowBarData: BarAreaData(show: false), + dotColor: const Color(0xFF023DFE), ); } LineChartBarData _buildAverageLine() { - return LineChartBarData( - spots: List.generate( - avgValues.length, - (i) => FlSpot(i.toDouble(), avgValues[i]), - ), - isCurved: true, + return _buildLine( + values: avgValues, color: Colors.white, - barWidth: 3, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData(show: false), + dotColor: null, ); } LineChartBarData _buildMaxLine() { - return LineChartBarData( - spots: List.generate( - maxValues.length, - (i) => FlSpot(i.toDouble(), maxValues[i]), - ), - isCurved: true, + return _buildLine( + values: maxValues, color: const Color(0xFF962DFF), - barWidth: 3, - isStrokeCapRound: true, - dotData: _buildDotData(const Color(0xFF5F00BD)), - belowBarData: BarAreaData(show: false), + dotColor: const Color(0xFF5F00BD), ); } @@ -120,6 +100,26 @@ class RangeOfAqiChart extends StatelessWidget { ); } + LineChartBarData _buildLine({ + required List values, + required Color color, + Color? dotColor, + }) { + return LineChartBarData( + spots: List.generate( + values.length, + (i) => FlSpot(i.toDouble(), values[i]), + ), + isCurved: true, + color: color, + barWidth: 4, + isStrokeCapRound: true, + dotData: + dotColor != null ? _buildDotData(dotColor) : const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + ); + } + FlTitlesData _titlesData(BuildContext context) { final titlesData = EnergyManagementChartsHelper.titlesData(context); return titlesData.copyWith( From 33f9add78a7be32d0b6502db6d5268f67076ca17 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:34:57 +0300 Subject: [PATCH 08/40] Extracted some logic of `RangeOfAqiChart` into a helper class. --- .../helpers/range_of_aqi_charts_helper.dart | 55 +++++++++ .../widgets/range_of_aqi_chart.dart | 105 ++++-------------- 2 files changed, 77 insertions(+), 83 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart new file mode 100644 index 00000000..d00c7357 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -0,0 +1,55 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.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'; + +abstract final class RangeOfAqiChartsHelper { + const RangeOfAqiChartsHelper._(); + + static const gradientData = <(Color color, String label)>[ + (ColorsManager.goodGreen, 'Good'), + (ColorsManager.moderateYellow, 'Moderate'), + (ColorsManager.poorOrange, 'Poor'), + (ColorsManager.unhealthyRed, 'Unhealthy'), + (ColorsManager.severePink, 'Severe'), + (ColorsManager.hazardousPurple, 'Hazardous'), + ]; + + static FlTitlesData titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + return titlesData.copyWith( + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 51, + maxIncluded: true, + getTitlesWidget: (value, meta) { + String text; + if (value >= 300) { + text = '300+'; + } else if (value == 255) { + text = '300'; + } else { + text = ((value / 50).round() * 50).toInt().toString(); + } + return Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + text, + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ); + }, + ), + ), + ); + } +} 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 e0a8107f..b179ec4a 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 @@ -1,8 +1,8 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.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 RangeOfAqiChart extends StatelessWidget { final List minValues; @@ -16,15 +16,6 @@ class RangeOfAqiChart extends StatelessWidget { required this.maxValues, }); - static const _gradientColors = [ - ColorsManager.goodGreen, - ColorsManager.moderateYellow, - ColorsManager.poorOrange, - ColorsManager.unhealthyRed, - ColorsManager.severePink, - ColorsManager.hazardousPurple, - ]; - @override Widget build(BuildContext context) { return Stack( @@ -33,10 +24,8 @@ class RangeOfAqiChart extends StatelessWidget { LineChartData( minY: 0, maxY: 301, - gridData: EnergyManagementChartsHelper.gridData( - horizontalInterval: 50, - ), - titlesData: _titlesData(context), + gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), + titlesData: RangeOfAqiChartsHelper.titlesData(context), borderData: EnergyManagementChartsHelper.borderData(), betweenBarsData: [ BetweenBarsData( @@ -45,18 +34,29 @@ class RangeOfAqiChart extends StatelessWidget { gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, - colors: _gradientColors - .map( - (e) => e.withValues(alpha: 0.8), - ) - .toList(), + colors: RangeOfAqiChartsHelper.gradientData.map((e) { + final (color, _) = e; + return color.withValues(alpha: 0.8); + }).toList(), ), ), ], lineBarsData: [ - _buildMaxLine(), - _buildAverageLine(), - _buildMinLine(), + _buildLine( + values: maxValues, + color: const Color(0xFF962DFF), + dotColor: const Color(0xFF5F00BD), + ), + _buildLine( + values: avgValues, + color: Colors.white, + dotColor: null, + ), + _buildLine( + values: minValues, + color: const Color(0xFF93AAFD), + dotColor: const Color(0xFF023DFE), + ), ], ), ), @@ -64,30 +64,6 @@ class RangeOfAqiChart extends StatelessWidget { ); } - LineChartBarData _buildMinLine() { - return _buildLine( - values: minValues, - color: const Color(0xFF93AAFD), - dotColor: const Color(0xFF023DFE), - ); - } - - LineChartBarData _buildAverageLine() { - return _buildLine( - values: avgValues, - color: Colors.white, - dotColor: null, - ); - } - - LineChartBarData _buildMaxLine() { - return _buildLine( - values: maxValues, - color: const Color(0xFF962DFF), - dotColor: const Color(0xFF5F00BD), - ); - } - FlDotData _buildDotData(Color color) { return FlDotData( show: true, @@ -119,41 +95,4 @@ class RangeOfAqiChart extends StatelessWidget { belowBarData: BarAreaData(show: false), ); } - - FlTitlesData _titlesData(BuildContext context) { - final titlesData = EnergyManagementChartsHelper.titlesData(context); - return titlesData.copyWith( - leftTitles: titlesData.leftTitles.copyWith( - sideTitles: titlesData.leftTitles.sideTitles.copyWith( - reservedSize: 70, - interval: 51, - maxIncluded: true, - getTitlesWidget: (value, meta) { - String text; - if (value >= 300) { - text = '300+'; - } else if (value == 255) { - text = '300'; - } else { - text = ((value / 50).round() * 50).toInt().toString(); - } - return Padding( - padding: const EdgeInsetsDirectional.only(end: 12), - child: FittedBox( - alignment: AlignmentDirectional.centerStart, - fit: BoxFit.scaleDown, - child: Text( - text, - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, - ), - ), - ), - ); - }, - ), - ), - ); - } } From 926bcd9a5d5a69ddeeb639baff26a1b8ab9f48cb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:47:25 +0300 Subject: [PATCH 09/40] Extracted lines data into a helper method for ease of readability. --- .../widgets/range_of_aqi_chart.dart | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) 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 b179ec4a..d53fee0e 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 @@ -16,6 +16,12 @@ class RangeOfAqiChart extends StatelessWidget { required this.maxValues, }); + List<(List values, Color color, Color? dotColor)> _lines() => [ + (maxValues, const Color(0xFF962DFF), const Color(0xFF5F00BD)), + (avgValues, Colors.white, null), + (minValues, const Color(0xFF93AAFD), const Color(0xFF023DFE)), + ]; + @override Widget build(BuildContext context) { return Stack( @@ -41,23 +47,10 @@ class RangeOfAqiChart extends StatelessWidget { ), ), ], - lineBarsData: [ - _buildLine( - values: maxValues, - color: const Color(0xFF962DFF), - dotColor: const Color(0xFF5F00BD), - ), - _buildLine( - values: avgValues, - color: Colors.white, - dotColor: null, - ), - _buildLine( - values: minValues, - color: const Color(0xFF93AAFD), - dotColor: const Color(0xFF023DFE), - ), - ], + lineBarsData: _lines().map((e) { + final (values, color, dotColor) = e; + return _buildLine(values: values, color: color, dotColor: dotColor); + }).toList(), ), ), ], @@ -67,7 +60,7 @@ class RangeOfAqiChart extends StatelessWidget { FlDotData _buildDotData(Color color) { return FlDotData( show: true, - getDotPainter: (spot, percent, bar, index) => FlDotCirclePainter( + getDotPainter: (_, __, ___, ____) => FlDotCirclePainter( radius: 2, color: ColorsManager.whiteColors, strokeWidth: 2, @@ -81,17 +74,14 @@ class RangeOfAqiChart extends StatelessWidget { required Color color, Color? dotColor, }) { + const invisibleDot = FlDotData(show: false); return LineChartBarData( - spots: List.generate( - values.length, - (i) => FlSpot(i.toDouble(), values[i]), - ), + spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])), isCurved: true, color: color, barWidth: 4, isStrokeCapRound: true, - dotData: - dotColor != null ? _buildDotData(dotColor) : const FlDotData(show: false), + dotData: dotColor != null ? _buildDotData(dotColor) : invisibleDot, belowBarData: BarAreaData(show: false), ); } From 902419f9c4e0d62f372a7b7e91995acca93acf2b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:58:05 +0300 Subject: [PATCH 10/40] Created `RangeOfAqi` model. --- lib/pages/analytics/models/range_of_aqi.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 lib/pages/analytics/models/range_of_aqi.dart diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart new file mode 100644 index 00000000..759666c2 --- /dev/null +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +class RangeOfAqi extends Equatable { + final double min; + final double avg; + final double max; + final DateTime date; + + const RangeOfAqi({ + required this.min, + required this.avg, + required this.max, + required this.date, + }); + + @override + List get props => [min, avg, max, date]; +} From eb8ba1806ca216a57bbe108d629e6c2b03aff31f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 10:59:07 +0300 Subject: [PATCH 11/40] Created `GetRangeOfAqiParam` model. --- .../analytics/params/get_range_of_aqi_param.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/pages/analytics/params/get_range_of_aqi_param.dart diff --git a/lib/pages/analytics/params/get_range_of_aqi_param.dart b/lib/pages/analytics/params/get_range_of_aqi_param.dart new file mode 100644 index 00000000..27a8bb35 --- /dev/null +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -0,0 +1,12 @@ +import 'package:equatable/equatable.dart'; + +class GetRangeOfAqiParam extends Equatable { + final DateTime date; + + const GetRangeOfAqiParam({ + required this.date, + }); + + @override + List get props => [date]; +} \ No newline at end of file From 4a3085e1b45a689bce36b82c2f46c43d16b3e700 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:00:57 +0300 Subject: [PATCH 12/40] Created `RangeOfAqiService` along with its fake implementation until the API is ready. --- .../range_of_aqi/fake_range_of_aqi_service.dart | 16 ++++++++++++++++ .../range_of_aqi/range_of_aqi_service.dart | 6 ++++++ 2 files changed, 22 insertions(+) create mode 100644 lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart create mode 100644 lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart 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 new file mode 100644 index 00000000..7e50e6aa --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart @@ -0,0 +1,16 @@ +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'; + +class FakeRangeOfAqiService implements RangeOfAqiService { + @override + Future> load(GetRangeOfAqiParam param) async { + return List.generate(30, (index) { + final date = param.date.add(Duration(days: index)); + final min = (index * 2).toDouble(); + final avg = min + 10; + final max = avg + 10; + return RangeOfAqi(min: min, avg: avg, max: max, date: date); + }); + } +} diff --git a/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart new file mode 100644 index 00000000..9e1657e3 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; + +abstract interface class RangeOfAqiService { + Future> load(GetRangeOfAqiParam param); +} \ No newline at end of file From 5c57143ea559d868ab164e34a9f93bec05afeae7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:09:45 +0300 Subject: [PATCH 13/40] Created `RangeOfAqiBloc` along with its events, and state. --- .../blocs/range_of_aqi/range_of_aqi_bloc.dart | 37 +++++++++++++++++++ .../range_of_aqi/range_of_aqi_event.dart | 21 +++++++++++ .../range_of_aqi/range_of_aqi_state.dart | 18 +++++++++ 3 files changed, 76 insertions(+) create mode 100644 lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart create mode 100644 lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart 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 new file mode 100644 index 00000000..e19f4345 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -0,0 +1,37 @@ +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/params/get_range_of_aqi_param.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; + +part 'range_of_aqi_event.dart'; +part 'range_of_aqi_state.dart'; + +class RangeOfAqiBloc extends Bloc { + RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { + on(_onLoadRangeOfAqiEvent); + on(_onClearRangeOfAqiEvent); + } + + final RangeOfAqiService _rangeOfAqiService; + + Future _onLoadRangeOfAqiEvent( + LoadRangeOfAqiEvent event, + Emitter emit, + ) async { + emit(const RangeOfAqiState(status: RangeOfAqiStatus.loading)); + try { + final rangeOfAqi = await _rangeOfAqiService.load(event.param); + emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); + } catch (e) { + emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e')); + } + } + + void _onClearRangeOfAqiEvent( + ClearRangeOfAqiEvent event, + Emitter emit, + ) { + emit(const RangeOfAqiState()); + } +} 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 new file mode 100644 index 00000000..8a429587 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart @@ -0,0 +1,21 @@ +part of 'range_of_aqi_bloc.dart'; + +sealed class RangeOfAqiEvent extends Equatable { + const RangeOfAqiEvent(); + + @override + List get props => []; +} + +class LoadRangeOfAqiEvent extends RangeOfAqiEvent { + const LoadRangeOfAqiEvent(this.param); + + final GetRangeOfAqiParam param; + + @override + List get props => [param]; +} + +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 new file mode 100644 index 00000000..392e98c1 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart @@ -0,0 +1,18 @@ +part of 'range_of_aqi_bloc.dart'; + +enum RangeOfAqiStatus { initial, loading, loaded, failure } + +final class RangeOfAqiState extends Equatable { + const RangeOfAqiState({ + this.rangeOfAqi = const [], + this.status = RangeOfAqiStatus.initial, + this.errorMessage, + }); + + final RangeOfAqiStatus status; + final List rangeOfAqi; + final String? errorMessage; + + @override + List get props => [status, rangeOfAqi, errorMessage]; +} From 9ab906d24c5f53279c383c501c3f4676279e7329 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:10:23 +0300 Subject: [PATCH 14/40] Injected `RangeOfAqiBloc` into `AnalyticsPage`. --- .../analytics/modules/analytics/views/analytics_page.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index 18f86a90..68a531c8 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.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/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'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; @@ -20,6 +21,7 @@ import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_devi 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'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart'; import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; @@ -94,6 +96,11 @@ class _AnalyticsPageState extends State { ), ), ), + BlocProvider( + create: (context) => RangeOfAqiBloc( + FakeRangeOfAqiService(), + ), + ), ], child: const AnalyticsPageForm(), ); From d4dd7a19ba6caccefabd66bed22086ae1e44f0e2 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:21:42 +0300 Subject: [PATCH 15/40] make the generated fake aqi range data, look better on the chart. --- .../fake_range_of_aqi_service.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 7e50e6aa..63f71a76 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 @@ -7,10 +7,20 @@ class FakeRangeOfAqiService implements RangeOfAqiService { Future> load(GetRangeOfAqiParam param) async { return List.generate(30, (index) { final date = param.date.add(Duration(days: index)); - final min = (index * 2).toDouble(); - final avg = min + 10; - final max = avg + 10; - return RangeOfAqi(min: min, avg: avg, max: max, date: date); + final min = (index * 8).toDouble(); + final avg = min + 40; + final max = avg + 40; + + final cappedMin = min > 301 ? 301.0 : min; + final cappedAvg = avg > 301 ? 301.0 : avg; + final cappedMax = max > 301 ? 301.0 : max; + + return RangeOfAqi( + min: cappedMin, + avg: cappedAvg, + max: cappedMax, + date: date, + ); }); } } From 4af81bcc1096cf579741f6cddd3bcb2e5b83bffb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:22:05 +0300 Subject: [PATCH 16/40] make the aqi range chart read its data from `RangeOfAqiBloc`. --- .../widgets/range_of_aqi_chart.dart | 27 ++++-- .../widgets/range_of_aqi_chart_box.dart | 91 ++++++------------- 2 files changed, 46 insertions(+), 72 deletions(-) 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 d53fee0e..5796c1d8 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 @@ -1,25 +1,34 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class RangeOfAqiChart extends StatelessWidget { - final List minValues; - final List avgValues; - final List maxValues; + final List chartData; const RangeOfAqiChart({ super.key, - required this.minValues, - required this.avgValues, - required this.maxValues, + required this.chartData, }); List<(List values, Color color, Color? dotColor)> _lines() => [ - (maxValues, const Color(0xFF962DFF), const Color(0xFF5F00BD)), - (avgValues, Colors.white, null), - (minValues, const Color(0xFF93AAFD), const Color(0xFF023DFE)), + ( + chartData.map((e) => e.max).toList(), + const Color(0xFF962DFF), + const Color(0xFF5F00BD) + ), + ( + chartData.map((e) => e.avg).toList(), + Colors.white, + null, + ), + ( + chartData.map((e) => e.min).toList(), + const Color(0xFF93AAFD), + const Color(0xFF023DFE) + ), ]; @override 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 685f1665..86696392 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 @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +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/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; class RangeOfAqiChartBox extends StatelessWidget { @@ -8,70 +11,32 @@ class RangeOfAqiChartBox extends StatelessWidget { @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: [ - ChartTitle(title: Text('Range of AQI')), - SizedBox(height: 10), - Divider(), - SizedBox(height: 20), - Expanded( - child: RangeOfAqiChart( - avgValues: [ - 50, - 70, - 90, - 110, - 130, - 150, - 170, - 190, - 210, - 230, - 250, - 270, - 290, - ], - minValues: [ - 0, - 20, - 40, - 60, - 80, - 100, - 120, - 140, - 160, - 180, - 200, - 220, - 240, - ], - maxValues: [ - 100, - 120, - 140, - 160, - 180, - 200, - 220, - 240, - 260, - 280, - 300, - 301, - 301, - ], - ), + 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: [ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + const ChartTitle(title: Text('Range of AQI')), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded( + child: RangeOfAqiChart( + chartData: state.rangeOfAqi, + ), + ), + ], + ), + ); + }, ); } } From 61acaa17c59322c4a4a55dcf8760048dc59b85cb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:22:11 +0300 Subject: [PATCH 17/40] fixed typo. --- .../modules/air_quality/helpers/range_of_aqi_charts_helper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index d00c7357..9c7ef2f2 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -27,7 +27,7 @@ abstract final class RangeOfAqiChartsHelper { getTitlesWidget: (value, meta) { String text; if (value >= 300) { - text = '300+'; + text = '301+'; } else if (value == 255) { text = '300'; } else { From 7305d511bc8c90e7c958c932bb71d37c6e6391b4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:23:33 +0300 Subject: [PATCH 18/40] Added `spaceUuid` to `GetRangeOfAqiParam` model. --- lib/pages/analytics/params/get_range_of_aqi_param.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 27a8bb35..f55337eb 100644 --- a/lib/pages/analytics/params/get_range_of_aqi_param.dart +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -2,11 +2,13 @@ import 'package:equatable/equatable.dart'; class GetRangeOfAqiParam extends Equatable { final DateTime date; + final String spaceUuid; const GetRangeOfAqiParam({ required this.date, + required this.spaceUuid, }); @override - List get props => [date]; + List get props => [date, spaceUuid]; } \ No newline at end of file From 82adbcf4df12622389fc206c6bcb1d08da3f36f1 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:24:00 +0300 Subject: [PATCH 19/40] loads and clears aqi range data in `FetchAirQualityDataHelper`. --- .../fetch_air_quality_data_helper.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 dd646063..a68e70da 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,8 +1,11 @@ import 'package:flutter/material.dart'; 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/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_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; abstract final class FetchAirQualityDataHelper { const FetchAirQualityDataHelper._(); @@ -12,11 +15,13 @@ abstract final class FetchAirQualityDataHelper { required String communityUuid, required String spaceUuid, }) { + final date = context.read().state.monthlyDate; loadAnalyticsDevices( context, communityUuid: communityUuid, spaceUuid: spaceUuid, ); + loadRangeOfAqi(context, spaceUuid: spaceUuid, date: date); } static void clearAllData(BuildContext context) { @@ -26,6 +31,8 @@ abstract final class FetchAirQualityDataHelper { context.read().add( const RealtimeDeviceChangesClosed(), ); + + context.read().add(const ClearRangeOfAqiEvent()); } static void loadAnalyticsDevices( @@ -49,4 +56,16 @@ abstract final class FetchAirQualityDataHelper { ), ); } + + static void loadRangeOfAqi( + BuildContext context, { + required String spaceUuid, + required DateTime date, + }) { + context.read().add( + LoadRangeOfAqiEvent( + GetRangeOfAqiParam(date: date, spaceUuid: spaceUuid), + ), + ); + } } From 12e4285b14b0a01ea77199b1907fa56f4fd8d8ee Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:24:53 +0300 Subject: [PATCH 20/40] removed unnecessary `Stack` widget from `RangeOfAqiChart`. --- .../widgets/range_of_aqi_chart.dart | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) 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 5796c1d8..0f941876 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 @@ -33,36 +33,32 @@ class RangeOfAqiChart extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - LineChart( - LineChartData( - minY: 0, - maxY: 301, - gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), - titlesData: RangeOfAqiChartsHelper.titlesData(context), - borderData: EnergyManagementChartsHelper.borderData(), - betweenBarsData: [ - BetweenBarsData( - fromIndex: 0, - toIndex: 2, - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: RangeOfAqiChartsHelper.gradientData.map((e) { - final (color, _) = e; - return color.withValues(alpha: 0.8); - }).toList(), - ), - ), - ], - lineBarsData: _lines().map((e) { - final (values, color, dotColor) = e; - return _buildLine(values: values, color: color, dotColor: dotColor); - }).toList(), + return LineChart( + LineChartData( + minY: 0, + maxY: 301, + gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), + titlesData: RangeOfAqiChartsHelper.titlesData(context), + borderData: EnergyManagementChartsHelper.borderData(), + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 2, + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: RangeOfAqiChartsHelper.gradientData.map((e) { + final (color, _) = e; + return color.withValues(alpha: 0.8); + }).toList(), + ), ), - ), - ], + ], + lineBarsData: _lines().map((e) { + final (values, color, dotColor) = e; + return _buildLine(values: values, color: color, dotColor: dotColor); + }).toList(), + ), ); } From fb4d44450fd78e57c1cb19027a187e9a0968c62a Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 11:25:12 +0300 Subject: [PATCH 21/40] Disabled animation in `RangeOfAqiChart`. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 1 + 1 file changed, 1 insertion(+) 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 0f941876..b6284e91 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 @@ -59,6 +59,7 @@ class RangeOfAqiChart extends StatelessWidget { return _buildLine(values: values, color: color, dotColor: dotColor); }).toList(), ), + duration: Duration.zero, ); } From 7e54cfdccd18c67e5bc3d63685610e06858bf421 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 13:25:14 +0300 Subject: [PATCH 22/40] Implemented min, max, average informative cells to `RangeOfAqiChartBox`. --- .../widgets/range_of_aqi_chart.dart | 3 +- .../widgets/range_of_aqi_chart_box.dart | 45 +++++++++++-- .../widgets/chart_informative_cell.dart | 63 +++++++++++++++++++ ...y_consumption_per_device_devices_list.dart | 39 +----------- 4 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart 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 b6284e91..4d60f33a 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 @@ -47,9 +47,10 @@ class RangeOfAqiChart extends StatelessWidget { gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, + stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0], colors: RangeOfAqiChartsHelper.gradientData.map((e) { final (color, _) = e; - return color.withValues(alpha: 0.8); + return color.withValues(alpha: 0.6); }).toList(), ), ), 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 86696392..07390480 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 @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; 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/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/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -24,15 +25,11 @@ class RangeOfAqiChartBox extends StatelessWidget { children: [ AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), - const ChartTitle(title: Text('Range of AQI')), + const RangeOfAqiChartTitle(), const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), - Expanded( - child: RangeOfAqiChart( - chartData: state.rangeOfAqi, - ), - ), + Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), ], ), ); @@ -40,3 +37,39 @@ class RangeOfAqiChartBox extends StatelessWidget { ); } } + +class RangeOfAqiChartTitle extends StatelessWidget { + const RangeOfAqiChartTitle({super.key}); + + static const List<(Color color, String title, bool hasBorder)> _colors = [ + (Color(0xFF962DFF), 'Max', false), + (Color(0xFF93AAFD), 'Min', false), + (Colors.transparent, 'Avg', true), + ]; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const ChartTitle(title: Text('Range of AQI')), + const Spacer(), + ..._colors.map( + (e) { + final (color, title, hasBorder) = e; + return Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: ChartInformativeCell( + title: Text(title), + color: color, + hasBorder: hasBorder, + ), + ); + }, + ), + const SizedBox(width: 34), + const Text('AQI'), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart new file mode 100644 index 00000000..05d2b2b5 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ChartInformativeCell extends StatelessWidget { + const ChartInformativeCell({ + super.key, + required this.title, + required this.color, + this.hasBorder = false, + }); + + final Widget title; + final Color color; + final bool hasBorder; + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.sizeOf(context).height * 0.0365, + padding: const EdgeInsetsDirectional.symmetric( + vertical: 8, + horizontal: 12, + ), + decoration: BoxDecoration( + borderRadius: BorderRadiusDirectional.circular(8), + border: Border.all( + color: ColorsManager.greyColor, + width: 1, + ), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.center, + child: Row( + spacing: 6, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + color: color, + border: Border.all(color: ColorsManager.grayBorder), + shape: BoxShape.circle, + ), + ), + // CircleAvatar( + // radius: 4, + // backgroundColor: color, + // ), + DefaultTextStyle( + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + child: title, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart index e6996f53..b7205424 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { const EnergyConsumptionPerDeviceDevicesList({ @@ -42,42 +42,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { return Tooltip( message: '${device.name}\n${device.productDevice?.uuid ?? ''}', - child: Container( - height: MediaQuery.sizeOf(context).height * 0.0365, - padding: const EdgeInsetsDirectional.symmetric( - vertical: 8, - horizontal: 12, - ), - decoration: BoxDecoration( - borderRadius: BorderRadiusDirectional.circular(8), - border: Border.all( - color: ColorsManager.greyColor, - width: 1, - ), - ), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.center, - child: Row( - spacing: 6, - children: [ - CircleAvatar( - radius: 4, - backgroundColor: deviceColor, - ), - Text( - device.name, - textAlign: TextAlign.center, - style: const TextStyle( - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - ], - ), - ), - ), + child: ChartInformativeCell(title: Text(device.name), color: deviceColor), ); } } From 63ca98895f6ffb5bf106f5dfda185aff88784595 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 13:25:24 +0300 Subject: [PATCH 23/40] moved `RangeOfAqiChartTitle`. --- .../widgets/range_of_aqi_chart_box.dart | 39 +------------------ .../widgets/range_of_aqi_chart_title.dart | 39 +++++++++++++++++++ .../widgets/chart_informative_cell.dart | 6 +-- 3 files changed, 41 insertions(+), 43 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart 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 07390480..8e2333fe 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 @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; 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/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/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -37,39 +36,3 @@ class RangeOfAqiChartBox extends StatelessWidget { ); } } - -class RangeOfAqiChartTitle extends StatelessWidget { - const RangeOfAqiChartTitle({super.key}); - - static const List<(Color color, String title, bool hasBorder)> _colors = [ - (Color(0xFF962DFF), 'Max', false), - (Color(0xFF93AAFD), 'Min', false), - (Colors.transparent, 'Avg', true), - ]; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const ChartTitle(title: Text('Range of AQI')), - const Spacer(), - ..._colors.map( - (e) { - final (color, title, hasBorder) = e; - return Padding( - padding: const EdgeInsetsDirectional.only(end: 16), - child: ChartInformativeCell( - title: Text(title), - color: color, - hasBorder: hasBorder, - ), - ); - }, - ), - const SizedBox(width: 34), - const Text('AQI'), - ], - ); - } -} 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 new file mode 100644 index 00000000..cea28c2f --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.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'; + +class RangeOfAqiChartTitle extends StatelessWidget { + const RangeOfAqiChartTitle({super.key}); + + static const List<(Color color, String title, bool hasBorder)> _colors = [ + (Color(0xFF962DFF), 'Max', false), + (Color(0xFF93AAFD), 'Min', false), + (Colors.transparent, 'Avg', true), + ]; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const ChartTitle(title: Text('Range of AQI')), + const Spacer(), + ..._colors.map( + (e) { + final (color, title, hasBorder) = e; + return Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: ChartInformativeCell( + title: Text(title), + color: color, + hasBorder: hasBorder, + ), + ); + }, + ), + const SizedBox(width: 34), + const Text('AQI'), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart index 05d2b2b5..eec31998 100644 --- a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -16,7 +16,7 @@ class ChartInformativeCell extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: MediaQuery.sizeOf(context).height * 0.0365, + height: MediaQuery.sizeOf(context).height * 0.0385, padding: const EdgeInsetsDirectional.symmetric( vertical: 8, horizontal: 12, @@ -43,10 +43,6 @@ class ChartInformativeCell extends StatelessWidget { shape: BoxShape.circle, ), ), - // CircleAvatar( - // radius: 4, - // backgroundColor: color, - // ), DefaultTextStyle( style: const TextStyle( color: ColorsManager.blackColor, From 171dc52e280f30313b65b86944e95bebc50159d3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:10:30 +0300 Subject: [PATCH 24/40] Created `AqiTypeDropdown`. --- .../fetch_air_quality_data_helper.dart | 15 +++- .../widgets/aqi_type_dropdown.dart | 80 +++++++++++++++++++ .../widgets/range_of_aqi_chart_title.dart | 21 ++++- .../params/get_range_of_aqi_param.dart | 8 +- 4 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart 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 a68e70da..65e62365 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,6 +1,7 @@ import 'package:flutter/material.dart'; 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/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'; @@ -21,7 +22,12 @@ abstract final class FetchAirQualityDataHelper { communityUuid: communityUuid, spaceUuid: spaceUuid, ); - loadRangeOfAqi(context, spaceUuid: spaceUuid, date: date); + loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: date, + aqiType: AqiType.aqi, + ); } static void clearAllData(BuildContext context) { @@ -61,10 +67,15 @@ abstract final class FetchAirQualityDataHelper { BuildContext context, { required String spaceUuid, required DateTime date, + required AqiType aqiType, }) { context.read().add( LoadRangeOfAqiEvent( - GetRangeOfAqiParam(date: date, spaceUuid: spaceUuid), + 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 new file mode 100644 index 00000000..a9374204 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +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'), + pm10('PM10'), + co2('CO2'), + voc('VOC'), + tvoc('TVOC'); + + final String value; + const AqiType(this.value); +} + +class AqiTypeDropdown extends StatefulWidget { + const AqiTypeDropdown({super.key, required this.onChanged}); + + final ValueChanged onChanged; + + @override + State createState() => _AqiTypeDropdownState(); +} + +class _AqiTypeDropdownState extends State { + AqiType? _selectedItem = AqiType.aqi; + + void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item); + + static const _defaultPadding = EdgeInsetsDirectional.symmetric( + horizontal: 12, + vertical: 2, + ); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: ColorsManager.greyColor, + width: 1, + ), + ), + child: DropdownButton( + value: _selectedItem, + isDense: true, + isExpanded: false, + selectedItemBuilder: (context) => [ + Text(_selectedItem?.value ?? ''), + ], + borderRadius: BorderRadius.circular(16), + dropdownColor: ColorsManager.whiteColors, + underline: const SizedBox.shrink(), + icon: const RotatedBox( + quarterTurns: 1, + child: Icon(Icons.chevron_right, size: 24), + ), + style: _getTextStyle(context), + padding: _defaultPadding, + items: AqiType.values + .map((e) => DropdownMenuItem(value: e, child: Text(e.value))) + .toList(), + onChanged: (value) { + _updateSelectedItem(value); + widget.onChanged(value); + }, + ), + ); + } + + TextStyle? _getTextStyle(BuildContext context) { + return context.textTheme.labelSmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w700, + fontSize: 14, + ); + } +} 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 cea28c2f..d07c96c8 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,6 +1,11 @@ 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/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/space_tree/bloc/space_tree_bloc.dart'; class RangeOfAqiChartTitle extends StatelessWidget { const RangeOfAqiChartTitle({super.key}); @@ -32,7 +37,21 @@ class RangeOfAqiChartTitle extends StatelessWidget { }, ), const SizedBox(width: 34), - const Text('AQI'), + AqiTypeDropdown( + onChanged: (value) { + final spaceTreeState = context.read().state; + final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; + + if (spaceUuid == null) return; + + FetchAirQualityDataHelper.loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: context.read().state.monthlyDate, + aqiType: value ?? AqiType.aqi, + ); + }, + ), ], ); } 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 f55337eb..bbf24658 100644 --- a/lib/pages/analytics/params/get_range_of_aqi_param.dart +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -1,14 +1,18 @@ 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 List get props => [date, spaceUuid]; -} \ No newline at end of file +} From 177c7f1030a0b6816995f40749bad2c4b91feaa6 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:33:31 +0300 Subject: [PATCH 25/40] Responsiveness of `RangeOfAqiChartTitle`. --- .../widgets/aqi_type_dropdown.dart | 8 +-- .../widgets/range_of_aqi_chart_title.dart | 65 ++++++++++++------- 2 files changed, 46 insertions(+), 27 deletions(-) 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 a9374204..1b519f2e 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 @@ -6,9 +6,10 @@ enum AqiType { aqi('AQI'), pm25('PM2.5'), pm10('PM10'), + hcho('HCHO'), + tvoc('TVOC'), co2('CO2'), - voc('VOC'), - tvoc('TVOC'); + c6h6('C6H6'); final String value; const AqiType(this.value); @@ -47,9 +48,6 @@ class _AqiTypeDropdownState extends State { value: _selectedItem, isDense: true, isExpanded: false, - selectedItemBuilder: (context) => [ - Text(_selectedItem?.value ?? ''), - ], borderRadius: BorderRadius.circular(16), dropdownColor: ColorsManager.whiteColors, underline: const SizedBox.shrink(), 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 d07c96c8..a674acaa 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 @@ -19,38 +19,59 @@ class RangeOfAqiChartTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisSize: MainAxisSize.min, children: [ - const ChartTitle(title: Text('Range of AQI')), - const Spacer(), + const Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: ChartTitle(title: Text('Range of AQI')), + ), + ), + const Spacer(flex: 3), ..._colors.map( (e) { final (color, title, hasBorder) = e; - return Padding( - padding: const EdgeInsetsDirectional.only(end: 16), - child: ChartInformativeCell( - title: Text(title), - color: color, - hasBorder: hasBorder, + return Expanded( + child: IntrinsicHeight( + child: FittedBox( + fit: BoxFit.fitWidth, + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: ChartInformativeCell( + title: Text(title), + color: color, + hasBorder: hasBorder, + ), + ), + ), ), ); }, ), - const SizedBox(width: 34), - AqiTypeDropdown( - onChanged: (value) { - final spaceTreeState = context.read().state; - final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; + const Spacer(), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + 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, - ); - }, + FetchAirQualityDataHelper.loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: context.read().state.monthlyDate, + aqiType: value ?? AqiType.aqi, + ); + }, + ), + ), ), ], ); From 296b03e1aa41bfa7ac407d796c1360d5cca19d26 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:54:56 +0300 Subject: [PATCH 26/40] shows month data instead of index on bottom titles of `RangeOfAqiChart`. --- .../helpers/range_of_aqi_charts_helper.dart | 14 +++++++++++++- .../air_quality/widgets/range_of_aqi_chart.dart | 3 ++- .../range_of_aqi/fake_range_of_aqi_service.dart | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 9c7ef2f2..5a455b88 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -1,5 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.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'; @@ -16,9 +17,20 @@ abstract final class RangeOfAqiChartsHelper { (ColorsManager.hazardousPurple, 'Hazardous'), ]; - static FlTitlesData titlesData(BuildContext context) { + static FlTitlesData titlesData(BuildContext context, List data) { final titlesData = EnergyManagementChartsHelper.titlesData(context); return titlesData.copyWith( + bottomTitles: titlesData.bottomTitles.copyWith( + sideTitles: titlesData.bottomTitles.sideTitles.copyWith( + getTitlesWidget: (value, meta) => Text( + data.isNotEmpty ? data[value.toInt()].date.day.toString() : '', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ), leftTitles: titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, 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 4d60f33a..b4ca17f4 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 @@ -38,8 +38,9 @@ class RangeOfAqiChart extends StatelessWidget { minY: 0, maxY: 301, gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), - titlesData: RangeOfAqiChartsHelper.titlesData(context), + titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), borderData: EnergyManagementChartsHelper.borderData(), + lineTouchData: EnergyManagementChartsHelper.lineTouchData(), betweenBarsData: [ BetweenBarsData( fromIndex: 0, 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 63f71a76..434826d8 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 @@ -6,7 +6,7 @@ class FakeRangeOfAqiService implements RangeOfAqiService { @override Future> load(GetRangeOfAqiParam param) async { return List.generate(30, (index) { - final date = param.date.add(Duration(days: index)); + final date = DateTime(2025, 5, 1).add(Duration(days: index)); final min = (index * 8).toDouble(); final avg = min + 40; final max = avg + 40; From ec7b0aa0787b19f07b892f4e2b6b941f6360c32d Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 15:58:34 +0300 Subject: [PATCH 27/40] shows `AnalyticsErrorWidget` and spacing under it only when there is an error. --- .../modules/air_quality/widgets/range_of_aqi_chart_box.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 8e2333fe..bfe498c5 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 @@ -22,8 +22,10 @@ class RangeOfAqiChartBox extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - AnalyticsErrorWidget(state.errorMessage), - const SizedBox(height: 10), + if (state.errorMessage != null) ...[ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + ], const RangeOfAqiChartTitle(), const SizedBox(height: 10), const Divider(), From cb4956f915cfc31d25ede9f32b42c745ac91d908 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 16:56:25 +0300 Subject: [PATCH 28/40] made range of aqi fake data random and not linear. --- .../fake_range_of_aqi_service.dart | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 434826d8..328e4433 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 @@ -5,20 +5,22 @@ import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_s class FakeRangeOfAqiService implements RangeOfAqiService { @override Future> load(GetRangeOfAqiParam param) async { + final random = DateTime.now().millisecondsSinceEpoch; + return List.generate(30, (index) { final date = DateTime(2025, 5, 1).add(Duration(days: index)); - final min = (index * 8).toDouble(); - final avg = min + 40; - final max = avg + 40; - final cappedMin = min > 301 ? 301.0 : min; - final cappedAvg = avg > 301 ? 301.0 : avg; - final cappedMax = max > 301 ? 301.0 : max; + final min = ((random + index * 17) % 200).toDouble(); + final avgDelta = ((random + index * 23) % 50).toDouble() + 20; + final maxDelta = ((random + index * 31) % 50).toDouble() + 30; + + final avg = (min + avgDelta).clamp(0.0, 301.0); + final max = (avg + maxDelta).clamp(0.0, 301.0); return RangeOfAqi( - min: cappedMin, - avg: cappedAvg, - max: cappedMax, + min: min, + avg: avg, + max: max, date: date, ); }); From 9546d7bdd1e3308604b9a9b3e8146dee37350c13 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 16:56:38 +0300 Subject: [PATCH 29/40] fixed titles widget for bottom title. --- .../helpers/range_of_aqi_charts_helper.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 5a455b88..1bff8981 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -22,11 +22,14 @@ abstract final class RangeOfAqiChartsHelper { return titlesData.copyWith( bottomTitles: titlesData.bottomTitles.copyWith( sideTitles: titlesData.bottomTitles.sideTitles.copyWith( - getTitlesWidget: (value, meta) => Text( - data.isNotEmpty ? data[value.toInt()].date.day.toString() : '', - style: context.textTheme.bodySmall?.copyWith( - fontSize: 12, - color: ColorsManager.lightGreyColor, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(top: 20.0), + child: Text( + data.isNotEmpty ? data[value.toInt()].date.day.toString() : '', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), ), ), ), From bee8652d03e606cab39c72d101836e0ca6c80e76 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Mon, 26 May 2025 16:59:44 +0300 Subject: [PATCH 30/40] responsivness --- .../air_quality/widgets/air_quality_end_side_widget.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart index 2d6ace36..106685c1 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -51,7 +51,6 @@ class AirQualityEndSideWidget extends StatelessWidget { Expanded( flex: 3, child: FittedBox( - fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerStart, child: SelectableText( 'AQI Sensor', @@ -65,9 +64,8 @@ class AirQualityEndSideWidget extends StatelessWidget { ), const Spacer(), Expanded( - flex: 2, + flex: 4, child: FittedBox( - fit: BoxFit.scaleDown, alignment: AlignmentDirectional.centerEnd, child: AnalyticsDeviceDropdown( onChanged: (value) { From 8a5173f42986c4865291bf03072dd57c0e05f2b4 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 09:35:22 +0300 Subject: [PATCH 31/40] made font size of `AqiTypeDropdown` slightly smaller. --- .../air_quality/widgets/aqi_type_dropdown.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 1b519f2e..ea85f075 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 @@ -29,11 +29,6 @@ class _AqiTypeDropdownState extends State { void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item); - static const _defaultPadding = EdgeInsetsDirectional.symmetric( - horizontal: 12, - vertical: 2, - ); - @override Widget build(BuildContext context) { return Container( @@ -47,7 +42,6 @@ class _AqiTypeDropdownState extends State { child: DropdownButton( value: _selectedItem, isDense: true, - isExpanded: false, borderRadius: BorderRadius.circular(16), dropdownColor: ColorsManager.whiteColors, underline: const SizedBox.shrink(), @@ -56,7 +50,10 @@ class _AqiTypeDropdownState extends State { child: Icon(Icons.chevron_right, size: 24), ), style: _getTextStyle(context), - padding: _defaultPadding, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 12, + vertical: 2, + ), items: AqiType.values .map((e) => DropdownMenuItem(value: e, child: Text(e.value))) .toList(), @@ -72,7 +69,7 @@ class _AqiTypeDropdownState extends State { return context.textTheme.labelSmall?.copyWith( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w700, - fontSize: 14, + fontSize: 12, ); } } From 1b0d8d446c653828c0272692038b3f9d7b11c55b Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 09:47:06 +0300 Subject: [PATCH 32/40] modified flex's values. --- .../analytics/modules/air_quality/views/air_quality_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 be3b9b04..03df3ba9 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 @@ -41,7 +41,7 @@ class AirQualityView extends StatelessWidget { spacing: 32, children: [ Expanded( - flex: 2, + flex: 5, child: Column( spacing: 20, children: [ @@ -50,7 +50,7 @@ class AirQualityView extends StatelessWidget { ], ), ), - Expanded(child: AirQualityEndSideWidget()), + Expanded(flex: 2, child: AirQualityEndSideWidget()), ], ), ), From f5d926f5a2171d684016c7445379c6e8e9b59dfe Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:21:59 +0300 Subject: [PATCH 33/40] modify left side titles. --- .../helpers/range_of_aqi_charts_helper.dart | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 1bff8981..381a8d03 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -37,17 +37,10 @@ abstract final class RangeOfAqiChartsHelper { leftTitles: titlesData.leftTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith( reservedSize: 70, - interval: 51, - maxIncluded: true, + interval: 50, + maxIncluded: false, getTitlesWidget: (value, meta) { - String text; - if (value >= 300) { - text = '301+'; - } else if (value == 255) { - text = '300'; - } else { - text = ((value / 50).round() * 50).toInt().toString(); - } + final text = value >= 300 ? '301+' : value.toInt().toString(); return Padding( padding: const EdgeInsetsDirectional.only(end: 12), child: FittedBox( From 3ac5254abf8b61ef61a7daf63df2efdbabcf6fc7 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:26:21 +0300 Subject: [PATCH 34/40] fixed bug in total energy consumption chart. --- .../widgets/total_energy_consumption_chart.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index 204261df..1f1f9a3f 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -35,9 +35,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget { List get _lineBarsData { return [ LineChartBarData( - preventCurveOvershootingThreshold: 0.1, - curveSmoothness: 0.55, - preventCurveOverShooting: true, spots: chartData .asMap() .entries From d90d3d4026aa234032f79c7a5ff6aaff22491afc Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:29:06 +0300 Subject: [PATCH 35/40] added loading state to range of aqi chart. --- .../widgets/range_of_aqi_chart_box.dart | 4 ++- .../widgets/range_of_aqi_chart_title.dart | 5 +++- .../fake_range_of_aqi_service.dart | 30 ++++++++++--------- 3 files changed, 23 insertions(+), 16 deletions(-) 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 bfe498c5..0fe4c4bd 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 @@ -26,7 +26,9 @@ class RangeOfAqiChartBox extends StatelessWidget { AnalyticsErrorWidget(state.errorMessage), const SizedBox(height: 10), ], - const RangeOfAqiChartTitle(), + RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, + ), const SizedBox(height: 10), const Divider(), const SizedBox(height: 20), 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 a674acaa..04cefd6c 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 @@ -5,10 +5,12 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type 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({super.key}); + const RangeOfAqiChartTitle({required this.isLoading, super.key}); + final bool isLoading; static const List<(Color color, String title, bool hasBorder)> _colors = [ (Color(0xFF962DFF), 'Max', false), @@ -20,6 +22,7 @@ class RangeOfAqiChartTitle extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ + ChartsLoadingWidget(isLoading: isLoading), const Expanded( flex: 3, child: FittedBox( 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 328e4433..13173c94 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 @@ -5,24 +5,26 @@ import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_s class FakeRangeOfAqiService implements RangeOfAqiService { @override Future> load(GetRangeOfAqiParam param) async { - final random = DateTime.now().millisecondsSinceEpoch; + return await Future.delayed(const Duration(milliseconds: 800), () { + final random = DateTime.now().millisecondsSinceEpoch; - return List.generate(30, (index) { - final date = DateTime(2025, 5, 1).add(Duration(days: index)); + return List.generate(30, (index) { + final date = DateTime(2025, 5, 1).add(Duration(days: index)); - final min = ((random + index * 17) % 200).toDouble(); - final avgDelta = ((random + index * 23) % 50).toDouble() + 20; - final maxDelta = ((random + index * 31) % 50).toDouble() + 30; + final min = ((random + index * 17) % 200).toDouble(); + final avgDelta = ((random + index * 23) % 50).toDouble() + 20; + final maxDelta = ((random + index * 31) % 50).toDouble() + 30; - final avg = (min + avgDelta).clamp(0.0, 301.0); - final max = (avg + maxDelta).clamp(0.0, 301.0); + 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, - date: date, - ); + return RangeOfAqi( + min: min, + avg: avg, + max: max, + date: date, + ); + }); }); } } From 043820f84faf1a2848e0de598c8cd460513bfbdb Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:33:48 +0300 Subject: [PATCH 36/40] does not emit an entirely new state when we already have chart data on loading. --- .../air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 e19f4345..febbcf58 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 @@ -19,7 +19,12 @@ class RangeOfAqiBloc extends Bloc { LoadRangeOfAqiEvent event, Emitter emit, ) async { - emit(const RangeOfAqiState(status: RangeOfAqiStatus.loading)); + emit( + RangeOfAqiState( + status: RangeOfAqiStatus.loading, + rangeOfAqi: state.rangeOfAqi, + ), + ); try { final rangeOfAqi = await _rangeOfAqiService.load(event.param); emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); From 1aa7bf216232990bd913f9afc6cd4ab14be061d3 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 12:37:36 +0300 Subject: [PATCH 37/40] fixed charts clipping overflow in chart. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 1 + .../widgets/energy_consumption_by_phases_chart.dart | 1 + .../widgets/energy_consumption_per_device_chart.dart | 1 + .../widgets/total_energy_consumption_chart.dart | 2 ++ 4 files changed, 5 insertions(+) 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 b4ca17f4..887cdb69 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 @@ -37,6 +37,7 @@ class RangeOfAqiChart extends StatelessWidget { LineChartData( minY: 0, maxY: 301, + clipData: const FlClipData.vertical(), gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), borderData: EnergyManagementChartsHelper.borderData(), diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart index 1497d0fd..0019d3b5 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart @@ -18,6 +18,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { Widget build(BuildContext context) { return BarChart( BarChartData( + gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, horizontalInterval: 250, diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart index fcf7d384..1e74ad31 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart @@ -12,6 +12,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget { Widget build(BuildContext context) { return LineChart( LineChartData( + clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, leftTitlesInterval: 250, diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index 1f1f9a3f..85b95c29 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -14,6 +14,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget { return Expanded( child: LineChart( LineChartData( + clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, leftTitlesInterval: 250, @@ -28,6 +29,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget { ), duration: Duration.zero, curve: Curves.easeIn, + ), ); } From a1142eb38c2fb85cab8ee7620ed72e77b820f989 Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 13:17:05 +0300 Subject: [PATCH 38/40] gave range of aqi chart a tooltip that shows the necessary data. --- .../helpers/range_of_aqi_charts_helper.dart | 52 +++++++++++++++++++ .../widgets/range_of_aqi_chart.dart | 6 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 381a8d03..21cb2a9e 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.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/range_of_aqi.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -60,4 +61,55 @@ abstract final class RangeOfAqiChartsHelper { ), ); } + + static List getTooltipItems( + List touchedSpots, + List chartData, + ) { + return touchedSpots.asMap().entries.map((entry) { + final index = entry.key; + final spot = entry.value; + + final label = switch (spot.barIndex) { + 0 => 'Max', + 1 => 'Avg', + 2 => 'Min', + _ => '', + }; + + final date = DateFormat('dd/MM').format(chartData[spot.x.toInt()].date); + + return LineTooltipItem( + index == 0 ? '$date\n' : '', + const TextStyle( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + children: [ + TextSpan(text: '$label: ${spot.y.toStringAsFixed(0)}'), + ], + ); + }).toList(); + } + + static LineTouchData lineTouchData( + List chartData, + ) { + return LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + showOnTopOfTheChartBoxArea: false, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItems: (touchedSpots) => RangeOfAqiChartsHelper.getTooltipItems( + touchedSpots, + chartData, + ), + ), + ); + } } 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 887cdb69..2cefaa8e 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 @@ -17,7 +17,7 @@ class RangeOfAqiChart extends StatelessWidget { ( chartData.map((e) => e.max).toList(), const Color(0xFF962DFF), - const Color(0xFF5F00BD) + const Color(0xFF5F00BD), ), ( chartData.map((e) => e.avg).toList(), @@ -27,7 +27,7 @@ class RangeOfAqiChart extends StatelessWidget { ( chartData.map((e) => e.min).toList(), const Color(0xFF93AAFD), - const Color(0xFF023DFE) + const Color(0xFF023DFE), ), ]; @@ -41,7 +41,7 @@ class RangeOfAqiChart extends StatelessWidget { gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), borderData: EnergyManagementChartsHelper.borderData(), - lineTouchData: EnergyManagementChartsHelper.lineTouchData(), + lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), betweenBarsData: [ BetweenBarsData( fromIndex: 0, From 03009ed27622c10be0396df7731f6d057c7bb42f Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 13:21:42 +0300 Subject: [PATCH 39/40] made a `RangeOfAqiChart._lines` a getter. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2cefaa8e..9423c30d 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,7 +13,7 @@ class RangeOfAqiChart extends StatelessWidget { required this.chartData, }); - List<(List values, Color color, Color? dotColor)> _lines() => [ + List<(List values, Color color, Color? dotColor)> get _lines => [ ( chartData.map((e) => e.max).toList(), const Color(0xFF962DFF), @@ -57,7 +57,7 @@ class RangeOfAqiChart extends StatelessWidget { ), ), ], - lineBarsData: _lines().map((e) { + lineBarsData: _lines.map((e) { final (values, color, dotColor) = e; return _buildLine(values: values, color: color, dotColor: dotColor); }).toList(), From 83363b4c50efa294bf06774d5bbebcb4cb46031c Mon Sep 17 00:00:00 2001 From: Faris Armoush Date: Tue, 27 May 2025 15:15:29 +0300 Subject: [PATCH 40/40] Made `RangeOfAqiChart._lines` colors use `ColorsManager` colors instead of statically defining them in the widget itself using Hex codes. --- .../modules/air_quality/widgets/range_of_aqi_chart.dart | 8 ++++---- lib/utils/color_manager.dart | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) 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 9423c30d..08a036c0 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 @@ -16,8 +16,8 @@ class RangeOfAqiChart extends StatelessWidget { List<(List values, Color color, Color? dotColor)> get _lines => [ ( chartData.map((e) => e.max).toList(), - const Color(0xFF962DFF), - const Color(0xFF5F00BD), + ColorsManager.maxPurple, + ColorsManager.maxPurpleDot, ), ( chartData.map((e) => e.avg).toList(), @@ -26,8 +26,8 @@ class RangeOfAqiChart extends StatelessWidget { ), ( chartData.map((e) => e.min).toList(), - const Color(0xFF93AAFD), - const Color(0xFF023DFE), + ColorsManager.minBlue, + ColorsManager.minBlueDot, ), ]; diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index c2e4e60d..41ceb29a 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -79,4 +79,8 @@ abstract class ColorsManager { static const Color unhealthyRed = Color(0xFFD40000); static const Color severePink = Color(0xFFD40094); static const Color hazardousPurple = Color(0xFFBA01FD); + static const Color maxPurple = Color(0xFF962DFF); + static const Color maxPurpleDot = Color(0xFF5F00BD); + static const Color minBlue = Color(0xFF93AAFD); + static const Color minBlueDot = Color(0xFF023DFE); }