3865 lines
148 KiB
Dart
3865 lines
148 KiB
Dart
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,
|
|
required this.chartHeader,
|
|
}) : super(key: key);
|
|
final dynamic chartData;
|
|
final Color bodyColor;
|
|
final String chartHeader;
|
|
|
|
List<int> showingTooltipOnSpots = []; // To store indices of active tooltips
|
|
late LineChartBarData tooltipsOnBar; // Needs proper initialization
|
|
|
|
// Define color lists
|
|
final Map<String, List<Color>> colorMap = {
|
|
"ECONOMY": [
|
|
Color(0xFF648CBA),
|
|
Color(0xFF90B0D5),
|
|
Color(0xFF98BCE5),
|
|
Color(0xFFA7B5C5),
|
|
Color(0xFFBED3EC),
|
|
Color(0xFFD4E3F4),
|
|
],
|
|
"SOCIAL": [
|
|
Color(0xFFD9BB99),
|
|
Color(0xFF87766E),
|
|
Color(0xFFC6A885),
|
|
Color(0xFFA28565),
|
|
Color(0xFFBFB29B),
|
|
Color(0xFFE8D8BB),
|
|
],
|
|
"ENVIRONMENT": [
|
|
Color(0xFF376C79),
|
|
Color(0xFF578D9C),
|
|
Color(0xFF7DAFBC),
|
|
Color(0xFF86C7D9),
|
|
Color(0xFF989898),
|
|
],
|
|
};
|
|
|
|
final Map<String, List<Color>> colorMapForFour = {
|
|
"ECONOMY": [
|
|
Color(0xFF648CBA),
|
|
Color(0xFF90B0D5),
|
|
Color(0xFF98BCE5),
|
|
Color(0xFFA7B5C5),
|
|
Color(0xFFBED3EC),
|
|
],
|
|
"SOCIAL": [
|
|
Color(0xFFD9BB99),
|
|
Color(0xFF87766E),
|
|
Color(0xFFC6A885),
|
|
Color(0xFFA28565),
|
|
Color(0xFFBFB29B),
|
|
],
|
|
"ENVIRONMENT": [
|
|
Color(0xFF376C79),
|
|
Color(0xFF578D9C),
|
|
Color(0xFF7DAFBC),
|
|
Color(0xFF86C7D9),
|
|
],
|
|
};
|
|
|
|
final Map<String, List<Color>> colorMapForThree = {
|
|
"ECONOMY": [Color(0xFF648CBA), Color(0xFF90B0D5), Color(0xFF98BCE5)],
|
|
"SOCIAL": [Color(0xFFD9BB99), Color(0xFF87766E), Color(0xFFC6A885)],
|
|
"ENVIRONMENT": [Color(0xFF376C79), Color(0xFF578D9C), Color(0xFF7DAFBC)],
|
|
};
|
|
|
|
// Define color lists
|
|
final Map<String, List<Color>> colorMapForTwo = {
|
|
"ECONOMY": [Color(0xFF648CBA), Color(0xFF90B0D5)],
|
|
"SOCIAL": [Color(0xFFD9BB99), Color(0xFF87766E)],
|
|
"ENVIRONMENT": [Color(0xFF376C79), Color(0xFF578D9C)],
|
|
};
|
|
|
|
// List<Color> get uniqueColors => colorMap[chartHeader] ?? [Colors.grey]; // Default to grey if header is unknown
|
|
// Function to get colors for both English & Arabic keys
|
|
List<Color> get uniqueColors {
|
|
final Map<String, String> translations = {
|
|
'اقتصاد': 'ECONOMY',
|
|
'اجتماعي': 'SOCIAL',
|
|
'بيئة': 'ENVIRONMENT',
|
|
};
|
|
|
|
String key = translations[chartHeader] ?? chartHeader;
|
|
return colorMap[key] ?? [Colors.grey];
|
|
}
|
|
|
|
List<Color> get uniqueColorsForTwo {
|
|
final Map<String, String> translations = {
|
|
'اقتصاد': 'ECONOMY',
|
|
'اجتماعي': 'SOCIAL',
|
|
'بيئة': 'ENVIRONMENT',
|
|
};
|
|
|
|
String key = translations[chartHeader] ?? chartHeader;
|
|
return colorMapForTwo[key] ?? [Colors.grey];
|
|
}
|
|
|
|
List<Color> get uniqueColorsForThree {
|
|
final Map<String, String> translations = {
|
|
'اقتصاد': 'ECONOMY',
|
|
'اجتماعي': 'SOCIAL',
|
|
'بيئة': 'ENVIRONMENT',
|
|
};
|
|
|
|
String key = translations[chartHeader] ?? chartHeader;
|
|
return colorMapForThree[key] ?? [Colors.grey];
|
|
}
|
|
|
|
List<Color> get uniqueColorsForFour {
|
|
final Map<String, String> translations = {
|
|
'اقتصاد': 'ECONOMY',
|
|
'اجتماعي': 'SOCIAL',
|
|
'بيئة': 'ENVIRONMENT',
|
|
};
|
|
|
|
String key = translations[chartHeader] ?? chartHeader;
|
|
return colorMapForFour[key] ?? [Colors.grey];
|
|
}
|
|
|
|
String capitalizeAndSplit(String input) {
|
|
return input
|
|
.split('_')
|
|
.map((word) => word[0].toUpperCase() + word.substring(1))
|
|
.join(' ');
|
|
}
|
|
|
|
Color _hexToColor(String hexColor) {
|
|
hexColor = hexColor.toUpperCase().replaceAll('#', ''); // Remove #
|
|
if (hexColor.length == 6) {
|
|
hexColor = 'FF$hexColor'; // Add alpha if missing
|
|
}
|
|
return Color(int.parse(hexColor, radix: 16));
|
|
}
|
|
|
|
String formatVerticalText(String text) {
|
|
return text.split('').join('\n'); // Inserts a newline after each character
|
|
}
|
|
|
|
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<ChartData> parseStackedBarChartData(
|
|
dynamic chartData,
|
|
String groupByKey,
|
|
String groupByValue,
|
|
) {
|
|
return chartData['response']
|
|
.where((entry) => entry['ObsKey'][groupByKey] == groupByValue)
|
|
.map<ChartData>((entry) {
|
|
return ChartData(
|
|
x: entry['ObsKey']['TIME_PERIOD'],
|
|
y: double.tryParse(entry['ObsValue']['Value']) ?? 0.0,
|
|
);
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
List<BarChartGroupData> parseColumnChartData(dynamic chartData) {
|
|
return chartData['response'].map<BarChartGroupData>((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 hexToColor(String hex) {
|
|
hex = hex.replaceFirst('#', ''); // Remove #
|
|
if (hex.length == 6) {
|
|
hex = 'FF$hex'; // Add full opacity
|
|
}
|
|
return Color(int.parse(hex, radix: 16));
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
Color adjustColorlineTrend(Color baseColor, int index) {
|
|
final HSLColor hsl = HSLColor.fromColor(baseColor);
|
|
|
|
// Darken the color slightly by reducing lightness
|
|
double lightnessFactor = hsl.lightness - ((index % 4) * 0.05);
|
|
lightnessFactor = lightnessFactor.clamp(0.3, 0.7); // Keeps colors darker
|
|
|
|
// Adjust opacity: Making it darker with alpha between 160-230
|
|
int alphaFactor = (230 - (index % 4) * 20).clamp(
|
|
160,
|
|
230,
|
|
); // Darker opacity
|
|
|
|
return hsl
|
|
.withLightness(lightnessFactor)
|
|
.toColor()
|
|
.withAlpha(alphaFactor); // Use withAlpha() instead of withOpacity()
|
|
}
|
|
|
|
List<PieChartSectionData> parsePieChartData(
|
|
dynamic chartData,
|
|
double totalValue,
|
|
int? touchedIndex,
|
|
) {
|
|
debugPrint('Chart Data: ${jsonEncode(chartData)}');
|
|
|
|
return chartData['response'].asMap().entries.map<PieChartSectionData>((
|
|
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
|
|
color: uniqueColors[index % uniqueColors.length],
|
|
// title: '${percentage.toStringAsFixed(1)}%',
|
|
title: percentage >= 0.1 ? '${percentage.toStringAsFixed(1)}%' : '',
|
|
radius: isTouched ? 70 : 50,
|
|
|
|
// Increase size when touched
|
|
titleStyle: TextStyle(
|
|
fontSize: isTouched ? 12 : (percentage < 1 ? 10 : 11),
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black,
|
|
// color: Colors.white,
|
|
),
|
|
titlePositionPercentageOffset: 1.4,
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
// List<Widget> generateIndicators(dynamic chartData) {
|
|
// var groupByKeyValue;
|
|
// if (chartData['chart_type_json']['chart_type'] == 'bar_chart_horizontal' ||
|
|
// chartData['chart_type_json']['chart_type'] == 'horizontal_rotate') {
|
|
// groupByKeyValue = chartData['chart_type_json']['y_group'] ?? '';
|
|
// } else {
|
|
// groupByKeyValue = chartData['chart_type_json']['x_group'] ?? '';
|
|
// }
|
|
//
|
|
// return [
|
|
// Center(
|
|
// child: Wrap(
|
|
// alignment: WrapAlignment.center, // Center-align all items
|
|
// spacing: 10, // Adjust horizontal spacing
|
|
// runSpacing: 5, // Adjust vertical spacing for wrapping
|
|
// children: chartData['response'].asMap().entries.map<Widget>((entry) {
|
|
// int index = entry.key;
|
|
// var data = entry.value;
|
|
// Color color = Colors.primaries[index % Colors.primaries.length];
|
|
// String title = data['ObsKey'][groupByKeyValue] ?? '';
|
|
//
|
|
// return Container(
|
|
// padding: EdgeInsets.symmetric(vertical: 4), // Add spacing
|
|
// child: Row(
|
|
// mainAxisSize:
|
|
// MainAxisSize.min, // Wrap content without extra space
|
|
// mainAxisAlignment:
|
|
// MainAxisAlignment.center, // Align items center
|
|
// crossAxisAlignment:
|
|
// CrossAxisAlignment.center, // Align vertically
|
|
// children: [
|
|
// Container(
|
|
// width: 10,
|
|
// height: 10,
|
|
// decoration: BoxDecoration(
|
|
// color: uniqueColors[index % uniqueColors.length],
|
|
// shape: BoxShape.circle,
|
|
// ),
|
|
// ),
|
|
// SizedBox(width: 5),
|
|
// ConstrainedBox(
|
|
// constraints: BoxConstraints(
|
|
// maxWidth: 200, // Ensures text wraps within a limit
|
|
// ),
|
|
// child: Text(
|
|
// title,
|
|
// style: TextStyle(fontSize: 12),
|
|
// softWrap: true, // Allow text to wrap naturally
|
|
// textAlign: TextAlign.center, // Center text alignment
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// );
|
|
// }).toList(),
|
|
// ),
|
|
// ),
|
|
// ];
|
|
// }
|
|
|
|
List<Widget> generateIndicators(dynamic chartData, double totalValue) {
|
|
var groupByKeyValue;
|
|
if (chartData['chart_type_json']['chart_type'] == 'bar_chart_horizontal' ||
|
|
chartData['chart_type_json']['chart_type'] == 'horizontal_rotate') {
|
|
groupByKeyValue = chartData['chart_type_json']['y_group'] ?? '';
|
|
} else {
|
|
groupByKeyValue = chartData['chart_type_json']['x_group'] ?? '';
|
|
}
|
|
return chartData['response'].asMap().entries.map<Widget>((entry) {
|
|
int index = entry.key;
|
|
var data = entry.value;
|
|
Color color = Colors.primaries[index % Colors.primaries.length];
|
|
String title = data['ObsKey'][groupByKeyValue] ?? '';
|
|
// String value = data['ObsValue']['Value'].toString();
|
|
String shortTitle =
|
|
title.length > 10 ? '${title.substring(0, 10)}…' : title;
|
|
|
|
// 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;
|
|
String percentage =
|
|
(totalValue > 0)
|
|
? ((value / totalValue) * 100).toStringAsFixed(1)
|
|
: "0.0";
|
|
// bool isTouched = index == touchedIndex;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: uniqueColors[index % uniqueColors.length],
|
|
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: 250),
|
|
child: Text(
|
|
'$title -- $percentage%',
|
|
style: TextStyle(fontSize: 12),
|
|
softWrap: true,
|
|
maxLines: 2,
|
|
// overflow: TextOverflow.ellipsis, // Ensures text doesn't overflow
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 5),
|
|
],
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
List<Widget> generateIndicatorsBar(
|
|
Map<String, dynamic> chartData,
|
|
List<String> groupByValues,
|
|
) {
|
|
List<Widget> indicators = [];
|
|
|
|
for (int i = 0; i < groupByValues.length; i++) {
|
|
String groupLabel = groupByValues[i];
|
|
|
|
indicators.add(
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 12,
|
|
height: 12,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.blue, // Replace with dynamic color if needed
|
|
),
|
|
),
|
|
SizedBox(width: 5),
|
|
Text(
|
|
groupLabel, // Displaying groupLabel
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return indicators;
|
|
}
|
|
|
|
Set<String> extractGroupByValues(dynamic chartData, String groupByKey) {
|
|
return (chartData['response'] as List<dynamic>).map<String>((entry) {
|
|
var value = entry['ObsKey'][groupByKey];
|
|
return value != null
|
|
? value.toString()
|
|
: ''; // Handle null and ensure conversion to String
|
|
}).toSet();
|
|
}
|
|
|
|
// @override
|
|
// void initState() {
|
|
// super.initState();
|
|
// tooltipsOnBar = lineBars.first; // Example: Assign the first line bar
|
|
// }
|
|
|
|
Widget buildChart(dynamic chartData, BuildContext context) {
|
|
// print('bodyColor- $bodyColor');
|
|
print('chartDataccccccc $chartData');
|
|
if (chartData == null || chartData['response'] == null) {
|
|
return Center(child: Text('No chart data available'));
|
|
}
|
|
|
|
print(chartData['chart_type_json']['TIME_PERIOD']);
|
|
print(chartData['chart_type_json']['x_group']);
|
|
|
|
String groupByKey = '';
|
|
|
|
if (chartData['chart_type_json']['chart_type'] == 'bar_chart_horizontal' ||
|
|
chartData['chart_type_json']['chart_type'] == 'horizontal_rotate') {
|
|
groupByKey = chartData['chart_type_json']['y_group'] ?? '';
|
|
} else {
|
|
groupByKey = chartData['chart_type_json']['x_group'] ?? '';
|
|
}
|
|
|
|
print('groupByKey $groupByKey');
|
|
Set<String> groupByValues = extractGroupByValues(chartData, groupByKey);
|
|
print('groupByValues $groupByValues');
|
|
|
|
switch (chartData['chart_type_json']['chart_type']) {
|
|
// case 'stacked_bar':
|
|
// String chartTitle =
|
|
// capitalizeAndSplit(chartData['chart_heading'] ?? '');
|
|
// // Define a list of colors
|
|
// final List<Color> 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: <StackedBarSeries<ChartData, String>>[
|
|
// for (int i = 0; i < groupByValues.length; i++)
|
|
// StackedBarSeries<ChartData, String>(
|
|
// 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<Color> 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: <CartesianSeries<ChartData, String>>[
|
|
// for (int i = 0; i < groupByValues.length; i++)
|
|
// StackedColumnSeries<ChartData, String>(
|
|
// 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<double>((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<int?> 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<int?>(
|
|
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: 55),
|
|
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: 30,
|
|
),
|
|
child: Column(
|
|
// Change Row to Column
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: generateIndicators(chartData, totalValue),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// ),
|
|
],
|
|
);
|
|
case 'fl_stacked_bar':
|
|
// Divide groupByValuesList into chunks of 5 items per row
|
|
List<String> groupByValuesList = groupByValues.toList();
|
|
int chunkSize = 5;
|
|
List<List<String>> 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,
|
|
getTooltipColor: (colorMap) => Colors.black,
|
|
getTooltipItem: (
|
|
groupData,
|
|
groupIndex,
|
|
rodData,
|
|
rodIndex,
|
|
) {
|
|
// Get the list of group names dynamically
|
|
|
|
List<String> 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<BarChartRodStackItem> rodStackItems =
|
|
rodData.rodStackItems;
|
|
List<TextSpan> 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<String> 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.black,
|
|
// 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<Color> uniqueColorsLine_trend_2 = [
|
|
Color(0xFF6097CD),
|
|
Color(0xFFD086A7),
|
|
Color(0xFF98BCE5),
|
|
Color(0xFFA7B5C5),
|
|
Color(0xFFBED3EC),
|
|
Color(0xFFD4E3F4),
|
|
];
|
|
Map<String, Color> groupColorMap = {};
|
|
int colorIndex = 0;
|
|
|
|
// for (String group in groupByValues) {
|
|
// // groupColorMap[group] = uniqueColorsLine_trend_2[
|
|
// // colorIndex % uniqueColorsLine_trend_2.length];
|
|
// groupColorMap[group] = uniqueColors[colorIndex % uniqueColors.length];
|
|
// // groupColorMap[group] = adjustColorlineTrend(bodyColor, colorIndex);
|
|
// colorIndex++;
|
|
// }
|
|
print('LnTrnd1');
|
|
|
|
// Extract all years from the chart data
|
|
List<int> years =
|
|
(chartData['response'] as List<dynamic>)
|
|
.map<int>(
|
|
(entry) =>
|
|
int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0,
|
|
)
|
|
.toList();
|
|
// Sort in ascending order
|
|
years.sort();
|
|
|
|
// Sort in descending order
|
|
// years.sort((a, b) => b.compareTo(a));
|
|
|
|
print(years);
|
|
|
|
print('years $years');
|
|
|
|
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<dynamic> filteredData =
|
|
(chartData['response'] as List<dynamic>).where((entry) {
|
|
int year =
|
|
int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0;
|
|
return year >= minYear && year <= maxYear;
|
|
}).toList();
|
|
print('LnTrnd2 $filteredData');
|
|
|
|
// Set<double> uniqueXValues = filteredData
|
|
// .map<double>(
|
|
// (entry) => double.parse(entry['ObsKey']['TIME_PERIOD']))
|
|
// .toSet();
|
|
|
|
Set<String> uniqueXValues =
|
|
filteredData.map<String>((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<String, int> 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<String> 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<double> uniqueXValuesProcessed =
|
|
uniqueXValues
|
|
.where((value) => value.isNotEmpty) // Remove any empty strings
|
|
.map(parseTimePeriod)
|
|
.toSet();
|
|
print('Processed X Values: $uniqueXValuesProcessed');
|
|
|
|
print('LnTrnd2.1');
|
|
|
|
var chartBarColors = chartData['chart_bar_color'] ?? {};
|
|
Map<String, String> barColorsMap = {};
|
|
chartBarColors.forEach((key, value) {
|
|
barColorsMap[key] = value.toString(); // Ensure values are strings
|
|
});
|
|
bool hasBarColors = barColorsMap.isNotEmpty;
|
|
|
|
var chartBarColorsRaw = chartData['chart_bar_color'] ?? {};
|
|
|
|
// Convert LinkedMap<dynamic, dynamic> to Map<String, String>
|
|
Map<String, String> chartBarColrs = Map<String, String>.from(
|
|
chartBarColorsRaw,
|
|
);
|
|
|
|
// Now map to Color
|
|
Map<String, Color> parsedChartBarColors = chartBarColrs.map((
|
|
key,
|
|
value,
|
|
) {
|
|
return MapEntry(key, _hexToColor(value));
|
|
});
|
|
|
|
print('parsedChartBarColors1 - $parsedChartBarColors');
|
|
|
|
for (String group in groupByValues) {
|
|
if (hasBarColors && chartData['dataset'] == 'population') {
|
|
groupColorMap[group] =
|
|
parsedChartBarColors[group]!; // Directly assign as Color
|
|
} else {
|
|
groupColorMap[group] =
|
|
uniqueColors[colorIndex % uniqueColors.length];
|
|
}
|
|
colorIndex++;
|
|
}
|
|
|
|
// Generate line bars for the chart
|
|
List<LineChartBarData> lineBars = lineBarsData(
|
|
filteredData,
|
|
groupByValues,
|
|
groupByKey,
|
|
parsedChartBarColors,
|
|
);
|
|
|
|
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(
|
|
// showingTooltipIndicators: showingTooltipOnSpots.map((index) {
|
|
// return ShowingTooltipIndicators([
|
|
// LineBarSpot(
|
|
// tooltipsOnBar,
|
|
// lineBarsData.indexOf(tooltipsOnBar),
|
|
// tooltipsOnBar.spots[index],
|
|
// ),
|
|
// ]);
|
|
// }).toList(),
|
|
lineTouchData: lineTouchData1(),
|
|
gridData: gridData(),
|
|
// titlesData: titlesData1(uniqueXValues),
|
|
titlesData: titlesData1(uniqueXValuesProcessed),
|
|
borderData: borderData(),
|
|
lineBarsData: lineBars,
|
|
// minY: 10000,
|
|
// 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<Color> uniqueColorsLine_trend_2 = [
|
|
Color(0xFF6097CD),
|
|
Color(0xFFD086A7),
|
|
];
|
|
Set<int> selectedYears = {1970, 1980, 1990, 2000, 2010, 2020};
|
|
Map<String, Color> groupColorMap = {};
|
|
int colorIndex = 0;
|
|
// for (String group in groupByValues) {
|
|
// // groupColorMap[group] = uniqueColors[colorIndex % uniqueColors.length];
|
|
// groupColorMap[group] = uniqueColorsLine_trend_2[
|
|
// colorIndex % uniqueColorsLine_trend_2.length];
|
|
// colorIndex++;
|
|
// }
|
|
|
|
// Extract all years from the chart data
|
|
List<int> years =
|
|
(chartData['response'] as List<dynamic>)
|
|
.map<int>(
|
|
(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<dynamic> filteredData =
|
|
(chartData['response'] as List<dynamic>).where((entry) {
|
|
int year =
|
|
int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0;
|
|
return selectedYears.contains(year);
|
|
}).toList();
|
|
|
|
Set<double> uniqueXValues =
|
|
filteredData
|
|
.map<double>(
|
|
(entry) => double.parse(entry['ObsKey']['TIME_PERIOD']),
|
|
)
|
|
.toSet();
|
|
|
|
var chartBarColors = chartData['chart_bar_color'] ?? {};
|
|
Map<String, String> barColorsMap = {};
|
|
|
|
chartBarColors.forEach((key, value) {
|
|
barColorsMap[key] = value.toString(); // Ensure values are strings
|
|
});
|
|
bool hasBarColors = barColorsMap.isNotEmpty;
|
|
// var chartBarColorsRaw = chartData['chart_bar_color'] ?? {};
|
|
// // Convert LinkedMap<dynamic, dynamic> to Map<String, String>
|
|
// Map<String, String> chartBarColrs =
|
|
// Map<String, String>.from(chartBarColorsRaw);
|
|
// // Now map to Color
|
|
// Map<String, Color> parsedChartBarColors =
|
|
// chartBarColrs.map((key, value) {
|
|
// return MapEntry(key, _hexToColor(value));
|
|
// });
|
|
//
|
|
|
|
List<dynamic> keys = chartBarColors['key'] ?? [];
|
|
List<dynamic> colors = chartBarColors['color'] ?? [];
|
|
|
|
print('RT1keys - $keys');
|
|
print('RT1colors - $colors');
|
|
|
|
// Convert to a map for easy lookup
|
|
Map<String, Color> parsedChartBarColors = {};
|
|
for (int i = 0; i < keys.length; i++) {
|
|
parsedChartBarColors[keys[i].toString()] = _hexToColor(
|
|
colors[i].toString(),
|
|
);
|
|
}
|
|
print('RTLinr1 - $parsedChartBarColors');
|
|
|
|
for (String group in groupByValues) {
|
|
if (parsedChartBarColors.containsKey(group)) {
|
|
groupColorMap[group] =
|
|
parsedChartBarColors[group]!; // Directly assign as Color
|
|
} else {
|
|
groupColorMap[group] =
|
|
uniqueColors[colorIndex % uniqueColors.length];
|
|
}
|
|
colorIndex++;
|
|
}
|
|
|
|
// Generate line bars for the chart
|
|
List<LineChartBarData> lineBars = lineBarsData(
|
|
filteredData,
|
|
groupByValues,
|
|
groupByKey,
|
|
parsedChartBarColors,
|
|
);
|
|
|
|
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, top: 10),
|
|
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<Color> uniqueColorsLine_trend_2 = [
|
|
// Color(0xFF6097CD),
|
|
// Color(0xFFD086A7),
|
|
// Color(0xFF98BCE5),
|
|
// Color(0xFFA7B5C5),
|
|
// Color(0xFFBED3EC),
|
|
// Color(0xFFD4E3F4),
|
|
// ];
|
|
Set<int> selectedYears = {1970, 1980, 1990, 2000, 2010, 2020};
|
|
Map<String, Color> groupColorMap = {};
|
|
int colorIndex = 0;
|
|
// for (String group in groupByValues) {
|
|
// // groupColorMap[group] = uniqueColorsLine_trend_2[
|
|
// // colorIndex % uniqueColorsLine_trend_2.length];
|
|
// groupColorMap[group] = uniqueColors[colorIndex % uniqueColors.length];
|
|
// // groupColorMap[group] = adjustColorlineTrend(bodyColor, colorIndex);
|
|
// colorIndex++;
|
|
// }
|
|
|
|
// Extract all years from the chart data
|
|
List<int> years =
|
|
(chartData['response'] as List<dynamic>)
|
|
.map<int>(
|
|
(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<dynamic> filteredData =
|
|
(chartData['response'] as List<dynamic>).where((entry) {
|
|
int year =
|
|
int.tryParse(entry['ObsKey']['TIME_PERIOD'] ?? '0') ?? 0;
|
|
return selectedYears.contains(year);
|
|
}).toList();
|
|
|
|
Set<double> uniqueXValues =
|
|
filteredData
|
|
.map<double>(
|
|
(entry) => double.parse(entry['ObsKey']['TIME_PERIOD']),
|
|
)
|
|
.toSet();
|
|
|
|
print('uniqueXValuesLineTrend2- $uniqueXValues');
|
|
|
|
var chartBarColors = chartData['chart_bar_color'] ?? {};
|
|
Map<String, String> barColorsMap = {};
|
|
chartBarColors.forEach((key, value) {
|
|
barColorsMap[key] = value.toString(); // Ensure values are strings
|
|
});
|
|
bool hasBarColors = barColorsMap.isNotEmpty;
|
|
|
|
var chartBarColorsRaw = chartData['chart_bar_color'] ?? {};
|
|
|
|
// Convert LinkedMap<dynamic, dynamic> to Map<String, String>
|
|
Map<String, String> chartBarColrs = Map<String, String>.from(
|
|
chartBarColorsRaw,
|
|
);
|
|
|
|
// Now map to Color
|
|
Map<String, Color> parsedChartBarColors = chartBarColrs.map((
|
|
key,
|
|
value,
|
|
) {
|
|
return MapEntry(key, _hexToColor(value));
|
|
});
|
|
|
|
for (String group in groupByValues) {
|
|
if (parsedChartBarColors.containsKey(group)) {
|
|
groupColorMap[group] =
|
|
parsedChartBarColors[group]!; // Directly assign as Color
|
|
} else {
|
|
groupColorMap[group] =
|
|
uniqueColors[colorIndex % uniqueColors.length];
|
|
}
|
|
colorIndex++;
|
|
}
|
|
|
|
// Generate line bars for the chart
|
|
List<LineChartBarData> 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<String> xAxisData = [];
|
|
List<double> yAxisData = [];
|
|
List<String> 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');
|
|
|
|
//Handled For Quarter Chart Case (sort by Year and Quarter)
|
|
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: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
double chartWidth =
|
|
xAxisData.length * (40 + 10); // Compute chart width
|
|
double screenWidth = constraints.maxWidth;
|
|
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.only(top: 10, left: 5),
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
minWidth:
|
|
screenWidth, // Ensure it at least fills available width
|
|
maxWidth:
|
|
chartWidth > screenWidth
|
|
? chartWidth
|
|
: screenWidth, // Prevent non-normalized constraints
|
|
),
|
|
child: Center(
|
|
child: SizedBox(
|
|
width:
|
|
xAxisData.length *
|
|
(40 + 10), // Bar width + manual spacing
|
|
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: 75.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
|
|
.black, // Change background color
|
|
// 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.right,
|
|
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,
|
|
barsSpace: 10,
|
|
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<String> xAxisData = [];
|
|
List<double> yAxisData = [];
|
|
List<Map<String, dynamic>> sortedData = [];
|
|
var chartConversion = chartData['chart_type_json']['conversion'];
|
|
var number_format = chartData['chart_type_json']['number_format'];
|
|
|
|
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<Map<String, dynamic>> 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,
|
|
maxY: _calculateMaxY(chartData),
|
|
rotationQuarterTurns: rotationTurns,
|
|
barTouchData: BarTouchData(
|
|
enabled: false,
|
|
handleBuiltInTouches: false,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
fitInsideHorizontally: true,
|
|
fitInsideVertically: true,
|
|
tooltipPadding: const EdgeInsets.all(5),
|
|
tooltipMargin: 12,
|
|
getTooltipColor: (group) => Colors.transparent,
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
if (rod.toY == 0) return null;
|
|
|
|
return BarTooltipItem(
|
|
// formatNumber(rod.toY),
|
|
(chartConversion != null &&
|
|
chartConversion.isNotEmpty &&
|
|
number_format != null)
|
|
? formatNumberConversion(
|
|
rod.toY,
|
|
chartConversion,
|
|
number_format,
|
|
)
|
|
: formatNumber(rod.toY),
|
|
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: 10),
|
|
softWrap: true,
|
|
maxLines: 2,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
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: 20,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY:
|
|
(yAxisData[index] == 0)
|
|
? 0.0001
|
|
: yAxisData[index],
|
|
// color: Colors.blueAccent,
|
|
color: uniqueColors[1],
|
|
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
|
|
}
|
|
var chartConversion = chartData['chart_type_json']['conversion'];
|
|
var number_format = chartData['chart_type_json']['number_format'];
|
|
var chartBarColors = chartData['chart_bar_color'] ?? {};
|
|
Map<String, String> barColorsMap = {};
|
|
chartBarColors.forEach((key, value) {
|
|
barColorsMap[key] = value.toString(); // Ensure values are strings
|
|
});
|
|
bool hasBarColors = barColorsMap.isNotEmpty;
|
|
|
|
var chartBarColorsRaw = chartData['chart_bar_color'] ?? {};
|
|
|
|
// Convert LinkedMap<dynamic, dynamic> to Map<String, String>
|
|
Map<String, String> chartBarColrs = Map<String, String>.from(
|
|
chartBarColorsRaw,
|
|
);
|
|
|
|
// Now map to Color
|
|
Map<String, Color> parsedChartBarColors = chartBarColrs.map((
|
|
key,
|
|
value,
|
|
) {
|
|
return MapEntry(key, _hexToColor(value));
|
|
});
|
|
|
|
if (kDebugMode) {
|
|
print('Converted barColorsMap: $barColorsMap');
|
|
print('hasBarColors: $hasBarColors');
|
|
}
|
|
|
|
print('multi_bar');
|
|
if (chartData.containsKey('chart_bar_color')) {
|
|
print('bar_colors - $chartBarColors');
|
|
}
|
|
if (kDebugMode) {
|
|
print('chartBarColors: $chartBarColors');
|
|
print('chartBarColors Type: ${chartBarColors.runtimeType}');
|
|
print('chartBarColors keys: ${chartBarColors?.keys}');
|
|
print('chartBarColors is null? ${chartBarColors == null}');
|
|
print('chartBarColors is empty? ${chartBarColors?.isEmpty}');
|
|
}
|
|
|
|
String cropKey = chartData['chart_type_json']['x_sub_group'];
|
|
// Group crops by CROP_TYPE
|
|
Map<String, List<String>> 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
|
|
barGroups:
|
|
hasBarColors
|
|
? _buildHorizontalRotateBarGroupsBarColors(
|
|
chartData,
|
|
groupByValues,
|
|
parsedChartBarColors,
|
|
)
|
|
: _buildHorizontalRotateBarGroups(
|
|
chartData,
|
|
groupByValues,
|
|
),
|
|
|
|
titlesData: FlTitlesData(
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize:
|
|
130, // 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: 95.0,
|
|
),
|
|
child: SizedBox(
|
|
width:
|
|
80, // Limit width to force wrapping
|
|
child: Transform.rotate(
|
|
angle: -1.5,
|
|
child: TooltipTheme(
|
|
data: TooltipThemeData(
|
|
decoration: BoxDecoration(
|
|
color:
|
|
Colors
|
|
.black, // Change background color
|
|
// 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,
|
|
softWrap: true,
|
|
maxLines: 2,
|
|
textAlign: TextAlign.right,
|
|
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(
|
|
enabled: true,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
getTooltipColor: (group) => Colors.black,
|
|
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 (rod.toY == 0) {
|
|
return null; // Don't show for zero values
|
|
}
|
|
|
|
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<String>? 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;
|
|
|
|
return BarTooltipItem(
|
|
'$groupLabel\n$crop',
|
|
const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
children: [
|
|
TextSpan(
|
|
text:
|
|
'- ${(chartConversion != null && chartConversion.isNotEmpty && number_format != null) ? formatNumberConversion(rod.toY, chartConversion, number_format) : formatNumber(rod.toY)}',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
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<String, List<String>> 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);
|
|
|
|
var chartBarColors = chartData['chart_bar_color'] ?? {};
|
|
Map<String, String> barColorsMap = {};
|
|
// chartBarColors.forEach((key, value) {
|
|
// barColorsMap[key] = value.toString(); // Ensure values are strings
|
|
// });
|
|
// bool hasBarColors = barColorsMap.isNotEmpty;
|
|
|
|
var chartBarColorsRaw = chartData['chart_bar_color'] ?? {};
|
|
|
|
// // Convert LinkedMap<dynamic, dynamic> to Map<String, String>
|
|
// Map<String, String> chartBarColrs =
|
|
// Map<String, String>.from(chartBarColorsRaw);
|
|
//
|
|
// // Now map to Color
|
|
// Map<String, Color> parsedChartBarColors =
|
|
// chartBarColrs.map((key, value) {
|
|
// print('HorizontalLegends1 -$key');
|
|
// return MapEntry(key, _hexToColor(value));
|
|
// });
|
|
|
|
List<dynamic> keys = chartBarColors['key'] ?? [];
|
|
List<dynamic> colors = chartBarColors['color'] ?? [];
|
|
|
|
print('RT1keys - $keys');
|
|
print('RT1colors - $colors');
|
|
|
|
// Convert to a map for easy lookup
|
|
Map<String, Color> parsedChartBarColors = {};
|
|
for (int i = 0; i < keys.length; i++) {
|
|
parsedChartBarColors[keys[i].toString()] = _hexToColor(
|
|
colors[i].toString(),
|
|
);
|
|
}
|
|
print('RT1 - $parsedChartBarColors');
|
|
bool hasBarColors = parsedChartBarColors.isNotEmpty;
|
|
|
|
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,
|
|
maxY: _calculateMaxY(chartData),
|
|
rotationQuarterTurns: rotationTurns,
|
|
|
|
// barTouchData: BarTouchData(
|
|
// // enabled: true,
|
|
// enabled: false,
|
|
// touchTooltipData: BarTouchTooltipData(
|
|
// // getTooltipColor: (group) => Colors.black12,
|
|
// getTooltipColor: (group) => Colors.transparent,
|
|
// tooltipHorizontalAlignment: FLHorizontalAlignment.center,
|
|
// tooltipRoundedRadius: 8,
|
|
// fitInsideHorizontally:
|
|
// true, // Ensure it fits within the screen
|
|
// fitInsideVertically: true,
|
|
// tooltipPadding: const EdgeInsets.all(5),
|
|
// tooltipMargin: 10,
|
|
//
|
|
// // 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(
|
|
// // // '$groupLabel\n$crop',
|
|
// // '$formattedValue',
|
|
// // const TextStyle(
|
|
// // color: Colors.black,
|
|
// // fontWeight: FontWeight.w400,
|
|
// // fontSize: 12,
|
|
// // ),
|
|
// // );
|
|
//
|
|
// return BarTooltipItem(
|
|
// ' $formattedValue', // This is your main tooltip text
|
|
// // textAlign: TextAlign.center,
|
|
// const TextStyle(
|
|
// color: Colors.black,
|
|
// fontWeight: FontWeight.w400,
|
|
// fontSize: 12,
|
|
// ),
|
|
// children: [
|
|
// TextSpan(
|
|
// text: '', // Add extra information here
|
|
// // text: '\n$crop', // Add extra information here
|
|
//
|
|
// style: TextStyle(
|
|
// color: Colors.grey[
|
|
// 700], // Optional: Different color for extra text
|
|
// fontSize: 8,
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// );
|
|
//
|
|
// // 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
|
|
// // // });
|
|
// // }
|
|
// // },
|
|
// ),
|
|
barTouchData: BarTouchData(
|
|
enabled: false, // Enable touch to show tooltip
|
|
handleBuiltInTouches: false,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
getTooltipColor:
|
|
(group) =>
|
|
Colors
|
|
.transparent, // Light background for visibility
|
|
tooltipHorizontalAlignment:
|
|
FLHorizontalAlignment.center,
|
|
tooltipRoundedRadius:
|
|
6, // Softer roundness for tooltips
|
|
fitInsideHorizontally: true,
|
|
fitInsideVertically: true,
|
|
tooltipPadding: const EdgeInsets.all(5),
|
|
tooltipMargin:
|
|
20, // Reduce margin for better alignment
|
|
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
// if (rod.toY == 0) return null; // Hide for zero values
|
|
|
|
String cropType = groupByValues.elementAt(
|
|
groupIndex,
|
|
);
|
|
String crop = groupedCrops[cropType]![rodIndex];
|
|
double value = rod.toY;
|
|
|
|
// Format values dynamically
|
|
// String formattedValue = value >= 1e6
|
|
// ? '${(value / 1e6).toStringAsFixed(1)}M'
|
|
// : value >= 1e3
|
|
// ? '${(value / 1e3).toStringAsFixed(1)}K'
|
|
// : value.toStringAsFixed(0);
|
|
|
|
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,
|
|
textAlign: TextAlign.right,
|
|
const TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 12,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
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.center,
|
|
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.right,
|
|
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),
|
|
barGroups:
|
|
hasBarColors
|
|
? _buildHorizontalRotateBarGroupsBarColors(
|
|
chartData,
|
|
groupByValues,
|
|
parsedChartBarColors,
|
|
)
|
|
: _buildHorizontalRotateBarGroups(
|
|
chartData,
|
|
groupByValues,
|
|
),
|
|
|
|
alignment: BarChartAlignment.spaceAround,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// ),
|
|
// )
|
|
const SizedBox(height: 10), // Space between chart and legend
|
|
_buildChartLegend(groupedCrops, parsedChartBarColors, uniqueColors),
|
|
],
|
|
);
|
|
|
|
default:
|
|
return Center(child: Text('Unknown chart type'));
|
|
}
|
|
}
|
|
|
|
// Start Of line trend chart
|
|
FlTitlesData titlesData2(Set<double> 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<LineChartBarData> lineBarsData2(List<dynamic> filteredData) {
|
|
List<LineChartBarData> lineBars = [];
|
|
|
|
// Create a map to store population values by year and gender
|
|
Map<int, Map<String, double>> 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<FlSpot> 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: bodyColor?? Colors.grey, // Set color for the ratio line
|
|
color: uniqueColors[1] ?? Colors.grey,
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
// color: bodyColor ,
|
|
// color: (bodyColor ?? Colors.grey).withAlpha(100),
|
|
color: (uniqueColors[1] ?? Colors.grey).withAlpha(100),
|
|
),
|
|
),
|
|
);
|
|
|
|
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<String, int> 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<String> 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<LineChartBarData> lineBarsData(
|
|
List<dynamic> filteredData,
|
|
Set<String> groupByValues,
|
|
String groupByKey,
|
|
Map<String, Color> chartBarColors,
|
|
) {
|
|
List<LineChartBarData> lineBars = [];
|
|
print('LineParsedChartBarColors $chartBarColors');
|
|
|
|
print('filteredData $filteredData');
|
|
print('filteredData222 $groupByValues');
|
|
// final List<Color> 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<String, Color> groupColorMap = {};
|
|
int colorIndex = 0;
|
|
|
|
// for (String group in groupByValues) {
|
|
// groupColorMap[group] = uniqueColors[colorIndex % uniqueColors.length];
|
|
// // groupColorMap[group] = adjustColorlineTrend(bodyColor, colorIndex);
|
|
// colorIndex++;
|
|
// }
|
|
|
|
for (String group in groupByValues) {
|
|
if (chartBarColors.isNotEmpty && chartBarColors.containsKey(group)) {
|
|
// Use predefined color from chartBarColors
|
|
groupColorMap[group] = chartBarColors[group]!;
|
|
} else {
|
|
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<FlSpot> spots = filteredData
|
|
// .where((entry) => entry['ObsKey'][groupByKey] == group)
|
|
// .map<FlSpot>((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<FlSpot> spots = filteredData
|
|
// .where((entry) => entry['ObsKey'][groupByKey] == group)
|
|
// .map<FlSpot>((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<FlSpot> spots =
|
|
filteredData
|
|
.where((entry) => entry['ObsKey'][groupByKey] == group)
|
|
.map<FlSpot>((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 $xValue');
|
|
|
|
// double yValue = double.parse(entry['ObsValue']['Value']);
|
|
double yValue;
|
|
var value = entry['ObsValue']['Value'];
|
|
|
|
print('LineTrend $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}",
|
|
);
|
|
}
|
|
print('LineTrendX $xValue');
|
|
print('LineTrendY $yValue');
|
|
return FlSpot(xValue, yValue);
|
|
})
|
|
.toList()
|
|
..sort((a, b) => a.x.compareTo(b.x));
|
|
|
|
print('spots - $spots');
|
|
|
|
// lineChartLabel = spots;
|
|
|
|
// List<FlSpot> spots = filteredData
|
|
// .where((entry) => entry['ObsKey'][groupByKey] == group)
|
|
// .map<FlSpot>((entry) {
|
|
// String timePeriod = entry['ObsKey']['TIME_PERIOD'];
|
|
// List<String> 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(
|
|
show: true,
|
|
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() {
|
|
var chartConversion = chartData['chart_type_json']['conversion'];
|
|
var number_format = chartData['chart_type_json']['number_format'];
|
|
return LineTouchData(
|
|
touchSpotThreshold: 10,
|
|
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
|
|
getTooltipColor: (spot) => Colors.black,
|
|
getTooltipItems: (List<LineBarSpot> 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:
|
|
(chartConversion != null &&
|
|
chartConversion.isNotEmpty &&
|
|
number_format != null)
|
|
? formatNumberConversion(
|
|
lineBarSpot.y,
|
|
chartConversion,
|
|
number_format,
|
|
)
|
|
: formatNumber(lineBarSpot.y),
|
|
// text: ' ${lineBarSpot.y}', // Keep the value white
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
],
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
handleBuiltInTouches: true,
|
|
);
|
|
}
|
|
|
|
//
|
|
// LineTouchData lineTouchData1() {
|
|
// var chartConversion = chartData['chart_type_json']['conversion'];
|
|
// var number_format = chartData['chart_type_json']['number_format'];
|
|
// return LineTouchData(
|
|
// enabled: true,
|
|
// handleBuiltInTouches: true,
|
|
// touchCallback:
|
|
// (FlTouchEvent event, LineTouchResponse? response) {
|
|
// if (response == null || response.lineBarSpots == null) {
|
|
// return;
|
|
// }
|
|
// // if (event is FlTapUpEvent) {
|
|
// // final spotIndex = response.lineBarSpots!.first.spotIndex;
|
|
// // setState(() {
|
|
// // if (showingTooltipOnSpots.contains(spotIndex)) {
|
|
// // showingTooltipOnSpots.remove(spotIndex);
|
|
// // } else {
|
|
// // showingTooltipOnSpots.add(spotIndex);
|
|
// // }
|
|
// // });
|
|
// // }
|
|
// },
|
|
// mouseCursorResolver:
|
|
// (FlTouchEvent event, LineTouchResponse? response) {
|
|
// if (response == null || response.lineBarSpots == null) {
|
|
// return SystemMouseCursors.basic;
|
|
// }
|
|
// return SystemMouseCursors.click;
|
|
// },
|
|
// touchSpotThreshold: 5,
|
|
// touchTooltipData: LineTouchTooltipData(
|
|
// tooltipRoundedRadius: 0,
|
|
// getTooltipColor: (spot) => Colors.transparent,
|
|
// fitInsideHorizontally: true, // Ensure it fits within the screen
|
|
// fitInsideVertically: true,
|
|
// tooltipPadding: EdgeInsets.all(8),
|
|
// tooltipMargin: 16,
|
|
// getTooltipItems: (List<LineBarSpot> touchedSpots) {
|
|
// return touchedSpots.map((LineBarSpot touchedSpot) {
|
|
// return LineTooltipItem(
|
|
// // formatNumber(touchedSpot.y),
|
|
// (chartConversion != null &&
|
|
// chartConversion.isNotEmpty &&
|
|
// number_format != null)
|
|
// ? formatNumberConversion(touchedSpot.y, chartConversion, number_format)
|
|
// : formatNumber(touchedSpot.y),
|
|
// TextStyle(
|
|
// color: Colors.black87,
|
|
// fontWeight: FontWeight.w400,
|
|
// fontSize: 15,
|
|
// ),
|
|
// );
|
|
// }).toList();
|
|
// },
|
|
// ),
|
|
// getTouchedSpotIndicator: (
|
|
// _,
|
|
// indicators,
|
|
// ) {
|
|
// return indicators
|
|
// .map((int index) => const TouchedSpotIndicatorData(
|
|
// FlLine(color: Colors.transparent),
|
|
// FlDotData(show: true),
|
|
// ))
|
|
// .toList();
|
|
// },
|
|
// distanceCalculator: (Offset touchPoint, Offset spotPixelCoordinates) =>
|
|
// (touchPoint - spotPixelCoordinates).distance,
|
|
// );
|
|
// }
|
|
|
|
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<double> xValues) {
|
|
print('tileDATa1xvalueline2 - $xValues');
|
|
|
|
double findClosest(double value, Set<double> 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;
|
|
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('LINETREND_POP- $formattedValue');
|
|
// // if (int.parse(formattedValue.replaceAll(RegExp(r'[^0-9]'), '')) % 10 != 0) {
|
|
// // return const SizedBox.shrink();
|
|
// // }
|
|
//
|
|
//
|
|
// // print('formattedValueLine - $formattedValue');
|
|
// return Text(
|
|
// formattedValue,
|
|
// textAlign: TextAlign.center,
|
|
// style: TextStyle(
|
|
// fontSize: 10,
|
|
// ));
|
|
//
|
|
// }
|
|
|
|
// Ensure value is a multiple of 10k, 2M, 100B, or 1T before processing
|
|
if (value >= 1000000000000) {
|
|
// Trillions (T)
|
|
if (value % 1000000000000 == 0) {
|
|
// Only multiples of 1T
|
|
formattedValue =
|
|
'${(value / 1000000000000).toStringAsFixed(0)}T';
|
|
}
|
|
} else if (value >= 1000000000) {
|
|
// Billions (B)
|
|
if (value % 100000000000 == 0) {
|
|
// Only multiples of 100B
|
|
formattedValue = '${(value / 1000000000).toStringAsFixed(0)}B';
|
|
}
|
|
} else if (value >= 1000000) {
|
|
// Millions (M)
|
|
if (value % 2000000 == 0) {
|
|
// Only multiples of 2M
|
|
formattedValue = '${(value / 1000000).toStringAsFixed(0)}M';
|
|
}
|
|
} else if (value >= 10000) {
|
|
// Thousands (k)
|
|
if (value % 10000 == 0) {
|
|
// Only multiples of 10k
|
|
formattedValue = '${(value / 1000).toStringAsFixed(0)}k';
|
|
}
|
|
}
|
|
|
|
print(
|
|
'Value: $value | Formatted: ${formattedValue ?? "Hidden"}',
|
|
); // Debugging
|
|
|
|
if (formattedValue != null) {
|
|
return Text(
|
|
formattedValue,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 10),
|
|
);
|
|
}
|
|
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
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: 5, left: 20.0),
|
|
child: SizedBox(
|
|
width: 40,
|
|
child: Transform.rotate(
|
|
// angle: 0.0,
|
|
angle: -1.5, // Slight rotation to improve readability
|
|
child: Text(
|
|
value.toInt().toString(),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.black, fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return SizedBox.shrink(); // Hide non-relevant labels
|
|
}
|
|
},
|
|
reservedSize: 40,
|
|
),
|
|
),
|
|
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<Color> uniqueColors1 = [
|
|
// Color(0xFF648CBA),
|
|
// Color(0xFF90B0D5),
|
|
// Color(0xFF98BCE5),
|
|
// Color(0xFFA7B5C5),
|
|
// Color(0xFFBED3EC),
|
|
// Color(0xFFD4E3F4),
|
|
// ];
|
|
//
|
|
// final List<Color> uniqueColors2 = [
|
|
// Color(0xFFD9BB99),
|
|
// Color(0xFF87766E),
|
|
// Color(0xFFC6A885),
|
|
// Color(0xFFA28565),
|
|
// Color(0xFFBFB29B),
|
|
// Color(0xFFE8D8BB),
|
|
// ];
|
|
//
|
|
// final List<Color> uniqueColors = [
|
|
// Color(0xFF376C79),
|
|
// Color(0xFF578D9C),
|
|
// Color(0xFF7DAFBC),
|
|
// Color(0xFF86C7D9),
|
|
// Color(0xFF989898),
|
|
// ];
|
|
|
|
Color _getColorForGroup(int index) {
|
|
// Use the index to get a color from the uniqueColors list
|
|
return uniqueColors[index % uniqueColors.length];
|
|
}
|
|
|
|
List<BarChartGroupData> _generateBarGroups(
|
|
BuildContext context,
|
|
dynamic chartData,
|
|
String groupByKey,
|
|
) {
|
|
Map<String, Map<String, double>> 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<BarChartGroupData> barGroups = [];
|
|
List<String> 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<BarChartRodStackItem> 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<Widget> _generateXTitles(dynamic chartData) {
|
|
Set<String> 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<BarChartGroupData> _buildBarGroups(dynamic chartData) {
|
|
// String groupByKey = chartData['group_by'];
|
|
// if (groupByKey == null || chartData['response'] == null) {
|
|
// return [];
|
|
// }
|
|
//
|
|
// // Extract unique group_by values
|
|
// Set<String> groupByValues = extractGroupByValues(chartData, groupByKey);
|
|
//
|
|
// // Create a map to hold grouped data
|
|
// Map<String, List<Map<String, dynamic>>> 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<BarChartGroupData> barGroups = [];
|
|
// int groupIndex = 0;
|
|
//
|
|
// groupedData.forEach((key, values) {
|
|
// int colorIndex = 0; // Track the color for each bar within the group
|
|
//
|
|
// List<BarChartRodData> 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<dynamic> 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;
|
|
}
|
|
}
|
|
|
|
// Apply different buffers based on maxY value
|
|
// return maxY < 500 ? maxY * 3.5 : maxY * 1.5;
|
|
return maxY * 1.5; // Add 10% buffer for better visualization
|
|
}
|
|
|
|
List<BarChartGroupData> _buildHorizontalRotateBarGroups(
|
|
dynamic chartData,
|
|
Set<String> groupByValues,
|
|
) {
|
|
String groupByKeyValueData;
|
|
|
|
if (chartData['chart_type_json']['chart_type'] == 'bar_chart_horizontal' ||
|
|
chartData['chart_type_json']['chart_type'] == 'horizontal_rotate') {
|
|
groupByKeyValueData = chartData['chart_type_json']['y_group'] ?? '';
|
|
} else {
|
|
groupByKeyValueData = chartData['chart_type_json']['x_group'] ?? '';
|
|
}
|
|
|
|
List<dynamic> responseData = chartData['response'];
|
|
List<BarChartGroupData> 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<dynamic> groupData =
|
|
responseData.where((entry) {
|
|
return entry['ObsKey'][groupByKeyValueData] == groupValue;
|
|
}).toList();
|
|
|
|
// Create BarChartRodData for each bar in the group
|
|
List<BarChartRodData> 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;
|
|
// double minBarHeight = 0.5;
|
|
print('multi_bar1.1');
|
|
// final List<Color> uniqueColorsForTwo = [
|
|
// Color(0xFF648CBA),
|
|
// Color(0xFF90B0D5),
|
|
// ];
|
|
|
|
// Use bodyColor instead of hardcoded colors
|
|
|
|
final barColor;
|
|
if (chartData['dataset'] == 'general_education' ||
|
|
chartData['dataset'] == 'higher_education' ||
|
|
chartData['dataset'] == 'air_transport' ||
|
|
chartData['dataset'] == 'labour_force' ||
|
|
chartData['dataset'] == 'gdp' ||
|
|
chartData['dataset'] == 'hotels' ||
|
|
chartData['dataset'] == 'hotel_guests' ||
|
|
chartData['dataset'] == 'health_services' ||
|
|
chartData['dataset'] == 'clinics') {
|
|
barColor =
|
|
uniqueColorsForTwo[colorIndex % uniqueColorsForTwo.length];
|
|
colorIndex++;
|
|
} else if (chartData['dataset'] == 'cropsk') {
|
|
barColor =
|
|
uniqueColorsForThree[colorIndex %
|
|
uniqueColorsForThree.length];
|
|
colorIndex++;
|
|
} else if (chartData['dataset'] == 'oil_and_gas') {
|
|
barColor =
|
|
uniqueColorsForFour[colorIndex % uniqueColorsForFour.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;
|
|
}
|
|
|
|
List<BarChartGroupData> _buildHorizontalRotateBarGroupsBarColors(
|
|
dynamic chartData,
|
|
Set<String> groupByValues,
|
|
Map<String, Color> chartBarColors,
|
|
) {
|
|
String groupByKeyValueData1;
|
|
|
|
if (chartData['chart_type_json']['chart_type'] == 'bar_chart_horizontal' ||
|
|
chartData['chart_type_json']['chart_type'] == 'horizontal_rotate') {
|
|
groupByKeyValueData1 = chartData['chart_type_json']['y_group'] ?? '';
|
|
} else {
|
|
groupByKeyValueData1 = chartData['chart_type_json']['x_group'] ?? '';
|
|
}
|
|
|
|
List<dynamic> responseData = chartData['response'];
|
|
List<BarChartGroupData> barGroups = [];
|
|
|
|
print('chartBarColorsRota- $chartBarColors');
|
|
|
|
// Define two shades for alternating colors
|
|
List<Color> uniqueColorsForTwo = [
|
|
chartBarColors['default'] ?? Colors.blue, // Original color
|
|
(chartBarColors['default'] ?? Colors.blue).withAlpha(
|
|
180,
|
|
), // Slightly modified shade
|
|
];
|
|
|
|
int colorIndex = 0;
|
|
// 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
|
|
List<dynamic> groupData =
|
|
responseData.where((entry) {
|
|
return entry['ObsKey'][groupByKeyValueData1] == groupValue;
|
|
}).toList();
|
|
|
|
Map<String, String> genderMapping = {
|
|
'ذكر': 'Male', // Arabic for Male
|
|
'أنثى': 'Female', // Arabic for Female
|
|
};
|
|
|
|
// Sort data so "Male" appears first, "Female" second
|
|
groupData.sort((a, b) {
|
|
String genderA =
|
|
genderMapping[a['ObsKey']['GENDER']] ?? a['ObsKey']['GENDER'] ?? "";
|
|
String genderB =
|
|
genderMapping[b['ObsKey']['GENDER']] ?? b['ObsKey']['GENDER'] ?? "";
|
|
|
|
return (genderA == 'Male' ? 0 : 1).compareTo(genderB == 'Male' ? 0 : 1);
|
|
});
|
|
|
|
// Create BarChartRodData for each bar in the group
|
|
List<BarChartRodData> barRods =
|
|
groupData.map((data) {
|
|
double value =
|
|
double.tryParse(data['ObsValue']['Value'].toString()) ?? 0.0;
|
|
|
|
// Get gender-based color
|
|
String gender = data['ObsKey']['GENDER'] ?? "";
|
|
// Convert Arabic gender to English if necessary
|
|
String normalizedGender = genderMapping[gender] ?? gender;
|
|
|
|
print('horizontalRotateGender - $gender');
|
|
// Map Arabic gender values to English equivalents
|
|
|
|
// Determine color dynamically
|
|
Color barColor;
|
|
if (chartData['dataset'] == 'health_services') {
|
|
barColor =
|
|
uniqueColorsForTwo[colorIndex %
|
|
2]; // Alternate between two colors
|
|
colorIndex++; // Update index for next bar
|
|
} else {
|
|
// barColor = chartBarColors[gender] ?? Colors.grey; // Default gender-based color
|
|
barColor = chartBarColors[normalizedGender] ?? Colors.grey;
|
|
}
|
|
|
|
print('GEG- $gender');
|
|
|
|
return BarChartRodData(
|
|
toY: value, // Use the parsed value
|
|
color: barColor, // Gender-based dynamic color
|
|
width: 20,
|
|
);
|
|
}).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);
|
|
}
|
|
}
|
|
|
|
Widget _buildChartLegend(
|
|
Map<String, List<String>> groupedCrops,
|
|
Map<String, Color> parsedChartBarColors,
|
|
List<Color> uniqueColors,
|
|
) {
|
|
// Extract unique crop types
|
|
Set<String> uniqueCropTypes =
|
|
groupedCrops.values.expand((list) => list).toSet();
|
|
|
|
print('HorizontalLegends2 -$uniqueCropTypes');
|
|
print('HorizontalLegends -$parsedChartBarColors');
|
|
|
|
return Wrap(
|
|
alignment: WrapAlignment.center,
|
|
spacing: 12,
|
|
runSpacing: 6,
|
|
children:
|
|
uniqueCropTypes.map((cropType) {
|
|
// Color cropColor = parsedChartBarColors[cropType] ?? Colors.grey; // Fetch color for each crop type
|
|
int index = uniqueCropTypes.toList().indexOf(
|
|
cropType,
|
|
); // Get index for cycling colors
|
|
Color cropColor =
|
|
parsedChartBarColors.isNotEmpty &&
|
|
parsedChartBarColors.containsKey(cropType)
|
|
? parsedChartBarColors[cropType]!
|
|
: uniqueColors[index % uniqueColors.length];
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircleAvatar(radius: 6, backgroundColor: cropColor),
|
|
const SizedBox(width: 4),
|
|
Text(cropType, style: const TextStyle(fontSize: 12)),
|
|
],
|
|
);
|
|
}).toList(),
|
|
);
|
|
}
|
|
|
|
class BarValuePainter extends CustomPainter {
|
|
BarValuePainter(this.barGroups, this.constraints);
|
|
final List<BarChartGroupData> 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;
|
|
}
|