mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-17 02:25:31 +00:00
Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1594-device-location-api-integration
This commit is contained in:
@ -0,0 +1,174 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AqiDistributionChart extends StatelessWidget {
|
||||
const AqiDistributionChart({super.key, required this.chartData});
|
||||
final List<AirQualityDataModel> chartData;
|
||||
|
||||
static const _rodStackItemsSpacing = 0.4;
|
||||
static const _barWidth = 13.0;
|
||||
static final _barBorderRadius = BorderRadius.circular(22);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sortedData = List<AirQualityDataModel>.from(chartData)
|
||||
..sort(
|
||||
(a, b) => a.date.compareTo(b.date),
|
||||
);
|
||||
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
maxY: 100.1,
|
||||
gridData: EnergyManagementChartsHelper.gridData(
|
||||
horizontalInterval: 20,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
barTouchData: _barTouchData(context),
|
||||
titlesData: _titlesData(context),
|
||||
barGroups: _buildBarGroups(sortedData),
|
||||
),
|
||||
duration: Duration.zero,
|
||||
);
|
||||
}
|
||||
|
||||
List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
|
||||
return List.generate(sortedData.length, (index) {
|
||||
final data = sortedData[index];
|
||||
final stackItems = <BarChartRodData>[];
|
||||
double currentY = 0;
|
||||
bool isFirstElement = true;
|
||||
|
||||
// Sort data by type to ensure consistent order
|
||||
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
|
||||
..sort((a, b) => a.type.compareTo(b.type));
|
||||
|
||||
for (final percentageData in sortedPercentageData) {
|
||||
stackItems.add(
|
||||
BarChartRodData(
|
||||
fromY: currentY,
|
||||
toY: currentY + percentageData.percentage ,
|
||||
color: AirQualityDataModel.metricColors[percentageData.name]!,
|
||||
borderRadius: isFirstElement
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(22),
|
||||
topRight: Radius.circular(22),
|
||||
)
|
||||
: _barBorderRadius,
|
||||
width: _barWidth,
|
||||
),
|
||||
);
|
||||
currentY += percentageData.percentage + _rodStackItemsSpacing;
|
||||
isFirstElement = false;
|
||||
}
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: stackItems,
|
||||
groupVertically: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
BarTouchData _barTouchData(BuildContext context) {
|
||||
return BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (_) => ColorsManager.whiteColors,
|
||||
tooltipBorder: const BorderSide(
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
final data = chartData[group.x.toInt()];
|
||||
|
||||
final List<TextSpan> children = [];
|
||||
|
||||
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 12,
|
||||
);
|
||||
|
||||
// Sort data by type to ensure consistent order
|
||||
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
|
||||
..sort((a, b) => a.type.compareTo(b.type));
|
||||
|
||||
for (final percentageData in sortedPercentageData) {
|
||||
children.add(TextSpan(
|
||||
text:
|
||||
'\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
|
||||
style: textStyle,
|
||||
));
|
||||
}
|
||||
|
||||
return BarTooltipItem(
|
||||
DateFormat('dd/MM/yyyy').format(data.date),
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FlTitlesData _titlesData(BuildContext context) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 20,
|
||||
);
|
||||
|
||||
final leftTitles = titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 20,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${value.toStringAsFixed(0)}%',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
chartData[value.toInt()].date.day.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
reservedSize: 36,
|
||||
),
|
||||
);
|
||||
|
||||
return titlesData.copyWith(
|
||||
leftTitles: leftTitles,
|
||||
bottomTitles: bottomTitles,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiDistributionChartBox extends StatelessWidget {
|
||||
const AqiDistributionChartBox({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AirQualityDistributionBloc, AirQualityDistributionState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(30),
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state.errorMessage != null) ...[
|
||||
AnalyticsErrorWidget(state.errorMessage),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
AqiDistributionChartTitle(
|
||||
isLoading: state.status == AirQualityDistributionStatus.loading,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: AqiDistributionChart(chartData: state.filteredChartData),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
|
||||
class AqiDistributionChartTitle extends StatelessWidget {
|
||||
const AqiDistributionChartTitle({required this.isLoading, super.key});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
@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('Distribution over Air Quality Index'),
|
||||
),
|
||||
),
|
||||
),
|
||||
FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
context
|
||||
.read<AirQualityDistributionBloc>()
|
||||
.add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -3,17 +3,18 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
enum AqiType {
|
||||
aqi('AQI', ''),
|
||||
pm25('PM2.5', 'µg/m³'),
|
||||
pm10('PM10', 'µg/m³'),
|
||||
hcho('HCHO', 'mg/m³'),
|
||||
tvoc('TVOC', 'µg/m³'),
|
||||
co2('CO2', 'ppm');
|
||||
aqi('AQI', '', 'aqi'),
|
||||
pm25('PM2.5', 'µg/m³', 'pm25'),
|
||||
pm10('PM10', 'µg/m³', 'pm10'),
|
||||
hcho('HCHO', 'mg/m³', 'hcho'),
|
||||
tvoc('TVOC', 'µg/m³', 'tvoc'),
|
||||
co2('CO2', 'ppm', 'co2');
|
||||
|
||||
const AqiType(this.value, this.unit);
|
||||
const AqiType(this.value, this.unit, this.code);
|
||||
|
||||
final String value;
|
||||
final String unit;
|
||||
final String code;
|
||||
}
|
||||
|
||||
class AqiTypeDropdown extends StatefulWidget {
|
||||
|
@ -13,23 +13,37 @@ class RangeOfAqiChart extends StatelessWidget {
|
||||
required this.chartData,
|
||||
});
|
||||
|
||||
List<(List<double> values, Color color, Color? dotColor)> get _lines => [
|
||||
(
|
||||
chartData.map((e) => e.max).toList(),
|
||||
ColorsManager.maxPurple,
|
||||
ColorsManager.maxPurpleDot,
|
||||
),
|
||||
(
|
||||
chartData.map((e) => e.avg).toList(),
|
||||
Colors.white,
|
||||
null,
|
||||
),
|
||||
(
|
||||
chartData.map((e) => e.min).toList(),
|
||||
ColorsManager.minBlue,
|
||||
ColorsManager.minBlueDot,
|
||||
),
|
||||
];
|
||||
List<(List<double> values, Color color, Color? dotColor)> get _lines {
|
||||
final sortedData = List<RangeOfAqi>.from(chartData)
|
||||
..sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
return [
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.max ?? 0;
|
||||
}).toList(),
|
||||
ColorsManager.maxPurple,
|
||||
ColorsManager.maxPurpleDot,
|
||||
),
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.average ?? 0;
|
||||
}).toList(),
|
||||
Colors.white,
|
||||
null,
|
||||
),
|
||||
(
|
||||
sortedData.map((e) {
|
||||
final value = e.data.firstOrNull;
|
||||
return value?.min ?? 0;
|
||||
}).toList(),
|
||||
ColorsManager.minBlue,
|
||||
ColorsManager.minBlueDot,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -32,7 +32,7 @@ class RangeOfAqiChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -1,15 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
|
||||
|
||||
class RangeOfAqiChartTitle extends StatelessWidget {
|
||||
const RangeOfAqiChartTitle({required this.isLoading, super.key});
|
||||
const RangeOfAqiChartTitle({
|
||||
required this.isLoading,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
|
||||
static const List<(Color color, String title, bool hasBorder)> _colors = [
|
||||
@ -66,12 +69,9 @@ class RangeOfAqiChartTitle extends StatelessWidget {
|
||||
|
||||
if (spaceUuid == null) return;
|
||||
|
||||
FetchAirQualityDataHelper.loadRangeOfAqi(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
|
||||
aqiType: value ?? AqiType.aqi,
|
||||
);
|
||||
if (value != null) {
|
||||
context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
Reference in New Issue
Block a user