indicators

This commit is contained in:
Surendiran 2026-05-29 17:58:51 +05:30
parent 820d0e4982
commit b927666c12

View File

@ -580,12 +580,21 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
final firstResponse =
responses.isNotEmpty ? responses.first : <String, dynamic>{};
final secondResponse = responses.length > 1 ? responses[1] : null;
final trendText = _textValue(secondResponse?['value']);
return _IndicatorItem(
title: _textValue(entry['chart_heading']),
value: _textValue(firstResponse['value']),
period: _textValue(firstResponse['display_value']),
trendText: _textValue(secondResponse?['value']),
trendText: trendText,
trendPeriod: _textValue(secondResponse?['display_value']),
icon: _indicatorItemIcon(
topicName,
subTopicName,
_textValue(entry['chart_heading']),
_textValue(entry['kpi']),
),
sparklineValues: _sparklineFromResponses(responses),
isTrendUp: _isPositiveTrend(trendText),
);
}).toList();
@ -616,6 +625,104 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
return double.tryParse(cleaned);
}
bool _isPositiveTrend(String trendText) {
if (trendText.isEmpty) return true;
return !trendText.trim().startsWith('-');
}
List<double> _sparklineFromResponses(List<Map<String, dynamic>> responses) {
final values = <double>[];
for (final response in responses) {
final parsed = _parseNumericTrend(_textValue(response['value']));
if (parsed != null) {
values.add(parsed);
}
}
if (values.length < 2) return const [];
final minValue = values.reduce(math.min);
final maxValue = values.reduce(math.max);
if (maxValue == minValue) {
return List<double>.filled(values.length, 0.5);
}
return values
.map((value) => (value - minValue) / (maxValue - minValue))
.toList();
}
IconData _indicatorItemIcon(
String topicName,
String subTopicName,
String heading,
String kpi,
) {
final normalizedHeading = _normalizeKey(heading);
final normalizedKpi = _normalizeKey(kpi);
if (normalizedHeading.contains('male') ||
normalizedKpi.contains('male')) {
return Icons.person_outline;
}
if (normalizedHeading.contains('female') ||
normalizedKpi.contains('female')) {
return Icons.person_outline;
}
if (normalizedHeading.contains('enrollment') ||
normalizedHeading.contains('student') ||
normalizedKpi.contains('student')) {
return Icons.school_outlined;
}
if (normalizedHeading.contains('teacher') ||
normalizedKpi.contains('teacher')) {
return Icons.menu_book_outlined;
}
if (normalizedHeading.contains('hospital') ||
normalizedKpi.contains('hospital')) {
return Icons.local_hospital_outlined;
}
if (normalizedHeading.contains('bed') ||
normalizedKpi.contains('bed')) {
return Icons.hotel_outlined;
}
if (normalizedHeading.contains('health') ||
normalizedKpi.contains('health')) {
return Icons.medical_services_outlined;
}
if (normalizedHeading.contains('government')) {
return Icons.account_balance_outlined;
}
if (normalizedHeading.contains('private')) {
return Icons.business_outlined;
}
if (normalizedHeading.contains('growth') ||
normalizedHeading.contains('rate')) {
return Icons.trending_up_rounded;
}
if (normalizedHeading.contains('gdp')) {
return Icons.show_chart_rounded;
}
if (normalizedHeading.contains('import') ||
normalizedHeading.contains('export') ||
normalizedHeading.contains('trade')) {
return Icons.swap_horiz_rounded;
}
if (normalizedHeading.contains('temperature') ||
normalizedHeading.contains('climate')) {
return Icons.thermostat_outlined;
}
if (normalizedHeading.contains('electric') ||
normalizedHeading.contains('consumption') ||
normalizedHeading.contains('generation')) {
return Icons.bolt_outlined;
}
if (normalizedHeading.contains('crop') ||
normalizedHeading.contains('land')) {
return Icons.grass_outlined;
}
return _sectionTileIcon(topicName, subTopicName);
}
List<_HomeSectionGroup> _buildDynamicTopicGroups(
Map<String, dynamic>? topic) {
final topicName = _textValue(topic?['main_topic']);
@ -1132,6 +1239,9 @@ class _IndicatorItem {
required this.period,
this.trendText = '',
this.trendPeriod = '',
this.icon = Icons.insights_outlined,
this.sparklineValues = const [],
this.isTrendUp = true,
});
final String title;
@ -1139,6 +1249,9 @@ class _IndicatorItem {
final String period;
final String trendText;
final String trendPeriod;
final IconData icon;
final List<double> sparklineValues;
final bool isTrendUp;
}
// Hero card
@ -1210,8 +1323,8 @@ class _HeroCard extends ConsumerWidget {
Flexible(
child: Text(
isArabic
? 'تحديث · ١٥ مايو ٢٠٢٦'
: 'Updated · 15 May 2026',
? 'تحديث · ٠١ مارس ٢٠٢٦'
: 'Updated · 01 Mar 2026',
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 10,
@ -2264,7 +2377,11 @@ class _IndicatorsSheet extends StatelessWidget {
const Divider(height: 1, indent: 20, color: _kPearl),
itemBuilder: (_, index) {
final indicator = indicators[index];
return _IndicatorRow(item: indicator, accentColor: iconColor);
return _IndicatorRow(
item: indicator,
accentColor: iconColor,
accentBg: iconBg,
);
},
),
),
@ -2276,17 +2393,36 @@ class _IndicatorsSheet extends StatelessWidget {
}
class _IndicatorRow extends StatelessWidget {
const _IndicatorRow({required this.item, required this.accentColor});
const _IndicatorRow({
required this.item,
required this.accentColor,
required this.accentBg,
});
final _IndicatorItem item;
final Color accentColor;
final Color accentBg;
@override
Widget build(BuildContext context) {
final up = item.isTrendUp;
final prevPeriod = item.period.isNotEmpty ? item.period : '';
final vsLabel = item.trendPeriod.isNotEmpty ? item.trendPeriod : prevPeriod;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: accentBg,
borderRadius: BorderRadius.circular(8),
),
child: Icon(item.icon, size: 18, color: accentColor),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -2299,13 +2435,13 @@ class _IndicatorRow extends StatelessWidget {
color: _kSlate900,
),
),
const SizedBox(height: 4),
Row(
children: [
if (item.period.isNotEmpty)
Text(
item.period,
style: const TextStyle(fontSize: 11, color: _kSlate400),
style:
const TextStyle(fontSize: 11, color: _kSlate400),
),
if (item.trendText.isNotEmpty) ...[
const SizedBox(width: 8),
@ -2313,19 +2449,44 @@ class _IndicatorRow extends StatelessWidget {
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: AppColors.successBg,
color: up ? AppColors.successBg : AppColors.errorBg,
borderRadius: BorderRadius.circular(999),
),
child: Text(
item.trendText +
(item.trendPeriod.isNotEmpty
? ' vs ${item.trendPeriod}'
: ''),
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppColors.success,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
up
? Icons.arrow_upward_rounded
: Icons.arrow_downward_rounded,
size: 9,
color:
up ? AppColors.success : AppColors.error,
),
const SizedBox(width: 2),
Text(
item.trendText.contains('%')
? item.trendText
: '${item.trendText}%',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: up
? AppColors.success
: AppColors.error,
),
),
if (vsLabel.isNotEmpty) ...[
const SizedBox(width: 3),
Text(
'vs $vsLabel',
style: const TextStyle(
fontSize: 9,
color: _kSlate400,
),
),
],
],
),
),
],
@ -2334,18 +2495,34 @@ class _IndicatorRow extends StatelessWidget {
],
),
),
const SizedBox(width: 12),
Text(
item.value.isEmpty ? '' : item.value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: accentColor,
fontFeatures: const [FontFeature.tabularFigures()],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.value.isEmpty ? '' : item.value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: _kSlate900,
fontFeatures: [FontFeature.tabularFigures()],
),
),
SizedBox(
width: 60,
height: 18,
child: CustomPaint(
painter: _SparklinePainter(
points: item.sparklineValues,
isUp: up,
),
),
),
],
),
const SizedBox(width: 8),
const Icon(Icons.arrow_forward_ios_rounded, size: 13, color: _kSlate400),
const Icon(Icons.arrow_forward_ios_rounded,
size: 13, color: _kSlate400),
],
),
);