uaestats_fe/lib/presentation/components/charts/horizontal_bar_chart.dart
2026-05-16 11:31:32 +05:30

478 lines
14 KiB
Dart
Executable File

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:uae_stat/domain/use_cases/language.dart';
import 'package:uae_stat/infrastructure/services/packages/list.dart';
import 'package:uae_stat/presentation/components/space.dart';
class HorizontalBarChart extends ConsumerWidget {
const HorizontalBarChart(
this.data, {
super.key,
required this.color,
this.shortlistTopN,
});
final Map<
String,
({
num value,
String? barEndText,
})> data;
final MaterialColor color;
final int? shortlistTopN;
List<Color> get _colorList => [
color.shade50,
color.shade300,
color.shade600,
color.shade900,
];
void _sortData() {
// Convert the Map to a List of Map entries
final entriesList = data.entries.toList();
// Sort the list by value in descending order
entriesList.sort(
(a, b) => b.value.value.compareTo(a.value.value),
);
// repopulate data
data.removeWhere((key, value) => true);
data.addEntries(entriesList);
}
void _shortListData() {
// Take the first 5 entries
final top5Entries = Map.fromEntries(
data.entries.take(shortlistTopN!),
);
data.removeWhere(
(key, value) => !top5Entries.keys.contains(key),
);
}
@override
Widget build(BuildContext context, ref) {
_sortData();
if (shortlistTopN != null) _shortListData();
final titles = Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: data.keys
.map<Widget>(
(e) => Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 4),
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Text(
e,
textAlign: TextAlign.end,
style: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 9,
color: Colors.black,
),
),
),
),
),
)
.toList()
..intersperseDivider(
4.verticalSpace,
),
);
final minVal = data.values.fold<num>(
double.maxFinite,
(prev, e) => e.value < prev ? e.value : prev,
);
final isAnyBarNegative = minVal < 0;
final maxVal = data.values.fold<num>(
double.negativeInfinity,
(prev, e) => e.value.abs() > prev ? e.value.abs() : prev,
);
final totalWidth = maxVal;
final bars = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(
data.length,
(i) {
final e = data.entries.elementAt(i);
if (!isAnyBarNegative) {
final bar = Container(
decoration: BoxDecoration(
borderRadius: const BorderRadiusDirectional.horizontal(
end: Radius.circular(10),
),
color: _colorList[i % _colorList.length],
),
child: FractionallySizedBox(
widthFactor: e.value.value / totalWidth,
alignment: AlignmentDirectional.centerStart,
child: double.infinity.verticalSpace,
),
);
final barSegment = Expanded(
child: Row(
children: [
Flexible(
child: bar,
),
if (e.value.barEndText != null) ...[
4.horizontalSpace,
SizedBox(
width: 40,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
e.value.barEndText!,
style: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 11,
color: Colors.black,
),
),
),
),
],
],
),
);
return barSegment;
}
final isBarNegative = e.value.value < 0;
final bar = DecoratedBox(
decoration: BoxDecoration(
color: _colorList[i % _colorList.length],
borderRadius: BorderRadiusDirectional.horizontal(
end: isBarNegative ? Radius.zero : const Radius.circular(10),
start: isBarNegative ? const Radius.circular(10) : Radius.zero,
),
),
child: FractionallySizedBox(
widthFactor: e.value.value.abs() / totalWidth,
alignment: AlignmentDirectional.centerStart,
child: double.infinity.verticalSpace,
),
);
final barSegment = isBarNegative
? Expanded(
child: Row(
children: [
Expanded(
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: bar,
),
),
Expanded(
child: e.value.barEndText == null
? const SizedBox.shrink()
: Row(
children: [
4.horizontalSpace,
Text(
e.value.barEndText!,
style: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 11,
color: Colors.black,
),
),
],
),
),
],
),
)
: Expanded(
child: Row(
children: [
Expanded(
child: e.value.barEndText == null
? const SizedBox.shrink()
: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
e.value.barEndText!,
textAlign: TextAlign.end,
style: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 11,
color: Colors.black,
),
),
4.horizontalSpace,
],
),
),
Expanded(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: bar,
),
),
],
),
);
return barSegment;
},
)..intersperseDivider(
8.verticalSpace,
),
);
return Row(
children: [
Expanded(
child: titles,
),
if (isAnyBarNegative) const VerticalDivider(),
Expanded(
flex: 3,
child: bars,
),
],
);
}
}
class HorizontalGroupedBarChart extends ConsumerWidget {
const HorizontalGroupedBarChart(
this.data, {
super.key,
this.shortlistTopN,
});
final Map<
String,
Map<
String,
({
num value,
String? barEndText,
Color barColor,
})>> data;
final int? shortlistTopN;
void _sortData() {
// Convert the Map to a List of Map entries
final entriesList = data.entries.toList();
// Sort the list by value in descending order
entriesList.sort(
(a, b) {
final bVal = b.value.values.fold<num>(
0,
(prev, e) => e.value > prev ? e.value : prev,
);
final aVal = a.value.values.fold<num>(
0,
(prev, e) => e.value > prev ? e.value : prev,
);
return bVal.compareTo(aVal);
},
);
// repopulate data
data.removeWhere((key, value) => true);
data.addEntries(entriesList);
}
void _shortListData() {
// Take the first 5 entries
final top5Entries = Map.fromEntries(
data.entries.take(shortlistTopN!),
);
data.removeWhere(
(key, value) => !top5Entries.keys.contains(key),
);
}
num minVal() => data.values.fold<num>(
double.infinity,
(prev, e) {
final localMin = e.values.map((r) => r.value).deriveMinimum()!;
return localMin < prev ? localMin : prev;
},
);
num maxVal() => data.values.fold<num>(
double.negativeInfinity,
(prev, e) {
final localMax = e.values.map((r) => r.value).deriveMaximum()!;
return localMax > prev ? localMax : prev;
},
);
@override
Widget build(BuildContext context, ref) {
_sortData();
if (shortlistTopN != null) _shortListData();
final legend = Column(
children: data.values.first.entries
.map<Widget>(
(key) => Row(
children: [
Container(
decoration: BoxDecoration(
color: key.value.barColor,
borderRadius: BorderRadius.circular(2),
),
height: 8,
width: 8,
),
3.horizontalSpace,
Text(
key.key,
style: const TextStyle(
fontSize: 11,
),
),
],
),
)
.toList()
..intersperseDivider(2.verticalSpace),
);
final titles = Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: data.keys
.map<Widget>(
(e) => Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 4),
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Text(
e,
textAlign: TextAlign.end,
style: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 9,
color: Colors.black,
),
),
),
),
),
)
.toList()
..intersperseDivider(
4.verticalSpace,
),
);
final minimum = minVal();
final isAnyBarNegative = minimum < 0;
final totalWidth = maxVal();
final graph = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: data.entries.map<Widget>(
(groupData) {
final stackedBarGroup = Column(
children: groupData.value.values.map<Widget>(
(barData) {
final barOnly = Container(
decoration: BoxDecoration(
color: barData.barColor,
borderRadius: const BorderRadiusDirectional.horizontal(
end: Radius.circular(10),
),
),
child: FractionallySizedBox(
widthFactor: barData.value / totalWidth,
alignment: AlignmentDirectional.centerStart,
child: double.infinity.verticalSpace,
),
);
final barWithText = Row(
children: [
Flexible(
child: barOnly,
),
if (barData.barEndText != null) ...[
4.horizontalSpace,
SizedBox(
width: 40,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
barData.barEndText!,
style: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 11,
color: Colors.black,
),
),
),
),
],
],
);
return Expanded(child: barWithText);
},
).toList()
..intersperseDivider(4.verticalSpace),
);
final group = Expanded(
child: stackedBarGroup,
);
return group;
},
).toList()
..intersperseDivider(
8.verticalSpace,
),
);
return Column(
children: [
Row(
children: [
const Expanded(
child: SizedBox.shrink(),
),
Expanded(
flex: 3,
child: legend,
),
],
),
6.verticalSpace,
Expanded(
child: Row(
children: [
Expanded(
child: titles,
),
if (isAnyBarNegative) const VerticalDivider(),
Expanded(
flex: 3,
child: graph,
),
],
),
),
],
);
}
}