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

268 lines
8.1 KiB
Dart
Executable File

import 'package:fl_chart/fl_chart.dart';
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/material_color.dart';
import 'package:uae_stat/infrastructure/services/packages/num_abbreviation.dart';
import 'package:uae_stat/presentation/components/space.dart';
/// individual pie charts per category
class ThemedPieChartPlural extends ConsumerWidget {
/// must provide either [materialColor] or [perSegmentColor]
const ThemedPieChartPlural({
super.key,
required this.data,
this.materialColor,
this.perSegmentColor,
this.badgePositionPercentageOffset = 2,
});
final Map<String, double> data;
/// key should match data
final Map<String, Color>? perSegmentColor;
final double badgePositionPercentageOffset;
/// generates segment colors from [MaterialColor]
/// do not provide if [perSegmentColor] is also provided
final MaterialColor? materialColor;
@override
Widget build(BuildContext context, ref) {
assert(!(perSegmentColor != null && materialColor != null));
final totalVal = data.values.fold(
0.0,
(prev, e) => prev + e,
);
final pieCharts = List.generate(
data.length,
(index) {
final e = data.entries.elementAt(index);
final fraction = e.value / totalVal;
final pieChartSectionData = [
PieChartSectionData(
radius: 11,
value: 1 - fraction,
color: Colors.grey.shade200,
showTitle: false,
),
PieChartSectionData(
radius: 11,
value: fraction,
color: _segmentColor(index),
showTitle: false,
),
];
return Column(
children: [
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
e.key,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 11,
),
),
),
),
4.verticalSpace,
Expanded(
flex: 4,
child: Stack(
alignment: Alignment.center,
children: [
Text(
'${(fraction * 100).round()}%',
),
PieChart(
PieChartData(
sections: pieChartSectionData,
),
),
],
),
),
4.verticalSpace,
Expanded(
child: Text(
e.value.toStringWithCommas(
abbreviateIfAbove10powN: 5,
),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 11,
),
),
),
],
);
},
);
// final pieChart = PieChart(
// PieChartData(
// sections: List.generate(
// data.length,
// (index) {
// final e = data[index];
// return PieChartSectionData(
// radius: 22,
// badgeWidget: ConstrainedBox(
// constraints: const BoxConstraints(maxWidth: 100),
// child: Text(
// '${e.key}\n${e.value.toStringWithCommas()}',
// textAlign: TextAlign.center,
// style: TextStyle(
// fontFamily: context.translate(
// 'Roboto',
// 'NotoKufi',
// ),
// fontSize: 10,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// badgePositionPercentageOffset: badgePositionPercentageOffset,
// title: '${((e.value / totalVal) * 100).toStringAsFixed(1)}%',
// titlePositionPercentageOffset: -0.6,
// titleStyle: TextStyle(
// fontFamily: context.translate(
// 'Roboto',
// 'NotoKufi',
// ),
// fontSize: 10,
// fontWeight: FontWeight.w600,
// ),
// showTitle: true,
// color: _segmentColor(index),
// value: e.value,
// );
// },
// ),
// ),
// );
return Wrap(
spacing: 10,
runSpacing: 10,
alignment: WrapAlignment.center,
children: pieCharts
.map(
(e) => SizedBox.square(
dimension: 130,
child: e,
),
)
.toList(),
);
}
Color _segmentColor(int index) {
if (materialColor == null) {
final key = data.keys.elementAt(index);
return perSegmentColor![key]!;
}
final colorIndex = index % materialColor!.colorListButFewer.length;
return materialColor!.colorListButFewer[colorIndex];
}
}
class ThemedPieChartSingular extends ConsumerWidget {
/// must provide either [materialColor] or [perSegmentColor]
const ThemedPieChartSingular({
super.key,
required this.data,
this.materialColor,
this.perSegmentColor,
this.badgePositionPercentageOffset = 2,
});
final Map<String, double> data;
/// key should match data
final Map<String, Color>? perSegmentColor;
final double badgePositionPercentageOffset;
/// generates segment colors from [MaterialColor]
/// do not provide if [perSegmentColor] is also provided
final MaterialColor? materialColor;
@override
Widget build(BuildContext context, ref) {
assert(!(perSegmentColor != null && materialColor != null));
final totalVal = data.values.fold(
0.0,
(prev, e) => prev + e,
);
final dataWithoutLessThan1PctSectors = data.entries
.where(
(emirateConsumption) =>
emirateConsumption.value.asPctOf(totalVal) > 1,
)
.toList();
dataWithoutLessThan1PctSectors.insert(
1,
dataWithoutLessThan1PctSectors
.removeAt(dataWithoutLessThan1PctSectors.length - 1),
);
final pieChart = PieChart(
PieChartData(
sections: List.generate(
dataWithoutLessThan1PctSectors.length,
(index) {
final e = dataWithoutLessThan1PctSectors[index];
return PieChartSectionData(
radius: 22,
badgeWidget: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 100),
child: Text(
'${e.key}\n${e.value.toStringWithCommas(
abbreviateIfAbove10powN: 5,
)}',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
badgePositionPercentageOffset: badgePositionPercentageOffset,
title: '${((e.value / totalVal) * 100).toStringAsFixed(1)}%',
titlePositionPercentageOffset: -0.6,
titleStyle: TextStyle(
fontFamily: context.translate(
'Roboto',
'NotoKufi',
),
fontSize: 10,
fontWeight: FontWeight.w600,
),
showTitle: true,
color: _segmentColor(index),
value: e.value,
);
},
),
),
);
return Padding(
padding: const EdgeInsets.all(20),
child: pieChart,
);
}
Color _segmentColor(int index) {
if (materialColor == null) {
final key = data.keys.elementAt(index);
return perSegmentColor![key]!;
}
final colorIndex = index % materialColor!.colorListButFewer.length;
return materialColor!.colorListButFewer[colorIndex];
}
}