2740 lines
102 KiB
Dart
2740 lines
102 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})
|
|
: super(key: key);
|
|
final dynamic chartData;
|
|
final Color bodyColor;
|
|
|
|
String capitalizeAndSplit(String input) {
|
|
return input
|
|
.split('_')
|
|
.map((word) => word[0].toUpperCase() + word.substring(1))
|
|
.join(' ');
|
|
}
|
|
|
|
String formatNumber(double value) {
|
|
if (value >= 1e12) {
|
|
return '${(value / 1e12).toStringAsFixed(2)}T';
|
|
} else if (value >= 1e9) {
|
|
return '${(value / 1e9).toStringAsFixed(2)}B';
|
|
} else if (value >= 1e6) {
|
|
return '${(value / 1e6).toStringAsFixed(2)}M';
|
|
} else if (value >= 1e3) {
|
|
return '${(value / 1e3).toStringAsFixed(2)}K';
|
|
} else {
|
|
return value.toStringAsFixed(2); // No decimals for small numbers
|
|
}
|
|
}
|
|
|
|
String formatNumberConversion(
|
|
double value, String chartConversion, String numberFormat,) {
|
|
// Convert the input value to base unit (actual value)
|
|
double baseValue = value;
|
|
|
|
if (numberFormat == 'M') {
|
|
baseValue = value * 1e6; // Convert from Million to base value
|
|
} else if (numberFormat == 'B') {
|
|
baseValue = value * 1e9; // Convert from Billion to base value
|
|
} else if (numberFormat == 'T') {
|
|
baseValue = value * 1e12; // Convert from Trillion to base value
|
|
} else if (numberFormat == 'K') {
|
|
baseValue = value * 1e3; // Convert from Thousand to base value
|
|
}
|
|
|
|
// Convert base value to the target chartConversion format
|
|
if (chartConversion == 'T') {
|
|
return '${(baseValue / 1e12).toStringAsFixed(2)}T'; // Convert to Trillion
|
|
} else if (chartConversion == 'B') {
|
|
return '${(baseValue / 1e9).toStringAsFixed(2)}B'; // Convert to Billion
|
|
} else if (chartConversion == 'M') {
|
|
return '${(baseValue / 1e6).toStringAsFixed(2)}M'; // Convert to Million
|
|
} else if (chartConversion == 'K') {
|
|
return '${(baseValue / 1e3).toStringAsFixed(2)}K'; // Convert to Thousand
|
|
} else {
|
|
return baseValue
|
|
.toStringAsFixed(2); // Default: Show value with 2 decimals
|
|
}
|
|
}
|
|
|
|
List<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 adjustColor(Color baseColor, int index) {
|
|
final HSLColor hsl = HSLColor.fromColor(baseColor);
|
|
|
|
// Lightness variation (keeping it light, between 0.6 and 0.9)
|
|
double lightnessFactor = 0.6 + (index % 4) * 0.1; // Varies between 0.6 to 0.8
|
|
final HSLColor adjustedHSL = hsl.withLightness(
|
|
lightnessFactor.clamp(0.6, 0.9), // Ensures light shades only
|
|
);
|
|
|
|
return adjustedHSL.toColor();
|
|
}
|
|
|
|
List<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
|
|
title: '${percentage.toStringAsFixed(1)}%',
|
|
radius: isTouched ? 60 : 50,
|
|
|
|
// Increase size when touched
|
|
titleStyle: TextStyle(
|
|
fontSize: isTouched ? 12 : 10,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black,
|
|
// color: Colors.white,
|
|
),
|
|
titlePositionPercentageOffset: 1.3,
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
List<Widget> generateIndicators(dynamic chartData, String groupByValue) {
|
|
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'][groupByValue] ?? '';
|
|
String shortTitle =
|
|
title.length > 10 ? '${title.substring(0, 10)}…' : title;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: adjustColor(bodyColor, index),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
TooltipTheme(
|
|
data: TooltipThemeData(
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueGrey[800], // Change background color
|
|
borderRadius:
|
|
BorderRadius.circular(8), // Optional: rounded corners
|
|
),
|
|
textStyle: TextStyle(color: Colors.white), // Change text color
|
|
),
|
|
child: Tooltip(
|
|
message: title, // Full text on hover
|
|
child: ConstrainedBox(
|
|
// Constrain width to allow wrapping
|
|
constraints: BoxConstraints(maxWidth: 200),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
),
|
|
// softWrap: true,
|
|
// maxLines: 2,
|
|
overflow: TextOverflow.ellipsis, // Ensures text doesn't overflow
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 5),
|
|
],
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Set<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();
|
|
}
|
|
|
|
Widget buildChart(dynamic chartData, BuildContext context) {
|
|
// print('bodyColor- $bodyColor');
|
|
print('chartDataccccccc $chartData');
|
|
// Extract group_by dynamically from the chartData
|
|
if (chartData == null || chartData['response'] == null) {
|
|
return Center(child: Text('No chart data available'));
|
|
}
|
|
|
|
print(chartData['chart_type_json']['TIME_PERIOD']);
|
|
|
|
String groupByKey = chartData['group_by'] ?? '';
|
|
print('groupByKey $groupByKey');
|
|
Set<String> groupByValues = extractGroupByValues(chartData, groupByKey);
|
|
print('groupByValues $groupByValues');
|
|
|
|
switch (chartData['chart_type']) {
|
|
// case 'stacked_bar':
|
|
// String chartTitle =
|
|
// capitalizeAndSplit(chartData['chart_heading'] ?? '');
|
|
// // Define a list of colors
|
|
// final List<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: 45),
|
|
Expanded(
|
|
// child: SingleChildScrollView(
|
|
// scrollDirection: Axis.horizontal,
|
|
//
|
|
|
|
// child: SingleChildScrollView(
|
|
// scrollDirection: Axis.vertical,
|
|
child: Center(
|
|
child: Container(
|
|
width: double.infinity,
|
|
constraints:
|
|
BoxConstraints(minHeight: 3), // Allow dynamic height
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 2.0, right: 2.0, bottom: 8.0, top: 1.0),
|
|
child: Column( // Change Row to Column
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: generateIndicators(chartData, chartData['group_by']),
|
|
),
|
|
|
|
|
|
// child: Wrap(
|
|
// spacing: 2,
|
|
// runSpacing: 5,
|
|
// children:
|
|
// generateIndicators(chartData, chartData['group_by']),
|
|
// ),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// ),
|
|
],
|
|
);
|
|
case 'fl_stacked_bar':
|
|
// Divide groupByValuesList into chunks of 5 items per row
|
|
List<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,
|
|
|
|
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.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];
|
|
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();
|
|
|
|
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');
|
|
|
|
// 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');
|
|
|
|
// Generate line bars for the chart
|
|
List<LineChartBarData> lineBars =
|
|
lineBarsData(filteredData, groupByValues, groupByKey);
|
|
|
|
print('LnTrnd3');
|
|
|
|
return Column(children: [
|
|
Text(
|
|
chartData['chart_heading'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 5),
|
|
Text(
|
|
chartData['chart_sub_heading'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w300,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 10),
|
|
// Chart
|
|
Expanded(
|
|
child: LineChart(LineChartData(
|
|
lineTouchData: lineTouchData1(),
|
|
gridData: gridData(),
|
|
// titlesData: titlesData1(uniqueXValues),
|
|
titlesData: titlesData1(uniqueXValuesProcessed),
|
|
borderData: borderData(),
|
|
lineBarsData: lineBars,
|
|
// minX: uniqueXValues.reduce((a, b) => a < b ? a : b),
|
|
// maxX: uniqueXValues.reduce((a, b) => a > b ? a : b),
|
|
// minX: uniqueXValuesProcessed.reduce((a, b) => a < b ? a : b),
|
|
// maxX: uniqueXValuesProcessed.reduce((a, b) => a > b ? a : b),
|
|
))),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Wrap(
|
|
spacing: 12,
|
|
runSpacing: 8,
|
|
children: groupByValues.map((group) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 12,
|
|
height: 12,
|
|
color: groupColorMap[group],
|
|
),
|
|
SizedBox(width: 6),
|
|
Text(
|
|
group,
|
|
style: TextStyle(fontSize: 14),
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
]);
|
|
case 'line_trend_population':
|
|
final List<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];
|
|
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();
|
|
|
|
// Generate line bars for the chart
|
|
List<LineChartBarData> lineBars =
|
|
lineBarsData(filteredData, groupByValues, groupByKey);
|
|
|
|
return Column(children: [
|
|
Text(
|
|
chartData['chart_heading'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 5),
|
|
Text(
|
|
chartData['chart_sub_heading'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w300,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 10),
|
|
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal, // Enable horizontal scrolling
|
|
padding: const EdgeInsets.only(right: 40),
|
|
child: SizedBox(
|
|
width: (uniqueXValues.length * 50) +
|
|
50, // Adjust width dynamically
|
|
child: LineChart(LineChartData(
|
|
lineTouchData: lineTouchData1(),
|
|
gridData: gridData(),
|
|
titlesData: titlesData1(uniqueXValues),
|
|
borderData: borderData(),
|
|
lineBarsData: lineBars,
|
|
minX: uniqueXValues.reduce((a, b) => a < b ? a : b),
|
|
maxX: uniqueXValues.reduce((a, b) => a > b ? a : b),
|
|
)),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Wrap(
|
|
spacing: 12,
|
|
runSpacing: 8,
|
|
children: groupByValues.map((group) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 12,
|
|
height: 12,
|
|
color: groupColorMap[group],
|
|
),
|
|
SizedBox(width: 6),
|
|
Text(
|
|
group,
|
|
style: TextStyle(fontSize: 14),
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
),
|
|
)
|
|
// Chart
|
|
]);
|
|
case 'line_trend_2':
|
|
final List<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];
|
|
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');
|
|
|
|
// 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');
|
|
|
|
if (xadditionalgrp == 'TIME_PERIOD') {
|
|
chartData['response'].sort((a, b) {
|
|
// Convert TIME_PERIOD to int for proper sorting
|
|
int timeA =
|
|
int.tryParse(a['ObsKey']['TIME_PERIOD'].toString()) ?? 0;
|
|
int timeB =
|
|
int.tryParse(b['ObsKey']['TIME_PERIOD'].toString()) ?? 0;
|
|
|
|
// Extract QUARTER and convert "Q1", "Q2", etc. to numeric values
|
|
int quarterA = int.tryParse(
|
|
a['ObsKey']['QUARTER'].toString().replaceAll('Q', '')) ??
|
|
0;
|
|
int quarterB = int.tryParse(
|
|
b['ObsKey']['QUARTER'].toString().replaceAll('Q', '')) ??
|
|
0;
|
|
|
|
// First, sort by TIME_PERIOD. If equal, sort by QUARTER
|
|
if (timeA != timeB) {
|
|
return timeA.compareTo(timeB);
|
|
} else {
|
|
return quarterA.compareTo(quarterB);
|
|
}
|
|
});
|
|
}
|
|
|
|
// if (xValue != null && yValue != null) {
|
|
// xAxisData.add(xValue.toString());
|
|
// yAxisData.add(double.tryParse(yValue.toString()) ?? 0.0);
|
|
// }
|
|
if (xValue != null && yValue != null) {
|
|
// Check if additional_x_group is "Timeperiod" and concatenate
|
|
String xLabel = xValue.toString();
|
|
if (xadditionalgroup == 'TIME_PERIOD' && xTimePeriod != null) {
|
|
xLabel = '$xTimePeriod-$xValue'; // Format: Label (TimePeriod)
|
|
}
|
|
|
|
xAxisData.add(xLabel);
|
|
|
|
yAxisData.add(double.tryParse(yValue.toString()) ?? 0.0);
|
|
// yAxisLabels.add(unitMsr ?? '');
|
|
}
|
|
}
|
|
return Column(children: [
|
|
Text(
|
|
chartData['chart_heading'] ?? '', // Chart title from data
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 5),
|
|
Text(
|
|
chartData['chart_sub_heading'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w300,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 10),
|
|
// Chart
|
|
Expanded(
|
|
child: BarChart(
|
|
BarChartData(
|
|
alignment: BarChartAlignment.spaceAround,
|
|
maxY: yAxisData.isNotEmpty
|
|
? yAxisData.reduce((a, b) => a > b ? a : b) * 1.2
|
|
: 10,
|
|
// barTouchData: BarTouchData(
|
|
// enabled: true,
|
|
// touchCallback: (FlTouchEvent event, barTouchResponse) {
|
|
// if (!event.isInterestedForInteractions || barTouchResponse == null || barTouchResponse.spot == null) {
|
|
// return;
|
|
// }
|
|
// },
|
|
// touchTooltipData: BarTouchTooltipData(
|
|
// getTooltipColor: (group) => Colors.transparent,
|
|
// getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
// if (rod.toY == 0) return null; // Hide for zero values
|
|
// return BarTooltipItem(
|
|
// formatNumber(rod.toY),
|
|
// TextStyle(color: Colors.black),
|
|
// );
|
|
// },
|
|
// ),
|
|
// ),
|
|
|
|
// barTouchData: BarTouchData(enabled: true),
|
|
barTouchData: BarTouchData(
|
|
enabled: false,
|
|
handleBuiltInTouches: false,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
getTooltipColor: (group) => Colors.transparent,
|
|
// fitInsideHorizontally: true,
|
|
// fitInsideVertically: true,
|
|
// tooltipPadding: const EdgeInsets.all(8),
|
|
tooltipMargin: 1,
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
// String unitMsrLabel = yAxisLabels.isNotEmpty && groupIndex < yAxisLabels.length
|
|
// ? yAxisLabels[groupIndex]
|
|
// : '';
|
|
if (rod.toY == 0) return null;
|
|
|
|
return BarTooltipItem(
|
|
(chartConversion != null &&
|
|
chartConversion.isNotEmpty &&
|
|
number_format != null &&
|
|
chartConversion.isNotEmpty)
|
|
? formatNumberConversion(rod.toY, chartConversion,
|
|
number_format) // If condition is true
|
|
: formatNumber(rod.toY),
|
|
const TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
titlesData: FlTitlesData(
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: false,
|
|
interval: (yAxisData.isNotEmpty
|
|
? yAxisData.reduce((a, b) => a > b ? a : b) / 5
|
|
: 1),
|
|
getTitlesWidget: (value, meta) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: Text('${value.toInt()}'),
|
|
);
|
|
},
|
|
reservedSize: 60,
|
|
),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
interval: 1,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value.toInt() < xAxisData.length) {
|
|
String title = xAxisData[value.toInt()];
|
|
String displayTitle = title.length > 10
|
|
? title.substring(0, 10) + '...'
|
|
: title;
|
|
|
|
return Padding(
|
|
padding:
|
|
const EdgeInsets.only(top: 8.0, left: 55.0),
|
|
child: SizedBox(
|
|
width: 60, // Limit width to force wrapping
|
|
child: Transform.rotate(
|
|
// angle: -0.5,
|
|
angle: -1.5,
|
|
|
|
child: TooltipTheme(
|
|
data: TooltipThemeData(
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueGrey[
|
|
800], // Change background color
|
|
borderRadius: BorderRadius.circular(
|
|
8), // Optional: rounded corners
|
|
),
|
|
textStyle: TextStyle(
|
|
color: Colors
|
|
.white), // Change text color
|
|
),
|
|
child: TooltipTheme(
|
|
data: TooltipThemeData(
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueGrey[
|
|
800], // Change background color
|
|
borderRadius: BorderRadius.circular(
|
|
8), // Optional: rounded corners
|
|
),
|
|
textStyle: TextStyle(
|
|
color: Colors
|
|
.white), // Change text color
|
|
),
|
|
child: Tooltip(
|
|
message: title,
|
|
child: Text(
|
|
// title,
|
|
displayTitle,
|
|
softWrap: true,
|
|
textAlign: TextAlign.end,
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)));
|
|
}
|
|
return Container();
|
|
},
|
|
reservedSize: 110,
|
|
),
|
|
),
|
|
topTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false), // Hide top titles
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles:
|
|
SideTitles(showTitles: false), // Hide right titles
|
|
),
|
|
),
|
|
gridData: FlGridData(show: false),
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: const Border(
|
|
// left: BorderSide(color: Colors.grey),
|
|
bottom: BorderSide(color: Colors.grey),
|
|
),
|
|
),
|
|
barGroups: List.generate(
|
|
xAxisData.length,
|
|
(index) => BarChartGroupData(
|
|
x: index,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY: yAxisData[index],
|
|
color: bodyColor,
|
|
borderRadius: BorderRadius.circular(4),
|
|
width: 20,
|
|
),
|
|
],
|
|
showingTooltipIndicators: [0],
|
|
),
|
|
),
|
|
),
|
|
))
|
|
]);
|
|
case 'bar_chart_horizontal':
|
|
// Extract groupBy values and their corresponding y-axis values
|
|
List<String> xAxisData = [];
|
|
List<double> yAxisData = [];
|
|
List<Map<String, dynamic>> sortedData = [];
|
|
|
|
for (var entry in chartData['response']) {
|
|
String? timePeriod = entry['ObsKey']['TIME_PERIOD'];
|
|
if (timePeriod != null) {
|
|
// Check if it contains a month (i.e., has "-" and follows "YYYY-MM" format)
|
|
bool hasMonth = RegExp(r'^\d{4}-\d{2}$').hasMatch(timePeriod);
|
|
|
|
if (hasMonth) {
|
|
print('Yes-TIME_PERIOD: $timePeriod');
|
|
sortedData.add(entry);
|
|
} else {
|
|
print('No-TIME_PERIOD: $timePeriod');
|
|
}
|
|
}
|
|
}
|
|
// Sort the data based on year and month
|
|
sortedData.sort((a, b) {
|
|
String timeA = a['ObsKey']['TIME_PERIOD'];
|
|
String timeB = b['ObsKey']['TIME_PERIOD'];
|
|
|
|
return timeA.compareTo(timeB); // Lexicographical sorting works for YYYY-MM
|
|
});
|
|
|
|
// If no valid TIME_PERIOD with a month is found, return original response
|
|
List<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,
|
|
rotationQuarterTurns: rotationTurns,
|
|
barTouchData: BarTouchData(
|
|
enabled: false,
|
|
handleBuiltInTouches: false,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
fitInsideHorizontally: true,
|
|
fitInsideVertically: true,
|
|
tooltipPadding: const EdgeInsets.all(8),
|
|
tooltipMargin: 16,
|
|
getTooltipColor: (group) => Colors.transparent,
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
if (rod.toY == 0) return null;
|
|
|
|
return BarTooltipItem(
|
|
formatNumber(rod.toY),
|
|
const TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
titlesData: FlTitlesData(
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: false,
|
|
interval: (yAxisData.isNotEmpty
|
|
? yAxisData.reduce((a, b) => a > b ? a : b) / 5
|
|
: 1),
|
|
getTitlesWidget: (value, meta) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: Text('${value.toInt()}'),
|
|
);
|
|
},
|
|
reservedSize: 60,
|
|
),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value.toInt() < xAxisData.length) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Transform.rotate(
|
|
angle: -1.58,
|
|
// angle: -45 *
|
|
// (3.1415927 / 180), // Rotating by -45 degrees
|
|
alignment: Alignment.center,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 100,
|
|
child: Text(
|
|
xAxisData[value.toInt()],
|
|
textAlign: TextAlign.right,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
),
|
|
softWrap: true,
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return Container();
|
|
},
|
|
reservedSize: 125,
|
|
),
|
|
),
|
|
topTitles: AxisTitles(
|
|
sideTitles:
|
|
SideTitles(showTitles: false), // Hide top titles
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles:
|
|
SideTitles(showTitles: false), // Hide right titles
|
|
),
|
|
),
|
|
gridData: FlGridData(show: false),
|
|
// borderData: FlBorderData(show: false),
|
|
|
|
borderData: FlBorderData(
|
|
show: false,
|
|
),
|
|
// groupsSpace: 5,
|
|
barGroups: List.generate(
|
|
xAxisData.length,
|
|
(index) => BarChartGroupData(
|
|
x: index,
|
|
// barsSpace: 2,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY: yAxisData[index],
|
|
// color: Colors.blueAccent,
|
|
color: bodyColor,
|
|
borderRadius: BorderRadius.circular(4),
|
|
width: 20,
|
|
),
|
|
],
|
|
showingTooltipIndicators: [0],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
]);
|
|
case 'fl_multi_bar':
|
|
double _calculateChartWidth(dynamic chartData) {
|
|
int totalBars = chartData['response']?.length ?? 0;
|
|
const double barWidth = 30; // Width for each bar, including spacing
|
|
return totalBars * barWidth; // Calculate total chart width
|
|
}
|
|
|
|
print('multi_bar');
|
|
|
|
String cropKey = chartData['chart_type_json']['x_sub_group'];
|
|
// Group crops by CROP_TYPE
|
|
Map<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
|
|
titlesData: FlTitlesData(
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 80, // Added space for rotated titles
|
|
getTitlesWidget: (value, meta) {
|
|
if (value < groupByValues.length) {
|
|
String title =
|
|
groupByValues.elementAt(value.toInt());
|
|
|
|
String displayTitle = title.length > 10
|
|
? title.substring(0, 10) + '...'
|
|
: title;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8.0,left: 30.0),
|
|
child: SizedBox(
|
|
width: 60, // Limit width to force wrapping
|
|
child: Transform.rotate(
|
|
angle: -1.5,
|
|
child: TooltipTheme(
|
|
data: TooltipThemeData(
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueGrey[
|
|
800], // Change background color
|
|
borderRadius: BorderRadius.circular(
|
|
8), // Optional: rounded corners
|
|
),
|
|
textStyle: TextStyle(
|
|
color: Colors.white), // Change text color
|
|
),
|
|
child: Tooltip(
|
|
message: title,
|
|
child: Text(
|
|
displayTitle,
|
|
softWrap: true,
|
|
maxLines:2 ,
|
|
style: TextStyle(fontSize: 10),
|
|
overflow: TextOverflow.ellipsis,
|
|
)),
|
|
),
|
|
)));
|
|
// return Transform.rotate(
|
|
// angle:
|
|
// -0.5, // Rotation in radians (~ -30 degrees)
|
|
// child: Text(
|
|
// title,
|
|
// style: const TextStyle(fontSize: 12),
|
|
// ),
|
|
// );
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
topTitles:
|
|
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
rightTitles:
|
|
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
barTouchData: BarTouchData(
|
|
touchTooltipData: BarTouchTooltipData(
|
|
tooltipHorizontalAlignment:
|
|
FLHorizontalAlignment.center,
|
|
tooltipRoundedRadius: 8,
|
|
fitInsideHorizontally:
|
|
true, // Ensure it fits within the screen
|
|
fitInsideVertically: true,
|
|
tooltipPadding: EdgeInsets.all(8),
|
|
tooltipMargin: 16,
|
|
// Only show tooltip when touched
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
if (rod.toY == 0 || touchedGroupIndex == -1) {
|
|
return null; // Don't show the tooltip if the value is 0 or there's no touch
|
|
}
|
|
if (groupIndex == touchedGroupIndex) {
|
|
// Get the group label dynamically
|
|
String groupLabel =
|
|
groupByValues.elementAt(groupIndex);
|
|
|
|
// Fetch the crop for the current group from groupedCrops
|
|
String cropType =
|
|
groupByValues.elementAt(groupIndex);
|
|
print('GrpcropType: $cropType');
|
|
// String crop = groupedCrops[cropType]![rodIndex];
|
|
|
|
// Safely fetch crop with null check
|
|
List<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;
|
|
|
|
String formattedValue;
|
|
if (value >= 1000000) {
|
|
formattedValue =
|
|
(value / 1000000).toStringAsFixed(1) + 'M';
|
|
} else if (value >= 1000) {
|
|
formattedValue =
|
|
(value / 1000).toStringAsFixed(1) + 'K';
|
|
} else {
|
|
formattedValue = value.toStringAsFixed(
|
|
0); // for values smaller than 1000
|
|
}
|
|
// print('Cropvaluevalue: $groupLabel\n$crop $value $formattedValue');
|
|
|
|
return BarTooltipItem(
|
|
'$groupLabel\n$crop',
|
|
const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
children: [
|
|
TextSpan(
|
|
// text: ' Value: ${rod.toY}',
|
|
text: ' Value: $formattedValue',
|
|
style: const TextStyle(
|
|
color: Colors.yellow,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
touchCallback: (event, response) {
|
|
if (event.isInterestedForInteractions &&
|
|
response != null &&
|
|
response.spot != null) {
|
|
// setState(() {
|
|
touchedGroupIndex =
|
|
response.spot!.touchedBarGroupIndex;
|
|
// });
|
|
} else {
|
|
// setState(() {
|
|
touchedGroupIndex = -1; // Reset if no interaction
|
|
// });
|
|
}
|
|
},
|
|
),
|
|
gridData: FlGridData(show: false),
|
|
),
|
|
),
|
|
),
|
|
))
|
|
]),
|
|
);
|
|
|
|
case 'horizontal_rotate':
|
|
String cropKey = chartData['chart_type_json']
|
|
['y_sub_group']; // Dynamically get crop key
|
|
// Group crops by CROP_TYPE
|
|
Map<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);
|
|
|
|
return Column(children: [
|
|
Text(
|
|
chartData['chart_heading'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 5),
|
|
Text(
|
|
chartData['chart_sub_heading'] ?? '',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w300,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
SizedBox(height: 5),
|
|
// Chart
|
|
Flexible(
|
|
// child: SingleChildScrollView(
|
|
// scrollDirection: Axis.vertical,
|
|
// child: SingleChildScrollView(
|
|
// scrollDirection: Axis.horizontal,
|
|
|
|
child: SizedBox(
|
|
// width: 500,
|
|
// width: 1000,
|
|
height: chartHeight,
|
|
child: Stack(children: [
|
|
BarChart(
|
|
BarChartData(
|
|
maxY: 400000,
|
|
rotationQuarterTurns: rotationTurns,
|
|
barTouchData: BarTouchData(
|
|
// enabled: true,
|
|
enabled: false,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
getTooltipColor: (group) => Colors.transparent,
|
|
tooltipHorizontalAlignment:
|
|
FLHorizontalAlignment.center,
|
|
tooltipRoundedRadius: 8,
|
|
fitInsideHorizontally:
|
|
true, // Ensure it fits within the screen
|
|
fitInsideVertically: true,
|
|
tooltipPadding: EdgeInsets.all(8),
|
|
tooltipMargin: 16,
|
|
// Only show tooltip when touched
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
// if (rod.toY == 0 || touchedGroupIndex == -1) {
|
|
// return null; // Don't show the tooltip if the value is 0 or there's no touch
|
|
// }
|
|
// Hide tooltips for 0 values
|
|
if (rod.toY == 0)
|
|
return null; // Hide for zero values
|
|
|
|
// if (groupIndex == touchedGroupIndex) {
|
|
// print('Group Index: $groupIndex, Group : $group');
|
|
|
|
// Get the group label dynamically
|
|
String groupLabel =
|
|
groupByValues.elementAt(groupIndex);
|
|
|
|
// Fetch the crop for the current group from groupedCrops
|
|
String cropType =
|
|
groupByValues.elementAt(groupIndex);
|
|
String crop = groupedCrops[cropType]![rodIndex];
|
|
double value = rod.toY;
|
|
|
|
String formattedValue;
|
|
if (value >= 1000000) {
|
|
formattedValue =
|
|
(value / 1000000).toStringAsFixed(1) + 'M';
|
|
} else if (value >= 1000) {
|
|
formattedValue =
|
|
(value / 1000).toStringAsFixed(1) + 'K';
|
|
} else {
|
|
formattedValue = value.toStringAsFixed(
|
|
0); // for values smaller than 1000
|
|
}
|
|
|
|
return BarTooltipItem(
|
|
'$formattedValue',
|
|
const TextStyle(
|
|
color: Colors.black,
|
|
fontWeight: FontWeight.w400,
|
|
fontSize: 12,
|
|
),
|
|
);
|
|
|
|
// return BarTooltipItem(
|
|
// '$groupLabel\n$crop',
|
|
// const TextStyle(
|
|
// color: Colors.white,
|
|
// fontWeight: FontWeight.bold,
|
|
// ),
|
|
// children: [
|
|
// TextSpan(
|
|
// text: ' Value: $formattedValue',
|
|
// style: const TextStyle(
|
|
// color: Colors.yellow,
|
|
// fontWeight: FontWeight.w500,
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// );
|
|
|
|
// }
|
|
// return null;
|
|
},
|
|
),
|
|
// touchCallback: (event, response) {
|
|
// if (event.isInterestedForInteractions &&
|
|
// response != null &&
|
|
// response.spot != null) {
|
|
// // setState(() {
|
|
// touchedGroupIndex =
|
|
// response.spot!.touchedBarGroupIndex;
|
|
// // });
|
|
// } else {
|
|
// // setState(() {
|
|
// touchedGroupIndex = -1; // Reset if no interaction
|
|
// // });
|
|
// }
|
|
// },
|
|
),
|
|
titlesData: FlTitlesData(
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
// showTitles: false,
|
|
reservedSize: 40,
|
|
interval: 100000,
|
|
getTitlesWidget: (value, meta) {
|
|
return SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
value.toInt().toString(),
|
|
textAlign: TextAlign.justify,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 150, // Added space for rotated titles
|
|
getTitlesWidget: (value, meta) {
|
|
if (value < groupByValues.length) {
|
|
String title =
|
|
groupByValues.elementAt(value.toInt());
|
|
return Transform.rotate(
|
|
angle:
|
|
-1.58, // Rotation in radians (~ -30 degrees)
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
title,
|
|
textAlign: TextAlign.end,
|
|
style: const TextStyle(fontSize: 12),
|
|
softWrap: true,
|
|
maxLines: 2,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false)),
|
|
topTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false)),
|
|
),
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: const Border(
|
|
// left: BorderSide(color: Colors.grey),
|
|
bottom: BorderSide(color: Colors.white),
|
|
),
|
|
),
|
|
gridData: FlGridData(
|
|
show: false,
|
|
drawVerticalLine: true,
|
|
verticalInterval: 1,
|
|
horizontalInterval: 100000,
|
|
getDrawingHorizontalLine: (value) {
|
|
return FlLine(
|
|
color: Colors.grey.withOpacity(0.5),
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
getDrawingVerticalLine: (value) {
|
|
return FlLine(
|
|
color: Colors.grey.withOpacity(0.5),
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
),
|
|
barGroups: _buildHorizontalRotateBarGroups(
|
|
chartData, groupByValues),
|
|
alignment: BarChartAlignment.spaceAround,
|
|
),
|
|
),
|
|
])),
|
|
|
|
// ),
|
|
// )
|
|
),
|
|
]);
|
|
|
|
default:
|
|
return Center(child: Text('Unknown chart type'));
|
|
}
|
|
}
|
|
|
|
// Start Of line trend chart
|
|
FlTitlesData titlesData2(Set<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: Colors.blue, // Set color for the ratio line
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
belowBarData: BarAreaData(show: true),
|
|
),
|
|
);
|
|
|
|
return lineBars;
|
|
}
|
|
|
|
/// Function to parse TIME_PERIOD into a EXACT value
|
|
double parseTimePeriod(String timePeriod) {
|
|
// Match formats
|
|
if (RegExp(r'^\d{4}$').hasMatch(timePeriod)) {
|
|
// Format: Year-only (e.g., "2020")
|
|
return double.parse(timePeriod); // Year as a double (2020 → 2020.0)
|
|
} else if (RegExp(r'^\d{4}-\d{2}$').hasMatch(timePeriod)) {
|
|
// Format: Year-Month (e.g., "2020-10")
|
|
return double.parse(
|
|
timePeriod.replaceAll('-', '.')); // Convert "2020-10" → 2020.10
|
|
} else if (RegExp(r'^\d{4}-[A-Za-z]{3}$').hasMatch(timePeriod)) {
|
|
// Format: Year-MonthAbbr (e.g., "2020-Oct")
|
|
Map<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) {
|
|
List<LineChartBarData> lineBars = [];
|
|
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];
|
|
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');
|
|
|
|
// double yValue = double.parse(entry['ObsValue']['Value']);
|
|
double yValue;
|
|
var value = entry['ObsValue']['Value'];
|
|
|
|
if (value is int) {
|
|
yValue = value.toDouble();
|
|
} else if (value is double) {
|
|
yValue = value;
|
|
} else if (value is String) {
|
|
yValue = double.tryParse(value) ?? 0.0; // Handle invalid strings safely
|
|
} else {
|
|
throw Exception("Unexpected value type: ${value.runtimeType}");
|
|
}
|
|
|
|
return FlSpot(xValue, yValue);
|
|
}).toList();
|
|
|
|
print('spots - $spots');
|
|
|
|
// List<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(
|
|
spots: spots,
|
|
isCurved: true,
|
|
color: groupColorMap[group],
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
dotData: FlDotData(show: true),
|
|
belowBarData: BarAreaData(show: false),
|
|
),
|
|
);
|
|
}
|
|
|
|
return lineBars;
|
|
}
|
|
|
|
// Helper functions for chart styles
|
|
LineTouchData lineTouchData1() {
|
|
return LineTouchData(
|
|
touchTooltipData: LineTouchTooltipData(
|
|
// tooltipBgColor: Colors.black.withOpacity(0.7), // Tooltip background
|
|
tooltipRoundedRadius: 8,
|
|
fitInsideHorizontally: true, // Ensure it fits within the screen
|
|
fitInsideVertically: true,
|
|
tooltipPadding: EdgeInsets.all(8),
|
|
tooltipMargin: 16, // Adds margin to prevent clipping
|
|
getTooltipItems: (List<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: ' ${lineBarSpot.y}', // Keep the value white
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
],
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
handleBuiltInTouches: true,
|
|
);
|
|
}
|
|
|
|
FlGridData gridData() {
|
|
return FlGridData(
|
|
show: false,
|
|
drawVerticalLine: true,
|
|
getDrawingHorizontalLine: (value) =>
|
|
FlLine(color: Colors.grey, strokeWidth: 1),
|
|
getDrawingVerticalLine: (value) =>
|
|
FlLine(color: Colors.grey, strokeWidth: 1),
|
|
);
|
|
}
|
|
|
|
FlTitlesData titlesData1(Set<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;
|
|
print('LineTrend2val1 - $value ');
|
|
|
|
if (value % 10 == 0) {
|
|
// Check if the value is in the millions or thousands range
|
|
if (kDebugMode) {
|
|
print('LineTrend2valKmode - $value ');
|
|
}
|
|
if (value >= 1000000000000) {
|
|
formattedValue =
|
|
'${(value / 1000000000000).toStringAsFixed(0)}T';
|
|
} else if (value >= 1000000000) {
|
|
formattedValue = '${(value / 1000000000).toStringAsFixed(0)}B';
|
|
} else if (value >= 1000000) {
|
|
formattedValue = '${(value / 1000000).toStringAsFixed(0)}M';
|
|
} else if (value >= 1000) {
|
|
formattedValue = '${(value / 1000).toStringAsFixed(0)}k';
|
|
} else {
|
|
formattedValue = value.toStringAsFixed(
|
|
0); // No decimals for values less than 1000
|
|
}
|
|
|
|
// Skip rendering if the formatted value is the same as the last one
|
|
if (_lastFormattedValue == formattedValue) {
|
|
return const SizedBox.shrink(); // Empty widget for duplicates
|
|
}
|
|
// Update the last formatted value for the next comparison
|
|
_lastFormattedValue = formattedValue;
|
|
|
|
// print('formattedValueLine - $formattedValue');
|
|
return Text(formattedValue, textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
|
|
));
|
|
}
|
|
return const SizedBox.shrink();
|
|
|
|
// return Text(
|
|
// 'LINETRENTD2',
|
|
// style: TextStyle(color: Colors.black, fontSize: 12),
|
|
// );
|
|
},
|
|
),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
interval: null,
|
|
// interval: 10, // Ensure each year is shown only once
|
|
getTitlesWidget: (value, meta) {
|
|
// print('BtmTiles :-$value');
|
|
// print('BtmTilesmeta :-$meta');
|
|
// if (xValues.contains(value))
|
|
double closestValue = findClosest(value, xValues);
|
|
if ((closestValue - value).abs() < 0.15) {
|
|
// print("Bottomtiles");
|
|
// print(value);
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 1.1),
|
|
child: Transform.rotate(
|
|
angle: 0.0,
|
|
// angle: -0.5, // Slight rotation to improve readability
|
|
child: Text(
|
|
value.toInt().toString(),
|
|
style: TextStyle(color: Colors.black, fontSize: 12),
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return SizedBox.shrink(); // Hide non-relevant labels
|
|
}
|
|
},
|
|
),
|
|
),
|
|
topTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false), // Hide top titles
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false), // Hide right titles
|
|
),
|
|
);
|
|
}
|
|
|
|
FlBorderData borderData() {
|
|
return FlBorderData(
|
|
show: true,
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.black, width: 1), // Show bottom border
|
|
left: BorderSide(color: Colors.black, width: 1), // Show left border
|
|
top: BorderSide.none, // Hide top border
|
|
right: BorderSide.none, // Hide right border
|
|
),
|
|
);
|
|
}
|
|
|
|
// End Of line trend chart
|
|
|
|
// Define the list of unique colors
|
|
final List<Color> uniqueColors = [
|
|
Color(0xFF648CBA),
|
|
Color(0xFF90B0D5),
|
|
Color(0xFF98BCE5),
|
|
Color(0xFFA7B5C5),
|
|
Color(0xFFBED3EC),
|
|
Color(0xFFD4E3F4),
|
|
];
|
|
|
|
Color _getColorForGroup(int index) {
|
|
// Use the index to get a color from the uniqueColors list
|
|
return uniqueColors[index % uniqueColors.length];
|
|
}
|
|
|
|
List<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;
|
|
}
|
|
}
|
|
|
|
return maxY * 1.1; // Add 10% buffer for better visualization
|
|
}
|
|
|
|
List<BarChartGroupData> _buildHorizontalRotateBarGroups(
|
|
dynamic chartData, Set<String> groupByValues) {
|
|
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'][chartData['group_by']] == 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;
|
|
print('multi_bar1.1');
|
|
final List<Color> uniqueColorsForTwo = [
|
|
Color(0xFF648CBA),
|
|
Color(0xFF90B0D5),
|
|
];
|
|
final barColor;
|
|
if (chartData['dataset'] == 'general_education' ||
|
|
chartData['dataset'] == 'higher_education' ||
|
|
chartData['dataset'] == 'air_transport' ||
|
|
chartData['dataset'] == 'labour_force' ||
|
|
chartData['dataset'] == 'gdp' ||
|
|
chartData['dataset'] == 'clinics') {
|
|
barColor = uniqueColorsForTwo[colorIndex % uniqueColorsForTwo.length];
|
|
colorIndex++;
|
|
} else {
|
|
barColor = uniqueColors[colorIndex % uniqueColors.length];
|
|
colorIndex++;
|
|
}
|
|
|
|
return BarChartRodData(
|
|
toY: value, // Use the parsed value
|
|
color: barColor, // Dynamic color
|
|
width: 20,
|
|
|
|
// backDrawRodData: BackgroundBarChartRodData(
|
|
// show: true,
|
|
// toY: 400000,
|
|
// color: Colors.grey.shade300,
|
|
// ),
|
|
);
|
|
}).toList();
|
|
|
|
// Add BarChartGroupData for the group
|
|
barGroups.add(
|
|
BarChartGroupData(
|
|
x: i,
|
|
barRods: barRods,
|
|
showingTooltipIndicators:
|
|
List.generate(barRods.length, (index) => index),
|
|
),
|
|
);
|
|
}
|
|
|
|
return barGroups;
|
|
}
|
|
|
|
/// End horizontal rotate and fl_multi_bar bar
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return buildChart(chartData, context);
|
|
}
|
|
}
|
|
|
|
class BarValuePainter extends CustomPainter {
|
|
BarValuePainter(this.barGroups, this.constraints);
|
|
final List<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;
|
|
}
|