import 'dart:convert'; import 'dart:math'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // import 'package:syncfusion_flutter_charts/charts.dart'; class ChartWidget extends StatelessWidget { ChartWidget({Key? key, required this.chartData, required this.bodyColor}) : super(key: key); final dynamic chartData; final Color bodyColor; String capitalizeAndSplit(String input) { return input .split('_') .map((word) => word[0].toUpperCase() + word.substring(1)) .join(' '); } String formatNumber(double value) { if (value >= 1e12) { return '${(value / 1e12).toStringAsFixed(2)}T'; } else if (value >= 1e9) { return '${(value / 1e9).toStringAsFixed(2)}B'; } else if (value >= 1e6) { return '${(value / 1e6).toStringAsFixed(2)}M'; } else if (value >= 1e3) { return '${(value / 1e3).toStringAsFixed(2)}K'; } else { return value.toStringAsFixed(2); // No decimals for small numbers } } String formatNumberConversion( double value, String chartConversion, String numberFormat,) { // Convert the input value to base unit (actual value) double baseValue = value; if (numberFormat == 'M') { baseValue = value * 1e6; // Convert from Million to base value } else if (numberFormat == 'B') { baseValue = value * 1e9; // Convert from Billion to base value } else if (numberFormat == 'T') { baseValue = value * 1e12; // Convert from Trillion to base value } else if (numberFormat == 'K') { baseValue = value * 1e3; // Convert from Thousand to base value } // Convert base value to the target chartConversion format if (chartConversion == 'T') { return '${(baseValue / 1e12).toStringAsFixed(2)}T'; // Convert to Trillion } else if (chartConversion == 'B') { return '${(baseValue / 1e9).toStringAsFixed(2)}B'; // Convert to Billion } else if (chartConversion == 'M') { return '${(baseValue / 1e6).toStringAsFixed(2)}M'; // Convert to Million } else if (chartConversion == 'K') { return '${(baseValue / 1e3).toStringAsFixed(2)}K'; // Convert to Thousand } else { return baseValue .toStringAsFixed(2); // Default: Show value with 2 decimals } } List parseStackedBarChartData( dynamic chartData, String groupByKey, String groupByValue) { return chartData['response'] .where((entry) => entry['ObsKey'][groupByKey] == groupByValue) .map((entry) { return ChartData( x: entry['ObsKey']['TIME_PERIOD'], y: double.tryParse(entry['ObsValue']['Value']) ?? 0.0, ); }).toList(); } List parseColumnChartData(dynamic chartData) { return chartData['response'].map((entry) { return BarChartGroupData( x: int.tryParse(entry['ObsKey']['TIME_PERIOD']) ?? 0, barRods: [ BarChartRodData( toY: double.tryParse(entry['ObsValue']['Value']) ?? 0.0, color: Colors.blue, width: 16, borderRadius: BorderRadius.zero, ), ], ); }).toList(); } Color adjustColor(Color baseColor, int index) { final HSLColor hsl = HSLColor.fromColor(baseColor); // Lightness variation (keeping it light, between 0.6 and 0.9) double lightnessFactor = 0.6 + (index % 4) * 0.1; // Varies between 0.6 to 0.8 final HSLColor adjustedHSL = hsl.withLightness( lightnessFactor.clamp(0.6, 0.9), // Ensures light shades only ); return adjustedHSL.toColor(); } List parsePieChartData( dynamic chartData, double totalValue, int? touchedIndex, ) { debugPrint('Chart Data: ${jsonEncode(chartData)}'); return chartData['response'] .asMap() .entries .map((entry) { int index = entry.key; var data = entry.value; // Handle different types: int, double, and String double value = 0.0; var rawValue = data['ObsValue']['Value']; if (rawValue is String) { value = double.tryParse(rawValue) ?? 0.0; } else if (rawValue is num) { value = rawValue.toDouble(); } else if (rawValue is double) { value = rawValue; } else { print('Unexpected Type for ObsValue[Value]: ${rawValue.runtimeType}'); } // double percentage = (value / totalValue) * 100; double percentage = (totalValue > 0) ? (value / totalValue) * 100 : 0.0; bool isTouched = index == touchedIndex; return PieChartSectionData( value: value, // color: Colors.primaries[index % Colors.primaries.length], color: adjustColor(bodyColor, index), // Adjust alpha dynamically title: '${percentage.toStringAsFixed(1)}%', radius: isTouched ? 60 : 50, // Increase size when touched titleStyle: TextStyle( fontSize: isTouched ? 12 : 10, fontWeight: FontWeight.bold, color: Colors.black, // color: Colors.white, ), titlePositionPercentageOffset: 1.3, ); }).toList(); } List generateIndicators(dynamic chartData, String groupByValue) { return chartData['response'].asMap().entries.map((entry) { int index = entry.key; var data = entry.value; Color color = Colors.primaries[index % Colors.primaries.length]; String title = data['ObsKey'][groupByValue] ?? ''; String shortTitle = title.length > 10 ? '${title.substring(0, 10)}…' : title; return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration( color: adjustColor(bodyColor, index), shape: BoxShape.circle, ), ), SizedBox(width: 8), TooltipTheme( data: TooltipThemeData( decoration: BoxDecoration( color: Colors.blueGrey[800], // Change background color borderRadius: BorderRadius.circular(8), // Optional: rounded corners ), textStyle: TextStyle(color: Colors.white), // Change text color ), child: Tooltip( message: title, // Full text on hover child: ConstrainedBox( // Constrain width to allow wrapping constraints: BoxConstraints(maxWidth: 200), child: Text( title, style: TextStyle( fontSize: 12, ), // softWrap: true, // maxLines: 2, overflow: TextOverflow.ellipsis, // Ensures text doesn't overflow ), ), ), ), SizedBox(width: 5), ], ); }).toList(); } Set extractGroupByValues(dynamic chartData, String groupByKey) { return (chartData['response'] as List).map((entry) { var value = entry['ObsKey'][groupByKey]; return value != null ? value.toString() : ''; // Handle null and ensure conversion to String }).toSet(); } Widget buildChart(dynamic chartData, BuildContext context) { // print('bodyColor- $bodyColor'); print('chartDataccccccc $chartData'); // Extract group_by dynamically from the chartData if (chartData == null || chartData['response'] == null) { return Center(child: Text('No chart data available')); } print(chartData['chart_type_json']['TIME_PERIOD']); String groupByKey = chartData['group_by'] ?? ''; print('groupByKey $groupByKey'); Set groupByValues = extractGroupByValues(chartData, groupByKey); print('groupByValues $groupByValues'); switch (chartData['chart_type']) { // 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, // ), // ], // ); case 'column_chart': // Case 3 for fl_chart column chart return BarChart( BarChartData( gridData: FlGridData(show: false), titlesData: FlTitlesData(show: true), borderData: FlBorderData(show: false), barGroups: parseColumnChartData(chartData), ), ); case 'pie_chart': double totalValue = chartData['response'].map((entry) { var value = entry['ObsValue']['Value']; // print('Processing value: $value, type: ${value.runtimeType}'); return (value is num) ? value.toDouble() : value is String ? double.tryParse(value) ?? 0.0 : 0.0; }).fold(0.0, (prev, element) => prev + element); ValueNotifier touchedIndex = ValueNotifier(null); return Column( children: [ Text( chartData['chart_heading'] ?? 'Pie Chart', // Chart title from data style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 15), Text( chartData['chart_sub_heading'] ?? '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 70), Expanded( child: ValueListenableBuilder( valueListenable: touchedIndex, builder: (context, value, child) { return PieChart( PieChartData( sections: parsePieChartData( chartData, totalValue, touchedIndex.value), borderData: FlBorderData(show: false), sectionsSpace: 2, centerSpaceRadius: 50, pieTouchData: PieTouchData( touchCallback: (FlTouchEvent event, pieTouchResponse) { if (pieTouchResponse?.touchedSection != null && event is! FlTapUpEvent) { touchedIndex.value = pieTouchResponse! .touchedSection!.touchedSectionIndex; } else { touchedIndex.value = null; // Reset when not touching } }, ), ), ); }, ), ), SizedBox(height: 45), Expanded( // child: SingleChildScrollView( // scrollDirection: Axis.horizontal, // // child: SingleChildScrollView( // scrollDirection: Axis.vertical, child: Center( child: Container( width: double.infinity, constraints: BoxConstraints(minHeight: 3), // Allow dynamic height child: Padding( padding: const EdgeInsets.only( left: 2.0, right: 2.0, bottom: 8.0, top: 1.0), child: Column( // Change Row to Column crossAxisAlignment: CrossAxisAlignment.start, children: generateIndicators(chartData, chartData['group_by']), ), // child: Wrap( // spacing: 2, // runSpacing: 5, // children: // generateIndicators(chartData, chartData['group_by']), // ), ), ), ), ), // ), ], ); case 'fl_stacked_bar': // Divide groupByValuesList into chunks of 5 items per row List groupByValuesList = groupByValues.toList(); int chunkSize = 5; List> chunkedGroupByValues = []; print('flStackedBar1'); for (int i = 0; i < groupByValuesList.length; i += chunkSize) { chunkedGroupByValues.add( groupByValuesList.sublist( i, i + chunkSize > groupByValuesList.length ? groupByValuesList.length : i + chunkSize), ); } return Column( children: [ Text( chartData['chart_heading'] ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 5), Text( chartData['chart_sub_heading'] ?? '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 15), AspectRatio( aspectRatio: 1.5, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceEvenly, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( // tooltipBgColor: Colors.black.withOpacity(0.8), fitInsideHorizontally: true, fitInsideVertically: true, // tooltipPadding: const EdgeInsets.all(8), tooltipPadding: const EdgeInsets.symmetric( horizontal: 4, vertical: 8), // Optional tooltipHorizontalAlignment: FLHorizontalAlignment.left, tooltipMargin: 16, getTooltipItem: (groupData, groupIndex, rodData, rodIndex) { // Get the list of group names dynamically List groupNames = []; for (var entry in chartData['response']) { String groupValue = entry['ObsKey'][groupByKey]; if (!groupNames.contains(groupValue)) { groupNames.add(groupValue); } } // Accessing the stacked rod items and calculating the tooltip text List rodStackItems = rodData.rodStackItems; List tooltipTextSpans = []; for (int i = 0; i < rodStackItems.length; i++) { double fromY = rodStackItems[i].fromY; double toY = rodStackItems[i].toY; double value = toY - fromY; String groupName = groupNames[i]; String formattedValue; if (value >= 1000000) { formattedValue = (value / 1000000).toStringAsFixed(1) + 'M'; } else if (value >= 1000) { formattedValue = (value / 1000).toStringAsFixed(1) + 'K'; } else { formattedValue = value.toStringAsFixed( 0); // for values smaller than 1000 } // Get the color for the current group Color groupColor = _getColorForGroup( i); // Replace with your color logic // Add a TextSpan for the colored circle and the group name with value tooltipTextSpans.addAll([ TextSpan( text: ' ● ', style: TextStyle( color: groupColor, fontSize: 14), // Circle color ), TextSpan( text: '$groupName - $formattedValue\n', style: TextStyle(color: Colors.white, fontSize: 12), ), ]); } return BarTooltipItem( '', TextStyle(color: Colors.white, fontSize: 12), children: tooltipTextSpans, textAlign: TextAlign.left, ); }, ), handleBuiltInTouches: true, // Enable built-in touch events ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 28, getTitlesWidget: (double value, TitleMeta meta) { return _generateXTitles(chartData)[value.toInt()]; }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: _generateLeftTitles, ), ), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), gridData: FlGridData(show: false), borderData: FlBorderData(show: false), barGroups: _generateBarGroups(context, chartData, groupByKey), ), ), ), // Legend Padding( padding: const EdgeInsets.all(5.0), child: Wrap( spacing: 10, // Horizontal spacing between items runSpacing: 5, // Vertical spacing between rows children: chunkedGroupByValues.expand((chunk) { return chunk.map((group) { List groupByValuesList = groupByValues.toList(); int index = groupByValuesList.indexOf(group); Color groupColor = _getColorForGroup(index); return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration( color: groupColor, shape: BoxShape.circle, ), ), SizedBox(width: 5), Tooltip( message: group, waitDuration: Duration(milliseconds: 500), showDuration: Duration(seconds: 2), decoration: BoxDecoration( color: Colors.blueGrey[900], borderRadius: BorderRadius.circular(4), ), textStyle: TextStyle(color: Colors.white), child: Text( group, // group.length > 10 // ? '${group.substring(0, 10)}...' // : group, // overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11), ), ) ], ); }).toList(); }).toList(), ), ) ], ); case 'line_trend': final List uniqueColorsLine_trend_2 = [ Color(0xFF6097CD), Color(0xFFD086A7), Color(0xFF98BCE5), Color(0xFFA7B5C5), Color(0xFFBED3EC), Color(0xFFD4E3F4), ]; Map groupColorMap = {}; int colorIndex = 0; for (String group in groupByValues) { groupColorMap[group] = uniqueColorsLine_trend_2[ colorIndex % uniqueColorsLine_trend_2.length]; colorIndex++; } print('LnTrnd1'); // Extract all years from the chart data List years = (chartData['response'] as List) .map((entry) => int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0) .toList(); if (years.isEmpty) { // Return an empty chart if no data return LineChart( LineChartData( titlesData: FlTitlesData(show: false), lineBarsData: [], ), ); } print('LnTrnd1.2'); // Find the maximum year and calculate the range for the last 5 years int maxYear = years.reduce((a, b) => a > b ? a : b); int minYear = maxYear - 5; // Filter chart data to only include entries within the last 5 years List filteredData = (chartData['response'] as List).where((entry) { int year = int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0; return year >= minYear && year <= maxYear; }).toList(); print('LnTrnd2'); // Set uniqueXValues = filteredData // .map( // (entry) => double.parse(entry['ObsKey']['TIME_PERIOD'])) // .toSet(); Set uniqueXValues = filteredData.map((entry) { String? timePeriod = entry['ObsKey']['TIME_PERIOD']; // Nullable String print('TIME_PERIOD- $timePeriod'); if (timePeriod == null) { print('TIME_PERIOD is null'); return ''; // Or handle the null case appropriately } // Check format using regex if (RegExp(r'^\d{4}$').hasMatch(timePeriod)) { print('Year format detected: $timePeriod'); return timePeriod; // Year only } else if (RegExp(r'^\d{4}-\d{2}$').hasMatch(timePeriod)) { print('Year-Month format detected: $timePeriod'); return timePeriod; // Year-Month } else if (RegExp(r'^\d{4}-[A-Za-z]{3}$').hasMatch(timePeriod)) { print('Year-MonthAbbr format detected: $timePeriod'); return timePeriod; // Year-MonthAbbr } else { print('Unknown format: $timePeriod'); return timePeriod; // Keep it as is } }).toSet(); print("Unique X Values: $uniqueXValues"); double parseTimePeriod(String timePeriod) { // Match formats if (RegExp(r'^\d{4}$').hasMatch(timePeriod)) { return double.parse(timePeriod); // Year-only (2019 → 2019.0) } else if (RegExp(r'^\d{4}-\d{2}$').hasMatch(timePeriod)) { return double.parse(timePeriod.replaceAll( '-', '.')); // Year-Month (2019-11 → 2019.11) } else if (RegExp(r'^\d{4}-[A-Za-z]{3}$').hasMatch(timePeriod)) { // Year-MonthAbbr (2019-Nov) Map monthMap = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12 }; List parts = timePeriod.split('-'); int month = monthMap[parts[1]] ?? 1; // Default to January if unknown return double.parse( '${parts[0]}.$month'); // Convert to format (2019-Nov → 2019.11) } throw FormatException("Invalid TIME_PERIOD format: $timePeriod"); } Set uniqueXValuesProcessed = uniqueXValues .where((value) => value.isNotEmpty) // Remove any empty strings .map(parseTimePeriod) .toSet(); print('Processed X Values: $uniqueXValuesProcessed'); print('LnTrnd2.1'); // Generate line bars for the chart List lineBars = lineBarsData(filteredData, groupByValues, groupByKey); print('LnTrnd3'); return Column(children: [ Text( chartData['chart_heading'] ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 5), Text( chartData['chart_sub_heading'] ?? '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 10), // Chart Expanded( child: LineChart(LineChartData( lineTouchData: lineTouchData1(), gridData: gridData(), // titlesData: titlesData1(uniqueXValues), titlesData: titlesData1(uniqueXValuesProcessed), borderData: borderData(), lineBarsData: lineBars, // minX: uniqueXValues.reduce((a, b) => a < b ? a : b), // maxX: uniqueXValues.reduce((a, b) => a > b ? a : b), // minX: uniqueXValuesProcessed.reduce((a, b) => a < b ? a : b), // maxX: uniqueXValuesProcessed.reduce((a, b) => a > b ? a : b), ))), Padding( padding: const EdgeInsets.all(8.0), child: Wrap( spacing: 12, runSpacing: 8, children: groupByValues.map((group) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, color: groupColorMap[group], ), SizedBox(width: 6), Text( group, style: TextStyle(fontSize: 14), ), ], ); }).toList(), ), ), ]); case 'line_trend_population': final List uniqueColorsLine_trend_2 = [ Color(0xFF6097CD), Color(0xFFD086A7), Color(0xFF98BCE5), Color(0xFFA7B5C5), Color(0xFFBED3EC), Color(0xFFD4E3F4), ]; Set selectedYears = {1970, 1980, 1990, 2000, 2010, 2020}; Map groupColorMap = {}; int colorIndex = 0; for (String group in groupByValues) { groupColorMap[group] = uniqueColorsLine_trend_2[ colorIndex % uniqueColorsLine_trend_2.length]; colorIndex++; } // Extract all years from the chart data List years = (chartData['response'] as List) .map((entry) => int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0) .toList(); if (years.isEmpty) { // Return an empty chart if no data return LineChart( LineChartData( titlesData: FlTitlesData(show: false), lineBarsData: [], ), ); } if (years.isNotEmpty) { // Find the latest available year int latestYear = years.reduce((a, b) => a > b ? a : b); selectedYears.add(latestYear); // Include the latest year in selection } // Filter chart data to include only the selected years List filteredData = (chartData['response'] as List).where((entry) { int year = int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0; return selectedYears.contains(year); }).toList(); Set uniqueXValues = filteredData .map( (entry) => double.parse(entry['ObsKey']['TIME_PERIOD'])) .toSet(); // Generate line bars for the chart List lineBars = lineBarsData(filteredData, groupByValues, groupByKey); return Column(children: [ Text( chartData['chart_heading'] ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 5), Text( chartData['chart_sub_heading'] ?? '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 10), Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, // Enable horizontal scrolling padding: const EdgeInsets.only(right: 40), child: SizedBox( width: (uniqueXValues.length * 50) + 50, // Adjust width dynamically child: LineChart(LineChartData( lineTouchData: lineTouchData1(), gridData: gridData(), titlesData: titlesData1(uniqueXValues), borderData: borderData(), lineBarsData: lineBars, minX: uniqueXValues.reduce((a, b) => a < b ? a : b), maxX: uniqueXValues.reduce((a, b) => a > b ? a : b), )), ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: Wrap( spacing: 12, runSpacing: 8, children: groupByValues.map((group) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, color: groupColorMap[group], ), SizedBox(width: 6), Text( group, style: TextStyle(fontSize: 14), ), ], ); }).toList(), ), ) // Chart ]); case 'line_trend_2': final List uniqueColorsLine_trend_2 = [ Color(0xFF6097CD), Color(0xFFD086A7), Color(0xFF98BCE5), Color(0xFFA7B5C5), Color(0xFFBED3EC), Color(0xFFD4E3F4), ]; Set selectedYears = {1970, 1980, 1990, 2000, 2010, 2020}; Map groupColorMap = {}; int colorIndex = 0; for (String group in groupByValues) { groupColorMap[group] = uniqueColorsLine_trend_2[ colorIndex % uniqueColorsLine_trend_2.length]; colorIndex++; } // Extract all years from the chart data List years = (chartData['response'] as List) .map((entry) => int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0) .toList(); if (years.isEmpty) { // Return an empty chart if no data return LineChart( LineChartData( titlesData: FlTitlesData(show: false), lineBarsData: [], ), ); } if (years.isNotEmpty) { // Find the latest available year int latestYear = years.reduce((a, b) => a > b ? a : b); selectedYears.add(latestYear); // Include the latest year in selection } // Filter chart data to include only the selected years List filteredData = (chartData['response'] as List).where((entry) { int year = int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0; return selectedYears.contains(year); }).toList(); Set uniqueXValues = filteredData .map( (entry) => double.parse(entry['ObsKey']['TIME_PERIOD'])) .toSet(); print('uniqueXValuesLineTrend2- $uniqueXValues'); // Generate line bars for the chart List lineBars = lineBarsData2(filteredData); return Column(children: [ Text( chartData['chart_heading'] ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 5), Text( '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 10), // Chart Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, // Enable horizontal scrolling padding: const EdgeInsets.only(right: 40, top: 20), child: SizedBox( width: (uniqueXValues.length * 50) + 50, // Adjust width dynamically child: LineChart( LineChartData( lineTouchData: lineTouchData1(), gridData: gridData(), titlesData: titlesData1(uniqueXValues), borderData: borderData(), lineBarsData: lineBars, minX: uniqueXValues.reduce((a, b) => a < b ? a : b), maxX: uniqueXValues.reduce((a, b) => a > b ? a : b), minY: 0, ), ), ), ), ), ]); case 'bar_chart': // Extract groupBy values and their corresponding y-axis values List xAxisData = []; List yAxisData = []; List yAxisLabels = []; var xadditionalgrp = chartData['chart_type_json']['additional_x_group']; var chartConversion = chartData['chart_type_json']['conversion']; var number_format = chartData['chart_type_json']['number_format']; print('xadditionalgrp-$xadditionalgrp'); for (var entry in chartData['response']) { var xValue = entry['ObsKey'][groupByKey]; var xTimePeriod = entry['ObsKey']['TIME_PERIOD']; var unitMsr = entry['ObsKey']['UNIT_MEASURE']; var yValue = entry['ObsValue']['Value']; var xadditionalgroup = chartData['chart_type_json']['additional_x_group']; print('chartData response: ${chartData['response']}'); print('chartConversion: $chartConversion'); print('number_format: $number_format'); print('xValue-$xValue'); print('Xadditionalt-$xTimePeriod'); print('xadditionalgroup-$xadditionalgroup'); print('yValue-$yValue'); print('unitMsr-$unitMsr'); if (xadditionalgrp == 'TIME_PERIOD') { chartData['response'].sort((a, b) { // Convert TIME_PERIOD to int for proper sorting int timeA = int.tryParse(a['ObsKey']['TIME_PERIOD'].toString()) ?? 0; int timeB = int.tryParse(b['ObsKey']['TIME_PERIOD'].toString()) ?? 0; // Extract QUARTER and convert "Q1", "Q2", etc. to numeric values int quarterA = int.tryParse( a['ObsKey']['QUARTER'].toString().replaceAll('Q', '')) ?? 0; int quarterB = int.tryParse( b['ObsKey']['QUARTER'].toString().replaceAll('Q', '')) ?? 0; // First, sort by TIME_PERIOD. If equal, sort by QUARTER if (timeA != timeB) { return timeA.compareTo(timeB); } else { return quarterA.compareTo(quarterB); } }); } // if (xValue != null && yValue != null) { // xAxisData.add(xValue.toString()); // yAxisData.add(double.tryParse(yValue.toString()) ?? 0.0); // } if (xValue != null && yValue != null) { // Check if additional_x_group is "Timeperiod" and concatenate String xLabel = xValue.toString(); if (xadditionalgroup == 'TIME_PERIOD' && xTimePeriod != null) { xLabel = '$xTimePeriod-$xValue'; // Format: Label (TimePeriod) } xAxisData.add(xLabel); yAxisData.add(double.tryParse(yValue.toString()) ?? 0.0); // yAxisLabels.add(unitMsr ?? ''); } } return Column(children: [ Text( chartData['chart_heading'] ?? '', // Chart title from data style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 5), Text( chartData['chart_sub_heading'] ?? '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 10), // Chart Expanded( child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: yAxisData.isNotEmpty ? yAxisData.reduce((a, b) => a > b ? a : b) * 1.2 : 10, // barTouchData: BarTouchData( // enabled: true, // touchCallback: (FlTouchEvent event, barTouchResponse) { // if (!event.isInterestedForInteractions || barTouchResponse == null || barTouchResponse.spot == null) { // return; // } // }, // touchTooltipData: BarTouchTooltipData( // getTooltipColor: (group) => Colors.transparent, // getTooltipItem: (group, groupIndex, rod, rodIndex) { // if (rod.toY == 0) return null; // Hide for zero values // return BarTooltipItem( // formatNumber(rod.toY), // TextStyle(color: Colors.black), // ); // }, // ), // ), // barTouchData: BarTouchData(enabled: true), barTouchData: BarTouchData( enabled: false, handleBuiltInTouches: false, touchTooltipData: BarTouchTooltipData( getTooltipColor: (group) => Colors.transparent, // fitInsideHorizontally: true, // fitInsideVertically: true, // tooltipPadding: const EdgeInsets.all(8), tooltipMargin: 1, getTooltipItem: (group, groupIndex, rod, rodIndex) { // String unitMsrLabel = yAxisLabels.isNotEmpty && groupIndex < yAxisLabels.length // ? yAxisLabels[groupIndex] // : ''; if (rod.toY == 0) return null; return BarTooltipItem( (chartConversion != null && chartConversion.isNotEmpty && number_format != null && chartConversion.isNotEmpty) ? formatNumberConversion(rod.toY, chartConversion, number_format) // If condition is true : formatNumber(rod.toY), const TextStyle( color: Colors.black, fontSize: 12, fontWeight: FontWeight.w400, ), ); }, ), ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: false, interval: (yAxisData.isNotEmpty ? yAxisData.reduce((a, b) => a > b ? a : b) / 5 : 1), getTitlesWidget: (value, meta) { return Padding( padding: const EdgeInsets.only(right: 8.0), child: Text('${value.toInt()}'), ); }, reservedSize: 60, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 1, getTitlesWidget: (value, meta) { if (value.toInt() < xAxisData.length) { String title = xAxisData[value.toInt()]; String displayTitle = title.length > 10 ? title.substring(0, 10) + '...' : title; return Padding( padding: const EdgeInsets.only(top: 8.0, left: 55.0), child: SizedBox( width: 60, // Limit width to force wrapping child: Transform.rotate( // angle: -0.5, angle: -1.5, child: TooltipTheme( data: TooltipThemeData( decoration: BoxDecoration( color: Colors.blueGrey[ 800], // Change background color borderRadius: BorderRadius.circular( 8), // Optional: rounded corners ), textStyle: TextStyle( color: Colors .white), // Change text color ), child: TooltipTheme( data: TooltipThemeData( decoration: BoxDecoration( color: Colors.blueGrey[ 800], // Change background color borderRadius: BorderRadius.circular( 8), // Optional: rounded corners ), textStyle: TextStyle( color: Colors .white), // Change text color ), child: Tooltip( message: title, child: Text( // title, displayTitle, softWrap: true, textAlign: TextAlign.end, style: TextStyle( fontSize: 10, ), overflow: TextOverflow.ellipsis, ), ), ), ), ))); } return Container(); }, reservedSize: 110, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide top titles ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide right titles ), ), gridData: FlGridData(show: false), borderData: FlBorderData( show: true, border: const Border( // left: BorderSide(color: Colors.grey), bottom: BorderSide(color: Colors.grey), ), ), barGroups: List.generate( xAxisData.length, (index) => BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: yAxisData[index], color: bodyColor, borderRadius: BorderRadius.circular(4), width: 20, ), ], showingTooltipIndicators: [0], ), ), ), )) ]); case 'bar_chart_horizontal': // Extract groupBy values and their corresponding y-axis values List xAxisData = []; List yAxisData = []; List> sortedData = []; for (var entry in chartData['response']) { String? timePeriod = entry['ObsKey']['TIME_PERIOD']; if (timePeriod != null) { // Check if it contains a month (i.e., has "-" and follows "YYYY-MM" format) bool hasMonth = RegExp(r'^\d{4}-\d{2}$').hasMatch(timePeriod); if (hasMonth) { print('Yes-TIME_PERIOD: $timePeriod'); sortedData.add(entry); } else { print('No-TIME_PERIOD: $timePeriod'); } } } // Sort the data based on year and month sortedData.sort((a, b) { String timeA = a['ObsKey']['TIME_PERIOD']; String timeB = b['ObsKey']['TIME_PERIOD']; return timeA.compareTo(timeB); // Lexicographical sorting works for YYYY-MM }); // If no valid TIME_PERIOD with a month is found, return original response List> finalData = sortedData.isNotEmpty ? sortedData : List.from(chartData['response']); for (var entry in finalData) { var xValue = entry['ObsKey'][groupByKey]; var yValue = entry['ObsValue']['Value']; if (xValue != null && yValue != null) { xAxisData.add(xValue.toString()); yAxisData.add(double.tryParse(yValue.toString()) ?? 0.0); } } print("chartData['chart_heading']"); print('RESpns- ${chartData['chart_heading']}'); print(chartData['response']); int rotationTurns = 1; return Column(children: [ Text( chartData['chart_heading'] ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), // SizedBox(height: 5), // Text( // chartData['chart_sub_heading'] ?? '', // style: TextStyle( // fontSize: 13, // fontWeight: FontWeight.w300, // ), // textAlign: TextAlign.center, // ), // SizedBox(height: 5), // Chart Flexible( child: BarChart( BarChartData( // alignment: BarChartAlignment.spaceAround, maxY: yAxisData.isNotEmpty ? yAxisData.reduce((a, b) => a > b ? a : b) * 1.2 : 10, rotationQuarterTurns: rotationTurns, barTouchData: BarTouchData( enabled: false, handleBuiltInTouches: false, touchTooltipData: BarTouchTooltipData( fitInsideHorizontally: true, fitInsideVertically: true, tooltipPadding: const EdgeInsets.all(8), tooltipMargin: 16, getTooltipColor: (group) => Colors.transparent, getTooltipItem: (group, groupIndex, rod, rodIndex) { if (rod.toY == 0) return null; return BarTooltipItem( formatNumber(rod.toY), const TextStyle( color: Colors.black, fontSize: 12, fontWeight: FontWeight.w400, ), ); }, ), ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: false, interval: (yAxisData.isNotEmpty ? yAxisData.reduce((a, b) => a > b ? a : b) / 5 : 1), getTitlesWidget: (value, meta) { return Padding( padding: const EdgeInsets.only(right: 8.0), child: Text('${value.toInt()}'), ); }, reservedSize: 60, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { if (value.toInt() < xAxisData.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Transform.rotate( angle: -1.58, // angle: -45 * // (3.1415927 / 180), // Rotating by -45 degrees alignment: Alignment.center, child: Center( child: SizedBox( width: 100, child: Text( xAxisData[value.toInt()], textAlign: TextAlign.right, style: const TextStyle( fontSize: 12, ), softWrap: true, maxLines: 3, ), ), ), ), ); } return Container(); }, reservedSize: 125, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide top titles ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide right titles ), ), gridData: FlGridData(show: false), // borderData: FlBorderData(show: false), borderData: FlBorderData( show: false, ), // groupsSpace: 5, barGroups: List.generate( xAxisData.length, (index) => BarChartGroupData( x: index, // barsSpace: 2, barRods: [ BarChartRodData( toY: yAxisData[index], // color: Colors.blueAccent, color: bodyColor, borderRadius: BorderRadius.circular(4), width: 20, ), ], showingTooltipIndicators: [0], ), ), ), ), ), ]); case 'fl_multi_bar': double _calculateChartWidth(dynamic chartData) { int totalBars = chartData['response']?.length ?? 0; const double barWidth = 30; // Width for each bar, including spacing return totalBars * barWidth; // Calculate total chart width } print('multi_bar'); String cropKey = chartData['chart_type_json']['x_sub_group']; // Group crops by CROP_TYPE Map> groupedCrops = {}; int touchedGroupIndex = -1; // Iterate over the chartData to group crops by CROP_TYPE for (var item in chartData['response']) { print("MultiBArRaw item: $item"); String crop = item['ObsKey'][cropKey]; String cropType = item['ObsKey'][groupByKey]; // Use a Set to avoid duplicates if (groupedCrops.containsKey(cropType)) { groupedCrops[cropType] = (groupedCrops[cropType]! + [crop]).toSet().toList(); } else { groupedCrops[cropType] = [crop]; } } print('Unique groupedCrops: $groupedCrops'); // for (var item in chartData['response']) { // String crop, cropType; // crop = item['ObsKey'][cropKey]; // cropType = item['ObsKey'][groupByKey]; // // if (groupedCrops.containsKey(cropType)) { // groupedCrops[cropType]!.add(crop); // } else { // groupedCrops[cropType] = [crop]; // } // } // // print('groupedCropsflmutli- $groupedCrops'); print('hormulti_bar'); return Center( child: Column(children: [ Text( chartData['chart_heading'] ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 5), Text( '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 10), // Chart Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, // Enable horizontal scrolling child: SizedBox( width: _calculateChartWidth( chartData), // Dynamically calculate the chart width child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: _calculateMaxY( chartData), // Dynamically calculate max Y barGroups: _buildHorizontalRotateBarGroups( chartData, groupByValues), // Build bar groups titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 80, // Added space for rotated titles getTitlesWidget: (value, meta) { if (value < groupByValues.length) { String title = groupByValues.elementAt(value.toInt()); String displayTitle = title.length > 10 ? title.substring(0, 10) + '...' : title; return Padding( padding: const EdgeInsets.only(top: 8.0,left: 30.0), child: SizedBox( width: 60, // Limit width to force wrapping child: Transform.rotate( angle: -1.5, child: TooltipTheme( data: TooltipThemeData( decoration: BoxDecoration( color: Colors.blueGrey[ 800], // Change background color borderRadius: BorderRadius.circular( 8), // Optional: rounded corners ), textStyle: TextStyle( color: Colors.white), // Change text color ), child: Tooltip( message: title, child: Text( displayTitle, softWrap: true, maxLines:2 , style: TextStyle(fontSize: 10), overflow: TextOverflow.ellipsis, )), ), ))); // return Transform.rotate( // angle: // -0.5, // Rotation in radians (~ -30 degrees) // child: Text( // title, // style: const TextStyle(fontSize: 12), // ), // ); } return const SizedBox.shrink(); }, ), ), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: false), barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( tooltipHorizontalAlignment: FLHorizontalAlignment.center, tooltipRoundedRadius: 8, fitInsideHorizontally: true, // Ensure it fits within the screen fitInsideVertically: true, tooltipPadding: EdgeInsets.all(8), tooltipMargin: 16, // Only show tooltip when touched getTooltipItem: (group, groupIndex, rod, rodIndex) { if (rod.toY == 0 || touchedGroupIndex == -1) { return null; // Don't show the tooltip if the value is 0 or there's no touch } if (groupIndex == touchedGroupIndex) { // Get the group label dynamically String groupLabel = groupByValues.elementAt(groupIndex); // Fetch the crop for the current group from groupedCrops String cropType = groupByValues.elementAt(groupIndex); print('GrpcropType: $cropType'); // String crop = groupedCrops[cropType]![rodIndex]; // Safely fetch crop with null check List? crops = groupedCrops[cropType]; String crop; if (crops != null && rodIndex < crops.length) { crop = crops[rodIndex]; } else { print( 'Crop is null or index out of bounds for cropType: $cropType and rodIndex: $rodIndex'); // crop = cropType; // Fallback to cropType crop = (crops != null && crops.isNotEmpty) ? crops.first : cropType; } // print('groupedCrops1: $groupedCrops - $rodIndex'); // print( // 'Groupcrop: $crop'); double value = rod.toY; String formattedValue; if (value >= 1000000) { formattedValue = (value / 1000000).toStringAsFixed(1) + 'M'; } else if (value >= 1000) { formattedValue = (value / 1000).toStringAsFixed(1) + 'K'; } else { formattedValue = value.toStringAsFixed( 0); // for values smaller than 1000 } // print('Cropvaluevalue: $groupLabel\n$crop $value $formattedValue'); return BarTooltipItem( '$groupLabel\n$crop', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), children: [ TextSpan( // text: ' Value: ${rod.toY}', text: ' Value: $formattedValue', style: const TextStyle( color: Colors.yellow, fontWeight: FontWeight.w500, ), ), ], ); } return null; }, ), touchCallback: (event, response) { if (event.isInterestedForInteractions && response != null && response.spot != null) { // setState(() { touchedGroupIndex = response.spot!.touchedBarGroupIndex; // }); } else { // setState(() { touchedGroupIndex = -1; // Reset if no interaction // }); } }, ), gridData: FlGridData(show: false), ), ), ), )) ]), ); case 'horizontal_rotate': String cropKey = chartData['chart_type_json'] ['y_sub_group']; // Dynamically get crop key // Group crops by CROP_TYPE Map> groupedCrops = {}; // Iterate over the chartData to group crops by CROP_TYPE for (var item in chartData['response']) { String crop, cropType; // crop = item['ObsKey']['CROP']; crop = item['ObsKey'][cropKey]; cropType = item['ObsKey'][groupByKey]; if (groupedCrops.containsKey(cropType)) { groupedCrops[cropType]!.add(crop); } else { groupedCrops[cropType] = [crop]; } } int touchedGroupIndex = -1; int rotationTurns = 1; print('Grouped Crops: $groupedCrops'); int numberOfBars = groupedCrops.length; // Number of grouped sets (bar groups) double barHeight = 10.0; // Height per individual bar double barSpacing = 1.0; // Space between bar sets double minHeight = 300.0; // Minimum chart height double maxHeight = 500.0; // Maximum chart height // Calculate total height dynamically double chartHeight = ((numberOfBars * barHeight) + ((numberOfBars - 1) * barSpacing)) .clamp(minHeight, maxHeight); return Column(children: [ Text( chartData['chart_heading'] ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, ), textAlign: TextAlign.center, ), SizedBox(height: 5), Text( chartData['chart_sub_heading'] ?? '', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w300, ), textAlign: TextAlign.center, ), SizedBox(height: 5), // Chart Flexible( // child: SingleChildScrollView( // scrollDirection: Axis.vertical, // child: SingleChildScrollView( // scrollDirection: Axis.horizontal, child: SizedBox( // width: 500, // width: 1000, height: chartHeight, child: Stack(children: [ BarChart( BarChartData( maxY: 400000, rotationQuarterTurns: rotationTurns, barTouchData: BarTouchData( // enabled: true, enabled: false, touchTooltipData: BarTouchTooltipData( getTooltipColor: (group) => Colors.transparent, tooltipHorizontalAlignment: FLHorizontalAlignment.center, tooltipRoundedRadius: 8, fitInsideHorizontally: true, // Ensure it fits within the screen fitInsideVertically: true, tooltipPadding: EdgeInsets.all(8), tooltipMargin: 16, // Only show tooltip when touched getTooltipItem: (group, groupIndex, rod, rodIndex) { // if (rod.toY == 0 || touchedGroupIndex == -1) { // return null; // Don't show the tooltip if the value is 0 or there's no touch // } // Hide tooltips for 0 values if (rod.toY == 0) return null; // Hide for zero values // if (groupIndex == touchedGroupIndex) { // print('Group Index: $groupIndex, Group : $group'); // Get the group label dynamically String groupLabel = groupByValues.elementAt(groupIndex); // Fetch the crop for the current group from groupedCrops String cropType = groupByValues.elementAt(groupIndex); String crop = groupedCrops[cropType]![rodIndex]; double value = rod.toY; String formattedValue; if (value >= 1000000) { formattedValue = (value / 1000000).toStringAsFixed(1) + 'M'; } else if (value >= 1000) { formattedValue = (value / 1000).toStringAsFixed(1) + 'K'; } else { formattedValue = value.toStringAsFixed( 0); // for values smaller than 1000 } return BarTooltipItem( '$formattedValue', const TextStyle( color: Colors.black, fontWeight: FontWeight.w400, fontSize: 12, ), ); // return BarTooltipItem( // '$groupLabel\n$crop', // const TextStyle( // color: Colors.white, // fontWeight: FontWeight.bold, // ), // children: [ // TextSpan( // text: ' Value: $formattedValue', // style: const TextStyle( // color: Colors.yellow, // fontWeight: FontWeight.w500, // ), // ), // ], // ); // } // return null; }, ), // touchCallback: (event, response) { // if (event.isInterestedForInteractions && // response != null && // response.spot != null) { // // setState(() { // touchedGroupIndex = // response.spot!.touchedBarGroupIndex; // // }); // } else { // // setState(() { // touchedGroupIndex = -1; // Reset if no interaction // // }); // } // }, ), titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( // showTitles: false, reservedSize: 40, interval: 100000, getTitlesWidget: (value, meta) { return SizedBox( width: 50, child: Text( value.toInt().toString(), textAlign: TextAlign.justify, style: const TextStyle( fontSize: 12, ), ), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 150, // Added space for rotated titles getTitlesWidget: (value, meta) { if (value < groupByValues.length) { String title = groupByValues.elementAt(value.toInt()); return Transform.rotate( angle: -1.58, // Rotation in radians (~ -30 degrees) child: Center( child: SizedBox( width: 120, child: Text( title, textAlign: TextAlign.end, style: const TextStyle(fontSize: 12), softWrap: true, maxLines: 2, ), ), ), ); } return const SizedBox.shrink(); }, ), ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData( show: true, border: const Border( // left: BorderSide(color: Colors.grey), bottom: BorderSide(color: Colors.white), ), ), gridData: FlGridData( show: false, drawVerticalLine: true, verticalInterval: 1, horizontalInterval: 100000, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey.withOpacity(0.5), strokeWidth: 1, ); }, getDrawingVerticalLine: (value) { return FlLine( color: Colors.grey.withOpacity(0.5), strokeWidth: 1, ); }, ), barGroups: _buildHorizontalRotateBarGroups( chartData, groupByValues), alignment: BarChartAlignment.spaceAround, ), ), ])), // ), // ) ), ]); default: return Center(child: Text('Unknown chart type')); } } // Start Of line trend chart FlTitlesData titlesData2(Set xValues) { return FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) { // Format values as millions (M) // String formattedValue = (value / 1000000).toStringAsFixed(1) + 'M'; return Text( value.toStringAsFixed(1), style: TextStyle(color: Colors.black, fontSize: 12), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: 1, // Ensure each year is shown only once getTitlesWidget: (value, meta) { if (xValues.contains(value)) { return Text( value.toInt().toString(), style: TextStyle(color: Colors.black, fontSize: 12), ); } else { return SizedBox.shrink(); // Hide non-relevant labels } }, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide top titles ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide right titles ), ); } List lineBarsData2(List filteredData) { List lineBars = []; // Create a map to store population values by year and gender Map> yearGenderMap = {}; for (var entry in filteredData) { int year = int.tryParse(entry['ObsKey']['TIME_PERIOD'].toString()) ?? 0; String gender = entry['ObsKey']['GENDER']; double value = double.tryParse(entry['ObsValue']['Value'].toString()) ?? 0.0; if (!yearGenderMap.containsKey(year)) { yearGenderMap[year] = {'M': 0.0, 'F': 0.0}; } if (gender == 'Male'|| gender == 'ذكر') { yearGenderMap[year]!['M'] = value; } else if (gender == 'Female' || gender == 'أنثى') { yearGenderMap[year]!['F'] = value; } } // Calculate the ratio (M/F) * 100 for each year List spots = []; yearGenderMap.forEach((year, genderMap) { if (genderMap['M'] != 0.0 && genderMap['F'] != 0.0) { double ratio = (genderMap['M']! / genderMap['F']!) * 100; spots.add(FlSpot(year.toDouble(), ratio)); } }); // Add a line for the ratio data lineBars.add( LineChartBarData( spots: spots, isCurved: true, color: Colors.blue, // Set color for the ratio line barWidth: 3, isStrokeCapRound: true, belowBarData: BarAreaData(show: true), ), ); return lineBars; } /// Function to parse TIME_PERIOD into a EXACT value double parseTimePeriod(String timePeriod) { // Match formats if (RegExp(r'^\d{4}$').hasMatch(timePeriod)) { // Format: Year-only (e.g., "2020") return double.parse(timePeriod); // Year as a double (2020 → 2020.0) } else if (RegExp(r'^\d{4}-\d{2}$').hasMatch(timePeriod)) { // Format: Year-Month (e.g., "2020-10") return double.parse( timePeriod.replaceAll('-', '.')); // Convert "2020-10" → 2020.10 } else if (RegExp(r'^\d{4}-[A-Za-z]{3}$').hasMatch(timePeriod)) { // Format: Year-MonthAbbr (e.g., "2020-Oct") Map monthMap = { 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12 }; List parts = timePeriod.split('-'); int month = monthMap[parts[1]] ?? 1; // Default to January if unknown return double.parse('${parts[0]}.$month'); // Convert "2020-Oct" → 2020.10 } // Throw an error for unsupported formats throw FormatException("Invalid TIME_PERIOD format: $timePeriod"); } List lineBarsData(List filteredData, Set groupByValues, String groupByKey) { List lineBars = []; print('filteredData $filteredData'); print('filteredData222 $groupByValues'); final List uniqueColors = [ Color(0xFF6097CD), Color(0xFFD086A7), Color(0xFF98BCE5), Color(0xFFA7B5C5), Color(0xFFBED3EC), Color(0xFFD4E3F4), ]; // Create a map to assign colors to each group in order Map groupColorMap = {}; int colorIndex = 0; for (String group in groupByValues) { groupColorMap[group] = uniqueColors[colorIndex % uniqueColors.length]; colorIndex++; } print('Group-Color Map: $groupColorMap'); // Iterate through each group and generate line data for (String group in groupByValues) { print('lineGrp1'); // List spots = filteredData // .where((entry) => entry['ObsKey'][groupByKey] == group) // .map((entry) { // print("FindTimePeriod"); // print(entry['ObsKey'][groupByKey]); // double xValue = double.parse(entry['ObsKey']['TIME_PERIOD']); // double yValue = double.parse(entry['ObsValue']['Value']); // return FlSpot(xValue, yValue); // }).toList(); // List spots = filteredData // .where((entry) => entry['ObsKey'][groupByKey] == group) // .map((entry) { // String timePeriod = entry['ObsKey']['TIME_PERIOD']; // print("FindTimePeriod: $timePeriod"); // // double xValue = parseTimePeriodToDouble(timePeriod); // Convert to double for graph plotting // double yValue = double.parse(entry['ObsValue']['Value']); // print('TimePeriod: $timePeriod, xValue: $xValue, yValue: $yValue'); // // return FlSpot(xValue, yValue); // }).toList(); List spots = filteredData .where((entry) => entry['ObsKey'][groupByKey] == group) .map((entry) { String timePeriod = entry['ObsKey']['TIME_PERIOD']; print("FindTimePeriod: $timePeriod"); // Use the timePeriod directly as a string for x-axis double xValue = parseTimePeriod(timePeriod); print('spotss'); // double yValue = double.parse(entry['ObsValue']['Value']); double yValue; var value = entry['ObsValue']['Value']; if (value is int) { yValue = value.toDouble(); } else if (value is double) { yValue = value; } else if (value is String) { yValue = double.tryParse(value) ?? 0.0; // Handle invalid strings safely } else { throw Exception("Unexpected value type: ${value.runtimeType}"); } return FlSpot(xValue, yValue); }).toList(); print('spots - $spots'); // List spots = filteredData // .where((entry) => entry['ObsKey'][groupByKey] == group) // .map((entry) { // String timePeriod = entry['ObsKey']['TIME_PERIOD']; // List parts = timePeriod.split('-'); // Split "2017-01" into ["2017", "01"] // // double year = double.parse(parts[0]); // Convert "2017" to 2017.0 // double month = double.parse(parts[1]) / 12; // Convert "01" to 1/12 // // double xValue = year + month; // E.g., "2017-06" becomes 2017.5 // double yValue = double.parse(entry['ObsValue']['Value']); // // return FlSpot(xValue, yValue); // }).toList(); print('lineGrp2'); // Add a line for this group lineBars.add( LineChartBarData( spots: spots, isCurved: true, color: groupColorMap[group], barWidth: 3, isStrokeCapRound: true, dotData: FlDotData(show: true), belowBarData: BarAreaData(show: false), ), ); } return lineBars; } // Helper functions for chart styles LineTouchData lineTouchData1() { return LineTouchData( touchTooltipData: LineTouchTooltipData( // tooltipBgColor: Colors.black.withOpacity(0.7), // Tooltip background tooltipRoundedRadius: 8, fitInsideHorizontally: true, // Ensure it fits within the screen fitInsideVertically: true, tooltipPadding: EdgeInsets.all(8), tooltipMargin: 16, // Adds margin to prevent clipping getTooltipItems: (List lineBarsSpot) { return lineBarsSpot.map((lineBarSpot) { Color lineColor = lineBarSpot.bar is LineChartBarData ? (lineBarSpot.bar).color ?? Colors.white : Colors.white; // Extract bar color safely return LineTooltipItem( '', const TextStyle(), children: [ TextSpan( text: '● ', // Unicode circle style: TextStyle( color: lineColor), // Set circle color to match bar ), TextSpan( text: formatNumber(lineBarSpot.y), // text: ' ${lineBarSpot.y}', // Keep the value white style: const TextStyle(color: Colors.white), ), ], ); }).toList(); }, ), handleBuiltInTouches: true, ); } FlGridData gridData() { return FlGridData( show: false, drawVerticalLine: true, getDrawingHorizontalLine: (value) => FlLine(color: Colors.grey, strokeWidth: 1), getDrawingVerticalLine: (value) => FlLine(color: Colors.grey, strokeWidth: 1), ); } FlTitlesData titlesData1(Set xValues) { print('tileDATa1xvalueline2 - $xValues'); double findClosest(double value, Set values) { return values .reduce((a, b) => (value - a).abs() < (value - b).abs() ? a : b); } return FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) { // Format values as millions (M) String formattedValue; print('LineTrend2val1 - $value '); if (value % 10 == 0) { // Check if the value is in the millions or thousands range if (kDebugMode) { print('LineTrend2valKmode - $value '); } if (value >= 1000000000000) { formattedValue = '${(value / 1000000000000).toStringAsFixed(0)}T'; } else if (value >= 1000000000) { formattedValue = '${(value / 1000000000).toStringAsFixed(0)}B'; } else if (value >= 1000000) { formattedValue = '${(value / 1000000).toStringAsFixed(0)}M'; } else if (value >= 1000) { formattedValue = '${(value / 1000).toStringAsFixed(0)}k'; } else { formattedValue = value.toStringAsFixed( 0); // No decimals for values less than 1000 } // Skip rendering if the formatted value is the same as the last one if (_lastFormattedValue == formattedValue) { return const SizedBox.shrink(); // Empty widget for duplicates } // Update the last formatted value for the next comparison _lastFormattedValue = formattedValue; // print('formattedValueLine - $formattedValue'); return Text(formattedValue, textAlign: TextAlign.center, style: TextStyle( fontSize: 10, )); } return const SizedBox.shrink(); // return Text( // 'LINETRENTD2', // style: TextStyle(color: Colors.black, fontSize: 12), // ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: null, // interval: 10, // Ensure each year is shown only once getTitlesWidget: (value, meta) { // print('BtmTiles :-$value'); // print('BtmTilesmeta :-$meta'); // if (xValues.contains(value)) double closestValue = findClosest(value, xValues); if ((closestValue - value).abs() < 0.15) { // print("Bottomtiles"); // print(value); return Padding( padding: const EdgeInsets.only(top: 1.1), child: Transform.rotate( angle: 0.0, // angle: -0.5, // Slight rotation to improve readability child: Text( value.toInt().toString(), style: TextStyle(color: Colors.black, fontSize: 12), ), ), ); } else { return SizedBox.shrink(); // Hide non-relevant labels } }, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide top titles ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), // Hide right titles ), ); } FlBorderData borderData() { return FlBorderData( show: true, border: Border( bottom: BorderSide(color: Colors.black, width: 1), // Show bottom border left: BorderSide(color: Colors.black, width: 1), // Show left border top: BorderSide.none, // Hide top border right: BorderSide.none, // Hide right border ), ); } // End Of line trend chart // Define the list of unique colors final List uniqueColors = [ Color(0xFF648CBA), Color(0xFF90B0D5), Color(0xFF98BCE5), Color(0xFFA7B5C5), Color(0xFFBED3EC), Color(0xFFD4E3F4), ]; Color _getColorForGroup(int index) { // Use the index to get a color from the uniqueColors list return uniqueColors[index % uniqueColors.length]; } List _generateBarGroups( BuildContext context, dynamic chartData, String groupByKey) { Map> groupedData = {}; print('flStackedBar3'); // Group data by `group_by` and `TIME_PERIOD`, accumulating the values for each group and year for (var entry in chartData['response']) { String groupValue = entry['ObsKey'][groupByKey]; String timePeriod = entry['ObsKey']['TIME_PERIOD']; // double value = double.tryParse(entry['ObsValue']['Value']) ?? 0.0; double value; var rawValue = entry['ObsValue']['Value']; if (rawValue is num) { value = rawValue.toDouble(); } else if (rawValue is double) { value = rawValue; } else { value = double.tryParse(rawValue.toString()) ?? 0.0; } print('flStackedBar3.1'); // print('fl_groupValue $groupValue'); // print('fl_timePeriod $timePeriod'); // print('fl_value $value'); if (!groupedData.containsKey(groupValue)) { groupedData[groupValue] = {}; } if (!groupedData[groupValue]!.containsKey(timePeriod)) { groupedData[groupValue]![timePeriod] = 0.0; } groupedData[groupValue]![timePeriod] = groupedData[groupValue]![timePeriod]! + value; } // Create BarChartGroupData for each group List barGroups = []; List timePeriods = groupedData.values.first.keys.toList(); print('timePeriods $timePeriods'); // double barWidth = calculateBarWidth(context, timePeriods.length); // For each time period, generate a BarChartGroupData for (int i = 0; i < timePeriods.length; i++) { // Intermediate variable to track the current toY value as we stack bars double currentToY = 0; List rodStackItems = []; // For each group (e.g., region), create stacked bars int groupIndex = 0; // Track group index for color assignment for (var group in groupedData.keys) { double value = groupedData[group]![timePeriods[i]] ?? 0.0; // Add stacked item with current `toY` value and the incremented one rodStackItems.add( BarChartRodStackItem( currentToY, // The current bottom of the stack currentToY + value, // The new top of the stack _getColorForGroup(groupIndex), // Color based on the index ), ); // Update currentToY after adding the value for this stack currentToY += value; groupIndex++; // Increment group index for the next group } // Create BarChartGroupData with the final stacked value BarChartGroupData barGroup = BarChartGroupData( x: i, // Index for the x-axis based on the time period barRods: [ BarChartRodData( toY: currentToY, // Use the accumulated `currentToY` value rodStackItems: rodStackItems, // Add the stacked items width: 20, // width: barWidth, borderRadius: BorderRadius.zero, color: Colors.black // This will act as a container for stacked items ), ], ); barGroups.add(barGroup); } return barGroups; } // Function to dynamically generate titles for the x-axis (time periods) List _generateXTitles(dynamic chartData) { Set timePeriods = {}; for (var entry in chartData['response']) { timePeriods.add(entry['ObsKey']['TIME_PERIOD']); } return timePeriods.map((period) { return Text( period, style: TextStyle(fontSize: 12), ); }).toList(); } // Function to dynamically set left axis titles (values) String? _lastFormattedValue; Widget _generateLeftTitles(double value, TitleMeta meta) { String formattedValue; if (value % 1000 == 0) { // Check if the value is in the millions or thousands range if (value >= 1000000) { formattedValue = '${(value / 1000000).toStringAsFixed(0)}M'; } else if (value >= 1000) { formattedValue = '${(value / 1000).toStringAsFixed(0)}k'; } else { formattedValue = value.toStringAsFixed(0); // No decimals for values less than 1000 } // Skip rendering if the formatted value is the same as the last one if (_lastFormattedValue == formattedValue) { return const SizedBox.shrink(); // Empty widget for duplicates } // Update the last formatted value for the next comparison _lastFormattedValue = formattedValue; print('formattedValue - $formattedValue'); return Text(formattedValue, style: TextStyle(fontSize: 10)); } return const SizedBox.shrink(); // return Container(); } /// Start horizontal rotate and fl_multi_bar bar // List _buildBarGroups(dynamic chartData) { // String groupByKey = chartData['group_by']; // if (groupByKey == null || chartData['response'] == null) { // return []; // } // // // Extract unique group_by values // Set groupByValues = extractGroupByValues(chartData, groupByKey); // // // Create a map to hold grouped data // Map>> groupedData = {}; // for (var entry in chartData['response']) { // String groupValue = entry['ObsKey'][groupByKey] ?? ''; // if (groupValue.isNotEmpty) { // groupedData.putIfAbsent(groupValue, () => []).add(entry); // } // } // // // Convert grouped data into BarChartGroupData // List barGroups = []; // int groupIndex = 0; // // groupedData.forEach((key, values) { // int colorIndex = 0; // Track the color for each bar within the group // // List rods = values.map((entry) { // double yValue = double.tryParse(entry['ObsValue']['Value'] ?? '0') ?? 0; // // // Cycle through colors for each bar // final barColor = uniqueColors[colorIndex % uniqueColors.length]; // colorIndex++; // // return BarChartRodData( // fromY: 0, // toY: yValue, // color: barColor, // width: 20, // Adjust bar width for better visibility // borderRadius: BorderRadius.circular(4), // ); // }).toList(); // // barGroups.add(BarChartGroupData( // x: groupIndex, // barRods: rods, // showingTooltipIndicators: [0], // )); // groupIndex++; // }); // // return barGroups; // } double _calculateMaxY(dynamic chartData) { // Dynamically calculate the maximum Y value print('hormulti_bar2'); List response = chartData['response'] ?? []; double maxY = 0; for (var entry in response) { // double value = double.tryParse(entry['ObsValue']['Value'] ?? '0') ?? 0; double value = (entry['ObsValue']['Value'] as num?)?.toDouble() ?? 0; if (value > maxY) { maxY = value; } } return maxY * 1.1; // Add 10% buffer for better visualization } List _buildHorizontalRotateBarGroups( dynamic chartData, Set groupByValues) { List responseData = chartData['response']; List barGroups = []; int colorIndex = 0; print('hormulti_bar1.1'); // Iterate through groupByValues and populate BarChartGroupData for (int i = 0; i < groupByValues.length; i++) { String groupValue = groupByValues.elementAt(i); // Filter data for the current group (grouping by CROP_TYPE) List groupData = responseData.where((entry) { return entry['ObsKey'][chartData['group_by']] == groupValue; }).toList(); // Create BarChartRodData for each bar in the group List barRods = groupData.map((data) { // Ensure to retrieve and parse 'ObsValue' value (which should be a double) double value = double.tryParse(data['ObsValue']['Value'].toString()) ?? 0.0; print('multi_bar1.1'); final List uniqueColorsForTwo = [ Color(0xFF648CBA), Color(0xFF90B0D5), ]; final barColor; if (chartData['dataset'] == 'general_education' || chartData['dataset'] == 'higher_education' || chartData['dataset'] == 'air_transport' || chartData['dataset'] == 'labour_force' || chartData['dataset'] == 'gdp' || chartData['dataset'] == 'clinics') { barColor = uniqueColorsForTwo[colorIndex % uniqueColorsForTwo.length]; colorIndex++; } else { barColor = uniqueColors[colorIndex % uniqueColors.length]; colorIndex++; } return BarChartRodData( toY: value, // Use the parsed value color: barColor, // Dynamic color width: 20, // backDrawRodData: BackgroundBarChartRodData( // show: true, // toY: 400000, // color: Colors.grey.shade300, // ), ); }).toList(); // Add BarChartGroupData for the group barGroups.add( BarChartGroupData( x: i, barRods: barRods, showingTooltipIndicators: List.generate(barRods.length, (index) => index), ), ); } return barGroups; } /// End horizontal rotate and fl_multi_bar bar @override Widget build(BuildContext context) { return buildChart(chartData, context); } } class BarValuePainter extends CustomPainter { BarValuePainter(this.barGroups, this.constraints); final List barGroups; final BoxConstraints constraints; @override void paint(Canvas canvas, Size size) { final textPainter = TextPainter( textAlign: TextAlign.center, textDirection: TextDirection.ltr, ); final paint = Paint()..color = Colors.black; for (var group in barGroups) { for (var rod in group.barRods) { final x = group.x.toDouble() * (size.width / barGroups.length); final y = (1 - rod.toY / 400000) * size.height; // Normalize height textPainter.text = TextSpan( text: rod.toY.toStringAsFixed(1), style: TextStyle(color: Colors.black, fontSize: 12), ); textPainter.layout(); textPainter.paint(canvas, Offset(x - textPainter.width / 2, y - 20)); } } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } double calculateBarWidth(BuildContext context, int totalBars) { double width = MediaQuery.of(context).size.width; // Chart width double padding = 16.0; // Space between bars double availableWidth = (width - 100); if (totalBars <= 0) { throw ArgumentError("TotalBars must be greater than 0"); } double barWidth = (availableWidth - (padding * (totalBars - 1))) / totalBars; print('barWidth - $barWidth'); // Ensure bar width does not exceed availableWidth if (barWidth > availableWidth) { barWidth = availableWidth; } // Optional: Add a minimum width constraint if needed double minWidth = 10.0; // Example minimum width if (barWidth < minWidth) { barWidth = minWidth; } return barWidth; } class ChartData { ChartData({this.x, required this.y, this.xDateTime}); final String? x; final double y; final DateTime? xDateTime; }