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]; +} 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..febbcf58 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -0,0 +1,42 @@ +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( + RangeOfAqiState( + status: RangeOfAqiStatus.loading, + rangeOfAqi: state.rangeOfAqi, + ), + ); + 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]; +} 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..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,8 +1,12 @@ 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'; 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 +16,18 @@ 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, + aqiType: AqiType.aqi, + ); } static void clearAllData(BuildContext context) { @@ -26,6 +37,8 @@ abstract final class FetchAirQualityDataHelper { context.read().add( const RealtimeDeviceChangesClosed(), ); + + context.read().add(const ClearRangeOfAqiEvent()); } static void loadAnalyticsDevices( @@ -49,4 +62,21 @@ abstract final class FetchAirQualityDataHelper { ), ); } + + static void loadRangeOfAqi( + BuildContext context, { + required String spaceUuid, + required DateTime date, + required AqiType aqiType, + }) { + context.read().add( + LoadRangeOfAqiEvent( + GetRangeOfAqiParam( + date: date, + spaceUuid: spaceUuid, + aqiType: aqiType, + ), + ), + ); + } } diff --git a/lib/pages/analytics/modules/air_quality/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..21cb2a9e --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -0,0 +1,115 @@ +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'; +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, List data) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + return titlesData.copyWith( + bottomTitles: titlesData.bottomTitles.copyWith( + sideTitles: titlesData.bottomTitles.sideTitles.copyWith( + 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, + ), + ), + ), + ), + ), + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 50, + maxIncluded: false, + getTitlesWidget: (value, meta) { + final text = value >= 300 ? '301+' : value.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, + ), + ), + ), + ); + }, + ), + ), + ); + } + + 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/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index 38f62cd7..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 @@ -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()), ], ), @@ -40,16 +41,16 @@ class AirQualityView extends StatelessWidget { spacing: 32, children: [ Expanded( - flex: 2, + flex: 5, child: Column( spacing: 20, children: [ - Expanded(child: Placeholder()), + Expanded(child: RangeOfAqiChartBox()), Expanded(child: Placeholder()), ], ), ), - Expanded(child: AirQualityEndSideWidget()), + Expanded(flex: 2, child: AirQualityEndSideWidget()), ], ), ), 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) { 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..ea85f075 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -0,0 +1,75 @@ +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'), + hcho('HCHO'), + tvoc('TVOC'), + co2('CO2'), + c6h6('C6H6'); + + 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); + + @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, + 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: const EdgeInsetsDirectional.symmetric( + horizontal: 12, + vertical: 2, + ), + 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: 12, + ); + } +} 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..08a036c0 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -0,0 +1,97 @@ +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 chartData; + + const RangeOfAqiChart({ + super.key, + required this.chartData, + }); + + List<(List values, Color color, Color? dotColor)> get _lines => [ + ( + chartData.map((e) => e.max).toList(), + ColorsManager.maxPurple, + ColorsManager.maxPurpleDot, + ), + ( + chartData.map((e) => e.avg).toList(), + Colors.white, + null, + ), + ( + chartData.map((e) => e.min).toList(), + ColorsManager.minBlue, + ColorsManager.minBlueDot, + ), + ]; + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + minY: 0, + maxY: 301, + clipData: const FlClipData.vertical(), + gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), + titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), + borderData: EnergyManagementChartsHelper.borderData(), + lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 2, + 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.6); + }).toList(), + ), + ), + ], + lineBarsData: _lines.map((e) { + final (values, color, dotColor) = e; + return _buildLine(values: values, color: color, dotColor: dotColor); + }).toList(), + ), + duration: Duration.zero, + ); + } + + FlDotData _buildDotData(Color color) { + return FlDotData( + show: true, + getDotPainter: (_, __, ___, ____) => FlDotCirclePainter( + radius: 2, + color: ColorsManager.whiteColors, + strokeWidth: 2, + strokeColor: color, + ), + ); + } + + LineChartBarData _buildLine({ + required List values, + required Color color, + Color? dotColor, + }) { + const invisibleDot = FlDotData(show: false); + 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) : invisibleDot, + belowBarData: BarAreaData(show: false), + ); + } +} 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..0fe4c4bd --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -0,0 +1,42 @@ +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/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'; + +class RangeOfAqiChartBox extends StatelessWidget { + const RangeOfAqiChartBox({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.errorMessage != null) ...[ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + ], + RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)), + ], + ), + ); + }, + ); + } +} 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..04cefd6c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -0,0 +1,82 @@ +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/analytics/widgets/charts_loading_widget.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; + +class RangeOfAqiChartTitle extends StatelessWidget { + const RangeOfAqiChartTitle({required this.isLoading, super.key}); + final bool isLoading; + + 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( + children: [ + ChartsLoadingWidget(isLoading: isLoading), + 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 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 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; + + FetchAirQualityDataHelper.loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: context.read().state.monthlyDate, + aqiType: value ?? AqiType.aqi, + ); + }, + ), + ), + ), + ], + ); + } +} 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(), ); 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..eec31998 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -0,0 +1,59 @@ +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.0385, + 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, + ), + ), + DefaultTextStyle( + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + child: title, + ), + ], + ), + ), + ); + } +} 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 11c088e8..b1af85c8 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, 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 001f4d2c..52c6f591 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/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), ); } } 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..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, + ), ); } @@ -35,9 +37,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget { List get _lineBarsData { return [ LineChartBarData( - preventCurveOvershootingThreshold: 0.1, - curveSmoothness: 0.55, - preventCurveOverShooting: true, spots: chartData .asMap() .entries 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..bbf24658 --- /dev/null +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -0,0 +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( + { + required this.date, + required this.spaceUuid, + required this.aqiType, + }); + + @override + List get props => [date, spaceUuid]; +} 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..13173c94 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart @@ -0,0 +1,30 @@ +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 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)); + + 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: 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 diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 5a892aa6..41ceb29a 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -73,4 +73,14 @@ 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); + static const Color maxPurple = Color(0xFF962DFF); + static const Color maxPurpleDot = Color(0xFF5F00BD); + static const Color minBlue = Color(0xFF93AAFD); + static const Color minBlueDot = Color(0xFF023DFE); }