diff --git a/.run/main.dart.run.xml b/.run/main.dart.run.xml index 4767aff8..4a58a7c9 100644 --- a/.run/main.dart.run.xml +++ b/.run/main.dart.run.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/lib/config/my_router.dart b/lib/config/my_router.dart index a4e4693f..da605ad4 100644 --- a/lib/config/my_router.dart +++ b/lib/config/my_router.dart @@ -295,6 +295,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:uae_stat/presentation/Screens/auth_verification/registration.dart'; +import 'package:uae_stat/presentation/Screens/demo_home2.dart'; import 'package:uae_stat/presentation/routes/drawer_routes/custom_drawer_routes.dart'; import '../domain/use_cases/preferences_use_case.dart'; @@ -302,6 +303,7 @@ import '../presentation/Screens/auth_verification/changepassword.dart'; import '../presentation/Screens/auth_verification/confirm_password.dart'; import '../presentation/Screens/auth_verification/create_new_pw.dart'; import '../presentation/Screens/auth_verification/otp_verification.dart'; +import '../presentation/Screens/charts/chart.dart'; import '../presentation/Screens/demo_home.dart'; import '../presentation/Screens/profilepage.dart'; import '../presentation/routes/auth_routes/login_route.dart'; @@ -313,13 +315,12 @@ final GoRouter router = GoRouter( routes: [ GoRoute( path: '/', -builder: (context, state) => LoginRoute(), + builder: (context, state) => LoginRoute(), //builder: (context, state) => MyHomePage(), ), GoRoute( path: '/login', builder: (context, state) => LoginRoute(), - ), GoRoute( path: '/myhomepage', @@ -330,8 +331,20 @@ builder: (context, state) => LoginRoute(), builder: (context, state) => RegisterScreen(), ), GoRoute( - path: '/DemoHome', - builder: (context, state) => ChartPage(), + path: '/DemoHome/:dataSets', + builder: (context, state) { + final dataSets = state.pathParameters['dataSets']!; + print('Router $dataSets'); + return ChartPage(dataSets: dataSets); + }, + ), + GoRoute( + path: '/Chart/:dataSets', + builder: (context, state) { + final dataSets = state.pathParameters['dataSets']!; + print('Router $dataSets'); + return ChartScreen(dataSets: dataSets); + }, ), GoRoute( path: '/mailverification', @@ -390,4 +403,3 @@ builder: (context, state) => LoginRoute(), ), ], ); - diff --git a/lib/presentation/Screens/charts/chart.dart b/lib/presentation/Screens/charts/chart.dart new file mode 100644 index 00000000..1911ddee --- /dev/null +++ b/lib/presentation/Screens/charts/chart.dart @@ -0,0 +1,1225 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; // Import Syncfusion charts package +import 'package:http/http.dart' as http; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: ChartScreen(dataSets: ''), + ); + } +} + +class ChartScreen extends StatefulWidget { + final String dataSets; + const ChartScreen({Key? key, required this.dataSets}); + @override + ChartScreenState createState() => ChartScreenState(); +} + +class ChartScreenState extends State { + List> selectedFiltersStorage = []; + List chartsData = []; + List cardData = []; + List filterData = []; + List> originalChartsData = []; + List> originalCardData = []; + bool isLoading = true; + List isChartData = []; + List nonChartData = []; + // dynamic firstYear; + // dynamic lastYearValue; + // dynamic secondLastYearValue; + // dynamic lastYear; + // dynamic secondLastYear; + // dynamic roundedAverage; + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + void initState() { + super.initState(); + fetchChartData(widget.dataSets); + } + + Future fetchChartData(String dataSets) async { + const baseUrl = + 'https://pb.venbait.in/api/getDataSet'; // Replace with your server URL + final url = Uri.parse('$baseUrl?dataset=$dataSets'); + + try { + final response = await http.get(url); + if (response.statusCode == 200) { + final List data = jsonDecode(response.body); + + // Variables to store filtered data + for (var item in data) { + if (item['is_chart'] == 'true') { + isChartData.add(item); + } else if (item['is_chart'] == 'false') { + nonChartData.add(item); + } + } + + // Print or use the filtered data + print('Chart Data: $isChartData'); + print('Non-Chart Data: $nonChartData'); + + // Save original data for reset + originalChartsData = List.from(isChartData); // Store original data + originalCardData = List.from(nonChartData); // Store original data + setState(() { + chartsData = isChartData; + cardData = nonChartData; + print('chartsData $chartsData'); + print('cardData $cardData'); + isLoading = false; + }); + } else { + throw Exception('Failed to load data'); + } + } catch (error) { + setState(() { + isLoading = false; + }); + print('Error fetching data: $error'); + } + } + + // void extractLastTwoYearsData(Map nonChartData) { + // try { + // final responseData = nonChartData['response']; + // final response = responseData is String + // ? jsonDecode(responseData) as List + // : responseData as List; + // + // // Extracting the time periods and sorting them in descending order + // final sortedResponse = response + // .map((item) => item['ObsKey']['TIME_PERIOD'].toString()) + // .toList() + // ..sort((a, b) => b.compareTo(a)); + // + // // Get the last year and second-to-last year + // lastYear = sortedResponse.isNotEmpty ? sortedResponse.first : null; + // secondLastYear = sortedResponse.length > 1 ? sortedResponse[1] : null; + // firstYear = sortedResponse.isNotEmpty + // ? sortedResponse.last + // : null; // First year (oldest) + // + // // Extract the corresponding values + // lastYearValue = response.firstWhere( + // (item) => item['ObsKey']['TIME_PERIOD'].toString() == lastYear, + // orElse: () => null)?['ObsValue']['Value']; + // + // secondLastYearValue = response.firstWhere( + // (item) => item['ObsKey']['TIME_PERIOD'].toString() == secondLastYear, + // orElse: () => null)?['ObsValue']['Value']; + // + // // Print or use the extracted values + // print('Last Year: $lastYear, Value: $lastYearValue'); + // print( + // 'Second-to-Last Year: $secondLastYear, Value: $secondLastYearValue'); + // + // // Extract all values and calculate the sum and average + // List allValues = response + // .map((item) => + // double.tryParse(item['ObsValue']['Value'].toString()) ?? 0.0) + // .toList(); + // + // double sum = allValues.reduce((a, b) => a + b); + // double average = sum / allValues.length; + // // Round the average to the nearest whole number + // roundedAverage = average.round(); + // + // // Print sum and rounded average + // print('Sum of Values: $sum'); + // print('Rounded Average Value: $roundedAverage'); + // } catch (e) { + // print('Error processing data: $e'); + // } + // } + +// When decoding JSON, you can convert the LinkedMap to a Map + Map processNonChartData(item) { + print('processNonChartData check test case'); + try { + final response = item['response']; + + // If 'response' is a LinkedMap, convert it to Map + final List responseData = response is LinkedHashMap + ? Map.from( + response) // Convert to Map + : response; + + // Now you can process the Map as expected + final sortedResponse = List.from( + responseData.map((e) => e['ObsKey']['TIME_PERIOD'].toString())) + ..sort((a, b) => b.compareTo(a)); + + String? lastYear = + sortedResponse.isNotEmpty ? sortedResponse.first : null; + String? secondLastYear = + sortedResponse.length > 1 ? sortedResponse[1] : null; + String? firstYear = + sortedResponse.isNotEmpty ? sortedResponse.last : null; + + String? lastYearValue = response.firstWhere( + (item) => item['ObsKey']['TIME_PERIOD'].toString() == lastYear, + orElse: () => null, + )?['ObsValue']['Value']; + + String? secondLastYearValue = response.firstWhere( + (item) => item['ObsKey']['TIME_PERIOD'].toString() == secondLastYear, + orElse: () => null, + )?['ObsValue']['Value']; + + List allValues = responseData + .map((item) => + double.tryParse(item['ObsValue']['Value'].toString()) ?? 0.0) + .toList(); + + double sum = allValues.reduce((a, b) => a + b); + double average = sum / allValues.length; + double roundedAverage = + average.roundToDouble(); // Rounds to the nearest integer + + return { + 'lastYear': lastYear ?? 'NA', + 'secondLastYear': secondLastYear ?? 'NA', + 'firstYear': firstYear ?? 'NA', + 'lastYearValue': lastYearValue ?? 'NA', + 'secondLastYearValue': secondLastYearValue ?? 'NA', + 'roundedAverage': roundedAverage, + 'allValues': allValues.isEmpty ? [0.0] : allValues, + }; + } catch (e) { + print('Error processing data: $e'); + return {}; + } + } + + String capitalizeAndSplit(String text) { + if (text.isEmpty) return text; + return text + .split('_') // Split by underscores + .map((word) => word[0].toUpperCase() + word.substring(1).toLowerCase()) + .join(' '); // Join words with a space + } + + List parseLineChartData(List response) { + return response.map((entry) { + // Attempt to fetch the TIMEPERIOD + final rawTimePeriod = + entry['ObsKey']?['Year'] ?? entry['ObsKey']?['TIME_PERIOD']; + final formattedTimePeriod = rawTimePeriod != null + ? rawTimePeriod.toString() // Use as-is if valid + : 'Unknown'; // Fallback value if null + + // Parse the value or fallback to 0.0 + final value = + double.tryParse(entry['ObsValue']?['Value']?.toString() ?? '0.0') ?? + 0.0; + + return ChartData( + timePeriod: formattedTimePeriod, + value: value, + ); + }).toList(); + } + + List parseBarChartData(List response) { + return response + .asMap() + .entries + .map((entry) => ChartData( + timePeriod: entry.value['ObsKey']['TIME_PERIOD'] ?? + 'Unknown', // Fallback to 'Unknown' if null + value: double.tryParse( + entry.value['ObsValue']['Value'].toString()) ?? + 0.0, // Handle null/invalid value + )) + .toList(); + } + +// Update parseColumnChartData for column chart + List parseColumnChartData(List response) { + return response.map((entry) { + // Extract TIME_PERIOD as x + final x = + int.tryParse(entry['ObsKey']?['TIME_PERIOD']?.toString() ?? '0') ?? 0; + // Extract Value as y + final y = + double.tryParse(entry['ObsValue']?['Value']?.toString() ?? '0.0') ?? + 0.0; + return ChartData(xInt: x, y: y); + }).toList(); + } + +// Update parseScatterChartData for scatter chart + List parseScatterChartData(List response) { + return response.map((entry) { + // Parse TIME_PERIOD into DateTime + final timePeriod = entry['ObsKey']?['TIME_PERIOD']; + final x = timePeriod != null + ? DateTime.tryParse('$timePeriod-01-01') // Convert year to DateTime + : DateTime.now(); // Fallback to current date if null + + // Parse Value into double + final y = + double.tryParse(entry['ObsValue']?['Value']?.toString() ?? '0.0') ?? + 0.0; + + return ChartData(xDateTime: x, y: y); + }).toList(); + } + + List parseStackedBarChartData( + dynamic chartData, String groupByKey, String groupByValue) { + return chartData['response'] + .where((entry) => entry['ObsKey'][groupByKey] == groupByValue) + .map((entry) { + // Specify the type here + return ChartData( + x: entry['ObsKey']['TIME_PERIOD'], + y: double.tryParse(entry['ObsValue']['Value']) ?? 0.0, + ); + }).toList(); + } + + void applyFilters(BuildContext context, List filters, List data, + List dataCard, List selectedFilters) { + print("Selected Filters before applying: $selectedFilters"); + // Check if all filter_data is empty + if (selectedFilters.every((filter) => filter['filter_data'].isEmpty)) { + setState(() { + chartsData = List.from(originalChartsData); // Restore the original data + cardData = List.from(originalCardData); // Restore the original data + }); + print('Returning as no filters are selected'); + Navigator.pop(context); + return; // Exit early since all filter_data are empty + } + + // Loop through each chart data in the `data` list + List filteredData = []; + for (var chart in data) { + // Extract response data for filtering + List response = chart['response'] ?? []; + + // Filter the response based on selected filters + var chartFilteredData = response.where((responseItem) { + final obsKey = responseItem['ObsKey']; + + // Check if each selected filter's `filter_data` matches `ObsKey` values + return selectedFilters.every((filter) { + final filterKey = filter['filter_key']; + final filterValues = filter['filter_data']; + + // Ensure the filter_key exists in the `ObsKey` and check if it matches any of the `filter_data` + if (obsKey.containsKey(filterKey)) { + final obsKeyValue = obsKey[filterKey]?.toString(); + return filterValues.isEmpty || filterValues.contains(obsKeyValue); + } + return false; + }); + }).toList(); + + // If any data matches the filter, add the whole chart data object + if (chartFilteredData.isNotEmpty) { + filteredData.add({ + ...chart, // Include all other properties of the chart object + 'response': chartFilteredData, // Only include filtered response data + }); + } + } + + List filteredCardData = []; + for (var chart in dataCard) { + Map chartData = Map.from(chart); + // Extract response data for filtering + List response = chartData['response'] ?? []; + + // Filter the response based on selected filters + var cardFilteredData = response.where((responseItem) { + final obsKey = responseItem['ObsKey']; + + // Check if each selected filter's `filter_data` matches `ObsKey` values + return selectedFilters.every((filter) { + final filterKey = filter['filter_key']; + final filterValues = filter['filter_data']; + + // Ensure the filter_key exists in the `ObsKey` and check if it matches any of the `filter_data` + if (obsKey.containsKey(filterKey)) { + final obsKeyValue = obsKey[filterKey]?.toString(); + return filterValues.isEmpty || filterValues.contains(obsKeyValue); + } + return false; + }); + }).toList(); + + // If any data matches the filter, add the whole chart data object + if (cardFilteredData.isNotEmpty) { + filteredCardData.add({ + ...chart, // Include all other properties of the chart object + 'response': cardFilteredData, // Only include filtered response data + }); + } + } + + // Update the chartsData with the filtered data + setState(() { + chartsData = filteredData; + cardData = filteredCardData; + }); + + print("Filtered Data: $filteredData"); + print("Filtered Card: $filteredCardData"); + + // Go back after applying filters + Navigator.pop(context); + } + + Widget buildChart(dynamic chartData) { + // Extract group_by dynamically from the chartData + String groupByKey = chartData['group_by']; + + // Extract unique values for the group_by key dynamically + Set groupByValues = {}; + chartData['response'].forEach((entry) { + groupByValues.add(entry['ObsKey'][groupByKey]); + }); + switch (chartData['chart_type']) { + case 'scatter': + return SfCartesianChart( + primaryXAxis: DateTimeAxis(), // Ensure it's DateTimeAxis + tooltipBehavior: TooltipBehavior(enable: true), + title: ChartTitle(text: 'Divorces'), + series: [ + ScatterSeries( + dataSource: parseScatterChartData(chartData['response']), + xValueMapper: (ChartData data, _) => + data.xDateTime ?? DateTime.now(), + yValueMapper: (ChartData data, _) => data.y, + name: chartData['dataset'], + dataLabelSettings: DataLabelSettings(isVisible: true), + ), + ], + ); + case 'column': + return SfCartesianChart( + primaryXAxis: NumericAxis( + title: AxisTitle(text: 'Year'), + interval: 1, // Ensure no fractional intervals + ), + title: ChartTitle(text: 'Marriages'), + series: >[ + ColumnSeries( + dataSource: parseColumnChartData(chartData['response']), + xValueMapper: (ChartData data, _) => data.xInt ?? 0, + yValueMapper: (ChartData data, _) => data.y, + name: chartData['dataset'], + color: Color(0xFF7DAFBC), + width: 0.8, + spacing: 0.2, + dataLabelSettings: + DataLabelSettings(isVisible: true), // Show data labels + ), + ], + ); + case 'line': + return SfCartesianChart( + primaryXAxis: CategoryAxis(), + title: ChartTitle(text: 'Line Chart'), + legend: Legend(isVisible: true), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + LineSeries( + dataSource: parseLineChartData(chartData['response']), + xValueMapper: (ChartData data, _) => data.timePeriod ?? '', + yValueMapper: (ChartData data, _) => data.value ?? 0.0, + name: 'Sales', + dataLabelSettings: DataLabelSettings(isVisible: true), + ), + ], + ); + case 'bar': + return SfCartesianChart( + primaryXAxis: CategoryAxis(), + title: ChartTitle(text: 'Bar Chart'), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + BarSeries( + dataSource: parseBarChartData(chartData['response']), + xValueMapper: (ChartData data, _) => data.timePeriod ?? '', + yValueMapper: (ChartData data, _) => data.value ?? 0.0, + name: 'Gold', + color: Color.fromRGBO(8, 142, 255, 1), + ), + ], + ); + case 'stacked_bar': + String chartTitle = + capitalizeAndSplit(chartData['chart_heading'] ?? ''); + // Define a list of colors + final List uniqueColors = [ + Color(0xFF648CBA), + Color(0xFF90B0D5), + Color(0xFF98BCE5), + Color(0xFFA7B5C5), + Color(0xFFBED3EC), + Color(0xFFD4E3F4), + ]; + return SfCartesianChart( + primaryXAxis: CategoryAxis(), + title: ChartTitle(text: chartData['chart_heading']), + legend: Legend( + isVisible: true, + position: LegendPosition.bottom, + overflowMode: LegendItemOverflowMode.scroll, + ), + tooltipBehavior: TooltipBehavior( + enable: true, // Enable tooltips + format: 'point.x : point.y', // Custom tooltip format + ), + series: >[ + for (int i = 0; i < groupByValues.length; i++) + StackedBarSeries( + dataSource: parseStackedBarChartData( + chartData, groupByKey, groupByValues.elementAt(i)), + xValueMapper: (ChartData data, _) => data.x ?? '', + yValueMapper: (ChartData data, _) => data.y, + name: groupByValues.elementAt(i), + color: uniqueColors[i % uniqueColors.length], + enableTooltip: true, + ), + ], + ); + case 'stacked_column': + String chartTitle = + capitalizeAndSplit(chartData['chart_heading'] ?? ''); + // Define a list of colors + final List uniqueColors = [ + Color(0xFF648CBA), + Color(0xFF90B0D5), + Color(0xFF98BCE5), + Color(0xFFA7B5C5), + Color(0xFFBED3EC), + Color(0xFFD4E3F4), + ]; + return SfCartesianChart( + primaryXAxis: CategoryAxis(), + title: ChartTitle(text: chartData['chart_heading']), + legend: Legend( + isVisible: true, + position: LegendPosition.bottom, + overflowMode: LegendItemOverflowMode.scroll, + ), + tooltipBehavior: TooltipBehavior( + enable: true, // Enable tooltips + format: 'point.x : point.y', // Custom tooltip format + ), + series: >[ + for (int i = 0; i < groupByValues.length; i++) + StackedColumnSeries( + dataSource: parseStackedBarChartData( + chartData, groupByKey, groupByValues.elementAt(i)), + xValueMapper: (ChartData data, _) => data.x ?? '', + yValueMapper: (ChartData data, _) => data.y, + name: groupByValues.elementAt(i), + color: uniqueColors[i % uniqueColors.length], + enableTooltip: true, + ), + ], + ); + default: + return Center(child: Text('Unknown chart type')); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + backgroundColor: Colors.brown[200]!, + appBar: AppBar( + backgroundColor: Colors.brown[200]!, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios_new, color: Colors.white), + onPressed: () { + context.go('/myhomepage'); + }, + ), + title: Text( + 'UAE Numbers', + style: TextStyle(color: Colors.white), + ), + ), + body: isLoading + ? Center(child: CircularProgressIndicator()) + : Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment + .end, // Aligns the row's content to the right + children: [ + Row( + children: [ + Text( + 'Bookmark', + style: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + Icon(Icons.bookmark_add_outlined, + color: Colors.white, size: 18), + ], + ), + SizedBox(width: 10), // Horizontal space between items + Row( + children: [ + Text( + 'Share', + style: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + Icon(Icons.share_sharp, + color: Colors.white, size: 18), + ], + ), + SizedBox(width: 10), // Horizontal space between items + IconButton( + icon: Icon(Icons.filter_alt_outlined, + color: Colors.white, size: 18), + onPressed: () { + showRightSideModal( + context, chartsData[0]["filters"], chartsData); + }, + ), + ], + ), + ], + ), + + // Padding( + // padding: const EdgeInsets.all(16.0), + // child: Align( + // alignment: Alignment.topRight, + // child: IconButton( + // icon: Icon(Icons.filter_list, color: Colors.white), + // onPressed: () { + // showRightSideModal( + // context, chartsData[0]["filters"], chartsData); + // }, + // ), + // ), + // ), + Expanded( + child: ListView( + children: [ + // nonChartData Cards + GridView.builder( + itemCount: cardData.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // 2 cards per row + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 177.5 / + 180, // Adjusted to maintain width and fixed height + ), + itemBuilder: (context, index) { + final item = cardData[index]; + print('item item item $item'); + final chart_type = item['chart_type']; + final chart_heading = item['chart_heading']; + final data = processNonChartData(item); + print('processNonChartData'); + + if (chart_type == 'total') { + return Card( + margin: const EdgeInsets.all(10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.public, + color: Color(0xFF90B0D5), size: 22), + const SizedBox(height: 5), + Text( + '${chart_heading ?? 'NA'}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + '(${data['lastYear'] ?? 'NA'})', + style: const TextStyle( + fontSize: 10, color: Colors.grey), + ), + Text( + '${data['lastYearValue'] ?? 'NA'}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF90B0D5), + ), + ), + const Divider( + color: Colors.grey, thickness: 1), + Text( + '${data['secondLastYearValue'] ?? 'NA'}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF90B0D5), + ), + ), + Text( + '(${data['secondLastYear'] ?? 'NA'})', + style: const TextStyle( + fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ); + } else if (chart_type == 'average') { + return Card( + margin: const EdgeInsets.all(10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.analytics, + color: Color(0xFF90B0D5), size: 22), + const SizedBox(height: 5), + Text( + '${chart_heading ?? 'NA'}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + '(${data['firstYear'] ?? 'NA'} - ${data['lastYear'] ?? 'NA'})', + style: const TextStyle( + fontSize: 10, color: Colors.grey), + ), + Text( + '${data['roundedAverage'] ?? 'NA'}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF90B0D5), + ), + ), + ], + ), + ), + ); + } else { + return const SizedBox + .shrink(); // Ignore unknown KPIs + } + }, + ), + // chartsData Cards + ListView.builder( + itemCount: chartsData.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Card( + margin: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + Container( + height: 300, + child: buildChart(chartsData[index]), + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ); + } + + void showRightSideModal(BuildContext context, List filters, List data) { + // Initialize selected filters structure from the stored value, if exists + List> selectedFilters = + selectedFiltersStorage.isNotEmpty + ? List.from(selectedFiltersStorage) // Use stored filters + : filters.map((filter) { + return {"filter_key": filter["filter_key"], "filter_data": []}; + }).toList(); // Or initialize empty filters + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Align( + alignment: Alignment.centerRight, + child: Container( + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.height, + color: Colors.white, + child: Column( + children: [ + // Header Section + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Filters', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + // Filters Section + Expanded( + child: ListView.builder( + itemCount: filters.length, + itemBuilder: (context, index) { + final filter = filters[index]; + final filterKey = filter["filter_key"]; + final filterData = filter["filter_data"]; + + return StatefulBuilder( + builder: (context, setState) { + // Get the corresponding selected filter object + var selectedFilter = selectedFilters + .firstWhere((f) => f["filter_key"] == filterKey); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + filterKey, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + InkWell( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, dialogSetState) { + return AlertDialog( + title: Text('Select $filterKey'), + content: SingleChildScrollView( + child: ListBody( + children: filterData + .map((value) { + return CheckboxListTile( + title: Text( + value.toString()), + value: selectedFilter[ + "filter_data"] + .contains(value), + onChanged: + (bool? isChecked) { + dialogSetState(() { + if (isChecked == + true) { + selectedFilter[ + "filter_data"] + .add(value); + } else { + selectedFilter[ + "filter_data"] + .remove(value); + } + }); + }, + ); + }).toList(), + ), + ), + actions: [ + TextButton( + onPressed: () { + // Store the updated selected filters after dialog closes + setState(() {}); + Navigator.pop(context); + }, + child: Text('OK'), + ), + ], + ); + }, + ); + }, + ); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8.0), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: + selectedFilter["filter_data"].isEmpty + ? [ + Text('Select $filterKey', + style: TextStyle( + color: Colors.grey)) + ] + : selectedFilter["filter_data"] + .map((value) { + return Chip( + label: Text(value), + onDeleted: () { + setState(() { + selectedFilter[ + "filter_data"] + .remove(value); + }); + }, + ); + }).toList(), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ), + ), + // Button Section + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: () { + // Clear all selected filters + selectedFilters.forEach((filter) { + filter["filter_data"].clear(); + }); + setState(() { + chartsData = List.from(originalChartsData); + cardData = List.from(originalCardData); + }); + // Reset the selectedFiltersStorage to empty when clearing + selectedFiltersStorage.clear(); + Navigator.pop(context); + }, + child: Text('Clear'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey, + ), + ), + ElevatedButton( + onPressed: () { + print( + "Selected Filters before applying: $selectedFilters"); + setState(() { + chartsData = originalChartsData; + cardData = originalCardData; + }); + applyFilters(context, filters, chartsData, cardData, + selectedFilters); + // Save the selected filters to storage after applying + selectedFiltersStorage = List.from(selectedFilters); + }, + child: Text('Apply Filter'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +class ChartData { + final int? xInt; // For column and other charts, using int for x + final DateTime? xDateTime; // For scatter charts, using DateTime for x + final double? y; // For column and other charts + final String? timePeriod; // For line and bar charts + final double? value; // For line and bar charts + late final double? y2; // For secondary data (stacked column chart) + late final double? y3; // For tertiary data (stacked column chart) + final double? y4; // For quaternary data (stacked column chart) + final String? x; // For stacked bar charts, using String for x + + // Constructor that handles all chart cases + ChartData({ + this.xInt, + this.xDateTime, + this.y, + this.timePeriod, + this.value, + this.y2, + this.y3, + this.y4, + this.x, + }); +} + +// [ +// { +// chart_type :"scatter" +// dataset :"divorces" +// is_chart :"true" +// kpi:"divorces" +// filters:[ +// { +// "filter_key": "REF_AREA", +// "filter_data": [ +// "AE", +// "AE-AJ", +// "AE-RK", +// "AE-FJ", +// "AE-AZ", +// "AE-DU", +// "AE-SH", +// "AE-UQ" +// ] +// }, +// { +// "filter_key": "TIME_PERIOD", +// "filter_data": [ +// "2021", +// "2017", +// "2018", +// "2019", +// "2020" +// ] +// }, +// { +// "filter_key": "DV_TYPE", +// "filter_data": [ +// "D_EM_NEF", +// "D_EM_EF", +// "D_NEW_NEF", +// "D_TOT", +// "D_NEM_EF" +// ] +// } +// ] +// group_by:"DV_TYPE" +// main_id :"social" +// response:[ +// { +// "ObsKey": { +// "DV_TYPE": "D_TOT", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE-DU", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2017", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "1484" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_TOT", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE-DU", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2019", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "1308" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_TOT", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE-DU", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2017", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "1484" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_TOT", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE-DU", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2018", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "1321" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_NEM_EF", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2017", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "181" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_NEM_EF", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2019", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "182" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_EM_NEF", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2017", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "766" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_EM_NEF", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2020", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "618" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_EM_EF", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2017", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "1717" +// } +// }, +// { +// "ObsKey": { +// "DV_TYPE": "D_EM_EF", +// "FREQ": "A", +// "MEASURE": "D", +// "REF_AREA": "AE", +// "SOURCE_DETAIL": "FCSAMD", +// "TIME_PERIOD": "2018", +// "UNIT_MEASURE": "NUMBER" +// }, +// "ObsValue": { +// "Value": "1720" +// } +// } +// +// ] +// sub_id :"vital_statistics" +// url :"https://releaseeuaestat.fcsc.gov.ae/rest/data/FCSA,DF_DV_NA,1.5.0/.A....?startPeriod=2017&endPeriod=2024&dimensionAtObservation=AllDimensions" +// } +// ] diff --git a/lib/presentation/Screens/demo_home.dart b/lib/presentation/Screens/demo_home.dart index 225f1b1b..895a5a29 100644 --- a/lib/presentation/Screens/demo_home.dart +++ b/lib/presentation/Screens/demo_home.dart @@ -13,12 +13,15 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - home: ChartPage(), + home: ChartPage(dataSets: ''), ); } } class ChartPage extends StatefulWidget { + final String dataSets; + const ChartPage({Key? key, required this.dataSets}); + @override ChartPageState createState() => ChartPageState(); } @@ -30,13 +33,13 @@ class ChartPageState extends State { @override void initState() { super.initState(); - fetchChartData(); + fetchChartData(widget.dataSets); } - Future fetchChartData() async { + Future fetchChartData(datasets) async { const baseUrl = 'https://pb.venbait.in/api/custom/apicall'; // Replace with your server URL - final url = Uri.parse('$baseUrl?dataset=hotels'); + final url = Uri.parse('$baseUrl?dataset=$datasets'); try { final response = await http.get(url); @@ -57,24 +60,34 @@ class ChartPageState extends State { } List parseLineChartData(List response) { - return response.map((entry) { - // Attempt to fetch the TIMEPERIOD - final rawTimePeriod = - entry['ObsKey']?['Year'] ?? entry['ObsKey']?['TIME_PERIOD']; - final formattedTimePeriod = rawTimePeriod != null - ? rawTimePeriod.toString() // Use as-is if valid - : 'Unknown'; // Fallback value if null + // Group by timePeriod (year) and sum the values + Map groupedData = {}; - // Parse the value or fallback to 0.0 + response.forEach((entry) { + final year = entry['ObsKey']['TIME_PERIOD']?.toString() ?? 'Unknown'; final value = - double.tryParse(entry['ObsValue']?['Value']?.toString() ?? '0.0') ?? + double.tryParse(entry['ObsValue']['Value']?.toString() ?? '0.0') ?? 0.0; - return ChartData( - timePeriod: formattedTimePeriod, - value: value, - ); - }).toList(); + // Add the value to the existing year (if exists) or initialize it + if (groupedData.containsKey(year)) { + groupedData[year] = groupedData[year]! + value; + } else { + groupedData[year] = value; + } + }); + + // Sort the grouped data by year in ascending order + var sortedEntries = groupedData.entries.toList() + ..sort((a, b) => int.parse(a.key).compareTo(int.parse(b.key))); + + // Convert the sorted data to a list of ChartData + return sortedEntries + .map((entry) => ChartData( + timePeriod: entry.key, + value: entry.value, + value1: 0.0)) // Add value1 as 0.0 + .toList(); } List parseBarChartData(List response) { @@ -82,63 +95,127 @@ class ChartPageState extends State { .asMap() .entries .map((entry) => ChartData( - timePeriod: entry.value['ObsKey']['TIME_PERIOD'] ?? - 'Unknown', // Fallback to 'Unknown' if null + timePeriod: entry.value['ObsKey']['TIME_PERIOD'] ?? 'Unknown', value: double.tryParse( entry.value['ObsValue']['Value'].toString()) ?? - 0.0, // Handle null/invalid value + 0.0, + value1: 0.0, // Ensure value1 is passed as well )) .toList(); } + List parseColumnChartData(List response) { + List chartDataList = []; + + // Temporary map to store values by TIME_PERIOD + Map timePeriodMap = {}; + + for (var entry in response) { + String timePeriod = entry['ObsKey']['TIME_PERIOD'].toString(); + double value = + double.tryParse(entry['ObsValue']['Value']?.toString() ?? '0.0') ?? + 0.0; + String indicator = entry['ObsKey']['H_INDICATOR']; + + // If the entry is for TOR, map it to 'value' + if (indicator == 'TOR') { + if (timePeriodMap.containsKey(timePeriod)) { + // Update the value by creating a new ChartData object + timePeriodMap[timePeriod] = ChartData( + timePeriod: timePeriod, + value: value, // Set the new value + value1: timePeriodMap[timePeriod]?.value1 ?? 0.0, + ); + } else { + timePeriodMap[timePeriod] = ChartData( + timePeriod: timePeriod, + value: value, + value1: 0.0, + ); + } + } + + // If the entry is for TAR, map it to 'value1' + if (indicator == 'TAR') { + if (timePeriodMap.containsKey(timePeriod)) { + // Create a new ChartData object with the updated value1 + timePeriodMap[timePeriod] = ChartData( + timePeriod: timePeriod, + value: timePeriodMap[timePeriod]?.value ?? + 0.0, // Retain the current value + value1: value, // Update the value1 + ); + } else { + timePeriodMap[timePeriod] = ChartData( + timePeriod: timePeriod, + value: 0.0, + value1: value, + ); + } + } + } + + // Convert the map to a list + chartDataList = timePeriodMap.values.toList(); + + return chartDataList; + } + Widget buildChart(dynamic chartData) { print('chartData $chartData'); switch (chartData['chart_type']) { - case 'line': + case 'area': return SfCartesianChart( - primaryXAxis: CategoryAxis( - title: AxisTitle(text: 'Year'), - labelRotation: 45, // Rotate labels if they overlap - ), - title: ChartTitle(text: 'Hotel Estabhlishments'), - legend: Legend(isVisible: true), - tooltipBehavior: TooltipBehavior( - enable: true, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - ), - child: Text( - '${data.timePeriod}: ${formatNumber(data.value)}', - style: const TextStyle(color: Colors.black), - ), - ); - }, - ), - series: >[ - LineSeries( - dataSource: parseLineChartData(chartData['response']), - xValueMapper: (ChartData data, _) => data.timePeriod, - yValueMapper: (ChartData data, _) => data.value, - name: chartData['dataset'], - color: Colors.red, - dataLabelSettings: DataLabelSettings( - isVisible: true, - builder: (dynamic data, dynamic point, dynamic series, - int pointIndex, int seriesIndex) { - return Text( - formatNumber(data.value), - style: const TextStyle(fontSize: 12, color: Colors.black), - ); - }, - ), + primaryXAxis: CategoryAxis( + title: AxisTitle(text: 'Year'), + labelRotation: 45, // Rotate labels if they overlap ), - ], - ); + title: ChartTitle(text: 'Hotel Estabhlishments'), + legend: Legend(isVisible: true), + tooltipBehavior: TooltipBehavior( + enable: true, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + ), + child: Text( + '${data.timePeriod}: ${formatNumber(data.value)}', + style: const TextStyle(color: Colors.black), + ), + ); + }, + ), + series: [ + // AreaSeries for the area chart + AreaSeries( + dataSource: parseLineChartData( + chartData['response']), // your chart data + color: Color(0xFF7DAFBC), // Area color + borderDrawMode: BorderDrawMode + .excludeBottom, // Optional, adjust border settings + borderColor: Colors.green, // Border color + borderWidth: 2, // Border width + xValueMapper: (ChartData data, _) => + data.timePeriod, // Map x to DateTime + yValueMapper: (ChartData data, _) => data.value, + // name: chartData['dataset'], + name: 'Hotels', + dataLabelSettings: DataLabelSettings( + isVisible: true, + builder: (dynamic data, dynamic point, dynamic series, + int pointIndex, int seriesIndex) { + return Text( + formatNumber(data.value), + style: const TextStyle(fontSize: 12, color: Colors.black), + ); + }, + ), // Map y to numerical value + ) + ]); case 'bar': return SfCartesianChart( primaryXAxis: CategoryAxis(), @@ -166,7 +243,7 @@ class ChartPageState extends State { xValueMapper: (ChartData data, _) => data.timePeriod, yValueMapper: (ChartData data, _) => data.value, name: chartData['dataset'], - color: Colors.red, + color: Color(0xFF7DAFBC), dataLabelSettings: DataLabelSettings( isVisible: true, builder: (dynamic data, dynamic point, dynamic series, @@ -182,6 +259,41 @@ class ChartPageState extends State { ), ], ); + case 'column': + return SfCartesianChart( + primaryXAxis: CategoryAxis( + title: AxisTitle(text: 'Year'), + labelRotation: 45, // Rotate labels if they overlap + ), + primaryYAxis: NumericAxis( + title: AxisTitle(text: 'No of rooms'), + ), + legend: Legend(isVisible: true), + title: ChartTitle(text: 'Hotel Occupancy Rate'), + enableSideBySideSeriesPlacement: true, + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + // First Series (value) + + ColumnSeries( + name: 'Available Rooms', + // opacity: 0.9, + // width: 0.4, + color: Color(0xFF587BA3), + dataSource: parseColumnChartData(chartData['response']), + xValueMapper: (ChartData data, _) => int.parse(data.timePeriod), + yValueMapper: (ChartData data, _) => data.value1, + ), + // Second Series (value1) + ColumnSeries( + color: Color(0xFF90B0D5), + name: 'Occupied Rooms', + dataSource: parseColumnChartData(chartData['response']), + xValueMapper: (ChartData data, _) => int.parse(data.timePeriod), + yValueMapper: (ChartData data, _) => data.value, + ), + ], // Numeric Y-axis to plot the values + ); default: return Center(child: Text('Unknown chart type')); } @@ -192,46 +304,84 @@ class ChartPageState extends State { return Scaffold( backgroundColor: const Color(0xFF7DAFBC), appBar: AppBar( - backgroundColor: Color(0xFF7DAFBC), + backgroundColor: const Color(0xFF7DAFBC), elevation: 0, leading: IconButton( - icon: Icon(Icons.arrow_back_ios_new, color: Colors.white), + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white), onPressed: () { context.go('/myhomepage'); }, ), - title: Text( + title: const Text( 'Charts', style: TextStyle(color: Colors.white), ), ), body: isLoading - ? Center(child: CircularProgressIndicator()) - : ListView.builder( - itemCount: chartsData.length, - itemBuilder: (context, index) { - return Card( - margin: EdgeInsets.all(10), + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + Card( + margin: const EdgeInsets.all(10), + color: Colors.white, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Text( - // 'Chart: ${chartsData[index]['chart_type']}', - // style: TextStyle( - // fontSize: 18, fontWeight: FontWeight.bold), - // ), - // SizedBox(height: 10), - Container( - height: 300, - child: buildChart(chartsData[index]), + children: const [ + Text( + 'Card 1 Title', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), ), + SizedBox(height: 10), + Text('Content for the first card goes here.'), ], ), ), - ); - }, + ), + Card( + margin: const EdgeInsets.all(10), + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + 'Card 2 Title', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 10), + Text('Content for the second card goes here.'), + ], + ), + ), + ), + Expanded( + child: ListView.builder( + itemCount: chartsData.length, + itemBuilder: (context, index) { + return Card( + margin: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 300, + child: buildChart(chartsData[index]), + ), + ], + ), + ), + ); + }, + ), + ), + ], ), ); } @@ -240,23 +390,35 @@ class ChartPageState extends State { class ChartData { final String timePeriod; final double value; + final double value1; final String formattedValue; - ChartData({required this.timePeriod, required this.value}) + ChartData( + {required this.timePeriod, required this.value, required this.value1}) : formattedValue = formatNumber(value); } +// String formatNumber(double value) { +// if (value >= 1e12) { +// return '${(value / 1e12).toStringAsFixed(2)} T'; // Trillions +// } else if (value >= 1e9) { +// return '${(value / 1e9).toStringAsFixed(2)} B'; // Billions +// } else if (value >= 1e6) { +// return '${(value / 1e6).toStringAsFixed(2)} M'; // Millions +// } else if (value >= 1e5) { +// return '${(value / 1e5).toStringAsFixed(2)} L'; // Lakhs +// } else if (value >= 1e3) { +// return '${(value / 1e3).toStringAsFixed(2)} K'; // Thousands +// } +// return value.toStringAsFixed(2); // Default to two decimal places +// } + String formatNumber(double value) { - if (value >= 1e12) { - return '${(value / 1e12).toStringAsFixed(2)} T'; // Trillions - } else if (value >= 1e9) { + if (value >= 1e9) { return '${(value / 1e9).toStringAsFixed(2)} B'; // Billions } else if (value >= 1e6) { return '${(value / 1e6).toStringAsFixed(2)} M'; // Millions - } else if (value >= 1e5) { - return '${(value / 1e5).toStringAsFixed(2)} L'; // Lakhs - } else if (value >= 1e3) { - return '${(value / 1e3).toStringAsFixed(2)} K'; // Thousands } - return value.toStringAsFixed(2); // Default to two decimal places + return value + .toStringAsFixed(2); // Default to two decimal places for smaller numbers } diff --git a/lib/presentation/Screens/demo_home2.dart b/lib/presentation/Screens/demo_home2.dart new file mode 100644 index 00000000..b1576aaa --- /dev/null +++ b/lib/presentation/Screens/demo_home2.dart @@ -0,0 +1,328 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; // Import Syncfusion charts package +import 'package:http/http.dart' as http; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: ChartPage2(dataSets: ''), + ); + } +} + +class ChartPage2 extends StatefulWidget { + final String dataSets; + const ChartPage2({Key? key, required this.dataSets}); + @override + ChartPageState createState() => ChartPageState(); +} + +class ChartPageState extends State { + List chartsData = []; + bool isLoading = true; + + @override + void initState() { + super.initState(); + fetchChartData(widget.dataSets); + } + + Future fetchChartData(dataSets) async { + const baseUrl = + 'https://pb.venbait.in/api/custom/apicall'; // Replace with your server URL + final url = Uri.parse('$baseUrl?dataset=$dataSets'); + + try { + final response = await http.get(url); + if (response.statusCode == 200) { + setState(() { + chartsData = jsonDecode(response.body); + isLoading = false; + }); + } else { + throw Exception('Failed to load data'); + } + } catch (error) { + setState(() { + isLoading = false; + }); + print('Error fetching data: $error'); + } + } + + List parseLineChartData(List response) { + return response.map((entry) { + // Attempt to fetch the TIMEPERIOD + final rawTimePeriod = + entry['ObsKey']?['Year'] ?? entry['ObsKey']?['TIME_PERIOD']; + final formattedTimePeriod = rawTimePeriod != null + ? rawTimePeriod.toString() // Use as-is if valid + : 'Unknown'; // Fallback value if null + + // Parse the value or fallback to 0.0 + final value = + double.tryParse(entry['ObsValue']?['Value']?.toString() ?? '0.0') ?? + 0.0; + + return ChartData( + timePeriod: formattedTimePeriod, + value: value, + ); + }).toList(); + } + + List parseBarChartData(List response) { + return response + .asMap() + .entries + .map((entry) => ChartData( + timePeriod: entry.value['ObsKey']['TIME_PERIOD'] ?? + 'Unknown', // Fallback to 'Unknown' if null + value: double.tryParse( + entry.value['ObsValue']['Value'].toString()) ?? + 0.0, // Handle null/invalid value + )) + .toList(); + } + +// Update parseColumnChartData for column chart + List parseColumnChartData(List response) { + return response.map((entry) { + // Extract TIME_PERIOD as x + final x = + int.tryParse(entry['ObsKey']?['TIME_PERIOD']?.toString() ?? '0') ?? 0; + // Extract Value as y + final y = + double.tryParse(entry['ObsValue']?['Value']?.toString() ?? '0.0') ?? + 0.0; + return ChartData(xInt: x, y: y); + }).toList(); + } + +// Update parseScatterChartData for scatter chart + List parseScatterChartData(List response) { + return response.map((entry) { + // Parse TIME_PERIOD into DateTime + final timePeriod = entry['ObsKey']?['TIME_PERIOD']; + final x = timePeriod != null + ? DateTime.tryParse('$timePeriod-01-01') // Convert year to DateTime + : DateTime.now(); // Fallback to current date if null + + // Parse Value into double + final y = + double.tryParse(entry['ObsValue']?['Value']?.toString() ?? '0.0') ?? + 0.0; + + return ChartData(xDateTime: x, y: y); + }).toList(); + } + + Widget buildChart(dynamic chartData) { + switch (chartData['chart_type']) { + case 'scatter': + return SfCartesianChart( + primaryXAxis: DateTimeAxis(), // Ensure it's DateTimeAxis + tooltipBehavior: TooltipBehavior(enable: true), + title: ChartTitle(text: 'Divorces'), + series: [ + ScatterSeries( + dataSource: parseScatterChartData(chartData['response']), + xValueMapper: (ChartData data, _) => + data.xDateTime ?? DateTime.now(), + yValueMapper: (ChartData data, _) => data.y, + name: chartData['dataset'], + dataLabelSettings: DataLabelSettings(isVisible: true), + ), + ], + ); + case 'column': + return SfCartesianChart( + primaryXAxis: NumericAxis( + title: AxisTitle(text: 'Year'), + interval: 1, // Ensure no fractional intervals + ), + title: ChartTitle(text: 'Marriages'), + series: >[ + ColumnSeries( + dataSource: parseColumnChartData(chartData['response']), + xValueMapper: (ChartData data, _) => data.xInt ?? 0, + yValueMapper: (ChartData data, _) => data.y, + name: chartData['dataset'], + color: Color(0xFF7DAFBC), + width: 0.8, + spacing: 0.2, + dataLabelSettings: + DataLabelSettings(isVisible: true), // Show data labels + ), + ], + ); + + case 'line': + return SfCartesianChart( + primaryXAxis: CategoryAxis(), + title: ChartTitle(text: 'Line Chart'), + legend: Legend(isVisible: true), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + LineSeries( + dataSource: parseLineChartData(chartData['response']), + xValueMapper: (ChartData data, _) => data.timePeriod ?? '', + yValueMapper: (ChartData data, _) => data.value ?? 0.0, + name: 'Sales', + dataLabelSettings: DataLabelSettings(isVisible: true), + ), + ], + ); + case 'bar': + return SfCartesianChart( + primaryXAxis: CategoryAxis(), + title: ChartTitle(text: 'Bar Chart'), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + BarSeries( + dataSource: parseBarChartData(chartData['response']), + xValueMapper: (ChartData data, _) => data.timePeriod ?? '', + yValueMapper: (ChartData data, _) => data.value ?? 0.0, + name: 'Gold', + color: Color.fromRGBO(8, 142, 255, 1), + ), + ], + ); + + default: + return Center(child: Text('Unknown chart type')); + } + } + + @override + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF7DAFBC), + appBar: AppBar( + backgroundColor: Color(0xFF7DAFBC), + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios_new, color: Colors.white), + onPressed: () { + context.go('/myhomepage'); + }, + ), + title: Text( + 'Charts', + style: TextStyle(color: Colors.white), + ), + ), + body: isLoading + ? Center(child: CircularProgressIndicator()) + : Column( + children: [ + // Row for two cards + Row( + children: [ + // First Card + Expanded( + flex: 6, + child: Card( + margin: EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Card 1', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 10), + Text('Content for Card 1 goes here.'), + ], + ), + ), + ), + ), + // Second Card + Expanded( + flex: 6, + child: Card( + margin: EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Card 2', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 10), + Text('Content for Card 2 goes here.'), + ], + ), + ), + ), + ), + ], + ), + // List of charts + Expanded( + child: ListView.builder( + itemCount: chartsData.length, + itemBuilder: (context, index) { + return Card( + margin: EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 10), + Container( + height: 300, + child: buildChart(chartsData[index]), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class ChartData { + final int? xInt; // For column and other charts, using int for x + final DateTime? xDateTime; // For scatter charts, using DateTime for x + final double y; // For column and other charts + final String? timePeriod; // For line and bar charts + final double? value; // For line and bar charts + final double? y2; // For secondary data (stacked column chart) + final double? y3; // For tertiary data (stacked column chart) + final double? y4; // For quaternary data (stacked column chart) + + // Constructor that handles all chart cases + ChartData({ + this.xInt, + this.xDateTime, + this.y = 0.0, + this.timePeriod, + this.value, + this.y2, + this.y3, + this.y4, + }); +} diff --git a/lib/presentation/routes/bottom_bar_routes/tab_routes/home_route.dart b/lib/presentation/routes/bottom_bar_routes/tab_routes/home_route.dart index cc4702c4..70ed8841 100644 --- a/lib/presentation/routes/bottom_bar_routes/tab_routes/home_route.dart +++ b/lib/presentation/routes/bottom_bar_routes/tab_routes/home_route.dart @@ -171,6 +171,20 @@ class MyHomePage extends StatefulWidget { State createState() => _MyHomePageState(); } +void handleInfoCardClick(BuildContext context, String data) { + // Handle navigation and pass dynamic data + print(data); + final dataSets = data; + print(dataSets); + if (dataSets == 'hotels') { + context.go('/Chart/$dataSets'); + } else if (dataSets == 'divorces') { + context.go('/Chart/$dataSets'); + } else if (dataSets == 'marriages') { + context.go('/Chart/$dataSets'); + } +} + class _MyHomePageState extends State { @override Widget build(BuildContext context) { @@ -226,6 +240,7 @@ class EconomyStats extends StatelessWidget { value: '1.62T', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => handleInfoCardClick(context, 'Inflation'), ), InfoCard( title: 'Inflation Rate', @@ -233,6 +248,7 @@ class EconomyStats extends StatelessWidget { value: '4.82%', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => handleInfoCardClick(context, 'Inflation'), ), ], ), @@ -247,6 +263,8 @@ class EconomyStats extends StatelessWidget { value: '215.4B', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => + handleInfoCardClick(context, 'commodities'), ), InfoCard( title: 'Hotel Guests', @@ -254,6 +272,7 @@ class EconomyStats extends StatelessWidget { value: '25.21M', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => handleInfoCardClick(context, 'hotels'), ), ], ), @@ -280,18 +299,22 @@ class EconomyStats extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ InfoCard( - title: 'Population', - subtitle: '(2022) (AED)', - value: '1.62T', + title: 'Divorces', + subtitle: '(2022)', + value: '0', bordercolor: Colors.brown[200]!, textcolor: Colors.brown[200]!, + onTap: () => + handleInfoCardClick(context, 'divorces'), ), InfoCard( - title: 'Labor Force', + title: 'Marriages', subtitle: '(2022)', - value: '4.82%', + value: '0', bordercolor: Colors.brown[200]!, textcolor: Colors.brown[200]!, + onTap: () => + handleInfoCardClick(context, 'marriages'), ), ], ), @@ -301,11 +324,13 @@ class EconomyStats extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ InfoCard( - title: 'Hospitals (Gov )', + title: 'Hospitals (Gov)', subtitle: '(Jan - Jan 2024) - AED', value: '215.4B', bordercolor: Colors.brown[200]!, textcolor: Colors.brown[200]!, + onTap: () => + handleInfoCardClick(context, 'Hospitals'), ), InfoCard( title: 'Schools', @@ -313,6 +338,8 @@ class EconomyStats extends StatelessWidget { value: '25.21M', bordercolor: Colors.brown[200]!, textcolor: Colors.brown[200]!, + onTap: () => + handleInfoCardClick(context, 'Schools'), ), ], ), @@ -345,6 +372,8 @@ class EconomyStats extends StatelessWidget { value: '1.62T', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => handleInfoCardClick( + context, 'Electricity'), ), InfoCard( title: 'Cruid oil', @@ -352,6 +381,8 @@ class EconomyStats extends StatelessWidget { value: '4.82%', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => + handleInfoCardClick(context, 'Cruid'), ), ], ), @@ -367,6 +398,8 @@ class EconomyStats extends StatelessWidget { value: '215.4B', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => + handleInfoCardClick(context, 'Export'), ), InfoCard( title: 'Desalinated water', @@ -374,6 +407,8 @@ class EconomyStats extends StatelessWidget { value: '25.21M', bordercolor: Color(0xFF7DAFBC), textcolor: Color(0xFF7DAFBC), + onTap: () => handleInfoCardClick( + context, 'Desalinated'), ), ], ), @@ -433,6 +468,7 @@ class InfoCard extends StatelessWidget { final String value; final Color bordercolor; final Color? textcolor; + final VoidCallback onTap; // New callback parameter const InfoCard({ Key? key, @@ -441,6 +477,7 @@ class InfoCard extends StatelessWidget { required this.value, required this.bordercolor, this.textcolor, + required this.onTap, // Initialize in constructor }) : super(key: key); @override @@ -450,9 +487,7 @@ class InfoCard extends StatelessWidget { return Expanded( child: GestureDetector( - onTap: () { - context.go('/DemoHome'); - }, + onTap: onTap, // Use callback for handling clicks child: Container( height: myheight / 9, width: mywidth / 4, diff --git a/pubspec.yaml b/pubspec.yaml index ebd8679b..315a6f95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: injectable: ^2.3.2 riverpod_annotation: ^2.3.3 cupertino_icons: ^1.0.6 - fl_chart: ^0.69.0 + fl_chart: ^0.69.2 marquee: ^2.2.3 # dynamic_layouts: # git: